Skip to content

Commit e563e63

Browse files
feat: introduce minimal AuthProvider interface with OAuthClientProvider adapter (#1710)
1 parent 13a0d34 commit e563e63

File tree

13 files changed

+1102
-293
lines changed

13 files changed

+1102
-293
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@modelcontextprotocol/client': minor
3+
---
4+
5+
Add `AuthProvider` for composable bearer-token auth; transports adapt `OAuthClientProvider` automatically
6+
7+
- New `AuthProvider` interface: `{ token(): Promise<string | undefined>; onUnauthorized?(ctx): Promise<void> }`. Transports call `token()` before every request and `onUnauthorized()` on 401 (then retry once).
8+
- Transport `authProvider` option now accepts `AuthProvider | OAuthClientProvider`. OAuth providers are adapted internally via `adaptOAuthProvider()` — no changes needed to existing `OAuthClientProvider` implementations.
9+
- For simple bearer tokens (API keys, gateway-managed tokens, service accounts): `{ authProvider: { token: async () => myKey } }` — one-line object literal, no class.
10+
- New `adaptOAuthProvider(provider)` export for explicit adaptation.
11+
- New `handleOAuthUnauthorized(provider, ctx)` helper — the standard OAuth `onUnauthorized` behavior.
12+
- New `isOAuthClientProvider()` type guard.
13+
- New `UnauthorizedContext` type.
14+
- Exported previously-internal auth helpers for building custom flows: `applyBasicAuth`, `applyPostAuth`, `applyPublicAuth`, `executeTokenRequest`.
15+
16+
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.

docs/client.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ A client connects to a server, discovers what it offers — tools, resources, pr
1313
The examples below use these imports. Adjust based on which features and transport you need:
1414

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

114114
## Authentication
115115

116-
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.
116+
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.
117+
118+
### Bearer tokens
119+
120+
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:
121+
122+
```ts source="../examples/client/src/clientGuide.examples.ts#auth_tokenProvider"
123+
const authProvider: AuthProvider = { token: async () => getStoredToken() };
124+
125+
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'), { authProvider });
126+
```
127+
128+
See [`simpleTokenProvider.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleTokenProvider.ts) for a complete runnable example.
117129

118130
### Client credentials
119131

docs/migration-SKILL.md

Lines changed: 62 additions & 57 deletions
Large diffs are not rendered by default.

docs/migration.md

Lines changed: 72 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ server.registerTool('ping', {
264264
```
265265

266266
This applies to:
267+
267268
- `inputSchema` in `registerTool()`
268269
- `outputSchema` in `registerTool()`
269270
- `argsSchema` in `registerPrompt()`
@@ -360,25 +361,21 @@ Common method string replacements:
360361

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

363-
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.
364+
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
365+
like `CallToolResultSchema` or `ElicitResultSchema` when making requests.
364366

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

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

370-
const result = await client.request(
371-
{ method: 'tools/call', params: { name: 'my-tool', arguments: {} } },
372-
CallToolResultSchema
373-
);
372+
const result = await client.request({ method: 'tools/call', params: { name: 'my-tool', arguments: {} } }, CallToolResultSchema);
374373
```
375374

376375
**After (v2):**
377376

378377
```typescript
379-
const result = await client.request(
380-
{ method: 'tools/call', params: { name: 'my-tool', arguments: {} } }
381-
);
378+
const result = await client.request({ method: 'tools/call', params: { name: 'my-tool', arguments: {} } });
382379
```
383380

384381
**`ctx.mcpReq.send()` — Before (v1):**
@@ -411,10 +408,7 @@ server.setRequestHandler('tools/call', async (request, ctx) => {
411408
```typescript
412409
import { CompatibilityCallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
413410

414-
const result = await client.callTool(
415-
{ name: 'my-tool', arguments: {} },
416-
CompatibilityCallToolResultSchema
417-
);
411+
const result = await client.callTool({ name: 'my-tool', arguments: {} }, CompatibilityCallToolResultSchema);
418412
```
419413

420414
**After (v2):**
@@ -473,43 +467,43 @@ import { JSONRPCErrorResponse, ResourceTemplateReference, isJSONRPCErrorResponse
473467

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

476-
| v1 | v2 |
477-
|----|-----|
470+
| v1 | v2 |
471+
| ---------------------------------------- | ---------------------------------------------------------------------- |
478472
| `RequestHandlerExtra` (flat, all fields) | `ServerContext` (server handlers) or `ClientContext` (client handlers) |
479-
| `extra` parameter name | `ctx` parameter name |
480-
| `extra.signal` | `ctx.mcpReq.signal` |
481-
| `extra.requestId` | `ctx.mcpReq.id` |
482-
| `extra._meta` | `ctx.mcpReq._meta` |
483-
| `extra.sendRequest(...)` | `ctx.mcpReq.send(...)` |
484-
| `extra.sendNotification(...)` | `ctx.mcpReq.notify(...)` |
485-
| `extra.authInfo` | `ctx.http?.authInfo` |
486-
| `extra.requestInfo` | `ctx.http?.req` (only on `ServerContext`) |
487-
| `extra.closeSSEStream` | `ctx.http?.closeSSE` (only on `ServerContext`) |
488-
| `extra.closeStandaloneSSEStream` | `ctx.http?.closeStandaloneSSE` (only on `ServerContext`) |
489-
| `extra.sessionId` | `ctx.sessionId` |
490-
| `extra.taskStore` | `ctx.task?.store` |
491-
| `extra.taskId` | `ctx.task?.id` |
492-
| `extra.taskRequestedTtl` | `ctx.task?.requestedTtl` |
473+
| `extra` parameter name | `ctx` parameter name |
474+
| `extra.signal` | `ctx.mcpReq.signal` |
475+
| `extra.requestId` | `ctx.mcpReq.id` |
476+
| `extra._meta` | `ctx.mcpReq._meta` |
477+
| `extra.sendRequest(...)` | `ctx.mcpReq.send(...)` |
478+
| `extra.sendNotification(...)` | `ctx.mcpReq.notify(...)` |
479+
| `extra.authInfo` | `ctx.http?.authInfo` |
480+
| `extra.requestInfo` | `ctx.http?.req` (only on `ServerContext`) |
481+
| `extra.closeSSEStream` | `ctx.http?.closeSSE` (only on `ServerContext`) |
482+
| `extra.closeStandaloneSSEStream` | `ctx.http?.closeStandaloneSSE` (only on `ServerContext`) |
483+
| `extra.sessionId` | `ctx.sessionId` |
484+
| `extra.taskStore` | `ctx.task?.store` |
485+
| `extra.taskId` | `ctx.task?.id` |
486+
| `extra.taskRequestedTtl` | `ctx.task?.requestedTtl` |
493487

494488
**Before (v1):**
495489

496490
```typescript
497491
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
498-
const headers = extra.requestInfo?.headers;
499-
const taskStore = extra.taskStore;
500-
await extra.sendNotification({ method: 'notifications/progress', params: { progressToken: 'abc', progress: 50, total: 100 } });
501-
return { content: [{ type: 'text', text: 'result' }] };
492+
const headers = extra.requestInfo?.headers;
493+
const taskStore = extra.taskStore;
494+
await extra.sendNotification({ method: 'notifications/progress', params: { progressToken: 'abc', progress: 50, total: 100 } });
495+
return { content: [{ type: 'text', text: 'result' }] };
502496
});
503497
```
504498

505499
**After (v2):**
506500

507501
```typescript
508502
server.setRequestHandler('tools/call', async (request, ctx) => {
509-
const headers = ctx.http?.req?.headers;
510-
const taskStore = ctx.task?.store;
511-
await ctx.mcpReq.notify({ method: 'notifications/progress', params: { progressToken: 'abc', progress: 50, total: 100 } });
512-
return { content: [{ type: 'text', text: 'result' }] };
503+
const headers = ctx.http?.req?.headers;
504+
const taskStore = ctx.task?.store;
505+
await ctx.mcpReq.notify({ method: 'notifications/progress', params: { progressToken: 'abc', progress: 50, total: 100 } });
506+
return { content: [{ type: 'text', text: 'result' }] };
513507
});
514508
```
515509

@@ -525,22 +519,22 @@ Context fields are organized into 4 groups:
525519

526520
```typescript
527521
server.setRequestHandler('tools/call', async (request, ctx) => {
528-
// Send a log message (respects client's log level filter)
529-
await ctx.mcpReq.log('info', 'Processing tool call', 'my-logger');
530-
531-
// Request client to sample an LLM
532-
const samplingResult = await ctx.mcpReq.requestSampling({
533-
messages: [{ role: 'user', content: { type: 'text', text: 'Hello' } }],
534-
maxTokens: 100,
535-
});
536-
537-
// Elicit user input via a form
538-
const elicitResult = await ctx.mcpReq.elicitInput({
539-
message: 'Please provide details',
540-
requestedSchema: { type: 'object', properties: { name: { type: 'string' } } },
541-
});
542-
543-
return { content: [{ type: 'text', text: 'done' }] };
522+
// Send a log message (respects client's log level filter)
523+
await ctx.mcpReq.log('info', 'Processing tool call', 'my-logger');
524+
525+
// Request client to sample an LLM
526+
const samplingResult = await ctx.mcpReq.requestSampling({
527+
messages: [{ role: 'user', content: { type: 'text', text: 'Hello' } }],
528+
maxTokens: 100
529+
});
530+
531+
// Elicit user input via a form
532+
const elicitResult = await ctx.mcpReq.elicitInput({
533+
message: 'Please provide details',
534+
requestedSchema: { type: 'object', properties: { name: { type: 'string' } } }
535+
});
536+
537+
return { content: [{ type: 'text', text: 'done' }] };
544538
});
545539
```
546540

@@ -602,21 +596,21 @@ try {
602596

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

605-
| Code | Description |
606-
| ------------------------------------------------- | ------------------------------------------ |
607-
| `SdkErrorCode.NotConnected` | Transport is not connected |
608-
| `SdkErrorCode.AlreadyConnected` | Transport is already connected |
609-
| `SdkErrorCode.NotInitialized` | Protocol is not initialized |
610-
| `SdkErrorCode.CapabilityNotSupported` | Required capability is not supported |
611-
| `SdkErrorCode.RequestTimeout` | Request timed out waiting for response |
612-
| `SdkErrorCode.ConnectionClosed` | Connection was closed |
613-
| `SdkErrorCode.SendFailed` | Failed to send message |
614-
| `SdkErrorCode.ClientHttpNotImplemented` | HTTP POST request failed |
615-
| `SdkErrorCode.ClientHttpAuthentication` | Server returned 401 after successful auth |
616-
| `SdkErrorCode.ClientHttpForbidden` | Server returned 403 after trying upscoping |
617-
| `SdkErrorCode.ClientHttpUnexpectedContent` | Unexpected content type in HTTP response |
618-
| `SdkErrorCode.ClientHttpFailedToOpenStream` | Failed to open SSE stream |
619-
| `SdkErrorCode.ClientHttpFailedToTerminateSession` | Failed to terminate session |
599+
| Code | Description |
600+
| ------------------------------------------------- | ------------------------------------------- |
601+
| `SdkErrorCode.NotConnected` | Transport is not connected |
602+
| `SdkErrorCode.AlreadyConnected` | Transport is already connected |
603+
| `SdkErrorCode.NotInitialized` | Protocol is not initialized |
604+
| `SdkErrorCode.CapabilityNotSupported` | Required capability is not supported |
605+
| `SdkErrorCode.RequestTimeout` | Request timed out waiting for response |
606+
| `SdkErrorCode.ConnectionClosed` | Connection was closed |
607+
| `SdkErrorCode.SendFailed` | Failed to send message |
608+
| `SdkErrorCode.ClientHttpNotImplemented` | HTTP POST request failed |
609+
| `SdkErrorCode.ClientHttpAuthentication` | Server returned 401 after re-authentication |
610+
| `SdkErrorCode.ClientHttpForbidden` | Server returned 403 after trying upscoping |
611+
| `SdkErrorCode.ClientHttpUnexpectedContent` | Unexpected content type in HTTP response |
612+
| `SdkErrorCode.ClientHttpFailedToOpenStream` | Failed to open SSE stream |
613+
| `SdkErrorCode.ClientHttpFailedToTerminateSession` | Failed to terminate session |
620614

621615
#### `StreamableHTTPError` removed
622616

@@ -647,7 +641,7 @@ try {
647641
if (error instanceof SdkError) {
648642
switch (error.code) {
649643
case SdkErrorCode.ClientHttpAuthentication:
650-
console.log('Auth failed after completing auth flow');
644+
console.log('Auth failed — server rejected token after re-auth');
651645
break;
652646
case SdkErrorCode.ClientHttpForbidden:
653647
console.log('Forbidden after upscoping attempt');
@@ -667,7 +661,8 @@ try {
667661

668662
#### Why this change?
669663

670-
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.
664+
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
665+
semantically inconsistent.
671666

672667
The new design:
673668

@@ -764,11 +759,11 @@ This means Cloudflare Workers users no longer need to explicitly pass the valida
764759
import { McpServer, CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server';
765760

766761
const server = new McpServer(
767-
{ name: 'my-server', version: '1.0.0' },
768-
{
769-
capabilities: { tools: {} },
770-
jsonSchemaValidator: new CfWorkerJsonSchemaValidator() // Required in v1
771-
}
762+
{ name: 'my-server', version: '1.0.0' },
763+
{
764+
capabilities: { tools: {} },
765+
jsonSchemaValidator: new CfWorkerJsonSchemaValidator() // Required in v1
766+
}
772767
);
773768
```
774769

@@ -778,9 +773,9 @@ const server = new McpServer(
778773
import { McpServer } from '@modelcontextprotocol/server';
779774

780775
const server = new McpServer(
781-
{ name: 'my-server', version: '1.0.0' },
782-
{ capabilities: { tools: {} } }
783-
// Validator auto-selected based on runtime
776+
{ name: 'my-server', version: '1.0.0' },
777+
{ capabilities: { tools: {} } }
778+
// Validator auto-selected based on runtime
784779
);
785780
```
786781

examples/client/src/clientGuide.examples.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
//#region imports
11-
import type { Prompt, Resource, Tool } from '@modelcontextprotocol/client';
11+
import type { AuthProvider, Prompt, Resource, Tool } from '@modelcontextprotocol/client';
1212
import {
1313
applyMiddlewares,
1414
Client,
@@ -107,6 +107,16 @@ async function serverInstructions_basic(client: Client) {
107107
// Authentication
108108
// ---------------------------------------------------------------------------
109109

110+
/** Example: Minimal AuthProvider for bearer auth with externally-managed tokens. */
111+
async function auth_tokenProvider(getStoredToken: () => Promise<string>) {
112+
//#region auth_tokenProvider
113+
const authProvider: AuthProvider = { token: async () => getStoredToken() };
114+
115+
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'), { authProvider });
116+
//#endregion auth_tokenProvider
117+
return transport;
118+
}
119+
110120
/** Example: Client credentials auth for service-to-service communication. */
111121
async function auth_clientCredentials() {
112122
//#region auth_clientCredentials
@@ -540,6 +550,7 @@ void connect_stdio;
540550
void connect_sseFallback;
541551
void disconnect_streamableHttp;
542552
void serverInstructions_basic;
553+
void auth_tokenProvider;
543554
void auth_clientCredentials;
544555
void auth_privateKeyJwt;
545556
void auth_crossAppAccess;

0 commit comments

Comments
 (0)