Skip to content

feat(server-auth-legacy): add frozen v1 Authorization-Server package#1908

Draft
felixweinberger wants to merge 4 commits intomainfrom
fweinberger/v2-bc-server-auth-legacy
Draft

feat(server-auth-legacy): add frozen v1 Authorization-Server package#1908
felixweinberger wants to merge 4 commits intomainfrom
fweinberger/v2-bc-server-auth-legacy

Conversation

@felixweinberger
Copy link
Copy Markdown
Contributor

Part of the v2 backwards-compatibility series — see reviewer guide.

New @modelcontextprotocol/server-auth-legacy package — frozen v1 copy of mcpAuthRouter/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-legacy package — frozen v1 copy of mcpAuthRouter/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:

Use a dedicated OAuth Authorization Server (IdP); SDK only ships Resource-Server glue

Evidence: Unblocks consumers on the v1 AS impl with a clear migration signal.

How Has This Been Tested?

  • Existing v1 tests ported
  • Integration: validated bump-only against 5 OSS repos via the v2-bc-integration validation branch
  • pnpm typecheck:all && pnpm lint:all && pnpm test:all green

Breaking Changes

None — additive @deprecated shim. Removed in v3.

Types of changes

  • New feature (non-breaking change which adds functionality)

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added or updated documentation as needed

Additional context

Stacks on: none

@felixweinberger felixweinberger added the v2-bc v2 backwards-compatibility series label Apr 15, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 15, 2026

🦋 Changeset detected

Latest commit: 126b89a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@modelcontextprotocol/server-auth-legacy Patch

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

@felixweinberger felixweinberger added this to the v2.0.0-bc milestone Apr 15, 2026
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 15, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@1908

@modelcontextprotocol/server

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server@1908

@modelcontextprotocol/server-auth-legacy

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server-auth-legacy@1908

@modelcontextprotocol/express

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/express@1908

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/fastify@1908

@modelcontextprotocol/hono

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/hono@1908

@modelcontextprotocol/node

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/node@1908

commit: 126b89a

@felixweinberger felixweinberger force-pushed the fweinberger/v2-bc-server-auth-legacy branch 2 times, most recently from 652b1db to 099bdf1 Compare April 16, 2026 09:43
@felixweinberger felixweinberger added the auth Issues and PRs related to Authentication / OAuth label Apr 16, 2026
@felixweinberger felixweinberger marked this pull request as ready for review April 16, 2026 17:46
@felixweinberger felixweinberger requested a review from a team as a code owner April 16, 2026 17:46
Comment thread packages/server-auth-legacy/src/handlers/authorize.ts
Comment thread packages/server-auth-legacy/README.md
Comment thread packages/server-auth-legacy/src/index.ts
@felixweinberger felixweinberger force-pushed the fweinberger/v2-bc-server-auth-legacy branch 3 times, most recently from 49747fd to 5700f66 Compare April 16, 2026 19:42
Comment thread .changeset/add-server-auth-legacy.md
Comment thread packages/server-auth-legacy/README.md Outdated
@felixweinberger felixweinberger marked this pull request as draft April 16, 2026 19:51
Comment thread packages/server-auth-legacy/src/index.ts Outdated
@felixweinberger felixweinberger force-pushed the fweinberger/v2-bc-server-auth-legacy branch from 5700f66 to 10c6b28 Compare April 16, 2026 20:30
@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

Comment thread docs/migration.md Outdated
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.
@felixweinberger felixweinberger force-pushed the fweinberger/v2-bc-server-auth-legacy branch from 10c6b28 to 7cac712 Compare April 16, 2026 21:07
@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

Comment thread packages/server-auth-legacy/test/index.test.ts
Comment thread packages/server-auth-legacy/tsdown.config.ts
@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

Comment thread packages/server-auth-legacy/test/router.test.ts Outdated
Comment thread packages/server-auth-legacy/package.json
…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.
@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

Comment thread docs/migration-SKILL.md

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."

Comment on lines +56 to +60
"peerDependenciesMeta": {
"express": {
"optional": true
}
},
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auth Issues and PRs related to Authentication / OAuth v2-bc v2 backwards-compatibility series

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant