modelcontextprotocol / modelcontextprotocol/typescript-sdk

Server auth defaults silently produce RFC 9728-noncompliant discovery when the MCP endpoint has a path

Open Beginner friendly
#2,751 0 comments 0 reactions 0 assignees View on GitHub

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:

  1. mcpAuthRouter({...}) without resourceServerUrl falls back to baseUrl, so Protected Resource Metadata (PRM) is mounted only at /.well-known/oauth-protected-resource with resource: "https://host/" — while the client connected to https://host/mcp.
  2. requireBearerAuth({ verifier }) without resourceMetadataUrl returns a 401 whose WWW-Authenticate has no resource_metadata parameter.

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-Authenticate when 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: "The resource value 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 selectResourceURLcheckResourceAllowed 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

  1. Docs/JSDoc (v1.x and main) — say explicitly that resourceServerUrl should be the MCP endpoint URL (not the origin) and that resourceMetadataUrl is required by the MCP spec, with the consequence of omitting each. I have a small PR ready for both branches.
  2. v2 (breaking changes acceptable) — consider making the intent impossible to miss, e.g. require resourceServerUrl in mcpAuthRouter (the neutral AuthMetadataOptions already requires it) and/or require resourceMetadataUrl in BearerAuthOptions, or accept a single mcpServerUrl and derive both.
  3. Optionally, a one-time warning in v1.x when requireBearerAuth runs without resourceMetadataUrl.

Happy to open the PRs once the direction is agreed.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.