When should an MCP server run a tool as a task instead of answering synchronously?
answer
- ask what happens if the connection dies
- repeating the work is the real cost
- people are slower than sockets
- durable state is not free
- some clients cannot use it at all
basics
~20 sUse a task when the work outlives what a single request can safely hold open — minutes, human approvals, or anything that must survive a dropped connection. Keep short, cheap, idempotent calls synchronous: a task adds durable state, polling and a retention policy nobody needs for a two-second query.
solid answer
~50 sThe decision turns on whether the work can finish inside the lifetime of one request. Under MCP 2026-07-28 a broken stream has no resumability, so a synchronous call that runs long is a call you may have to re-issue from scratch — with a new JSON-RPC id, and potentially re-doing everything. A task converts that risk into a durable handle: the `taskId` survives reconnects and can be polled from any node behind a load balancer. So run as a task anything measured in minutes, anything gated on a human, and anything expensive enough that repeating it is unacceptable. Keep everything else synchronous, because a task is not free: it means persisting state outside the process, a lifecycle, cancellation semantics and a retention policy for finished work. And since tasks are an opt-in extension, a server must still have a synchronous path for clients that never declared `io.modelcontextprotocol/tasks`.
go deeper
Grasp the basic criterion: quick calls answer directly, while work that takes minutes or needs a person is better handed back as a task you check on later.
Be able to justify it mechanically — a long synchronous call dies with its stream and, since 2026-07-28 has no resumability, must be re-issued from scratch, whereas a taskId survives.
Weigh both sides out loud: durable storage, retention, abandoned-task cleanup and the fact that a synchronous fallback is still required for clients that never opted into the extension.
Own the alternative and the boundary — decide between the standardized extension and a hand-rolled handle passed as an ordinary tool argument, and set the authorization rule that a handle is never identity.
## Framing the decision This is a design judgment, not a rule in the specification. The specification gives you a mechanism — the `io.modelcontextprotocol/tasks` extension, `CreateTaskResult`, `tasks/get`, `tasks/update`, `tasks/cancel` — and leaves the choice of when to use it to the server author. A strong answer therefore names the forces rather than reciting the API. ## The forces pushing toward a task **Duration versus connection lifetime.** The blunt version: can this finish before something between the client and the server gives up? Load balancers, proxies and mobile networks all impose their own idle and total-duration limits, and none of them are yours. Anything routinely measured in minutes should not be riding a held-open response. **Loss of a stream is loss of the work.** Revision 2026-07-28 removed SSE resumability outright — no event ids, no `Last-Event-ID`. If the stream carrying a synchronous call breaks, the in-flight request is lost and the client must re-issue it as a new request with a new JSON-RPC id. If the operation is expensive, non-idempotent, or has already had side effects, that is a genuinely bad outcome. A task decouples the work's lifetime from any connection's. **Human latency.** If the operation can pause for an approval, a credential, or a choice, its duration is set by a person, not a machine. A task's `input_required` status plus `tasks/update` is built for exactly this; a synchronous call gated on a human is a request held open for however long lunch takes. **Scale-out.** With protocol-level sessions removed in 2026-07-28, an explicit server-minted handle is the sanctioned way to carry state across requests. A `taskId` lets the create, the polls and the cancel each land on different nodes, which is what a horizontally-scaled remote server needs. ## The forces pushing back **Tasks are real infrastructure.** A task must survive the process that created it, or the design is a lie — so its state goes in shared storage. That brings serialization, migration of that stored shape, and failure handling when the storage is down. **Retention becomes a policy question.** How long does a `completed` task's result stay fetchable? Long enough that a client which slept and reconnected can still collect it; short enough to bound storage. There is no protocol answer; you have to pick, document and enforce one. **Abandoned tasks accumulate.** A client can start work and vanish, or park a task in `input_required` and never update it. The server needs its own timeouts, and clients should `tasks/cancel` what they abandon — but you cannot rely on that. **Support is not guaranteed.** Tasks are an extension. A client that never declared `io.modelcontextprotocol/tasks` must be answered synchronously or refused, so the synchronous path exists regardless. If your operation is only viable as a task, you have a tool that some clients simply cannot use, and that is a product decision, not a technical one. ## The often-better third option Before reaching for tasks, ask whether the operation can be made small. Splitting a long job into a start call that returns a server-minted handle as an ordinary tool result, plus separate progress and fetch tools taking that handle as a plain argument, gives you durability with no extension dependency at all — it is just tools and arguments. That pattern works with every client, at the cost of designing the handle, its lifetime and its authorization yourself. Tasks are the standardized version of the same idea; hand-rolling is the portable version. Choosing between them is exactly the kind of tradeoff this question is asked to surface. ## Security note on handles Whichever route you take, possession of a handle is not authentication. MCP 2026-07-28 replaced the old "session hijacking" discussion with "state handle hijacking" and is explicit that holding a handle must not be treated as proof of identity. A `taskId` that leaks must not let a different caller read someone else's results — authorize every `tasks/get`, `tasks/update` and `tasks/cancel` against the credential presented on that request, exactly as you would any other call. ## A workable heuristic - Sub-second to a few seconds, idempotent, no side effects: synchronous, always. - Tens of seconds: synchronous is usually still fine; measure your worst-case network path before assuming so. - Minutes, or any human gate, or side effects you cannot afford to repeat: task. - Hours: task, plus an explicit retention and cleanup policy, plus a client story for what happens when nobody comes back. ## Interview framing Lead with the criterion — can the work finish inside one request's realistic lifetime, and is repeating it acceptable if it cannot — then name the costs a task imposes (durable state, retention, abandoned-task cleanup, extension dependency), then mention the hand-rolled handle alternative and the rule that a handle is not authentication. That is the shape of an answer that sounds like someone who has operated one of these rather than read the schema.
- What is the alternative to the tasks extension if you need durability but your clients may not support it?Hand-roll it: a start tool returns a server-minted handle as an ordinary tool result, and separate status and fetch tools take that handle as a plain argument. That works with any client because it is just tools and arguments, and it matches the 2026-07-28 rule that cross-request state must be an explicit identifier. The cost is that you design the handle's lifetime, cleanup and authorization yourself.
- What operational policy does adopting tasks force you to write?Retention and cleanup. You must decide how long a completed task's result stays fetchable, when an abandoned task in working or input_required is reclaimed, and where that state lives so any node can serve it. None of this is specified by MCP 2026-07-28; the protocol gives you the lifecycle and leaves the storage economics to you.
- Does holding a valid taskId entitle a caller to that task's result?No. MCP 2026-07-28 is explicit that possession of a state handle must not be treated as authentication — the revision renamed the old session-hijacking concern to state handle hijacking for exactly this reason. Authorize tasks/get, tasks/update and tasks/cancel against the credential presented on each request, so a leaked taskId does not expose another caller's work.
saying these in an interview costs you the question
- Says every slow tool should be a task with no cost discussion
- Ignores that clients may not support the extension at all
- Treats a taskId as proof the caller may read the result
- Forgets that finished tasks need a retention policy
- Assumes a held-open synchronous call is safe because progress is being sent