Skip to content

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.

RoleWhat it is
Authorization serverThe tenant issuer, e.g. https://acme.oauth.work
Resource serverYour MCP server, identified by its URL
ClientThe agent, registered dynamically, by URL, or as an agent

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.

An MCP client that has never seen this authorization server before can register itself — no credential, no operator involved:

POST /register
Content-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.

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.

POST /token
Content-Type: application/x-www-form-urlencoded
DPoP: <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/search
Authorization: DPoP eyJ…
DPoP: <fresh proof for this method and URL>
Content-Type: application/json
{ "query": "quarterly revenue" }

If you are writing the MCP server, verify all four. Skipping any one of them undoes the others:

  1. Signature — against the JWKS resolved from the token’s own iss.
  2. Audienceaud must include your resource id. Otherwise a token minted for another service works here too.
  3. Scope — the tool’s required scope must be present.
  4. 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 Forbidden
WWW-Authenticate: Bearer resource_metadata="…", scope="tool:search", error="insufficient_scope"
{ "error": "insufficient_scope", "required_scope": "tool:search" }

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 endpoints
GET /mcp/tools # tool list with required scopes
POST /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.

When one agent calls another, the second should act on behalf of the first without inheriting more authority. See A2A delegation.