Claude Code MCP Tasks: Four Gates Control Creation, Persistence & Elicitation
This article details the four conditional gates—client capability extension, feature flags, session type, and call origin—that determine whether an MCP tool call becomes a persistent task in Claude Code, explaining why tasks work in terminal CLI but silently degrade in CI, VS Code, or subagents, and provides decision tables for deployment scenarios.
The article opens with a concrete deployment scenario: an MCP server exports a report taking eight minutes. The team wraps it as an MCP task returning a handle; in the terminal the call returns immediately, the task appears in /tasks, and Claude continues working. The same server code plugged into CI via claude -p "<prompt>" fails to create a task and hits an eight-minute timeout. A colleague invoking the same tool from the VS Code extension also sees no task. The root cause is not a server bug—Claude Code's documentation lists six conditions for a call to become an MCP task, only two of which concern the server (tasks support, transport). The remaining four depend on deployment, session type, and call origin. The protocol defines task shape but not who may create, continue, or answer a task; those three capabilities are governed by the host's session form, and they are not controlled by a single switch.
First Gate: The Extension in the Request
On connections negotiated per the 2026-07-28 revision, Claude Code includes its client capabilities in each request's params._meta["io.modelcontextprotocol/clientCapabilities"], specifically an extensions object containing "io.modelcontextprotocol/tasks": {}. Critically, a tools/call that cannot become a task omits this extension. This design delegates the decision to the server: if the key is present, the server replies with resultType: "task"; otherwise it processes synchronously. If the server unconditionally returns a task result without seeing the extension, Claude responds with Unsupported result type 'task' for tools/call. Therefore "server supports tasks" and "this call can become a task" are distinct propositions—the former is a repository concern, the latter must be re-evaluated per request. Conflating them is the most common error when adopting the extension.
Second Gate: Feature Flags and Provider
Becoming a task also requires the session to fetch feature flags. This excludes three deployment classes: third-party providers (e.g., Amazon Bedrock), sessions with telemetry disabled, and sessions with flag fetching disabled. Setting MCP_SDK_GENERATION=v1 or MCP_PROTOCOL_NEGOTIATION=legacy also bypasses protocol negotiation. These exclusions share a trait: they do not error. The session works normally, tools invoke normally, but the request lacks the extension, so the server sees an ordinary call. This is silent degradation, not a failure path. Server authors must not log "missing extension" as "client lacks task support"; the same client and user can flip the outcome by switching providers or toggling telemetry. Logs should record the request's conditions, not inferred client capabilities. Version numbers are not reliable probes either: the documented floor is v2.1.304+, but the 2.1.304 release notes did not mention the feature; capabilities roll out by session type. Upgrade is necessary but not sufficient.
Third Gate: Session Type, Not Interface Type
As of v2.1.304+, Anthropic has enabled MCP tasks for terminal CLI sessions, Agent SDK sessions, and GitHub Actions runs; other surfaces such as the VS Code extension and the desktop app have not. Both counterexamples are interactive surfaces where the user sits nearby, yet tasks are absent. Hence "interactive implies tasks" is a false heuristic. Task availability follows Anthropic's per-session-type rollout, independent of user presence. Moreover, "interactive" in the documentation is narrower than everyday usage: elicitation/create is only available in interactive terminal sessions. An IDE panel with dialogs can interact during ordinary tool calls but may not be the session that services elicitation on the task path. These two conditions must be read separately; the existence of one does not imply the other. The documentation does not explain why these three surfaces were prioritized; the verifiable fact is the conclusion and its operational consequence: a terminal validation pass does not guarantee reproducibility in the IDE. A parallel gap exists in the spec: the official MCP support matrix (community-maintained) lists Apps, OAuth client credentials, Enterprise auth, and Skills—but no Tasks column. The Tasks extension page states only "Host support varies by client," deferring to each client's docs. Thus "Does it support MCP Tasks?" cannot be answered by any summary table; it resolves to a specific host's specific session.
Fourth Gate: Call Must Originate from the Main Conversation
Only calls from the main conversation can become tasks; subagent-initiated calls never do. The auto-backgrounding section states this more plainly: Claude Code only backgrounds main-conversation calls; subagent calls never move. No reason is given. The author infers that task results return via task notifications requiring a stable delivery target; a subagent branch converges into a single tool result, but tasks often outlive that convergence, leaving nowhere to backfill. This inference explains motivation but does not change the conclusion: subagent calls neither become MCP tasks nor get auto-backgrounded. This restriction is easily underestimated because subagents are the natural place to "offload long work"; the result is that long work cannot go there. Either move the call back to the main conversation so it becomes a task, or accept a synchronous call inside the subagent with an increased timeout.
One-Shot Runs vs. Long Sessions: The Input Stream Difference
Besides interactive sessions, only Agent SDK sessions or claude -p runs reading --input-format stream-json can obtain tasks, and only when three environment variables are simultaneously satisfied: CLAUDE_AUTO_BACKGROUND_TASKS=1, CLAUDE_CODE_DISABLE_MCP_TASK_BACKGROUND not 1, and CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS not 0. A one-shot claude -p "<prompt>" is excluded. These three variables each control a distinct behavior. CLAUDE_AUTO_BACKGROUND_TASKS=1 permits non-interactive sessions to move calls to the background; the docs note non-interactive mode defaults to no move because "a one-shot run may end before results arrive." CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS not 0 because setting it to 0 means the opposite: Claude waits for every MCP tool call to finish. This flag is a precondition only on the non-interactive branch; in interactive sessions setting it to 0 still creates tasks but Claude waits throughout. CLAUDE_CODE_DISABLE_MCP_TASK_BACKGROUND is the trickiest: setting it to 1 makes Claude wait for the task result, yet auto-backgrounding remains on (default two minutes), after which the call becomes an ordinary background call—no longer an MCP task, no cross-session survival. It is a switch that appears to strengthen but actually weakens. Combined, the three variables mean: non-interactive sessions must both allow backgrounding and not disable it. This is counter-intuitive for CI wrapper scripts, because intuition suggests enabling "wait for result." The docs prescribe the correct pattern elsewhere: Agent SDK or stream-json sessions must keep the input open, because once input closes the run no longer waits for MCP tasks. This translates to a concrete engineering constraint: CI cannot be written as claude -p "<prompt>" then wait for process exit; it must use the stream-json form, hold stdin open until the task ends or is explicitly closed. The habitual "write prompt then close stdin" severs the link between the task and the run.
Backgrounding and Tasks Are Not the Same
MCP tool calls in the main conversation exceeding two minutes are auto-moved to a background task; Claude immediately receives a task ID and continues, results arrive as task notifications; this background task appears in /tasks, can be stopped, but does not survive across sessions. The same section notes that MCP task entries display runs on the server; survives exiting this session. This line distinguishes "MCP task" from "auto-backgrounded ordinary call," not remote vs. local: the exit section clarifies that stdio-server tasks also carry this line. Consequently /tasks mixes two object types. Users see "background task" for both, both can be stopped, both show progress, but one merely moves the wait locally while the other truly runs server-side and can be recovered across sessions. Judging whether a task was actually created cannot rely on "I saw it in /tasks." Another overlapping switch: setting CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1 or launching in bare mode does not make Claude wait for MCP tasks. Disabling backgrounding ≠ switching to synchronous wait; the two are separate in this implementation.
Who Holds the Task's Clock
This section addresses task duration because the docs state it explicitly and its relation to the three title capabilities is easily misread. By default, background tasks have no Claude Code-side time limit. The tool call's wall-clock limit and idle timeout apply only until the server answers with a task handle. If Claude is made to wait for the result, those two limits cover the entire task; answered status checks count as idle activity. Default values matter because the opening "eight minutes" likely came from custom config. Per-server timeout is a per-call hard wall clock; values below 1000 ms are ignored and fall back to MCP_TOOL_TIMEOUT; unset, it defaults to ~28 hours. Idle window defaults to five minutes (HTTP, SSE, WebSocket, claude.ai connector) and thirty minutes (stdio). For server authors, these defaults dictate how fast the task handle must be returned. Before the task answer, if five minutes pass with neither response nor progress notification, the HTTP connection is idled out before the handle arrives; the server docs' "accept first, then work" refers to this moment. Once a task is established and backgrounded, Claude Code is no longer the party that can kill it. Eight minutes or eight hours depends on the server's own lifecycle and its willingness to keep answering tasks/get. The docs add that ttlMs expiry does not make Claude Code abandon a task, making "how long counts as timeout" entirely a server-side question, not a client-side backstop. Read backwards: setting CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS=0 to "safely" wait for the result puts long tasks back under the client's wall-clock timer. This also explains why the three environment variables must combine as they do. In non-interactive scenes, wanting a task means you cannot simultaneously demand the client wait for it; in interactive scenes tasks still establish, but once you make Claude wait, wall-clock and idle again cover the whole task.
What Remains After Exit
Remote-server tasks continue running after you exit; to continue tracking, resume the same session. Stdio server processes stop when Claude Code exits, even though their tasks in /tasks also show runs on the server; survives exiting this session; unless that server's docs state work survives the process, you should let it finish or stop it before exit. The same status line means different things for remote vs. stdio: it describes Claude Code's record persisting, not the server process persisting. Blurring this distinction leads teams to assume "tasks all run server-side" and lose seven minutes of work on a local server. Recovery conditions are tabulated in the docs:
You run /clear, /branch, or resume another session: as long as Claude Code stays open, results arrive at your current session; after exit, to find it again you must resume the session where the task started.
You exit Claude Code: no exit option cancels a remote server task, even if the exit dialog lists it under The following will stop when you exit:.
You interactively return to the task's session ( claude --resume, claude --continue, or /resume): after the server reconnects, Claude Code resumes following; if the task finished meanwhile and the server still recognizes it, Claude gets the result.
You use a one-shot claude -p to resume: this run does not follow the task.
You start a new session or use --fork-session: the new session does not follow.
An implicit condition appears in the status message table, not the recovery table: the server name and connection settings must remain unchanged. If the name is deleted, renamed, or omitted from --mcp-config, the entry shows
server '<name>' is not configured in this session; the task may still be running on it; if a same-named server changes URL, command, args, env vars, headers, or timeout, it shows
server '<name>' was reconfigured to a different transport since this task was created. Thus task identity is a four-tuple: session, server name, connection settings, taskId. Changing any item severs Claude Code's path back. This is inferred from the documented failure conditions, not verbatim, but explains why the docs repeatedly require "keep server name and connection settings unchanged." A similar failure with higher cost: after a server disconnects, you must reconnect from /mcp before cancelling the task; otherwise the cancel request never emits, Claude Code forgets the task, and you lose traceability forever. A cancelled request that never sends costs the task's trackability.
Who Can Answer Mid-Task Questions
When a task enters input_required, Claude Code reads inputRequests, a map from server-chosen keys to { method, params }. elicitation/create is only provided in interactive terminal sessions; roots/list is available everywhere. When Claude Code cannot service a request, it asks the server to cancel the task. Non-interactive sessions are more direct: input-requiring tasks are cancelled, and neither your Elicitation hooks nor the Agent SDK's onElicitation callback run. This negates a natural design: using an Elicitation hook to auto-answer works in ordinary tool calls but on the task path non-interactive sessions never reach the hook. The docs place this statement in the MCP Tasks chapter; the author found no generalization beyond tasks, so it applies only to the task path. v2.1.310 release notes mention a related change: in non-interactive sessions, MCP tool calls timing out while awaiting server elicitation now tell Claude no answer arrived and not to retry the call. This is a timeout-path wording change, not the cancel path. The two are easily conflated: one occurs at "no answer in time," the other at "no one here to answer." Two more details deceive: when the /tasks panel is open, the elicitation dialog is hidden—close the panel to answer; users think the task stalled, but the question was blocked. Second, declining or pressing Esc is also an answer; the server decides whether the task continues or ends. Thus "user declined" and "no one answered" are distinct in the protocol; the former can advance the task, the latter waits indefinitely.
Why Cancel Instead of Queue for an Answer
This is the design choice most worth expanding because the docs describe the behavior but not the rationale. The author's inferences follow. A task entering input_required essentially means "a person needs to answer a question." Non-interactive sessions lack that person. If the client chose to queue, it would need to promise three things: someone will return; the returnee is authorized to answer; the answer can reach the still-living task. The first fails: the task lifecycle allows tasks to outlive sessions—you exit, the remote task keeps running. Who answers the hanging question? An exited session has no dialog and no owner. Queuing turns "temporarily no answer" into "permanent hang"; cancel at least gives a definite state the server can act on. The second should not be the client's job: the spec positions elicitation as server-to-user, noting the flow lets the client retain control over user interaction and data sharing. Client answering for the user forges consent; form-mode data enters the client's view, and the spec imposes MUST-level restrictions on sensitive info. A session that cannot confirm "who is answering" should conservatively not answer. The third is the delivery path: task answers travel via tasks/update, requiring the key to match the currently unsatisfied inputRequests. After session close, who sends that tasks/update? The docs provide no such route. Non-interactive cancel honestly tells the server "no legitimate answerer exists" rather than pretending it can still wait. Finally, cancel is cooperative: the spec says the server may not stop immediately and may reach other terminal states. So non-interactive cancel is not "work lost"; it returns agency to the server to decide whether to finalize, retain intermediate state, or re-accept on next run.
inputRequests Keys Are a Contract
The spec defines inputRequests as a map from "server-assigned identifiers" to request objects; the server chooses the key. Client-side, the docs state each key is answered via exactly one tasks/update; inputResponses uses the same key to map results; keys beyond 16 are ignored. The spec requires tasks/update to ignore unknown or already-satisfied keys. Together these give keys several properties: stable, enumerable, single-use, quantity-limited. Do not use one-off random IDs: "answer once" means retries won't replay; a mistake has no second chance. If a task resumes and re-enters input_required, you likely want the key to continue the prior semantics; but key reuse on resume is unspecified, leaving it to the server—this is inference, not rule. Do not pack multiple questions into one key: the client returns one result per key; three questions in one key leaves the server unable to tell which parts were answered. Do not use keys as session state or sequence numbers: beyond 16 are ignored, and which 16 count is unspecified. Keys should describe "what is asked" not "which question number"; confirm-target is more stable than q1; rerunning the task preserves semantics. These last points are advice, not spec clauses. The spec only requires keys be server-assigned and client-filled by key.
Same input_required , Two Outcomes in Two Sessions
Placing the rules into a concrete response makes the difference visible. Suppose a deployment-task server needs two decisions mid-run; it can return from tasks/get like:
{
"resultType": "complete",
"taskId": "deploy-7f3a9c2e",
"status": "input_required",
"createdAt": "2026-10-22T14:03:07Z",
"lastUpdatedAt": "2026-10-22T14:04:10Z",
"ttlMs": null,
"statusMessage": "Waiting for two decisions",
"inputRequests": {
"confirm-target": {
"method": "elicitation/create",
"params": {"...":Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architecture Development Notes
Focused on architecture design, technology trend analysis, and practical development experience sharing.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
