- 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
Serve Tools over HTTP
createMCPServer exposes your TanStack server tools over MCP. A host such as Claude Desktop or Cursor can then call them.
Your route returns server.fetch(request), and the host connects to that URL.
Quick setup#
This setup uses TanStack Start. For Cloudflare Workers, see Cloudflare Workers.
- Install the packages:
npm i @tanstack/ai @tanstack/ai-mcp @modelcontextprotocol/server zodpnpm add @tanstack/ai @tanstack/ai-mcp @modelcontextprotocol/server zodyarn add @tanstack/ai @tanstack/ai-mcp @modelcontextprotocol/server zodbun add @tanstack/ai @tanstack/ai-mcp @modelcontextprotocol/server zod- Create src/mcp-server.ts with your tools:
// src/mcp-server.ts
import { toolDefinition } from '@tanstack/ai'
import { createMCPServer } from '@tanstack/ai-mcp/server'
import { z } from 'zod'
const getWeather = toolDefinition({
name: 'get_weather',
description: 'Get the weather for a city',
inputSchema: z.object({
city: z.string(),
}),
}).server(async ({ city }) => {
return `Sunny in ${city}`
})
export const server = createMCPServer({
name: 'weather',
version: '1.0.0',
tools: [getWeather],
})
export function handleMcp(request: Request) {
return server.fetch(request)
}- Create the route, and send each request to handleMcp:
// src/routes/api.mcp.ts
import { createFileRoute } from '@tanstack/react-router'
import { handleMcp } from '../mcp-server'
export const Route = createFileRoute('/api/mcp')({
server: {
handlers: {
GET: ({ request }) => handleMcp(request),
POST: ({ request }) => handleMcp(request),
DELETE: ({ request }) => handleMcp(request),
},
},
})- Start the app. Then add the URL of the route (for example, http://localhost:3000/api/mcp) to your MCP host.
The host lists get_weather, and it can call the tool. The answer is Sunny in Paris for the city Paris.
For a full app with a tool, a resource, and a prompt, follow Build an MCP Server.
How it works#
- You create the server once. handleMcp calls server.fetch for each request.
- The server speaks MCP over HTTP. It finds the tool by name, parses the input with inputSchema, and runs your .server() function.
- By default the server keeps no session. As a result, any instance can answer any request, and a serverless host works with no extra setup.
To call this server from your own chat() route, see MCP Server Tools.
Cloudflare Workers#
- Save the server code from the quick setup as src/mcp-server.ts.
- Call handleMcp for each request:
// src/index.ts
import { handleMcp } from './mcp-server'
export default {
async fetch(request: Request) {
return handleMcp(request)
},
}The worker URL is the MCP URL. Each request can reach a different instance. This works because the server keeps no spec 2025 session by default. See MCP Server Sessions.
Tell the host what a tool does#
A host asks the user before it runs a tool, unless the tool says that it only reads. Set metadata.title and metadata.annotations on the tool definition. The host gets them as the MCP tool title and annotations.
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'
export const listNotes = toolDefinition({
name: 'list_notes',
description: 'List the notes of the signed-in user',
inputSchema: z.object({}),
metadata: {
title: 'List notes',
annotations: { readOnlyHint: true, idempotentHint: true },
},
}).server(async () => [])The annotation names are the MCP names:
- readOnlyHint: the tool changes nothing.
- destructiveHint: the tool can delete or overwrite data.
- idempotentHint: a repeat call with the same input changes nothing more.
- openWorldHint: the tool reaches outside your system.
To show an MCP Apps view for a tool, set metadata._meta. The host gets it as the MCP tool _meta. The key ui.resourceUri links the tool to the ui:// resource of the view.
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'
export const showChart = toolDefinition({
name: 'show_chart',
description: 'Show the sales chart',
inputSchema: z.object({}),
metadata: {
_meta: { ui: { resourceUri: 'ui://charts/sales' } },
},
}).server(async () => ({ total: 42 }))Call the server with types#
Your app calls the deployed server. You want a wrong tool name or a wrong argument to fail at compile time.
- Export server from src/mcp-server.ts.
- In the app, import its type with import type.
- Pass typeof server to createMCPClient, with the URL of the server.
// app/weather.ts
import { createMCPClient } from '@tanstack/ai-mcp'
import type { server } from '../src/mcp-server'
export async function forecast(city: string) {
const client = await createMCPClient<typeof server>({
transport: { type: 'http', url: 'https://mcp.example.com/api/mcp' },
})
try {
return await client.callTool('get_weather', { city })
} finally {
await client.close()
}
}Now callTool('get_weather', { city }) goes to the deployed server, and callTool('get_wether', { city }) fails the type check.
What the types check:
- callTool accepts only the tool names of server, and the input type of each tool.
- getPrompt accepts only the prompt names of server, and the argument type of each prompt.
- readResource accepts only the resource URIs of server.
import type keeps the server code out of the app bundle.
Add more to your server#
Pick the one you need:
- Serve documents and prompts: Serve Resources and Prompts.
- Let only signed-in users call the tools: MCP Server Auth.
- Ask the user a question from a tool: Ask for Input.
- Run a long tool in the background: MCP Server Tasks.
- Let a desktop host start your server as a local process: MCP Server on stdio.
Advanced#
Log SDK errors#
Some errors never reach your tool code:
- transport errors
- protocol errors
- rejected requests
Pass onerror to send them to your logs. serveMCPStdio also sends its transport errors there. onerror only reports. The response does not change.
import { createMCPServer } from '@tanstack/ai-mcp/server'
const server = createMCPServer({
name: 'weather',
version: '1.0.0',
onerror: (error) => console.error('MCP error', error),
})Serve an MCP extension#
An MCP extension adds methods that the core spec does not have. For example, the Skills extension adds skills/list and skills/get. A client uses an extension only when the server lists it in capabilities.extensions.
Pass setup to get each SDK McpServer before it connects. Declare the extension there, then add a handler for each of its methods:
import { createMCPServer } from '@tanstack/ai-mcp/server'
import { z } from 'zod'
const server = createMCPServer({
name: 'notes',
version: '1.0.0',
setup: (mcp, context) => {
const userId = context?.userId
mcp.server.registerCapabilities({
extensions: { 'com.example/notes': {} },
})
mcp.server.setRequestHandler(
'notes/count',
{ params: z.object({}) },
() => ({ count: typeof userId === 'string' ? userId.length : 0 }),
)
},
})
export function handleMcp(request: Request, userId: string) {
return server.handle(request, { context: { userId } })
}Rules for setup:
- It runs each time the server builds an McpServer. That is each request, or once for each spec 2025 session when sessions is 'memory'. Keep it fast.
- The second argument is the context object from server.handle. On a spec 2025 memory session, that value is from the request that opened the session. A later request on that session does not run setup again. A tool on the later request sees the new context.
- It must be synchronous. The server connects as soon as setup returns, so an async function fails the request.
- Add new methods only. Do not replace a method that the server already handles, such as tools/call.
To call the method from a client, see Extension methods.
Shape the result yourself#
By default, the server shapes the tool result for you:
- It parses the tool output with its outputSchema. If the output does not match, the call returns a tool error that names the tool.
- It sends the output as one text block.
- If the output is an object, it also goes on structuredContent.
When you want more than one block, or isError without an exception, return an MCP CallToolResult from a tool with no outputSchema. The server sends it as is.
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'
import { db } from './db'
export const countNotes = toolDefinition({
name: 'count_notes',
description: 'Count the notes and list them',
inputSchema: z.object({}),
}).server(async () => {
const notes = await db.notes.list()
return {
content: [
{ type: 'text' as const, text: `${notes.length} notes.` },
{ type: 'text' as const, text: JSON.stringify(notes) },
],
structuredContent: { count: notes.length },
}
})The first block is a short summary for the model. The second block is the data.
Fixed lists and subscriptions#
createMCPServer serves fixed lists of tools, resources, and prompts. As a result:
- It reports listChanged: false.
- It answers subscriptions/listen with JSON-RPC -32601.
- A Worker does not keep an idle subscription stream open.
What the typed HTTP client returns#
For createMCPClient<typeof server>({ transport }):
- The client connects to the URL and speaks MCP. The server auth option runs, the same as for any host.
- callTool returns the MCP result. For a tool with an outputSchema, structuredContent has the output type of the tool.
Call the server in the same process#
When the app and the server run in one process, pass the server object:
import { createMCPClient } from '@tanstack/ai-mcp'
import { server } from '../src/mcp-server'
const client = await createMCPClient({ server })
const text = await client.callTool('get_weather', { city: 'Paris' })This client opens no connection. It calls the tool function directly and returns the tool output.
- The server auth option does not run.
- The client has no tools(), so you cannot pass it to chat().
- A tool gets the spec 2026 context. ctx.context.requestInput throws, and ctx.context.sample uses the sample option of the server.
- callTool parses the output with the tool outputSchema, the same as the HTTP server.
- readResource(uri, context) puts context on the resource ctx.context. Without it, ctx.context is {}.