Server Progress Reporting
How to Report Progress
The entire server-side responsibility comes down to a single method call: ctx.report_progress(). Whenever you want to inform the client how far along the work has progressed, you simply await it:
from fastmcp import Context
async def some_long_running_task(ctx: Context):
# ... do some work ...
# report progress
await ctx.report_progress(progress=<current>, total=<total>)
# ... do more work ...
# report more progress
await ctx.report_progress(progress=<current>, total=<total>)
# ... finish up ...
The progress argument is a float representing how much work has been completed so far. The total argument is an optional float representing the full scope of the work. Crucially, both values are unitless — you decide what the scale means.
It could be:
- A count of items processed
- A number of bytes transferred
- A number of pipeline stages completed
- A plain percentage out of
100 - Or any other monotonic scale that makes sense for your task.
The protocol does not prescribe units; that is left entirely to your implementation.
In practice, three patterns cover the vast majority of use cases.
Patern 1: Known total count:
When you know the total item count upfront, you can report absolute progress by passing the current item index as progress and the total count as total — for example, progress=3, total=10 after processing the third of ten records.
await ctx.report_progress(progress=3, total=10) # 30% complete
Pattern 2: Percentage out of 100:
When the total is unknown but you still want a meaningful percentage, passing progress=75, total=100 always works regardless of what the underlying work actually is.
await ctx.report_progress(progress=75, total=100) # 75% complete
Pattern 3: Indeterminate progress:
Finally, when you have no idea how much work remains, you can omit total entirely and simply increment progress as work accumulates — this is called indeterminate progress, and it signals to the client that a definitive completion percentage cannot be calculated.
await ctx.report_progress(progress=1) # indeterminate progress
await ctx.report_progress(progress=2) # still indeterminate, but more work done
For operations that span multiple distinct stages, the recommended approach is to divide the total range into slices and assign each stage its own sub-range. Suppose you have four stages and you choose total=100 for convenience. You might report values from 0 to 25 during the first stage, 25 to 60 during the second, 60 to 80 during the third, and 80 to 100 during the fourth. The exact boundaries are up to you — what matters is that progress never decreases and that you reach the total value when truly finished.
There are a few requirements to keep in mind. The ctx parameter must appear in the tool's function signature typed as Context; FastMCP detects it there and injects the live context object automatically when the tool is called. Because report_progress() is a coroutine the tool should be declared async so that you can await the call. If you use a synchronous tool, FastMCP will still handle the call for you, but using async def is the recommended approach.
Finally, if the client does not have a progress_handler registered, progress notifications simply do nothing — they are silently discarded without raising any error. This means it is always safe to add progress reporting to a tool; it costs nothing when the client is not listening.
The server-side story ends there. How those notifications are received and acted upon is entirely the client's concern, handled through a progress_handler callback on the client side.
As a reminder, this is our client handler for progress notifications:
async def progress_handler(
progress: float,
total: float | None,
message: str | None,
) -> None:
if total is not None and total > 0:
percentage = (progress / total) * 100
percent_str = f"{percentage:.0f}%"
else:
percent_str = str(int(progress))
msg_part = f"{message}" if message else ""
print(f"[Progress] {percent_str} - {msg_part}")
In our server, we're going to add 3 progress updates to the dog age calculation tool:
- When we start looking up the breed multiplier
- When we have resolved the multiplier and are about to calculate the result
- When we have finished the calculation and are done
@mcp.tool
async def dog_to_human_age(
age: Annotated[int, Field(ge=0, le=30, description="The dog's age in years")],
breed: Annotated[str, Field(description="The dog's breed")],
ctx: Context,
) -> int:
"""Calculate the real age of a dog in human years based on its breed."""
total_steps = 3
# Progress: breed lookup
await ctx.report_progress(
progress=0,
total=total_steps,
message=f"Looking up breed multiplier for '{breed}'",
)
[...]
# Progress: multiplier resolved
await ctx.report_progress(
progress=1, total=total_steps, message="Multiplier resolved"
)
result = age * multiplier
# Progress: calculate result
await ctx.report_progress(
progress=total_steps,
total=total_steps,
message="Done",
)
return result
The full code is the same as before but with the progress updates added in:
cat > $HOME/workspace/puppy_guide/server/main.py << EOF
import os
from difflib import get_close_matches
from typing import Annotated
from dotenv import load_dotenv
from pydantic importPractical MCP with FastMCP & LangChain
Engineering the Agentic ExperienceEnroll now to unlock current content and receive all future updates for free. Your purchase supports the author and fuels the creation of more exciting content. Act fast, as the price will rise as the course nears completion!
