A library you import into an MCP server to keep its long-running tasks durable across restarts. It implements the server side of the MCP Tasks extension (io.modelcontextprotocol/tasks, SEP-2663). It is not a server you add to mcp.json, and it is not a to-do manager.
When a tool call takes minutes, the extension lets the server return a task handle instead of making the client wait. The client then polls tasks/get for the result. If the server restarts halfway, a handle kept in memory is gone, and the client polls a task that no longer exists. With WalTaskStore, the task is on disk before createTask() returns, so tasks/get still answers after a crash or a redeploy.
You get the task state machine, TTL, polling hints and the input-request round trip (tasks/update). The main entry point has no runtime dependencies. Transport, auth and the client are not included; for the client, use the official @modelcontextprotocol/ext-tasks package. This library targets the stable 2026-07-28 revision.
import { TaskLifecycle } from "mcp-durable-tasks";
import { WalTaskStore } from "mcp-durable-tasks/wal";
const tasks = new TaskLifecycle({ store: new WalTaskStore({ dir: "./data" }) });
// In tools/call: return a task instead of a result.
const created = await tasks.createTask({ ttlMs: 3_600_000 });
runTheWork(tasks.handle(created.taskId)); // not awaited
// In tasks/get, also after a restart:
const view = await tasks.getTask(created.taskId);The worker writes its progress through the handle:
async function runTheWork(task) {
await task.progress("indexing", { pollIntervalMs: 2_000 });
if (task.signal.aborted) return task.cancelled("client cancelled");
await task.complete({ content: [{ type: "text", text: "done" }] });
}If tasks do not need to outlive the process (in tests, for example), use MemoryTaskStore from the main entry point instead.
pnpm add mcp-durable-tasks process-walNeeds Node.js 22 or newer. process-wal is an optional peer dependency, and only the /wal entry point uses it. If you only want MemoryTaskStore, skip it.
| Entry point | What it contains | Needs |
|---|---|---|
mcp-durable-tasks |
the engine, MemoryTaskStore, types |
nothing |
mcp-durable-tasks/wal |
WalTaskStore |
process-wal |
mcp-durable-tasks/testing |
the store conformance kit | nothing |
The main entry point uses no Node built-ins, so the engine and MemoryTaskStore also run in web-standard runtimes.
WalTaskStore is for one process and one disk. It uses process-wal, which has a single writer. If two processes open the same directory, the log gets corrupted. That fits MCP servers over stdio (Claude Code, Cursor, VS Code), where long tasks such as builds and test suites are common. If your server is stateless behind a load balancer, implement TaskStore on your shared database. It has five methods, and the conformance kit tests your implementation.
A running worker stays in one process. The saved record is safe when several instances write it, but the promise that requestInput() returns and the cancellation signal live in the process that runs the worker. While it runs, tasks/update and tasks/cancel must reach that same instance. Over Streamable HTTP, the client sends the task ID in an Mcp-Name header, so your load balancer can route on it. That routing is your job: this library does not read headers and has no queue. See process boundaries.
WalTaskStore appends every change to a write-ahead log before the call returns, and replays the log when it opens.
| Mode | Process crash, SIGKILL, restart |
Host or power loss |
|---|---|---|
fsync: false (default) |
Recoverable | Not guaranteed |
fsync: true |
Recoverable | Requests a storage flush first |
The left column is tested. test/crash.test.ts starts real child processes, kills them with SIGKILL and reopens the directory. Durability lists every scenario.
A worker's suspended requestInput() call does not survive a restart, but the answers do. A restarted worker calls task.resumeInput() first, which returns the replies that arrived while no worker was waiting (at most once).
Two things can surprise you:
- A task record is capped at 8 MiB by default (
maxEntryBytes). A change over the cap throwsTaskEntryTooLargeErrorand saves nothing, so complete the task with a summary instead of the full output (size limits). - Results, errors and input payloads must be plain JSON. The stores reject
Map,Date,bigint, cycles and similar values before changing anything.
examples/crash-recovery/ is a real MCP server whose task outlives its process. One command starts the server, defers work through tools/call, kills the server with SIGKILL, starts it again and reads the finished result from the new process:
pnpm --filter mcp-durable-tasks-example-crash-recovery run demoThe key output, as text:
3. Kill the server outright — SIGKILL, no shutdown hook, no flush
process gone. Whatever is on disk is all there is.
5. Ask the new process for the task the dead one finished
tasks/get → completed
result → {"content":[{"type":"text","text":"indexed 5 files, 0 errors"}], ...}
The demo runs on every pull request. It also shows how to serve tasks/* through the official SDK.
Since @modelcontextprotocol/server 2.3.0 (typescript-sdk#2599), register tasks/get, tasks/update and tasks/cancel with an explicit schema: setRequestHandler(method, schemas, handler). Typed registration and fallbackRequestHandler still answer -32601 for get and cancel (measured on 2.4.0). Two details are easy to miss:
- The SDK answers
-32020before any handler runs when theMcp-Nameheader does not matchparams.taskId. - On a
2026-07-28request,params.inputResponsesarrives asctx.mcpReq.inputResponses. Atasks/updatehandler must read it there.
Before 2.3.0, the SDK answers -32601 to get and cancel before any handler runs, because both names belong to the retired 2025-11-25 registry. You need a narrow workaround; the compatibility profile lists the two we verified.
TaskStore has five methods. The conformance kit ships with the package, so you can test your implementation:
import { describe, it } from "vitest";
import { runTaskStoreConformance } from "mcp-durable-tasks/testing";
runTaskStoreConformance(
"RedisTaskStore",
() => ({
store: new RedisTaskStore({ url }),
reopen: () => new RedisTaskStore({ url }),
}),
{
runner: {
describe,
it,
skip: (context, reason) =>
(context as { skip(reason?: string): void }).skip(reason),
},
},
);The kit works with any test runner. The factory runs once per check and must return a new store each time. Two parts are optional: advanceTime(ms) if your store has a clock you can control, and reopen() if a new instance can reconnect to the same data. Checks that need a missing part are reported as skipped, never as passed. The API reference has the rest.
If you publish a store, open an issue and we can link it from these docs.
- API: every option, method and error
- Durability: what survives what, and the two unsupported configurations
- Internals: design decisions and source layout
- Contract and conformance: scope, invariants, SDK compatibility findings and open questions about the extension
- Changelog
This version is v0.5.0. Before 1.0.0, breaking changes ship as minor releases and the public API may still change; the changelog lists them. The roadmap is in the contract.
TaskLifecycle.onTaskChanged(taskId, listener) observes committed snapshots for
the optional notifications/tasks message. The engine emits; your host owns
subscriptions and delivery. Notifications are best-effort; recover with
tasks/get. See the API and
public SDK host example. There is no client entry point and none is planned; use @modelcontextprotocol/ext-tasks/client.
- MCP Tasks extension overview
- Tasks extension specification, revision 2026-07-28
- SEP-2663 proposal
- Official schema for revision 2026-07-28
@modelcontextprotocol/ext-tasks, the official client-side package- TypeScript SDK v2 migration notes for protocol revision 2026-07-28
Needs Node.js 22.13 or newer. The repository pins pnpm 11.
git clone https://github.com/AndresSaa/mcp-durable-tasks.git
cd mcp-durable-tasks
corepack pnpm install --frozen-lockfile
corepack pnpm lint
corepack pnpm test
corepack pnpm coverage
corepack pnpm lint:package # packs, installs and imports the real tarball
corepack pnpm check:schema # compares the vendored schema against upstreamMIT
