MCP authorization
Model Context Protocol servers are resource servers, and MCP clients are OAuth clients. oauth.work issues real OAuth 2.1 access tokens for them — scoped, consented, audience-bound, and discoverable — so an agent connects to a tool the way the spec intends, not through a shared API key.
The pieces
Section titled “The pieces”| Role | What it is |
|---|---|
| Authorization server | The tenant issuer, e.g. https://acme.oauth.work |
| Resource server | Your MCP server, identified by its URL |
| Client | The agent, registered dynamically, by URL, or as an agent |
Discovery
Section titled “Discovery”An MCP server publishes protected-resource metadata (RFC 9728) naming its authorization server:
// https://tools.acme.com/.well-known/oauth-protected-resource/mcp{ "resource": "https://tools.acme.com/mcp", "authorization_servers": ["https://acme.oauth.work"], "scopes_supported": ["mcp:tools", "tool:search", "tool:weather"], "bearer_methods_supported": ["header"], "dpop_signing_alg_values_supported": ["ES256", "EdDSA", "RS256"]}The client fetches that, then fetches the authorization server’s own metadata at
https://acme.oauth.work/.well-known/oauth-authorization-server to find the endpoints.
A 401 from the resource server also carries a WWW-Authenticate challenge pointing at the same
metadata, so a client that started without discovery can find its way from the first rejection.
The authorization server metadata also names a Markdown recipe an agent can read to work out the rest of this page for itself — see agent discovery.
Register the client
Section titled “Register the client”An MCP client that has never seen this authorization server before can register itself — no credential, no operator involved:
POST /registerContent-Type: application/json
{ "client_name": "Research Agent", "redirect_uris": ["http://127.0.0.1:8976/callback"], "grant_types": ["authorization_code", "refresh_token"], "token_endpoint_auth_method": "none"}Or skip registration entirely by using an https URL as the client_id — see
URL client IDs.
Both paths produce a public client limited to first-party scopes. To grant an agent specific tool scopes, provision it through the management API instead.
Authorize the agent
Section titled “Authorize the agent”Authorization-code + PKCE, requesting only the tools it needs, bound to the resource it will call:
GET /authorize ?response_type=code &client_id=<client id> &redirect_uri=http://127.0.0.1:8976/callback &resource=https://tools.acme.com/mcp &scope=tool:search%20tool:weather &code_challenge=<S256> &code_challenge_method=S256 &state=…The resource parameter is what binds the token’s audience. Without it the token has no aud, and
a well-built MCP server will refuse it.
Consent is explicit: the user sees which tools the agent is asking for and approves them. To make
that request more precise than a scope list — specific tools, at a specific server, with a call
budget — add authorization_details, and push the whole request
through PAR so it cannot be tampered with in the browser.
Exchange and call
Section titled “Exchange and call”POST /tokenContent-Type: application/x-www-form-urlencodedDPoP: <proof>
grant_type=authorization_code&code=…&client_id=…&code_verifier=…&redirect_uri=…With a DPoP proof the token comes back token_type: "DPoP" and is bound to the agent’s key. Then
call the tool:
POST https://tools.acme.com/mcp/tools/searchAuthorization: DPoP eyJ…DPoP: <fresh proof for this method and URL>Content-Type: application/json
{ "query": "quarterly revenue" }What a resource server must check
Section titled “What a resource server must check”If you are writing the MCP server, verify all four. Skipping any one of them undoes the others:
- Signature — against the JWKS resolved from the token’s own
iss. - Audience —
audmust include your resource id. Otherwise a token minted for another service works here too. - Scope — the tool’s required scope must be present.
- DPoP — when the token carries
cnf.jkt, require a fresh proof matching the method and URL.
The demo server also honours authorization_details: when the grant carries mcp_tool details
scoped to this resource, they further restrict which tools are callable, beyond the flat scope.
Failures should use the standard shapes, so clients can react:
HTTP/1.1 403 ForbiddenWWW-Authenticate: Bearer resource_metadata="…", scope="tool:search", error="insufficient_scope"
{ "error": "insufficient_scope", "required_scope": "tool:search" }The demo server
Section titled “The demo server”The platform hosts a reference MCP resource server at /mcp — for example
https://acme.oauth.work/mcp — with mock search, weather, and email.send tools, each gated by
its own scope. Use it to exercise discovery, consent, DPoP, audience binding, and scoped invocation
end to end before pointing agents at your own servers.
GET /mcp # server description and endpointsGET /mcp/tools # tool list with required scopesPOST /mcp/tools/search # invoke (token required)Its resource id is <issuer>/mcp, and tool calls require an audience-bound token — a broadly
scoped token minted without a resource indicator is refused, deliberately, so that the demo
teaches the right shape.
Every invocation writes an mcp_tool_called audit event naming the actor and the
tool.
Multi-agent chains
Section titled “Multi-agent chains”When one agent calls another, the second should act on behalf of the first without inheriting more authority. See A2A delegation.