Skip to content

feat(server): OAuth scope challenge support (step-up auth)#1624

Draft
SamMorrowDrums wants to merge 2 commits intomodelcontextprotocol:mainfrom
SamMorrowDrums:scope-challenge-server-sdk
Draft

feat(server): OAuth scope challenge support (step-up auth)#1624
SamMorrowDrums wants to merge 2 commits intomodelcontextprotocol:mainfrom
SamMorrowDrums:scope-challenge-server-sdk

Conversation

@SamMorrowDrums
Copy link

Summary

Server-side OAuth scope challenge (step-up auth) support per MCP spec §10.1.

Servers can now declare required OAuth scopes per tool and the transport automatically returns HTTP 403 with WWW-Authenticate headers when a client's token lacks sufficient scopes — triggering the client's existing re-authorization flow.

Relates to #1151

Design

Scope challenges are enforced at the transport layer before the SSE stream opens (not in tool handlers). This is the only viable architecture because HTTP status codes are committed before handlers execute. The existing client-side 403 handling already works unchanged.

Developer API

const server = new McpServer({ name: "my-server", version: "1.0.0" });

// Option 1: Co-located with tool registration
server.registerTool("get_repo", {
  description: "Get repository details",
  inputSchema: z.object({ repo: z.string() }),
  scopes: ["repo:read"],
}, handler);

// Option 2: Scope hierarchy (broader scope implies narrower)
server.registerTool("get_repo", {
  description: "Get repository details", 
  inputSchema: z.object({ repo: z.string() }),
  scopes: { required: ["repo:read"], accepted: ["repo:read", "repo"] },
}, handler);

// Option 3: Decoupled — set scopes separately from registration
server.setToolScopes("get_repo", ["repo:read"]);
// Transport configuration
const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: () => randomUUID(),
  scopeChallenge: {
    resourceMetadataUrl: "https://auth.example.com/.well-known/oauth-protected-resource",
  },
});

// Auto-wired on connect — no manual plumbing needed
await server.connect(transport);

Key decisions

  • required vs accepted: accepted scopes let the call through without challenge; required scopes are what's recommended in the 403 challenge. This supports scope hierarchies (e.g. repo implies repo:read)
  • Additive scoping: The scope value in WWW-Authenticate is always the union of existing token scopes + required scopes, so clients never lose scopes during step-up (matching github/github-mcp-server pattern)
  • Implementer owns scope determination: The SDK reads authInfo.scopes as populated by the implementer's auth middleware — it does not determine what scopes are active
  • HTTP-only: Scope challenges are ignored for stdio and other non-HTTP transports
  • No handler changes: Pre-execution check only; mid-handler scope challenges are discussed in the proposal doc as future work

What's included

File What
packages/server/src/server/mcp.ts ToolScopeConfig, scopes on registerTool(), setToolScopes(), getToolScopes(), auto-wiring
packages/server/src/server/streamableHttp.ts ScopeChallengeConfig, setScopeResolver(), _checkScopeChallenge()
packages/middleware/node/src/streamableHttp.ts setScopeResolver() delegation
packages/server/test/server/scopeChallenge.test.ts 17 tests covering scope checks, overrides, batches, auto-wiring
docs/proposals/scope-challenge-server-sdk.md Full research + proposal for working group discussion

What's NOT included (out of scope)

  • No changes to Protocol layer, JSON-RPC handling, stdio transport, or client SDK
  • No mid-handler scope challenges (discussed in proposal §6 as future work)
  • No changes to the MCP specification — this implements existing spec behavior
  • No Tool schema changes (scope metadata is server-side only for now)

Testing

  • 17 new tests for scope challenge flow
  • All existing tests pass (381/382 — the 1 skip is a pre-existing Cloudflare Workers env issue)
  • Full typecheck and lint clean

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>
@changeset-bot
Copy link

changeset-bot bot commented Mar 4, 2026

🦋 Changeset detected

Latest commit: b7c3ad1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@modelcontextprotocol/server Minor
@modelcontextprotocol/node Major
@modelcontextprotocol/express Major
@modelcontextprotocol/hono Major

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

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 4, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@1624

@modelcontextprotocol/server

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server@1624

@modelcontextprotocol/express

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/express@1624

@modelcontextprotocol/hono

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/hono@1624

@modelcontextprotocol/node

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/node@1624

commit: 032daf6

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant