On this page
- Tasks moved out of the core protocol
- The server decides when a request becomes a task
- A simplified lifecycle
- Why server-directed task creation is useful
- Do not assume a task is created unless the request opted in
- Persist the task ID immediately
- The local operation ID and task ID solve different problems
- tasks/get is authoritative for task state
- Poll with backoff
- Do not treat a polling timeout as task failure
- tasks/update supports task lifecycle changes
- Cancellation is a race
- Persist cancellation intent separately
- Subscription notifications can reduce polling latency
- Do not depend on tasks/list
- Task ownership must be explicit in the app
- Authentication can expire while a task runs
- App restart recovery should scan local active tasks
- Unknown create outcome is the hardest case
- Task result persistence should be separate from status polling
- Task retention is a product/security concern
- Long-running work needs cost/runtime guards
- User approval should happen before task creation for side effects
- Task progress is not necessarily linear
- Tasks and MRTR can interact
- Avoid holding chat generation open unnecessarily
- Notifications need user consent/policy too
- Trace task lifecycle separately from tool call duration
- A durable task record
- Test task recovery aggressively
- Test cross-instance statelessness
- Where BYOKchat fits
- 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
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.
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.