Skip to main content
The Client class connects to any MCP server and exposes its tools, resources, and prompts:
A client mirrors a server. The server registers tools, resources, and prompts; the client consumes those same three primitives — it calls tools, reads resources, and fetches prompts. The argument to 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. Each connect() 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.
The client implements 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 the versionNegotiation option, and the choice is made when you connect. The default is 'legacy' on every transport, HTTP and stdio alike. Client.connect('http://localhost:3000') runs the plain 2025 sequence — byte-identical to today’s behavior, with no probe and no new headers — so existing code keeps working unchanged until you ask for more. The default never probes, which also avoids a stall: a server/discover probe against an unresponsive legacy server can hang. You opt into the modern era explicitly. Pass { mode: 'auto' } to probe the server with server/discover and use the modern era when the server supports it, falling back to legacy otherwise. 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.
The fastmcp CLI applies its own per-transport defaults on top of the library — an HTTP URL defaults to { mode: 'auto' } there. That is a CLI choice, not a library default. The Client class always defaults to 'legacy' unless you set versionNegotiation yourself.

Error handling

callTool() throws a ToolCallError when the server returns isError: true. The error carries the server’s content blocks on its content field:
When you would rather inspect the failure than catch it, 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: