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.
This setup uses TanStack Start. For Cloudflare Workers, see Cloudflare Workers.
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// 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)
}// 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),
},
},
})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.
To call this server from your own chat() route, see MCP Server Tools.
// 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.
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:
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 }))Your app calls the deployed server. You want a wrong tool name or a wrong argument to fail at compile time.
// 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:
import type keeps the server code out of the app bundle.
Pick the one you need:
Some errors never reach your tool code:
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),
})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:
To call the method from a client, see Extension methods.
By default, the server shapes the tool result for you:
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.
createMCPServer serves fixed lists of tools, resources, and prompts. As a result:
For createMCPClient<typeof server>({ transport }):
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.