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
Why guard on progressToken?
The countdown tool only sends notifications when `ctx.mcpReq._meta?.progressToken` is defined. Why not always send them?