> ## Documentation Index
> Fetch the complete documentation index at: https://fastmcp-ts.docs.prefect.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Client basics

> Connect to MCP servers, call tools, and manage connection lifecycle

The `Client` class connects to any MCP server and exposes its tools, resources, and prompts:

```typescript theme={null}
import { Client } from '@prefecthq/fastmcp-ts/client'

const client = await Client.connect('http://localhost:3000')

const tools     = await client.listTools()
const resources = await client.listResources()
const prompts   = await client.listPrompts()

const result = await client.callTool('add', { a: 1, b: 2 })
const config = await client.readResource('config://settings')
const review = await client.getPrompt('review_code', { code: 'const x = 1' })

await client.close()
```

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](/clients/transports#transport-inputs). 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](/clients/handlers), and a [sampling adapter](/clients/sampling) connects the sampling request to a real LLM. Connecting to a protected server adds [authentication](/clients/auth); connecting to several at once gives you a [multi-server client](/clients/multi-server).

## 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.

```typescript theme={null}
const client = new Client('http://localhost:3000')
await client.connect()   // refCount → 1
await client.connect()   // refCount → 2
await client.close()     // refCount → 1, still open
await client.close()     // refCount → 0, connection closed
```

The client implements `Symbol.asyncDispose`, so `await using` closes it automatically on scope exit:

```typescript theme={null}
await using client = await Client.connect('http://localhost:3000')
const result = await client.callTool('add', { a: 1, b: 2 })
// closed automatically here
```

## Protocol era

Every connection speaks one of two [protocol eras](/concepts/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.

```typescript theme={null}
import { Client } from '@prefecthq/fastmcp-ts/client'

const client = await Client.connect('http://localhost:3000', {
  versionNegotiation: { mode: 'auto' },   // modern when supported, else legacy
})

client.getProtocolEra()   // 'modern' or 'legacy'
```

The `fastmcp` [CLI](/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:

```typescript theme={null}
import { ToolCallError } from '@prefecthq/fastmcp-ts/client'

try {
  await client.callTool('risky', {})
} catch (err) {
  if (err instanceof ToolCallError) {
    console.error(err.content)   // ContentBlock[] from the server
  }
}
```

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:

```typescript theme={null}
import { toResult } from '@prefecthq/fastmcp-ts/client'

const result = await toResult(client.callTool('add', { a: 1, b: 2 }))
if (result.ok) console.log(result.value)
else console.error(result.error)
```

## 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:

```typescript theme={null}
const client = await Client.connect('http://localhost:3000', {
  defaultOptions: { timeout: 30, tool: { timeout: 120 } },
})

await client.callTool('slow', {}, { timeout: 300 })   // overrides for this call
```

## Narrow interfaces

`Client` implements three segregated interfaces — `IToolsClient`, `IResourcesClient`, and `IPromptsClient` — so functions can declare only the capability they need:

```typescript theme={null}
import type { IToolsClient } from '@prefecthq/fastmcp-ts/client'

async function callSomeTool(client: IToolsClient) {
  return client.callTool('add', { a: 1, b: 2 })
}
```
