In MCP stdio, what is stderr for and why is it not an error signal?
answer
- the third stream has a job
- MAY capture, forward, or discard
- its name misleads supervisors
- failure is in-band instead
- the deprecated feature's migration target
basics
~20 sOn MCP's stdio transport stderr is the server's logging channel. A client may capture, forward or discard it, but should not read output on stderr as an indication that the request or the server failed.
solid answer
~50 sKeeping logs off stdout is what preserves the newline-delimited framing, so the transport gives the server `stderr` as its diagnostic channel. Servers may write **UTF-8 log strings** there; the client **MAY** capture, forward to its own logs, or simply discard them, and it **SHOULD NOT** treat the presence of stderr output as an error signal. Chatty servers are normal: a framework startup line, a dependency warning, a debug trace all land on stderr and mean nothing about protocol health. Failure is signalled inside the protocol instead — a JSON-RPC `error` object, or `isError` on a tool result — or by the process exiting non-zero. This matters more in revision 2026-07-28 because the logging feature (`logging/setLevel` and the `notifications/message` stream) is **deprecated**, and the stated migration for stdio servers is exactly this: log to `stderr`, and use OpenTelemetry for real observability.
go deeper
Know the split: stdout carries only MCP messages, stderr carries the server's logs, and seeing text on stderr does not mean anything went wrong.
Explain the client's latitude — it MAY capture, forward or discard stderr and SHOULD NOT treat it as an error signal — and name where failures really appear: a JSON-RPC error, isError, or a non-zero exit.
Bring the operational detail: draining the stderr pipe so the server never blocks on a log write, keeping secrets out of lines that land in host logs, and not wiring a health check to stderr activity.
Take a position on observability. Protocol logging is deprecated as of 2026-07-28, so decide organisation-wide that stdio servers log to stderr and export real telemetry through OpenTelemetry rather than through MCP.
## Three streams, three jobs A stdio MCP server inherits the usual trio. **stdin** carries requests and notifications from the client. **stdout** carries responses and notifications back and is reserved exclusively for valid MCP messages. That exclusivity would leave a server with nowhere to say anything human-readable, so the transport hands it the third stream: **stderr is for logging**. The server MAY write UTF-8 strings there. It is free-form text, not JSON-RPC — nothing on stderr is parsed as a protocol message. ## What the client does with it The client's obligations are deliberately loose. It **MAY** capture stderr, forward it into the host application's own log, surface it in a developer console, or discard it entirely. What it **SHOULD NOT** do is interpret output on stderr as an error signal. That rule exists because the assumption is so tempting and so wrong. The stream is named "standard error", so a naive supervisor treats any byte on it as a failure: it marks the server unhealthy, tears down the subprocess, or turns a successful `tools/call` into a red error in the UI. In reality almost every real server writes to stderr constantly and correctly: - logging frameworks default their console appender to stderr; - runtimes print deprecation and experimental-feature warnings there; - dependency loaders, native-library probes and TLS stacks emit notices; - the server's own debug tracing goes there by design. A server that logs one line per request is healthy, not broken. ## Where failure is actually signalled Errors are in-band, not on stderr: - **Protocol-level failure** is a JSON-RPC `error` object on stdout, with a code such as `-32602` for invalid params or `-32601` for an unknown method. - **Tool-execution failure** that the model should see and possibly recover from is a normal result carrying `isError`, so the failure text reaches the model instead of the client's exception handler. - **Process-level failure** is the subprocess exiting, and its exit code. A crashed server is detected by the process ending or by stdout closing, not by stderr chatter. A useful mental model: stderr is out-of-band telemetry for humans, and the protocol carries everything a machine is supposed to act on. ## The 2026-07-28 change that makes this the primary path Earlier revisions offered a protocol-level logging feature: the client called `logging/setLevel` and the server pushed `notifications/message` entries carrying structured log records. In revision 2026-07-28 that whole feature is **deprecated** (it stays in the spec for at least twelve months, with the earliest removal being the first revision released on or after 2027-07-28, and new implementations SHOULD NOT adopt it). The migration guidance is explicit and splits in two: **on stdio, log to stderr**; for real observability, use OpenTelemetry rather than the protocol. The residue of the old feature in 2026-07-28 is a per-request `_meta` key, `io.modelcontextprotocol/logLevel`. Without it a server **MUST NOT** emit `notifications/message` at all — another reason a new stdio server should simply log to stderr and not build on the deprecated path. ## Operational consequences **Do not let stderr fill the pipe.** If the client spawns the subprocess with a pipe for stderr and then never reads it, the pipe buffer fills and the server blocks on its next log write — which looks exactly like a hung request. Either read the stream continuously, or redirect it to a file or to the client's own stderr. **Secrets leak here.** stderr often ends up in host logs the user never asked about. Log identifiers, not argument values, tokens or file contents. **Interleaving is not a problem.** stderr and stdout are separate streams; log output can never corrupt the message framing. That is the whole point of the split, and it is why "just print it" is a defect on stdout and correct on stderr. **The remote binding is different.** Over Streamable HTTP there is no stderr; a remote server logs to its own infrastructure. The stderr rule is specific to the subprocess binding.
- If stderr is not the failure signal, how does a client learn that a tool call failed?In-band. A protocol-level problem comes back as a JSON-RPC `error` object with a code such as `-32602` for invalid params. An execution failure the model should see comes back as a normal result with `isError` set, so the failure text is fed to the model rather than thrown at the client. A dead server is detected by the process exiting or stdout closing.
- What happens if the client pipes stderr but never reads it?The pipe buffer fills and the server blocks on its next write to stderr. From the client's side that looks like a hung request with no response, and it is easy to misdiagnose as a protocol bug. Either drain the stream continuously or redirect it to a file or to the client's own stderr.
- Does the same advice apply to a remote server over Streamable HTTP?No. There is no stderr across an HTTP connection; a remote server logs to its own infrastructure. The stderr rule belongs to the subprocess binding specifically. The part that carries over from 2026-07-28 is the deprecation of protocol logging: use OpenTelemetry for observability rather than pushing log records over MCP.
saying these in an interview costs you the question
- Treats any stderr output as a failed request
- Says the server should log to stdout with a prefix
- Claims stderr output can corrupt message framing
- Thinks logging/setLevel is still the recommended path
- Logs tool arguments and tokens into stderr