CourseModel Context Protocol · Module 9: Production and Ecosystem · part 63 of 83
Part 63 · Module 9: Production and Ecosystem

Topic 6: Long-running work

9 min read·22 Sept 2026

Progress and cooperative cancellation

Module 5 covered the handler side: await ctx.report_progress(done, total, message) streams notifications/progress while the tool runs, and it is a no-op when the caller did not ask. Cancellation in Python async code is cooperative: the runtime can only stop your handler at an await, so a long loop needs regular await points, and state must be consistent at each one. run_import checkpoints before its sleep, so a cancellation there loses nothing.

The client that every Part B to E demo uses shows the client half: answering the confirmation and printing progress.

python
"""Call import_notes through the balancer: a confirmation round, then the import with progress."""
from __future__ import annotations

import json
import sys
from pathlib import Path

import anyio

from mcp import Client
from mcp.client import ClientRequestContext
from mcp.shared.exceptions import MCPError
from mcp.types import ElicitRequestParams, ElicitResult

BATCH = Path(__file__).parent / "data" / "m09_import_batch.json"


async def confirm(context: ClientRequestContext, params: ElicitRequestParams) -> ElicitResult:
    print(f"  question from server: {params.message!r} -> answering yes")
    return ElicitResult(action="accept", content={"ok": True})


async def show_progress(progress: float, total: float | None, message: str | None) -> None:
    print(f"  progress {progress:.0f}/{total:.0f}: {message}")


async def main(url: str, key: str, count: int, mode: str = "auto") -> None:
    items = json.loads(BATCH.read_text(encoding="utf-8"))[:count]
    async with Client(url, elicitation_callback=confirm, mode=mode) as client:
        try:
            result = await client.call_tool(
                "import_notes", {"items": items, "idempotency_key": key}, progress_callback=show_progress
            )
        except MCPError as exc:
            print(f"  MCPError {exc.code}: {exc.message}")
            return
        print("  result:", json.dumps(result.structured_content))


if __name__ == "__main__":
    anyio.run(main, sys.argv[1], sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 2)

The rest of this course is yours to keep

This course is bought on its own, once, and stays readable afterwards, including the parts added to it later.