Skip to main content
RapidDev - Software Development Agency
mcp-tutorial

How to add streaming responses to an MCP server

Add streaming to your MCP server using progress notifications for long-running tools and Streamable HTTP transport for real-time communication. Send progress tokens to update clients on operation status, and use the Streamable HTTP transport to enable server-sent events for persistent connections. This keeps users informed during slow operations instead of waiting in silence.

What you'll learn

  • How to send progress notifications from long-running MCP tools
  • How to configure Streamable HTTP transport for real-time streaming
  • How to report incremental results during tool execution
  • How to handle streaming in both TypeScript and Python MCP servers
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced8 min read30-45 minMCP TypeScript SDK 1.x, Streamable HTTP transportMarch 2026RapidDev Engineering Team
TL;DR

Add streaming to your MCP server using progress notifications for long-running tools and Streamable HTTP transport for real-time communication. Send progress tokens to update clients on operation status, and use the Streamable HTTP transport to enable server-sent events for persistent connections. This keeps users informed during slow operations instead of waiting in silence.

Adding Streaming and Progress Notifications to MCP Servers

Long-running MCP tools — database migrations, API batch operations, file processing — can take seconds or minutes to complete. Without streaming, the client waits in silence with no feedback. MCP addresses this with two mechanisms: progress notifications that report completion percentage during tool execution, and Streamable HTTP transport that enables real-time server-to-client communication.

This tutorial covers both patterns. You will learn to send progress updates from your tool handlers and configure the Streamable HTTP transport for persistent connections with server-sent events (SSE).

Prerequisites

  • A working MCP server with registered tools
  • Understanding of MCP tool result format and transports
  • Node.js 18+ or Python 3.10+ installed
  • Familiarity with async programming and event streams

Step-by-step guide

1

Send progress notifications from tool handlers

When a client calls a tool, it can include a _meta.progressToken in the request. Your tool handler checks for this token and sends notifications/progress messages back to the client with the current progress (a number between 0 and total). This lets the client display a progress bar or status text. Access the progress token through the extra argument passed to your handler.

typescript
1// TypeScript
2import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3import { z } from "zod";
4
5const server = new McpServer({ name: "streaming-server", version: "1.0.0" });
6
7server.registerTool("process-files", {
8 description: "Process a batch of files with progress updates",
9 inputSchema: {
10 directory: z.string().describe("Directory to process"),
11 },
12}, async ({ directory }, extra) => {
13 const files = await getFileList(directory);
14 const total = files.length;
15 const results: string[] = [];
16
17 for (let i = 0; i < files.length; i++) {
18 // Send progress notification
19 if (extra.sendNotification) {
20 await extra.sendNotification({
21 method: "notifications/progress",
22 params: {
23 progressToken: extra._meta?.progressToken,
24 progress: i,
25 total,
26 message: `Processing ${files[i]}...`,
27 },
28 });
29 }
30
31 const result = await processFile(files[i]);
32 results.push(result);
33 }
34
35 return {
36 content: [{
37 type: "text",
38 text: `Processed ${total} files:\n${results.join("\n")}`,
39 }],
40 };
41});

Expected result: The client receives real-time progress updates as each file is processed.

2

Configure Streamable HTTP transport

The Streamable HTTP transport replaces the older SSE transport and supports bidirectional communication over HTTP. It uses server-sent events for server-to-client streaming and standard HTTP POST for client-to-server messages. Set up an Express server with the Streamable HTTP transport handler.

typescript
1// TypeScript
2import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4import express from "express";
5
6const app = express();
7const server = new McpServer({ name: "http-streaming-server", version: "1.0.0" });
8
9// Register tools...
10
11const transport = new StreamableHTTPServerTransport({
12 sessionIdGenerator: () => crypto.randomUUID(),
13});
14
15app.post("/mcp", async (req, res) => {
16 await transport.handleRequest(req, res);
17});
18
19app.get("/mcp", async (req, res) => {
20 // SSE endpoint for server-to-client streaming
21 await transport.handleRequest(req, res);
22});
23
24app.delete("/mcp", async (req, res) => {
25 // Session termination
26 await transport.handleRequest(req, res);
27});
28
29await server.connect(transport);
30
31app.listen(3000, () => {
32 console.error("Streamable HTTP MCP server on port 3000");
33});

Expected result: The server accepts HTTP connections and streams responses back to clients via server-sent events.

3

Report incremental results during execution

For tools that produce partial results over time (like search across multiple databases or multi-step analysis), send logging messages to keep the client informed. Use the notifications/message method to send log-level messages that clients can display in their UI.

typescript
1// TypeScript
2server.registerTool("multi-source-search", {
3 description: "Search across multiple data sources with incremental results",
4 inputSchema: {
5 query: z.string().describe("Search query"),
6 },
7}, async ({ query }, extra) => {
8 const sources = ["database", "documents", "api", "cache"];
9 const allResults: any[] = [];
10
11 for (let i = 0; i < sources.length; i++) {
12 const source = sources[i];
13
14 // Progress notification
15 if (extra.sendNotification) {
16 await extra.sendNotification({
17 method: "notifications/progress",
18 params: {
19 progressToken: extra._meta?.progressToken,
20 progress: i,
21 total: sources.length,
22 message: `Searching ${source}...`,
23 },
24 });
25
26 // Log intermediate results
27 await extra.sendNotification({
28 method: "notifications/message",
29 params: {
30 level: "info",
31 data: `Found results in ${source}`,
32 },
33 });
34 }
35
36 const results = await searchSource(source, query);
37 allResults.push(...results);
38 }
39
40 return {
41 content: [{
42 type: "text",
43 text: JSON.stringify({ total: allResults.length, results: allResults }, null, 2),
44 }],
45 };
46});

Expected result: The client sees progress for each data source and receives intermediate log messages during the search.

4

Handle streaming in Python FastMCP

In Python, access the MCP context to send progress notifications. FastMCP provides a context object through dependency injection that lets you send notifications during tool execution.

typescript
1# Python
2from mcp.server.fastmcp import FastMCP, Context
3
4mcp = FastMCP("streaming-python-server")
5
6@mcp.tool()
7async def process_batch(items: list[str], ctx: Context) -> str:
8 """Process a batch of items with progress reporting.
9
10 Args:
11 items: List of items to process
12 ctx: MCP context (injected automatically)
13 """
14 results = []
15 total = len(items)
16
17 for i, item in enumerate(items):
18 # Send progress
19 await ctx.report_progress(i, total)
20 await ctx.info(f"Processing item {i+1}/{total}: {item}")
21
22 result = await process_item(item)
23 results.append(result)
24
25 await ctx.report_progress(total, total)
26 return f"Processed {total} items: {', '.join(results)}"

Expected result: The Python server sends progress notifications as each item is processed.

5

Configure clients to connect via Streamable HTTP

MCP clients connect to Streamable HTTP servers using the URL directly. In Claude Desktop's configuration, specify the url field instead of command and args. The client handles the SSE connection for receiving streaming responses automatically. For production streaming MCP deployments with load balancing and session management, RapidDev can help architect the infrastructure.

typescript
1// claude_desktop_config.json
2{
3 "mcpServers": {
4 "my-streaming-server": {
5 "url": "http://localhost:3000/mcp"
6 }
7 }
8}
9
10// For remote servers with auth:
11{
12 "mcpServers": {
13 "my-remote-server": {
14 "url": "https://mcp.example.com/mcp"
15 }
16 }
17}

Expected result: Claude Desktop connects to your Streamable HTTP server and receives streaming progress updates during tool execution.

Complete working example

src/index.ts
1import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3import { z } from "zod";
4import express from "express";
5import crypto from "crypto";
6
7const app = express();
8const server = new McpServer({ name: "streaming-demo", version: "1.0.0" });
9
10server.registerTool("long-task", {
11 description: "A long-running task that reports progress",
12 inputSchema: {
13 steps: z.number().int().min(1).max(20).default(5)
14 .describe("Number of processing steps"),
15 },
16}, async ({ steps }, extra) => {
17 const results: string[] = [];
18
19 for (let i = 0; i < steps; i++) {
20 // Report progress
21 if (extra.sendNotification && extra._meta?.progressToken) {
22 await extra.sendNotification({
23 method: "notifications/progress",
24 params: {
25 progressToken: extra._meta.progressToken,
26 progress: i,
27 total: steps,
28 message: `Step ${i + 1} of ${steps}`,
29 },
30 });
31 }
32
33 // Simulate work
34 await new Promise(resolve => setTimeout(resolve, 1000));
35 results.push(`Step ${i + 1}: completed`);
36 }
37
38 return {
39 content: [{
40 type: "text",
41 text: `All ${steps} steps completed:\n${results.join("\n")}`,
42 }],
43 };
44});
45
46const transport = new StreamableHTTPServerTransport({
47 sessionIdGenerator: () => crypto.randomUUID(),
48});
49
50app.post("/mcp", async (req, res) => transport.handleRequest(req, res));
51app.get("/mcp", async (req, res) => transport.handleRequest(req, res));
52app.delete("/mcp", async (req, res) => transport.handleRequest(req, res));
53
54await server.connect(transport);
55
56app.listen(3000, () => {
57 console.error("Streaming MCP server on http://localhost:3000/mcp");
58});

Common mistakes when adding streaming responses to an MCP server

Why it's a problem: Sending progress without checking for progressToken

How to avoid: Only send notifications/progress if extra._meta?.progressToken exists. Not all clients request progress updates.

Why it's a problem: Forgetting the GET and DELETE handlers for Streamable HTTP

How to avoid: Streamable HTTP requires all three methods: POST (requests), GET (SSE stream), DELETE (session cleanup). Missing handlers break the transport.

Why it's a problem: Using the deprecated SSE transport

How to avoid: The standalone SSE transport is deprecated. Use StreamableHTTPServerTransport which includes SSE streaming built-in.

Why it's a problem: Not sending final progress notification

How to avoid: Send a final progress with progress === total so the client knows the operation is 100% complete.

Best practices

  • Send progress notifications for any tool that takes more than 2-3 seconds to complete
  • Include a human-readable message in each progress notification describing the current step
  • Check for progressToken before sending progress — not all clients support it
  • Use Streamable HTTP transport for remote servers that need real-time streaming
  • Send a final progress notification with progress === total to signal completion
  • Use notifications/message for log-level information during long operations
  • Keep progress increments meaningful — do not send hundreds of updates per second
  • Test streaming behavior with the MCP Inspector which displays progress in real-time

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I have an MCP server with a long-running tool. Show me how to send progress notifications during execution using the extra.sendNotification callback, and how to set up Streamable HTTP transport for real-time streaming.

MCP Prompt

Add progress reporting to my MCP tool [tool-name]. It processes [N items] in a loop. Send notifications/progress with the current item index and total count. Also set up Streamable HTTP transport so remote clients can receive the updates.

Frequently asked questions

What is the difference between stdio and Streamable HTTP transport?

Stdio communicates via stdin/stdout and is used for local servers launched as child processes. Streamable HTTP uses HTTP POST/GET/DELETE with SSE streaming and is used for remote servers accessible over the network. Streamable HTTP supports progress streaming natively.

Does the deprecated SSE transport still work?

The standalone SSE transport still functions but is deprecated. Migrate to Streamable HTTP, which includes SSE capability and adds support for stateless operation and session management.

Can I stream partial tool results (not just progress)?

MCP tools return a single result at the end of execution. For partial results, use notifications/message to send intermediate log messages. The final return value should contain the complete result.

How do I test streaming without a client?

Use the MCP Inspector (npx @modelcontextprotocol/inspector) which connects to your server and displays progress notifications in real-time. You can also use curl to test the HTTP endpoints directly.

Is Streamable HTTP required for progress notifications?

No. Progress notifications work over stdio transport too. The client receives them as JSON-RPC notifications on the same channel. Streamable HTTP adds the ability for the server to push notifications proactively via SSE.

Can I use WebSockets instead of SSE?

MCP uses SSE (server-sent events) as part of the Streamable HTTP transport, not WebSockets. SSE is simpler, works through proxies, and is sufficient for the server-to-client push pattern. For full duplex needs, consider using stdio locally. The RapidDev team can help evaluate transport options for your deployment scenario.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.