diff --git a/README.md b/README.md
index 3bd832f884..02c5aa6ac7 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 3a6e9973b7..ea359f67ee 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 37033ef0c9..465a69a1d0 100644
--- a/docs/documents.md
+++ b/docs/documents.md
@@ -10,8 +10,8 @@ children:
# Documents
-- [Server Quickstart](./server-quickstart.md) – build a weather server from scratch and connect it to Claude for Desktop
+- [Server Quickstart](./server-quickstart.md) – build a weather server from scratch and connect it to VS Code
- [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/docs/server-quickstart.md b/docs/server-quickstart.md
index d0a7e6ed0d..ffa49cec57 100644
--- a/docs/server-quickstart.md
+++ b/docs/server-quickstart.md
@@ -4,14 +4,11 @@ title: Server Quickstart
# Quickstart: Build a weather server
-In this tutorial, we'll build a simple MCP weather server and connect it to a host, Claude for Desktop.
+In this tutorial, we'll build a simple MCP weather server and connect it to a host.
## What we'll be building
-We'll build a server that exposes two tools: `get-alerts` and `get-forecast`. Then we'll connect the server to an MCP host (in this case, Claude for Desktop).
-
-> [!NOTE]
-> Servers can connect to any client. We've chosen Claude for Desktop here for simplicity, but we also have a guide on [building your own client](./client.md) as well as a [list of other clients here](https://modelcontextprotocol.io/clients).
+We'll build a server that exposes two tools: `get-alerts` and `get-forecast`. Then we'll connect the server to an MCP host (in this case, VS Code with GitHub Copilot).
## Core MCP Concepts
@@ -365,80 +362,49 @@ main().catch((error) => {
Make sure to run `npm run build` to build your server! This is a very important step in getting your server to connect.
-Let's now test your server from an existing MCP host, Claude for Desktop.
-
-## Testing your server with Claude for Desktop
-
-> [!NOTE]
-> Claude for Desktop is not yet available on Linux. Linux users can proceed to the [Client guide](./client.md) to build an MCP client that connects to the server we just built.
-
-First, make sure you have Claude for Desktop installed. [You can install the latest version here.](https://claude.ai/download) If you already have Claude for Desktop, **make sure it's updated to the latest version.**
+Let's now test your server from an existing MCP host.
-We'll need to configure Claude for Desktop for whichever MCP servers you want to use. To do this, open your Claude for Desktop App configuration at `~/Library/Application Support/Claude/claude_desktop_config.json` in a text editor. Make sure to create the file if it doesn't exist.
+## Testing your server in VS Code
-For example, if you have [VS Code](https://code.visualstudio.com/) installed:
+[VS Code](https://code.visualstudio.com/) with [GitHub Copilot](https://github.com/features/copilot) can discover and invoke MCP tools via agent mode. [Copilot Free](https://github.com/features/copilot/plans) is sufficient to follow along.
-**macOS/Linux:**
-
-```bash
-code ~/Library/Application\ Support/Claude/claude_desktop_config.json
-```
-
-**Windows:**
+> [!NOTE]
+> Servers can connect to any client. We've chosen VS Code here for simplicity, but we also have a guide on [building your own client](./client-quickstart.md) as well as a [list of other clients here](https://modelcontextprotocol.io/clients).
-```powershell
-code $env:AppData\Claude\claude_desktop_config.json
-```
+### Prerequisites
-You'll then add your servers in the `mcpServers` key. The MCP UI elements will only show up in Claude for Desktop if at least one server is properly configured.
+1. Install [VS Code](https://code.visualstudio.com/) (version 1.99 or later).
+2. Install the **GitHub Copilot** extension from the VS Code Extensions marketplace.
+3. Sign in to your GitHub account when prompted.
-In this case, we'll add our single weather server like so:
+### Configure the MCP server
-**macOS/Linux:**
+Create a `.vscode/mcp.json` file in your `weather` project root:
```json
{
- "mcpServers": {
+ "servers": {
"weather": {
+ "type": "stdio",
"command": "node",
- "args": ["/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js"]
+ "args": ["./build/index.js"]
}
}
}
```
-**Windows:**
+VS Code may prompt you to trust the MCP server when it detects this file. If prompted, confirm to start the server.
-```json
-{
- "mcpServers": {
- "weather": {
- "command": "node",
- "args": ["C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\index.js"]
- }
- }
-}
-```
-
-This tells Claude for Desktop:
-
-1. There's an MCP server named "weather"
-2. Launch it by running `node /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js`
-
-Save the file, and restart **Claude for Desktop**.
-
-### Test with commands
+To verify, run **MCP: List Servers** from the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`). The `weather` server should show a running status.
-Let's make sure Claude for Desktop is picking up the two tools we've exposed in our `weather` server. You can do this by looking for the "Add files, connectors, and more /" icon.
+### Use the tools
-After clicking on the plus icon, hover over the "Connectors" menu. You should see the `weather` server listed.
-
-If your server isn't being picked up by Claude for Desktop, proceed to the [Troubleshooting](#troubleshooting) section for debugging tips.
-
-If the server has shown up in the "Connectors" menu, you can now test your server by running the following commands in Claude for Desktop:
-
-- What's the weather in Sacramento?
-- What are the active weather alerts in Texas?
+1. Open **Copilot Chat** (`Ctrl+Alt+I` / `Ctrl+Cmd+I`).
+2. Select **Agent** mode from the mode selector at the top of the chat panel.
+3. Click the **Tools** button to confirm `get-alerts` and `get-forecast` appear.
+4. Try these prompts:
+ - "What's the weather in Sacramento?"
+ - "What are the active weather alerts in Texas?"
> [!NOTE]
> Since this is the US National Weather Service, the queries will only work for US locations.
@@ -447,57 +413,30 @@ If the server has shown up in the "Connectors" menu, you can now test your serve
When you ask a question:
-1. The client sends your question to Claude
-2. Claude analyzes the available tools and decides which one(s) to use
+1. The client sends your question to the LLM
+2. The LLM analyzes the available tools and decides which one(s) to use
3. The client executes the chosen tool(s) through the MCP server
-4. The results are sent back to Claude
-5. Claude formulates a natural language response
-6. The response is displayed to you!
+4. The results are sent back to the LLM
+5. The LLM formulates a natural language response
+6. The response is displayed to you
## Troubleshooting
-Claude for Desktop integration issues
-
-**Getting logs from Claude for Desktop**
-
-Claude.app logging related to MCP is written to log files in `~/Library/Logs/Claude`:
+VS Code integration issues
-- `mcp.log` will contain general logging about MCP connections and connection failures.
-- Files named `mcp-server-SERVERNAME.log` will contain error (stderr) logging from the named server.
+**Server not appearing or fails to start**
-You can run the following command to list recent logs and follow along with any new ones:
+1. Verify you have VS Code 1.99 or later (`Help > About`) and that GitHub Copilot is installed.
+2. Verify the server builds without errors: run `npm run build` in the `weather` directory.
+3. Test it manually: run `node build/index.js` — the process should start and wait for input. Press `Ctrl+C` to exit.
+4. Check the server logs: in **MCP: List Servers**, select the server and choose **Show Output**.
+5. If the `node` command is not found, use the full path to the Node binary.
-```bash
-# Check Claude's logs for errors
-tail -n 20 -f ~/Library/Logs/Claude/mcp*.log
-```
-
-**Server not showing up in Claude**
-
-1. Check your `claude_desktop_config.json` file syntax
-2. Make sure the path to your project is absolute and not relative
-3. Restart Claude for Desktop completely
-
-> [!WARNING]
-> To properly restart Claude for Desktop, you must fully quit the application:
->
-> - **Windows**: Right-click the Claude icon in the system tray (which may be hidden in the "hidden icons" menu) and select "Quit" or "Exit".
-> - **macOS**: Use Cmd+Q or select "Quit Claude" from the menu bar.
->
-> Simply closing the window does not fully quit the application, and your MCP server configuration changes will not take effect.
-
-**Tool calls failing silently**
+**Tools don't appear in Copilot Chat**
-If Claude attempts to use the tools but they fail:
-
-1. Check Claude's logs for errors
-2. Verify your server builds and runs without errors
-3. Try restarting Claude for Desktop
-
-**None of this is working. What do I do?**
-
-Please refer to our [debugging guide](https://modelcontextprotocol.io/legacy/tools/debugging) for better debugging tools and more detailed guidance.
+1. Confirm you're in **Agent** mode (not Ask or Edit mode).
+2. Run **MCP: Reset Cached Tools** from the Command Palette, then recheck the **Tools** list.
@@ -524,9 +463,6 @@ This isn't an error - it just means there are no current weather alerts for that
-> [!NOTE]
-> For more advanced troubleshooting, check out our guide on [Debugging MCP](https://modelcontextprotocol.io/legacy/tools/debugging).
-
## Next steps
Now that your server is running locally, here are some ways to go further:
diff --git a/examples/client/src/clientGuide.examples.ts b/examples/client/src/clientGuide.examples.ts
index 8cefcf2248..91f0afd853 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;
diff --git a/packages/client/src/client/client.examples.ts b/packages/client/src/client/client.examples.ts
index b18c267014..b08694cfbd 100644
--- a/packages/client/src/client/client.examples.ts
+++ b/packages/client/src/client/client.examples.ts
@@ -7,7 +7,12 @@
* @module
*/
+import type { Prompt, Resource, Tool } from '@modelcontextprotocol/core';
+
import { Client } from './client.js';
+import { SSEClientTransport } from './sse.js';
+import { StdioClientTransport } from './stdio.js';
+import { StreamableHTTPClientTransport } from './streamableHttp.js';
/**
* Example: Using listChanged to automatically track tool and prompt updates.
@@ -36,3 +41,154 @@ function ClientOptions_listChanged() {
//#endregion ClientOptions_listChanged
return client;
}
+
+/**
+ * Example: Connect to a local server process over stdio.
+ */
+async function Client_connect_stdio() {
+ //#region Client_connect_stdio
+ const client = new Client({ name: 'my-client', version: '1.0.0' });
+ const transport = new StdioClientTransport({ command: 'my-mcp-server' });
+ await client.connect(transport);
+ //#endregion Client_connect_stdio
+ return client;
+}
+
+/**
+ * Example: Connect with Streamable HTTP, falling back to legacy SSE.
+ */
+async function Client_connect_sseFallback(url: string) {
+ //#region Client_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 Client_connect_sseFallback
+}
+
+/**
+ * Example: Call a tool on the connected server.
+ */
+async function Client_callTool_basic(client: Client) {
+ //#region Client_callTool_basic
+ const result = await client.callTool({
+ name: 'calculate-bmi',
+ arguments: { weightKg: 70, heightM: 1.75 }
+ });
+
+ // Tool-level errors are returned in the result, not thrown
+ if (result.isError) {
+ console.error('Tool error:', result.content);
+ return;
+ }
+
+ console.log(result.content);
+ //#endregion Client_callTool_basic
+}
+
+/**
+ * Example: Access machine-readable structured output from a tool call.
+ */
+async function Client_callTool_structuredOutput(client: Client) {
+ //#region Client_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 Client_callTool_structuredOutput
+}
+
+/**
+ * Example: Handle a sampling request from the server.
+ */
+function Client_setRequestHandler_sampling(client: Client) {
+ //#region Client_setRequestHandler_sampling
+ 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 Client_setRequestHandler_sampling
+}
+
+/**
+ * Example: List tools with cursor-based pagination.
+ */
+async function Client_listTools_pagination(client: Client) {
+ //#region Client_listTools_pagination
+ const allTools: Tool[] = [];
+ let cursor: string | undefined;
+ do {
+ const { tools, nextCursor } = await client.listTools({ cursor });
+ allTools.push(...tools);
+ cursor = nextCursor;
+ } while (cursor);
+ console.log(
+ 'Available tools:',
+ allTools.map(t => t.name)
+ );
+ //#endregion Client_listTools_pagination
+}
+
+/**
+ * Example: List prompts with cursor-based pagination.
+ */
+async function Client_listPrompts_pagination(client: Client) {
+ //#region Client_listPrompts_pagination
+ const allPrompts: Prompt[] = [];
+ let cursor: string | undefined;
+ do {
+ const { prompts, nextCursor } = await client.listPrompts({ cursor });
+ allPrompts.push(...prompts);
+ cursor = nextCursor;
+ } while (cursor);
+ console.log(
+ 'Available prompts:',
+ allPrompts.map(p => p.name)
+ );
+ //#endregion Client_listPrompts_pagination
+}
+
+/**
+ * Example: List resources with cursor-based pagination.
+ */
+async function Client_listResources_pagination(client: Client) {
+ //#region Client_listResources_pagination
+ const allResources: Resource[] = [];
+ let cursor: string | undefined;
+ do {
+ const { resources, nextCursor } = await client.listResources({ cursor });
+ allResources.push(...resources);
+ cursor = nextCursor;
+ } while (cursor);
+ console.log(
+ 'Available resources:',
+ allResources.map(r => r.name)
+ );
+ //#endregion Client_listResources_pagination
+}
diff --git a/packages/client/src/client/client.ts b/packages/client/src/client/client.ts
index 3c434f7234..c2d9996248 100644
--- a/packages/client/src/client/client.ts
+++ b/packages/client/src/client/client.ts
@@ -287,7 +287,30 @@ export class Client extends Protocol {
}
/**
- * Override request handler registration to enforce client-side validation for elicitation.
+ * Registers a handler for server-initiated requests (sampling, elicitation, roots).
+ * The client must declare the corresponding capability for the handler to be accepted.
+ * Replaces any previously registered handler for the same method.
+ *
+ * For `sampling/createMessage` and `elicitation/create`, the handler is automatically
+ * wrapped with schema validation for both the incoming request and the returned result.
+ *
+ * @example Handling a sampling request
+ * ```ts source="./client.examples.ts#Client_setRequestHandler_sampling"
+ * 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'
+ * }
+ * };
+ * });
+ * ```
*/
public override setRequestHandler(
method: M,
@@ -416,6 +439,35 @@ export class Client extends Protocol {
}
}
+ /**
+ * Connects to a server via the given transport and performs the MCP initialization handshake.
+ *
+ * @example Basic usage (stdio)
+ * ```ts source="./client.examples.ts#Client_connect_stdio"
+ * const client = new Client({ name: 'my-client', version: '1.0.0' });
+ * const transport = new StdioClientTransport({ command: 'my-mcp-server' });
+ * await client.connect(transport);
+ * ```
+ *
+ * @example Streamable HTTP with SSE fallback
+ * ```ts source="./client.examples.ts#Client_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 };
+ * }
+ * ```
+ */
override async connect(transport: Transport, options?: RequestOptions): Promise {
await super.connect(transport);
// When transport sessionId is already set this means we are trying to reconnect.
@@ -655,22 +707,47 @@ export class Client extends Protocol {
assertClientRequestTaskCapability(this._capabilities.tasks?.requests, method, 'Client');
}
+ /** Sends a ping to the server to check connectivity. */
async ping(options?: RequestOptions) {
return this.request({ method: 'ping' }, EmptyResultSchema, options);
}
+ /** Requests argument autocompletion suggestions from the server for a prompt or resource. */
async complete(params: CompleteRequest['params'], options?: RequestOptions) {
return this.request({ method: 'completion/complete', params }, CompleteResultSchema, options);
}
+ /** Sets the minimum severity level for log messages sent by the server. */
async setLoggingLevel(level: LoggingLevel, options?: RequestOptions) {
return this.request({ method: 'logging/setLevel', params: { level } }, EmptyResultSchema, options);
}
+ /** Retrieves a prompt by name from the server, passing the given arguments for template substitution. */
async getPrompt(params: GetPromptRequest['params'], options?: RequestOptions) {
return this.request({ method: 'prompts/get', params }, GetPromptResultSchema, options);
}
+ /**
+ * Lists available prompts. Results may be paginated — loop on `nextCursor` to collect all pages.
+ *
+ * Returns an empty list if the server does not advertise prompts capability
+ * (or throws if {@linkcode ClientOptions.enforceStrictCapabilities} is enabled).
+ *
+ * @example
+ * ```ts source="./client.examples.ts#Client_listPrompts_pagination"
+ * const allPrompts: Prompt[] = [];
+ * let cursor: string | undefined;
+ * do {
+ * const { prompts, nextCursor } = await client.listPrompts({ cursor });
+ * allPrompts.push(...prompts);
+ * cursor = nextCursor;
+ * } while (cursor);
+ * console.log(
+ * 'Available prompts:',
+ * allPrompts.map(p => p.name)
+ * );
+ * ```
+ */
async listPrompts(params?: ListPromptsRequest['params'], options?: RequestOptions) {
if (!this._serverCapabilities?.prompts && !this._enforceStrictCapabilities) {
// Respect capability negotiation: server does not support prompts
@@ -680,6 +757,27 @@ export class Client extends Protocol {
return this.request({ method: 'prompts/list', params }, ListPromptsResultSchema, options);
}
+ /**
+ * Lists available resources. Results may be paginated — loop on `nextCursor` to collect all pages.
+ *
+ * Returns an empty list if the server does not advertise resources capability
+ * (or throws if {@linkcode ClientOptions.enforceStrictCapabilities} is enabled).
+ *
+ * @example
+ * ```ts source="./client.examples.ts#Client_listResources_pagination"
+ * const allResources: Resource[] = [];
+ * let cursor: string | undefined;
+ * do {
+ * const { resources, nextCursor } = await client.listResources({ cursor });
+ * allResources.push(...resources);
+ * cursor = nextCursor;
+ * } while (cursor);
+ * console.log(
+ * 'Available resources:',
+ * allResources.map(r => r.name)
+ * );
+ * ```
+ */
async listResources(params?: ListResourcesRequest['params'], options?: RequestOptions) {
if (!this._serverCapabilities?.resources && !this._enforceStrictCapabilities) {
// Respect capability negotiation: server does not support resources
@@ -689,6 +787,12 @@ export class Client extends Protocol {
return this.request({ method: 'resources/list', params }, ListResourcesResultSchema, options);
}
+ /**
+ * Lists available resource URI templates for dynamic resources. Results may be paginated — see {@linkcode listResources | listResources()} for the cursor pattern.
+ *
+ * Returns an empty list if the server does not advertise resources capability
+ * (or throws if {@linkcode ClientOptions.enforceStrictCapabilities} is enabled).
+ */
async listResourceTemplates(params?: ListResourceTemplatesRequest['params'], options?: RequestOptions) {
if (!this._serverCapabilities?.resources && !this._enforceStrictCapabilities) {
// Respect capability negotiation: server does not support resources
@@ -700,22 +804,59 @@ export class Client extends Protocol {
return this.request({ method: 'resources/templates/list', params }, ListResourceTemplatesResultSchema, options);
}
+ /** Reads the contents of a resource by URI. */
async readResource(params: ReadResourceRequest['params'], options?: RequestOptions) {
return this.request({ method: 'resources/read', params }, ReadResourceResultSchema, options);
}
+ /** Subscribes to change notifications for a resource. The server must support resource subscriptions. */
async subscribeResource(params: SubscribeRequest['params'], options?: RequestOptions) {
return this.request({ method: 'resources/subscribe', params }, EmptyResultSchema, options);
}
+ /** Unsubscribes from change notifications for a resource. */
async unsubscribeResource(params: UnsubscribeRequest['params'], options?: RequestOptions) {
return this.request({ method: 'resources/unsubscribe', params }, EmptyResultSchema, options);
}
/**
- * Calls a tool and waits for the result. Automatically validates structured output if the tool has an `outputSchema`.
+ * Calls a tool on the connected server and returns the result. Automatically validates structured output
+ * if the tool has an `outputSchema`.
+ *
+ * Tool results have two error surfaces: `result.isError` for tool-level failures (the tool ran but reported
+ * a problem), and thrown {@linkcode ProtocolError} for protocol-level failures or {@linkcode SdkError} for
+ * SDK-level issues (timeouts, missing capabilities).
*
* For task-based execution with streaming behavior, use {@linkcode ExperimentalClientTasks.callToolStream | client.experimental.tasks.callToolStream()} instead.
+ *
+ * @example Basic usage
+ * ```ts source="./client.examples.ts#Client_callTool_basic"
+ * const result = await client.callTool({
+ * name: 'calculate-bmi',
+ * arguments: { weightKg: 70, heightM: 1.75 }
+ * });
+ *
+ * // Tool-level errors are returned in the result, not thrown
+ * if (result.isError) {
+ * console.error('Tool error:', result.content);
+ * return;
+ * }
+ *
+ * console.log(result.content);
+ * ```
+ *
+ * @example Structured output
+ * ```ts source="./client.examples.ts#Client_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 }
+ * }
+ * ```
*/
async callTool(
params: CallToolRequest['params'],
@@ -820,6 +961,27 @@ export class Client extends Protocol {
return this._cachedToolOutputValidators.get(toolName);
}
+ /**
+ * Lists available tools. Results may be paginated — loop on `nextCursor` to collect all pages.
+ *
+ * Returns an empty list if the server does not advertise tools capability
+ * (or throws if {@linkcode ClientOptions.enforceStrictCapabilities} is enabled).
+ *
+ * @example
+ * ```ts source="./client.examples.ts#Client_listTools_pagination"
+ * const allTools: Tool[] = [];
+ * let cursor: string | undefined;
+ * do {
+ * const { tools, nextCursor } = await client.listTools({ cursor });
+ * allTools.push(...tools);
+ * cursor = nextCursor;
+ * } while (cursor);
+ * console.log(
+ * 'Available tools:',
+ * allTools.map(t => t.name)
+ * );
+ * ```
+ */
async listTools(params?: ListToolsRequest['params'], options?: RequestOptions) {
if (!this._serverCapabilities?.tools && !this._enforceStrictCapabilities) {
// Respect capability negotiation: server does not support tools
@@ -894,6 +1056,7 @@ export class Client extends Protocol {
this.setNotificationHandler(notificationMethod, handler);
}
+ /** Notifies the server that the client's root list has changed. Requires the `roots.listChanged` capability. */
async sendRootsListChanged() {
return this.notification({ method: 'notifications/roots/list_changed' });
}