# How to add streaming responses to an MCP server

- Tool: MCP
- Difficulty: Advanced
- Time required: 30-45 min
- Compatibility: MCP TypeScript SDK 1.x, Streamable HTTP transport
- Last updated: March 2026

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

## Before you start

- 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
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

const server = new McpServer({ name: "streaming-server", version: "1.0.0" });

server.registerTool("process-files", {
  description: "Process a batch of files with progress updates",
  inputSchema: {
    directory: z.string().describe("Directory to process"),
  },
}, async ({ directory }, extra) => {
  const files = await getFileList(directory);
  const total = files.length;
  const results: string[] = [];

  for (let i = 0; i < files.length; i++) {
    // Send progress notification
    if (extra.sendNotification) {
      await extra.sendNotification({
        method: "notifications/progress",
        params: {
          progressToken: extra._meta?.progressToken,
          progress: i,
          total,
          message: `Processing ${files[i]}...`,
        },
      });
    }

    const result = await processFile(files[i]);
    results.push(result);
  }

  return {
    content: [{
      type: "text",
      text: `Processed ${total} files:\n${results.join("\n")}`,
    }],
  };
});
```

**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
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";

const app = express();
const server = new McpServer({ name: "http-streaming-server", version: "1.0.0" });

// Register tools...

const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: () => crypto.randomUUID(),
});

app.post("/mcp", async (req, res) => {
  await transport.handleRequest(req, res);
});

app.get("/mcp", async (req, res) => {
  // SSE endpoint for server-to-client streaming
  await transport.handleRequest(req, res);
});

app.delete("/mcp", async (req, res) => {
  // Session termination
  await transport.handleRequest(req, res);
});

await server.connect(transport);

app.listen(3000, () => {
  console.error("Streamable HTTP MCP server on port 3000");
});
```

**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
server.registerTool("multi-source-search", {
  description: "Search across multiple data sources with incremental results",
  inputSchema: {
    query: z.string().describe("Search query"),
  },
}, async ({ query }, extra) => {
  const sources = ["database", "documents", "api", "cache"];
  const allResults: any[] = [];

  for (let i = 0; i < sources.length; i++) {
    const source = sources[i];

    // Progress notification
    if (extra.sendNotification) {
      await extra.sendNotification({
        method: "notifications/progress",
        params: {
          progressToken: extra._meta?.progressToken,
          progress: i,
          total: sources.length,
          message: `Searching ${source}...`,
        },
      });

      // Log intermediate results
      await extra.sendNotification({
        method: "notifications/message",
        params: {
          level: "info",
          data: `Found results in ${source}`,
        },
      });
    }

    const results = await searchSource(source, query);
    allResults.push(...results);
  }

  return {
    content: [{
      type: "text",
      text: JSON.stringify({ total: allResults.length, results: allResults }, null, 2),
    }],
  };
});
```

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

```
# Python
from mcp.server.fastmcp import FastMCP, Context

mcp = FastMCP("streaming-python-server")

@mcp.tool()
async def process_batch(items: list[str], ctx: Context) -> str:
    """Process a batch of items with progress reporting.

    Args:
        items: List of items to process
        ctx: MCP context (injected automatically)
    """
    results = []
    total = len(items)

    for i, item in enumerate(items):
        # Send progress
        await ctx.report_progress(i, total)
        await ctx.info(f"Processing item {i+1}/{total}: {item}")

        result = await process_item(item)
        results.append(result)

    await ctx.report_progress(total, total)
    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.

```
// claude_desktop_config.json
{
  "mcpServers": {
    "my-streaming-server": {
      "url": "http://localhost:3000/mcp"
    }
  }
}

// For remote servers with auth:
{
  "mcpServers": {
    "my-remote-server": {
      "url": "https://mcp.example.com/mcp"
    }
  }
}
```

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

## Complete code example

File: `src/index.ts`

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";
import express from "express";
import crypto from "crypto";

const app = express();
const server = new McpServer({ name: "streaming-demo", version: "1.0.0" });

server.registerTool("long-task", {
  description: "A long-running task that reports progress",
  inputSchema: {
    steps: z.number().int().min(1).max(20).default(5)
      .describe("Number of processing steps"),
  },
}, async ({ steps }, extra) => {
  const results: string[] = [];

  for (let i = 0; i < steps; i++) {
    // Report progress
    if (extra.sendNotification && extra._meta?.progressToken) {
      await extra.sendNotification({
        method: "notifications/progress",
        params: {
          progressToken: extra._meta.progressToken,
          progress: i,
          total: steps,
          message: `Step ${i + 1} of ${steps}`,
        },
      });
    }

    // Simulate work
    await new Promise(resolve => setTimeout(resolve, 1000));
    results.push(`Step ${i + 1}: completed`);
  }

  return {
    content: [{
      type: "text",
      text: `All ${steps} steps completed:\n${results.join("\n")}`,
    }],
  };
});

const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: () => crypto.randomUUID(),
});

app.post("/mcp", async (req, res) => transport.handleRequest(req, res));
app.get("/mcp", async (req, res) => transport.handleRequest(req, res));
app.delete("/mcp", async (req, res) => transport.handleRequest(req, res));

await server.connect(transport);

app.listen(3000, () => {
  console.error("Streaming MCP server on http://localhost:3000/mcp");
});
```

## Common mistakes

- **Sending progress without checking for progressToken** — undefined Fix: Only send notifications/progress if extra._meta?.progressToken exists. Not all clients request progress updates.
- **Forgetting the GET and DELETE handlers for Streamable HTTP** — undefined Fix: Streamable HTTP requires all three methods: POST (requests), GET (SSE stream), DELETE (session cleanup). Missing handlers break the transport.
- **Using the deprecated SSE transport** — undefined Fix: The standalone SSE transport is deprecated. Use StreamableHTTPServerTransport which includes SSE streaming built-in.
- **Not sending final progress notification** — undefined Fix: 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

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

---

Source: https://www.rapidevelopers.com/mcp-tutorial/how-to-add-streaming-to-mcp-server
© RapidDev — https://www.rapidevelopers.com/mcp-tutorial/how-to-add-streaming-to-mcp-server
