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, andconnect()are the whole setup;connect()runs theinitializehandshake.StreamableHTTPClientTransportconnects to remote servers;StdioClientTransport, from@modelcontextprotocol/client/stdio, spawns local ones;SSEClientTransportis the fallback for SSE-only servers.InMemoryTransport.createLinkedPair()links a client and a server in one process.- After
connect(),getServerVersion(),getServerCapabilities(), andgetInstructions()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