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
8 changes: 8 additions & 0 deletions .changeset/oauth-error-compat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@modelcontextprotocol/core': minor
'@modelcontextprotocol/client': patch
---

Restore OAuth error backwards compatibility with SDK 1.x: the per-code error subclasses (`InvalidGrantError`, `ServerError`, …) are exported again as deprecated wrappers around `OAuthError`, and SDK-produced OAuth errors are constructed as the matching subclass so 1.x-style
`instanceof` classification keeps working. Adds `oauthErrorFromCode()`, `isTransientOAuthError()` (which treats unknown error codes as transient, matching 1.x retry semantics), a deprecated `errorCode` alias for `code`, and the deprecated `OAUTH_ERRORS` map. The OAuth
`InvalidRequestError` returns as `OAuthInvalidRequestError` (the original name is taken by a JSON-RPC protocol type).
54 changes: 23 additions & 31 deletions docs/migration-SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,41 +178,33 @@ if (error instanceof SdkHttpError) {

### OAuth error consolidation

Individual OAuth error classes replaced with single `OAuthError` class and `OAuthErrorCode` enum:

| v1 Class | v2 Equivalent |
| ------------------------------ | ---------------------------------------------------------- |
| `InvalidRequestError` | `OAuthError` with `OAuthErrorCode.InvalidRequest` |
| `InvalidClientError` | `OAuthError` with `OAuthErrorCode.InvalidClient` |
| `InvalidGrantError` | `OAuthError` with `OAuthErrorCode.InvalidGrant` |
| `UnauthorizedClientError` | `OAuthError` with `OAuthErrorCode.UnauthorizedClient` |
| `UnsupportedGrantTypeError` | `OAuthError` with `OAuthErrorCode.UnsupportedGrantType` |
| `InvalidScopeError` | `OAuthError` with `OAuthErrorCode.InvalidScope` |
| `AccessDeniedError` | `OAuthError` with `OAuthErrorCode.AccessDenied` |
| `ServerError` | `OAuthError` with `OAuthErrorCode.ServerError` |
| `TemporarilyUnavailableError` | `OAuthError` with `OAuthErrorCode.TemporarilyUnavailable` |
| `UnsupportedResponseTypeError` | `OAuthError` with `OAuthErrorCode.UnsupportedResponseType` |
| `UnsupportedTokenTypeError` | `OAuthError` with `OAuthErrorCode.UnsupportedTokenType` |
| `InvalidTokenError` | `OAuthError` with `OAuthErrorCode.InvalidToken` |
| `MethodNotAllowedError` | `OAuthError` with `OAuthErrorCode.MethodNotAllowed` |
| `TooManyRequestsError` | `OAuthError` with `OAuthErrorCode.TooManyRequests` |
| `InvalidClientMetadataError` | `OAuthError` with `OAuthErrorCode.InvalidClientMetadata` |
| `InsufficientScopeError` | `OAuthError` with `OAuthErrorCode.InsufficientScope` |
| `InvalidTargetError` | `OAuthError` with `OAuthErrorCode.InvalidTarget` |
| `CustomOAuthError` | `new OAuthError(customCode, message)` |

Removed: `OAUTH_ERRORS` constant.

Update OAuth error handling:
OAuth errors consolidated into `OAuthError` with `OAuthErrorCode` enum on `error.code`. The v1 subclasses remain exported as `@deprecated` compatibility wrappers, and SDK-produced errors are constructed as the matching subclass — `instanceof` checks from v1 continue to work
WITHOUT code changes, with ONE exception:

| v1 Class | v2 status |
| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidRequestError` (OAuth) | RENAMED → `OAuthInvalidRequestError` (name now owned by a JSON-RPC protocol type). Rewrite `instanceof InvalidRequestError` → `error.code === OAuthErrorCode.InvalidRequest` |
| All 16 other subclasses (`InvalidGrantError`, `ServerError`, …) and `CustomOAuthError` | Unchanged names, deprecated; prefer `error.code === OAuthErrorCode.X` |
| `OAUTH_ERRORS` | Still exported, deprecated; prefer `oauthErrorFromCode(code, message, errorUri?)` |

Semantics to preserve when rewriting v1 code:

1. `error.errorCode` → `error.code` (deprecated alias exists).
2. `error.name` is `'OAuthError'` for ALL OAuth errors including subclasses (v1 used the subclass name). Never match on subclass names.
3. UNKNOWN-CODE TRAP: v1 collapsed unrecognized error codes into `ServerError` (transient/retryable). v2 preserves the raw code on a plain `OAuthError`. Rewriting `instanceof ServerError` retry checks as `error.code === OAuthErrorCode.ServerError` SILENTLY DROPS retry for
non-standard codes (e.g. `invalid_refresh_token`). Use `isTransientOAuthError(error)` instead — it returns true for `server_error`, `temporarily_unavailable`, `too_many_requests`, AND any code not in `OAuthErrorCode` (v1 parity).

```typescript
// v1
import { InvalidClientError, InvalidGrantError } from '@modelcontextprotocol/client';
if (error instanceof InvalidClientError) { ... }
if (error instanceof ServerError) {
scheduleRetry();
}

// v2
import { OAuthError, OAuthErrorCode } from '@modelcontextprotocol/client';
if (error instanceof OAuthError && error.code === OAuthErrorCode.InvalidClient) { ... }
// v2 — equivalent semantics
import { isTransientOAuthError } from '@modelcontextprotocol/client';
if (isTransientOAuthError(error)) {
scheduleRetry();
}
```
Comment on lines +195 to +208
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-section rewrite in docs/migration-SKILL.md accidentally deleted unrelated migration content: the ## 6. McpServer API Changes heading, the variadic .tool()/.prompt()/.resource() removal note, the "IMPORTANT: v2 requires Standard Schema objects (wrap with z.object())" paragraph, and the "Unchanged APIs" paragraph. The file's numbering now jumps from section 5 to section 7, the ### Tools/### Prompts/### Resources subsections are orphaned, and step 4 of section 15 still references the now-missing "section 6" — please restore the deleted heading and paragraphs alongside the new OAuth content.

Extended reasoning...

What happened: The diff hunk in docs/migration-SKILL.md (lines ~178–230) replaces the old OAuth-error table with the new compatibility-focused prose. But the deleted span extends past the OAuth section boundary: it also removes (1) the **Unchanged APIs** (only import paths changed)… paragraph that closed the error-class section, (2) the ## 6. McpServer API Changes heading, (3) the sentence "The variadic .tool(), .prompt(), .resource() methods are removed. Use the register* methods with a config object.", and (4) the **IMPORTANT**: v2 requires schema objects implementing Standard Schema… paragraph (raw shapes no longer supported, wrap with z.object(), fromJsonSchema guidance). None of this content is re-added anywhere in the new text — the added lines are exclusively about OAuth errors.\n\nHow it manifests in the post-PR file: The top-level numbering jumps from ## 5. Removed / Renamed Type Aliases and Symbols (line 82) directly to ## 7. Headers API (line 294) — there is no section 6 anywhere in the file. The ### Tools, ### Prompts, ### Resources, ### Schema Migration Quick Reference, and ### Removed core exports subsections that previously belonged under "McpServer API Changes" now sit orphaned directly after the OAuth-error subsection, structurally nested under section 5. Grepping the post-PR file for McpServer API Changes, variadic, or Unchanged APIs returns nothing, confirming the content was deleted rather than moved.\n\nStep-by-step proof:\n1. Pre-PR, the file's structure was: ### OAuth error consolidation (table + rewrite example) → **Unchanged APIs** paragraph → ## 6. McpServer API Changes → variadic-removal sentence → IMPORTANT/Standard-Schema paragraph → ### Tools → … → ## 7. Headers API.\n2. The diff hunk @@ -178,52 +178,34 @@ removes everything from the old OAuth table through the IMPORTANT paragraph and inserts only the new OAuth compatibility prose, ending right before ### Tools.\n3. Post-PR, line 527 (## 15. Migration Steps, step 4) still reads "Replace .tool() / .prompt() / .resource() calls with registerTool / registerPrompt / registerResource per section 6" — a dangling reference to a section that no longer exists.\n4. The PR description scopes the change to OAuth error compatibility; nothing in it claims to restructure the McpServer API section, so this is collateral damage from the rewrite, not an intentional doc change.\n\nWhy it matters: This file is the LLM-targeted migration skill (migrate-v1-to-v2). The deleted variadic-method-removal statement and the Standard-Schema/z.object() requirement are core migration directives that an LLM consuming the skill relies on; some of that guidance partially survives in step 5 of section 15 and in the Schema Migration Quick Reference table, but the section heading, the variadic-removal statement, and the Unchanged-APIs paragraph are simply gone, and the broken numbering/orphaned subsections degrade the skill's structure.\n\nHow to fix: Re-insert, between the new OAuth code example and the ### Tools subsection, the previously existing content: the **Unchanged APIs** paragraph, the ## 6. McpServer API Changes heading, the variadic-removal sentence, and the IMPORTANT Standard-Schema paragraph. That restores the section numbering (5 → 6 → 7), re-parents the ### Tools/### Prompts/### Resources subsections, and makes the "per section 6" reference in section 15 valid again.


**Unchanged APIs** (only import paths changed): `Client` constructor and most methods, `McpServer` constructor, `server.connect()`, `server.close()`, all client transports (`StreamableHTTPClientTransport`, `SSEClientTransport`, `StdioClientTransport`), `StdioServerTransport`, all
Expand Down
90 changes: 32 additions & 58 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -798,85 +798,59 @@ The new design:

### OAuth error refactoring

The OAuth error classes have been consolidated into a single `OAuthError` class with an `OAuthErrorCode` enum.
OAuth errors are now built around a single `OAuthError` class with an `OAuthErrorCode` enum: `error.code` carries the OAuth error code (e.g. `'invalid_grant'`), including non-standard codes returned by real-world authorization servers.

#### Removed classes

The following individual error classes have been removed in favor of `OAuthError` with the appropriate code:

| v1 Class | v2 Equivalent |
| ------------------------------ | ----------------------------------------------------------------- |
| `InvalidRequestError` | `new OAuthError(OAuthErrorCode.InvalidRequest, message)` |
| `InvalidClientError` | `new OAuthError(OAuthErrorCode.InvalidClient, message)` |
| `InvalidGrantError` | `new OAuthError(OAuthErrorCode.InvalidGrant, message)` |
| `UnauthorizedClientError` | `new OAuthError(OAuthErrorCode.UnauthorizedClient, message)` |
| `UnsupportedGrantTypeError` | `new OAuthError(OAuthErrorCode.UnsupportedGrantType, message)` |
| `InvalidScopeError` | `new OAuthError(OAuthErrorCode.InvalidScope, message)` |
| `AccessDeniedError` | `new OAuthError(OAuthErrorCode.AccessDenied, message)` |
| `ServerError` | `new OAuthError(OAuthErrorCode.ServerError, message)` |
| `TemporarilyUnavailableError` | `new OAuthError(OAuthErrorCode.TemporarilyUnavailable, message)` |
| `UnsupportedResponseTypeError` | `new OAuthError(OAuthErrorCode.UnsupportedResponseType, message)` |
| `UnsupportedTokenTypeError` | `new OAuthError(OAuthErrorCode.UnsupportedTokenType, message)` |
| `InvalidTokenError` | `new OAuthError(OAuthErrorCode.InvalidToken, message)` |
| `MethodNotAllowedError` | `new OAuthError(OAuthErrorCode.MethodNotAllowed, message)` |
| `TooManyRequestsError` | `new OAuthError(OAuthErrorCode.TooManyRequests, message)` |
| `InvalidClientMetadataError` | `new OAuthError(OAuthErrorCode.InvalidClientMetadata, message)` |
| `InsufficientScopeError` | `new OAuthError(OAuthErrorCode.InsufficientScope, message)` |
| `InvalidTargetError` | `new OAuthError(OAuthErrorCode.InvalidTarget, message)` |
| `CustomOAuthError` | `new OAuthError(customCode, message)` |

The `OAUTH_ERRORS` constant has also been removed.

If you need the v1 OAuth error classes and `mcpAuthRouter` during migration, `@modelcontextprotocol/server-legacy/auth` provides a frozen copy:

```typescript
import { mcpAuthRouter, InvalidClientError } from '@modelcontextprotocol/server-legacy/auth';
```

This package is deprecated and will not receive new features. Use a dedicated OAuth provider in production.

**Before (v1):**
For backwards compatibility, the v1 error subclasses are still exported (as `@deprecated` wrappers around `OAuthError`), and errors produced by the SDK from server error responses are constructed as the matching subclass — so v1-style `instanceof` classification keeps working:

```typescript
import { InvalidClientError, InvalidGrantError, ServerError } from '@modelcontextprotocol/client';
import { InvalidGrantError, OAuthError, OAuthErrorCode } from '@modelcontextprotocol/client';

try {
await refreshToken();
} catch (error) {
if (error instanceof InvalidClientError) {
// Handle invalid client
} else if (error instanceof InvalidGrantError) {
// Handle invalid grant
} else if (error instanceof ServerError) {
// Handle server error
// v1 style — still works:
if (error instanceof InvalidGrantError) {
// Drop stored tokens, re-authorize
}
// v2 style — preferred:
if (error instanceof OAuthError && error.code === OAuthErrorCode.InvalidGrant) {
// Drop stored tokens, re-authorize
}
}
```

**After (v2):**
Differences to be aware of when migrating:

- **One rename:** the OAuth `InvalidRequestError` class is now `OAuthInvalidRequestError`, because v2 exports a JSON-RPC `InvalidRequestError` interface from the protocol types under the same name. Check `error.code === OAuthErrorCode.InvalidRequest` instead.
- **`error.name` is always `'OAuthError'`**, including for the subclasses (v1 used the subclass name). Use `instanceof` or `error.code` to distinguish kinds.
- **`error.errorCode` is deprecated** — it remains available as an alias, but use `error.code`.
- **Unknown error codes are preserved.** v1 collapsed unrecognized codes (e.g. `invalid_refresh_token`) into `ServerError`, which made them look transient/retryable. v2 keeps the raw code on a plain `OAuthError`. If you had retry logic keyed on `instanceof ServerError`, use the
new `isTransientOAuthError(error)` helper, which treats the RFC transient codes _and_ unknown codes as retryable — matching v1 behavior:

```typescript
import { OAuthError, OAuthErrorCode } from '@modelcontextprotocol/client';
import { isTransientOAuthError } from '@modelcontextprotocol/client';

try {
await refreshToken();
} catch (error) {
if (error instanceof OAuthError) {
switch (error.code) {
case OAuthErrorCode.InvalidClient:
// Handle invalid client
break;
case OAuthErrorCode.InvalidGrant:
// Handle invalid grant
break;
case OAuthErrorCode.ServerError:
// Handle server error
break;
}
if (isTransientOAuthError(error)) {
scheduleRetry();
} else {
throw error;
}
}
```

- `OAUTH_ERRORS` (the code → class map) is still exported, `@deprecated`. `oauthErrorFromCode(code, message, errorUri?)` is the supported way to construct an error from a code.

If you need the full v1 OAuth _server_ surface (`mcpAuthRouter` and friends) during migration, `@modelcontextprotocol/server-legacy/auth` provides a frozen copy:

```typescript
import { mcpAuthRouter } from '@modelcontextprotocol/server-legacy/auth';
```

This package is deprecated and will not receive new features. Use a dedicated OAuth provider in production. Note that error classes imported from `server-legacy/auth` are distinct from the `@modelcontextprotocol/client` ones — don't mix them in `instanceof` checks.

### Experimental tasks interception removed

The 2025-11 experimental tasks side-channel woven through `Protocol` has been removed in preparation for the SEP-2663 Tasks Extension. The following are gone with no in-place replacement:
Expand Down
14 changes: 9 additions & 5 deletions packages/client/src/client/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import type {
} from '@modelcontextprotocol/core';
import {
checkResourceAllowed,
isTransientOAuthError,
LATEST_PROTOCOL_VERSION,
OAuthClientInformationFullSchema,
OAuthError,
OAuthErrorCode,
oauthErrorFromCode,
OAuthErrorResponseSchema,
OAuthMetadataSchema,
OAuthProtectedResourceMetadataSchema,
Expand Down Expand Up @@ -527,7 +529,7 @@ export async function parseErrorResponse(input: Response | string): Promise<OAut
} catch (error) {
// Not a valid OAuth error response, but try to inform the user of the raw data anyway
const errorMessage = `${statusCode ? `HTTP ${statusCode}: ` : ''}Invalid OAuth error response: ${error}. Raw body: ${body}`;
return new OAuthError(OAuthErrorCode.ServerError, errorMessage);
return oauthErrorFromCode(OAuthErrorCode.ServerError, errorMessage);
}
}

Expand Down Expand Up @@ -710,7 +712,7 @@ async function authInternal(
const clientMetadataUrl = provider.clientMetadataUrl;

if (clientMetadataUrl && !isHttpsUrl(clientMetadataUrl)) {
throw new OAuthError(
throw oauthErrorFromCode(
OAuthErrorCode.InvalidClientMetadata,
`clientMetadataUrl must be a valid HTTPS URL with a non-root pathname, got: ${clientMetadataUrl}`
);
Expand Down Expand Up @@ -777,8 +779,10 @@ async function authInternal(
await provider.saveTokens(newTokens);
return 'AUTHORIZED';
} catch (error) {
// If this is a ServerError, or an unknown type, log it out and try to continue. Otherwise, escalate so we can fix things and retry.
if (!(error instanceof OAuthError) || error.code === OAuthErrorCode.ServerError) {
// If this is a transient OAuth error or an unrecognized code (which 1.x collapsed
// into ServerError), or not an OAuth error at all, log it out and try to continue
// with a fresh authorization flow. Otherwise, escalate so we can fix things and retry.
if (!(error instanceof OAuthError) || isTransientOAuthError(error)) {
// Could not refresh OAuth tokens
} else {
// Refresh failed for another reason, re-throw
Expand Down Expand Up @@ -819,7 +823,7 @@ async function authInternal(
*/
export function validateClientMetadataUrl(url: string | undefined): void {
if (url && !isHttpsUrl(url)) {
throw new OAuthError(
throw oauthErrorFromCode(
OAuthErrorCode.InvalidClientMetadata,
`clientMetadataUrl must be a valid HTTPS URL with a non-root pathname, got: ${url}`
);
Expand Down
Loading
Loading