Your long tool holds the HTTP call until the work ends. You have no task id for the host to poll.
Set execution: 'task' on toolDefinition.
The handler returns a task handle before the work ends. The tool function continues in this process. There is no queue.
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)
},
}Your tool function returns the final value. When the tool function ends, the store saves that value.
The task keeps running after the call returns the task handle. So the task does not use the request that started it.
If you run on a Worker, pass waitUntil from the Worker context.
The server calls waitUntil with the tool promise. After the store saves the tool result or the tool error, this promise settles.
If you omit waitUntil, a Worker can stop before the tool function ends. On one long-lived process, you can omit waitUntil.
A new store on each request has no record of the old task.
The package @tanstack/ai-mcp/server exports inMemoryTaskStore. The default store keeps each task in memory for this process. If you build the server once, you can omit taskStore.
If more than one instance can receive tasks/get, pass a taskStore that those instances share.
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, and advertises the tasks capability for tools/call.
On spec 2025, the call response is the CreateTaskResult from the spec. The task field is a Task with status working. This status means that the tool function is still at work.
{
"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:
tasks/result wraps the value as content. When the value is an object, tasks/result also returns structuredContent. If the status is not completed, tasks/result returns the error Task result is not ready.
A task uses one status:
If the tool function throws, the task keeps the text from that error in statusMessage. If that error has no text, the task keeps The tool failed.
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 them.
The host polls until the status is completed or failed. A TanStack AI host polls until the task ends.
See MCP Server Tools.
After the status is completed, the host reads { text: 'Quarterly report' } from structuredContent.