feat(server): scope challenges for resources, prompts, completions (stacked on #1624)#2157
Draft
SamMorrowDrums wants to merge 4 commits into
Draft
Conversation
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>
Builds on modelcontextprotocol#1624 to extend the pre-execution scope challenge check from `tools/call` to the rest of the MCP operation surface: - `resources/read`: static URIs match exactly; templated URIs match the template. `registerResource()` accepts `scopes` (string[], ToolScopeConfig, or a dynamic resolver `(uri, variables) => scopes | Promise<scopes>` for cases where the required scope depends on path parameters, e.g. public vs private repositories). - `prompts/get`: `registerPrompt()` accepts `scopes`. Exact name lookup. - `completion/complete`: explicit, separate scope domain via `setCompletionScopes(ref, argumentName, scopes)`. No inheritance from the referenced prompt or resource (search and read are typically distinct capabilities, e.g. `repo:list` vs `repo:read`). Pass `'*'` as argumentName to apply scopes to every argument of a reference. Architecture: - Widen `ScopeResolver` from `(toolName) => ToolScopeConfig` to `(request: JSONRPCRequest) => ScopeResolution | Promise<...>`. - New `ScopeResolution` carries the resolved scope config and an `operationName` label used in 403 error descriptions (e.g. `tool:get_repo`, `resource:github://octo/hello`). - `_checkScopeChallenge` becomes async to support async resource resolvers. - `McpServer.connect()` auto-wires a router that dispatches on JSON-RPC method. - New public API: `setPromptScopes`, `setResourceScopes`, `setCompletionScopes`, and matching `get*Scopes` accessors. Public type exports: `ResourceScopeConfig`, `ResourceScopeFn`, `ScopeResolution`. Tests: 14 new behavior tests covering static and templated resources, dynamic per-request resolution, prompts, completions (including the no-inheritance contract and `'*'` wildcard), override registries, and mixed JSON-RPC batches. All 96 tests in @modelcontextprotocol/server pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🦋 Changeset detectedLatest commit: 3bcf533 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on top of #1624. Until that merges, the diff in this PR will include those commits; once #1624 merges this PR will rebase cleanly against
main.Summary
Extends the pre-execution scope challenge check from
tools/callto the rest of the MCP operation surface:resources/read,prompts/get, andcompletion/complete.Same architecture, same opinions on per-operation scopes, AND/OR semantics, and SEP-2350 alignment, just generalised across primitives.
What's new
Resources
registerResource()now acceptsscopesin its config. For templated resources,scopescan also be a function that receives the concrete URI and matched template variables, so the required scope can depend on path parameters (for example, public vs private repositories).Prompts
registerPrompt()acceptsscopesin its config. Exact-name lookup like tools.Completions
Completions get an explicit, separate scope domain. No inheritance from the referenced prompt or resource by design: search and read are typically distinct capabilities (
repo:listis not implied byrepo:read). If you genuinely want them identical, pass the same scopes array to bothregisterPromptandsetCompletionScopes.Central overrides
For decoupled or centralised scope configuration:
setPromptScopes(name, scopes),setResourceScopes(uriOrTemplateName, scopes),setCompletionScopes(ref, arg, scopes). All take precedence over registration-time scopes.Architecture
ScopeResolverwidens from(toolName) => ToolScopeConfigto(request: JSONRPCRequest) => ScopeResolution | Promise<...>. Breaking for any alpha consumer ofsetScopeResolver; no external consumers yet.ScopeResolution = { operationName: string; scopes: ToolScopeConfig }. TheoperationName(e.g.tool:get_repo,resource:github://octo/hello,prompt:summarise,completion:prompt:summarise/repository) is used in the default 403 error description and is passed tobuildErrorDescription._checkScopeChallengebecomes async to support async resource resolvers.McpServer.connect()auto-wires an operation-aware router that dispatches on JSON-RPC method.What's NOT in scope
*/listscope filtering (SEP-1881). When tools, resources, or prompts declare scopes,tools/list,resources/list,prompts/list, andresources/templates/listcould automatically filter their results to items the current token can satisfy. Intentionally out of scope for this PR but worth a follow-up.Testing
14 new behaviour tests in
scopeChallengePrimitives.test.tscovering:'*'argument wildcard.setResourceScopes,setPromptScopes.The 17 tool tests in
scopeChallenge.test.tscontinue to pass unchanged. All 96 tests in@modelcontextprotocol/serverpass; typecheck and lint clean.Notes for review
{ inherit: true }flag) if real-world usage shows it's wanted.accepted(OR escape hatch) extends to all primitives because it lives onToolScopeConfig. Dynamic resource resolvers can returnToolScopeConfigdirectly to useacceptedper-URI.