Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/auth-surface-delta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@modelcontextprotocol/client': minor
'@modelcontextprotocol/core': minor
---

Add the public surface for the 2026-07-28 authorization requirements. New `AuthOptions` type names the `auth()` options object and adds `iss` and `skipIssuerMetadataValidation` fields. `OAuthClientProvider.clientInformation()` / `.saveClientInformation()` / `.tokens()` / `.saveTokens()` accept an optional `OAuthClientInformationContext` carrying the authorization server's `issuer` so providers can key persisted credentials per authorization server. New `StoredOAuthTokens` / `StoredOAuthClientInformation` aliases add an `issuer` stamp field on top of the wire types (kept off the wire schemas so an authorization server cannot populate it) and become the parameter/return types of the credential methods. New `OAuthClientFlowError` base class in `authErrors.ts` for the flow-specific error classes that follow. All changes are additive — existing `OAuthClientProvider` implementations compile unchanged; the new fields are inert until the behavior changes that follow wire them up.
21 changes: 21 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1474,6 +1474,27 @@ The following APIs are unchanged between v1 and v2 (only the import paths change
`Session not found` — unchanged from v1. Note that this use of `-32001` is an SDK convention, not a spec-assigned error code, and it is expected to be re-derived as error handling for the 2026 protocol revision (`2026-07-28`) is adopted. Avoid hard-coding the `-32001` code in
client logic; key off the HTTP `404` status instead.

## Authorization (2026-07-28 spec)

The 2026-07-28 protocol revision adds client-side authorization requirements (RFC 9207 `iss` validation, RFC 8414 §3.3 issuer-echo, per-authorization-server credential isolation, scope step-up, DCR `application_type`, and refresh-token guidance). The SDK adds the public surface for these now and will implement the parts that land in SDK code (defaulting them on) as the SEP-2468/2352/2350/837/2207 behavior changes land; the parts that live in your `OAuthClientProvider` implementation, your `clientMetadata`, or your host UI are listed under [Conformance obligations for `OAuthClientProvider` implementers](#conformance-obligations-for-oauthclientprovider-implementers).

### `auth()` options are now `AuthOptions`

The inline options object on `auth()` is now the named `AuthOptions` type, exported from `@modelcontextprotocol/client`. Existing call sites need no change. New fields (both currently inert — the validation behavior they feed lands in the follow-up changes tracked by SEP-2468):

- `iss?: string` — the form-urldecoded `iss` query parameter from the authorization callback. Pass it alongside `authorizationCode`; it is forwarded to RFC 9207 issuer validation once that lands.
- `skipIssuerMetadataValidation?: boolean` — opt-out for the RFC 8414 §3.3 issuer-echo check during discovery. **Security-weakening**; use only with authorization servers known to publish a mismatched `issuer`.

### `OAuthClientProvider` credential methods receive an `issuer` context

`clientInformation(ctx?)`, `saveClientInformation(info, ctx?)`, `tokens(ctx?)`, and `saveTokens(tokens, ctx?)` now receive an optional `OAuthClientInformationContext` parameter carrying `{ issuer: string }` — the authorization server's `issuer` identifier. Providers that persist credentials should key storage by this value so that credentials registered with one authorization server are never sent to another. Providers with a single credential set may ignore the parameter; existing implementations compile unchanged. The SDK does not yet pass this argument; it begins doing so when the SEP-2352 behavior change lands.

New TypeScript-only aliases `StoredOAuthTokens` and `StoredOAuthClientInformation` add an optional `issuer?: string` field on top of the wire types and are used as the parameter/return types of `tokens()` / `saveTokens()` and `clientInformation()` / `saveClientInformation()`. The `issuer` field is **not** part of the RFC 6749/7591 wire responses and is intentionally absent from `OAuthTokensSchema` / `OAuthClientInformationSchema` so an authorization server cannot populate it; once the SEP-2352 behavior change lands the SDK will stamp it onto credentials before calling `saveTokens` / `saveClientInformation`. Provider implementations should round-trip it unchanged. The field is currently inert.

Comment thread
claude[bot] marked this conversation as resolved.
### Conformance obligations for `OAuthClientProvider` implementers

<!-- Filled in as the SEP-2468/2352/2350/837/2207 behavior PRs land. -->

## Using an LLM to migrate your code

An LLM-optimized version of this guide is available at [`docs/migration-SKILL.md`](migration-SKILL.md). It contains dense mapping tables designed for tools like Claude Code to mechanically apply all the changes described above. You can paste it into your LLM context or load it as
Expand Down
108 changes: 80 additions & 28 deletions packages/client/src/client/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
OAuthClientMetadata,
OAuthMetadata,
OAuthProtectedResourceMetadata,
OAuthTokens
OAuthTokens,
StoredOAuthClientInformation,
StoredOAuthTokens
} from '@modelcontextprotocol/core';
import {
checkResourceAllowed,
Expand Down Expand Up @@ -82,6 +84,22 @@
onUnauthorized?(ctx: UnauthorizedContext): Promise<void>;
}

/**
* Context passed to the credential-persistence methods on
* {@linkcode OAuthClientProvider} — `clientInformation` / `saveClientInformation`
* and `tokens` / `saveTokens`. Carries the resolved authorization-server `issuer`
* so provider implementations can key persisted credentials per authorization
* server (RFC 6749 §2.2 — client identifiers are unique to the AS that issued
* them). Providers that store a single credential set may ignore it.
*/
export interface OAuthClientInformationContext {
/**
* The authorization server's `issuer` identifier from its validated metadata
* document, used as the binding key for persisted credentials.
*/
issuer: string;
}
Comment thread
felixweinberger marked this conversation as resolved.

/**
* Type guard distinguishing `OAuthClientProvider` from a minimal `AuthProvider`.
* Transports use this at construction time to classify the `authProvider` option.
Expand Down Expand Up @@ -167,8 +185,14 @@
* Loads information about this OAuth client, as registered already with the
* server, or returns `undefined` if the client is not registered with the
* server.
*
* @param ctx - Carries the resolved authorization-server `issuer`. Providers
* that persist credentials per authorization server should return the entry
* keyed by `ctx.issuer`. Providers with a single credential set may ignore it.
*/
clientInformation(): OAuthClientInformationMixed | undefined | Promise<OAuthClientInformationMixed | undefined>;
clientInformation(
ctx?: OAuthClientInformationContext
): StoredOAuthClientInformation | undefined | Promise<StoredOAuthClientInformation | undefined>;

/**
* If implemented, this permits the OAuth client to dynamically register with
Expand All @@ -177,20 +201,32 @@
*
* This method is not required to be implemented if client information is
* statically known (e.g., pre-registered).
*
* @param ctx - Carries the resolved authorization-server `issuer`. Providers
* that persist credentials per authorization server should store the entry
* keyed by `ctx.issuer`.
*/
saveClientInformation?(clientInformation: OAuthClientInformationMixed): void | Promise<void>;
saveClientInformation?(clientInformation: StoredOAuthClientInformation, ctx?: OAuthClientInformationContext): void | Promise<void>;

/**
* Loads any existing OAuth tokens for the current session, or returns
* `undefined` if there are no saved tokens.
*
* @param ctx - Carries the resolved authorization-server `issuer`. Providers
* that persist tokens per authorization server should return the entry
* keyed by `ctx.issuer`. Providers with a single token set may ignore it.
*/
tokens(): OAuthTokens | undefined | Promise<OAuthTokens | undefined>;
tokens(ctx?: OAuthClientInformationContext): StoredOAuthTokens | undefined | Promise<StoredOAuthTokens | undefined>;

/**
* Stores new OAuth tokens for the current session, after a successful
* authorization.
*
* @param ctx - Carries the resolved authorization-server `issuer`. Providers
* that persist tokens per authorization server should store the entry
* keyed by `ctx.issuer`.
*/
saveTokens(tokens: OAuthTokens): void | Promise<void>;
saveTokens(tokens: StoredOAuthTokens, ctx?: OAuthClientInformationContext): void | Promise<void>;

/**
* Invoked to redirect the user agent to the given URL to begin the authorization flow.
Expand Down Expand Up @@ -531,22 +567,50 @@
}
}

/**
* Options for {@linkcode auth}. The full OAuth flow orchestrator's input.
*/
export interface AuthOptions {
/** The MCP server URL — the protected resource the flow authorizes against. */
serverUrl: string | URL;
/**
* The authorization code returned by the authorization server on the redirect
* callback. When set, {@linkcode auth} exchanges it for tokens; when unset,
* {@linkcode auth} runs discovery and either refreshes or initiates redirect.
*/
authorizationCode?: string;
/**
* The form-urldecoded `iss` query parameter from the authorization callback,
* if present. Passed through to RFC 9207 §2.4 issuer validation alongside
* `authorizationCode`. The validation behavior is wired up in a follow-up
* change; this field is currently inert.
*/
iss?: string;

Check failure on line 588 in packages/client/src/client/auth.ts

View check run for this annotation

Claude / Claude Code Review

iss has no path through transport finishAuth(), the documented callback entry point

AuthOptions.iss gives auth() callers a way to forward the RFC 9207 callback parameter, but the documented entry point for completing the authorization-code callback — StreamableHTTPClientTransport.finishAuth(code) / SSEClientTransport.finishAuth(code) — is unchanged and calls auth() with no way to inject iss, so transport-based clients have no path to RFC 9207 mix-up protection. Since this PR is meant to be the complete surface delta, consider extending finishAuth (e.g. finishAuth(code, { iss }?
Comment thread
claude[bot] marked this conversation as resolved.
Comment thread
felixweinberger marked this conversation as resolved.
/** Scope to request; computed by Scope Selection Strategy when omitted. */
scope?: string;
/** Explicit `resource_metadata` URL from a `WWW-Authenticate` challenge. */
resourceMetadataUrl?: URL;
/** Custom `fetch` implementation. */
fetchFn?: FetchLike;
/**
* Opt-out for the RFC 8414 §3.3 issuer-echo check during authorization
* server discovery. Disabling it is **security-weakening** and intended only
* for authorization servers known to publish a mismatched `issuer`. The
* check itself is wired up in a follow-up change; this flag is currently
* inert.
*
* @default false
*/
skipIssuerMetadataValidation?: boolean;
}

/**
* Orchestrates the full auth flow with a server.
*
* This can be used as a single entry point for all authorization functionality,
* instead of linking together the other lower-level functions in this module.
*/
export async function auth(
provider: OAuthClientProvider,
options: {
serverUrl: string | URL;
authorizationCode?: string;
scope?: string;
resourceMetadataUrl?: URL;
fetchFn?: FetchLike;
}
): Promise<AuthResult> {
export async function auth(provider: OAuthClientProvider, options: AuthOptions): Promise<AuthResult> {
try {
return await authInternal(provider, options);
} catch (error) {
Expand Down Expand Up @@ -600,19 +664,7 @@

async function authInternal(
provider: OAuthClientProvider,
{
serverUrl,
authorizationCode,
scope,
resourceMetadataUrl,
fetchFn
}: {
serverUrl: string | URL;
authorizationCode?: string;
scope?: string;
resourceMetadataUrl?: URL;
fetchFn?: FetchLike;
}
{ serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }: AuthOptions
): Promise<AuthResult> {
// Check if the provider has cached discovery state to skip discovery
const cachedState = await provider.discoveryState?.();
Expand Down
24 changes: 24 additions & 0 deletions packages/client/src/client/authErrors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Error classes thrown by the OAuth client flow ({@linkcode auth} and helpers).
*
* Each behavior change in the 2026-07-28 authorization requirements adds its
* dedicated error class to this module so callers can `instanceof`-dispatch on
* the failure mode without string-matching messages.
*/

/**
* Base class for the OAuth-client-flow error family. Concrete subclasses are
* added to this module alongside the SEP-2468/837/2207/2350/2352 behavior
* changes that throw them, so callers can catch the whole family with a single
* `instanceof OAuthClientFlowError` guard once those land.
*
* @remarks Nothing in the SDK throws this base class directly. In the release
* that introduces it no subclass exists yet — the guard is a forward-compat
* hook and will not match anything until the first behavior change ships.
*/
export class OAuthClientFlowError extends Error {
constructor(message: string) {
super(message);
this.name = new.target.name;
}
}
Comment thread
claude[bot] marked this conversation as resolved.
3 changes: 3 additions & 0 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@

export type {
AddClientAuthentication,
AuthOptions,
AuthProvider,
AuthResult,
ClientAuthMethod,
OAuthClientInformationContext,
OAuthClientProvider,
OAuthDiscoveryState,
OAuthServerInfo
Expand All @@ -37,6 +39,7 @@ export {
UnauthorizedError,
validateClientMetadataUrl
} from './client/auth.js';
export { OAuthClientFlowError } from './client/authErrors.js';
export type {
AssertionCallback,
ClientCredentialsProviderOptions,
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/exports/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ export type {
OAuthTokenRevocationRequest,
OAuthTokens,
OpenIdProviderDiscoveryMetadata,
OpenIdProviderMetadata
OpenIdProviderMetadata,
StoredOAuthClientInformation,
StoredOAuthTokens
} from '../../shared/auth.js';

// Auth utilities
Expand Down
20 changes: 20 additions & 0 deletions packages/core/src/shared/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,26 @@ export type OAuthClientMetadata = z.infer<typeof OAuthClientMetadataSchema>;
export type OAuthClientInformation = z.infer<typeof OAuthClientInformationSchema>;
export type OAuthClientInformationFull = z.infer<typeof OAuthClientInformationFullSchema>;
export type OAuthClientInformationMixed = OAuthClientInformation | OAuthClientInformationFull;

/**
* {@linkcode OAuthTokens} as persisted by an `OAuthClientProvider`. Adds an
* SDK-stamped authorization-server `issuer` identifier so stored tokens are
* bound to the AS that issued them. The `issuer` field is **not** part of the
* RFC 6749 wire response and is intentionally absent from the wire-response
* schema; the client SDK writes it before calling `saveTokens`.
*/
export type StoredOAuthTokens = OAuthTokens & { issuer?: string };

/**
* {@linkcode OAuthClientInformationMixed} as persisted by an
* `OAuthClientProvider`. Adds an SDK-stamped authorization-server `issuer`
* identifier so stored client credentials are bound to the AS that issued them.
* The `issuer` field is **not** part of the RFC 7591 wire response and is
* intentionally absent from the wire-response schema; the client SDK writes it
* before calling `saveClientInformation`.
*/
export type StoredOAuthClientInformation = OAuthClientInformationMixed & { issuer?: string };
Comment thread
felixweinberger marked this conversation as resolved.

export type OAuthClientRegistrationError = z.infer<typeof OAuthClientRegistrationErrorSchema>;
export type OAuthTokenRevocationRequest = z.infer<typeof OAuthTokenRevocationRequestSchema>;
export type OAuthProtectedResourceMetadata = z.infer<typeof OAuthProtectedResourceMetadataSchema>;
Expand Down
Loading