Long-Running Tool Calls: The Terminal-State Protocol for Operation, Poll, and Cancel
After the initial request times out, the tool may still be running server-side; issuing a cancel does not mean the side effects have stopped. This article establishes an operation identity, polling watermark, cancellation race handling, and a single terminal-state acceptance point for a single long-running tool call.
Key Points
- Topic Focus: AI Agent、MCP、DevOps
- Suitable for: indie hackers, full-stack engineers, AI application developers
- Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
Upload analysis, report generation, or bulk mutations can run for ten minutes. If the client only gets a timeout after 30 seconds, it does not know whether the server never started, is still processing, or has already completed but the response was lost. Calling again creates a second operation; clicking cancel and immediately marking the local node as failed can let a success result that arrives later silently trigger downstream steps.
MCP Timeouts, Retries, and Idempotency explains when a single request may be retried, and Agent Long-Running Workflows covers checkpoints for cross-step Agents. This article narrows the boundary to a single long-running tool call: how to obtain an operation identity, wait, supply additional input, cancel, and hand the single terminal state to the calling node.
Negotiate Capabilities First, Then Submit the Operation
MCP Tasks was introduced in the 2025-11-25 spec and is still marked experimental. The client must do capability negotiation against a fixed protocol version; when the server, client, or the tool does not declare support, it cannot assume that tasks/get or tasks/cancel exist, and should use the tool's explicitly published synchronous upper bound or reject long tasks. Google AIP-151's long-running operation provides another operation-resource shape; the two have different fields, and the adapter layer must not treat similar URLs as the same protocol.
The executor persists toolOperation.v1: callNodeId, request digest, tool and descriptor version, body, operation/task ID, protocol adapter version, submission idempotency key, poll cursor, server state, input round, cancel generation, expiry time, and final result digest. The initial submission state is SUBMITTING. Only when the server returns a queryable ID, or an existing operation is found by querying with the same submission key, does it move to WORKING; if the connection drops before either, it moves to SUBMISSION_UNKNOWN, and you must first query by key—you must not generate a new key and retry blindly.
SUBMITTING -> WORKING <-> INPUT_REQUIRED
SUBMITTING -> SUBMISSION_UNKNOWN -> WORKING | NOT_FOUND_SAFE
WORKING | INPUT_REQUIRED -> CANCEL_REQUESTED
WORKING | INPUT_REQUIRED -> TERMINAL_OBSERVED
TERMINAL_OBSERVED -> FETCHING_RESULT -> ACCEPTED
CANCEL_REQUESTED -> CANCELLED_ACCEPTED | TERMINAL_OBSERVED
MCP Task states include working, input_required, completed, failed, and cancelled. Locally appended states must not masquerade as protocol states. INPUT_REQUIRED pauses auto-poll and hands the server request, operation ID, input schema digest, and deadline to the Agent Human Approval Gate; the reply must be bound to this input round. Polling follows the server-suggested interval plus jitter, bounded by the total wait budget, max QPS, and expiry; an HTTP 202 only means the request has been accepted for processing, not that the business has completed.
Cancel Is a Request, Not a Reverse Completion
When issuing a cancel, first use CAS to bump cancelGeneration and record CANCEL_REQUESTED, then send the protocol request. Per the MCP Tasks contract, a valid tasks/cancel must transition the Task to an irreversible cancelled before responding; therefore the successful response itself is the evidence of the cancel terminal state. If the Task has already entered completed or failed, the server must reject the cancel with -32602, and the client then reads the existing terminal state—it must not rewrite it to cancelled. Once cancellation wins the race, even if the underlying execution later completes, the Task must stay cancelled; any possibly-residual external effects are recorded separately as EFFECT_RECONCILIATION_REQUIRED, and a late completion must not be accepted as a protocol success. The Agent Runtime State Machine only consumes this fact.
tasks/get only returns the Task state; after seeing completed, failed, or cancelled, there is still a result-fetch phase. MCP's tasks/result is what returns the result or JSON-RPC error that the original request would have returned; for non-terminal calls it also blocks. The executor must obtain and verify this result for completed/failed before entering the single acceptance point acceptTerminal. The transaction simultaneously matches the call node, request digest, Task ID, tool version, execution epoch, cancel generation, terminal state, and result digest, then writes a terminal receipt and a unique ready outbox. cancelled can be accepted by a successful cancel response; if the result is obtainable before deletion it is attached to the audit receipt, but it must never be treated as a success payload. The Agent Tool Output Defense continues to validate the returned content.
When the operation expires or get returns 404, distinguish NEVER_CREATED, EXPIRED_WITH_RECEIPT, and UNKNOWN. Only when the server proves via the submission key that it was never created is re-submission allowed; an expired operation with a completion receipt can continue to be accepted; the rest goes to manual reconciliation. A client disconnect only stops local waiting, and unless the contract explicitly states so, it must not be automatically treated as a server-side cancel.
Drive Every Race into a Test
Fault injection covers: the server loses the initial response after creation, two pollers return out of order, input_required replay, completion and cancel arriving simultaneously, cancel ACK lost, tasks/result lost or truncated after tasks/get completes, callbacks from an old execution epoch, Task ID misuse, 404 before and after expiry, and poll 429/503. Assert that each call node has at most one terminal receipt, that completed does not advance when the result has not been fetched, that a late completion does not advance after a successful cancel, and that all UNKNOWN states are queryable by Task or submission key.
The gate requires zero terminal-state divergence across ten thousand ordered time series; zero second operations from duplicate submissions; poll amplification not exceeding the configured QPS; cancel confirmation p99 and UNKNOWN reconciliation deadlines below 60 seconds and 15 minutes respectively; and 100% of downstream advances traceable back to a terminal receipt. At launch, first record a shadow operation only for side-effect-free report tools, then let 5% of long tasks use the new adapter. On rollback, stop creating new operations, bump the execution epoch, and have the old executor take over only the in-flight items in the same operation ledger; you must not switch back to "on timeout, just re-call." When the experimental MCP Tasks contract changes, disable the corresponding adapter by protocol version while keeping query and reconciliation capability until all old operations terminate.
Related
Related Content
Article
From Copilot to Codex: AI Coding Tools Enter the Agent Era
Technical trends, engineering practices, and indie-developer perspectives around AI, AI Agents, and Vibe Coding.
Article
Why Does MCP Matter? Understanding the AI Tool Protocol
Technical trends, engineering practices, and an independent developer perspective on MCP and AI Agents.
Article
Core Architecture of AI Agents: Planning, Memory, Tools, and Execution
Technical trends, engineering practices, and independent developer perspectives around AI Agents and large language models.
Article
How Should RAG and Agents Work Together?
An independent developer's perspective on technical trends and engineering practices involving RAG and AI agents.