Skip to content
MCP

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.

  1. Set execution: 'task' on the tool definition.
  2. Return the final value from the tool function.
  3. Create the server once, outside the request handler.
ts
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

  1. The host calls the tool.
  2. The handler returns a task handle before the work ends.
  3. The tool function continues in this process. There is no queue.
  4. When the tool function ends, the task store saves the value.
  5. 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.
ts
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.

SpecWhat the call returns
2025-11-25A task handle. The host polls tasks/get, then reads tasks/result.
2026-07-28The 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.

json
{
  "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:

FieldValue
taskIdThe id from the call.
statusworking, completed, or failed.
ttlnull.
createdAtThe time of the first set.
lastUpdatedAtThe time of the last set.
statusMessageThe error text, when the status is failed.

Task status

StatusMeaning
workingThe tool function is still at work.
completedThe store holds the tool result.
failedThe 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.