GoRunner docs
Search or askSearch⌘K

Sign-in for developers

How an AI app signs in: standard OAuth 2.1, found by discovery, with PKCE and dynamic registration.

Cloud, Self-hosted and Embed

A compliant MCP client needs nothing special. This page is for writing one, or finding why one fails.

The flow

  1. 1Calls the MCP server with no token, and gets 401 with where to lookPOST /mcp/build (AI app to GoRunner)
  2. 2Reads the server’s metadata, then the sign-in server’sGET /.well-known/oauth-protected-resource/mcp/build (AI app to GoRunner)
  3. 3Registers itself: a public client, no secretPOST /oauth/register (AI app to GoRunner)
  4. 4Opens the sign-in page with a PKCE challengeGET /oauth/authorize (AI app to Browser)
  5. 5The person signs in and presses Allow access (Browser to GoRunner)
  6. 6Sends the browser back to the app’s callbackcode, state, iss (GoRunner to Browser)
  7. 7Trades the code and its verifier for tokensPOST /oauth/token (AI app to GoRunner)
  8. 8Calls the MCP server with the access tokenAuthorization: Bearer gra_… (AI app to GoRunner)
  1. AI app → GoRunner Calls the MCP server with no token, and gets 401 with where to lookPOST /mcp/build
  2. AI app → GoRunner Reads the server’s metadata, then the sign-in server’sGET /.well-known/oauth-protected-resource/mcp/build
  3. AI app → GoRunner Registers itself: a public client, no secretPOST /oauth/register
  4. AI app → Browser Opens the sign-in page with a PKCE challengeGET /oauth/authorize
  5. Browser → GoRunner The person signs in and presses Allow access
  6. GoRunner → Browser Sends the browser back to the app’s callbackcode, state, iss
  7. AI app → GoRunner Trades the code and its verifier for tokensPOST /oauth/token
  8. AI app → GoRunner Calls the MCP server with the access tokenAuthorization: Bearer gra_…

Discovery

A call without a token answers 401. Its header points at the metadata, and names the scopes the server’s tools use:

http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="GoRunner",
  resource_metadata="…/.well-known/oauth-protected-resource/mcp/build",
  scope="workflow:read connection:read run:read workflow:write …"
Protected resource (RFC 9728)
/.well-known/oauth-protected-resource, then the server’s path: /mcp/build, /mcp/tools or a workspace’s own address
Authorization server (RFC 8414)
/.well-known/oauth-authorization-server

For /mcp/build, scope lists the six permissions of Draft, run and publish. For /mcp/tools, it lists workflow:read, run:read and workflow:run. Both documents are at your installation’s address, https://api.gorunner.io on Cloud, which is also the issuer.

Registration

Registration is open and makes a public client, with no secret.

bash
curl -s -X POST "https://api.gorunner.io/oauth/register" \
  -H "Content-Type: application/json" \
  -d '{"client_name": "My agent",
       "redirect_uris": ["http://127.0.0.1/callback"]}'
Output
{
  "client_id": "grc_69866495cb3c0a69d24c77a5aeb3068620b922cd",
  "client_id_issued_at": 1761412345,
  "client_name": "My agent",
  "redirect_uris": ["http://127.0.0.1/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}
Callbacks
1 to 10. One that isn’t allowed is dropped, and the answer lists the ones kept. If none is left, registration fails.
Same app again
The same name, callbacks, logo and site get the same client back: one made in the last 12 hours, or one that already has a connection.
Limit
20 new registrations a day from one address that never lead to a connection.
Logo and site
A logo_uri or client_uri you send comes back in the answer, if it is a web address.

Callbacks

Since v2026.10.22

https
Any host.
http
Only localhost, 127.0.0.1 or [::1]. The port may change between sign-ins; the path may not.
An app’s own
Cursor’s cursor://anysphere.cursor-mcp/oauth/callback, and Raycast’s raycast://mcp-oauth-callback or com.raycast:/oauth. Any other app uses a reverse-DNS form, such as com.example.app:/callback. A self-hosted installation allows more exact app callbacks with OAUTH_EXTRA_REDIRECT_URIS: whole addresses, no query.
Never
A query on an app’s callback, a fragment, or a user name in the address.

Authorize and token

PKCE
S256 only. plain is refused.
Scopes
Any of the ten permissions, and offline_access. None asked for means read only. openid, profile and email are ignored. The person’s choice and role narrow them.
resource
/mcp/build, /mcp/tools, or an address under one. Left out, the token works on both. Anything else is invalid_target.
iss
On every redirect back, success or error. Since v2026.10.22.
The token request
application/x-www-form-urlencoded, with grant_type=authorization_code, code, code_verifier and client_id. A redirect_uri, if sent, must match the authorize request.

Tokens

CredentialLastsNotes
Authorization code60 secondsOnce.
Access token gra_…1 hourWorks only on the MCP server it was made for. The API refuses it with 401.
Refresh token grr_…60 daysA new one with every use. The old one works 30 seconds more; using it after that ends the whole connection.
The connection180 daysThen the person signs in again. Disconnecting ends it at once.

A registered client always gets a refresh token, with or without offline_access. Since v2026.10.22.

To disconnect, post token as application/x-www-form-urlencoded to /oauth/revoke. Either token ends the whole connection. The answer is 200 with no body, whatever you send, so it can't be used to test a token.

When sign-in is refused

These answers are OAuth's own shape, {"error": "…", "error_description": "…"}, with error a string. The last two rows are in the API's shape instead.

WhereStatuserrorWhy
/oauth/token400invalid_requestNot form-encoded, or grant_type, code, code_verifier, client_id or refresh_token is missing.
/oauth/token400unsupported_grant_typeAnything but authorization_code and refresh_token.
/oauth/token400invalid_grantThe code is unknown, expired or used; it was issued to another client; its redirect_uri or code_verifier doesn’t match. Or the refresh token is unknown or expired, or was used after its 30 seconds, or the person disconnected the app.
/oauth/register400invalid_redirect_uri, invalid_client_metadataNo callback that can be kept, more than 10, a client secret asked for, another grant, a body that isn’t JSON, or one over 16 KB.
/oauth/register429invalid_requestOver the limit of 20 unused registrations a day.
All three500server_errorNothing was issued. Try again.
Discovery404invalid_requestNo such protected resource.
/oauth/token, /oauth/register, /oauth/revoke429temporarily_unavailableOver the limit below, with Retry-After.
/oauth/token, /oauth/register, /oauth/revoke405invalid_requestA wrong method. Allow says POST.
/oauth/authorize, discovery429, 405An object: {"error": {"code": "RATE_LIMITED", "message": "…"}}Over the limit below, with Retry-After, or a wrong method.
All of them503An object: {"error": {"code": "BUSY", "message": "…"}}The server holds as much request data as it can. Retry after Retry-After.

A refresh token that answers invalid_grant will never work again. Stop retrying it, and start the sign-in from the beginning.

At /oauth/authorize, a bad client_id or redirect_uri shows the person a page with 400, and nothing goes back to the app. After those two are known good, an error goes back to the callback as error, error_description, state and iss: unsupported_response_type, invalid_request, invalid_target, invalid_scope, server_error, or access_denied when the person declines. To an app’s own callback (cursor://…), it is a page with a button instead of an automatic redirect, and only for the exact callbacks listed above.

Can a client running in a browser sign in?

Yes. Discovery, registration, token and revoke answer Access-Control-Allow-Origin: *, never with credentials. The sign-in page itself is a normal page the browser opens. A self-hosted installation turns this off with OAUTH_PUBLIC_CORS=off.

The MCP servers themselves don’t allow other origins: a page can sign in, but its calls to /mcp/build have to go through a server of its own.

How does it work in a product that embeds GoRunner?

The same way, at the account’s own address: /mcp/embed/<account>/build or /tools. The person signs in through the product, not GoRunner. See Let customers sign in AI apps.

Addresses

/mcp/build
The build server: Build tools
/mcp/tools
Your published workflows as tools
/mcp/build/w/<workspace ID>
One workspace’s own address. API tokens use it.
/mcp/tools/w/<workspace ID>
One workspace’s tools. A credential for another workspace gets 403.
/oauth/register, /oauth/authorize, /oauth/token, /oauth/revoke
The sign-in server

One address may make 120 sign-in requests a minute, discovery included. A self-hosted installation changes it with OAUTH_RATE_LIMIT.

How the servers answer

Sessions
None. Each request stands alone and no Mcp-Session-Id is sent, so every request needs its token.
POST
A request answers 200 with one JSON object, never a stream. A notification answers 202 with no body. Accept must allow both application/json and text/event-stream, or it is 400. Content-Type must be application/json, or it is 415.
GET, DELETE
405 with a working token, because there is no stream to open and no session to end. 401 without one, like every method.
401
No token, or an expired or revoked one. It carries the WWW-Authenticate header above, with error="invalid_token" when a token was sent. Refresh, or sign in again.
MCP-Protocol-Version
A version the server doesn’t know is 400. Left out, the call still works.
A method the server doesn’t have
A JSON-RPC error, -32601, on 200. A tool that fails is isError: true in a 200.

A 401 on GET is normal for a client without a working token. If they repeat with no sign-in after them, that client has lost its connection and needs someone to connect it again.