MCP TypeScript SDK (V2)
    Preparing search index...

    Building MCP clients

    This guide covers the TypeScript SDK APIs for building MCP clients. For protocol-level concepts, see the MCP overview.

    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.

    The examples below use these imports. Adjust based on which features and transport you need:

    import type { Prompt, Resource, Tool } from '@modelcontextprotocol/client';
    import {
    applyMiddlewares,
    CallToolResultSchema,
    Client,
    ClientCredentialsProvider,
    createMiddleware,
    PrivateKeyJwtProvider,
    ProtocolError,
    SdkError,
    SdkErrorCode,
    SSEClientTransport,
    StdioClientTransport,
    StreamableHTTPClientTransport
    } from '@modelcontextprotocol/client';

    For remote HTTP servers, use StreamableHTTPClientTransport:

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

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

    await client.connect(transport);

    For a full interactive client over Streamable HTTP, see simpleStreamableHttp.ts.

    For local, process-spawned servers (Claude Desktop, CLI tools), use StdioClientTransport. The transport spawns the server process and communicates over stdin/stdout:

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

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

    await client.connect(transport);

    To support both modern Streamable HTTP and legacy SSE servers, try StreamableHTTPClientTransport first and fall back to SSEClientTransport on failure:

    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 };
    }

    For a complete example with error reporting, see streamableHttpWithSseFallbackClient.ts.

    Call await client.close() to disconnect. Pending requests are rejected with a CONNECTION_CLOSED error.

    For Streamable HTTP, terminate the server-side session first (per the MCP specification):

    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).

    Servers can provide an instructions string during initialization that describes how to use them — cross-tool relationships, workflow patterns, and constraints (see Instructions in the MCP specification). Retrieve it after connecting and include it in the model's system prompt:

    const instructions = client.getInstructions();

    const systemPrompt = ['You are a helpful assistant.', instructions].filter(Boolean).join('\n\n');

    console.log(systemPrompt);

    MCP servers can require OAuth 2.0 authentication before accepting client connections (see Authorization in the MCP specification). Pass an authProvider to StreamableHTTPClientTransport to enable this — the SDK provides built-in providers for common machine-to-machine flows, or you can implement the full OAuthClientProvider interface for user-facing OAuth.

    ClientCredentialsProvider handles the client_credentials grant flow for service-to-service communication:

    const authProvider = new ClientCredentialsProvider({
    clientId: 'my-service',
    clientSecret: 'my-secret'
    });

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

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

    await client.connect(transport);

    PrivateKeyJwtProvider signs JWT assertions for the private_key_jwt token endpoint auth method, avoiding a shared client secret:

    const authProvider = new PrivateKeyJwtProvider({
    clientId: 'my-service',
    privateKey: pemEncodedKey,
    algorithm: 'RS256'
    });

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

    For a runnable example supporting both auth methods via environment variables, see simpleClientCredentials.ts.

    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 a complete working OAuth flow, see simpleOAuthClient.ts and simpleOAuthClientProvider.ts.

    Tools are callable actions offered by servers — discovering and invoking them is usually how your client enables an LLM to take action (see Tools in the MCP overview).

    Use listTools() to discover available tools, and callTool() to invoke one. Results may be paginated — loop on nextCursor to collect all pages:

    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:',
    allTools.map(t => t.name)
    );

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

    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:

    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 }
    }

    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:

    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 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 in the MCP overview).

    Use listResources() and readResource() to discover and read server-provided data. Results may be paginated — loop on nextCursor to collect all pages:

    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:',
    allResources.map(r => r.name)
    );

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

    To discover URI templates for dynamic resources, use listResourceTemplates().

    If the server supports resource subscriptions, use subscribeResource() to receive notifications when a resource changes, then re-read it:

    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 are reusable message templates that servers offer to help structure interactions with models (see Prompts in the MCP overview).

    Use listPrompts() and getPrompt() to list available prompts and retrieve them with arguments. Results may be paginated — loop on nextCursor to collect all pages:

    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:',
    allPrompts.map(p => p.name)
    );

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

    Both prompts and resources can support argument completions. Use complete() to request autocompletion suggestions from the server as a user types:

    const { completion } = await client.complete({
    ref: {
    type: 'ref/prompt',
    name: 'review-code'
    },
    argument: {
    name: 'language',
    value: 'type'
    }
    });
    console.log(completion.values); // e.g. ['typescript']

    The 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:

    const client = new Client(
    { name: 'my-client', version: '1.0.0' },
    {
    listChanged: {
    tools: {
    onChanged: (error, tools) => {
    if (error) {
    console.error('Failed to refresh tools:', error);
    return;
    }
    console.log('Tools updated:', tools);
    }
    },
    prompts: {
    onChanged: (error, prompts) => console.log('Prompts updated:', prompts)
    }
    }
    }
    );

    For full control — or for notification types not covered by listChanged (such as log messages) — register handlers directly with setNotificationHandler():

    // Server log messages (sent by the server during request processing)
    client.setNotificationHandler('notifications/message', notification => {
    const { level, data } = notification.params;
    console.log(`[${level}]`, data);
    });

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

    To control the minimum severity of log messages the server sends, use setLoggingLevel():

    await client.setLoggingLevel('warning');
    
    Warning

    listChanged and setNotificationHandler() are mutually exclusive per notification type — using both for the same notification will cause the manual handler to be overwritten.

    MCP is bidirectional — servers can send requests to the client during tool execution, as long as the client declares matching capabilities (see Architecture in the MCP overview). Declare the corresponding capability when constructing the Client and register a request handler:

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

    When a server needs an LLM completion during tool execution, it sends a sampling/createMessage request to the client (see Sampling in the MCP overview). Register a handler to fulfill it:

    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'
    }
    };
    });

    When a server needs user input during tool execution, it sends an elicitation/create request to the client (see Elicitation in the MCP overview). The client should present the form to the user and return the collected data, or { action: 'decline' }:

    client.setRequestHandler('elicitation/create', async request => {
    console.log('Server asks:', request.params.message);

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

    return { action: 'decline' };
    });

    For a full form-based elicitation handler with AJV validation, see simpleStreamableHttp.ts. For URL elicitation mode, see elicitationUrlExample.ts.

    Roots let the client expose filesystem boundaries to the server (see Roots in the MCP overview). Declare the roots capability and register a roots/list 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 client.sendRootsListChanged().

    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:

    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;
    }
    }

    ProtocolError represents JSON-RPC errors from the server (method not found, invalid params, internal error). SdkError represents local SDK errors — REQUEST_TIMEOUT, CONNECTION_CLOSED, CAPABILITY_NOT_SUPPORTED, and others.

    Set client.onerror to catch out-of-band transport errors (SSE disconnects, parse errors). Set client.onclose to detect when the connection drops — pending requests are rejected with a CONNECTION_CLOSED error:

    // 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');
    };

    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 SdkErrorCode.RequestTimeout:

    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');
    }
    }

    Use createMiddleware() and 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:

    const authMiddleware = createMiddleware(async (next, input, init) => {
    const headers = new Headers(init?.headers);
    headers.set('X-Custom-Header', 'my-value');
    return next(input, { ...init, headers });
    });

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

    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:

    let lastToken: string | undefined;

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

    For an end-to-end example of server-initiated SSE disconnection and automatic client reconnection with event replay, see ssePollingClient.ts.

    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 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:

    For a full runnable example, see simpleTaskInteractiveClient.ts.

    • examples/client/ — Full runnable client examples
    • Server guide — Building MCP servers with this SDK
    • MCP overview — Protocol-level concepts: participants, layers, primitives
    • Migration guide — Upgrading from previous SDK versions
    • FAQ — Frequently asked questions and troubleshooting
    Feature Description Example
    Parallel tool calls Run multiple tool calls concurrently via Promise.all parallelToolCallsClient.ts
    SSE disconnect / reconnection Server-initiated SSE disconnect with automatic reconnection and event replay ssePollingClient.ts
    Multiple clients Independent client lifecycles to the same server multipleClientsParallel.ts
    URL elicitation Handle sensitive data collection via browser elicitationUrlExample.ts