---
name: ai-mcp
description: >
  Host-side Model Context Protocol (MCP) for TanStack AI: connect to
  external MCP servers, host your own tools with createMCPServer,
  discover and run tools inside any adapter's chat() loop, read resources
  and prompts, generate TypeScript types (typed tool names/pool keys)
  with the bundled CLI, and manage lifecycle with close()/await using.
type: sub-skill
library: tanstack-ai
library_version: '0.2.5'
sources:
  - 'TanStack/ai:docs/tools/mcp.md'
  - 'TanStack/ai:packages/ai-mcp/src/client.ts'
  - 'TanStack/ai:packages/ai-mcp/src/pool.ts'
  - 'TanStack/ai:packages/ai-mcp/src/resources.ts'
  - 'TanStack/ai:packages/ai-mcp/src/transport.ts'
  - 'TanStack/ai:packages/ai-mcp/src/server/create-server.ts'
  - 'TanStack/ai:packages/ai-mcp/src/server/stdio.ts'
---

# `@tanstack/ai-mcp`

This skill covers the `@tanstack/ai-mcp` package. Read `ai-core/tool-calling/SKILL.md`
first — MCP tools flow into `chat()` the same way hand-written tools do.
If you host the tools yourself, use `createMCPServer`.

## When to use this package

Use `@tanstack/ai-mcp` when:

- A third-party MCP server exposes tools you want an agent or chat loop to call.
- You want to read MCP server resources (files, text, data) or prompts into a
  `chat()` message list.
- You want generated TypeScript types for an external MCP server's tool
  signatures (via the bundled `generate` CLI).
- You are running tool execution on the server side and want to connect to MCP
  servers with HTTP (Streamable HTTP or SSE) or stdio transports.
- You want to expose your own tools as an MCP server over HTTP or stdio.

Do NOT use this package for browser/client-side code — MCP connections are
server-side only.

## Install

```bash
pnpm add @tanstack/ai-mcp
```

The package has these subpaths:

- `.` exports `createMCPClient`, `createMCPClients`, converters, and types.
- `./stdio` exports the Node-only client transport `stdioTransport`.
- `./server` exports `createMCPServer`.
- `./server/stdio` exports `serveMCPStdio`.
- `./apps` exports `createMcpAppCallHandler`.

Import `./stdio` and `./server/stdio` only from Node code.
Those entries use Node I/O.

## Host an MCP server

Import `createMCPServer` from `@tanstack/ai-mcp/server`.
Pass tools from `toolDefinition().server()`.
Call `server.fetch(request)` in your HTTP route.

```typescript
import { toolDefinition } from '@tanstack/ai'
import { createMCPServer } from '@tanstack/ai-mcp/server'
import { z } from 'zod'

const getWeather = toolDefinition({
  name: 'get_weather',
  description: 'Current weather for a city',
  inputSchema: z.object({ city: z.string() }),
}).server(async ({ city }) => {
  return { city, temperature: 18, conditions: 'clear' }
})

const server = createMCPServer({
  name: 'weather',
  version: '1.0.0',
  tools: [getWeather],
})

export function handleMcp(request: Request) {
  return server.fetch(request)
}

// Mount handleMcp on GET, POST, and DELETE.
// GET is the spec 2025 stream.
// DELETE closes a spec 2025 session.
```

`createMCPServer` speaks spec `2026-07-28`.
`createMCPServer` also speaks spec 2025. By default it keeps no spec 2025 session.

`stdioTransport` from `@tanstack/ai-mcp/stdio` connects your client to a command.
`serveMCPStdio` from `@tanstack/ai-mcp/server/stdio` serves your server on stdin and stdout.
Write logs with `console.error`.
stdout carries only protocol messages.

```typescript
import { toolDefinition } from '@tanstack/ai'
import { createMCPServer } from '@tanstack/ai-mcp/server'
import { serveMCPStdio } from '@tanstack/ai-mcp/server/stdio'
import { z } from 'zod'

const getWeather = toolDefinition({
  name: 'get_weather',
  description: 'Current weather for a city',
  inputSchema: z.object({ city: z.string() }),
}).server(async ({ city }) => {
  return { city, temperature: 18, conditions: 'clear' }
})

const server = createMCPServer({
  name: 'weather',
  version: '1.0.0',
  tools: [getWeather],
})

serveMCPStdio(server)
```

You can also pass `resources` and `prompts`.
Build them with `resourceDefinition` and `promptDefinition` from `@tanstack/ai-mcp/server`.
A resource `read(uri, variables, ctx)` gets the requested URI, the template variables, and `ctx.context` (the `handle` context plus `authInfo`).
A template resource can take `list(ctx)`, which returns `{ resources }` for `resources/list`.

A tool reads its hooks on `ctx.context`.
Give `.server()` the type `MCPToolContext` from `@tanstack/ai-mcp/server`.
Then `ctx.context.requestInput` and `ctx.context.sample` type-check.
On spec 2026, `ctx.context.requestInput` throws, and the handler returns `input_required`.
The client runs the tool again with the answer.
Code before `requestInput` runs on each call, so it can run more than once.
Put work that must run once after `requestInput` returns.
On spec 2026, a tool asks one question per call. A second `requestInput` throws an Error.
If the user declines or cancels, `requestInput` throws an Error, and the call ends with a tool error.
In an `execution: 'task'` tool, `ctx.context.requestInput` throws an error.
On spec 2025 with `sessions: 'memory'`, `requestInput` waits on the open session. Without a session, it throws.
The same tool call then continues.

```typescript
import { toolDefinition } from '@tanstack/ai'
import { createMCPServer } from '@tanstack/ai-mcp/server'
import type { MCPToolContext } from '@tanstack/ai-mcp/server'
import { z } from 'zod'

const askCity = toolDefinition({
  name: 'ask_city',
  description: 'Ask which city to use',
  inputSchema: z.object({}),
}).server<MCPToolContext>(async (_args, ctx) => {
  const city = await ctx.context.requestInput({ message: 'Which city?' })
  return { city }
})

const server = createMCPServer({
  name: 'weather',
  version: '1.0.0',
  tools: [askCity],
})

export function handleMcp(request: Request) {
  return server.fetch(request)
}
```

Mount `handleMcp` on GET, POST, and DELETE.

If a tool calls `ctx.context.sample` on spec 2026, pass `sample` to `createMCPServer`.
On spec 2026, `ctx.context.sample` calls the `sample` function.
On spec 2025, `ctx.context.sample` asks the MCP client.
A tool with `execution: 'task'` returns a spec 2025 task handle.
Spec 2026 has no tasks, so that tool runs inline there.

### Require a bearer token

The server is an OAuth resource server. Your authorization server issues the token.
Pass `auth` with a `verifier`. It is the `OAuthTokenVerifier` type from the MCP SDK.
`jwtVerifier` checks a JWT against the JWKS of the provider.
`introspectionVerifier` checks an opaque token at an RFC 7662 endpoint.
A missing or bad token returns 401. A token without a scope in `requiredScopes` returns 403.
A tool reads the token as `ctx.context.authInfo`.
Sessions and tasks belong to the `clientId` plus the `sub` claim of the token.
Serve the OAuth discovery documents with `oauthMetadataResponse` at the app root.

```typescript
import { createMCPServer, jwtVerifier } from '@tanstack/ai-mcp/server'

const server = createMCPServer({
  name: 'notes',
  version: '1.0.0',
  auth: {
    verifier: jwtVerifier({
      jwksUrl: 'https://auth.example.com/.well-known/jwks.json',
      issuer: 'https://auth.example.com/',
      audience: 'https://mcp.example.com/mcp',
    }),
    requiredScopes: ['notes:read'],
  },
})
```

### Use the auth the app already has

When a middleware already verified the caller, pass the result to `server.handle`.
`server.fetch(request)` stays a plain Fetch handler. `server.handle` takes options.
`options.authInfo` is the SDK `AuthInfo`. The server skips its `auth` gate for that request.
`options.context` reaches every tool call, resource read, and resource list of that request on `ctx.context`.
Type the values with `MCPToolContext<{ db: Db }>`.
`authInfo`, `requestInput`, and `sample` win over a same-named value in `context`.

```typescript
import { server } from './mcp-server'
import { verifyCaller } from './auth'

export async function handleMcp(request: Request) {
  const caller = await verifyCaller(request)
  if (caller instanceof Response) return caller
  return server.handle(request, {
    authInfo: caller.authInfo,
    context: { db: caller.db },
  })
}
```

### Describe a tool to the host

Set `metadata.title` and `metadata.annotations` on the tool definition.
The host gets them as the MCP tool title and annotations.
Use the MCP names: `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`.
A host skips its confirmation for a tool with `readOnlyHint: true`.
Set `metadata._meta` to send the MCP tool `_meta`, for example `{ ui: { resourceUri: 'ui://view' } }` for an MCP Apps view.
Pass `onerror` to `createMCPServer` to log transport and protocol errors from the SDK. `serveMCPStdio` also sends its transport errors there.

A tool with no `outputSchema` can return an MCP `CallToolResult`.
The server sends it as is: its content blocks, its `structuredContent`, and its `isError`.

### Spec 2025 on a host with many instances

The default is `sessions: 'stateless'`. It works on a host with many instances, such as Cloudflare Workers.
A new server answers each spec 2025 request, and no session is kept.
In that mode, `ctx.context.requestInput` throws for a spec 2025 client.
`ctx.context.sample` calls the `sample` option, or throws when it is not set.
Set `sessions: 'reject'` to serve spec 2026 only. A spec 2025 request then gets the SDK rejection.
Set `sessions: 'memory'` to keep spec 2025 sessions in the process for 30 idle minutes. Route them with sticky sessions on the `mcp-session-id` header.
`serveMCPStdio` uses `'memory'` when `sessions` is not set.

### Call a `createMCPServer` server with its types

For a deployed server, pass `typeof server` and a transport.
Import the server with `import type`, so its code stays out of the app.
The client speaks MCP, so the server `auth` option runs.
`callTool` accepts only the server tool names and their input types.
`callTool` returns the raw MCP result.
For a tool with an `outputSchema`, `structuredContent` has the tool output type.
`getPrompt` accepts only the server prompt names and their argument types.
`readResource` accepts only the server resource URIs.

```typescript
import { createMCPClient } from '@tanstack/ai-mcp'
import type { server } from './mcp-server'

const remote = await createMCPClient<typeof server>({
  transport: { type: 'http', url: 'https://mcp.example.com/api/mcp' },
})
await remote.callTool('get_weather', { city: 'Paris' })
```

`createMCPClient({ server })` is a different client.
It calls the tool functions in the same process and returns the tool output, parsed with the `outputSchema`.
`readResource(uri, context)` puts `context` on the resource `ctx.context`. Without it, `ctx.context` is `{}`.
It opens no connection, and the server `auth` option does not run.
It has no `tools()`, so do not pass it to `chat()`.
Use it only when the app and the server run in one process.

## `createMCPClient` — single server

```typescript
import { createMCPClient } from '@tanstack/ai-mcp'

const client = await createMCPClient({
  transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
  prefix: 'weather', // optional: prefixes all tool names (e.g. 'weather_get_forecast')
  name: 'my-app', // optional: client identity sent to the server
})
```

`createMCPClient` connects immediately and returns an `MCPClient`.
If the connection fails, `createMCPClient` throws `MCPConnectionError`.
`createMCPClient` tries spec `2026-07-28` first.
If the server does not support that spec, the client uses the 2025 initialize handshake.
The client keeps negotiation mode `auto`.

### Transports

#### Streamable HTTP (default for internet-facing servers)

```typescript
import { createMCPClient } from '@tanstack/ai-mcp'

const client = await createMCPClient({
  transport: {
    type: 'http',
    url: 'https://mcp.example.com/mcp',
    headers: { Authorization: 'Bearer sk-...' },
  },
})
```

#### SSE

```typescript
import { createMCPClient } from '@tanstack/ai-mcp'

const client = await createMCPClient({
  transport: {
    type: 'sse',
    url: 'https://mcp.example.com/sse',
    headers: { Authorization: 'Bearer sk-...' },
  },
})
```

#### stdio (Node-only — import from `/stdio` subpath)

```typescript
import { createMCPClient } from '@tanstack/ai-mcp'
import { stdioTransport } from '@tanstack/ai-mcp/stdio'

const client = await createMCPClient({
  transport: stdioTransport({
    command: 'npx',
    args: ['-y', 'my-mcp-server'],
    env: { API_KEY: process.env.API_KEY ?? '' },
  }),
})
```

#### Custom transport (escape hatch)

Pass any `Transport` from `@modelcontextprotocol/client`:

```typescript
// InMemoryTransport comes from @modelcontextprotocol/client.
// @tanstack/ai-mcp re-exports it. Any Transport from that package works here.
import { createMCPClient, InMemoryTransport } from '@tanstack/ai-mcp'

const [clientTransport] = InMemoryTransport.createLinkedPair()
const client = await createMCPClient({ transport: clientTransport })
```

### Authentication

Two levels:

- **Static tokens** — pass `headers` on the `http`/`sse` config (sent with
  every request): `headers: { Authorization: 'Bearer ...' }`.
- **OAuth 2.1 (MCP authorization spec).** Pass `authProvider` on the
  `http` or `sse` config. The value is an `OAuthClientProvider` from
  `@modelcontextprotocol/client`. The transport attaches tokens, refreshes
  them, and retries on 401.

```typescript
import { createMCPClient } from '@tanstack/ai-mcp'
// An OAuthClientProvider from @modelcontextprotocol/client.
// You persist the tokens on the server.
import { myOAuthProvider } from './oauth-provider'

const client = await createMCPClient({
  transport: {
    type: 'http',
    url: 'https://mcp.example.com/mcp',
    authProvider: myOAuthProvider,
  },
})
```

Caveat: interactive authorization-code flows need `transport.finishAuth(code)`,
and `createMCPClient` does not expose its internal transport. For redirect
flows, construct the `StreamableHTTPClientTransport` yourself with the
`authProvider`, keep a reference, call `finishAuth(code)` in the OAuth
callback route, then pass the transport via the escape hatch above. For
server-side providers backed by pre-provisioned/refreshable tokens, the
config form is sufficient.
Import `StreamableHTTPClientTransport` from `@modelcontextprotocol/client`.

## Three type-safety modes

### Mode 1 — Auto-discovery (no types needed)

`client.tools()` lists every tool the server exposes. Args are typed `unknown`
at compile time but the tool's JSON Schema is forwarded to the LLM.

```typescript
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient } from '@tanstack/ai-mcp'

const client = await createMCPClient({
  transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
})

const tools = await client.tools()
// tools: McpServerTool[]  (args unknown)

const stream = chat({
  adapter: openaiText('gpt-5.5'),
  messages: [{ role: 'user', content: 'What is the weather in Paris?' }],
  tools,
})
```

Use `{ lazy: true }` to defer schema sending via the existing `LazyToolManager`:

```typescript
import { createMCPClient } from '@tanstack/ai-mcp'

const client = await createMCPClient({
  transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
})

const tools = await client.tools({ lazy: true })
```

### Mode 2 — Typed via `toolDefinition` instances

Pass bare `toolDefinition()` instances (no `.server()` call) to `client.tools([...])`.
The MCP client binds a `callTool` proxy as the execute function while
input/output validation and TypeScript types come from the definitions' Zod schemas.
Only the named tools are returned (allowlist = the definitions' `name`s).
Throws `MCPToolNotFoundError` if the server does not expose a tool with that name.

```typescript
import { toolDefinition } from '@tanstack/ai'
import { createMCPClient } from '@tanstack/ai-mcp'
import { z } from 'zod'

const getWeatherDef = toolDefinition({
  name: 'get_weather',
  description: 'Current weather for a city',
  inputSchema: z.object({ city: z.string() }),
  outputSchema: z.object({ temperature: z.number(), conditions: z.string() }),
})

const client = await createMCPClient({
  transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
})

// Returns MappedServerTools<typeof defs> — fully typed per definition.
const tools = await client.tools([getWeatherDef])
```

### Mode 3 — Generated types (via `generate` CLI)

Run `npx @tanstack/ai-mcp generate` to introspect live servers and emit a
`ServerDescriptor` interface per server. Pass the generated interface as the
generic to `createMCPClient<WeatherServer>(...)` to narrow discovered tool
names to the server's literals (args stay untyped — use Mode 2 for typed args).

See the "Codegen CLI" section below for details.

## Tool policy: `toolFilter` and `needsApproval`

By default every server tool reaches the model and runs without approval.
Set a policy on the client. It applies in `tools()`, in `chat({ mcp })`, and
per server in `createMCPClients`. Both callbacks receive the raw MCP tool
definition (native unprefixed `name`, `title`, `annotations`).

```typescript
import { createMCPClient } from '@tanstack/ai-mcp'

const mcp = await createMCPClient({
  transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
  // Hide tools from the model. Unannotated tools fail this check.
  toolFilter: (tool) => tool.annotations?.readOnlyHint === true,
  // Pause for approval before these tools run (auto-discovery only).
  needsApproval: (tool) => tool.annotations?.destructiveHint !== false,
})
```

- `toolFilter` also applies to `tools([defs])`: a hidden definition throws
  `MCPToolNotFoundError`. MCP Apps widget calls also honor it. It does not
  apply to `callTool()`.
- `needsApproval` does not change `tools([defs])`: each `toolDefinition` keeps
  its own `needsApproval`.
- MCP Apps widget calls have no approval step, so the call handler refuses a
  tool that `needsApproval` marks (`{ ok: false, error: 'Tool needs approval: <name>' }`).
- Annotations are server-declared hints. For an untrusted server, filter by
  `tool.name` instead.

## Lifecycle

**The caller owns the lifecycle.** `chat()` never closes the client.

Tools execute lazily while the response stream is consumed — close only after
the stream is drained. In a streaming route handler, `try/finally` around the
`return` (or `await using` at function scope) closes the client before the
body streams; use a middleware terminal hook there instead (see Common
Mistakes below).

```typescript
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import type { ModelMessage } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient } from '@tanstack/ai-mcp'

// Option 1: middleware terminal hooks (streaming route handlers)
export async function POST(request: Request) {
  const { messages } = await request.json()
  const client = await createMCPClient({
    transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
  })
  const stream = chat({
    adapter: openaiText('gpt-5.5'),
    messages,
    tools: await client.tools(),
    middleware: [
      {
        name: 'mcp-close',
        onFinish: () => client.close(),
        onAbort: () => client.close(),
        onError: () => client.close(),
      },
    ],
  })
  return toServerSentEventsResponse(stream)
}

// Option 2: explicit close after in-scope consumption
export async function runToCompletion(messages: Array<ModelMessage>) {
  const client = await createMCPClient({
    transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
  })
  try {
    const stream = chat({
      adapter: openaiText('gpt-5.5'),
      messages,
      tools: await client.tools(),
    })
    for await (const chunk of stream) {
      // stream fully consumed inside this block
    }
  } finally {
    await client.close()
  }
}

// Option 3: await using (TypeScript 5.2+ with Symbol.asyncDispose) —
// same rule: consume the stream before the scope exits.
export async function runWithUsing(messages: Array<ModelMessage>) {
  await using client = await createMCPClient({
    transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
  })
  const stream = chat({
    adapter: openaiText('gpt-5.5'),
    messages,
    tools: await client.tools(),
  })
  for await (const chunk of stream) {
    // ... consume the stream in this scope; close() runs at scope exit
  }
}
```

## `chat({ mcp })` — discovery + lifecycle in one prop

Rather than calling `client.tools()` and `client.close()` yourself, pass the
`mcp` option to `chat()` and let it manage the full lifecycle.

```typescript
// ChatMCPOptions shape:
// mcp: {
//   clients: Array<MCPClient | MCPClients>,
//   connection?: 'close' | 'keep-alive',  // default: 'close'
//   lazyTools?: boolean,
//   onDiscoveryError?: (error: unknown, source) => void,
// }
```

**Behavior:**

- `chat()` calls `.tools()` on every entry in `clients` at run start and merges
  all results into the tool list.
- `lazyTools: true` is forwarded to `tools({ lazy: true })`.
- `connection: 'close'` (default) — each client is closed when the run ends
  (after the agent loop completes and the stream is drained). With
  `'keep-alive'`, `chat()` never closes the clients — the caller owns their
  lifecycle (keep connections warm across requests).
- `onDiscoveryError`: throw (or re-throw) to abort the entire call; return
  normally to skip that source and continue. Omitting the handler re-throws
  (fail-fast).

**When to use `mcp` vs. the tools spread:**

| Approach                                                | Use when                                                                  |
| ------------------------------------------------------- | ------------------------------------------------------------------------- |
| `chat({ mcp: { clients: [...] } })`                     | Convenience: discovery + lifecycle handled for you; untyped args are fine |
| `tools: [...await client.tools([toolDefinition(...)])]` | Fully-typed args/results via Zod schemas (`toolDefinition` mode)          |

**Server-side example:**

```typescript
// Any framework route handler that receives a Request works (TanStack Start,
// Next.js, Hono, ...).
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient } from '@tanstack/ai-mcp'

// Created once at module scope; connection: 'keep-alive' below keeps it warm
// across requests.
const mcpClient = await createMCPClient({
  transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
})

export async function POST(request: Request) {
  const { messages } = await request.json()

  const stream = chat({
    adapter: openaiText('gpt-5.5'),
    messages,
    mcp: {
      clients: [mcpClient],
      connection: 'keep-alive', // chat() won't close it — reuse across requests
      onDiscoveryError: (err, source) => {
        console.warn('MCP discovery failed for source, skipping:', err)
        // returning skips this source; throw to fail the whole call fast
      },
    },
  })

  return toServerSentEventsResponse(stream)
  // connection: 'keep-alive' — chat() never closes mcpClient; it stays warm for the next request.
}
```

You can also pass an `MCPClients` pool directly:

```typescript
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClients } from '@tanstack/ai-mcp'

const pool = await createMCPClients({
  github: { transport: { type: 'http', url: 'https://mcp.github.com/mcp' } },
  linear: { transport: { type: 'http', url: 'https://mcp.linear.app/mcp' } },
})

export async function POST(request: Request) {
  const { messages } = await request.json()

  const stream = chat({
    adapter: openaiText('gpt-5.5'),
    messages,
    mcp: { clients: [pool], connection: 'keep-alive' },
  })

  return toServerSentEventsResponse(stream)
}
```

## MCP input request

When `chat()` receives an MCP input request, the run outcome is an interrupt.
The stream ends with `RUN_FINISHED`.
The outcome type is `interrupt`.
The stream does not emit `RUN_ERROR` for this pause.

Read each interrupt whose `reason` is `mcp_input`.
The payload key is `tanstack:interruptPayload`.
`form` means the server asks the user for input.
`sampling` means the server asks for a model result.
The interrupt id is `mcp_input_` plus the tool call id.

```typescript
import { chat, INTERRUPT_PAYLOAD_METADATA_KEY } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient } from '@tanstack/ai-mcp'

const client = await createMCPClient({
  transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
})

try {
  const stream = chat({
    adapter: openaiText('gpt-5.5'),
    messages: [{ role: 'user', content: 'What is the weather in Paris?' }],
    tools: await client.tools(),
  })

  for await (const chunk of stream) {
    if (chunk.type !== 'RUN_FINISHED') continue
    if (chunk.outcome?.type !== 'interrupt') continue

    for (const item of chunk.outcome.interrupts) {
      if (item.reason !== 'mcp_input') continue
      const payload = item.metadata?.[INTERRUPT_PAYLOAD_METADATA_KEY]
      if (typeof payload !== 'object' || payload === null) continue
      if (!('kind' in payload)) continue
      // payload.kind is 'form' or 'sampling'
      // payload.request is the MCP input body
    }
  }
} finally {
  await client.close()
}
```

The interrupt has a generic binding.
In `useChat`, the item `kind` is `generic`.
Call `resolveInterrupt(answer)` or `cancel()` on the item.
For a `form`, the answer is an object that matches `request.requestedSchema`.
A `createMCPServer` server asks for `{ value: string }`.
For `sampling`, the answer is the reply text or a full `CreateMessageResult`.
The route must pass `parentRunId` and `resume` to `chat()`.
The next run calls the tool again, and the tool reads `ctx.inputResponse`.
On spec 2026, the MCP client sends that answer with `inputResponses` and the server `requestState`.
On spec 2025, the tool call fails, because `chat()` cannot pause that call.

## `createMCPClients` — multiple servers

Connect to many MCP servers in parallel. Each config key becomes the default
prefix for that server's tools, preventing name collisions across servers.

```typescript
import { createMCPClients } from '@tanstack/ai-mcp'

await using pool = await createMCPClients({
  github: { transport: { type: 'http', url: 'https://mcp.github.com/mcp' } },
  linear: { transport: { type: 'http', url: 'https://mcp.linear.app/mcp' } },
})

// Tool names auto-prefixed: 'github_search_repos', 'linear_create_issue', etc.
const tools = await pool.tools()

// Forward lazy flag to every server:
const lazyTools = await pool.tools({ lazy: true })

// Per-server typed access (keys are typed as string here; generated
// MCPServers types make them literal — see Codegen CLI below):
const githubTools = await pool.clients.github!.tools()
```

`createMCPClients` connects in parallel, closes already-connected clients if
any connection fails (no leaks), and throws `MCPConnectionError` naming the
failed server(s).

Override or disable prefixing:

```typescript
import { createMCPClients } from '@tanstack/ai-mcp'

await using pool = await createMCPClients({
  // 'gh_search_repos'
  github: {
    transport: { type: 'http', url: 'https://mcp.github.com/mcp' },
    prefix: 'gh',
  },
  // 'create_issue' (no prefix)
  linear: {
    transport: { type: 'http', url: 'https://mcp.linear.app/mcp' },
    prefix: '',
  },
})
```

## Abort signal — cancelling in-flight MCP calls

TanStack AI stops waiting for MCP tool calls when the chat run's
`AbortController` fires (e.g. client disconnect, server abort). The
`abortSignal` is threaded through `ToolExecutionContext` into every tool call
with no extra code. For a task-required tool, aborting stops the local task
stream and sends a best-effort `tasks/cancel` for a remote task the MCP
server has already created. Cancel is best-effort: a server that ignores
`tasks/cancel` may keep running until TTL.

You can also read it in a hand-written server tool that wraps an MCP call:

```typescript
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'

const fetchData = toolDefinition({
  name: 'fetch_data',
  description: 'Fetch a record from a slow upstream API',
  inputSchema: z.object({ id: z.string() }),
})

const myTool = fetchData.server(async (args, ctx) => {
  // Forward to any async work that accepts an AbortSignal.
  const result = await fetch(`https://slow.api/data/${args.id}`, {
    signal: ctx?.abortSignal,
  })
  return result.json()
})
```

## Resources

```typescript
import { createMCPClient, mcpResourceToContentPart } from '@tanstack/ai-mcp'

const client = await createMCPClient({
  transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
})

// List all resources the server exposes.
const resources = await client.resources()

// Read a specific resource by URI.
const resource = await client.readResource(resources[0]!.uri)

// Convert one content block to a TanStack ContentPart.
const part = mcpResourceToContentPart(resource.contents[0]!)
// part: ContentPart  (type: 'text' always for v1)
```

Inject resources into a chat turn:

```typescript
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient, mcpResourceToContentPart } from '@tanstack/ai-mcp'

const client = await createMCPClient({
  transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
})
const resource = await client.readResource('file:///project/README.md')
const parts = resource.contents.map(mcpResourceToContentPart)

const stream = chat({
  adapter: openaiText('gpt-5.5'),
  messages: [
    {
      role: 'user',
      content: [
        ...parts,
        { type: 'text', content: 'Summarize this document.' },
      ],
    },
  ],
})
```

## Prompts

```typescript
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient, mcpPromptToMessages } from '@tanstack/ai-mcp'

const client = await createMCPClient({
  transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
})

// List prompts the server exposes.
const prompts = await client.prompts()

// Get a prompt (with optional arguments).
const prompt = await client.getPrompt('review_code', { language: 'TypeScript' })

// Convert to TanStack ModelMessage[] for use in chat().
const messages = mcpPromptToMessages(prompt)
// messages: ModelMessage[]  (role: 'user' | 'assistant')

const stream = chat({
  adapter: openaiText('gpt-5.5'),
  messages: [...messages, { role: 'user', content: 'Review src/index.ts.' }],
})
```

## MCP Apps

MCP Apps let an MCP tool surface a UI widget (static or interactive) on the
client. Two variants exist. See `docs/mcp/apps.md` for the full guide.

### Static widgets — `UIResourcePart`

When an MCP tool result carries a `ui://` resource, TanStack AI emits a
`UIResourcePart` on the **assistant `UIMessage`**, alongside the normal
`ToolCallPart` / `ToolResultPart`. It is purely presentational — it never
enters model input. The resource is read eagerly during the `chat()` run; if
the read fails the tool result still flows to the model and the widget is
simply absent (fail-soft). Static widgets require the MCP source to expose
`readResource` — both `createMCPClient` and a `createMCPClients` pool do.

```typescript
import type { UIResourcePart } from '@tanstack/ai'

// UIResourcePart shape (on the assistant UIMessage):
// {
//   type: 'ui-resource'
//   resource: { uri: string; mimeType: string; text?: string; blob?: string }
//   serverId?: string     // pool prefix / config key — routes interactive calls
//   toolCallId: string    // links to the originating tool call
//   toolName: string      // server-native MCP tool name whose UI this renders
//   meta?: Record<string, unknown>  // reserved — currently always undefined
// }
```

### Interactive apps — `createMcpAppCallHandler`

For interactive apps (the widget iframe posts tool-call / prompt / link
actions back), mount `createMcpAppCallHandler` from `@tanstack/ai-mcp/apps`
at a POST route. Pass the MCP client(s) you already created — a single
`MCPClient`, an `MCPClients` pool, or an array of either. The handler reads
each client's transport descriptor via `client.getInfo()` /
`pool.getServers()` (pure config, not a live socket) and **reconnects
per-call** (stateless / serverless-safe). It matches the widget-supplied
native (unprefixed) tool name against the server's unprefixed tool names,
enforces a same-server allowlist, and returns `{ ok: true, result }` or
`{ ok: false, error }`.

For a pool, the `serverId` on the `UIResourcePart` is the config key (the
tool prefix); for a single client it is the client's `prefix` (or the sole
default when `serverId` is absent and there is exactly one client).

```typescript group=mcp-app-handler
import { createMCPClients } from '@tanstack/ai-mcp'
import {
  createMcpAppCallHandler,
  inMemoryMcpSessionStore,
} from '@tanstack/ai-mcp/apps'

// Reuse the same pool you pass to chat({ mcp: { clients: [mcp] } }).
const mcp = await createMCPClients({
  weather: {
    transport: { type: 'http', url: 'https://mcp-app.example.com/mcp' },
  },
})

// Minimal — reconnect-per-call via getServers() descriptor.
const handler = createMcpAppCallHandler({ clients: mcp })

// Options:
// clients   — MCPClient | MCPClients | Array<MCPClient | MCPClients> (required).
//             The handler reads transport descriptors via client.getInfo() /
//             pool.getServers() — the client does not need a live connection.
// store     — optional dynamic/stateful session store (e.g.
//             inMemoryMcpSessionStore()); used alongside clients.
// allowTool — optional authorizer receiving the WHOLE request:
//             (req: McpAppCallRequest) => boolean | Promise<boolean>.
//             The server-exposure check is ALWAYS enforced (the handler
//             rejects any tool the server does not expose). `allowTool`
//             is an ADDITIONAL restriction AND-ed on top: a request must
//             satisfy BOTH the server-exposure check and allowTool.
const handlerWithStore = createMcpAppCallHandler({
  clients: mcp,
  store: inMemoryMcpSessionStore(),
  allowTool: (req) => req.toolName === 'place_order',
})
```

The handler invokes the server (`body: { threadId, serverId?, toolName, args?, messageId? }`):

```typescript group=mcp-app-handler
export async function POST(request: Request) {
  const body = await request.json()
  const result = await handler(body)
  // { ok: true; result: unknown } | { ok: false; error: string }
  return Response.json(result)
}
```

### Client side — `useMcpAppBridge` + `MCPAppResource`

In React/Preact, create the bridge with the `useMcpAppBridge` hook (from
`@tanstack/ai-react` / `@tanstack/ai-preact`) — it returns a **stable** bridge
per `threadId`/`callEndpoint` and always calls your latest `sendMessage`/`onLink`,
so the bridge isn't recreated on every render (no `useMemo` / `exhaustive-deps`
by hand). It's a thin wrapper over the framework-agnostic `createMcpAppBridge`
from `@tanstack/ai-client` (use that directly outside React/Preact). Render
resources with `MCPAppResource` from `@tanstack/ai-react/mcp-apps` (also
`@tanstack/ai-preact/mcp-apps`, which requires a `preact/compat` alias).
`MCPAppResource` uses `@mcp-ui/client`'s `AppRenderer` under the hood — React
only. Solid, Vue, Svelte, and Angular renderers are deferred.

The bridge exposes `{ callTool, sendPrompt, openLink }` and routes the
iframe's actions: `tool` → POST to `callEndpoint`; `prompt` →
`chat.sendMessage`; `link` → `onLink(url)` if provided, otherwise the link
is dropped (with a console warning) and `openLink` returns `{ isError: true }`
— it does NOT hang. `toolName` is read from `part.toolName`; it is not a
prop. Omit `bridge` for display-only (inert) rendering.

```tsx
import { useChat, useMcpAppBridge } from '@tanstack/ai-react'
import { fetchServerSentEvents } from '@tanstack/ai-client'
import { MCPAppResource } from '@tanstack/ai-react/mcp-apps'

function ChatPage() {
  const threadId = 'weather-chat'
  const { messages, sendMessage } = useChat({
    connection: fetchServerSentEvents('/api/chat'),
  })

  const bridge = useMcpAppBridge({
    threadId,
    callEndpoint: '/api/mcp-app/call',
    chat: { sendMessage: async (content) => void sendMessage({ content }) },
    // Opt in to link navigation — absent means links are dropped.
    onLink: (url) => window.open(url, '_blank', 'noopener'),
  })

  return (
    <div>
      {messages.map((msg) =>
        msg.parts.map((part, i) => {
          if (part.type === 'text') return <p key={i}>{part.content}</p>
          if (part.type === 'ui-resource') {
            return (
              <MCPAppResource
                key={i}
                part={part}
                bridge={bridge}
                sandbox={{ url: new URL('https://sandbox.example.com') }}
                // toolInput is optional; toolName comes from part.toolName.
              />
            )
          }
          return null
        }),
      )}
    </div>
  )
}
```

## Codegen CLI

Generate TypeScript types (typed tool names and pool keys) by introspecting live MCP servers.

**1. Create `mcp.config.ts` at your project root:**

```typescript
import { defineConfig } from '@tanstack/ai-mcp'

export default defineConfig({
  servers: {
    github: {
      transport: { type: 'http', url: 'https://mcp.github.com/mcp' },
      // prefix must match the runtime createMCPClient({ prefix }) value
    },
  },
  outFile: './src/mcp-types.generated.ts',
})
```

**2. Run the generator:**

```bash
npx @tanstack/ai-mcp generate
```

This connects to each server, lists its tools/resources/prompts, converts JSON
Schemas to TypeScript, and writes one `interface <Name>Server extends ServerDescriptor`
per server plus a combined `interface MCPServers` for pool typing.

**3. Use the generated types:**

```typescript
// Single server — narrows tools() return to descriptor-keyed tool names.
import type { GithubServer } from './src/mcp-types.generated'
import { createMCPClient, createMCPClients } from '@tanstack/ai-mcp'

const client = await createMCPClient<GithubServer>({
  transport: { type: 'http', url: 'https://mcp.github.com/mcp' },
})
const tools = await client.tools() // typed to GithubServer's tool names

// Multiple servers via the generated MCPServers map.
import type { MCPServers } from './src/mcp-types.generated'

const pool = await createMCPClients<MCPServers>({
  github: { transport: { type: 'http', url: 'https://mcp.github.com/mcp' } },
})
// pool.clients.github is MCPClient<GithubServer>
// missing/extra keys are a compile error
```

Codegen deps (`json-schema-to-typescript`, `jiti`) are bundled into the CLI bin
and do NOT appear in the library's runtime dependency graph.

## Error classes

- `MCPConnectionError` — thrown when a server connection fails or when calling
  methods after `close()`.
- `MCPToolNotFoundError` — thrown from `client.tools([defs])` when a definition's
  `name` is not exposed by the server, or the client's `toolFilter` hides it.
- `MCPTaskRequiredToolError` — thrown when a task-required tool is bound via
  `tools([defs])` or called via `callTool()` and the server does not declare
  the tasks capability for `tools/call`. Auto-discovery skips those tools
  instead of throwing.
- `DuplicateToolNameError` — thrown by a single pool's own `tools()` when two
  tools within that pool share the same name (same server or pool clients with no
  prefix). Exported from `@tanstack/ai-mcp`.
- `MCPDuplicateToolNameError` — thrown by `chat()` when tools from separate
  `mcp.clients` entries collide after merging. Exported from `@tanstack/ai`
  (not `@tanstack/ai-mcp`), so users can `instanceof` it at the `chat()` call site.

```typescript
import {
  MCPConnectionError,
  MCPToolNotFoundError,
  MCPTaskRequiredToolError,
  DuplicateToolNameError,
} from '@tanstack/ai-mcp'

import { MCPDuplicateToolNameError } from '@tanstack/ai'
```

## Complete server-route example

```typescript
// src/routes/api.chat.ts — mount POST in your framework's route handler
// (TanStack Start server route, Next.js route handler, Hono, ...).
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClients } from '@tanstack/ai-mcp'

export async function POST(request: Request) {
  const { messages } = await request.json()

  const pool = await createMCPClients({
    github: {
      transport: { type: 'http', url: 'https://mcp.github.com/mcp' },
    },
    linear: {
      transport: {
        type: 'http',
        url: 'https://mcp.linear.app/mcp',
        headers: {
          Authorization: `Bearer ${process.env.LINEAR_KEY ?? ''}`,
        },
      },
    },
  })

  const stream = chat({
    adapter: openaiText('gpt-5.5'),
    messages,
    tools: await pool.tools(),
    // Close after the run ends — tools execute while the response streams,
    // so `await using` / try-finally would close the pool too early here.
    middleware: [
      {
        name: 'mcp-close',
        onFinish: () => pool.close(),
        onAbort: () => pool.close(),
        onError: () => pool.close(),
      },
    ],
  })

  return toServerSentEventsResponse(stream)
}
```

## Common Mistakes

### a. HIGH: closing the client before the stream finishes

`chat()` executes tools lazily as the model calls them during streaming.
If you close the MCP client before the response stream is fully consumed,
in-flight tool calls will fail.

Wrong:

```typescript
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient } from '@tanstack/ai-mcp'

export async function POST(request: Request) {
  const { messages } = await request.json()
  const client = await createMCPClient({
    transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
  })
  const tools = await client.tools()
  const stream = chat({ adapter: openaiText('gpt-5.5'), messages, tools })
  await client.close() // closes before the stream runs tools
  return toServerSentEventsResponse(stream)
}
```

This includes `try/finally` around the `return`, and `await using` at function
scope — both close before the returned `Response` body streams.

Correct — close in middleware terminal hooks (exactly one of
`onFinish`/`onAbort`/`onError` fires per run), or consume the stream in scope
before closing:

```typescript
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient } from '@tanstack/ai-mcp'

export async function POST(request: Request) {
  const { messages } = await request.json()
  const client = await createMCPClient({
    transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
  })

  const stream = chat({
    adapter: openaiText('gpt-5.5'),
    messages,
    tools: await client.tools(),
    middleware: [
      {
        name: 'mcp-close',
        onFinish: () => client.close(),
        onAbort: () => client.close(),
        onError: () => client.close(),
      },
    ],
  })
  return toServerSentEventsResponse(stream)
}
```

### b. HIGH: importing `stdioTransport` from the main entry point

`stdioTransport` is only available from `@tanstack/ai-mcp/stdio`. Importing it
from `@tanstack/ai-mcp` will fail with a module-not-found error and would
bundle Node.js child-process code into edge bundles.

Wrong:

```typescript ignore
import { stdioTransport } from '@tanstack/ai-mcp' // does not exist here
```

Correct:

```typescript
import { stdioTransport } from '@tanstack/ai-mcp/stdio'
```

### c. MEDIUM: using `client.tools([defs])` without matching names

The name field on each `toolDefinition` must exactly match the tool name the MCP
server exposes. Mismatches throw `MCPToolNotFoundError` at call time, not at
type-check time (unless generated types are in use).

### d. MEDIUM: not setting a prefix when multiple servers share tool names

Two different errors can arise depending on where the collision is detected:

- **Within a single `createMCPClients` pool** — calling `pool.tools()` throws
  `DuplicateToolNameError` (from `@tanstack/ai-mcp`) when two servers in that
  pool expose the same name with no prefix to separate them.
- **Across separate `mcp.clients` entries in `chat()`** — `chat()` throws
  `MCPDuplicateToolNameError` (from `@tanstack/ai`) after merging discovered
  tools from all `mcp.clients` entries.

In both cases, the fix is the same: use `createMCPClients` (which auto-prefixes
by config key) or set an explicit `prefix` on each `createMCPClient` call.

### e. HIGH: importing `@modelcontextprotocol/sdk`

Use `@modelcontextprotocol/client` for client transports.
Use `@modelcontextprotocol/server` for server helpers.
`@tanstack/ai-mcp` re-exports `InMemoryTransport` from the client package.

Wrong:

```typescript ignore
import { InMemoryTransport } from '@modelcontextprotocol/sdk'
```

Correct:

```typescript
import { InMemoryTransport } from '@modelcontextprotocol/client'
```

## Cross-References

- See also: ai-core/tool-calling/SKILL.md — MCP tools are ServerTools; all tool
  patterns (approval, lazy, client-side) apply.
- See also: ai-core/chat-experience/SKILL.md — wiring tools into `chat()`.
