Skip to content

MCP OAuth token endpoint proxy (authorization_code + refresh_token)

POST
/oauth/token
curl --request POST \
--url https://example.com/api/oauth/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data grant_type=refresh_token \
--data refresh_token=example \
--data client_id=example \
--data resource=https://example.com

Forwards authorization_code and refresh_token grants to Supabase /auth/v1/oauth/token. Discovered as token_endpoint from /.well-known/oauth-authorization-server for MCP clients.

Accepts both application/x-www-form-urlencoded (OAuth 2.1 standard) and application/json request bodies.

RFC 8707 (Resource Indicators): clients MAY include an optional resource parameter. The canonical value is the production MCP endpoint (https://api.apphandoff.com/mcp). During the compatibility transition, the API base URL and legacy /api/mcp-bot endpoint remain accepted as exact aliases; all other values return invalid_target (HTTP 400). The value is forwarded upstream so RFC 8707-aware issuers can populate the JWT aud claim.

One of:
object
grant_type
required
string
Allowed values: refresh_token
refresh_token
required
string
client_id
required

Supabase OAuth client UUID or an HTTPS Client ID Metadata Document URL.

string
resource

RFC 8707 resource indicator. Optional; the canonical value is the /mcp endpoint. The API base URL and legacy /api/mcp-bot endpoint remain accepted as exact transition aliases. Every other value returns invalid_target.

string format: uri
<= 512 characters

Token payload from Supabase

Media type application/json
object

Invalid request. Local validation failures use the Error envelope (INVALID_BODY — schema validation failure; INVALID_RESOURCEresource does not match the canonical API URL). Failures from the grant exchange itself use OAuthTokenError; in particular a dead or unknown refresh token returns invalid_grant, which means re-authorize, not retry.

Media type application/json
One of:

RFC 6749 §5.2 token-endpoint error. error is ALWAYS a registered code, never prose: clients switch on it, and invalid_grant in particular is the signal to discard the grant and re-run the authorization flow rather than retry. Upstream Supabase GoTrue does not emit this shape, so the translation happens here (lib/oauth-token-error.ts); the raw upstream code is preserved in error_code and the prose in error_description.

object
error
required
string
Allowed values: invalid_request invalid_client invalid_grant unauthorized_client unsupported_grant_type invalid_scope invalid_target server_error
error_description
required

Human-readable detail. Never load-bearing for client control flow.

string
error_code

Upstream GoTrue error code, verbatim (e.g. refresh_token_not_found).

string
code
required

Legacy AppHandoff envelope key, retained for existing consumers.

string
Allowed value: TOKEN_ERROR

Rate limited

Media type application/json
object
error
required

Human-readable error message

string
code

Stable machine-readable error code for client branching

string
fieldErrors

First validation message per field path

object
key
additional properties
string
issues

Structured validation issues (Zod)

Array<object>
object
path
required
string
message
required
string
retryAfter

Seconds until rate limit resets (429 responses)

number
retryAfter

Seconds until the rate limit resets

number
Retry-After
string

Server error

Media type application/json
object
error
required

Human-readable error message

string
code

Stable machine-readable error code for client branching

string
fieldErrors

First validation message per field path

object
key
additional properties
string
issues

Structured validation issues (Zod)

Array<object>
object
path
required
string
message
required
string
retryAfter

Seconds until rate limit resets (429 responses)

number

Upstream Supabase token exchange error, including a network failure reaching it. Reported as server_error — retryable, unlike a token fault.

Media type application/json

RFC 6749 §5.2 token-endpoint error. error is ALWAYS a registered code, never prose: clients switch on it, and invalid_grant in particular is the signal to discard the grant and re-run the authorization flow rather than retry. Upstream Supabase GoTrue does not emit this shape, so the translation happens here (lib/oauth-token-error.ts); the raw upstream code is preserved in error_code and the prose in error_description.

object
error
required
string
Allowed values: invalid_request invalid_client invalid_grant unauthorized_client unsupported_grant_type invalid_scope invalid_target server_error
error_description
required

Human-readable detail. Never load-bearing for client control flow.

string
error_code

Upstream GoTrue error code, verbatim (e.g. refresh_token_not_found).

string
code
required

Legacy AppHandoff envelope key, retained for existing consumers.

string
Allowed value: TOKEN_ERROR

CIMD mapping lookup failed (temporarily_unavailable).

Media type application/json
object
error
required
string
Allowed values: temporarily_unavailable
error_description
string