HomeJournalThis post

MCP Tasks for Long-Running Tools

Make long-running protocol work reconnectable, cancellable, expiring, input-aware, authorized, and truthful across transport loss.

JP
JP Casabianca
UI/UX designer and full-stack engineer · Bogotá

MCP tasks let a tool return durable work that a client can poll, reconnect to, cancel, or resume after input instead of holding one fragile request open. This guide turns that lifecycle into explicit states, ownership checks, and failure fixtures.

The intended reader builds a Model Context Protocol server or client for work lasting beyond a normal response. You will leave with a transition reducer, TTL policy, reconnect contract, and late-cancel test that keep progress honest.

The lifecycle distinguishes asynchronous MCP, task lifecycle, progress notifications, and cancellation so transport updates never become the source of truth.

MCP tasks: durable work moves through reconnectable states An authored system diagram connects Created, Working, Needs input, Terminal as one decision path. CreatedWorkingNeeds inputTerminal
  1. Created
  2. Working
  3. Needs input
  4. Terminal
Figure 1: A durable state machine survives transport loss because each transition is persisted and authorized independently of the connection.

MCP tasks separate work from connection

MCP tasks begins with persisting identity, state, owner, expiry, progress cursor, and result independently from any one transport session. This is the narrow boundary for this section; everything outside it belongs in a separate capacity, policy, or product decision. The Model Context Protocol task utility specification defines task creation, retrieval, results, cancel operations, states, and expiry for the protocol's experimental task utility.

Work through four explicit moves:

  • Create one unguessable task identifier
  • Bind it to caller and authorization context
  • Persist every accepted transition
  • Return state without replaying work

In a working review, I would put the first move beside the input fixture, use the second to expose the decision boundary, and make the third observable before polishing the interface. The fourth move is the release check. This order matters because a convincing happy path can still conceal incompatible state, unfair scheduling, inaccessible fallback, or ownership ambiguity. Keeping each move named also lets another engineer reproduce the result without inheriting private context.

The named failure mode is using the request connection as the only task record. Its consequence is disconnects become ambiguous retries and duplicate side effects.

Mitigate it with durable state with independent retrieval. The release receipt is the same identifier returning the same state after reconnect. Those fields connect the implementation to the article's single question and make a later update comparable instead of anecdotal.

Make every lifecycle edge explicit

A useful MCP tasks decision depends on allowing only documented transitions among working, input-required, completed, failed, cancelled, and expired states. This is the narrow boundary for this section; everything outside it belongs in a separate capacity, policy, or product decision. The MCP SEP-1686 proposal records the design motivation and lifecycle tradeoffs behind reconnectable long-running work.

Work through four explicit moves:

  • List valid source and target pairs
  • Reject mutation from terminal states
  • Timestamp and version each transition
  • Record the actor and reason

In a working review, I would put the first move beside the input fixture, use the second to expose the decision boundary, and make the third observable before polishing the interface. The fourth move is the release check. This order matters because a convincing happy path can still conceal incompatible state, unfair scheduling, inaccessible fallback, or ownership ambiguity. Keeping each move named also lets another engineer reproduce the result without inheriting private context.

The named failure mode is letting handlers assign arbitrary status strings. Its consequence is clients observe impossible reversals or terminal work that changes.

Mitigate it with one reducer shared by server paths and tests. The release receipt is a transition matrix with exhaustive negative cases. Those fields connect the implementation to the article's single question and make a later update comparable instead of anecdotal.

Exercise TTL and reconnect fixtures

The worked MCP tasks fixture makes freezing time while clients detach before progress, after input requests, and around expiry. This is the narrow boundary for this section; everything outside it belongs in a separate capacity, policy, or product decision.

Work through four explicit moves:

  • Create with a declared lifetime
  • Advance through a missed update
  • Reconnect before and after expiry
  • Assert cleanup and result behavior

In a working review, I would put the first move beside the input fixture, use the second to expose the decision boundary, and make the third observable before polishing the interface. The fourth move is the release check. This order matters because a convincing happy path can still conceal incompatible state, unfair scheduling, inaccessible fallback, or ownership ambiguity. Keeping each move named also lets another engineer reproduce the result without inheriting private context.

The named failure mode is testing only a continuously connected client. Its consequence is the feature fails at the exact boundary it was created to handle.

Mitigate it with deterministic clocks and stored progress cursors. The release receipt is a replay log for connected, detached, and expired cohorts. Those fields connect the implementation to the article's single question and make a later update comparable instead of anecdotal.

SignalDecisionProof
Client disconnectsKeep bounded workSame task resumes
Input requiredPause executionPrompt and deadline persist
Terminal stateReject late mutationOutcome remains immutable
Figure 2: Reconnect, input, cancellation, and expiry are state transitions with durable evidence, not ad hoc socket behavior.

Implement one transition reducer

MCP tasks needs an explicit rule for making transition validation a pure function before wiring storage, notifications, or tool execution. This is the narrow boundary for this section; everything outside it belongs in a separate capacity, policy, or product decision.

Work through four explicit moves:

  • Represent terminal states as a set
  • Check caller authorization separately
  • Return a new immutable record
  • Assert illegal late changes throw

In a working review, I would put the first move beside the input fixture, use the second to expose the decision boundary, and make the third observable before polishing the interface. The fourth move is the release check. This order matters because a convincing happy path can still conceal incompatible state, unfair scheduling, inaccessible fallback, or ownership ambiguity. Keeping each move named also lets another engineer reproduce the result without inheriting private context.

The named failure mode is copying lifecycle rules into several endpoints. Its consequence is cancel, result, and input paths drift under maintenance.

Mitigate it with one reducer plus integration adapters. The release receipt is a runnable exhaustive state test. Those fields connect the implementation to the article's single question and make a later update comparable instead of anecdotal.

Runnable artifact. Save this as mcp-tasks-long-running-tools.test.mjs and run node --test mcp-tasks-long-running-tools.test.mjs. Expected result: PASS: terminal MCP task rejects late cancel. The checked-in copy lives with this batch's evidence.

import assert from "node:assert/strict";
import test from "node:test";

const terminal = new Set(["completed", "failed", "cancelled", "expired"]);
function transition(task, next, actor) {
  assert.equal(actor, task.owner, "owner mismatch");
  assert.equal(terminal.has(task.state), false, "terminal task is immutable");
  const allowed = { working: ["input_required", "completed", "failed", "cancelled"],
    input_required: ["working", "cancelled", "expired"] };
  assert.ok(allowed[task.state]?.includes(next), "illegal transition");
  return { ...task, state: next, version: task.version + 1 };
}

test("reconnect and late cancel preserve truth", () => {
  const done = transition({ id: "t1", owner: "jp", state: "working", version: 1 }, "completed", "jp");
  assert.throws(() => transition(done, "cancelled", "jp"), /terminal/);
  console.log("PASS: terminal MCP task rejects late cancellation");
});

Treat progress as a lossy view

In production, MCP tasks turns on persisting current state and resumable cursors while allowing duplicate, delayed, or missed notifications. This is the narrow boundary for this section; everything outside it belongs in a separate capacity, policy, or product decision.

Work through four explicit moves:

  • Give updates monotonic sequence numbers
  • Make retrieval authoritative
  • Coalesce noisy intermediate percentages
  • Explain units and indeterminate work

In a working review, I would put the first move beside the input fixture, use the second to expose the decision boundary, and make the third observable before polishing the interface. The fourth move is the release check. This order matters because a convincing happy path can still conceal incompatible state, unfair scheduling, inaccessible fallback, or ownership ambiguity. Keeping each move named also lets another engineer reproduce the result without inheriting private context.

The named failure mode is requiring every notification to arrive exactly once. Its consequence is reconnect logic stalls or renders progress backward.

Mitigate it with idempotent cursors and state refresh. The release receipt is a dropped-notification test ending in the correct result. Those fields connect the implementation to the article's single question and make a later update comparable instead of anecdotal.

Bind authorization across time

Safe MCP tasks requires rechecking task owner, delegated subject, server policy, and credential audience on every read or mutation. This is the narrow boundary for this section; everything outside it belongs in a separate capacity, policy, or product decision.

Work through four explicit moves:

  • Store the creation principal
  • Authorize the current caller
  • Prevent identifier-only access
  • Audit result and cancel reads

In a working review, I would put the first move beside the input fixture, use the second to expose the decision boundary, and make the third observable before polishing the interface. The fourth move is the release check. This order matters because a convincing happy path can still conceal incompatible state, unfair scheduling, inaccessible fallback, or ownership ambiguity. Keeping each move named also lets another engineer reproduce the result without inheriting private context.

The named failure mode is treating knowledge of the identifier as permission. Its consequence is another client can observe or stop private work.

Mitigate it with resource authorization and non-enumerable identifiers. The release receipt is cross-principal denial fixtures for read, result, and cancel. Those fields connect the implementation to the article's single question and make a later update comparable instead of anecdotal.

  1. CreateCreate

    Server returns a task identifier and initial state.

  2. DetachDetach

    Connection ends while owned work continues.

  3. ResumeResume

    Authorized client reads state and later progress.

  4. ResolveResolve

    One immutable result or error becomes available.

Figure 3: Reconnection reads durable state first, then resumes observation; it never restarts the underlying side effect by assumption.

Define cancel as a request

A MCP tasks rollout should preserve distinguishing accepted cancel intent, effect interruption, reconciliation, and a final cancelled or completed outcome. This is the narrow boundary for this section; everything outside it belongs in a separate capacity, policy, or product decision.

Work through four explicit moves:

  • Persist cancel intent first
  • Signal the owned worker
  • Check external effect state
  • Publish one truthful terminal result

In a working review, I would put the first move beside the input fixture, use the second to expose the decision boundary, and make the third observable before polishing the interface. The fourth move is the release check. This order matters because a convincing happy path can still conceal incompatible state, unfair scheduling, inaccessible fallback, or ownership ambiguity. Keeping each move named also lets another engineer reproduce the result without inheriting private context.

The named failure mode is rewriting completed work as cancelled after a late request. Its consequence is the protocol lies about an effect that already happened.

Mitigate it with terminal immutability and reconciliation. The release receipt is a race test where completion and cancel cross deterministically. Those fields connect the implementation to the article's single question and make a later update comparable instead of anecdotal.

Ship the protocol with recovery evidence

The evidence for MCP tasks is strongest when testing storage restart, duplicate create, input resumption, notification loss, verifier outage, and worker takeover before release. This is the narrow boundary for this section; everything outside it belongs in a separate capacity, policy, or product decision.

Work through four explicit moves:

  • Seed one fixture per boundary
  • Restart every owning process
  • Compare state before and after
  • Document unsupported recovery cases

In a working review, I would put the first move beside the input fixture, use the second to expose the decision boundary, and make the third observable before polishing the interface. The fourth move is the release check. This order matters because a convincing happy path can still conceal incompatible state, unfair scheduling, inaccessible fallback, or ownership ambiguity. Keeping each move named also lets another engineer reproduce the result without inheriting private context.

The named failure mode is declaring durability from a happy-path demo. Its consequence is production restarts strand or repeat consequential work.

Mitigate it with fault injection and bounded ownership leases. The release receipt is a recovery matrix with zero unexplained transitions. Those fields connect the implementation to the article's single question and make a later update comparable instead of anecdotal.

Put the decision into practice

MCP tasks make long-running tools survivable by moving identity and state out of the connection. A correct implementation persists legal transitions, treats updates as hints, reauthorizes later access, reconciles cancel races, and exposes one immutable outcome.

Start with the runnable terminal-state fixture, then add a frozen clock and storage adapter. Reconnect before expiry, reconnect after expiry, cross principals, and race completion against a cancel request before letting the task own a real external effect.

The method connects to four existing Journal notes: durable AI agents, AI agent permission budgets, idempotency lifecycle contracts, leases and fencing tokens. Each link covers an adjacent boundary while this article stays focused on one outcome. Keep the fixture, visual evidence, command output, and release receipt together so the next review can test the claim against the same starting conditions.