MCP server

OAuth

How MCP clients sign in: OAuth 2.1 with PKCE and dynamic registration.

PMKIN is its own OAuth 2.1 authorization server for the MCP server, following the MCP authorization spec. Clients that implement it, like Claude, ChatGPT and Cursor, sign in without any setup. This page is for people building a client: it walks through the flow endpoint by endpoint.

Don’t want OAuth? Send a management token as Authorization: Bearer pmk_mgmt_… to https://api.pmkin.io/mcp instead. See Connect with a token.

At a glance#

  • Public clients only, with PKCE (S256). There are no client secrets.
  • Dynamic client registration (RFC 7591). No pre-registration.
  • Resource indicators (RFC 8707): every token is bound to the MCP URL it was issued for.
  • Access tokens start with pmk_oat_ and last 1 hour. Refresh tokens start with pmk_ort_, last 30 days and rotate on every use.
  • There are no scopes. A grant can do what the user can do in their projects through the MCP tools.

1. Discover the server#

Call the MCP server without a token. The 401 points at the protected resource metadata (RFC 9728):

POST https://mcp.pmkin.io/mcp
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.pmkin.io/.well-known/oauth-protected-resource/mcp"

Protected resource metadata#

GET/.well-known/oauth-protected-resource/mcp

Also served at /.well-known/oauth-protected-resource. For the older per-project URL it’s /.well-known/oauth-protected-resource/p/<projectId>.

Response
{
"authorization_servers": ["https://mcp.pmkin.io"],
"bearer_methods_supported": ["header"],
"resource": "https://mcp.pmkin.io/mcp",
"resource_name": "PMKIN"
}

Authorization server metadata#

GET/.well-known/oauth-authorization-server

The issuer’s metadata (RFC 8414), with every endpoint below.

Response
{
"authorization_endpoint": "https://mcp.pmkin.io/oauth/authorize",
"authorization_response_iss_parameter_supported": true,
"code_challenge_methods_supported": ["S256"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"issuer": "https://mcp.pmkin.io",
"registration_endpoint": "https://mcp.pmkin.io/oauth/register",
"response_types_supported": ["code"],
"revocation_endpoint": "https://mcp.pmkin.io/oauth/revoke",
"revocation_endpoint_auth_methods_supported": ["none"],
"service_documentation": "https://pmkin.io",
"token_endpoint": "https://mcp.pmkin.io/oauth/token",
"token_endpoint_auth_methods_supported": ["none"]
}

2. Register the client#

Register a client#

POST/oauth/register
Body (JSON)
redirect_urisstring[]required
client_namestring
token_endpoint_auth_methodstring
grant_typesstring[]
response_typesstring[]
Request
curl https://mcp.pmkin.io/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My Agent",
"redirect_uris": ["http://127.0.0.1:33418/callback"],
"token_endpoint_auth_method": "none"
}'
201 Created
{
"client_id": "c9a3f0d2...",
"client_id_issued_at": 1791100800,
"client_name": "My Agent",
"grant_types": ["authorization_code", "refresh_token"],
"redirect_uris": ["http://127.0.0.1:33418/callback"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}

Register once and keep the client_id. A client nobody approves within a day is deleted. At most 10 registrations a minute come from one address (429 with Retry-After: 60 past that). Invalid metadata answers 400 with invalid_client_metadata or invalid_redirect_uri.

3. Send the user to sign in#

Authorize#

GET/oauth/authorize
Query parameters
response_typestringrequired
client_idstringrequired
redirect_uristringrequired
code_challengestringrequired
code_challenge_methodstringrequired
resourcestringrequired
statestring
Open in the browser
https://mcp.pmkin.io/oauth/authorize
?response_type=code
&client_id=c9a3f0d2...
&redirect_uri=http%3A%2F%2F127.0.0.1%3A33418%2Fcallback
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&state=af0ifjsldkj
&resource=https%3A%2F%2Fmcp.pmkin.io%2Fmcp

The user signs in to PMKIN and sees a consent page naming your client and where it will send them back. When they approve, PMKIN redirects to your redirect_uri with a single-use code that expires in 10 minutes, the state and the issuer (iss, RFC 9207). If they deny, you get error=access_denied instead.

Redirect after approval
http://127.0.0.1:33418/callback
?code=...
&state=af0ifjsldkj
&iss=https%3A%2F%2Fmcp.pmkin.io
Errors stay in PMKIN

An invalid request never reaches your redirect URI. The user sees the problem on PMKIN’s consent page, for example unknown_client, invalid_redirect_uri, invalid_pkce or invalid_resource.

4. Get tokens#

Exchange the code#

POST/oauth/token

Form-encoded. Send the same redirect_uri and resource as in the authorize request, and the PKCE code_verifier (43 to 128 characters).

Request
curl https://mcp.pmkin.io/oauth/token \
-d grant_type=authorization_code \
-d code=... \
-d client_id=c9a3f0d2... \
-d redirect_uri=http://127.0.0.1:33418/callback \
-d code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk \
-d resource=https://mcp.pmkin.io/mcp
200 OK
{
"access_token": "pmk_oat_...",
"expires_in": 3600,
"refresh_token": "pmk_ort_...",
"token_type": "Bearer"
}

Send the access token as Authorization: Bearer pmk_oat_… on every MCP request. A user has at most 50 connected apps; reconnecting the same app doesn’t count again.

Refresh#

POST/oauth/token

Before the access token expires, or after a 401, trade the refresh token for a new pair. The old refresh token stops working: store the new one.

Request
curl https://mcp.pmkin.io/oauth/token \
-d grant_type=refresh_token \
-d refresh_token=pmk_ort_... \
-d client_id=c9a3f0d2... \
-d resource=https://mcp.pmkin.io/mcp
Replays disconnect the app

Using a refresh token that was already rotated, or an authorization code twice, looks like a stolen token. PMKIN revokes the grant and the user has to connect again. Make sure two processes never refresh with the same token.

Revoke#

POST/oauth/revoke

Disconnects the app (RFC 7009). Send either token; both stop working. Always answers 200, except 400 without a token. Users can also disconnect apps on the project’s MCP page.

Request
curl https://mcp.pmkin.io/oauth/revoke \
-d token=pmk_ort_... \
-d client_id=c9a3f0d2...

Errors#

The token endpoint answers RFC 6749 errors with Cache-Control: no-store: 400 for invalid_request, invalid_grant and unsupported_grant_type, and 401 for invalid_client. The description says what to do.

A token error
HTTP/1.1 400 Bad Request
Cache-Control: no-store
{
"error": "invalid_grant",
"error_description": "The authorization code is invalid, expired or already used. Start the sign-in again."
}

The MCP server answers a missing, expired or revoked access token, or one issued for another URL, with 401 and error="invalid_token". Refresh, or start the sign-in again if refreshing fails. A user who left the project’s team loses access right away.

An invalid token
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="This app was disconnected from PMKIN. Connect it again to continue.", resource_metadata="https://mcp.pmkin.io/.well-known/oauth-protected-resource/mcp"