Progress & cancellation
Long-running actions stream progress forward, and may be cancelled at any moment. Both are first-class in the protocol.
Streaming progress
Section titled “Streaming progress”tesseron.action('importCsv') .input(z.object({ url: z.string().url() })) .handler(async ({ url }, ctx) => { ctx.progress({ message: 'downloading', percent: 5 }); const rows = await fetchCsv(url);
for (let i = 0; i < rows.length; i += 100) { if (ctx.signal.aborted) throw new Error('Cancelled'); ctx.progress({ message: `${i}/${rows.length}`, percent: 5 + Math.floor((i / rows.length) * 90), }); await importBatch(rows.slice(i, i + 100)); }
return { imported: rows.length }; });Wire format - sent by the app as a notification (no response):
{ "jsonrpc": "2.0", "method": "actions/progress", "params": { "invocationId": "inv_abc", "message": "500/2000", "percent": 27, "data": { "etaMs": 14000 } }}All three payload fields (message, percent, data) are optional. Send any combination. The MCP gateway forwards the notification to the agent as MCP notifications/progress; MCP clients render them at their leisure.
Guideline: cap progress updates at ~2 / second. Faster rates spam the agent UI without adding information.
Cancellation
Section titled “Cancellation”tesseron.action('generateReport') .input(...) .handler(async (input, ctx) => { const rows = await slowQuery(ctx.signal); // pass signal down if (ctx.signal.aborted) throw new Cancelled(); return formatReport(rows); });ctx.signalis a standardAbortSignal. Pass it tofetch,setTimeout, database drivers, or anything else that accepts one.- Cancellation fires for two reasons: the agent explicitly cancelled, or the action's timeout expired. Your handler treats them the same way - yield as fast as you can.
- After abort, the SDK returns an error response with code
-32001 Cancelled(explicit) or-32002 Timeout(timer).
Wire format - notification from gateway to app:
{ "jsonrpc": "2.0", "method": "actions/cancel", "params": { "invocationId": "inv_abc" }}The app doesn't acknowledge the cancellation. It just aborts the signal and lets the normal response path return an error.
Reading the progress on the agent side
Section titled “Reading the progress on the agent side”When the agent is Claude Code, progress notifications surface in the running tool-call block. For other MCP clients, behaviour varies - some render a progress bar, some print each message, some ignore them entirely. Don't depend on rich rendering; treat progress as "best-effort hint".
What NOT to use progress for
Section titled “What NOT to use progress for”- Final results. Use the response.
- Data the next handler needs. Use a return value, a sampling round-trip, or a resource.
- Error surfacing. Return an error response.
Next: sampling - handlers that call back into the LLM.