| shape | how-to |
|---|
A client holds one connection to one server: construct a Client, pick a transport, and connect().
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.
:::
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.
:::
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.
:::
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.
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.
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. close()tears down the transport and rejects in-flight requests.- Protocol-revision differences live on the protocol versions page, not here.