feat(server-auth-legacy): add frozen v1 Authorization-Server package#1908
feat(server-auth-legacy): add frozen v1 Authorization-Server package#1908felixweinberger wants to merge 4 commits intomainfrom
Conversation
🦋 Changeset detectedLatest commit: 126b89a The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
@modelcontextprotocol/client
@modelcontextprotocol/server
@modelcontextprotocol/server-auth-legacy
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
652b1db to
099bdf1
Compare
49747fd to
5700f66
Compare
5700f66 to
10c6b28
Compare
|
@claude review |
Frozen, deprecated copy of the v1 SDK's src/server/auth/ Authorization Server helpers (mcpAuthRouter, ProxyOAuthServerProvider, OAuth handlers, middleware, and error subclasses) as a standalone package for v1 -> v2 migration. The package carries a package.json "deprecated" field directing users to a dedicated IdP plus the Resource Server helpers in @modelcontextprotocol/express. Imports of OAuth types/schemas are rewritten to @modelcontextprotocol/core; AuthInfo is re-exported from core for structural compatibility with v2 request-handler context. Minimal edits vs v1 source: override modifiers and noUncheckedIndexedAccess fixes to satisfy the v2 strict tsconfig; behaviour is unchanged.
10c6b28 to
7cac712
Compare
|
@claude review |
|
@claude review |
…a describe block Ports the two remaining v1 test files/blocks flagged in review: - test/middleware/bearerAuth.test.ts (20 cases): requireBearerAuth coverage - test/router.test.ts: append 'MCP Auth Metadata Router' describe (4 cases) - test/helpers/http.ts: createExpressResponseMock (used by bearerAuth tests) All 10 v1 server/auth test files now ported. 142 tests pass.
|
@claude review |
|
|
||
| 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. |
There was a problem hiding this comment.
🔴 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 bynew OAuthError(OAuthErrorCode.*, msg)from@modelcontextprotocol/client - "Removed:
OAUTH_ERRORSconstant"
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:149—if (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: InvalidGrantError → OAuthError 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) → false → res.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 InvalidTokenError → requireBearerAuth 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/OAuthTokenVerifierimplementations used with@modelcontextprotocol/server-auth-legacy, keep throwing the legacy subclasses exported from that package)", and drop/qualify the "Removed:OAUTH_ERRORSconstant" 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'sOAuthError— the handlers'instanceofchecks won't match the core class."
| "peerDependenciesMeta": { | ||
| "express": { | ||
| "optional": true | ||
| } | ||
| }, |
There was a problem hiding this comment.
🔴 🔴 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, andsrc/handlers/token.ts:3all do a top-level runtimeimport express from 'express'(not type-only — each callsexpress.Router()/express.urlencoded()/express.json()at module evaluation).src/index.tsre-exports runtime values from every one of those modules (mcpAuthRouter,authorizationHandler,tokenHandler,revocationHandler,clientRegistrationHandler,metadataHandler, etc.).package.jsonexposes only the"."entry — there are no subpath exports — so even importing a pure type likeOAuthErrororAuthInfoloadsdist/index.mjs, which has a top-levelimport '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.mjscontains a bareimport ... 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.
Part of the v2 backwards-compatibility series — see reviewer guide.
New
@modelcontextprotocol/server-auth-legacypackage — frozen v1 copy ofmcpAuthRouter/ProxyOAuthServerProvider/handlers.npm deprecated on publish with a message pointing to a real IdP + the RS helpers in/express.Motivation and Context
New
@modelcontextprotocol/server-auth-legacypackage — frozen v1 copy ofmcpAuthRouter/ProxyOAuthServerProvider/handlers.npm deprecated on publish with a message pointing to a real IdP + the RS helpers in/express.v1 vs v2 pattern & evidence
v1 pattern:
`import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js'`v2-native:
Evidence: Unblocks consumers on the v1 AS impl with a clear migration signal.
How Has This Been Tested?
v2-bc-integrationvalidation branchpnpm typecheck:all && pnpm lint:all && pnpm test:allgreenBreaking Changes
None — additive
@deprecatedshim. Removed in v3.Types of changes
Checklist
Additional context
Stacks on: none