You have a tool, a file, and a prompt. A host cannot call them yet.
This tutorial serves all three from one TanStack Start app. A page in that app calls the server and shows the three results.
The server object keeps the types. createMCPClient({ server }) checks the tool name, the resource URI, and the prompt arguments. That client calls the server in the same process. It opens no connection, and the server auth option does not run.
This tutorial uses React and Start. The short guide is Serve Tools over HTTP.
npx @tanstack/cli@latest createPick React. For more options, see Start getting started.
Then 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 zodThe server file defines the tool, the resource, and the prompt.
The route sends each HTTP request to that server.
The page calls the server and shows the forecast, the guide, and the brief.
Create src/mcp-server.ts.
The tool returns the weather for one city. outputSchema makes that result a string for the client.
The resource is a short city guide. A host reads that file by URI.
The prompt asks for a one-day plan. A host renders that prompt with a city name.
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)
}Create the server once. Export server. handleMcp calls fetch for each request.
server carries the tool list, the resource list, and the prompt list. An app that calls the deployed server can use those types with import type. Call the server with types shows how.
Create src/routes/api.mcp.ts. The route path is /api/mcp.
GET is the spec 2025 stream. DELETE closes that session. POST carries the JSON-RPC body.
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),
},
},
})Create src/lib/call-server.ts.
Pass server to createMCPClient. The page does not need a model key.
The call is type-safe:
A wrong name is a type error. A number for city is a type error.
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,
}
},
)Create src/routes/index.tsx. 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.
If the tool must ask the user, open Ask for Input.
If a tool returns before the work ends, open MCP Server Tasks.
If the server must require a bearer token, open MCP Server Auth.