TanStack
MCP

Serve Tools over HTTP

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.

ts
// 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.

Installation

Install these packages:

shell
npm i @tanstack/ai-mcp @modelcontextprotocol/server

TanStack Start

  1. Save the server code as src/mcp-server.ts.
  2. Forward each request to handleMcp.
ts
// 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.

Cloudflare Workers

  1. Save the server code as src/mcp-server.ts.
  2. Call handleMcp for each request.
ts
// 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.

Tell the host what a tool does

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.

ts
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.

Shape the result yourself

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.

ts
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.

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.

  1. Export server from src/mcp-server.ts.
  2. In the app, import its type with import type.
  3. Pass typeof server to createMCPClient, with the URL of the server.
ts
// 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.

  • callTool accepts only the tool names of server, and each tool's input type.
  • callTool returns the MCP result. For a tool with an outputSchema, structuredContent has the tool output type.
  • getPrompt accepts only the prompt names of server, and each prompt's argument type.
  • readResource accepts only the resource URIs of server.
  • import type keeps the server code out of the app bundle.

Call the server in the same process

When the app and the server run in one process, pass the server object:

ts
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.

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.