- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
MCP Server Tasks
A task tool returns a task id and finishes later. The host polls that id instead of holding the HTTP call open.
Set execution: 'task' on the tool. The call returns a task handle at once, and the host polls for the result.
Quick setup#
You need an MCP server first. If you do not have one, start with Serve Tools over HTTP. This setup is for one long-lived process, such as a Node server. For a Worker, see Run tasks on a Worker.
- Set execution: 'task' on the tool definition.
- Return the final value from the tool function.
- Create the server once, outside the request handler.
import { toolDefinition } from '@tanstack/ai'
import { createMCPServer } from '@tanstack/ai-mcp/server'
const buildReport = toolDefinition({
name: 'build_report',
description: 'Build a long report',
execution: 'task',
}).server(async () => {
const text = await writeReport()
return { text }
})
async function writeReport() {
await new Promise((resolve) => {
setTimeout(resolve, 5000)
})
return 'Quarterly report'
}
const server = createMCPServer({
name: 'reports',
version: '1.0.0',
tools: [buildReport],
})
export function handleMcp(request: Request) {
return server.fetch(request)
}The host calls build_report and gets a task handle at once. After 5 seconds, the status is completed, and the host reads { text: 'Quarterly report' } from structuredContent.
How it works#
- The host calls the tool.
- The handler returns a task handle before the work ends.
- The tool function continues in this process. There is no queue.
- When the tool function ends, the task store saves the value.
- The host polls until the status is completed or failed.
The default task store keeps each task in memory for this process. A TanStack AI host polls until the task ends. See MCP Server Tools.
What a task tool can use#
The task keeps running after the call returns the task handle. As a result, the task cannot use the request that started it:
- ctx.context.requestInput throws an error. Ask for input in a tool without execution: 'task'.
- ctx.context.sample calls the sample option of createMCPServer. If you do not pass sample, it throws an error.
- ctx.abortSignal does not abort when the call request ends.
Run tasks on a Worker#
A Worker can stop when the response is sent. It can also build a new server for each request. Pass two more options:
- taskStore: create it once with inMemoryTaskStore, outside fetch. A new store on each request has no record of the old task.
- waitUntil: pass waitUntil from the Worker context. Without it, a Worker can stop before the tool function ends.
import { toolDefinition } from '@tanstack/ai'
import { createMCPServer, inMemoryTaskStore } from '@tanstack/ai-mcp/server'
const taskStore = inMemoryTaskStore()
const buildReport = toolDefinition({
name: 'build_report',
description: 'Build a long report',
execution: 'task',
}).server(async () => {
const text = await writeReport()
return { text }
})
async function writeReport() {
await new Promise((resolve) => {
setTimeout(resolve, 5000)
})
return 'Quarterly report'
}
export default {
async fetch(
request: Request,
_env: unknown,
ctx: { waitUntil(promise: Promise<unknown>): void },
) {
const server = createMCPServer({
name: 'reports',
version: '1.0.0',
tools: [buildReport],
taskStore,
waitUntil: (promise) => ctx.waitUntil(promise),
})
return server.fetch(request)
},
}The server calls waitUntil with the tool promise. This promise settles after the store saves the tool result or the tool error.
Advanced#
Share the store across instances#
If more than one instance can receive tasks/get, pass a taskStore that those instances share. A task store has three methods:
- get: read one task by id. Return null when the id is absent.
- set: save one task.
- delete: remove one task.
@tanstack/ai-mcp/server exports inMemoryTaskStore and the TaskStore type.
Spec 2025 and spec 2026#
Tasks are part of spec 2025-11-25. Spec 2026-07-28 has no tasks yet.
| Spec | What the call returns |
|---|---|
| 2025-11-25 | A task handle. The host polls tasks/get, then reads tasks/result. |
| 2026-07-28 | The tool result. The tool runs inline, and the call waits for it. |
On spec 2025, the server sets execution.taskSupport to required. It also advertises the tasks capability for tools/call.
The task handle#
On spec 2025, the call response is the CreateTaskResult from the spec. The task field is a Task with status working.
{
"content": [{ "type": "text", "text": "task-id" }],
"task": {
"taskId": "task-id",
"status": "working",
"ttl": null,
"createdAt": "2026-09-22T12:00:00.000Z",
"lastUpdatedAt": "2026-09-22T12:00:00.000Z"
}
}tasks/get returns the same Task fields:
| Field | Value |
|---|---|
| taskId | The id from the call. |
| status | working, completed, or failed. |
| ttl | null. |
| createdAt | The time of the first set. |
| lastUpdatedAt | The time of the last set. |
| statusMessage | The error text, when the status is failed. |
Task status#
| Status | Meaning |
|---|---|
| working | The tool function is still at work. |
| completed | The store holds the tool result. |
| failed | The tool function threw an error. |
If the tool function throws, the task keeps the text of that error in statusMessage. If that error has no text, the task keeps The tool failed.
Read the result#
tasks/result wraps the value as content. When the value is an object, tasks/result also returns structuredContent.
Errors:
- If the status is not completed, tasks/result returns the error Task result is not ready.
- An unknown task id on tasks/get returns the error Task not found.
- When the server has auth, a task belongs to the caller that started it: the clientId of the token plus its sub claim. Another caller also gets Task not found. MCP Server Auth shows how the verifier sets these values.