Skip to content
Merged
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
10 changes: 10 additions & 0 deletions .changeset/expose-auth-server-discovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@modelcontextprotocol/client': minor
---

Add `discoverOAuthServerInfo()` function and unified discovery state caching for OAuth

- New `discoverOAuthServerInfo(serverUrl)` export that performs RFC 9728 protected resource metadata discovery followed by authorization server metadata discovery in a single call. Use this for operations like token refresh and revocation that need the authorization server URL outside of `auth()`.
- New `OAuthDiscoveryState` type and optional `OAuthClientProvider` methods `saveDiscoveryState()` / `discoveryState()` allow providers to persist all discovery results (auth server URL, resource metadata URL, resource metadata, auth server metadata) across sessions. This avoids redundant discovery requests and handles browser redirect scenarios where discovery state would otherwise be lost.
- New `'discovery'` scope for `invalidateCredentials()` to clear cached discovery state.
- New `OAuthServerInfo` type exported for the return value of `discoverOAuthServerInfo()`.
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ Include what changed, why, and how to migrate. Search for related sections and g
- **Testing**: Co-locate tests with source files, use descriptive test names
- **Comments**: JSDoc for public APIs, inline comments for complex logic

### JSDoc `@example` Code Snippets

JSDoc `@example` tags should pull type-checked code from companion `.examples.ts` files (e.g., `client.ts` → `client.examples.ts`). Use `` ```ts source="./file.examples.ts#regionName" `` fences referencing `//#region regionName` blocks; region names follow `exportedName_variant` or `ClassName_methodName_variant` pattern (e.g., `applyMiddlewares_basicUsage`, `Client_connect_basicUsage`). For whole-file inclusion (any file type), omit the `#regionName`.

Run `pnpm sync:snippets` to sync example content into JSDoc comments and markdown files.

## Architecture Overview

### Core Layers
Expand Down
8 changes: 8 additions & 0 deletions common/eslint-config/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ export default defineConfig(
'unicorn/consistent-function-scoping': 'off'
}
},
{
// Example files contain intentionally unused functions (one per region)
files: ['**/*.examples.ts'],
rules: {
'@typescript-eslint/no-unused-vars': 'off',
'no-console': 'off'
}
},
{
// Ignore generated protocol types everywhere
ignores: ['**/spec.types.ts']
Expand Down
394 changes: 355 additions & 39 deletions docs/client.md

Large diffs are not rendered by default.

305 changes: 218 additions & 87 deletions docs/server.md

Large diffs are not rendered by default.

340 changes: 340 additions & 0 deletions examples/client/src/clientGuide.examples.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,340 @@
/**
* Type-checked examples for docs/client.md.
*
* Regions are synced into markdown code fences via `pnpm sync:snippets`.
* Each function wraps a single region. The function name matches the region name.
*
* @module
*/

import {
applyMiddlewares,
CallToolResultSchema,
Client,
ClientCredentialsProvider,
createMiddleware,
PrivateKeyJwtProvider,
SSEClientTransport,
StdioClientTransport,
StreamableHTTPClientTransport
} from '@modelcontextprotocol/client';

// ---------------------------------------------------------------------------
// Connecting to a server
// ---------------------------------------------------------------------------

/** Example: Streamable HTTP transport. */
async function connect_streamableHttp() {
//#region connect_streamableHttp
const client = new Client({ name: 'my-client', version: '1.0.0' });

const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'));

await client.connect(transport);
//#endregion connect_streamableHttp
}

/** Example: stdio transport for local process-spawned servers. */
async function connect_stdio() {
//#region connect_stdio
const client = new Client({ name: 'my-client', version: '1.0.0' });

const transport = new StdioClientTransport({
command: 'node',
args: ['server.js']
});

await client.connect(transport);
//#endregion connect_stdio
}

/** Example: Try Streamable HTTP, fall back to legacy SSE. */
async function connect_sseFallback(url: string) {
//#region connect_sseFallback
const baseUrl = new URL(url);

try {
// Try modern Streamable HTTP transport first
const client = new Client({ name: 'my-client', version: '1.0.0' });
const transport = new StreamableHTTPClientTransport(baseUrl);
await client.connect(transport);
return { client, transport };
} catch {
// Fall back to legacy SSE transport
const client = new Client({ name: 'my-client', version: '1.0.0' });
const transport = new SSEClientTransport(baseUrl);
await client.connect(transport);
return { client, transport };
}
//#endregion connect_sseFallback
}

// ---------------------------------------------------------------------------
// Authentication
// ---------------------------------------------------------------------------

/** Example: Client credentials auth for service-to-service communication. */
async function auth_clientCredentials() {
//#region auth_clientCredentials
const authProvider = new ClientCredentialsProvider({
clientId: 'my-service',
clientSecret: 'my-secret'
});

const client = new Client({ name: 'my-client', version: '1.0.0' });

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

await client.connect(transport);
//#endregion auth_clientCredentials
}

/** Example: Private key JWT auth. */
async function auth_privateKeyJwt(pemEncodedKey: string) {
//#region auth_privateKeyJwt
const authProvider = new PrivateKeyJwtProvider({
clientId: 'my-service',
privateKey: pemEncodedKey,
algorithm: 'RS256'
});

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

// ---------------------------------------------------------------------------
// Using server features
// ---------------------------------------------------------------------------

/** Example: List and call tools. */
async function callTool_basic(client: Client) {
//#region callTool_basic
const { tools } = await client.listTools();
console.log(
'Available tools:',
tools.map(t => t.name)
);

const result = await client.callTool({
name: 'calculate-bmi',
arguments: { weightKg: 70, heightM: 1.75 }
});
console.log(result.content);
//#endregion callTool_basic
}

/** Example: List and read resources. */
async function readResource_basic(client: Client) {
//#region readResource_basic
const { resources } = await client.listResources();
console.log(
'Available resources:',
resources.map(r => r.name)
);

const { contents } = await client.readResource({ uri: 'config://app' });
for (const item of contents) {
console.log(item);
}
//#endregion readResource_basic
}

/** Example: List and get prompts. */
async function getPrompt_basic(client: Client) {
//#region getPrompt_basic
const { prompts } = await client.listPrompts();
console.log(
'Available prompts:',
prompts.map(p => p.name)
);

const { messages } = await client.getPrompt({
name: 'review-code',
arguments: { code: 'console.log("hello")' }
});
console.log(messages);
//#endregion getPrompt_basic
}

/** Example: Request argument completions. */
async function complete_basic(client: Client) {
//#region complete_basic
const { completion } = await client.complete({
ref: {
type: 'ref/prompt',
name: 'review-code'
},
argument: {
name: 'language',
value: 'type'
}
});
console.log(completion.values); // e.g. ['typescript']
//#endregion complete_basic
}

// ---------------------------------------------------------------------------
// Notifications
// ---------------------------------------------------------------------------

/** Example: Handle log messages and list-change notifications. */
function notificationHandler_basic(client: Client) {
//#region notificationHandler_basic
// Server log messages (e.g. from ctx.mcpReq.log() in tool handlers)
client.setNotificationHandler('notifications/message', notification => {
const { level, data } = notification.params;
console.log(`[${level}]`, data);
});

// Server's resource list changed — re-fetch the list
client.setNotificationHandler('notifications/resources/list_changed', async () => {
const { resources } = await client.listResources();
console.log('Resources changed:', resources.length);
});
//#endregion notificationHandler_basic
}

/** Example: Automatic list-change tracking via the listChanged option. */
async function listChanged_basic() {
//#region listChanged_basic
const client = new Client(
{ name: 'my-client', version: '1.0.0' },
{
listChanged: {
tools: {
onChanged: (error, tools) => {
if (error) {
console.error('Failed to refresh tools:', error);
return;
}
console.log('Tools updated:', tools);
}
},
prompts: {
onChanged: (error, prompts) => console.log('Prompts updated:', prompts)
}
}
}
);
//#endregion listChanged_basic
return client;
}

// ---------------------------------------------------------------------------
// Handling server-initiated requests
// ---------------------------------------------------------------------------

/** Example: Declare client capabilities for sampling and elicitation. */
function capabilities_declaration() {
//#region capabilities_declaration
const client = new Client(
{ name: 'my-client', version: '1.0.0' },
{
capabilities: {
sampling: {},
elicitation: { form: {} }
}
}
);
//#endregion capabilities_declaration
return client;
}

/** Example: Handle a sampling request from the server. */
function sampling_handler(client: Client) {
//#region sampling_handler
client.setRequestHandler('sampling/createMessage', async request => {
const lastMessage = request.params.messages.at(-1);
console.log('Sampling request:', lastMessage);

// In production, send messages to your LLM here
return {
model: 'my-model',
role: 'assistant' as const,
content: {
type: 'text' as const,
text: 'Response from the model'
}
};
});
//#endregion sampling_handler
}

/** Example: Handle an elicitation request from the server. */
function elicitation_handler(client: Client) {
//#region elicitation_handler
client.setRequestHandler('elicitation/create', async request => {
console.log('Server asks:', request.params.message);

if (request.params.mode === 'form') {
// Present the schema-driven form to the user
console.log('Schema:', request.params.requestedSchema);
return { action: 'accept', content: { confirm: true } };
}

return { action: 'decline' };
});
//#endregion elicitation_handler
}

// ---------------------------------------------------------------------------
// Advanced patterns
// ---------------------------------------------------------------------------

/** Example: Client middleware that adds a custom header. */
async function middleware_basic() {
//#region middleware_basic
const authMiddleware = createMiddleware(async (next, input, init) => {
const headers = new Headers(init?.headers);
headers.set('X-Custom-Header', 'my-value');
return next(input, { ...init, headers });
});

const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'), {
fetch: applyMiddlewares(authMiddleware)(fetch)
});
//#endregion middleware_basic
return transport;
}

/** Example: Track resumption tokens for SSE reconnection. */
async function resumptionToken_basic(client: Client) {
//#region resumptionToken_basic
let lastToken: string | undefined;

const result = await client.request(
{
method: 'tools/call',
params: { name: 'long-running-task', arguments: {} }
},
CallToolResultSchema,
{
resumptionToken: lastToken,
onresumptiontoken: (token: string) => {
lastToken = token;
// Persist token to survive restarts
}
}
);
console.log(result);
//#endregion resumptionToken_basic
}

// Suppress unused-function warnings (functions exist solely for type-checking)
void connect_streamableHttp;
void connect_stdio;
void connect_sseFallback;
void auth_clientCredentials;
void auth_privateKeyJwt;
void callTool_basic;
void readResource_basic;
void getPrompt_basic;
void complete_basic;
void notificationHandler_basic;
void listChanged_basic;
void capabilities_declaration;
void sampling_handler;
void elicitation_handler;
void middleware_basic;
void resumptionToken_basic;
Loading
Loading