This tutorial serves a tool, a file, and a prompt with createMCPServer on a TanStack Start route.
A page in the same app calls the server with a typed client and shows the three results. It takes about 10 minutes, and you do not need a model key.
This tutorial uses React and Start. For the short guide, open Serve Tools over HTTP.
npx @tanstack/cli@latest createPick React. For more options, see Start getting started.
Install these packages:
npm i @tanstack/ai @tanstack/ai-mcp zodpnpm add @tanstack/ai @tanstack/ai-mcp zodyarn add @tanstack/ai @tanstack/ai-mcp zodbun add @tanstack/ai @tanstack/ai-mcp zodCreate src/mcp-server.ts. It defines three things:
import { toolDefinition } from '@tanstack/ai'
import {
createMCPServer,
promptDefinition,
resourceDefinition,
} 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(),
}),
outputSchema: z.string(),
}).server(async ({ city }) => {
return `Sunny in ${city}`
})
const cityGuide = resourceDefinition({
uri: 'file:///city-guide.md',
name: 'city-guide',
mimeType: 'text/markdown',
}).read(async () => ({
text: '# Paris\n\nPack a light jacket.',
}))
const tripBrief = promptDefinition({
name: 'trip_brief',
description: 'Write a short trip brief for a city',
argsSchema: z.object({
city: z.string(),
}),
}).render(async ({ city }) => [
{
role: 'user',
content: `Write a one-day plan for ${city}.`,
},
])
export const server = createMCPServer({
name: 'travel',
version: '1.0.0',
tools: [getWeather],
resources: [cityGuide],
prompts: [tripBrief],
})
export function handleMcp(request: Request) {
return server.fetch(request)
}The file creates the server once. handleMcp calls server.fetch for each request.
Create src/routes/api.mcp.ts. The route path is /api/mcp.
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),
},
},
})Each method has a job:
Create src/lib/call-server.ts. Pass server to createMCPClient.
import { createServerFn } from '@tanstack/react-start'
import { createMCPClient } from '@tanstack/ai-mcp'
import { server } from '../mcp-server'
export type DeskResult = {
forecast: string
guide: string
brief: string
}
export const callDesk = createServerFn({ method: 'POST' }).handler(
async (): Promise<DeskResult> => {
const client = await createMCPClient({ server })
const forecast = await client.callTool('get_weather', { city: 'Paris' })
const guide = await client.readResource('file:///city-guide.md')
const prompt = await client.getPrompt('trip_brief', { city: 'Paris' })
const message = prompt[0]
return {
forecast,
guide: guide.text,
brief: message === undefined ? '' : message.content,
}
},
)The calls are type-safe:
A wrong name is a type error. A number for city is a type error.
Replace src/routes/index.tsx with this page. The button calls callDesk.
import { createFileRoute } from '@tanstack/react-router'
import { useState } from 'react'
import { callDesk } from '@/lib/call-server'
import type { DeskResult } from '@/lib/call-server'
function McpServerPage() {
const [result, setResult] = useState<DeskResult | null>(null)
const [error, setError] = useState('')
const [pending, setPending] = useState(false)
async function onCall() {
setError('')
setPending(true)
try {
setResult(await callDesk())
} catch (caught) {
setResult(null)
setError(caught instanceof Error ? caught.message : 'The call failed.')
} finally {
setPending(false)
}
}
return (
<main className="mx-auto max-w-2xl p-6">
<h1 className="mb-2 text-3xl font-bold text-white">Travel desk</h1>
<p className="mb-6 text-gray-300">
This page calls your MCP server. The server has one tool, one resource,
and one prompt.
</p>
<button
type="button"
className="rounded bg-white px-4 py-2 font-semibold text-gray-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white disabled:opacity-60"
disabled={pending}
onClick={() => {
void onCall()
}}
>
{pending ? 'The call is in progress' : 'Call the server'}
</button>
{error !== '' ? (
<p className="mt-4 text-red-300" role="alert">
{error}
</p>
) : null}
{result !== null ? (
<div className="mt-8 space-y-6 text-gray-100">
<section>
<h2 className="text-xl font-semibold">Forecast</h2>
<p>{result.forecast}</p>
</section>
<section>
<h2 className="text-xl font-semibold">City guide</h2>
<pre className="whitespace-pre-wrap">{result.guide}</pre>
</section>
<section>
<h2 className="text-xl font-semibold">Trip brief</h2>
<p>{result.brief}</p>
</section>
</div>
) : null}
</main>
)
}
export const Route = createFileRoute('/')({
component: McpServerPage,
})The page shows Sunny in Paris, the city guide, and the trip brief.
The full example is on GitHub: TanStack/ai examples/react/mcp-server.
The app has three parts:
The server object keeps the types. createMCPClient({ server }) uses them to check the tool name, the resource URI, and the prompt arguments.
This client calls the server in the same process:
An app that calls the deployed server over HTTP can use the same types with import type. Call the server with types shows how.
Next, pick the one you need: