Moving Beyond Blocking RPC: Where Tasks and Elicitations Belong in the Stack
When building agent tooling, the earliest architectural decision you face is where execution state should live. The Model Context Protocol initially treated every tool interaction as a synchronous remote procedure call: the host issues tools/call, the client blocks on the open connection, the server does the work, and the output returns in a single payload. That model is simple and clean for deterministic, short-lived operations like querying a vector database or evaluating an isolated math expression.
In production, however, synchronous RPC collapses the moment tools tackle multi-minute operations like broad web research, deep repository indexing, or executing long-running integration test suites. Network timeouts terminate idle connections, local transport pipes get clogged, and host user interfaces lock up while waiting for a single monolithic return. I have seen teams attempt to patch this by ratcheting socket timeouts up to fifteen minutes or injecting ad-hoc polling loops into system prompts, but those workarounds create brittle, opaque systems that fail unpredictably under load.
Decentralized / Open Standard
▲
│ ★ MCP (Tasks + Elicitations)
│ (Local/remote RPC, UI-rendering contract)
│
LangGraph / Temporal │
(Durable orchestration, │
developer-configured UI) │
│
◄─────────────────────────────────┼────────────────────────────────►
Deep Custom Orchestration │ Turnkey Managed Platform
(Code-first, self-hosted) │ (Proprietary cloud API)
│
│ OpenAI Assistants / Runs
│ (Closed server-side state,
│ hosted runtime)
▼
Proprietary / Vendor-Locked
The deeper question is architectural placement: do you manage long-running durability inside an internal graph checkpointer, or do you offload execution to the tool server? Frameworks like LangGraph, Inngest, and Temporal centralize state within durable workflow engines—an approach I evaluated when analyzing Durable Workflows Over Ephemeral Scripts: Inside Ramp's Laminar Architecture. That pattern makes complete sense when you control both the agent harness and the underlying worker infrastructure. But MCP exists across an open boundary between a general-purpose host (such as Claude Desktop or an IDE extension) and decentralized, third-party plugin servers.
MCP Tasks and Elicitations solve this boundary problem by formalizing stateful lifecycle coordination at the protocol level. Instead of forcing the host to maintain open execution sockets or guess whether a background process is dead, the plugin server takes custody of the long-running job while the host orchestrates its lifecycle. Coupled with elicitation primitives that let servers query users mid-flight for missing parameters or credentials, this transforms MCP from an ephemeral request-response protocol into a structured, asynchronous runtime.
The Protocol Wire: Deconstructing Task Handles, Polling Loops, and Cancellation
The wire mechanics of the io.modelcontextprotocol/tasks extension rely on non-blocking job handles rather than long-lived streaming connections. When an agent invokes a tool that kicks off a protracted workload, the server does not hold the tools/call response open. Instead, it yields an immediate task descriptor containing a unique taskId, an initial lifecycle status (typically working), and transport guidance such as pollIntervalMs.
sequenceDiagram
autonumber
actor User
participant Host as Claude Host
participant Server as MCP Server
User->>Host: Prompt requiring deep research
Host->>Server: tools/call (execute_research, params)
Server-->>Host: Task Handle (taskId: "task-8042", status: "working", pollIntervalMs: 5000)
Note over Host: Turn remains responsive, no blocked socket
Host->>Server: tasks/get (taskId: "task-8042")
Server-->>Host: Task Status (status: "working", progress: 0.35)
Note over Host: Wait 5000ms backoff interval
Host->>Server: tasks/get (taskId: "task-8042")
Server-->>Host: Task Status (status: "completed", result: { summary: "..." })
Host-->>User: Synthesized research output
Once the host receives this task handle, the active turn is freed. The host transitions into a periodic evaluation loop driven by tasks/get. In this model, the server provides explicit pacing through pollIntervalMs. Honoring this field is critical: if a host polls a local stdio pipe too aggressively, it saturates process I/O serialization; if it polls a remote HTTP transport too fast, it risks triggering rate limits on downstream reverse proxies. Respecting server-provided polling metadata ensures that long-running operations do not flood the transport layer while still reporting incremental progress markers back to the user interface.
State recovery and cancellation are handled explicitly within this lifecycle. If an operator stops generation or redirects Claude mid-turn, the host issues a tasks/cancel request with the associated taskId. It is worth recognizing that tasks/cancel is an advisory RPC across the network boundary: the host requests termination, but whether the tool server can interrupt an active child process or a third-party API depends on its internal runtime mechanics. A disciplined tool server must trap this cancellation request, abort its internal worker thread or subprocess, and release downstream resources, transitioning the task record to cancelled.
In-Flight Elicitation: Form Schemas, OAuth Redirects, and Headless Safety
Long-running jobs rarely execute cleanly without encountering missing prerequisites. A deep research task might find an asset hidden behind a corporate credential wall, or a deployment plugin might require explicit operator sign-off before executing a destructive database migration. Historically, a tool server faced an impossible dilemma: either abort the entire job back to the model, or rely on the LLM to invent missing arguments. MCP Elicitations resolve this by creating an in-flight query mechanism negotiated through capabilities.elicitation.
stateDiagram-v2
[*] --> working: Tool invocation generates taskId
working --> input_required: Server requests elicitation
working --> completed: Execution finishes successfully
working --> failed: Internal exception or timeout
working --> cancelled: Host issues tasks/cancel
input_required --> working: User response - accept
input_required --> cancelled: User response - cancel
input_required --> failed: User response - decline or TTL timeout
completed --> [*]
failed --> [*]
cancelled --> [*]
Elicitation operates primarily in two distinct modes:
- Form Mode (
requestedSchema): When a server needs structured inputs, it pauses internal task execution, transitions toinput_required, and yields a human-readable prompt alongside a standard JSON Schema. The host client parses this schema and renders native input components—such as text fields, dropdown selectors, or boolean toggles—directly within the chat interface. The user fills out the form, and the host dispatches the structured payload back to the server. - URL Mode: When authentication requires third-party identity providers or external security consoles, the server supplies a target URL instead of requesting raw credentials in chat. The host displays a secure redirect link, allowing the operator to complete an OAuth grant or token authorization out-of-band before execution resumes.
The protocol defines three explicit response primitives returned by the client: accept (providing the requested schema arguments or confirming completion), decline (indicating the user refused the specific prompt), and cancel (terminating the overarching task entirely).
The most important operational property here is how the system behaves in headless or automated environments, such as CI/CD runners or background batch jobs. If an agent operates without an interactive user and the host omits capabilities.elicitation during client initialization, the server must fail closed immediately upon encountering an elicitation requirement. Allowing a model to guess credentials, mock permissions, or bypass parameter validation in headless runs introduces critical security vulnerabilities. By declaring interaction capabilities up front, MCP ensures that autonomous execution remains strictly deterministic and fails safely when human judgment is unavailable.
Stress-Testing the Durability Claim: What the Protocol Delivers vs. What Server Runtimes Must Invent
The industry often conflates an asynchronous task interface with durable execution, assuming that because an operation returns a handle and runs in the background, it is inherently resilient to failure. When an engineer discovers that Claude supports MCP Tasks, the immediate instinct is to treat it as an out-of-the-box workflow engine. That assumption is dangerous. What the io.modelcontextprotocol/tasks extension standardizes is strictly the wire contract: uniform descriptor negotiation (taskId), standardized status transitions (working, input_required, completed), and clean decoupling that keeps host interfaces from timing out on protracted work.
Beneath that clean abstraction, the specification does not provide server-side durability. The protocol does not dictate how an external server records execution state, recovers from worker process crashes, implements automatic step retries, or persists jobs across daemon restarts. If a self-hosted MCP server running on a developer machine or an ephemeral container terminates mid-execution while performing a forty-minute codebase crawl, an in-memory task dictionary simply vanishes. When the host issues its next tasks/get, the lookup fails completely. The client-server handshake survives, but the underlying execution state is lost.
| Platform / Architecture | Long-Running Execution Model | State Persistence Mechanism | Human-in-the-Loop Interaction Pattern |
|---|---|---|---|
| Model Context Protocol (MCP Tasks) | Client-driven polling (tasks/get) over RPC; server executes background jobs asynchronously and returns durable taskId handles. |
Server-dependent; protocol mandates no native checkpointer, requiring plugin authors to implement custom durable stores. | Native client schema negotiation (capabilities.elicitation); servers emit dynamic form schemas or external URL authentication redirects. |
| OpenAI Assistants API | Server-side managed run lifecycle; client polls thread runs through states like queued and in_progress. |
Fully managed, proprietary backend storage; threads and run objects persist inside OpenAI's hosted infrastructure. | Explicit requires_action transitions; runs pause until the client submits required tool execution outputs back to the thread. |
| LangGraph | Deterministic state graph execution; steps progress through compiled node edges across asynchronous workflows. | Granular state checkpointing at node boundaries backed by persistent key-value or document stores like PostgreSQL or Redis. | Programmatic interrupt() calls pausing graph execution; resumes when external callers supply updated state payloads. |
| Temporal | Deterministic event-sourcing with workflow activities, automated step retries, and execution replays. | Append-only event history logged to persistent cluster storage; workflows reconstruct state deterministically. | Workflow definitions blocking on external notification handlers, approval callbacks, or webhook completion tokens. |
Contrast this with dedicated workflow orchestrators. Temporal guarantees deterministic replay through append-only event logs, while LangGraph snapshots node-level memory to external databases after every step. OpenAI’s Assistants API abstracts the worker tier entirely, managing state inside its own infrastructure. MCP intentionally takes a different path: it acts as an open, decentralized RPC boundary. It offers a structured client coordination layer, but leaves worker resilience entirely to the server author.
If you build an MCP server that handles long-running, multi-stage jobs, you cannot rely on the protocol to guarantee durability. You must back task tables with durable storage like SQLite or Redis, implement idempotency keys on initial invocations, and manage process supervisors capable of resuming background jobs across restarts—patterns I analyzed when evaluating Durable AI Coding in Ephemeral Sandboxes: Dissecting Vercel Labs' durable-harness. Without that deliberate engineering on the server side, an async handle is just an ephemeral pointer to an unshielded process.
Threat Vectors and Distributed Edge Cases: Form Spoofing, Leased TTLs, and Zombie Workers
Shifting from synchronous request-reply RPC to asynchronous background execution and interactive user queries fundamentally changes your threat model. In an environment where tools execute autonomously over extended periods and solicit user input on the fly, engineers must account for both security boundaries and classic distributed systems failure modes.
The most critical security vector is dynamic form spoofing via confused deputy attacks. Because MCP allows a server to emit an arbitrary requestedSchema containing custom labels, field descriptions, and input types rendered natively by the host client, an untrusted or compromised plugin can craft inputs that mimic official authentication dialogs. A rogue tool can pause a task and present an elicitation form requesting personal access tokens, cloud provider secrets, or elevated system passwords under the guise of an operational prerequisite. If host clients render these inputs without clear provenance indicators, distinct visual sandboxing, and visible server attribution, users can easily be deceived into providing sensitive credentials. Host runtimes must treat arbitrary schemas as untrusted inputs, sanitize field definitions, and isolate server-requested forms from native host chrome.
On the operational side, asynchronous task lifecycles introduce severe resource management gotchas. When a task transitions to input_required to solicit user confirmation or parameters, the server worker thread or fiber enters a suspended state awaiting a response. If the operator steps away, closes their client, or abandons the turn, that background worker remains blocked indefinitely. Without aggressive, lease-based time-to-live (TTL) counters and automated cleanup sweepers, server worker pools quickly exhaust their concurrency limits on abandoned jobs. Production servers must enforce deterministic timeouts that fail idle tasks safely with explicit error classifications rather than holding connections open.
A related failure mode is the creation of zombie workers following client disconnects. If Claude Desktop crashes, a user closes their session, or a network disruption drops the connection while a server runs a multi-hour web scraping job, the server receives no notification over standard transport channels. Unless the host explicitly dispatches a tasks/cancel RPC before terminating, the background process continues executing, chewing through rate limits, third-party API quotas, and expensive cloud compute. Furthermore, cancellation itself is an advisory remote call; if the server's internal worker is blocked inside an uninterruptible system call or third-party HTTP request that does not respect cancellation tokens, the job continues to leak resources.
Finally, transport selection dictates operational stability. While HTTP and SSE provide decoupled streaming channels, local stdio operates over a single serialized pipe. Spawning heavy background jobs inside a local process that flood standard I/O will block incoming tasks/get polling queries behind the queue, turning an asynchronous architecture back into a frozen, unresponsive interface.
Verifying Task Hygiene: An Inspection Harness for Polling Cadence and Advisory Cancellation
When validating an MCP server, unit tests that merely mock protocol envelopes miss the failure modes that emerge under load. If your server accepts a background task, reports a recommended polling interval, and accepts a cancellation dispatch, you still need an outside check verifying that the runtime behaves cleanly: does the client respect the pacing guidance, and does an advisory cancellation actually terminate the underlying worker instead of leaving an orphaned process running indefinitely?
I find it most effective to run a standalone inspection harness against the server before putting it behind Claude. The harness initiates an asynchronous tool call, verifies the initial handle, polls the task while benchmarking response latency against the server's pollIntervalMs, dispatches an abrupt tasks/cancel, and measures process teardown latency to verify that background resources are released:
import asyncio
import time
import httpx
import psutil
async def audit_task_lifecycle(base_url: str, server_pid: int):
client = httpx.AsyncClient(base_url=base_url, timeout=10.0)
process = psutil.Process(server_pid)
# 1. Trigger asynchronous task
init_resp = await client.post("/tools/call", json={
"name": "deep_workload",
"arguments": {"target": "repo_scan"}
})
data = init_resp.json()
task_id = data["taskId"]
recommended_interval = data.get("pollIntervalMs", 2000) / 1000.0
# 2. Verify polling cadence adherence
t0 = time.perf_counter()
poll_resp = await client.post("/tasks/get", json={"taskId": task_id})
elapsed = time.perf_counter() - t0
assert poll_resp.json()["status"] in ("working", "input_required")
# 3. Dispatch abrupt cancellation
cancel_start = time.perf_counter()
cancel_resp = await client.post("/tasks/cancel", json={"taskId": task_id})
assert cancel_resp.status_code == 200
# 4. Measure worker teardown and verify resource release
cancelled = False
for _ in range(20):
await asyncio.sleep(0.2)
status_check = await client.post("/tasks/get", json={"taskId": task_id})
if status_check.json().get("status") == "cancelled":
cancelled = True
break
teardown_time = time.perf_counter() - cancel_start
assert cancelled, f"Task {task_id} failed to transition to cancelled state"
# Confirm child worker processes were reaped
children = process.children(recursive=True)
assert len(children) == 0, f"Leaked {len(children)} zombie child processes"
print(f"Verified: {task_id} teardown completed in {teardown_time:.3f}s")
Running this audit against a local or remote plugin uncovers two common flaws immediately. First, servers that naively return pollIntervalMs: 0 or omit the field entirely cause clients to hammer endpoints with unbounded request loops. Second, servers that handle tasks/cancel as an empty acknowledgment without wiring an abort token into their child process executor leave expensive background jobs spinning indefinitely. An inspection harness turns advisory protocol semantics into hard verification gates.
Architecting Production MCP Plugins: Three Practical Blueprints
When moving from theoretical protocol specifications to production plugins, three architectural patterns demonstrate how to wire task state, transport boundaries, and elicitation flows effectively.
The first pattern is a Long-Running Local Repository Static Analysis Daemon, designed for developer tooling where heavy compute must run on the developer's machine without blocking the Claude Desktop conversation turn. Here, you connect Claude Desktop's stdio transport to an MCP server that delegates AST parsing, comprehensive type-checking, and dependency vulnerability scans to an internal Node.js child_process worker pool. Instead of holding intermediate memory in the process heap, task descriptors and incremental file progress are backed by a local SQLite store. The host polls via tasks/get under exponential backoff while the main process remains completely free to serialize protocol frames. The primary failure mode to watch for in this topology is worker process deadlocks on unhandled syntax exceptions or syntax tree parsing bugs, alongside runaway memory growth if your scanner indexes unignored node_modules or build directories without strict file exclusion filters.
For distributed infrastructure environments, a second blueprint to implement is an Interactive Database Migration and Schema Mutation Gate. In this setup, a remote MCP service accepts asynchronous DDL operations through a Python AsyncPG executor, tracking active jobs inside a centralized Redis task registry. When an agent requests a destructive schema modification or encounters an unconfigured connection parameter, the server transitions the task to input_required and emits a Form Mode payload with a detailed requestedSchema. The Claude interface renders this directly into an interactive form, giving human operators a structured review gate for SQL statements, rollback strategies, and approval confirmations. Handling accept and decline action payloads cleanly requires modeling these transitions as deterministic state machines—a rigor similar to what I discussed in Formal Verification via Agentic Statecharts: Beyond Probabilistic Unit Testing in Production Codebases. The critical production gotcha here is connection lease management: if an operator leaves an elicitation form unsubmitted, the underlying database transaction will hold locks and exhaust connection pool quotas unless you enforce strict idle lease timeouts that roll back pending migrations.
The third pattern addresses external enterprise authentication through a Third-Party OAuth Authorization Proxy for Agentic Workflows. When a background ingestion task encounters an expired SaaS token or requires elevated tenant permissions, keeping credentials out of the prompt context is paramount. Built on a cloud-hosted FastAPI service, this architecture pauses execution and dispatches an interactive URL Mode redirect pointing to an OAuth 2.0 PKCE dance callback endpoint. Claude's interface triggers the browser navigation, allowing the user to authenticate out-of-band on the identity provider's domain. Once the callback completes, an internal tasks/update dispatcher updates the task state back to working with refreshed access claims. The failure mode to guard against here is client-side browser popup blocking combined with timing skew: if an operator completes authentication out-of-band after the host has exhausted its polling deadline or timed out its task lease, the server must reject stale resume attempts cleanly rather than executing against expired job context.
Resources
(no official sources were available to link)
Updated 2026-10-22 by Mehran Mozaffari.
Related posts
31 October 2026
Unreal Engine Skills for Claude Code: How Epic Bypassed the Binary Asset Barrier to Mutate Live Viewports
30 October 2026
Agent Memory Was an Indexing Problem All Along
28 October 2026
The Codebase Is an Environment, Not a Context Window
26 October 2026
Should you self-host OpenDuet as your proactive AI coworker? Not yet
24 October 2026
How AutoSwarm Turns Isolated AI CLIs into a Coordinated Swarm Without a Shared Runtime
24 October 2026
Should You Switch Your Agent Harness to Anvaya? Only if Orchestrator Memory Is Your Bottleneck
