feat(server): OAuth scope challenge support (step-up auth)#1624
feat(server): OAuth scope challenge support (step-up auth)#1624SamMorrowDrums wants to merge 3 commits into
Conversation
🦋 Changeset detectedLatest commit: e52a2cc The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@modelcontextprotocol/client
@modelcontextprotocol/server
@modelcontextprotocol/express
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
This comment was marked as abuse.
This comment was marked as abuse.
|
Cross-checking this against SEP-2350 which landed after this PR was opened - a few things shifted:
|
Implement server-side scope challenge handling per MCP spec §10.1. This enables servers to declare required OAuth scopes per tool and automatically return HTTP 403 with WWW-Authenticate headers when a client's token lacks sufficient scopes. Key additions: - ToolScopeConfig type for declaring required/accepted scopes per tool - ScopeChallengeConfig on StreamableHTTP transport options - Pre-execution scope check in transport layer (before SSE stream opens) - McpServer.registerTool() accepts scopes option (string[] or config) - McpServer.setToolScopes() for decoupled/centralized scope declaration - Auto-wiring of scope resolver in McpServer.connect() - NodeStreamableHTTPServerTransport delegates setScopeResolver() - Additive scoping: challenges include union of existing + required scopes - 17 tests covering scope checks, overrides, batches, and auto-wiring - Proposal document for SDK devs and Tool Scopes Working Group Scope challenges are HTTP-only (ignored for stdio), operate at the transport layer before handlers execute, and follow the additive scoping pattern established by github/github-mcp-server. Relates to modelcontextprotocol#1151 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Apply review feedback from @localden on PR modelcontextprotocol#1624: - Flip the WWW-Authenticate `scope` value to advertise only the per-operation `required` scopes by default, per RFC 6750 Section 3.1 and SEP-2350. Add an opt-in `scopeChallenge.includeGrantedScopes` flag that restores the additive union behaviour for servers that need to defend against non-accumulating clients. - Change `ToolScopeConfig.required` to AND semantics (every scope must be present in the token). `accepted` is now the explicit OR/hierarchy escape hatch. - Escape `"` and `\` in all WWW-Authenticate quoted-string auth-param values per RFC 7235. - Replace the duck-typed transport check in `McpServer.connect` with a typed `ScopeAware` interface and `isScopeAware` guard. Export `ScopeAware`, `ScopeResolver`, `ScopeChallengeConfig`, `ToolScopeConfig`, and `isScopeAware` from `@modelcontextprotocol/server`. - Tests rewritten to focus on public-surface behaviour. 17 tests covering 403 emission, AND-required, OR-accepted, the `includeGrantedScopes` opt-in, header quoting, batch handling, setToolScopes override, custom error description, and auto-wiring. - Proposal doc updated to reflect SEP-2350 alignment and call out resources/prompts/completions step-up as follow-up work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
012707a to
e52a2cc
Compare
|
Thanks for the review @localden, all four points addressed and rebased on latest 1. Per-operation 2.
I will note that there are still some situations that come unstuck here where you have two required scopes, and one of them has a hierarchy. It is possible you could end up not issuing a challenge when needed in that OR case (because you don't know which scope(s) contain the other). I think perhaps there always needs to be an option for custom I deliberately kept 3. Quoted-string escaping. Added a 4. Resources / prompts. Noted in the proposal doc as follow-up. I can add a stacked PR for this (also covering completions, since those can require scopes too). One open question: re-auth I researched how every SDK handles the related 401 case (full re-auth, no refresh token) while a client has accumulated scopes from prior step-ups. The picture is:
SEP-2350 and the draft spec are silent on this. The Python PR #2676 authored by @dogacancolak pins the "drop on 401" invariant explicitly as the natural down-scoping moment. I think the spec should make this normative either way to stop the next round of cross-SDK drift. Does that need a PR or SEP clarification itself? Marking this PR ready for review on the back of the rebase and the fixes above. |
Summary
Server-side OAuth scope challenge (step-up auth) support for the Streamable HTTP transport, aligned with merged SEP-2350 and RFC 6750 Section 3.1.
Servers declare required OAuth scopes per tool. The transport checks the client's token scopes (read from
authInfo.scopes) before the tool runs and, when insufficient, returnsHTTP 403with aWWW-Authenticate: Bearer error="insufficient_scope", ...header, triggering the client's step-up authorization flow.Companion: client-side scope accumulation lands in #1657.
Relates to #1151.
Design
Scope challenges are enforced at the transport layer before the SSE stream opens. HTTP status codes are committed before tool handlers execute, so this is the only viable interception point.
Developer API
Key decisions
requiredby default. The 403scopevalue advertises only the scopes needed for the current operation, matching SEP-2350. Client-side accumulation handles the union.includeGrantedScopesopt-in. Servers that need to defend against clients which still replace scopes on every challenge can set this to true to get the previous additive union behaviour.requiredis AND,acceptedis OR.requiredmeans every scope must be present in the token.accepted, when provided, flips satisfaction to "any of these scopes satisfies", supporting hierarchies likerepoimplyingrepo:read.accepteddoes not influence the 403 challenge advertisement, only the gate.authInfo.scopesas populated by the implementer's auth middleware. It does not determine which scopes are active.tools/callonly for now. Resources, prompts, and completions are mechanically equivalent and tracked as a stacked follow-up PR."or\in a scope or error description cannot break the header.What is included
packages/server/src/server/streamableHttp.tsScopeChallengeConfig,ScopeResolver,ScopeAware,isScopeAware,setScopeResolver(),_checkScopeChallenge(),quoteAuthParam()packages/server/src/server/mcp.tsToolScopeConfig,scopesonregisterTool(),setToolScopes(),getToolScopes(), typed auto-wiring onconnect()packages/server/src/index.tsisScopeAwarepackages/middleware/node/src/streamableHttp.tssetScopeResolver()delegationpackages/server/test/server/scopeChallenge.test.tsdocs/proposals/scope-challenge-server-sdk.mdWhat is not included
Toolschema. Scope metadata is server-side only.Testing
@modelcontextprotocol/serverpass.