Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
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 @@ Replace all `@modelcontextprotocol/sdk/...` imports using this table.
| `@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 @@ new URL(ctx.http?.req?.url).searchParams.get('debug')

### 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.
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 @@ Access validators explicitly:
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`.
Comment thread
claude[bot] marked this conversation as resolved.
- **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'
}
}
];
78 changes: 78 additions & 0 deletions packages/server-auth-legacy/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"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"
},
"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