MCP authorization
How the Model Context Protocol 2026-07-28 authorization model (OAuth 2.1 resource servers, scope challenges, CIMD, RFC 9207, SEP-2350, Enterprise-Managed Authorization) maps onto permdock/mcp.
Status: planned
Phase: 2
Adapter phases: permdock/mcp 2.
What it is
The MCP specification 2026-07-28 defines how MCP servers authorize callers:
- Servers are OAuth 2.1 resource servers. Clients implement Protected Resource Metadata (RFC 9728) and resource indicators (RFC 8707); servers validate bearer tokens and should return
WWW-Authenticatewithscopehints. Aninsufficient_scopeerror enables step-up: the client goes back to the authorization server for more scope and retries. - TypeScript SDK v2 (
@modelcontextprotocol/server2.0.0, published 2026-07-28) exposesregisterTool(..., { scopeChallenge })andrequireScopes(), which produce403 insufficient_scopestep-up challenges, andctx.http.authInfo.scopesfor handler-level checks that returnisError: true. The SDK gives serversAuthInfo(scopes,clientId,expiresAt); per-tool checks beyond scopes are left to the server. - Stateless core. The 2026-07-28 release made the core stateless; elicitation and Tasks now work through multi-round-trip requests (MRTR) rather than server-held session state.
- Client ID Metadata Documents (CIMD) replace Dynamic Client Registration, which is deprecated. A client identifies itself with a URL that hosts its metadata instead of registering per server.
- RFC 9207
issvalidation is required, so a client checks that the authorization response came from the issuer it expected (mix-up protection). - SEP-2350 clarifies scope accumulation during step-up: a step-up request asks for the union of already-held scopes and the newly required ones, so a client does not lose earlier grants.
- Enterprise-Managed Authorization (EMA) is a stabilised extension: the client exchanges an SSO identity assertion for an ID-JAG (identity assertion JWT authorization grant) via RFC 8693 token exchange and redeems it with an RFC 7523 JWT-bearer grant at the MCP server's authorization server. Support is discovered through
authorization_grant_profiles_supportedin authorization server metadata.
Why it matters for PermDock
Scopes are coarse: post:delete says an agent may delete posts, not which posts. The spec explicitly leaves per-tool and per-resource authorization to the server, and in the official SDK that is a hand-written if inside each tool handler. PermDock's job is to make the fine-grained half declarative while staying inside the protocol's coarse-grained half: a permission reference carries a scope for the OAuth layer and a policy with conditions for the tool layer, and the same Decision drives both the WWW-Authenticate challenge and the tool's refusal. See the mcp adapter.
How PermDock uses it
import { createPermDock } from 'permdock/mcp'
const { protectServer } = createPermDock(policy, {
subject: (authInfo) => userFrom(authInfo), // principal from the validated token
// actor = authInfo.clientId, delegation = authInfo.scopes and authorization_details, filled automatically
})
const guarded = protectServer(server)
guarded.registerTool(
'delete_post',
{ permission: permissions.post.delete, inputSchema, data: (args) => loadPost(args.id) },
handler,
)What protectServer does with each MCP concept:
scopeChallenge.permission.scope(post:delete) becomes the tool'sscopeChallenge. A caller whose token lacks it receives the SDK's403 insufficient_scopestep-up challenge before the handler runs. Because SEP-2350 says the client accumulates scopes, PermDock lists only the missing scope in the challenge.- Handler-level check. When the scope is present,
protectServerbuilds a request-scopedPermDockfromauthInfo, loads the instance withdata(args), validatesargsagainst the resource schema (validate: 'boundary'), and callsdecide(permission, instance).deniedreturnsisError: truewith theDecisionreasons andalternativesasstructuredContent, so the model can self-correct instead of retrying. list_toolsfiltering. The tool list is filtered per caller usingcanon the collection or a representative check, so a model never sees tools it cannot use (the same idea ascapabilityMiddlewarein ai-sdk).- Elicitation for approvals.
approval-requiredis surfaced as an elicitation request. Because the core is stateless, the approval is carried through the multi-round-trip request and re-checked againstDecision.tokenon resume (see approvals). - EMA and CIMD. PermDock does not implement the token exchange; it consumes the resulting
authInfo. An ID-JAG-derived token still yieldsscopesand aclientId, soactoranddelegationare filled the same way. A CIMD client id is a URL and is recorded verbatim asactor.idfor audit. - RFC 9207. Issuer validation is a client concern and is out of scope for the server adapter; the docs for the MCP example client note it.
- Two-principal subject.
principalcomes from the token's user,actorfrom the client,delegationfrom the scopes and anyauthorization_details. The decision is the principal's grants intersected with the delegation, so an agent can never exceed its user (see delegation).
Mapping table
| MCP 2026-07-28 concept | PermDock feature |
|---|---|
| Server as OAuth 2.1 resource server | subject: (authInfo) => ... builds the principal from the validated token |
Bearer verification on a Fetch host (mcp-handler withMcpAuth, verifyToken) | Produces the AuthInfo PermDock consumes; the recipe fills it from permdock/jwt so scopes and clientId arrive verified (MCP adapter, Hosting) |
RFC 9728 Protected Resource Metadata (protectedResourceHandler) | Not written by PermDock; its scopes_supported should list every permission scope the server exposes, which permdock collect emits |
registerTool(..., { scopeChallenge }) | permission option on guarded.registerTool; permission.scope fills scopeChallenge |
requireScopes() | Applied automatically for tools with a permission |
403 insufficient_scope step-up | Emitted when permission.scope is missing from authInfo.scopes |
| SEP-2350 scope accumulation | Challenge lists only missing scopes; simulate() can pre-compute the full set for a plan |
ctx.http.authInfo.scopes | delegation.scopes; intersected with principal grants |
authInfo.clientId | actor.id with actor.kind: 'mcp' |
Handler returning isError: true | Decision denied, with reasons and alternatives in structuredContent |
Tool inputSchema | Cross-checked against the resource's Standard JSON Schema; args validated at the boundary |
| Stateless elicitation (MRTR) | approval-required outcome; approval re-checked against Decision.token |
| Tasks extension | Long-running approvals; open question below |
| CIMD client identity | Recorded as actor.id; no registration step in PermDock |
RFC 9207 iss validation | Client-side; documented in the example client, not enforced by the adapter |
| EMA (ID-JAG via RFC 8693, redeemed with RFC 7523) | Transparent: resulting authInfo is consumed like any other token |
authorization_grant_profiles_supported | Not read by PermDock; belongs to the client's discovery step |
Sources
- MCP specification 2026-07-28, Authorization.
- MCP 2026-07-28 release post: stateless core, CIMD over DCR, RFC 9207, SEP-2350.
- Enterprise-Managed Authorization extension.
- MCP specification index and llms.txt.
- Landscape research, MCP section: SDK v2
requireBearerAuth,AuthInfo, hand-written per-tool checks.
Open questions
- Whether
approval-requiredfor long-running tools should use elicitation (interactive) or the Tasks extension (durable), or expose both through an adapter option. - How much of
authorization_detailsthe adapter should parse fromauthInfowhen the token is an ID-JAG-derived access token rather than a plain scope token. - Whether
list_toolsfiltering should evaluate a collection permission per tool or run the instance check with no data (which PermDock treats as unknown, not granted).
GNAP (RFC 9635)
How the Grant Negotiation and Authorization Protocol's access rights array maps onto PermDock permissions and delegation, why interaction and continuation look like approval-required, and why PermDock only tracks GNAP for now.
OpenID AuthZEN
How PermDock speaks the OpenID AuthZEN Authorization API 1.0 as a policy decision point (permdock/authzen) and as a policy enforcement point (the pdp provider).