TTypeScript SDKSource ↗
A client holds one connection to one server: construct a Client , pick a transport , and connect() .

Connect to a server

A client holds one connection to one server: construct a Client, pick a transport, and connect().

Create a client and connect over HTTP

Client takes a name and a version; StreamableHTTPClientTransport takes the server's MCP endpoint URL.

import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';

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

connect() runs the initialize handshake and resolves once it completes. The client now holds the negotiated protocol version, the server's capabilities, and its instructions.

::: info Coming from v1? Client and the transport classes keep their names — only the import paths moved, to @modelcontextprotocol/client and its /stdio subpath. Run the codemod, then see the upgrade guide. :::

Connect to a local process over stdio

For a server you run as a child process, change only the transport: StdioClientTransport, imported from @modelcontextprotocol/client/stdio, spawns the command and speaks JSON-RPC over its stdin and 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);

server.js runs as a child of your process. close() shuts it down in order: close stdin, then SIGTERM, then SIGKILL.

::: tip InMemoryTransport.createLinkedPair() is the third transport: it links a Client and an McpServer inside one process, no network and no child process. Test a server builds on it. :::

Fall back to SSE for servers that predate Streamable HTTP

An SSE-only server speaks the older HTTP+SSE transport instead of Streamable HTTP. Try StreamableHTTPClientTransport first; when it fails, retry with SSEClientTransport on a fresh Client.

try {
    const client = new Client({ name: 'my-client', version: '1.0.0' });
    await client.connect(new StreamableHTTPClientTransport(new URL(url)));
    return client;
} catch {
    const client = new Client({ name: 'my-client', version: '1.0.0' });
    await client.connect(new SSEClientTransport(new URL(url)));
    return client;
}

Whichever branch returns, the Client behaves the same from here on — nothing downstream depends on the transport.

::: info versionNegotiation in ClientOptions controls which protocol revision connect() negotiates — see Protocol versions. :::

Read what the server told you at connect time

Three accessors return what the server declared during the handshake; all of them return undefined until connect() resolves.

console.log(client.getServerVersion());
console.log(client.getServerCapabilities());
console.log(client.getInstructions());

Connected to a server named travel that registered one tool and set instructions, that prints:

{ name: 'travel', version: '2.1.0' }
{ tools: { listChanged: true } }
Call list-trips before book-trip. Dates are ISO 8601.

The capability object gates every verb on the next page: only ask for what the server advertised. getInstructions() is the server's usage guide for the model — put it in the system prompt.

A fourth accessor, getDiscoverResult(), tells the eras apart at connect time. Present, it is the modern-era DiscoverResult — persistable with JSON.stringify and usable on a later connect as prior: { kind: 'modern', discover } to skip the probe. Absent on a connected client, the era is legacy. This page's client used the default legacy handshake:

// The default mode ran the legacy initialize handshake — no DiscoverResult.
console.log(client.getDiscoverResult());
undefined

Under versionNegotiation: { mode: 'auto' } against a 2026-era server it returns the advertisement — see Protocol versions for the cached-verdict shapes and Caching discovery verdicts for the full host-side loop.

Disconnect cleanly

Over Streamable HTTP, terminate the server-side session, then close the client.

await transport.terminateSession();
await client.close();

close() tears down the transport and rejects every request still in flight with a CONNECTION_CLOSED error. terminateSession() returns without sending anything when the server never issued a session ID. On the other transports, close() alone is the whole teardown.

Recap

  • new Client({ name, version }), a transport, and connect() are the whole setup; connect() runs the initialize handshake.
  • StreamableHTTPClientTransport connects to remote servers; StdioClientTransport, from @modelcontextprotocol/client/stdio, spawns local ones; SSEClientTransport is the fallback for SSE-only servers.
  • InMemoryTransport.createLinkedPair() links a client and a server in one process.
  • After connect(), getServerVersion(), getServerCapabilities(), and getInstructions() return what the server declared; getDiscoverResult() tells the eras apart (present = modern, absent = legacy).
  • close() tears down the transport and rejects in-flight requests.
  • Protocol-revision differences live on the protocol versions page, not here.

Source: docs/clients/connect.md

Generated by smolify with deterministic-repository-import-v1 · 1 source file