modelcontextprotocol / modelcontextprotocol/typescript-sdk
Server auth defaults silently produce RFC 9728-noncompliant discovery when the MCP endpoint has a path
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 13.4k
- Forks
- 2.2k
- Avg merge
- 3d 15h
- Merged PRs (30d)
- 4
Description
Summary
When an MCP endpoint is served under a path (the common /mcp case), the default behaviour of mcpAuthRouter and requireBearerAuth produces an OAuth discovery surface that violates the MCP authorization spec and RFC 9728, and nothing warns the server author:
mcpAuthRouter({...})withoutresourceServerUrlfalls back tobaseUrl, so Protected Resource Metadata (PRM) is mounted only at/.well-known/oauth-protected-resourcewithresource: "https://host/"— while the client connected tohttps://host/mcp.requireBearerAuth({ verifier })withoutresourceMetadataUrlreturns a401whoseWWW-Authenticatehas noresource_metadataparameter.
Each option is documented as "optional", but for an MCP server both are effectively required:
- MCP Authorization, Authorization Server Location: "MCP servers MUST use the HTTP header
WWW-Authenticatewhen returning a 401 Unauthorized to indicate the location of the resource server metadata URL as described in RFC9728 Section 5.1." - RFC 9728 §3 puts the metadata for a resource with a path at
/.well-known/oauth-protected-resource/<path>; §3.3: "Theresourcevalue returned MUST be identical to the protected resource's resource identifier … If these values are not identical, the data contained in the response MUST NOT be used."
The reference client in this SDK masks the problem: discoverMetadataWithFallback falls back to the root well-known and selectResourceURL → checkResourceAllowed accepts a resource that is a parent path of the endpoint. Clients that enforce §3.3 literally do not: Gemini CLI throws ResourceMismatchError (packages/core/src/mcp/oauth-utils.ts, exact protocol//host+pathname comparison) and aborts discovery before ever reaching registration_endpoint, so dynamic client registration never starts. Antigravity CLI (agy) behaves the same. The result for server authors is "works in Claude Code / Codex, silently fails in Gemini CLI / Antigravity" with no hint that the server is at fault.
Reproduction
@modelcontextprotocol/sdk 1.25.1 (same on 1.29.0 and main), Node 24.
import express from 'express';
import { mcpAuthRouter, getOAuthProtectedResourceMetadataUrl } from '@modelcontextprotocol/sdk/server/auth/router.js';
import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
import { ProxyOAuthServerProvider } from '@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js';
import { InvalidTokenError } from '@modelcontextprotocol/sdk/server/auth/errors.js';
const BASE_URL = 'https://mcp.example.com';
const provider = new ProxyOAuthServerProvider({
endpoints: { authorizationUrl: 'https://as.example.com/authorize', tokenUrl: 'https://as.example.com/token' },
verifyAccessToken: async () => { throw new InvalidTokenError('invalid'); },
getClient: async () => undefined,
});
const app = express();
// What the README-level wiring looks like today: only baseUrl/issuerUrl, no resourceServerUrl / resourceMetadataUrl
app.use(mcpAuthRouter({ provider, issuerUrl: new URL(BASE_URL), baseUrl: new URL(BASE_URL), scopesSupported: ['s1'] }));
app.all('/mcp', requireBearerAuth({ verifier: provider }), (_req, res) => res.json({ ok: true }));
app.listen(3000);
$ curl -si -X POST localhost:3000/mcp -H 'Content-Type: application/json' -d '{}' | grep -i www-authenticate
WWW-Authenticate: Bearer error="invalid_token", error_description="Missing Authorization header"
$ curl -s localhost:3000/.well-known/oauth-protected-resource
{"resource":"https://mcp.example.com/","authorization_servers":["https://mcp.example.com/"],"scopes_supported":["s1"]}
$ curl -s -o /dev/null -w '%{http_code}\n' localhost:3000/.well-known/oauth-protected-resource/mcp
404
Passing the two options fixes the surface (path-scoped PRM with resource: ".../mcp", resource_metadata in the challenge), but the default silently produces the broken shape above.
Suggested changes
- Docs/JSDoc (v1.x and main) — say explicitly that
resourceServerUrlshould be the MCP endpoint URL (not the origin) and thatresourceMetadataUrlis required by the MCP spec, with the consequence of omitting each. I have a small PR ready for both branches. - v2 (breaking changes acceptable) — consider making the intent impossible to miss, e.g. require
resourceServerUrlinmcpAuthRouter(the neutralAuthMetadataOptionsalready requires it) and/or requireresourceMetadataUrlinBearerAuthOptions, or accept a singlemcpServerUrland derive both. - Optionally, a one-time warning in v1.x when
requireBearerAuthruns withoutresourceMetadataUrl.
Happy to open the PRs once the direction is agreed.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the option definitions and JSDoc behind server/auth/router.js and server/auth/middleware/bearerAuth.js, then compare the documented defaults with the MCP authorization requirements cited in the issue. Update the v1.x and main documentation to identify the MCP endpoint URL, explain the consequences of omitting resourceServerUrl or resourceMetadataUrl, and make the required configuration clear.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- authentication, documentation
- Issue type
- Documentation
- Difficulty
- 2/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100