<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 "> Streaming Progress

Streaming Progress

Lesson 9 (Serving over HTTP) mentioned that a Streamable HTTP response is one SSE event or many, depending on what the tool does. Every call you've made so far sent exactly one. A tool that takes a few seconds can send several instead, progress updates that arrive while it's still running, not just a result at the end.

Add a countdown tool

A client that wants updates puts a progressToken on the call, in _meta. No token means it didn't ask, so send nothing. Add this to src/mcp-server.ts:

server.registerTool(
  "countdown",
  {
    title: "Countdown Timer",
    description: "Counts down from a number with real-time progress updates",
    inputSchema: z.object({
      seconds: z.number().describe("Number of seconds to count down from")
    })
  },
  async ({ seconds }, ctx) => {
    const progressToken = ctx.mcpReq._meta?.progressToken;

    for (let i = seconds; i > 0; i--) {
      if (progressToken !== undefined) {
        await ctx.mcpReq.notify({
          method: "notifications/progress",
          params: {
            progressToken,
            progress: seconds - i + 1,
            total: seconds,
            message: `${i} seconds remaining!`
          }
        });
      }
      await new Promise(resolve => setTimeout(resolve, 1000));
    }

    return { content: [{ type: "text", text: "🚀 Blastoff!" }] };
  }
);

ctx.mcpReq carries every per-request helper, notify for sending notifications and signal for reacting to cancellation. progressToken is opt-in: a caller that didn't send one gets no notifications, so guard every notify call on it being defined. progress must increase on every notification; total is optional but is what lets a client show a percentage instead of a bare counter.

Note: a response stream cannot be resumed. If it drops mid-call the request is lost, and the client has to send a fresh one with a new id rather than picking up where the stream stopped, so nothing already streamed arrives twice. That is what makes progress notifications a fit for work measured in seconds. Work that should outlive a single request wants the Tasks extension lesson 11 (Going Further) points at instead.

Watch it stream in Inspector

Restart http-server.ts if it isn't running.

npx @modelcontextprotocol/inspector --catalog inspector-catalog.json

Connect to demo-server-http, call countdown from the Tools tab with seconds: 5, and open the Notifications panel. Each progress update lands there in real time as it arrives, timestamped, before the tool result shows up in Tools.

Watch it stream in your assistant

Whether you see anything depends on your client, because progress is opt-in per call. A client that wants updates attaches a progressToken; one that doesn't gets a single result, and your server is correct either way. That's what the guard is for.

Ask your assistant to run the countdown, for example "count down from 10 using the countdown tool." If it attached a token you'll see incremental updates in the chat UI as each notification arrives; if it didn't, the call simply completes with 🚀 Blastoff! and no intermediate output.

Check your understanding

Question 1 of 3

Why guard on progressToken?

The countdown tool only sends notifications when `ctx.mcpReq._meta?.progressToken` is defined. Why not always send them?