BYOKchat Blog

MCP Tasks Explained

Understand the MCP Tasks extension for long-running work: task creation, tasks/get, tasks/update, cancellation, subscriptions, persistence, retries, and recovery.

· 7 min read

On this page
  1. Tasks moved out of the core protocol
  2. The server decides when a request becomes a task
  3. A simplified lifecycle
  4. Why server-directed task creation is useful
  5. Do not assume a task is created unless the request opted in
  6. Persist the task ID immediately
  7. The local operation ID and task ID solve different problems
  8. tasks/get is authoritative for task state
  9. Poll with backoff
  10. Do not treat a polling timeout as task failure
  11. tasks/update supports task lifecycle changes
  12. Cancellation is a race
  13. Persist cancellation intent separately
  14. Subscription notifications can reduce polling latency
  15. Do not depend on tasks/list
  16. Task ownership must be explicit in the app
  17. Authentication can expire while a task runs
  18. App restart recovery should scan local active tasks
  19. Unknown create outcome is the hardest case
  20. Task result persistence should be separate from status polling
  21. Task retention is a product/security concern
  22. Long-running work needs cost/runtime guards
  23. User approval should happen before task creation for side effects
  24. Task progress is not necessarily linear
  25. Tasks and MRTR can interact
  26. Avoid holding chat generation open unnecessarily
  27. Notifications need user consent/policy too
  28. Trace task lifecycle separately from tool call duration
  29. A durable task record
  30. Test task recovery aggressively
  31. Test cross-instance statelessness
  32. Where BYOKchat fits
  33. Further reading

Some MCP operations are too long-running to fit comfortably inside one request.

Examples:

index a repository
export a large dataset
run a deployment
transcode media
process thousands of documents
perform a bulk migration

The MCP Tasks extension exists for this class of work.

A task is not just “a slow tool call.”

It is a durable operation with an identity and a lifecycle that can outlive one network request.

Tasks moved out of the core protocol

In the 2026-07-28 MCP generation, Tasks moved from the experimental core into the io.modelcontextprotocol/tasks extension.

That architectural move matters.

Clients should not assume every MCP server supports Tasks just because it supports MCP.

Treat Tasks as an explicitly negotiated/advertised extension.

The server decides when a request becomes a task

The modern design is server-directed.

A client opts into the Tasks extension for a request.

If the server chooses to execute that operation asynchronously, it can return a task handle instead of the normal immediate result.

Conceptually:

client: tools/call + tasks extension opt-in
server: CreateTaskResult(taskId=task_123)

The client then follows the task lifecycle.

A simplified lifecycle

Diagram illustrating the surrounding section

Depending on the extension and workflow, tasks/update, tasks/cancel, and subscription notifications may also participate.

Why server-directed task creation is useful

The server knows operational details the client may not.

For the same tool:

small input -> immediate result
large input -> background task

The client does not need to predict the runtime perfectly.

It only needs to support the extension and handle either result shape correctly.

Do not assume a task is created unless the request opted in

The modern Tasks design ties task creation to explicit extension participation.

A server should not unexpectedly convert a normal request into a task when the client did not advertise/opt into the extension for that request.

This protects compatibility with clients that do not know how to persist or poll tasks.

Persist the task ID immediately

Once the server returns:

taskId = task_123

persist it before relying on in-memory UI state.

On mobile and desktop, the app can:

  • crash;
  • be terminated;
  • be suspended;
  • lose network;
  • restart after an update.

A durable record might include:

local operation ID
MCP server connection ID
task ID
originating tool/request
created time
last known status
last poll time
conversation/tool-round ID

Do not store secrets unnecessarily.

The local operation ID and task ID solve different problems

Use both.

local operation ID -> your app's durable identity
MCP task ID        -> server's durable identity

If the server ever changes or a request is recreated, the app still has its own stable audit record.

tasks/get is authoritative for task state

Polling is a normal part of the current Tasks lifecycle.

A client can call:

tasks/get(taskId)

to retrieve current state.

The exact state vocabulary depends on the extension/spec/SDK representation, but your app should normalize it into a useful local lifecycle such as:

queued
running
waiting_for_input
completed
failed
cancelled
unknown

Do not build UI directly around one SDK enum without considering migration/versioning.

Poll with backoff

Do not poll long-running tasks in a tight loop.

A reasonable strategy can use:

short delay initially
-> increasing delay
-> bounded maximum interval

with server-provided retry hints when available.

Also stop polling when:

  • task reaches terminal state;
  • user cancels monitoring;
  • auth requires user action;
  • operation expires according to policy.

Do not treat a polling timeout as task failure

If tasks/get times out, the task may still be running.

These are different states:

status request failed

and:

task failed

The UI can show:

Task status temporarily unavailable
Last known: running

rather than falsely marking the task failed.

tasks/update supports task lifecycle changes

The 2026-07-28 Tasks extension includes tasks/update as part of the lifecycle.

The exact update semantics depend on the extension contract, but clients should treat it as an explicit protocol operation rather than mutating local state and assuming the server agrees.

If the user changes task state through the client:

send update
-> wait for server result
-> reconcile authoritative state

Cancellation is a race

A task may complete while the client is trying to cancel it.

Possible timeline:

T0 client sees running
T1 task completes on server
T2 user taps Cancel
T3 client sends tasks/cancel
T4 server reports already completed

The correct UI is not:

Cancelled

unless the server confirms that outcome.

Cancellation is a request, not retroactive deletion of work.

Persist cancellation intent separately

Useful local states:

running
cancel_requested
cancelled
completed

This avoids pretending that pressing the button instantly changed remote state.

Subscription notifications can reduce polling latency

Modern MCP uses subscriptions/listen for opt-in notifications.

For supported task-related updates, a client can listen for changes instead of relying only on polling.

But subscriptions should complement, not replace, authoritative status reads.

A subscription stream can disconnect or miss events while the app is suspended.

A robust pattern is:

notification received
-> refresh relevant task state

or:

reconnect
-> reconcile all active tasks

Do not depend on tasks/list

The redesigned Tasks extension removed broad task listing from the older experimental design because listing cannot be safely scoped in a stateless protocol without clear ownership/session boundaries.

The practical client implication is:

Persist task IDs you care about.

Do not assume the server can later enumerate every task the app ever created.

Task ownership must be explicit in the app

A task should be associated with:

  • server connection;
  • authorization/account profile;
  • originating user/workspace/chat;
  • original operation.

If the active account changes, do not blindly query old task IDs with the new account context.

Authentication can expire while a task runs

A task may take longer than the access token lifetime.

When polling returns an authorization error:

refresh/re-authorize according to OAuth policy
-> retry status request

Do not mark the task failed merely because the client’s token expired.

The task may still be running on the server.

App restart recovery should scan local active tasks

On startup:

load non-terminal task records
-> group by MCP server/account
-> re-establish auth as needed
-> query tasks/get
-> reconcile state

Do not start a duplicate original tool call just because the app lost the in-memory task object.

Unknown create outcome is the hardest case

Consider:

client sends tools/call + task opt-in
server creates task
network drops before client receives taskId

The client does not know the task ID.

Blindly replaying the original operation may create a second task.

This is an unknown-outcome problem.

Mitigation depends on server/tool semantics:

  • idempotency keys;
  • app-provided operation IDs;
  • reconciliation endpoint;
  • server deduplication;
  • user-visible recovery choice.

Tasks do not magically solve request idempotency.

Task result persistence should be separate from status polling

Once a task completes, persist the final result into the conversation/tool-round history according to your data model.

Do not rely on being able to call tasks/get forever.

A server may eventually expire task records.

Task retention is a product/security concern

Ask:

How long does the server retain task state?
Can the result contain sensitive data?
Can task IDs be guessed?
Does task access require the same authorization context?

Treat task identifiers as potentially sensitive references even if they are not credentials by themselves.

Long-running work needs cost/runtime guards

A model may accidentally trigger an expensive job.

Before task creation, the host can enforce:

permission approval
estimated scope preview
maximum allowed size
allowed tool list
user/account policy

After task creation, the server should also enforce its own quotas and authorization.

User approval should happen before task creation for side effects

If a tool is configured as Ask, get approval before sending the request that may create a durable task.

Do not create a deployment/export/delete task and then ask permission afterward.

See How to Build an MCP Client Permission System.

Task progress is not necessarily linear

Avoid assuming:

0% -> 100%

unless the server provides meaningful progress semantics.

A safer generic UI uses:

Queued
Running
Waiting for input
Completed
Failed

and displays percentage only when the server reports trustworthy progress.

Tasks and MRTR can interact

A long-running workflow may need input partway through.

Your architecture should not assume:

task = no interaction

or:

MRTR = only short requests

Keep durable task state and interactive input state as composable lifecycle features.

Avoid holding chat generation open unnecessarily

A model may initiate a task, but the host does not need to keep the original model streaming request alive for ten minutes.

A cleaner flow can be:

model requests tool
-> server creates task
-> host records task
-> model tells user work started
-> task completes later
-> host surfaces completion / resumes workflow if product supports it

This is a product decision, but the task architecture makes it possible.

Notifications need user consent/policy too

If a task can finish while the app is backgrounded, the client may offer a local notification.

Do not include sensitive task details on the lock screen by default.

Use privacy-aware notification text such as:

Background AI task completed

when appropriate.

Trace task lifecycle separately from tool call duration

A useful trace distinguishes:

tool request duration
create-task response duration
queue time
run time
poll count
final result fetch

Do not report “tool latency = 8 minutes” as one opaque number if the server spent 7 minutes queued.

A durable task record

interface MCPTaskRecord {
  localOperationID: string
  serverID: string
  credentialProfileID?: string
  taskID: string
  toolName?: string
  state: TaskState
  createdAt: Date
  updatedAt: Date
  lastCheckedAt?: Date
  conversationID?: string
  toolRoundID?: string
}

The exact schema will differ, but persistence should not depend on a live SDK object.

Test task recovery aggressively

Important cases:

task completes normally
poll request times out
auth expires mid-task
subscription disconnects
task fails
cancel races with completion
app restarts while running
app restarts after completion before result persisted
server forgets/expunges task
unknown create outcome
server returns malformed task state

These are much more important than testing only the happy path.

Test cross-instance statelessness

Task creation might happen on instance A while tasks/get lands on instance B.

That should work if the server’s task state is stored durably/appropriately.

Diagram illustrating the surrounding section

A process-local task map is not enough for horizontally scaled production servers.

Where BYOKchat fits

A local-first MCP host can persist task handles with tool-round history and recover them independently from the active model provider.

The client can keep:

  • server/account identity;
  • tool permission state;
  • local operation ID;
  • MCP task ID;
  • last known task status;
  • final tool result.

That makes long-running MCP work reliable without tying it to one provider stream or one app session.

Further reading

Keep reading