My MCP server had read tools anyone could call, but the write tools — recording that a story was reproduced — had to act as a specific, signed-in person. So it needed auth. I did not want to bolt on Auth0; I wanted the MCP client (Claude, ChatGPT) to run the standard OAuth flow against my own server.
It turns out an MCP server that requires auth just has to be a small OAuth 2.1 authorization server: publish two discovery documents, support dynamic client registration (the client registers itself), require PKCE, and serve an authorization-code flow with a consent screen. I reused the site's existing session for consent, so a user who is already signed in just clicks Allow.
The wall I hit: my token endpoint kept returning unsupported_grant_type, even though the client was clearly sending grant_type=authorization_code. The body was there — but empty when I parsed it. The cause: on AWS API Gateway (HTTP API), a form-urlencoded body arrives base64-encoded, with isBase64Encoded: true, and I was parsing the still-encoded string. Decoding it first fixed it instantly.
After that the whole chain worked: the client registers, the user consents, the code exchanges for a bearer token, and every tool call acts as that person. No identity provider, no passwords — just the spec.
Reproduce this
Prerequisites: an MCP server you host (Streamable HTTP), on a platform where you control the HTTP responses. This story's server runs on AWS Lambda behind API Gateway HTTP API.
Your agent will need from you: your server's base URL, and how your existing users sign in (so the consent page can reuse that session).
Steps:
- Return 401 with a
WWW-Authenticateheader from the MCP endpoint when there is no valid token, so the client can discover the authorization server. - Serve two discovery documents as JSON:
/.well-known/oauth-protected-resourceand/.well-known/oauth-authorization-server. - Implement Dynamic Client Registration (
POST /register) — the client registers itself and receives a client_id. - Implement the authorization-code flow with PKCE (S256 required) plus an interactive consent page that binds client_id, redirect_uri, code_challenge and the signed-in user to a one-time code.
- Implement the token endpoint: exchange the code plus verifier for a bearer access token. On API Gateway HTTP API, decode the base64 form body first (check
isBase64Encoded) before parsinggrant_type,codeandcode_verifier. - On every MCP call, resolve the bearer token to the user and act as that person.
Verify: run the chain end to end — register a client, complete consent, exchange the code for a token, then call a tool with that token and confirm it acts as the signed-in user. A wrong or absent token must return 401 with the discovery pointer.
Reference implementation, open source: https://github.com/worklore/worklore-mcp Protocol spec: https://modelcontextprotocol.io