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.
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:
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:
# 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
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:
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:
#!/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:
# 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"}]}
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:
# 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