PermDock
Standards

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-Authenticate with scope hints. An insufficient_scope error enables step-up: the client goes back to the authorization server for more scope and retries.
  • TypeScript SDK v2 (@modelcontextprotocol/server 2.0.0, published 2026-07-28) exposes registerTool(..., { scopeChallenge }) and requireScopes(), which produce 403 insufficient_scope step-up challenges, and ctx.http.authInfo.scopes for handler-level checks that return isError: true. The SDK gives servers AuthInfo (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 iss validation 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_supported in 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's scopeChallenge. A caller whose token lacks it receives the SDK's 403 insufficient_scope step-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, protectServer builds a request-scoped PermDock from authInfo, loads the instance with data(args), validates args against the resource schema (validate: 'boundary'), and calls decide(permission, instance). denied returns isError: true with the Decision reasons and alternatives as structuredContent, so the model can self-correct instead of retrying.
  • list_tools filtering. The tool list is filtered per caller using can on the collection or a representative check, so a model never sees tools it cannot use (the same idea as capabilityMiddleware in ai-sdk).
  • Elicitation for approvals. approval-required is surfaced as an elicitation request. Because the core is stateless, the approval is carried through the multi-round-trip request and re-checked against Decision.token on resume (see approvals).
  • EMA and CIMD. PermDock does not implement the token exchange; it consumes the resulting authInfo. An ID-JAG-derived token still yields scopes and a clientId, so actor and delegation are filled the same way. A CIMD client id is a URL and is recorded verbatim as actor.id for 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. principal comes from the token's user, actor from the client, delegation from the scopes and any authorization_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 conceptPermDock feature
Server as OAuth 2.1 resource serversubject: (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-upEmitted when permission.scope is missing from authInfo.scopes
SEP-2350 scope accumulationChallenge lists only missing scopes; simulate() can pre-compute the full set for a plan
ctx.http.authInfo.scopesdelegation.scopes; intersected with principal grants
authInfo.clientIdactor.id with actor.kind: 'mcp'
Handler returning isError: trueDecision denied, with reasons and alternatives in structuredContent
Tool inputSchemaCross-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 extensionLong-running approvals; open question below
CIMD client identityRecorded as actor.id; no registration step in PermDock
RFC 9207 iss validationClient-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_supportedNot read by PermDock; belongs to the client's discovery step

Sources

Open questions

  • Whether approval-required for long-running tools should use elicitation (interactive) or the Tasks extension (durable), or expose both through an adapter option.
  • How much of authorization_details the adapter should parse from authInfo when the token is an ID-JAG-derived access token rather than a plain scope token.
  • Whether list_tools filtering should evaluate a collection permission per tool or run the instance check with no data (which PermDock treats as unknown, not granted).

On this page