You have TanStack server tools. A host cannot call those tools over HTTP.
For a tool, a resource, and a prompt in one app, open Build an MCP Server.
createMCPServer serves those tools over MCP. Return server.fetch(request) from your route.
// 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 server once. handleMcp calls fetch for each request.
Install these packages:
npm i @tanstack/ai-mcp @modelcontextprotocol/serverpnpm add @tanstack/ai-mcp @modelcontextprotocol/serveryarn add @tanstack/ai-mcp @modelcontextprotocol/serverbun add @tanstack/ai-mcp @modelcontextprotocol/server// 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 route path is /api/mcp.
// src/index.ts
import { handleMcp } from './mcp-server'
export default {
async fetch(request: Request) {
return handleMcp(request)
},
}The worker URL is the MCP URL.
The host can list get_weather. Then the host can call that tool.
To call this URL from chat(), see MCP Server Tools.
A host asks the user before it runs a tool, unless the tool says 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:
The server sends the tool output as one text block. An object 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.
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()
}
}The client connects to the URL and speaks MCP. The server auth option runs, the same as for any host.
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.
Now callTool('get_weather', { city }) goes to the deployed server, and callTool('get_wether', { city }) fails the type check.
If the host starts a local process, see MCP Server on stdio.