Lesson 8 - Step 4 of 6

SSE Response Streaming

Implement Server-Sent Events to stream JSON-RPC responses to clients.

Streaming Responses with SSE

Instead of sending a single JSON response, we can stream multiple events using Server-Sent Events. This is useful for long-running operations, progress updates, or sending multiple responses.

SSE Writer Class

Let's create a helper class to properly format and send SSE events:

python
"""
sse_writer.py - Server-Sent Events writer for HTTP responses.
"""

import json


class SSEWriter:
    """Write Server-Sent Events to an HTTP response."""

    def __init__(self, wfile):
        """
        Initialize with a writable file object.

        Args:
            wfile: The response write file (self.wfile from handler)
        """
        self.wfile = wfile
        self.event_id = 0

    def send_event(self, data, event_type="message", event_id=None):
        """
        Send a single SSE event.

        Args:
            data: The event data (will be JSON-encoded if dict)
            event_type: The event type (default: "message")
            event_id: Optional event ID (auto-incremented if None)
        """
        # Auto-increment event ID if not provided
        if event_id is None:
            self.event_id += 1
            event_id = self.event_id

        # JSON-encode dict data
        if isinstance(data, dict):
            data = json.dumps(data)

        # Build the event
        lines = []
        lines.append(f"event: {event_type}")
        lines.append(f"id: {event_id}")
        lines.append(f"data: {data}")

        # Join with newlines and add blank line
        event = "\n".join(lines) + "\n\n"

        # Write and flush
        self.wfile.write(event.encode("utf-8"))
        self.wfile.flush()

    def send_json_rpc(self, response):
        """
        Send a JSON-RPC response as an SSE event.

        Args:
            response: A JSON-RPC response dict
        """
        self.send_event(response, event_type="message")

    def send_comment(self, text):
        """
        Send an SSE comment (for keep-alive).

        Args:
            text: Comment text
        """
        comment = f": {text}\n\n"
        self.wfile.write(comment.encode("utf-8"))
        self.wfile.flush()

Using SSE in the Handler

Now integrate the SSE writer into your HTTP handler:

python
class MCPHTTPHandler(BaseHTTPRequestHandler):
    """Handle MCP requests with SSE support."""

    def send_sse_headers(self):
        """Send headers for SSE response."""
        self.send_response(200)
        self.send_header("Content-Type", "text/event-stream")
        self.send_header("Cache-Control", "no-cache")
        self.send_header("Connection", "keep-alive")
        self.send_header("Access-Control-Allow-Origin", "*")
        self.end_headers()

    def handle_sse_response(self, request):
        """Handle a request with SSE streaming response."""
        # Send SSE headers
        self.send_sse_headers()

        # Create SSE writer
        sse = SSEWriter(self.wfile)

        # Process the request
        method = request.get("method")
        request_id = request.get("id")
        params = request.get("params", {})

        # Route and handle the request
        try:
            if method == "initialize":
                response = self.handle_initialize(request_id, params)
            elif method == "tools/list":
                response = self.handle_tools_list(request_id)
            elif method == "tools/call":
                # Tools might send progress events
                response = self.handle_tools_call(request_id, params, sse)
            else:
                response = {
                    "jsonrpc": "2.0",
                    "id": request_id,
                    "error": {
                        "code": -32601,
                        "message": f"Method not found: {method}"
                    }
                }

            # Send the final response
            sse.send_json_rpc(response)

        except Exception as e:
            # Send error response
            sse.send_json_rpc({
                "jsonrpc": "2.0",
                "id": request_id,
                "error": {
                    "code": -32603,
                    "message": f"Internal error: {e}"
                }
            })

Progress Notifications

For long-running tool calls, you can send progress notifications before the final result:

python
def handle_tools_call(self, request_id, params, sse):
    """Handle tools/call with progress streaming."""
    tool_name = params.get("name", "")
    arguments = params.get("arguments", {})

    # For demonstration: a slow tool that reports progress
    if tool_name == "slow_search":
        query = arguments.get("query", "")

        # Send progress notifications
        for i in range(1, 4):
            sse.send_json_rpc({
                "jsonrpc": "2.0",
                "method": "notifications/progress",
                "params": {
                    "progressToken": request_id,
                    "progress": i * 25,
                    "total": 100,
                    "message": f"Searching... {i}/4 complete"
                }
            })
            import time
            time.sleep(0.5)  # Simulate work

        # Return final result
        return {
            "jsonrpc": "2.0",
            "id": request_id,
            "result": {
                "content": [
                    {
                        "type": "text",
                        "text": f"Found 42 results for '{query}'"
                    }
                ]
            }
        }

    # Unknown tool
    return {
        "jsonrpc": "2.0",
        "id": request_id,
        "error": {
            "code": -32602,
            "message": f"Unknown tool: {tool_name}"
        }
    }

SSE Output Example

When streaming a slow tool call, the SSE output looks like this:

text
event: message
id: 1
data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":1,"progress":25,"total":100,"message":"Searching... 1/4 complete"}}

event: message
id: 2
data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":1,"progress":50,"total":100,"message":"Searching... 2/4 complete"}}

event: message
id: 3
data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":1,"progress":75,"total":100,"message":"Searching... 3/4 complete"}}

event: message
id: 4
data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"Found 42 results for 'example'"}]}}

Keep-Alive

For long-lived SSE connections, send periodic comments (lines starting with :) to prevent timeouts. Comments are ignored by clients but keep the connection alive.

Testing SSE Responses

Test SSE streaming with curl:

bash
# Request with Accept header for SSE
curl -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "slow_search",
      "arguments": {"query": "example"}
    }
  }'

# Output shows streamed events in real-time:
# event: message
# id: 1
# data: {"jsonrpc":"2.0","method":"notifications/progress",...}
# ...
Next Step

Now let's implement session management to track client connections and maintain state between requests.