Lesson 7 - Step 3 of 5

Writing to stdout

Learn to write JSON-RPC responses correctly with proper encoding and flushing.

The Critical Rules of stdout

Writing to stdout in MCP STDIO transport requires careful attention. The client parses every byte from your stdout as JSON-RPC messages.

CRITICAL: stdout is ONLY for JSON-RPC

Never write anything to stdout except valid JSON-RPC messages:

  • No print() statements for debugging
  • No error messages or stack traces
  • No progress indicators or status messages
  • No library output (some libraries print to stdout!)

Any non-JSON output will cause parse errors and break the connection.

Basic Response Writing

Here's the fundamental pattern for writing JSON-RPC responses:

python
import sys
import json

def send_response(msg_id, result):
    """Send a JSON-RPC success response."""
    response = {
        "jsonrpc": "2.0",
        "id": msg_id,
        "result": result
    }

    # Convert to JSON - compact, single line!
    json_str = json.dumps(response)

    # Write with newline terminator
    sys.stdout.write(json_str + "\n")

    # CRITICAL: Flush immediately!
    sys.stdout.flush()

Why Flushing Matters

Python buffers stdout output for efficiency. Without explicit flushing, your response might sit in the buffer instead of being sent to the client:

python
# BAD - response may be buffered
sys.stdout.write(json_str + "\n")
# Client waits... and waits... and times out!

# GOOD - response sent immediately
sys.stdout.write(json_str + "\n")
sys.stdout.flush()  # Forces the data out to the pipe
Alternative: Unbuffered stdout

You can disable buffering entirely at startup:

python -u server.py

Or set the environment variable: PYTHONUNBUFFERED=1

Sending Error Responses

JSON-RPC errors follow a specific format. The error object must include a code and message:

python
def send_error(msg_id, code, message, data=None):
    """Send a JSON-RPC error response."""
    error = {
        "code": code,
        "message": message
    }

    # Optional: include additional error data
    if data is not None:
        error["data"] = data

    response = {
        "jsonrpc": "2.0",
        "id": msg_id,
        "error": error
    }

    json_str = json.dumps(response)
    sys.stdout.write(json_str + "\n")
    sys.stdout.flush()

# Standard JSON-RPC error codes
PARSE_ERROR = -32700
INVALID_REQUEST = -32600
METHOD_NOT_FOUND = -32601
INVALID_PARAMS = -32602
INTERNAL_ERROR = -32603

# Usage
send_error(1, METHOD_NOT_FOUND, "Unknown method: foo/bar")

Complete Response Module

Here's a reusable module for handling all stdout operations:

python
#!/usr/bin/env python3
"""MCP STDIO Response Writer Module."""

import sys
import json

# JSON-RPC 2.0 Error Codes
class ErrorCode:
    PARSE_ERROR = -32700
    INVALID_REQUEST = -32600
    METHOD_NOT_FOUND = -32601
    INVALID_PARAMS = -32602
    INTERNAL_ERROR = -32603

def write_message(message):
    """
    Write a JSON-RPC message to stdout.

    Args:
        message: Dictionary to serialize and send
    """
    # Serialize to compact JSON (no pretty printing!)
    json_str = json.dumps(message, separators=(',', ':'))

    # Write with newline terminator
    sys.stdout.write(json_str)
    sys.stdout.write("\n")

    # Flush immediately
    sys.stdout.flush()

def send_response(msg_id, result):
    """
    Send a successful JSON-RPC response.

    Args:
        msg_id: The request ID to respond to
        result: The result payload
    """
    write_message({
        "jsonrpc": "2.0",
        "id": msg_id,
        "result": result
    })

def send_error(msg_id, code, message, data=None):
    """
    Send a JSON-RPC error response.

    Args:
        msg_id: The request ID to respond to
        code: Error code (use ErrorCode constants)
        message: Human-readable error message
        data: Optional additional error data
    """
    error = {"code": code, "message": message}
    if data is not None:
        error["data"] = data

    write_message({
        "jsonrpc": "2.0",
        "id": msg_id,
        "error": error
    })

def send_notification(method, params=None):
    """
    Send a JSON-RPC notification (no response expected).

    Args:
        method: The notification method name
        params: Optional parameters
    """
    message = {
        "jsonrpc": "2.0",
        "method": method
    }
    if params is not None:
        message["params"] = params

    write_message(message)

Handling Special Characters

The json.dumps() function handles encoding automatically, but be aware of these considerations:

python
# json.dumps handles special characters correctly
text = 'Line 1\nLine 2\tTabbed'
result = {"content": [{"type": "text", "text": text}]}

# Outputs: {"content":[{"type":"text","text":"Line 1\nLine 2\tTabbed"}]}
# Note: \n becomes \\n in JSON string (escaped)
json_str = json.dumps(result)

# Unicode is also handled
text = "Hello"
result = {"content": [{"type": "text", "text": text}]}
json_str = json.dumps(result, ensure_ascii=False)
# Outputs with Unicode preserved: {"content":[{"type":"text","text":"Hello"}]}
Watch for Library Pollution

Some Python libraries print to stdout unexpectedly. Be especially careful with:

  • Warning messages from libraries
  • Progress bars (tqdm, etc.)
  • Debug output from third-party code
  • Deprecation warnings

Consider redirecting stdout/stderr at the module level for problematic libraries.

Testing Your Output

Here's a simple test to verify your stdout output is correct:

bash
# Send a request and capture the response
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' \
  | python server.py \
  | head -1 \
  | python -m json.tool

# If the output is valid JSON, it will be pretty-printed
# If not, you'll get a parse error