- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
Manual MCP: typed tools, resources & prompts
Call the MCP client yourself when you need typed tool arguments, resources, or prompts. Pass the results into chat().
On this path, you own the client and you close it. If you only need tools, and you want chat() to close the client, use Managed MCP with chat().
Quick setup: typed tools#
- Describe the tool with toolDefinition(). The name must match the tool name on the server.
- Pass the definition to mcp.tools([...]), and spread the result into tools.
- Close the client in a middleware hook.
// src/routes/api.chat.ts
import { createFileRoute } from '@tanstack/react-router'
import { chat, toServerSentEventsResponse, toolDefinition } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient } from '@tanstack/ai-mcp'
import { z } from 'zod'
const searchDef = toolDefinition({
name: 'search',
description: 'Search for items',
inputSchema: z.object({ query: z.string() }),
outputSchema: z.array(z.object({ id: z.string(), title: z.string() })),
})
export const Route = createFileRoute('/api/chat')({
server: {
handlers: {
POST: async ({ request }) => {
const { messages, threadId } = await request.json()
const mcp = await createMCPClient({
transport: { type: 'http', url: process.env.MCP_URL! },
})
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages,
threadId,
// Typed MCP tools, merged with any other tools you pass
tools: [...(await mcp.tools([searchDef]))],
// Close after the run ends. Tools run while the response streams.
middleware: [
{
name: 'mcp-close',
onFinish: () => mcp.close(),
onAbort: () => mcp.close(),
onError: () => mcp.close(),
},
],
})
return toServerSentEventsResponse(stream)
},
},
},
})The search tool now has the argument type { query: string }, and Zod checks the arguments at runtime.
How it works#
- mcp.tools([searchDef]) returns only the tools that you name. If a name is not on the server, it throws MCPToolNotFoundError. See Mode 2: explicit definitions.
- chat() runs tools while the response streams. If you close the client in a finally around the return, the client closes before the tools run, and the tool calls fail.
- After the run, exactly one of onFinish, onAbort, or onError runs. So a middleware hook is the safe place for close().
Server instructions#
A server can send instructions that tell the model how to use its tools. Put them in the system prompt, so the model reads them before it calls a tool.
Read them from mcp.instructions after connect. The value is undefined when the server sends none.
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient } from '@tanstack/ai-mcp'
const mcp = await createMCPClient({
transport: { type: 'http', url: process.env.MCP_URL! },
})
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: [{ role: 'user', content: 'What is the weather in Paris?' }],
threadId: 'thread-1',
systemPrompts: mcp.instructions ? [mcp.instructions] : [],
tools: await mcp.tools(),
})In a multi-server pool, each client has its own value: pool.clients.github!.instructions.
Extension methods#
A server can serve an MCP extension, with methods that MCPClient has no helper for. Call them with mcp.request. It takes the same arguments as the SDK Client.request, and it uses the client's requestOptions.
- Declare the extension in clientOptions.capabilities.extensions. The spec asks both sides to list the extensions they support, and a server can change what it sends based on this list.
- After connect, look for the extension in mcp.capabilities.extensions.
- Call the method. For a method that is not in the MCP spec, pass a result schema as the second argument.
import { createMCPClient } from '@tanstack/ai-mcp'
import { z } from 'zod'
const mcp = await createMCPClient({
transport: { type: 'http', url: process.env.MCP_URL! },
clientOptions: {
capabilities: { extensions: { 'com.example/notes': {} } },
},
})
const extensions = mcp.capabilities.extensions
if (
typeof extensions === 'object' &&
extensions !== null &&
'com.example/notes' in extensions
) {
const notes = await mcp.request(
{ method: 'notes/count', params: {} },
z.object({ count: z.number() }),
)
console.log(notes.count)
}To serve an extension, see Serve an MCP extension.
Skip the discover request#
On protocol 2026-07-28, the client sends server/discover when it connects. Some hosts open a new connection for each request. They pay for that round trip each time.
Save the result per user or token, then pass it back as prior:
import { createMCPClient } from '@tanstack/ai-mcp'
import type { DiscoverResult } from '@tanstack/ai-mcp'
const MAX_AGE_MS = 5 * 60_000
const saved = new Map<string, { discover: DiscoverResult; savedAt: number }>()
export async function connect(token: string) {
const hit = saved.get(token)
const fresh = hit !== undefined && Date.now() - hit.savedAt < MAX_AGE_MS
const mcp = await createMCPClient({
transport: {
type: 'http',
url: process.env.MCP_URL!,
headers: { Authorization: `Bearer ${token}` },
},
prior: fresh ? { kind: 'modern', discover: hit.discover } : undefined,
})
const discover = mcp.getDiscoverResult()
if (!fresh && discover) saved.set(token, { discover, savedAt: Date.now() })
return mcp
}- getDiscoverResult() returns the server/discover result of the connection. It is undefined when the server speaks protocol 2025-11-25.
- The result is plain JSON, so you can keep it in a cache or a database.
- Do not share a saved result between users. The spec treats it as private to one authorization context. See Caching.
- You decide how long to keep it. A stale result fails on the first request after connect, not on connect.
- With prior, connect sends no request. A server that is down, or a token that is no longer valid, fails on the first call.
Resources#
MCP resources are documents that the server exposes: files, database records, web pages. Read one and add it to a message as content parts:
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient, mcpResourceToContentPart } from '@tanstack/ai-mcp'
const mcp = await createMCPClient({
transport: { type: 'http', url: process.env.MCP_URL! },
})
const resources = await mcp.resources()
// resources: Array<{ uri: string; name: string; ... }>
const readResult = await mcp.readResource(resources[0]!.uri)
const parts = readResult.contents.map(mcpResourceToContentPart)
// Add the resource to a user message
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: [
{
role: 'user',
content: [
...parts,
{ type: 'text', content: 'Summarize the above document.' },
],
},
],
threadId: 'thread-1',
})mcpResourceToContentPart maps each MCP content block to a ContentPart:
| MCP content block | ContentPart |
|---|---|
| Has a text field | { type: 'text', content: text } |
| Has a blob field | { type: 'text', content: '[binary resource <uri>]' } |
| Anything else | { type: 'text', content: JSON.stringify(content) } |
Resource templates#
import { createMCPClient } from '@tanstack/ai-mcp'
const mcp = await createMCPClient({
transport: { type: 'http', url: process.env.MCP_URL! },
})
const templates = await mcp.resourceTemplates()
// templates: Array<ResourceTemplate>Prompts#
MCP prompts are message templates that the server exposes. Get a prompt, change it to ModelMessage[] with mcpPromptToMessages, and put it at the start of the conversation:
import { createFileRoute } from '@tanstack/react-router'
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient, mcpPromptToMessages } from '@tanstack/ai-mcp'
export const Route = createFileRoute('/api/chat')({
server: {
handlers: {
POST: async ({ request }) => {
const { messages, threadId } = await request.json()
const mcp = await createMCPClient({
transport: { type: 'http', url: process.env.MCP_URL! },
})
try {
// List all available prompts on the server
const available = await mcp.prompts()
// available: Array<{ name: string; description?: string; arguments?: ... }>
// Get one prompt, and fill its template arguments
const prompt = await mcp.getPrompt('summarize', { language: 'english' })
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: [
// Start the conversation with the prompt messages of the server
...mcpPromptToMessages(prompt),
// Then add the messages of the user
...messages,
],
threadId,
})
return toServerSentEventsResponse(stream)
} finally {
// Safe here: all MCP calls (prompts/getPrompt) completed before chat()
// started, and no MCP tools go to the run. If you also spread
// MCP tools into `tools`, close in a middleware hook instead
// (see "Quick setup: typed tools" above).
await mcp.close()
}
},
},
},
})getPrompt(name, args?) takes an optional args of type Record<string, string>. It fills the template variables of the prompt.
mcpPromptToMessages maps each MCP prompt message to a ModelMessage:
| MCP prompt message | ModelMessage |
|---|---|
| role === 'assistant' | { role: 'assistant', content: text } |
| Any other role | { role: 'user', content: text } |
| Content that is not text | content is the JSON.stringify output |
Cancellation#
A chat run can stop, for example when the user leaves the page or an AbortController fires. Then TanStack AI stops the wait for MCP tool calls in progress. The abort signal of the run goes to the execute function of each tool in ToolExecutionContext.abortSignal.
- For a normal callTool request, the signal aborts the request.
- For a task-required tool, it stops the local task stream. Then it sends a best-effort tasks/cancel for a remote task that the server already made.
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient } from '@tanstack/ai-mcp'
const messages = [{ role: 'user' as const, content: 'Hello' }]
const mcp = await createMCPClient({
transport: { type: 'http', url: process.env.MCP_URL! },
})
const controller = new AbortController()
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages,
threadId: 'thread-1',
tools: await mcp.tools(),
abortController: controller,
})
// Stop the run and the wait for MCP tool calls in progress:
controller.abort()Full server and client example#
This TanStack Start route connects to two MCP servers and streams the response to the browser.
Server route (src/routes/api.chat.ts):
import { createFileRoute } from '@tanstack/react-router'
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClients } from '@tanstack/ai-mcp'
export const Route = createFileRoute('/api/chat')({
server: {
handlers: {
POST: async ({ request }) => {
const body = await request.json()
if (typeof body !== 'object' || body === null || !Array.isArray(body.messages)) {
return new Response('Bad request', { status: 400 })
}
const pool = await createMCPClients({
github: {
transport: {
type: 'http',
url: process.env.GITHUB_MCP_URL!,
headers: { Authorization: `Bearer ${process.env.GITHUB_MCP_TOKEN}` },
},
},
linear: {
transport: {
type: 'http',
url: process.env.LINEAR_MCP_URL!,
headers: { Authorization: `Bearer ${process.env.LINEAR_MCP_TOKEN}` },
},
},
})
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: body.messages,
threadId: body.threadId,
tools: await pool.tools(),
// Close after the run ends. Tools run while the response streams.
middleware: [
{
name: 'mcp-close',
onFinish: () => pool.close(),
onAbort: () => pool.close(),
onError: () => pool.close(),
},
],
})
return toServerSentEventsResponse(stream)
},
},
},
})Client component (src/components/Chat.tsx):
import { useChat } from '@tanstack/ai-react'
import { fetchServerSentEvents } from '@tanstack/ai-client'
const chatOptions = {
connection: fetchServerSentEvents('/api/chat'),
threadId: 'support-chat',
}
export function Chat() {
const { messages, sendMessage, status } = useChat(chatOptions)
return (
<div>
<ul>
{messages.map((m) => (
<li key={m.id}>
<strong>{m.role}:</strong>{' '}
{m.parts.find((p) => p.type === 'text')?.content}
</li>
))}
</ul>
<button
onClick={() => sendMessage({ content: 'List my open GitHub issues' })}
disabled={status === 'streaming'}
>
Ask
</button>
</div>
)
}Click Ask. The model calls the GitHub tools, and the answer streams into the list.
To check tool names at compile time too, see MCP Type Generation.