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 withpmk_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):
HTTP/1.1 401 UnauthorizedWWW-Authenticate: Bearer resource_metadata="https://mcp.pmkin.io/.well-known/oauth-protected-resource/mcp"
Protected resource metadata#
/.well-known/oauth-protected-resource/mcpAlso served at /.well-known/oauth-protected-resource. For the older per-project URL it’s /.well-known/oauth-protected-resource/p/<projectId>.
{"authorization_servers": ["https://mcp.pmkin.io"],"bearer_methods_supported": ["header"],"resource": "https://mcp.pmkin.io/mcp","resource_name": "PMKIN"}
Authorization server metadata#
/.well-known/oauth-authorization-serverThe issuer’s metadata (RFC 8414), with every endpoint below.
{"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#
/oauth/registerredirect_urisstring[]required- Where to send the user back.
httpsURLs,httponlocalhost,127.0.0.1or[::1], or a native app’s own scheme likecursor://…. No fragments or user info, at most 10. Schemes likejavascript,dataandfileare refused. client_namestring- The name users see on the consent page and in the activity log. Cut to 100 characters. Defaults to “MCP client”.
token_endpoint_auth_methodstring- Must be
noneif sent. grant_typesstring[]- A subset of
authorization_codeandrefresh_token. response_typesstring[]- Must be
codeif sent.
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"}'
{"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#
/oauth/authorizeresponse_typestringrequired- Always
code. client_idstringrequired- From registration.
redirect_uristringrequired- One of the registered URIs, exactly. Loopback URIs may use another port.
code_challengestringrequired- The PKCE challenge: the SHA-256 of the verifier, base64url.
code_challenge_methodstringrequired- Always
S256. resourcestringrequired- The MCP URL the token is for:
https://mcp.pmkin.io/mcp(all the user’s projects) orhttps://mcp.pmkin.io/p/<projectId>. statestring- Returned unchanged with the code. Use it against CSRF.
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.
http://127.0.0.1:33418/callback?code=...&state=af0ifjsldkj&iss=https%3A%2F%2Fmcp.pmkin.io
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#
/oauth/tokenForm-encoded. Send the same redirect_uri and resource as in the authorize request, and the PKCE code_verifier (43 to 128 characters).
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
{"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#
/oauth/tokenBefore 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.
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
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#
/oauth/revokeDisconnects 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.
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.
HTTP/1.1 400 Bad RequestCache-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.
HTTP/1.1 401 UnauthorizedWWW-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"