Sign-in for developers
How an AI app signs in: standard OAuth 2.1, found by discovery, with PKCE and dynamic registration.
A compliant MCP client needs nothing special. This page is for writing one, or finding why one fails.
The flow
- 1Calls the MCP server with no token, and gets 401 with where to lookPOST /mcp/build (AI app to GoRunner)
- 2Reads the server’s metadata, then the sign-in server’sGET /.well-known/oauth-protected-resource/mcp/build (AI app to GoRunner)
- 3Registers itself: a public client, no secretPOST /oauth/register (AI app to GoRunner)
- 4Opens the sign-in page with a PKCE challengeGET /oauth/authorize (AI app to Browser)
- 5The person signs in and presses Allow access (Browser to GoRunner)
- 6Sends the browser back to the app’s callbackcode, state, iss (GoRunner to Browser)
- 7Trades the code and its verifier for tokensPOST /oauth/token (AI app to GoRunner)
- 8Calls the MCP server with the access tokenAuthorization: Bearer gra_… (AI app to GoRunner)
- AI app → GoRunner Calls the MCP server with no token, and gets 401 with where to lookPOST /mcp/build
- AI app → GoRunner Reads the server’s metadata, then the sign-in server’sGET /.well-known/oauth-protected-resource/mcp/build
- AI app → GoRunner Registers itself: a public client, no secretPOST /oauth/register
- AI app → Browser Opens the sign-in page with a PKCE challengeGET /oauth/authorize
- Browser → GoRunner The person signs in and presses Allow access
- GoRunner → Browser Sends the browser back to the app’s callbackcode, state, iss
- AI app → GoRunner Trades the code and its verifier for tokensPOST /oauth/token
- 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/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/toolsor 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.
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"]}'{
"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_uriorclient_uriyou send comes back in the answer, if it is a web address.
Callbacks
- https
- Any host.
- http
- Only
localhost,127.0.0.1or[::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’sraycast://mcp-oauth-callbackorcom.raycast:/oauth. Any other app uses a reverse-DNS form, such ascom.example.app:/callback. A self-hosted installation allows more exact app callbacks withOAUTH_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.
plainis refused. - Scopes
- Any of the ten permissions, and
offline_access. None asked for means read only.openid,profileandemailare 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 isinvalid_target.iss- On every redirect back, success or error. Since v2026.10.22.
- The token request
application/x-www-form-urlencoded, withgrant_type=authorization_code,code,code_verifierandclient_id. Aredirect_uri, if sent, must match the authorize request.
Tokens
| Credential | Lasts | Notes |
|---|---|---|
| Authorization code | 60 seconds | Once. |
Access token gra_… | 1 hour | Works only on the MCP server it was made for. The API refuses it with 401. |
Refresh token grr_… | 60 days | A new one with every use. The old one works 30 seconds more; using it after that ends the whole connection. |
| The connection | 180 days | Then 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.
| Where | Status | error | Why |
|---|---|---|---|
/oauth/token | 400 | invalid_request | Not form-encoded, or grant_type, code, code_verifier, client_id or refresh_token is missing. |
/oauth/token | 400 | unsupported_ | Anything but authorization_ and refresh_token. |
/oauth/token | 400 | invalid_grant | The 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/register | 400 | invalid_, invalid_ | No 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/register | 429 | invalid_request | Over the limit of 20 unused registrations a day. |
| All three | 500 | server_error | Nothing was issued. Try again. |
| Discovery | 404 | invalid_request | No such protected resource. |
/oauth/token, /oauth/register, /oauth/revoke | 429 | temporarily_ | Over the limit below, with Retry-After. |
/oauth/token, /oauth/register, /oauth/revoke | 405 | invalid_request | A wrong method. Allow says POST. |
/oauth/authorize, discovery | 429, 405 | An object: {"error": {"code": "RATE_ | Over the limit below, with Retry-After, or a wrong method. |
| All of them | 503 | An 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-Idis sent, so every request needs its token. POST- A request answers
200with one JSON object, never a stream. A notification answers202with no body.Acceptmust allow bothapplication/jsonandtext/event-stream, or it is400.Content-Typemust beapplication/json, or it is415. GET,DELETE405with a working token, because there is no stream to open and no session to end.401without one, like every method.401- No token, or an expired or revoked one. It carries the
WWW-Authenticateheader above, witherror="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, on200. A tool that fails isisError: truein a200.
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.