MCP authorization
How the Model Context Protocol 2026-07-28 authorization model (OAuth 2.1 resource servers, scope challenges, RFC 8707, MRTR, CIMD, RFC 9207, Enterprise-Managed Authorization) maps onto permdock/mcp.
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). - Scope challenges name the operation. A runtime
insufficient_scopechallenge SHOULD carryscopewith every scope the current operation needs, in one challenge, plusresource_metadata. Scope accumulation (SEP-2350) is the client's job: on step-up it requests the union of its earlier scopes and the challenged ones. - Audience binding. Servers MUST validate that a token was issued for them as the RFC 8707 audience.
- Multi-round-trip requests (MRTR). Server-to-client requests (
elicitation/create,sampling/createMessage,roots/list) now travel inside aninput_requiredresult ontools/call,prompts/getorresources/read, with an opaquerequestStatethe client echoes on retry. The server MUST treatrequestStateas attacker-controlled and protect it when it influences authorization; a client MAY retry at once when the result has noinputRequests, and a server MUST NOT send an input request the client did not declare. - 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, withresource_metadata, before the handler runs. The challenge names every scope the operation needs, which is the tool's one permission scope; the client adds the scopes it already holds.resource. With the option set, a token whoseauthInfo.resourceis absent or names another server is refused withinvalid_tokenand lists no tools.- 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 from the subject's snapshot: a tool is listed when the delegation can cover its permission, some grant allows it and no deny blocks every row, so a model never sees tools it cannot use (the same idea ascapabilityMiddlewarein ai-sdk). Listing is a hint, never a decision; the call itself is decided in full with the real instance.- Approvals. With
approval.atset and a client that declares URL elicitation,approval-requiredis an MRTRinput_requiredresult: a URL-modeelicitation/createrequest pointing at the approval page and the approval token asrequestState. The retry carries the state back and the approval is re-checked againstDecision.token(see approvals). Otherwise it is a structured tool result carryingtoken. A barerequestStateis never sent, because the client may retry it at once. - Step-up. An
insufficient-user-authenticationdenial carriesacr_valuesandmax_agefrom the failing assurance grants; withstepUp.atset it is a URL-mode request to that page with both appended. The adapter does not use the Tasks extension: a long-running approval is durable in theApprovalStore, not in an MCP task. - 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. The adapter parses no token itself:authorization_detailsreachdelegationonly when the token layer puts them onauthInfo.extra.authorizationDetails(or the raw claim nameauthorization_details);createPermDockandsubjectFromMcpread the same two keys. 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, naming the operation's scopes and resource_metadata |
| SEP-2350 scope accumulation | Client-side; the challenge names the operation's scopes, and simulate() can pre-compute the full set for a plan |
| RFC 8707 audience validation | resource option; a mismatch is invalid_token |
ctx.http.authInfo.scopes | delegation.scopes; intersected with principal grants |
authInfo.clientId | actor.id with actor.kind: 'mcp-client' (or actorKind) |
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 |
MRTR input_required | approval-required and step-up as URL-mode requests when the client declares URL elicitation; requestState is the approval token (or sealed by createRequestStateCodec) |
| Tasks extension | Not used; long-running approvals live in the ApprovalStore |
| 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 |
Specification check
Checked on 2026-09-30 against the published 2026-07-28 text and @modelcontextprotocol/server 2.2.0:
- Authorization, Scope Challenge Handling: the challenge carries the scopes the operation needs and
resource_metadata; accumulation is client-side. PermDock previously listed held plus missing scopes and now lists only the needed ones. - Multi Round-Trip Requests:
input_requiredontools/call,prompts/getandresources/read; URL-mode elicitation has noelicitationIdon this revision. - SDK 2.2.0 supports it: handlers may return
InputRequiredResult(built withinputRequired()or as a literal), readctx.mcpReq.requestState()andctx.mcpReq.inputResponseson retry, and seal state withcreateRequestStateCodecwired intoServerOptions.requestState.verify. The client drives rounds automatically and gives up after its round limit. A 2025-11-25 session gets the same result through the SDK's legacy shim.
Sources
- MCP specification 2026-07-28, Authorization.
- MCP 2026-07-28 release post: stateless core, CIMD over DCR, RFC 9207, SEP-2350.
- MCP specification 2026-07-28, Multi Round-Trip Requests.
- Enterprise-Managed Authorization extension.
- MCP specification index and llms.txt.
- Landscape research, MCP section: SDK v2
requireBearerAuth,AuthInfo, hand-written per-tool checks.
Last updated on
FAPI 2.0 Security Profile
What the FAPI 2.0 Security Profile requires of a resource server and how permdock/jwt with profile: 'fapi2' and the OpenAPI emitter enforce those requirements before a permission check runs.
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).