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
16 changes: 16 additions & 0 deletions .changeset/token-provider-composable-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@modelcontextprotocol/client': minor
---

Add `AuthProvider` for composable bearer-token auth; transports adapt `OAuthClientProvider` automatically

- New `AuthProvider` interface: `{ token(): Promise<string | undefined>; onUnauthorized?(ctx): Promise<void> }`. Transports call `token()` before every request and `onUnauthorized()` on 401 (then retry once).
- Transport `authProvider` option now accepts `AuthProvider | OAuthClientProvider`. OAuth providers are adapted internally via `adaptOAuthProvider()` — no changes needed to existing `OAuthClientProvider` implementations.
- For simple bearer tokens (API keys, gateway-managed tokens, service accounts): `{ authProvider: { token: async () => myKey } }` — one-line object literal, no class.
- New `adaptOAuthProvider(provider)` export for explicit adaptation.
- New `handleOAuthUnauthorized(provider, ctx)` helper — the standard OAuth `onUnauthorized` behavior.
- New `isOAuthClientProvider()` type guard.
- New `UnauthorizedContext` type.
- Exported previously-internal auth helpers for building custom flows: `applyBasicAuth`, `applyPostAuth`, `applyPublicAuth`, `executeTokenRequest`.

Transports are simplified internally — ~50 lines of inline OAuth orchestration (auth() calls, WWW-Authenticate parsing, circuit-breaker state) moved into the adapter's `onUnauthorized()` implementation. `OAuthClientProvider` itself is unchanged.
16 changes: 14 additions & 2 deletions docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ A client connects to a server, discovers what it offers — tools, resources, pr
The examples below use these imports. Adjust based on which features and transport you need:

```ts source="../examples/client/src/clientGuide.examples.ts#imports"
import type { Prompt, Resource, Tool } from '@modelcontextprotocol/client';
import type { AuthProvider, Prompt, Resource, Tool } from '@modelcontextprotocol/client';
import {
applyMiddlewares,
Client,
Expand Down Expand Up @@ -113,7 +113,19 @@ console.log(systemPrompt);

## Authentication

MCP servers can require OAuth 2.0 authentication before accepting client connections (see [Authorization](https://modelcontextprotocol.io/specification/latest/basic/authorization) in the MCP specification). Pass an `authProvider` to {@linkcode @modelcontextprotocol/client!client/streamableHttp.StreamableHTTPClientTransport | StreamableHTTPClientTransport} to enable this — the SDK provides built-in providers for common machine-to-machine flows, or you can implement the full {@linkcode @modelcontextprotocol/client!client/auth.OAuthClientProvider | OAuthClientProvider} interface for user-facing OAuth.
MCP servers can require authentication before accepting client connections (see [Authorization](https://modelcontextprotocol.io/specification/latest/basic/authorization) in the MCP specification). Pass an {@linkcode @modelcontextprotocol/client!client/auth.AuthProvider | AuthProvider} to {@linkcode @modelcontextprotocol/client!client/streamableHttp.StreamableHTTPClientTransport | StreamableHTTPClientTransport}. The transport calls `token()` before every request and `onUnauthorized()` (if provided) on 401, then retries once.

### Bearer tokens

For servers that accept bearer tokens managed outside the SDK — API keys, tokens from a gateway or proxy, service-account credentials — implement only `token()`. With no `onUnauthorized()`, a 401 throws {@linkcode @modelcontextprotocol/client!client/auth.UnauthorizedError | UnauthorizedError} immediately:

```ts source="../examples/client/src/clientGuide.examples.ts#auth_tokenProvider"
const authProvider: AuthProvider = { token: async () => getStoredToken() };

const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'), { authProvider });
```

See [`simpleTokenProvider.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleTokenProvider.ts) for a complete runnable example.

### Client credentials

Expand Down
125 changes: 65 additions & 60 deletions docs/migration-SKILL.md

Large diffs are not rendered by default.

149 changes: 72 additions & 77 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ server.registerTool('ping', {
```

This applies to:

- `inputSchema` in `registerTool()`
- `outputSchema` in `registerTool()`
- `argsSchema` in `registerPrompt()`
Expand Down Expand Up @@ -339,25 +340,21 @@ Common method string replacements:

### `Protocol.request()`, `ctx.mcpReq.send()`, and `Client.callTool()` no longer take a schema parameter

The public `Protocol.request()`, `BaseContext.mcpReq.send()`, and `Client.callTool()` methods no longer accept a Zod result schema argument. The SDK now resolves the correct result schema internally based on the method name. This means you no longer need to import result schemas like `CallToolResultSchema` or `ElicitResultSchema` when making requests.
The public `Protocol.request()`, `BaseContext.mcpReq.send()`, and `Client.callTool()` methods no longer accept a Zod result schema argument. The SDK now resolves the correct result schema internally based on the method name. This means you no longer need to import result schemas
like `CallToolResultSchema` or `ElicitResultSchema` when making requests.

**`client.request()` — Before (v1):**

```typescript
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';

const result = await client.request(
{ method: 'tools/call', params: { name: 'my-tool', arguments: {} } },
CallToolResultSchema
);
const result = await client.request({ method: 'tools/call', params: { name: 'my-tool', arguments: {} } }, CallToolResultSchema);
```

**After (v2):**

```typescript
const result = await client.request(
{ method: 'tools/call', params: { name: 'my-tool', arguments: {} } }
);
const result = await client.request({ method: 'tools/call', params: { name: 'my-tool', arguments: {} } });
```

**`ctx.mcpReq.send()` — Before (v1):**
Expand Down Expand Up @@ -390,10 +387,7 @@ server.setRequestHandler('tools/call', async (request, ctx) => {
```typescript
import { CompatibilityCallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';

const result = await client.callTool(
{ name: 'my-tool', arguments: {} },
CompatibilityCallToolResultSchema
);
const result = await client.callTool({ name: 'my-tool', arguments: {} }, CompatibilityCallToolResultSchema);
```

**After (v2):**
Expand Down Expand Up @@ -452,43 +446,43 @@ import { JSONRPCErrorResponse, ResourceTemplateReference, isJSONRPCErrorResponse

The `RequestHandlerExtra` type has been replaced with a structured context type hierarchy using nested groups:

| v1 | v2 |
|----|-----|
| v1 | v2 |
| ---------------------------------------- | ---------------------------------------------------------------------- |
| `RequestHandlerExtra` (flat, all fields) | `ServerContext` (server handlers) or `ClientContext` (client handlers) |
| `extra` parameter name | `ctx` parameter name |
| `extra.signal` | `ctx.mcpReq.signal` |
| `extra.requestId` | `ctx.mcpReq.id` |
| `extra._meta` | `ctx.mcpReq._meta` |
| `extra.sendRequest(...)` | `ctx.mcpReq.send(...)` |
| `extra.sendNotification(...)` | `ctx.mcpReq.notify(...)` |
| `extra.authInfo` | `ctx.http?.authInfo` |
| `extra.requestInfo` | `ctx.http?.req` (only on `ServerContext`) |
| `extra.closeSSEStream` | `ctx.http?.closeSSE` (only on `ServerContext`) |
| `extra.closeStandaloneSSEStream` | `ctx.http?.closeStandaloneSSE` (only on `ServerContext`) |
| `extra.sessionId` | `ctx.sessionId` |
| `extra.taskStore` | `ctx.task?.store` |
| `extra.taskId` | `ctx.task?.id` |
| `extra.taskRequestedTtl` | `ctx.task?.requestedTtl` |
| `extra` parameter name | `ctx` parameter name |
| `extra.signal` | `ctx.mcpReq.signal` |
| `extra.requestId` | `ctx.mcpReq.id` |
| `extra._meta` | `ctx.mcpReq._meta` |
| `extra.sendRequest(...)` | `ctx.mcpReq.send(...)` |
| `extra.sendNotification(...)` | `ctx.mcpReq.notify(...)` |
| `extra.authInfo` | `ctx.http?.authInfo` |
| `extra.requestInfo` | `ctx.http?.req` (only on `ServerContext`) |
| `extra.closeSSEStream` | `ctx.http?.closeSSE` (only on `ServerContext`) |
| `extra.closeStandaloneSSEStream` | `ctx.http?.closeStandaloneSSE` (only on `ServerContext`) |
| `extra.sessionId` | `ctx.sessionId` |
| `extra.taskStore` | `ctx.task?.store` |
| `extra.taskId` | `ctx.task?.id` |
| `extra.taskRequestedTtl` | `ctx.task?.requestedTtl` |

**Before (v1):**

```typescript
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
const headers = extra.requestInfo?.headers;
const taskStore = extra.taskStore;
await extra.sendNotification({ method: 'notifications/progress', params: { progressToken: 'abc', progress: 50, total: 100 } });
return { content: [{ type: 'text', text: 'result' }] };
const headers = extra.requestInfo?.headers;
const taskStore = extra.taskStore;
await extra.sendNotification({ method: 'notifications/progress', params: { progressToken: 'abc', progress: 50, total: 100 } });
return { content: [{ type: 'text', text: 'result' }] };
});
```

**After (v2):**

```typescript
server.setRequestHandler('tools/call', async (request, ctx) => {
const headers = ctx.http?.req?.headers;
const taskStore = ctx.task?.store;
await ctx.mcpReq.notify({ method: 'notifications/progress', params: { progressToken: 'abc', progress: 50, total: 100 } });
return { content: [{ type: 'text', text: 'result' }] };
const headers = ctx.http?.req?.headers;
const taskStore = ctx.task?.store;
await ctx.mcpReq.notify({ method: 'notifications/progress', params: { progressToken: 'abc', progress: 50, total: 100 } });
return { content: [{ type: 'text', text: 'result' }] };
});
```

Expand All @@ -504,22 +498,22 @@ Context fields are organized into 4 groups:

```typescript
server.setRequestHandler('tools/call', async (request, ctx) => {
// Send a log message (respects client's log level filter)
await ctx.mcpReq.log('info', 'Processing tool call', 'my-logger');

// Request client to sample an LLM
const samplingResult = await ctx.mcpReq.requestSampling({
messages: [{ role: 'user', content: { type: 'text', text: 'Hello' } }],
maxTokens: 100,
});

// Elicit user input via a form
const elicitResult = await ctx.mcpReq.elicitInput({
message: 'Please provide details',
requestedSchema: { type: 'object', properties: { name: { type: 'string' } } },
});

return { content: [{ type: 'text', text: 'done' }] };
// Send a log message (respects client's log level filter)
await ctx.mcpReq.log('info', 'Processing tool call', 'my-logger');

// Request client to sample an LLM
const samplingResult = await ctx.mcpReq.requestSampling({
messages: [{ role: 'user', content: { type: 'text', text: 'Hello' } }],
maxTokens: 100
});

// Elicit user input via a form
const elicitResult = await ctx.mcpReq.elicitInput({
message: 'Please provide details',
requestedSchema: { type: 'object', properties: { name: { type: 'string' } } }
});

return { content: [{ type: 'text', text: 'done' }] };
});
```

Expand Down Expand Up @@ -581,21 +575,21 @@ try {

The new `SdkErrorCode` enum contains string-valued codes for local SDK errors:

| Code | Description |
| ------------------------------------------------- | ------------------------------------------ |
| `SdkErrorCode.NotConnected` | Transport is not connected |
| `SdkErrorCode.AlreadyConnected` | Transport is already connected |
| `SdkErrorCode.NotInitialized` | Protocol is not initialized |
| `SdkErrorCode.CapabilityNotSupported` | Required capability is not supported |
| `SdkErrorCode.RequestTimeout` | Request timed out waiting for response |
| `SdkErrorCode.ConnectionClosed` | Connection was closed |
| `SdkErrorCode.SendFailed` | Failed to send message |
| `SdkErrorCode.ClientHttpNotImplemented` | HTTP POST request failed |
| `SdkErrorCode.ClientHttpAuthentication` | Server returned 401 after successful auth |
| `SdkErrorCode.ClientHttpForbidden` | Server returned 403 after trying upscoping |
| `SdkErrorCode.ClientHttpUnexpectedContent` | Unexpected content type in HTTP response |
| `SdkErrorCode.ClientHttpFailedToOpenStream` | Failed to open SSE stream |
| `SdkErrorCode.ClientHttpFailedToTerminateSession` | Failed to terminate session |
| Code | Description |
| ------------------------------------------------- | ------------------------------------------- |
| `SdkErrorCode.NotConnected` | Transport is not connected |
| `SdkErrorCode.AlreadyConnected` | Transport is already connected |
| `SdkErrorCode.NotInitialized` | Protocol is not initialized |
| `SdkErrorCode.CapabilityNotSupported` | Required capability is not supported |
| `SdkErrorCode.RequestTimeout` | Request timed out waiting for response |
| `SdkErrorCode.ConnectionClosed` | Connection was closed |
| `SdkErrorCode.SendFailed` | Failed to send message |
| `SdkErrorCode.ClientHttpNotImplemented` | HTTP POST request failed |
| `SdkErrorCode.ClientHttpAuthentication` | Server returned 401 after re-authentication |
| `SdkErrorCode.ClientHttpForbidden` | Server returned 403 after trying upscoping |
| `SdkErrorCode.ClientHttpUnexpectedContent` | Unexpected content type in HTTP response |
| `SdkErrorCode.ClientHttpFailedToOpenStream` | Failed to open SSE stream |
| `SdkErrorCode.ClientHttpFailedToTerminateSession` | Failed to terminate session |

#### `StreamableHTTPError` removed

Expand Down Expand Up @@ -626,7 +620,7 @@ try {
if (error instanceof SdkError) {
switch (error.code) {
case SdkErrorCode.ClientHttpAuthentication:
console.log('Auth failed after completing auth flow');
console.log('Auth failed — server rejected token after re-auth');
break;
case SdkErrorCode.ClientHttpForbidden:
console.log('Forbidden after upscoping attempt');
Expand All @@ -646,7 +640,8 @@ try {

#### Why this change?

Previously, `ErrorCode.RequestTimeout` (-32001) and `ErrorCode.ConnectionClosed` (-32000) were used for local timeout/connection errors. However, these errors never cross the wire as JSON-RPC responses - they are rejected locally. Using protocol error codes for local errors was semantically inconsistent.
Previously, `ErrorCode.RequestTimeout` (-32001) and `ErrorCode.ConnectionClosed` (-32000) were used for local timeout/connection errors. However, these errors never cross the wire as JSON-RPC responses - they are rejected locally. Using protocol error codes for local errors was
semantically inconsistent.

The new design:

Expand Down Expand Up @@ -743,11 +738,11 @@ This means Cloudflare Workers users no longer need to explicitly pass the valida
import { McpServer, CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server';

const server = new McpServer(
{ name: 'my-server', version: '1.0.0' },
{
capabilities: { tools: {} },
jsonSchemaValidator: new CfWorkerJsonSchemaValidator() // Required in v1
}
{ name: 'my-server', version: '1.0.0' },
{
capabilities: { tools: {} },
jsonSchemaValidator: new CfWorkerJsonSchemaValidator() // Required in v1
}
);
```

Expand All @@ -757,9 +752,9 @@ const server = new McpServer(
import { McpServer } from '@modelcontextprotocol/server';

const server = new McpServer(
{ name: 'my-server', version: '1.0.0' },
{ capabilities: { tools: {} } }
// Validator auto-selected based on runtime
{ name: 'my-server', version: '1.0.0' },
{ capabilities: { tools: {} } }
// Validator auto-selected based on runtime
);
```

Expand Down
13 changes: 12 additions & 1 deletion examples/client/src/clientGuide.examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

//#region imports
import type { Prompt, Resource, Tool } from '@modelcontextprotocol/client';
import type { AuthProvider, Prompt, Resource, Tool } from '@modelcontextprotocol/client';
import {
applyMiddlewares,
Client,
Expand Down Expand Up @@ -107,6 +107,16 @@ async function serverInstructions_basic(client: Client) {
// Authentication
// ---------------------------------------------------------------------------

/** Example: Minimal AuthProvider for bearer auth with externally-managed tokens. */
async function auth_tokenProvider(getStoredToken: () => Promise<string>) {
//#region auth_tokenProvider
const authProvider: AuthProvider = { token: async () => getStoredToken() };

const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'), { authProvider });
//#endregion auth_tokenProvider
return transport;
}

/** Example: Client credentials auth for service-to-service communication. */
async function auth_clientCredentials() {
//#region auth_clientCredentials
Expand Down Expand Up @@ -540,6 +550,7 @@ void connect_stdio;
void connect_sseFallback;
void disconnect_streamableHttp;
void serverInstructions_basic;
void auth_tokenProvider;
void auth_clientCredentials;
void auth_privateKeyJwt;
void auth_crossAppAccess;
Expand Down
Loading
Loading