Model Context ProtocolMCP Python SDK 2.2 · LangChain 1.4 · Python 3.10+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
27 small wins to finish your pathNext lesson →

Progress notifications

A progress notification is a message a tool sends while it runs to report how far a slow job has got, before the result is ready.

Last updated: 29 Sep, 2026 · MCP 2.2

This lesson's server goes in a file of its own, progress_demo.py, and shop.py stays as it is.

Exampleprogress_demo.py
import asyncio

from mcp.server import MCPServer
from mcp.server.mcpserver import Context

mcp = MCPServer("Shop support")


@mcp.tool()
async def export_orders(order_ids: list[str], ctx: Context) -> str:
    """Export orders to the accounting system, one at a time."""
    for done, order_id in enumerate(order_ids, start=1):
        await asyncio.sleep(0.1)
        await ctx.report_progress(done, total=len(order_ids), message=f"exported {order_id}")
    return f"Exported {len(order_ids)} orders."

await ctx.report_progress(progress, total, message) sends one notification. progress must go up with each report. total is optional; leave it out when you do not know it rather than guessing.

Listening from the client

Example
import asyncio

from mcp import Client
from progress_demo import mcp


async def main():
    async with Client(mcp) as client:
        async def show(progress, total, message):
            print(f"  {progress:.0f}/{total:.0f} {message}")

        result = await client.call_tool("export_orders", {"order_ids": ["A17", "B42", "C03"]}, progress_callback=show)
        print(result.content[0].text)


asyncio.run(main())
  • Three reports arrived while the tool ran, one per order, before the result line.
  • The callback goes on the call, not the client, so each call can handle progress its own way.
Example
import asyncio

from mcp import Client
from progress_demo import mcp


async def main():
    async with Client(mcp) as client:
        result = await client.call_tool("export_orders", {"order_ids": ["A17", "B42"]})
        print(result.content[0].text)


asyncio.run(main())

Without a callback, report_progress does nothing and nothing fails, so a tool can report unconditionally without checking whether anyone is listening.

With a callback vs without one

No progress_callbackWith a callback
report_progressDoes nothingSends each report
The tool codeUnchangedUnchanged
The client seesOnly the resultEach step, then the result

When to report progress

  • A tool that loops over many items, like an export or a batch.
  • Work that waits on a slow service, so the user knows it is alive.
  • Any call that runs long enough to look stuck.
Watch out
progress must increase with each report. Sending the same or a lower number confuses a client drawing a bar, so count the work done, not the work left.
Try it yourself
  • Leave out total and print what the callback receives.
  • Report progress only for every second order.
  • Make the callback print a bar of # characters, one per order.
PreviousLifespan

Every expert started right here.