diff --git a/README.md b/README.md index 3bd832f88..02c5aa6ac 100644 --- a/README.md +++ b/README.md @@ -128,8 +128,8 @@ Next steps: - Local SDK docs: - [docs/server.md](docs/server.md) – building MCP servers, transports, tools/resources/prompts, sampling, elicitation, tasks, and deployment patterns. - - [docs/client.md](docs/client.md) – using the high-level client, transports, OAuth helpers, handling server‑initiated requests, and tasks. - - [docs/faq.md](docs/faq.md) – environment and troubleshooting FAQs (including Node.js Web Crypto support). + - [docs/client.md](docs/client.md) – building MCP clients: connecting, tools, resources, prompts, server-initiated requests, and error handling + - [docs/faq.md](docs/faq.md) – frequently asked questions and troubleshooting - External references: - [SDK API documentation](https://modelcontextprotocol.github.io/typescript-sdk/) - [Model Context Protocol documentation](https://modelcontextprotocol.io) diff --git a/docs/client.md b/docs/client.md index 3a6e9973b..ea359f67e 100644 --- a/docs/client.md +++ b/docs/client.md @@ -2,21 +2,35 @@ title: Client Guide --- -# Client overview +# Building MCP clients -This guide covers SDK usage for building MCP clients in TypeScript. For protocol-level details and message formats, see the [MCP specification](https://modelcontextprotocol.io/specification/latest/). +This guide covers the TypeScript SDK APIs for building MCP clients. For protocol-level concepts, see the [MCP overview](https://modelcontextprotocol.io/docs/learn/architecture). -The SDK provides a {@linkcode @modelcontextprotocol/client!client/client.Client | Client} class from `@modelcontextprotocol/client` that connects to MCP servers over different transports: +A client connects to a server, discovers what it offers — tools, resources, prompts — and invokes them. Beyond that core loop, this guide covers authentication, error handling, and responding to server-initiated requests like sampling and elicitation. -- **Streamable HTTP** – for remote HTTP servers. -- **stdio** – for local processes you spawn. -- **SSE** – for legacy HTTP+SSE servers (deprecated). +## Imports -For a feature‑rich starting point, see [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleStreamableHttp.ts). +The examples below use these imports. Adjust based on which features and transport you need: -## Connecting to a server +```ts source="../examples/client/src/clientGuide.examples.ts#imports" +import type { Prompt, Resource, Tool } from '@modelcontextprotocol/client'; +import { + applyMiddlewares, + CallToolResultSchema, + Client, + ClientCredentialsProvider, + createMiddleware, + PrivateKeyJwtProvider, + ProtocolError, + SdkError, + SdkErrorCode, + SSEClientTransport, + StdioClientTransport, + StreamableHTTPClientTransport +} from '@modelcontextprotocol/client'; +``` -Construct a `Client` with a name and version, create a transport, and call {@linkcode @modelcontextprotocol/client!client/client.Client#connect | client.connect(transport)}. The client automatically performs the MCP initialization handshake. +## Connecting to a server ### Streamable HTTP @@ -30,12 +44,11 @@ const transport = new StreamableHTTPClientTransport(new URL('http://localhost:30 await client.connect(transport); ``` -> [!NOTE] -> For a full interactive client over Streamable HTTP, see [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleStreamableHttp.ts). +For a full interactive client over Streamable HTTP, see [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleStreamableHttp.ts). ### stdio -For local, process‑spawned servers (Claude Desktop, CLI tools), use {@linkcode @modelcontextprotocol/client!client/stdio.StdioClientTransport | StdioClientTransport}. The transport spawns the server process and communicates over stdin/stdout: +For local, process-spawned servers (Claude Desktop, CLI tools), use {@linkcode @modelcontextprotocol/client!client/stdio.StdioClientTransport | StdioClientTransport}. The transport spawns the server process and communicates over stdin/stdout: ```ts source="../examples/client/src/clientGuide.examples.ts#connect_stdio" const client = new Client({ name: 'my-client', version: '1.0.0' }); @@ -50,7 +63,7 @@ await client.connect(transport); ### SSE fallback for legacy servers -To support both modern Streamable HTTP and legacy SSE servers, try `StreamableHTTPClientTransport` first and fall back to {@linkcode @modelcontextprotocol/client!client/sse.SSEClientTransport | SSEClientTransport} on failure: +To support both modern Streamable HTTP and legacy SSE servers, try {@linkcode @modelcontextprotocol/client!client/streamableHttp.StreamableHTTPClientTransport | StreamableHTTPClientTransport} first and fall back to {@linkcode @modelcontextprotocol/client!client/sse.SSEClientTransport | SSEClientTransport} on failure: ```ts source="../examples/client/src/clientGuide.examples.ts#connect_sseFallback" const baseUrl = new URL(url); @@ -70,16 +83,40 @@ try { } ``` -> [!NOTE] -> For a complete example with error reporting, see [`streamableHttpWithSseFallbackClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/streamableHttpWithSseFallbackClient.ts). +For a complete example with error reporting, see [`streamableHttpWithSseFallbackClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/streamableHttpWithSseFallbackClient.ts). + +### Disconnecting + +Call {@linkcode @modelcontextprotocol/client!client/client.Client#close | await client.close() } to disconnect. Pending requests are rejected with a {@linkcode @modelcontextprotocol/client!index.SdkErrorCode.ConnectionClosed | CONNECTION_CLOSED} error. + +For Streamable HTTP, terminate the server-side session first (per the MCP specification): + +```ts source="../examples/client/src/clientGuide.examples.ts#disconnect_streamableHttp" +await transport.terminateSession(); // notify the server (recommended) +await client.close(); +``` + +For stdio, `client.close()` handles graceful process shutdown (closes stdin, then SIGTERM, then SIGKILL if needed). + +### Server instructions + +Servers can provide an `instructions` string during initialization that describes how to use them — cross-tool relationships, workflow patterns, and constraints (see [Instructions](https://modelcontextprotocol.io/specification/latest/basic/lifecycle#instructions) in the MCP specification). Retrieve it after connecting and include it in the model's system prompt: + +```ts source="../examples/client/src/clientGuide.examples.ts#serverInstructions_basic" +const instructions = client.getInstructions(); + +const systemPrompt = ['You are a helpful assistant.', instructions].filter(Boolean).join('\n\n'); + +console.log(systemPrompt); +``` ## Authentication -For OAuth‑secured MCP servers, pass an `authProvider` to `StreamableHTTPClientTransport`. 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 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. ### Client credentials -{@linkcode @modelcontextprotocol/client!client/authExtensions.ClientCredentialsProvider | ClientCredentialsProvider} handles the `client_credentials` grant flow for service‑to‑service communication: +{@linkcode @modelcontextprotocol/client!client/authExtensions.ClientCredentialsProvider | ClientCredentialsProvider} handles the `client_credentials` grant flow for service-to-service communication: ```ts source="../examples/client/src/clientGuide.examples.ts#auth_clientCredentials" const authProvider = new ClientCredentialsProvider({ @@ -108,34 +145,31 @@ const authProvider = new PrivateKeyJwtProvider({ const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'), { authProvider }); ``` -> [!NOTE] -> For a runnable example supporting both auth methods via environment variables, see [`simpleClientCredentials.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleClientCredentials.ts). +For a runnable example supporting both auth methods via environment variables, see [`simpleClientCredentials.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleClientCredentials.ts). ### Full OAuth with user authorization -For user‑facing applications, implement the `OAuthClientProvider` interface to handle the full authorization code flow (redirects, code verifiers, token storage, dynamic client registration). The `connect()` call will throw `UnauthorizedError` when authorization is needed — catch it, complete the browser flow, call `transport.finishAuth(code)`, and reconnect. +For user-facing applications, implement the {@linkcode @modelcontextprotocol/client!client/auth.OAuthClientProvider | OAuthClientProvider} interface to handle the full authorization code flow (redirects, code verifiers, token storage, dynamic client registration). The {@linkcode @modelcontextprotocol/client!client/client.Client#connect | connect()} call will throw {@linkcode @modelcontextprotocol/client!client/auth.UnauthorizedError | UnauthorizedError} when authorization is needed — catch it, complete the browser flow, call {@linkcode @modelcontextprotocol/client!client/streamableHttp.StreamableHTTPClientTransport#finishAuth | transport.finishAuth(code)}, and reconnect. -> [!NOTE] -> For a complete working OAuth flow, see [`simpleOAuthClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleOAuthClient.ts) and [`simpleOAuthClientProvider.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleOAuthClientProvider.ts). -> -> For protocol details, see [Authorization](https://modelcontextprotocol.io/specification/latest/basic/authorization) in the MCP specification. +For a complete working OAuth flow, see [`simpleOAuthClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleOAuthClient.ts) and [`simpleOAuthClientProvider.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleOAuthClientProvider.ts). -## Using server features +## Tools -Once connected, the `Client` provides high‑level helpers for the three core MCP primitives: tools, resources, and prompts. These handle JSON‑RPC request/response encoding automatically. +Tools are callable actions offered by servers — discovering and invoking them is usually how your client enables an LLM to take action (see [Tools](https://modelcontextprotocol.io/docs/learn/server-concepts#tools) in the MCP overview). -> [!NOTE] -> For a full runnable client exercising tools, resources, and prompts, see [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleStreamableHttp.ts). - -### Tools - -Use {@linkcode @modelcontextprotocol/client!client/client.Client#listTools | listTools()} to discover available tools, and {@linkcode @modelcontextprotocol/client!client/client.Client#callTool | callTool()} to invoke one: +Use {@linkcode @modelcontextprotocol/client!client/client.Client#listTools | listTools()} to discover available tools, and {@linkcode @modelcontextprotocol/client!client/client.Client#callTool | callTool()} to invoke one. Results may be paginated — loop on `nextCursor` to collect all pages: ```ts source="../examples/client/src/clientGuide.examples.ts#callTool_basic" -const { tools } = await client.listTools(); +const allTools: Tool[] = []; +let toolCursor: string | undefined; +do { + const { tools, nextCursor } = await client.listTools({ cursor: toolCursor }); + allTools.push(...tools); + toolCursor = nextCursor; +} while (toolCursor); console.log( 'Available tools:', - tools.map(t => t.name) + allTools.map(t => t.name) ); const result = await client.callTool({ @@ -145,18 +179,52 @@ const result = await client.callTool({ console.log(result.content); ``` -> [!NOTE] -> See [Tools](https://modelcontextprotocol.io/specification/latest/server/tools) in the MCP specification for the full protocol details. +Tool results may include a `structuredContent` field — a machine-readable JSON object for programmatic use by the client application, complementing `content` which is for the LLM: -### Resources +```ts source="../examples/client/src/clientGuide.examples.ts#callTool_structuredOutput" +const result = await client.callTool({ + name: 'calculate-bmi', + arguments: { weightKg: 70, heightM: 1.75 } +}); + +// Machine-readable output for the client application +if (result.structuredContent) { + console.log(result.structuredContent); // e.g. { bmi: 22.86 } +} +``` + +### Tracking progress + +Pass `onprogress` to receive incremental progress notifications from long-running tools. Use `resetTimeoutOnProgress` to keep the request alive while the server is actively reporting, and `maxTotalTimeout` as an absolute cap: + +```ts source="../examples/client/src/clientGuide.examples.ts#callTool_progress" +const result = await client.callTool({ name: 'long-operation', arguments: {} }, undefined, { + onprogress: ({ progress, total }) => { + console.log(`Progress: ${progress}/${total ?? '?'}`); + }, + resetTimeoutOnProgress: true, + maxTotalTimeout: 600_000 +}); +console.log(result.content); +``` + +## Resources + +Resources are read-only data — files, database schemas, configuration — that your application can retrieve from a server and attach as context for the model (see [Resources](https://modelcontextprotocol.io/docs/learn/server-concepts#resources) in the MCP overview). -Use {@linkcode @modelcontextprotocol/client!client/client.Client#listResources | listResources()} and {@linkcode @modelcontextprotocol/client!client/client.Client#readResource | readResource()} to discover and read server‑provided data: +Use {@linkcode @modelcontextprotocol/client!client/client.Client#listResources | listResources()} and {@linkcode @modelcontextprotocol/client!client/client.Client#readResource | readResource()} to discover and read server-provided data. Results may be paginated — loop on `nextCursor` to collect all pages: ```ts source="../examples/client/src/clientGuide.examples.ts#readResource_basic" -const { resources } = await client.listResources(); +const allResources: Resource[] = []; +let resourceCursor: string | undefined; +do { + const { resources, nextCursor } = await client.listResources({ cursor: resourceCursor }); + allResources.push(...resources); + resourceCursor = nextCursor; +} while (resourceCursor); console.log( 'Available resources:', - resources.map(r => r.name) + allResources.map(r => r.name) ); const { contents } = await client.readResource({ uri: 'config://app' }); @@ -165,18 +233,43 @@ for (const item of contents) { } ``` -> [!NOTE] -> See [Resources](https://modelcontextprotocol.io/specification/latest/server/resources) in the MCP specification for the full protocol details. +To discover URI templates for dynamic resources, use {@linkcode @modelcontextprotocol/client!client/client.Client#listResourceTemplates | listResourceTemplates()}. + +### Subscribing to resource changes + +If the server supports resource subscriptions, use {@linkcode @modelcontextprotocol/client!client/client.Client#subscribeResource | subscribeResource()} to receive notifications when a resource changes, then re-read it: + +```ts source="../examples/client/src/clientGuide.examples.ts#subscribeResource_basic" +await client.subscribeResource({ uri: 'config://app' }); + +client.setNotificationHandler('notifications/resources/updated', async notification => { + if (notification.params.uri === 'config://app') { + const { contents } = await client.readResource({ uri: 'config://app' }); + console.log('Config updated:', contents); + } +}); + +// Later: stop receiving updates +await client.unsubscribeResource({ uri: 'config://app' }); +``` + +## Prompts -### Prompts +Prompts are reusable message templates that servers offer to help structure interactions with models (see [Prompts](https://modelcontextprotocol.io/docs/learn/server-concepts#prompts) in the MCP overview). -Use {@linkcode @modelcontextprotocol/client!client/client.Client#listPrompts | listPrompts()} and {@linkcode @modelcontextprotocol/client!client/client.Client#getPrompt | getPrompt()} to retrieve prompt templates from the server: +Use {@linkcode @modelcontextprotocol/client!client/client.Client#listPrompts | listPrompts()} and {@linkcode @modelcontextprotocol/client!client/client.Client#getPrompt | getPrompt()} to list available prompts and retrieve them with arguments. Results may be paginated — loop on `nextCursor` to collect all pages: ```ts source="../examples/client/src/clientGuide.examples.ts#getPrompt_basic" -const { prompts } = await client.listPrompts(); +const allPrompts: Prompt[] = []; +let promptCursor: string | undefined; +do { + const { prompts, nextCursor } = await client.listPrompts({ cursor: promptCursor }); + allPrompts.push(...prompts); + promptCursor = nextCursor; +} while (promptCursor); console.log( 'Available prompts:', - prompts.map(p => p.name) + allPrompts.map(p => p.name) ); const { messages } = await client.getPrompt({ @@ -186,12 +279,9 @@ const { messages } = await client.getPrompt({ console.log(messages); ``` -> [!NOTE] -> See [Prompts](https://modelcontextprotocol.io/specification/latest/server/prompts) in the MCP specification for the full protocol details. - -### Completions +## Completions -If a server supports argument completions on prompts or resources, use {@linkcode @modelcontextprotocol/client!client/client.Client#complete | complete()} to request suggestions. This is the client‑side counterpart to {@linkcode @modelcontextprotocol/server!server/completable.completable | completable()} on the server: +Both prompts and resources can support argument completions. Use {@linkcode @modelcontextprotocol/client!client/client.Client#complete | complete()} to request autocompletion suggestions from the server as a user types: ```ts source="../examples/client/src/clientGuide.examples.ts#complete_basic" const { completion } = await client.complete({ @@ -209,9 +299,9 @@ console.log(completion.values); // e.g. ['typescript'] ## Notifications -### Automatic list‑change tracking +### Automatic list-change tracking -The `listChanged` client option keeps a local cache of tools, prompts, or resources in sync with the server. Compared to manually handling notifications, it provides automatic server capability gating, debouncing (300 ms by default), auto‑refresh, and error‑first callbacks: +The {@linkcode @modelcontextprotocol/client!client/client.ClientOptions | listChanged} client option keeps a local cache of tools, prompts, or resources in sync with the server. It provides automatic server capability gating, debouncing (300 ms by default), auto-refresh, and error-first callbacks: ```ts source="../examples/client/src/clientGuide.examples.ts#listChanged_basic" const client = new Client( @@ -240,7 +330,7 @@ const client = new Client( For full control — or for notification types not covered by `listChanged` (such as log messages) — register handlers directly with {@linkcode @modelcontextprotocol/client!client/client.Client#setNotificationHandler | setNotificationHandler()}: ```ts source="../examples/client/src/clientGuide.examples.ts#notificationHandler_basic" -// Server log messages (e.g. from ctx.mcpReq.log() in tool handlers) +// Server log messages (sent by the server during request processing) client.setNotificationHandler('notifications/message', notification => { const { level, data } = notification.params; console.log(`[${level}]`, data); @@ -253,15 +343,18 @@ client.setNotificationHandler('notifications/resources/list_changed', async () = }); ``` -Note that `listChanged` and `setNotificationHandler` are mutually exclusive per notification type — using both for the same notification will cause the manual handler to be overwritten. +To control the minimum severity of log messages the server sends, use {@linkcode @modelcontextprotocol/client!client/client.Client#setLoggingLevel | setLoggingLevel()}: -## Handling server‑initiated requests +```ts source="../examples/client/src/clientGuide.examples.ts#setLoggingLevel_basic" +await client.setLoggingLevel('warning'); +``` -MCP is bidirectional — servers can also send requests *to* the client. To handle these, declare the corresponding capability when constructing the `Client` and register a request handler. The two main server‑initiated request types are **sampling** (LLM completions) and **elicitation** (user input). +> [!WARNING] +> `listChanged` and {@linkcode @modelcontextprotocol/client!client/client.Client#setNotificationHandler | setNotificationHandler()} are mutually exclusive per notification type — using both for the same notification will cause the manual handler to be overwritten. -### Declaring capabilities +## Handling server-initiated requests -Pass a {@linkcode @modelcontextprotocol/client!client/client.ClientOptions | `capabilities`} object when constructing the `Client`. The server reads these during initialization and will only send requests your client has declared support for: +MCP is bidirectional — servers can send requests *to* the client during tool execution, as long as the client declares matching capabilities (see [Architecture](https://modelcontextprotocol.io/docs/learn/architecture) in the MCP overview). Declare the corresponding capability when constructing the {@linkcode @modelcontextprotocol/client!client/client.Client | Client} and register a request handler: ```ts source="../examples/client/src/clientGuide.examples.ts#capabilities_declaration" const client = new Client( @@ -277,7 +370,7 @@ const client = new Client( ### Sampling -When a server calls `server.createMessage(...)` inside a tool handler, the request is routed to the client. Register a handler for `sampling/createMessage` to fulfill it: +When a server needs an LLM completion during tool execution, it sends a `sampling/createMessage` request to the client (see [Sampling](https://modelcontextprotocol.io/docs/learn/client-concepts#sampling) in the MCP overview). Register a handler to fulfill it: ```ts source="../examples/client/src/clientGuide.examples.ts#sampling_handler" client.setRequestHandler('sampling/createMessage', async request => { @@ -296,12 +389,9 @@ client.setRequestHandler('sampling/createMessage', async request => { }); ``` -> [!NOTE] -> See [Sampling](https://modelcontextprotocol.io/specification/latest/client/sampling) in the MCP specification for the full protocol details. - ### Elicitation -When a server calls `server.elicitInput(...)`, the request arrives at the client as an `elicitation/create` request. The client should present the form to the user and return the collected data, or `{ action: 'decline' }`: +When a server needs user input during tool execution, it sends an `elicitation/create` request to the client (see [Elicitation](https://modelcontextprotocol.io/docs/learn/client-concepts#elicitation) in the MCP overview). The client should present the form to the user and return the collected data, or `{ action: 'decline' }`: ```ts source="../examples/client/src/clientGuide.examples.ts#elicitation_handler" client.setRequestHandler('elicitation/create', async request => { @@ -317,14 +407,95 @@ client.setRequestHandler('elicitation/create', async request => { }); ``` -> [!NOTE] -> For a full form‑based elicitation handler with AJV validation, see [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleStreamableHttp.ts). For URL elicitation mode (`mode: 'url'`), see [`elicitationUrlExample.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/elicitationUrlExample.ts). -> -> For protocol details, see [Elicitation](https://modelcontextprotocol.io/specification/latest/client/elicitation) in the MCP specification. +For a full form-based elicitation handler with AJV validation, see [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleStreamableHttp.ts). For URL elicitation mode, see [`elicitationUrlExample.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/elicitationUrlExample.ts). -## Advanced patterns +### Roots -### Client middleware +Roots let the client expose filesystem boundaries to the server (see [Roots](https://modelcontextprotocol.io/docs/learn/client-concepts#roots) in the MCP overview). Declare the `roots` capability and register a `roots/list` handler: + +```ts source="../examples/client/src/clientGuide.examples.ts#roots_handler" +client.setRequestHandler('roots/list', async () => { + return { + roots: [ + { uri: 'file:///home/user/projects/my-app', name: 'My App' }, + { uri: 'file:///home/user/data', name: 'Data' } + ] + }; +}); +``` + +When the available roots change, notify the server with {@linkcode @modelcontextprotocol/client!client/client.Client#sendRootsListChanged | client.sendRootsListChanged()}. + +## Error handling + +### Tool errors vs protocol errors + +{@linkcode @modelcontextprotocol/client!client/client.Client#callTool | callTool()} has two error surfaces: the tool can *run but report failure* via `isError: true` in the result, or the *request itself can fail* and throw an exception. Always check both: + +```ts source="../examples/client/src/clientGuide.examples.ts#errorHandling_toolErrors" +try { + const result = await client.callTool({ + name: 'fetch-data', + arguments: { url: 'https://example.com' } + }); + + // Tool-level error: the tool ran but reported a problem + if (result.isError) { + console.error('Tool error:', result.content); + return; + } + + console.log('Success:', result.content); +} catch (error) { + // Protocol-level error: the request itself failed + if (error instanceof ProtocolError) { + console.error(`Protocol error ${error.code}: ${error.message}`); + } else if (error instanceof SdkError) { + console.error(`SDK error [${error.code}]: ${error.message}`); + } else { + throw error; + } +} +``` + +{@linkcode @modelcontextprotocol/client!index.ProtocolError | ProtocolError} represents JSON-RPC errors from the server (method not found, invalid params, internal error). {@linkcode @modelcontextprotocol/client!index.SdkError | SdkError} represents local SDK errors — {@linkcode @modelcontextprotocol/client!index.SdkErrorCode.RequestTimeout | REQUEST_TIMEOUT}, {@linkcode @modelcontextprotocol/client!index.SdkErrorCode.ConnectionClosed | CONNECTION_CLOSED}, {@linkcode @modelcontextprotocol/client!index.SdkErrorCode.CapabilityNotSupported | CAPABILITY_NOT_SUPPORTED}, and others. + +### Connection lifecycle + +Set {@linkcode @modelcontextprotocol/client!client/client.Client#onerror | client.onerror} to catch out-of-band transport errors (SSE disconnects, parse errors). Set {@linkcode @modelcontextprotocol/client!client/client.Client#onclose | client.onclose} to detect when the connection drops — pending requests are rejected with a {@linkcode @modelcontextprotocol/client!index.SdkErrorCode.ConnectionClosed | CONNECTION_CLOSED} error: + +```ts source="../examples/client/src/clientGuide.examples.ts#errorHandling_lifecycle" +// Out-of-band errors (SSE disconnects, parse errors) +client.onerror = error => { + console.error('Transport error:', error.message); +}; + +// Connection closed (pending requests are rejected with CONNECTION_CLOSED) +client.onclose = () => { + console.log('Connection closed'); +}; +``` + +### Timeouts + +All requests have a 60-second default timeout. Pass a custom `timeout` in the options to override it. On timeout, the SDK sends a cancellation notification to the server and rejects the promise with {@linkcode @modelcontextprotocol/client!index.SdkErrorCode.RequestTimeout | SdkErrorCode.RequestTimeout}: + +```ts source="../examples/client/src/clientGuide.examples.ts#errorHandling_timeout" +try { + const result = await client.callTool( + { name: 'slow-task', arguments: {} }, + undefined, + { timeout: 120_000 } // 2 minutes instead of the default 60 seconds + ); + console.log(result.content); +} catch (error) { + if (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) { + console.error('Request timed out'); + } +} +``` + +## Client middleware Use {@linkcode @modelcontextprotocol/client!client/middleware.createMiddleware | createMiddleware()} and {@linkcode @modelcontextprotocol/client!client/middleware.applyMiddlewares | applyMiddlewares()} to compose fetch middleware pipelines. Middleware wraps the underlying `fetch` call and can add headers, handle retries, or log requests. Pass the enhanced fetch to the transport via the `fetch` option: @@ -340,9 +511,9 @@ const transport = new StreamableHTTPClientTransport(new URL('http://localhost:30 }); ``` -### Resumption tokens +## Resumption tokens -When using SSE‑based streaming, the server can assign event IDs. Pass `onresumptiontoken` to track them, and `resumptionToken` to resume from where you left off after a disconnection: +When using SSE-based streaming, the server can assign event IDs. Pass `onresumptiontoken` to track them, and `resumptionToken` to resume from where you left off after a disconnection: ```ts source="../examples/client/src/clientGuide.examples.ts#resumptionToken_basic" let lastToken: string | undefined; @@ -364,29 +535,33 @@ const result = await client.request( console.log(result); ``` -> [!NOTE] -> For an end‑to‑end example of server‑initiated SSE disconnection and automatic client reconnection with event replay, see [`ssePollingClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/ssePollingClient.ts). +For an end-to-end example of server-initiated SSE disconnection and automatic client reconnection with event replay, see [`ssePollingClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/ssePollingClient.ts). ## Tasks (experimental) -Task-based execution enables "call-now, fetch-later" patterns for long-running operations. Instead of returning a result immediately, a tool creates a task that can be polled or resumed later. To use tasks: +> [!WARNING] +> The tasks API is experimental and may change without notice. + +Task-based execution enables "call-now, fetch-later" patterns for long-running operations (see [Tasks](https://modelcontextprotocol.io/specification/latest/basic/utilities/tasks) in the MCP specification). Instead of returning a result immediately, a tool creates a task that can be polled or resumed later. To use tasks: - Call {@linkcode @modelcontextprotocol/client!experimental/tasks/client.ExperimentalClientTasks#callToolStream | client.experimental.tasks.callToolStream(...)} to start a tool call that may create a task and emit status updates over time. - Call {@linkcode @modelcontextprotocol/client!experimental/tasks/client.ExperimentalClientTasks#getTask | client.experimental.tasks.getTask(...)} and {@linkcode @modelcontextprotocol/client!experimental/tasks/client.ExperimentalClientTasks#getTaskResult | getTaskResult(...)} to check status and fetch results after reconnecting. -> [!NOTE] -> For a full runnable example, see [`simpleTaskInteractiveClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleTaskInteractiveClient.ts). +For a full runnable example, see [`simpleTaskInteractiveClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleTaskInteractiveClient.ts). -> [!WARNING] -> The tasks API is experimental and may change without notice. +## See also -## More client features +- [`examples/client/`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/client) — Full runnable client examples +- [Server guide](./server.md) — Building MCP servers with this SDK +- [MCP overview](https://modelcontextprotocol.io/docs/learn/architecture) — Protocol-level concepts: participants, layers, primitives +- [Migration guide](./migration.md) — Upgrading from previous SDK versions +- [FAQ](./faq.md) — Frequently asked questions and troubleshooting -The sections above cover the essentials. The table below links to additional capabilities. +### Additional examples -| Feature | Description | Reference | -|---------|-------------|-----------| +| Feature | Description | Example | +|---------|-------------|---------| | Parallel tool calls | Run multiple tool calls concurrently via `Promise.all` | [`parallelToolCallsClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/parallelToolCallsClient.ts) | -| SSE disconnect / reconnection | Server‑initiated SSE disconnect with automatic reconnection and event replay | [`ssePollingClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/ssePollingClient.ts) | +| SSE disconnect / reconnection | Server-initiated SSE disconnect with automatic reconnection and event replay | [`ssePollingClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/ssePollingClient.ts) | | Multiple clients | Independent client lifecycles to the same server | [`multipleClientsParallel.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/multipleClientsParallel.ts) | | URL elicitation | Handle sensitive data collection via browser | [`elicitationUrlExample.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/elicitationUrlExample.ts) | diff --git a/docs/documents.md b/docs/documents.md index 37033ef0c..af7c22c0b 100644 --- a/docs/documents.md +++ b/docs/documents.md @@ -13,5 +13,5 @@ children: - [Server Quickstart](./server-quickstart.md) – build a weather server from scratch and connect it to Claude for Desktop - [Server](./server.md) – building MCP servers, transports, tools/resources/prompts, sampling, elicitation, tasks, and deployment patterns - [Client Quickstart](./client-quickstart.md) – build an LLM-powered chatbot that connects to an MCP server and calls its tools -- [Client](./client.md) – using the high-level client, transports, OAuth helpers, handling server‑initiated requests, and tasks +- [Client](./client.md) – building MCP clients: connecting, tools, resources, prompts, server-initiated requests, and error handling - [FAQ](./faq.md) – frequently asked questions and troubleshooting diff --git a/examples/client/src/clientGuide.examples.ts b/examples/client/src/clientGuide.examples.ts index 8cefcf224..91f0afd85 100644 --- a/examples/client/src/clientGuide.examples.ts +++ b/examples/client/src/clientGuide.examples.ts @@ -7,6 +7,8 @@ * @module */ +//#region imports +import type { Prompt, Resource, Tool } from '@modelcontextprotocol/client'; import { applyMiddlewares, CallToolResultSchema, @@ -14,10 +16,14 @@ import { ClientCredentialsProvider, createMiddleware, PrivateKeyJwtProvider, + ProtocolError, + SdkError, + SdkErrorCode, SSEClientTransport, StdioClientTransport, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; +//#endregion imports // --------------------------------------------------------------------------- // Connecting to a server @@ -69,6 +75,33 @@ async function connect_sseFallback(url: string) { //#endregion connect_sseFallback } +// --------------------------------------------------------------------------- +// Disconnecting +// --------------------------------------------------------------------------- + +/** Example: Graceful disconnect for Streamable HTTP. */ +async function disconnect_streamableHttp(client: Client, transport: StreamableHTTPClientTransport) { + //#region disconnect_streamableHttp + await transport.terminateSession(); // notify the server (recommended) + await client.close(); + //#endregion disconnect_streamableHttp +} + +// --------------------------------------------------------------------------- +// Server instructions +// --------------------------------------------------------------------------- + +/** Example: Access server instructions after connecting. */ +async function serverInstructions_basic(client: Client) { + //#region serverInstructions_basic + const instructions = client.getInstructions(); + + const systemPrompt = ['You are a helpful assistant.', instructions].filter(Boolean).join('\n\n'); + + console.log(systemPrompt); + //#endregion serverInstructions_basic +} + // --------------------------------------------------------------------------- // Authentication // --------------------------------------------------------------------------- @@ -110,10 +143,16 @@ async function auth_privateKeyJwt(pemEncodedKey: string) { /** Example: List and call tools. */ async function callTool_basic(client: Client) { //#region callTool_basic - const { tools } = await client.listTools(); + const allTools: Tool[] = []; + let toolCursor: string | undefined; + do { + const { tools, nextCursor } = await client.listTools({ cursor: toolCursor }); + allTools.push(...tools); + toolCursor = nextCursor; + } while (toolCursor); console.log( 'Available tools:', - tools.map(t => t.name) + allTools.map(t => t.name) ); const result = await client.callTool({ @@ -124,13 +163,48 @@ async function callTool_basic(client: Client) { //#endregion callTool_basic } +/** Example: Structured tool output. */ +async function callTool_structuredOutput(client: Client) { + //#region callTool_structuredOutput + const result = await client.callTool({ + name: 'calculate-bmi', + arguments: { weightKg: 70, heightM: 1.75 } + }); + + // Machine-readable output for the client application + if (result.structuredContent) { + console.log(result.structuredContent); // e.g. { bmi: 22.86 } + } + //#endregion callTool_structuredOutput +} + +/** Example: Track progress of a long-running tool call. */ +async function callTool_progress(client: Client) { + //#region callTool_progress + const result = await client.callTool({ name: 'long-operation', arguments: {} }, undefined, { + onprogress: ({ progress, total }) => { + console.log(`Progress: ${progress}/${total ?? '?'}`); + }, + resetTimeoutOnProgress: true, + maxTotalTimeout: 600_000 + }); + console.log(result.content); + //#endregion callTool_progress +} + /** Example: List and read resources. */ async function readResource_basic(client: Client) { //#region readResource_basic - const { resources } = await client.listResources(); + const allResources: Resource[] = []; + let resourceCursor: string | undefined; + do { + const { resources, nextCursor } = await client.listResources({ cursor: resourceCursor }); + allResources.push(...resources); + resourceCursor = nextCursor; + } while (resourceCursor); console.log( 'Available resources:', - resources.map(r => r.name) + allResources.map(r => r.name) ); const { contents } = await client.readResource({ uri: 'config://app' }); @@ -140,13 +214,36 @@ async function readResource_basic(client: Client) { //#endregion readResource_basic } +/** Example: Subscribe to resource changes. */ +async function subscribeResource_basic(client: Client) { + //#region subscribeResource_basic + await client.subscribeResource({ uri: 'config://app' }); + + client.setNotificationHandler('notifications/resources/updated', async notification => { + if (notification.params.uri === 'config://app') { + const { contents } = await client.readResource({ uri: 'config://app' }); + console.log('Config updated:', contents); + } + }); + + // Later: stop receiving updates + await client.unsubscribeResource({ uri: 'config://app' }); + //#endregion subscribeResource_basic +} + /** Example: List and get prompts. */ async function getPrompt_basic(client: Client) { //#region getPrompt_basic - const { prompts } = await client.listPrompts(); + const allPrompts: Prompt[] = []; + let promptCursor: string | undefined; + do { + const { prompts, nextCursor } = await client.listPrompts({ cursor: promptCursor }); + allPrompts.push(...prompts); + promptCursor = nextCursor; + } while (promptCursor); console.log( 'Available prompts:', - prompts.map(p => p.name) + allPrompts.map(p => p.name) ); const { messages } = await client.getPrompt({ @@ -181,7 +278,7 @@ async function complete_basic(client: Client) { /** 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) + // Server log messages (sent by the server during request processing) client.setNotificationHandler('notifications/message', notification => { const { level, data } = notification.params; console.log(`[${level}]`, data); @@ -195,6 +292,13 @@ function notificationHandler_basic(client: Client) { //#endregion notificationHandler_basic } +/** Example: Control server log level. */ +async function setLoggingLevel_basic(client: Client) { + //#region setLoggingLevel_basic + await client.setLoggingLevel('warning'); + //#endregion setLoggingLevel_basic +} + /** Example: Automatic list-change tracking via the listChanged option. */ async function listChanged_basic() { //#region listChanged_basic @@ -278,6 +382,86 @@ function elicitation_handler(client: Client) { //#endregion elicitation_handler } +/** Example: Expose filesystem roots to the server. */ +function roots_handler(client: Client) { + //#region roots_handler + client.setRequestHandler('roots/list', async () => { + return { + roots: [ + { uri: 'file:///home/user/projects/my-app', name: 'My App' }, + { uri: 'file:///home/user/data', name: 'Data' } + ] + }; + }); + //#endregion roots_handler +} + +// --------------------------------------------------------------------------- +// Error handling +// --------------------------------------------------------------------------- + +/** Example: Tool errors vs protocol errors. */ +async function errorHandling_toolErrors(client: Client) { + //#region errorHandling_toolErrors + try { + const result = await client.callTool({ + name: 'fetch-data', + arguments: { url: 'https://example.com' } + }); + + // Tool-level error: the tool ran but reported a problem + if (result.isError) { + console.error('Tool error:', result.content); + return; + } + + console.log('Success:', result.content); + } catch (error) { + // Protocol-level error: the request itself failed + if (error instanceof ProtocolError) { + console.error(`Protocol error ${error.code}: ${error.message}`); + } else if (error instanceof SdkError) { + console.error(`SDK error [${error.code}]: ${error.message}`); + } else { + throw error; + } + } + //#endregion errorHandling_toolErrors +} + +/** Example: Connection lifecycle callbacks. */ +function errorHandling_lifecycle(client: Client) { + //#region errorHandling_lifecycle + // Out-of-band errors (SSE disconnects, parse errors) + client.onerror = error => { + console.error('Transport error:', error.message); + }; + + // Connection closed (pending requests are rejected with CONNECTION_CLOSED) + client.onclose = () => { + console.log('Connection closed'); + }; + //#endregion errorHandling_lifecycle +} + +/** Example: Custom timeouts. */ +async function errorHandling_timeout(client: Client) { + //#region errorHandling_timeout + try { + const result = await client.callTool( + { name: 'slow-task', arguments: {} }, + undefined, + { timeout: 120_000 } // 2 minutes instead of the default 60 seconds + ); + console.log(result.content); + } catch (error) { + if (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) { + console.error('Request timed out'); + } + } + //#endregion errorHandling_timeout +} + // --------------------------------------------------------------------------- // Advanced patterns // --------------------------------------------------------------------------- @@ -325,16 +509,26 @@ async function resumptionToken_basic(client: Client) { void connect_streamableHttp; void connect_stdio; void connect_sseFallback; +void disconnect_streamableHttp; +void serverInstructions_basic; void auth_clientCredentials; void auth_privateKeyJwt; void callTool_basic; +void callTool_structuredOutput; +void callTool_progress; void readResource_basic; +void subscribeResource_basic; void getPrompt_basic; void complete_basic; void notificationHandler_basic; +void setLoggingLevel_basic; void listChanged_basic; void capabilities_declaration; void sampling_handler; void elicitation_handler; +void roots_handler; +void errorHandling_toolErrors; +void errorHandling_lifecycle; +void errorHandling_timeout; void middleware_basic; void resumptionToken_basic;