Observe and control execution
session.callTool() returns a ToolExecution for immediate and task-backed results. Choose the runtime policy after the server responds.
Settle and observe progress
import { resultFromTaskOutcome } from "@modelcontextprotocol/ext-tasks/client";
const execution = await session.callTool("render-report", {
accountId: "acct-42",
});
const { outcome, lastTask } = await execution.settle({
onEvent(event) {
if (event.type === "task") {
console.log(event.task.status, event.task.statusMessage);
}
},
});
const report = resultFromTaskOutcome(outcome);An immediate result may produce no task event. A task-backed call reports task snapshots as it progresses. lastTask contains the most recent snapshot.
Task-backed executions also expose the task handle and handoff API:
if (execution.kind === "task") {
console.log(execution.handle.taskId);
await execution.handoff((reference) => taskStore.save(reference));
}Read the terminal outcome
const outcome = await execution.result();
switch (outcome.status) {
case "completed":
console.log(outcome.result);
break;
case "failed":
console.error(outcome.error.message);
break;
case "cancelled":
console.log("The work was cancelled");
break;
}Switch on outcome.status when failed and cancelled states are application data. Use resultFromTaskOutcome(outcome) for result-or-throw handling.
Choose what happens when you stop waiting
| Method | Effect |
|---|---|
cancel() | Requests remote cancellation and releases local ownership. |
detach() | Releases local ownership while the remote task keeps running. |
close() | Releases ownership and best-effort cancels unfinished work. |
Aborting a settlement or observation signal stops local waiting. Use cancel() to request remote cancellation.
Observe the update stream yourself
A UI may need its own async stream instead of an onEvent callback:
for await (const event of execution.updates()) {
if (event.type === "task") renderProgress(event.task);
if (event.type === "outcome") renderOutcome(event.outcome);
}updates() has one consumer. A second acquisition throws TaskUpdatesAlreadyAcquiredError. Use settle({ onEvent }) when a separate stream is unnecessary.
Set request context
const execution = await session.callTool(
"render-report",
{ accountId: "acct-42" },
{
headers: { "x-trace-id": "trace-9" },
requestTimeoutMs: 15_000,
signal: AbortSignal.timeout(60_000),
},
);headers and requestTimeoutMs apply to the initial call and every task follow-up request. The timeout is per request. signal bounds the caller's local lifecycle across the operation.
For a task ID obtained elsewhere, pass the same request context when creating session.task(taskId, options). Manual controllers and cross-session handoff are covered in Application input and recovery.
Next steps
Continue with application input and recovery when a tool can pause for input or outlive this connection. See Troubleshooting for ownership and cancellation failures.