Logging to stderr
Learn to debug your MCP server without breaking the protocol.
stderr: Your Debug Channel
Since stdout is reserved exclusively for JSON-RPC messages, you need another way to output debug information. That's where stderr comes in.
┌─────────────────────────────────────────────────────────────┐
│ MCP Server Process │
├─────────────────────────────────────────────────────────────┤
│ │
│ stdout ─────► JSON-RPC messages ─────► MCP Client (parsed) │
│ │
│ stderr ─────► Debug logs ─────► Terminal/Log file (ignored)│
│ │
└─────────────────────────────────────────────────────────────┘
The MCP client reads stdout for protocol messages but ignores stderr. This makes stderr perfect for logging, debugging, and diagnostics.
Basic stderr Logging
The simplest approach is direct writes to sys.stderr:
import sys
def log(message):
"""Write a debug message to stderr."""
sys.stderr.write(f"[DEBUG] {message}\n")
sys.stderr.flush()
# Usage
log("Server starting...")
log(f"Received method: {method}")
log(f"Tool call result: {result}")
Like stdout, stderr can be buffered. Always call flush() to ensure
your debug messages appear immediately. This is especially important when
debugging crashes or hangs.
Using Python's Logging Module
For production servers, Python's logging module provides more
control over log levels, formatting, and output:
import sys
import logging
# Configure logging to stderr
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.StreamHandler(sys.stderr)
]
)
logger = logging.getLogger("mcp-server")
# Usage
logger.info("Server starting...")
logger.debug(f"Received method: {method}")
logger.warning("Client sent deprecated parameter")
logger.error(f"Tool execution failed: {error}")
Log Levels for MCP Servers
Use appropriate log levels to filter output based on verbosity needs:
| Level | Use For |
|---|---|
DEBUG |
Detailed message content, internal state |
INFO |
Server lifecycle events, method calls |
WARNING |
Deprecated usage, recoverable issues |
ERROR |
Failures that affect operation |
Structured Logging
For complex debugging, consider structured logging that outputs JSON to stderr:
import sys
import json
from datetime import datetime
def log_structured(level, message, **extra):
"""Write a structured JSON log entry to stderr."""
entry = {
"timestamp": datetime.utcnow().isoformat(),
"level": level,
"message": message,
**extra
}
sys.stderr.write(json.dumps(entry) + "\n")
sys.stderr.flush()
# Usage
log_structured("INFO", "Tool called",
tool="echo",
arguments={"message": "hello"},
duration_ms=5
)
# Output:
# {"timestamp":"2024-01-15T10:30:45.123456","level":"INFO","message":"Tool called","tool":"echo","arguments":{"message":"hello"},"duration_ms":5}
Complete Logging Module
Here's a production-ready logging setup for MCP servers:
#!/usr/bin/env python3
"""MCP Server Logging Module."""
import sys
import logging
import os
def setup_logging(name="mcp-server"):
"""
Configure logging for an MCP STDIO server.
Args:
name: Logger name (appears in log output)
Returns:
Configured logger instance
"""
# Get log level from environment (default: INFO)
level_name = os.environ.get("MCP_LOG_LEVEL", "INFO").upper()
level = getattr(logging, level_name, logging.INFO)
# Create logger
logger = logging.getLogger(name)
logger.setLevel(level)
# Clear any existing handlers
logger.handlers.clear()
# Create stderr handler
handler = logging.StreamHandler(sys.stderr)
handler.setLevel(level)
# Create formatter
formatter = logging.Formatter(
'%(asctime)s [%(name)s] %(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
handler.setFormatter(formatter)
# Add handler to logger
logger.addHandler(handler)
return logger
# Usage in your server:
# from logging_module import setup_logging
# logger = setup_logging("my-mcp-server")
# logger.info("Starting server...")
# logger.debug(f"Request: {request}")
Redirecting print() to stderr
If you have existing code that uses print(), you can redirect
it to stderr to prevent protocol corruption:
import sys
# Option 1: Use print's file parameter
print("Debug message", file=sys.stderr)
# Option 2: Redirect all print() calls to stderr
# Do this BEFORE any other imports that might print
original_stdout = sys.stdout
sys.stdout = sys.stderr
# Now print() goes to stderr
print("This won't break the protocol!")
# Restore stdout for JSON-RPC output
def write_response(response):
original_stdout.write(response + "\n")
original_stdout.flush()
Some third-party libraries print warnings or progress to stdout. Either:
- Configure the library to use stderr
- Suppress the library's output entirely
- Redirect stdout before importing the library
Viewing stderr in Claude Desktop
When running your server with Claude Desktop, stderr output appears in the application logs. You can view these for debugging:
# macOS - View Claude Desktop logs
tail -f ~/Library/Logs/Claude/mcp*.log
# Linux - View Claude Desktop logs
tail -f ~/.config/Claude/logs/mcp*.log
# Windows - View Claude Desktop logs
Get-Content "$env:APPDATA\Claude\logs\mcp*.log" -Wait
Debugging Tips
- Log request IDs: Include the message ID in logs to trace request/response pairs
- Log timestamps: Essential for understanding timing issues
- Use environment variables: Control log verbosity without code changes
- Log on entry/exit: Log when functions start and complete
- Include context: Log relevant data like method names and parameters