Client class connects to any MCP server and exposes its tools, resources, and prompts:
Client.connect() is wherever the server lives: a URL, a stdio subprocess, an in-process FastMCP instance, or an mcpServers config. The client detects the right transport from what you pass, so the rest of the API is identical no matter where the server runs.
The relationship runs both ways. Beyond consuming primitives, a client also answers requests the server initiates: when a tool calls ctx.sample() the client performs the inference, and when it calls ctx.elicit() the client collects input. You wire those responses with handlers, and a sampling adapter connects the sampling request to a real LLM. Connecting to a protected server adds authentication; connecting to several at once gives you a multi-server client.
Lifecycle
Connections are ref-counted: multiple callers can share one client. Eachconnect() increments the count, each close() decrements it; the underlying connection is established on the first connect and torn down when the count reaches zero. isConnected() reports whether the underlying connection is live.
Symbol.asyncDispose, so await using closes it automatically on scope exit:
Protocol era
Every connection speaks one of two protocol eras: the legacy era, which is the 2025 protocol, or the modern era, protocol revision 2026-07-28. A server serves both at once. A client picks one per connection through theversionNegotiation option, and the choice is made when you connect.
The default is { mode: 'auto' } on every transport, HTTP and stdio alike. Client.connect('http://localhost:3000') probes the server once with server/discover and uses the modern era on definitive modern evidence; anything else falls back to the plain legacy initialize handshake, so a 2025-only server keeps working with no configuration. The probe is stall-safe in the SDK: over stdio it runs against a short-lived sibling process and a timeout falls back to legacy, while over HTTP a probe timeout rejects with a typed error rather than hanging.
Pass { mode: 'legacy' } to opt out: no probe, and the connect sequence is byte-identical to 2025 behavior. Pass { mode: { pin: '2026-07-28' } } to require the modern era outright; connect() throws -32022 UnsupportedProtocolVersion if the server cannot speak it. After connecting, getProtocolEra() reports the negotiated result: 'modern', 'legacy', or undefined before you connect.
fastmcp CLI follows the same default on every transport, and its --legacy flag maps to { mode: 'legacy' }.
Client capabilities
Client capabilities tell a server which optional protocol features the client can actually use. FastMCP infers capabilities for configured sampling and elicitation handlers and for roots. You can add capabilities for other features throughClientOptions.capabilities. FastMCP preserves your settings while ensuring capabilities backed by configured handlers and roots remain enabled, so advertising an extension does not remove inferred support.
Extensions live in the extensions capability map. Each key is a namespaced extension identifier, and its value contains settings defined by that extension. Advertise only extensions your host implements. Extensions are opt-in, and a server that does not recognize one can continue with core MCP behavior.
Error handling
callTool() throws a ToolCallError when the server returns isError: true. The error carries the server’s content blocks on its content field:
callToolRaw() returns the full result including isError and never throws. For an error-as-value style across any call, the standalone toResult() utility wraps a promise into a discriminated union — { ok: true, value } on success, { ok: false, error } on failure:
Timeouts
ClientOptions.defaultOptions accepts per-scope timeout defaults — tool.timeout, resource.timeout, prompt.timeout, and a global timeout fallback. Timeouts are in seconds in the public API. A per-request timeout in the call’s options takes precedence:
Narrow interfaces
Client implements three segregated interfaces — IToolsClient, IResourcesClient, and IPromptsClient — so functions can declare only the capability they need: