Quick answer

A long-running MCP tool that gives no feedback turns the agent into a black box. The agent cannot tell whether the tool is working, has stalled, or should be abandoned, and users staring at a spinner have no better information. The Model Context Protocol defines progress notifications for exactly this: the tool reports incremental progress through a token the caller supplied, and the client surfaces it without ending the call. If your tool can run longer than a few seconds, accept a progress token, emit percentage or stage updates, and keep the final result separate from those notifications.

How progress flows in MCP

The caller includes a progress token when it invokes the tool. The server uses that token to send notifications on the same channel while the work continues. The sequence is distinct from the final tool result.

StageMessageMeaning
Calltools/call with _meta.progressTokenClient wants progress updates
During worknotifications/progressPartial status, request still open
Donetools/call resultFinal payload, ends the call

If the client does not supply a token, the tool simply runs without emitting progress; the token is optional, so the server must not require it.

Emitting structured updates

A progress notification carries a numeric value and, when the total is known, a total so the client can compute a percentage. A message field adds human-readable context.

async function exportDataset(args, progressToken, send) {
  const rows = await fetchRowCount(args.filter);
  const total = rows.length;
  for (let i = 0; i < total; i += 1) {
    await processOne(rows[i]);
    if (progressToken !== undefined && (i % 100 === 0 || i === total - 1)) {
      send({
        jsonrpc: "2.0",
        method: "notifications/progress",
        params: {
          progressToken,
          progress: i + 1,
          total,
          message: `Processing row ${i + 1} of ${total}`,
        },
      });
    }
  }
  return { exported: total };
}

Throttling matters: emitting a notification for every row in a large dataset floods the channel. Send updates at a bounded cadence or when the percentage meaningfully changes, and always emit a final notification so the client does not sit below 100 percent before the result arrives.

What the client should do

The client owns how progress is presented, but it should treat progress notifications as advisory and the tool result as the only completion signal. A notification reaching 100 percent does not mean the call finished; the final result may still carry an error after all rows were processed. Keep any cancellation tied to a separate mechanism rather than assuming progress implies success.

  • Map progress to a percentage only when total is present; otherwise show the raw stage message.
  • Update the same status line rather than appending a new line per notification.
  • Reset the indicator when the final result arrives, whether it is success or an error.
  • Never use a progress notification as the tool's return value.

Progress versus cancellation versus async jobs

These three tools solve different problems and are often conflated. Progress tells the caller the request is alive. Cancellation lets the caller stop it. An async-job model returns a handle immediately and lets the caller poll later. Use progress for work that completes within the request lifetime but takes a while; use the async-job pattern for work that outlives a reasonable request window or survives client disconnects. Progress and cancellation compose well together, since an informed user is more likely to cancel cleanly rather than kill the process.

Progress over streamable HTTP and across reconnects

The transport changes how progress reaches the client, so design the notifications to survive it. Over stdio, progress and the final result share one local process and arrive in order with little risk. Over streamable HTTP, a shared tool may serve many clients and each progress notification has to be delivered to the connection that owns the matching token, which makes echoing the correct token even more important. If a client reconnects mid-request, the server should be able to resume status for an in-flight job rather than restarting the work, ideally by correlating the progress token with a short-lived job identifier on the server. Treat progress notifications as idempotent and safe to repeat: a reconnecting client may receive a status it already saw, so include a monotonically increasing value and let the client ignore anything older than the latest it has applied. This keeps a flaky network from making the displayed progress jump backward while the underlying work continues once, not twice.

Common implementation mistakes

The most frequent error is sending progress without echoing the caller's exact token, so the client cannot match the notification to the request and drops it. Others include emitting only a spinner with no numeric value when the total is actually knowable, continuing to send notifications after the final result, and failing to throttle, which makes a long export generate thousands of messages. Validate the full lifecycle in a test client that records the token, every notification, and the final result in order.

FAQ

Is a progress token required on every tool call?

No. It is optional metadata the client may include. The server must work correctly when it is absent by simply running the tool without notifications, and use the token only when provided.

What if I cannot know the total amount of work?

Send progress without a total and rely on the message field for stage context, such as "connecting" or "scanning page 3." The client then shows an indeterminate indicator rather than a misleading percentage.

How often should I notify?

At a bounded cadence, roughly every few hundred milliseconds or when a meaningful percentage boundary is crossed. Throttle tight loops and always include a final update before the result, so the interface never appears stuck.

Does progress remove the need for a timeout?

It does not. Progress indicates liveness but a tool can still run longer than the client is willing to wait. Keep a deadline and a cancellation path, and use progress to distinguish genuine work from a true hang rather than as a reason to wait indefinitely.

This is part of the The MCP Server Guide — start there for the full design of tools that agents can call reliably.