npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

mcp-durable-tasks

v0.2.1

Published

Server-side implementation of the MCP Tasks extension (SEP-2663). Not a to-do manager.

Readme

mcp-durable-tasks — durable, resumable task state for MCP servers

CI npm MCP Tasks Node.js 22+ TypeScript Runtime dependencies Modules License: MIT OpenSSF Scorecard

A durable task state machine for the MCP Tasks extension (io.modelcontextprotocol/tasks, SEP-2663). Pure TypeScript, with zero runtime dependencies on its main entry point. Its one job is to hold a long-running operation's state; with WalTaskStore, state already confirmed by the application remains available to tasks/get after the connection or the whole process has gone.

This is a library you import into an MCP server — not a server you add to mcp.json, and not a to-do manager. "Tasks" here means the extension's durable task handles: the thing a server returns instead of blocking on work that takes minutes.

import { TaskLifecycle, MemoryTaskStore } from "mcp-durable-tasks";

const tasks = new TaskLifecycle({ store: new MemoryTaskStore() });

// tools/call decides to defer: hand back a task instead of a result.
const created = await tasks.createTask({ ttlMs: 3_600_000 });
runTheWork(tasks.handle(created.taskId)); // not awaited

// tasks/get, later, possibly from another connection.
const view = await tasks.getTask(created.taskId);

The worker writes 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" }] });
}

Before you adopt it

WalTaskStore is for one process and one disk. It inherits process-wal's single-writer contract: two processes on the same directory corrupt the log. That fits stdio MCP servers — Claude Code, Cursor, VS Code, a local Codex — which is where the genuinely long tasks live: builds, test suites, migrations, indexing. For a stateless HTTP server behind a load balancer, implement TaskStore over your shared database; it is five methods, and mcp-durable-tasks/testing gives you the test suite before you write the first one.

Worker coordination has process affinity. TaskStore compare-and-swap protects the durable record when several instances can write it, but the promise requestInput() returns and the cancellation signal belong to the process running that worker. While a worker is live, tasks/update and tasks/cancel have to reach that instance — sticky routing, or coordination you provide. This library deliberately does not embed a broker or a queue. The specification does not define that delivery mechanism either; see the process-boundary contract.

Contents

Status

The current release is v0.2.1. It includes the engine, both stores, the public conformance kit, property-based state-machine tests, real crash tests and the runnable crash-recovery example. It is published from GitHub Actions through npm trusted publishing, with provenance tied to the release tag and commit.

v0.1.0 was the one-time manual registry bootstrap. No later release uses a long-lived npm token.

The public API remains provisional until 1.0.0; the version roadmap and open extension questions live in the contract and conformance profile.

Install

Requires Node.js 22 or newer.

The library supports Node.js 22 from its first release; working from source uses the pinned pnpm 11 toolchain and therefore needs Node.js 22.13 or newer.

To install the current registry release:

pnpm add mcp-durable-tasks

To validate the exact tag from source instead:

git clone https://github.com/AndresSaa/mcp-durable-tasks.git
cd mcp-durable-tasks
git checkout v0.2.1
corepack pnpm install --frozen-lockfile
corepack pnpm test

process-wal is an optional peer dependency, needed only by the /wal entry point. Nothing else in the package requires it:

pnpm add mcp-durable-tasks process-wal

| Entry point | What it holds | 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 run in web-standard runtimes too.

Documentation

  • 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 extension questions

Official references

What survives what

WalTaskStore writes every mutation to a write-ahead log before returning, and replays it on open. A returned createTask() means the task is already durable — the extension states that normatively, and it is the invariant this package exists for.

Automatic compaction happens only after the triggering mutation is durable. A compaction failure therefore does not turn that committed mutation into a rejection. Pass onCompactionError to observe it. The event says whether the underlying WAL became unusable; in that case the committed call still returns, and the next store operation fails with ERR_WAL_UNUSABLE so the host can close and reopen the store for recovery.

WAL entries are bounded, not unbounded. WalTaskStore defaults maxEntryBytes to 8 MiB per encoded task record (including its envelope and metadata): enough for roughly 100,000 ordinary 80-byte build-log lines, while keeping synchronous JSON encoding and snapshot compaction under a hard per-task ceiling. Set a different limit when your workload warrants it.

An oversized mutation does not commit and throws TaskEntryTooLargeError, whose stable code is ERR_ENTRY_TOO_LARGE. A worker can therefore preserve its completed work by retrying the terminal transition with a summary or truncated result:

import { isTaskEntryTooLargeError } from "mcp-durable-tasks";

try {
  await task.complete(fullResult);
} catch (error) {
  if (!isTaskEntryTooLargeError(error)) throw error;
  await task.complete({ truncated: true, summary: summarize(fullResult) });
}

Raising the limit only moves this failure boundary; it never removes it. Use the predicate rather than instanceof: duplicated npm packages and the separate CJS entry bundles do not guarantee shared class identity.

Task results, errors, and input payloads are recursively validated as plain JSON before state changes. Values such as Map, Date, bigint, functions, cycles, array holes, and non-finite numbers are rejected consistently by both stores; the in-memory view can therefore never differ from the value replayed from WAL. JavaScript's -0 is valid JSON, so it is accepted and canonicalised to 0 before mutation; it is the only accepted finite number whose identity changes through JSON.stringify/JSON.parse. The input round-trip additionally validates the full MCP shape before a write: sampling content must be one of its discriminated text, image, audio, tool-use or tool-result blocks; elicitation schemas stay within MCP's primitive subset; URL elicitation requires an opaque id and a valid URL; roots use file://; binary blocks are Base64; and accepted elicitation values are primitives or string arrays. These rules are pinned to the @modelcontextprotocol/[email protected] source, not inferred from the lossy generated extension schema. A JSON object is not accepted merely because it is serialisable.

| 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, not asserted: test/crash.test.ts runs real child processes, kills them with SIGKILL at a known state, and reopens the directory. A task created and never touched again comes back as working; one parked on input comes back with its requests and its used-key ledger intact, so the restarted process still refuses to reuse a key the dead one issued; a task completed the instant before the signal keeps its result; an acknowledged progress version is a hard lower bound after a kill in later appends; and a confirmed multi-task snapshot survives a kill in a compaction loop. Those tests load dist/, so what is proven to recover is the package you install.

The deterministic examples are backed by a fast-check state-machine model: random schedules combine progress, TTL changes and clock rollback, partial and duplicate input responses, cancellation, terminal races and injected CAS conflicts. Every step compares the full record, closed wire projection and single-settlement worker effects against an independent reference state.

See it survive a crash

examples/crash-recovery/ is a real MCP server whose task outlives the process. One command starts it, defers work through tools/call, kills the server with SIGKILL, starts it again, and reads the finished result back from the new process:

pnpm --filter mcp-durable-tasks-example-crash-recovery run demo

Watch the crash-recovery demo

The recording above runs the example from a clean checkout. Its essential output is also included below for text-only readers:

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"}], ...}

It runs on every pull request, so it cannot quietly stop being true. The server also shows the simplest verified host workaround — see the next section.

Known gap in the official SDK

On a 2026-07-28 server built with @modelcontextprotocol/server v2, two of the extension's three methods cannot be served through the SDK's registered handlers. tasks/get and tasks/cancel answer -32601 before any handler runs — including fallbackRequestHandler — because both names belong to the retired 2025-11-25 method registry and the protocol-era gate fires on the way in. tasks/update works, because SEP-2663 introduced it and no era claims the name.

This is measured, not inferred; the environment and findings are in the compatibility profile. The crash-recovery example uses the simplest verified host workaround: answer those two HTTP requests in middleware before createMcpHandler. Renaming the methods below Protocol, at the transport seam, is also verified. The SDK bug is tracked in typescript-sdk#2598, with an upstream fix under review.

Writing your own store

TaskStore is five methods, and you do not have to guess whether yours is correct — the conformance kit ships with the package:

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),
    },
  },
);

It has no dependencies and no opinion about your test runner; pass describe and it from whichever you use. If you would rather not involve one at all, checkTaskStore(name, factory) runs the same checks and hands back a report.

The factory is called once per check and must return a fresh store each time; the kit rejects a reused instance. Give it an advanceTime(ms) if your store has a clock seam, and reopen() if a new instance can reconnect to the same backing data. Without those seams, the TTL or cross-instance durability checks are reported as skipped rather than quietly passing. Runner integrations must provide runner.skip(context, reason) to record a real skip; if they do not, a missing optional seam fails with an actionable message instead of producing a false green. The reopen path exercises a full partial-input round and proves that live request/response maps are cleared while usedInputKeys remains durable across both reopen boundaries.

Every factory, check, close() and dispose() operation has a 10-second deadline by default (timeoutMs changes it). Cleanup failures are check failures; they are never swallowed.

TaskPatch follows ordinary object-spread visibility: only enumerable own string-keyed properties participate. For those properties, undefined means delete rather than leave unchanged; the kit pins both parts of that rule.

Publish it, open an issue, and it gets linked from these docs.

Development

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 upstream

License

MIT