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