| title | Client |
|---|
This guide covers SDK usage for building MCP clients in TypeScript. For protocol-level details and message formats, see the MCP specification.
The SDK provides a {@linkcode @modelcontextprotocol/client!client/client.Client | Client} class from @modelcontextprotocol/client that connects to MCP servers over different transports:
- Streamable HTTP – for remote HTTP servers.
- stdio – for local processes you spawn.
- SSE – for legacy HTTP+SSE servers (deprecated).
For a feature‑rich starting point, see simpleStreamableHttp.ts.
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.
For remote HTTP servers, use {@linkcode @modelcontextprotocol/client!client/streamableHttp.StreamableHTTPClientTransport | 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);Note
For a full interactive client over Streamable HTTP, see simpleStreamableHttp.ts.
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:
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 {@linkcode @modelcontextprotocol/client!client/sse.SSEClientTransport | 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 };
}Note
For a complete example with error reporting, see streamableHttpWithSseFallbackClient.ts.
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.
{@linkcode @modelcontextprotocol/client!client/authExtensions.ClientCredentialsProvider | 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);{@linkcode @modelcontextprotocol/client!client/authExtensions.PrivateKeyJwtProvider | 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 });Note
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.
Note
For a complete working OAuth flow, see simpleOAuthClient.ts and simpleOAuthClientProvider.ts.
For protocol details, see Authorization in the MCP specification.
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.
Note
For a full runnable client exercising tools, resources, and prompts, see simpleStreamableHttp.ts.
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:
const { tools } = await client.listTools();
console.log(
'Available tools:',
tools.map(t => t.name)
);
const result = await client.callTool({
name: 'calculate-bmi',
arguments: { weightKg: 70, heightM: 1.75 }
});
console.log(result.content);Note
See Tools in the MCP specification for the full protocol details.
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:
const { resources } = await client.listResources();
console.log(
'Available resources:',
resources.map(r => r.name)
);
const { contents } = await client.readResource({ uri: 'config://app' });
for (const item of contents) {
console.log(item);
}Note
See Resources in the MCP specification for the full protocol details.
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:
const { prompts } = await client.listPrompts();
console.log(
'Available prompts:',
prompts.map(p => p.name)
);
const { messages } = await client.getPrompt({
name: 'review-code',
arguments: { code: 'console.log("hello")' }
});
console.log(messages);Note
See Prompts in the MCP specification for the full protocol details.
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:
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. Compared to manually handling notifications, 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 {@linkcode @modelcontextprotocol/client!client/client.Client#setNotificationHandler | setNotificationHandler()}:
// Server log messages (e.g. from ctx.mcpReq.log() in tool handlers)
client.setNotificationHandler('notifications/message', notification => {
const { level, data } = notification.params;
console.log(`[${level}]`, data);
});
// Server's resource list changed — re-fetch the list
client.setNotificationHandler('notifications/resources/list_changed', async () => {
const { resources } = await client.listResources();
console.log('Resources changed:', resources.length);
});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.
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).
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:
const client = new Client(
{ name: 'my-client', version: '1.0.0' },
{
capabilities: {
sampling: {},
elicitation: { form: {} }
}
}
);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:
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'
}
};
});Note
See Sampling in the MCP specification for the full protocol details.
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' }:
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' };
});Note
For a full form‑based elicitation handler with AJV validation, see simpleStreamableHttp.ts. For URL elicitation mode, see elicitationUrlExample.ts and the Capabilities guide.
For protocol details, see Elicitation in the MCP specification.
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:
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);Note
For an end‑to‑end example of server‑initiated SSE disconnection and automatic client reconnection with event replay, see ssePollingClient.ts.
The sections above cover the essentials. The table below links to additional capabilities.
| Feature | Description | Reference |
|---|---|---|
| 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 |
| Tasks (experimental) | Long‑running tool calls with status streaming | simpleTaskInteractiveClient.ts, Capabilities guide |