Effective logging strategies in MCP differ significantly from standard web server development. In a traditional REST API environment, developers often view console output or tail log files to trace execution. In an MCP server operating over standard input/output (stdio), the standard output stream is the network connection itself. Writing raw text or debug statements to standard output corrupts the JSON-RPC stream, causing immediate connection failures.
To maintain a stable connection while still obtaining diagnostic information, you must direct logs to specific channels that do not interfere with the protocol transport. This involves utilizing standard error (stderr) for system-level diagnostics and the MCP protocol's native logging capabilities for client-facing information.
The most frequent cause of immediate server termination during development is the accidental use of print statements. When an MCP client initiates a connection, it expects every line received from the server's standard output to be a valid JSON-RPC message.
If your code executes print("Starting server..."), the client receives this raw string, fails to parse it as JSON, and raises a validation error. To prevent this, all non-protocol output must be redirected.
The following diagram illustrates how data streams must be separated in an MCP architecture. The JSON-RPC traffic occupies the standard output channel exclusively, while logs travel via standard error or encapsulated notifications.
Separation of communication channels prevents protocol corruption. Protocol messages and encapsulated log notifications share the standard output, while raw debug text uses standard error.
The standard error stream (stderr) is the primary destination for low-level system logs, stack traces, and unhandled exceptions. MCP clients typically capture this stream and write it to a dedicated log file on the host machine.
In Python, you can configure the root logger to write to stderr automatically. This ensures that even if a library you are using attempts to log information, it does not accidentally write to stdout.
When setting up your logging configuration, ensure the stream handler is explicitly targeted:
import logging
import sys
# Configure logging to write to stderr
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
stream=sys.stderr
)
logger = logging.getLogger("mcp_server")
logger.info("Server initialization complete") # Safe
When analyzing logs from stderr, you will often see the raw internal state of your application. This is where you look when the server fails to start or crashes silently. Since these logs bypass the JSON-RPC parser, they remain visible even if the protocol handshake never completes.
For messages that are intended to be seen by the user or the AI assistant within the client interface, MCP defines a specific notification method: notifications/message.
Unlike raw writes to stderr, these logs are structured JSON objects sent over stdout. They are wrapped in a valid JSON-RPC envelope, so they do not break the protocol. Clients process these notifications and can display them in a debug console or an inspector window.
The structure of a log notification includes a severity level and the data to be logged:
{
"jsonrpc": "2.0",
"method": "notifications/message",
"params": {
"level": "info",
"logger": "database_tool",
"data": "Query executed successfully: SELECT * FROM users"
}
}
Using the MCP SDK, you send these logs through the server context context rather than a standard print function. This method allows the client to filter messages based on severity, similar to how syslog handles log levels.
When the transport is working but the request fails, the server returns a JSON-RPC error response. Analyzing these errors requires understanding the error codes defined in the specification.
A standard error response looks like this:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32602,
"message": "Invalid params",
"data": "Missing required argument: 'query'"
}
}
The code integer tells you the category of the failure. The standard range includes:
When debugging, look at the data field. Well-implemented servers populate this field with specific details, such as which argument was missing or the text of the exception that occurred.
Connection issues often occur at the boundaries of the session, startup and shutdown. Since the client manages the server process, logs related to process spawning are located in the client's own application logs, not the server's output.
If your server does not appear in the client integration list, check the client logs for exit codes.
The following chart visualizes the volume and type of log messages you might encounter during a typical debugging session where a tool fails. Note how transport messages (keep-alives) dominate the traffic, while actual errors are sparse but significant.
In a typical session, transport traffic (blue) is constant. Protocol errors (red) appear as spikes coinciding with application log events (pink), helping pinpoint the exact moment of failure.
MCP servers often handle multiple requests concurrently, especially when using SSE (Server-Sent Events) transport or when multiple tools are invoked in sequence. In these scenarios, a linear log file can be confusing because log entries from different requests are interleaved.
To analyze logs effectively in an asynchronous context, you should track the Request ID. Every JSON-RPC request includes a unique id field. When logging inside a tool handler or resource reader, include this ID in your log message.
If you are using Python's contextvars, you can store the request ID at the entry point of the request and automatically inject it into every log record created during that execution context. This allows you to filter your log file for a specific ID (e.g., grep "req-123" server.log) and reconstruct the entire flow of a single interaction, ignoring the noise from other concurrent operations.
By enforcing strict separation of streams and understanding the error codes returned by the protocol, you transform opaque connection failures into solvable logic problems. The next step is configuring the client application to execute your server with the correct environment settings.
Was this section helpful?
sys.stderr.contextvars for managing context-local state in asynchronous Python applications.© 2026 ApX Machine LearningEngineered with