Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-server-auth-legacy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/server-auth-legacy': patch
---

Add `@modelcontextprotocol/server-auth-legacy`, a deprecated, frozen copy of the v1 SDK's `src/server/auth/` Authorization Server helpers (`mcpAuthRouter`, `ProxyOAuthServerProvider`, OAuth handlers/middleware/errors). Provided solely for v1 → v2 migration; new code should use a dedicated IdP plus the Resource Server helpers in `@modelcontextprotocol/express`.
Comment thread
claude[bot] marked this conversation as resolved.
1 change: 1 addition & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@modelcontextprotocol/hono": "2.0.0-alpha.0",
"@modelcontextprotocol/node": "2.0.0-alpha.0",
"@modelcontextprotocol/server": "2.0.0-alpha.0",
"@modelcontextprotocol/server-auth-legacy": "2.0.0-alpha.2",
"@modelcontextprotocol/test-conformance": "2.0.0-alpha.0",
"@modelcontextprotocol/test-helpers": "2.0.0-alpha.0",
"@modelcontextprotocol/test-integration": "2.0.0-alpha.0"
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ jobs:
run:
pnpm dlx pkg-pr-new publish --packageManager=npm --pnpm './packages/server' './packages/client'
'./packages/middleware/express' './packages/middleware/fastify' './packages/middleware/hono' './packages/middleware/node'
'./packages/server-auth-legacy'
4 changes: 2 additions & 2 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ For production use, you can either:

The SDK ships several runnable server examples under `examples/server/src`. Start from the server examples index in [`examples/server/README.md`](../examples/server/README.md) and the entry-point quick start in the root [`README.md`](../README.md).

### Why did we remove `server` auth exports?
### Where are the server auth helpers?

Server authentication & authorization is outside of the scope of the SDK, and the recommendation is to use packages that focus on this area specifically (or a full-fledged Authorization Server for those who use such). Example packages provide an example with `better-auth`.
All v1 `server/auth/*` exports are available in the deprecated `@modelcontextprotocol/server-auth-legacy` package (frozen v1 copy). New code should use a dedicated IdP/OAuth library; example packages provide a demo with `better-auth`.

### Why did we remove `server` SSE transport?

Expand Down
7 changes: 3 additions & 4 deletions docs/migration-SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
| `@modelcontextprotocol/sdk/server/stdio.js` | `@modelcontextprotocol/server` |
| `@modelcontextprotocol/sdk/server/streamableHttp.js` | `@modelcontextprotocol/node` (class renamed to `NodeStreamableHTTPServerTransport`) OR `@modelcontextprotocol/server` (web-standard `WebStandardStreamableHTTPServerTransport` for Cloudflare Workers, Deno, etc.) |
| `@modelcontextprotocol/sdk/server/sse.js` | REMOVED (migrate to Streamable HTTP) |
| `@modelcontextprotocol/sdk/server/auth/*` | REMOVED (use external auth library) |
| `@modelcontextprotocol/sdk/server/auth/*` | `@modelcontextprotocol/server-auth-legacy` (deprecated; frozen v1 copy) |
| `@modelcontextprotocol/sdk/server/middleware.js` | `@modelcontextprotocol/express` (signature changed, see section 8) |

### Types / shared imports
Expand Down Expand Up @@ -319,8 +319,7 @@

### Server-side auth

All server OAuth exports removed: `mcpAuthRouter`, `OAuthServerProvider`, `OAuthTokenVerifier`, `requireBearerAuth`, `authenticateClient`, `ProxyOAuthServerProvider`, `allowedMethods`, and associated types. Use an external auth library (e.g., `better-auth`). See
`examples/server/src/` for demos.
All v1 `server/auth/*` exports (`mcpAuthRouter`, `OAuthServerProvider`, `OAuthTokenVerifier`, `requireBearerAuth`, `mcpAuthMetadataRouter`, `authenticateClient`, `ProxyOAuthServerProvider`, `allowedMethods`, etc.) are available in the deprecated `@modelcontextprotocol/server-auth-legacy` package. New code should use an external IdP/OAuth library. See `examples/server/src/` for demos.

Check failure on line 322 in docs/migration-SKILL.md

View check run for this annotation

Claude / Claude Code Review

migration docs contradict themselves on OAuth error classes; following both yields runtime 500s

The §"OAuth error consolidation" table (migration-SKILL.md ~L167-204, migration.md ~L725-754) still says `InvalidGrantError`/`InvalidTokenError`/…/`OAUTH_ERRORS` are *removed* and must be replaced with core's `OAuthError(OAuthErrorCode.*)`, but this PR's edits to L57/L322/L504 now route `server/auth/*` → `@modelcontextprotocol/server-auth-legacy`, which re-exports that exact subclass hierarchy. The two sections give mutually-exclusive instructions for the same v1 symbols, and applying both is a
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The §"OAuth error consolidation" table (migration-SKILL.md ~L167-204, migration.md ~L725-754) still says InvalidGrantError/InvalidTokenError/…/OAUTH_ERRORS are removed and must be replaced with core's OAuthError(OAuthErrorCode.*), but this PR's edits to L57/L322/L504 now route server/auth/*@modelcontextprotocol/server-auth-legacy, which re-exports that exact subclass hierarchy. The two sections give mutually-exclusive instructions for the same v1 symbols, and applying both is a runtime trap: the legacy handlers' catch blocks check error instanceof OAuthError against the legacy class, so a provider rewritten to throw core's OAuthError falls through to 500 server_error instead of 4xx. Please scope the OAuth-error-consolidation section to client-side usage and/or add a note in §"Server-side auth" that providers passed to server-auth-legacy must keep throwing the legacy subclasses from that package.

Extended reasoning...

What's wrong

This PR's commit 7cac712 rewrote three locations in docs/migration-SKILL.md (L57 import-mapping row, L322 §"Server-side auth", L504 step 9) and docs/migration.md §"Server auth moved" to map @modelcontextprotocol/sdk/server/auth/*@modelcontextprotocol/server-auth-legacy. That package's src/index.ts does export * from './errors.js', re-exporting the full v1 error hierarchy: InvalidGrantError, InvalidTokenError, InvalidClientError, …, OAUTH_ERRORS.

However, the pre-existing §"OAuth error consolidation" table (migration-SKILL.md section 5, ~L167-204; migration.md §"OAuth error refactoring", ~L725-754) was left unchanged. It still flatly states:

  • Each subclass (InvalidGrantError, InvalidTokenError, etc.) is replaced by new OAuthError(OAuthErrorCode.*, msg) from @modelcontextprotocol/client
  • "Removed: OAUTH_ERRORS constant"

In v1 these subclasses were defined only in src/server/auth/errors.ts (verified via git grep on origin/v1.x; src/client/auth.ts imported them from there). So both sections govern the identical v1 symbols, and after this PR they give mutually-exclusive instructions: §3/§8 says "import these unchanged from server-auth-legacy", §5 says "they're removed, throw core's OAuthError instead".

Why this is a runtime trap, not just a prose nit

There are now two distinct OAuthError class definitions:

  • packages/core/src/auth/errors.ts:100 — constructor (code: OAuthErrorCode | string, message: string)
  • packages/server-auth-legacy/src/errors.ts:6 — constructor (message: string, errorUri?: string)

instanceof does not cross-match between them. Every legacy handler's catch block tests against the legacy class:

  • handlers/token.ts:149if (error instanceof OAuthError) { res.status(400)... } else { res.status(500).json(serverError) }
  • middleware/bearerAuth.ts:88-97 — same pattern (with 401/403 for the recognized subclasses, 500 fallback)
  • handlers/revoke.ts, handlers/register.ts, middleware/clientAuth.ts, handlers/authorize.ts — same pattern

A provider that throws core's OAuthError (per the §5 table) fails every one of these instanceof checks and falls through to the generic 500 server_error branch.

Step-by-step proof

Step What the SKILL says What the user does
1 L57: server/auth/*server-auth-legacy Swaps import { tokenHandler, requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/...' to from '@modelcontextprotocol/server-auth-legacy'
2 L504 step 3: "Replace removed type aliases per section 5" Reaches §5's OAuth-error table
3 §5 table: InvalidGrantErrorOAuthError w/ OAuthErrorCode.InvalidGrant; example imports from @modelcontextprotocol/client Rewrites their OAuthServerProvider.exchangeAuthorizationCode's throw new InvalidGrantError('expired')throw new OAuthError(OAuthErrorCode.InvalidGrant, 'expired') (core class)
4 Client POSTs /token with an expired code
5 tokenHandler catch: error instanceof OAuthError (legacy) → falseres.status(500).json({error:'server_error'})

Expected (v1 behavior): 400 {"error":"invalid_grant","error_description":"expired"}.
Actual after following both sections: 500 {"error":"server_error","error_description":"Internal Server Error"}.

The same applies to OAuthTokenVerifier.verifyAccessToken throwing InvalidTokenErrorrequireBearerAuth returns 500 instead of 401 + WWW-Authenticate.

Why this was introduced by this PR

Before 7cac712, migration-SKILL.md L57 said server/auth/* → "REMOVED (use external auth library)" and §5 said the error subclasses are removed. That was internally consistent — the classes really were gone from the SDK, so the only remaining usage was client-side and §5's mapping to core's OAuthError was correct everywhere. This PR re-introduced the subclasses via server-auth-legacy and updated three doc locations to point there, but did not reconcile §5, which now makes a factually false claim ("Removed: OAUTH_ERRORS constant" — it's exported from server-auth-legacy) and a functionally dangerous one (rewrite provider throws to a class the legacy handlers don't recognize).

Why nothing else catches it

migration-SKILL.md is explicitly designed for mechanical/LLM application — step 15 says "apply in this order" and step 3 is a blanket "Replace removed type aliases per section 5" with no client/server scoping. The ported test suite (test/handlers/token.test.ts) only exercises providers that throw the legacy subclasses, so CI doesn't see the cross-class case. This matches the repo's review checklist: "flag prose that now contradicts the implementation".

Fix

Either or both of:

  • Scope §5's OAuth-error table to client-side: add a lead-in like "(client-side OAuth flows only — for OAuthServerProvider/OAuthTokenVerifier implementations used with @modelcontextprotocol/server-auth-legacy, keep throwing the legacy subclasses exported from that package)", and drop/qualify the "Removed: OAUTH_ERRORS constant" line.
  • Add a sentence to §"Server-side auth" (migration-SKILL.md L322, migration.md §"Server auth moved"): "Providers/verifiers passed to these handlers must throw the error subclasses (InvalidGrantError, InvalidTokenError, …) from @modelcontextprotocol/server-auth-legacy, not core's OAuthError — the handlers' instanceof checks won't match the core class."


### Host header validation (Express)

Expand Down Expand Up @@ -502,6 +501,6 @@
6. Replace plain header objects with `new Headers({...})` and bracket access (`headers['x']`) with `.get()` calls per section 7
7. If using `hostHeaderValidation` from server, update import and signature per section 8
8. If using server SSE transport, migrate to Streamable HTTP
9. If using server auth from the SDK, migrate to an external auth library
9. If using server auth from the SDK, import from `@modelcontextprotocol/server-auth-legacy` (deprecated; frozen v1 copy)
10. If relying on `listTools()`/`listPrompts()`/etc. throwing on missing capabilities, set `enforceStrictCapabilities: true`
11. Verify: build with `tsc` / run tests
6 changes: 3 additions & 3 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,11 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'));
```

### Server auth removed
### Server auth moved

Server-side OAuth/auth has been removed entirely from the SDK. This includes `mcpAuthRouter`, `OAuthServerProvider`, `OAuthTokenVerifier`, `requireBearerAuth`, `authenticateClient`, `ProxyOAuthServerProvider`, `allowedMethods`, and all associated types.
The full v1 `server/auth/*` tree (including `mcpAuthRouter`, `ProxyOAuthServerProvider`, `requireBearerAuth`, `mcpAuthMetadataRouter`, `OAuthTokenVerifier`, `authenticateClient`, `allowedMethods`, and associated types) is available as a frozen v1 copy in the deprecated `@modelcontextprotocol/server-auth-legacy` package.

Use a dedicated auth library (e.g., `better-auth`) or a full Authorization Server instead. See the [examples](../examples/server/src/) for a working demo with `better-auth`.
New code should use a dedicated IdP/OAuth library (or `better-auth` per the [examples](../examples/server/src/)) instead of running an Authorization Server from the SDK.

Note: `AuthInfo` has moved from `server/auth/types.ts` to the core types and is now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`.

Expand Down
22 changes: 22 additions & 0 deletions packages/server-auth-legacy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# @modelcontextprotocol/server-auth-legacy

<!-- prettier-ignore -->
> [!WARNING]
> **Deprecated.** This package is a frozen copy of the v1 SDK's `src/server/auth/` Authorization Server helpers (`mcpAuthRouter`, `ProxyOAuthServerProvider`, etc.). It exists solely to ease migration from `@modelcontextprotocol/sdk` v1 and will not receive new features or non-critical bug fixes.

The v2 SDK no longer ships an OAuth Authorization Server implementation. MCP servers are Resource Servers; running your own AS is an anti-pattern for most deployments.

## Migration

- **Resource Server glue** (`requireBearerAuth`, `mcpAuthMetadataRouter`, Protected Resource Metadata): use the first-class helpers in `@modelcontextprotocol/express`.
- **Authorization Server**: use a dedicated IdP (Auth0, Keycloak, Okta, etc.) or a purpose-built OAuth library.

## Usage (legacy)

```ts
import express from 'express';
import { mcpAuthRouter, ProxyOAuthServerProvider } from '@modelcontextprotocol/server-auth-legacy';

const app = express();
app.use(mcpAuthRouter({ provider, issuerUrl: new URL('https://example.com') }));
```
12 changes: 12 additions & 0 deletions packages/server-auth-legacy/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// @ts-check

import baseConfig from '@modelcontextprotocol/eslint-config';

export default [
...baseConfig,
{
settings: {
'import/internal-regex': '^@modelcontextprotocol/core'
}
}
];
83 changes: 83 additions & 0 deletions packages/server-auth-legacy/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
{
"name": "@modelcontextprotocol/server-auth-legacy",
"private": false,
"version": "2.0.0-alpha.2",
Comment thread
felixweinberger marked this conversation as resolved.
"description": "Frozen v1 OAuth Authorization Server helpers (mcpAuthRouter, ProxyOAuthServerProvider) for the Model Context Protocol TypeScript SDK. Deprecated; use a dedicated OAuth server in production.",
"deprecated": "The MCP SDK no longer ships an Authorization Server implementation. This package is a frozen copy of the v1 src/server/auth helpers for migration purposes only and will not receive new features. Use a dedicated OAuth Authorization Server (e.g. an IdP) and the Resource Server helpers in @modelcontextprotocol/express instead.",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
"homepage": "https://modelcontextprotocol.io",
"bugs": "https://github.com/modelcontextprotocol/typescript-sdk/issues",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/modelcontextprotocol/typescript-sdk.git"
},
"engines": {
"node": ">=20"
},
"keywords": [
"modelcontextprotocol",
"mcp",
"oauth",
"express",
"legacy"
],
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs"
}
},
"files": [
"dist"
],
"scripts": {
"typecheck": "tsgo -p tsconfig.json --noEmit",
"build": "tsdown",
"build:watch": "tsdown --watch",
"prepack": "npm run build",
"lint": "eslint src/ && prettier --ignore-path ../../.prettierignore --check .",
"lint:fix": "eslint src/ --fix && prettier --ignore-path ../../.prettierignore --write .",
"check": "pnpm run typecheck && pnpm run lint",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"cors": "catalog:runtimeServerOnly",
"express-rate-limit": "^8.2.1",
"pkce-challenge": "catalog:runtimeShared",
"zod": "catalog:runtimeShared"
},
"peerDependencies": {
"express": "catalog:runtimeServerOnly"
},
"peerDependenciesMeta": {
"express": {
"optional": true
}
},

Check failure on line 60 in packages/server-auth-legacy/package.json

View check run for this annotation

Claude / Claude Code Review

express marked optional peer but is unconditionally imported at module load

🔴 126b89a marks `express` as an *optional* peer dependency, but every entry-point file in this package does a top-level runtime `import express from 'express'` (router.ts + all 5 handlers/*.ts), and the single `"."` export barrel re-exports from all of them — there is no express-free subset of the API. With `optional: true`, npm 7+/pnpm will silently skip auto-install and suppress the missing-peer warning, so `npm i @modelcontextprotocol/server-auth-legacy` alone followed by *any* import throws
Comment on lines +56 to +60
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 🔴 126b89a marks express as an optional peer dependency, but every entry-point file in this package does a top-level runtime import express from 'express' (router.ts + all 5 handlers/*.ts), and the single "." export barrel re-exports from all of them — there is no express-free subset of the API. With optional: true, npm 7+/pnpm will silently skip auto-install and suppress the missing-peer warning, so npm i @modelcontextprotocol/server-auth-legacy alone followed by any import throws ERR_MODULE_NOT_FOUND: Cannot find package 'express' at load time. Sibling @modelcontextprotocol/express (packages/middleware/express/package.json:45-48) correctly keeps express as a required peer; please revert 126b89a (drop the peerDependenciesMeta block).

Extended reasoning...

What changed

Commit 126b89a (the most recent commit on this PR, pushed after the last review round) added to packages/server-auth-legacy/package.json:

"peerDependenciesMeta": {
    "express": {
        "optional": true
    }
}

This marks the express peer dependency as optional, which tells npm 7+ and pnpm to (a) not auto-install it when this package is installed, and (b) not emit the "unmet peer dependency" warning when it's absent.

Why it's wrong

The optional-peer pattern is only valid when either (1) the import of that peer is lazy/conditional, or (2) some subset of the package's API is reachable without it. Neither holds here:

  • src/router.ts:3, src/handlers/authorize.ts:2, src/handlers/metadata.ts:4, src/handlers/register.ts:7, src/handlers/revoke.ts:4, and src/handlers/token.ts:3 all do a top-level runtime import express from 'express' (not type-only — each calls express.Router() / express.urlencoded() / express.json() at module evaluation).
  • src/index.ts re-exports runtime values from every one of those modules (mcpAuthRouter, authorizationHandler, tokenHandler, revocationHandler, clientRegistrationHandler, metadataHandler, etc.).
  • package.json exposes only the "." entry — there are no subpath exports — so even importing a pure type like OAuthError or AuthInfo loads dist/index.mjs, which has a top-level import 'express'.
  • tsdown leaves peer dependencies external (the earlier bundle-analysis comment #3096487104 confirmed: "The bundle's only remaining external imports are express, express-rate-limit, zod/v4, cors, node:crypto, and pkce-challenge"), so dist/index.mjs contains a bare import ... from 'express' that Node must resolve at load time.

Additionally, express-rate-limit (a hard dependency of this package) itself peer-depends on express, so the dependency graph is inconsistent regardless.

Step-by-step proof

Step Result
1. npm i @modelcontextprotocol/server-auth-legacy (fresh project, no express) npm 7+ installs without auto-installing express and without warning, because peerDependenciesMeta.express.optional: true
2. import { OAuthError } from '@modelcontextprotocol/server-auth-legacy' Node resolves ./dist/index.mjs
3. dist/index.mjs evaluates its top-level import express from 'express' ERR_MODULE_NOT_FOUND: Cannot find package 'express'
4. Same for any other import (mcpAuthRouter, requireBearerAuth, AuthInfo, …) Same crash — there is no reachable symbol that bypasses the express import

So the only effect of 126b89a is to remove the helpful install-time signal and replace it with a runtime crash.

Repo precedent

Sibling @modelcontextprotocol/express (packages/middleware/express/package.json:45-48) declares express as a required peer with no peerDependenciesMeta — that's the correct pattern for a package whose entire API is express middleware, and this package is in exactly the same position.

Fix

Revert 126b89a — delete the peerDependenciesMeta block (lines 56-60) so express is a required peer again. Alternatively, if the intent is to avoid forcing a specific express version on consumers who already have one, that's already what a required peer dep does; optional is the wrong knob.

"devDependencies": {
"@modelcontextprotocol/core": "workspace:^",
"@modelcontextprotocol/tsconfig": "workspace:^",
"@modelcontextprotocol/vitest-config": "workspace:^",
"@modelcontextprotocol/eslint-config": "workspace:^",
"@eslint/js": "catalog:devTools",
"@types/cors": "catalog:devTools",
"@types/express": "catalog:devTools",
"@types/express-serve-static-core": "catalog:devTools",
"@types/supertest": "catalog:devTools",
"@typescript/native-preview": "catalog:devTools",
"eslint": "catalog:devTools",
"eslint-config-prettier": "catalog:devTools",
"eslint-plugin-n": "catalog:devTools",
"express": "catalog:runtimeServerOnly",
"prettier": "catalog:devTools",
"supertest": "catalog:devTools",
"tsdown": "catalog:devTools",
"typescript": "catalog:devTools",
"typescript-eslint": "catalog:devTools",
"vitest": "catalog:devTools"
}
}
22 changes: 22 additions & 0 deletions packages/server-auth-legacy/src/clients.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { OAuthClientInformationFull } from '@modelcontextprotocol/core';

/**
* Stores information about registered OAuth clients for this server.
*/
export interface OAuthRegisteredClientsStore {
/**
* Returns information about a registered client, based on its ID.
*/
getClient(clientId: string): OAuthClientInformationFull | undefined | Promise<OAuthClientInformationFull | undefined>;

/**
* Registers a new client with the server. The client ID and secret will be automatically generated by the library. A modified version of the client information can be returned to reflect specific values enforced by the server.
*
* NOTE: Implementations should NOT delete expired client secrets in-place. Auth middleware provided by this library will automatically check the `client_secret_expires_at` field and reject requests with expired secrets. Any custom logic for authenticating clients should check the `client_secret_expires_at` field as well.
*
* If unimplemented, dynamic client registration is unsupported.
*/
registerClient?(
client: Omit<OAuthClientInformationFull, 'client_id' | 'client_id_issued_at'>
): OAuthClientInformationFull | Promise<OAuthClientInformationFull>;
}
Loading
Loading