<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 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.

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

If you're using GitHub Copilot, this is the version worth seeing. Copilot attaches a progressToken to every tool call on its own, you don't opt in from the client side, the assistant already did. Ask it to run the countdown, for example "count down from 10 using the countdown tool," and watch for a progress bar in the chat interface, filling in as each notification arrives, not a static "working…" spinner.

If you're using Claude Code, skip this step. Claude Code's MCP client doesn't attach a progressToken to tool calls and its terminal UI doesn't render progress notifications, so a countdown tool call there just shows a generic "Calling demo-server..." with none of the intermediate updates visible, even though your server is sending them. The Inspector step above is the reliable way to see this tool's streaming behavior on Claude Code.

Note: whichever client you saw it in, this is the same mechanism, just rendered differently. A Notifications panel entry and a progress bar are two clients making two different choices about what to do with the same notifications/progress messages.

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?