Skip to content

Commit cc9016c

Browse files
committed
Scope the stray-output docs to flushed writes
The diversion catches output that is actually flushed to stdout while serving. A print() in a block-buffered process usually stays unflushed in sys.stdout's user-space buffer, and the interpreter's exit flush drains it onto the restored protocol stream, so the docs now say so instead of implying every stray print is diverted. The logging advice is unchanged: its handler flushes each record. No-Verification-Needed: docs-only wording change
1 parent 84f033d commit cc9016c

4 files changed

Lines changed: 9 additions & 7 deletions

File tree

docs/get-started/real-host.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ Once that command sits and waits, what's left is almost always one of three thin
165165

166166
* **A relative path.** The host launches your server from *its* working directory, not the one you registered from. `server.py` where `/absolute/path/to/server.py` is needed is the single most common failure. If the host can't find `uv` either, that path has to be absolute too.
167167
* **The host is still running its old config.** Hosts read their config at launch. Claude Desktop in particular has to be *fully quit* (not just its window closed) and reopened before an edit to `claude_desktop_config.json` takes effect.
168-
* **Something reached stdout before serving began.** On stdio, stdout *is* the protocol. The SDK diverts stray output to stderr while serving, but anything that reaches stdout before then (a wrapper script echoing, an import-time `print()` in an unbuffered process) hands the host a corrupt message and it drops the connection. Log with the `logging` module, which writes to stderr. **[Logging](../handlers/logging.md)** has the whole story.
168+
* **Something reached stdout outside the diverted window.** On stdio, stdout *is* the protocol. The SDK diverts flushed stray output to stderr while serving, but output flushed to stdout before then (a wrapper script echoing, an import-time `print()` in an unbuffered process), or a buffered `print()` drained at interpreter exit, hands the host a corrupt message and it drops the connection. Log with the `logging` module, which flushes each record to stderr. **[Logging](../handlers/logging.md)** has the whole story.
169169

170170
Claude Desktop keeps a log per server: `mcp-server-<NAME>.log` is your server's stderr, next to `mcp.log` for connections, under `~/Library/Logs/Claude` on macOS and `%APPDATA%\Claude\logs` on Windows.
171171

docs/handlers/logging.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@ For a **stdio** server this question matters more than usual. The host launched
3333
The standard library already does the right thing: log output goes to `sys.stderr` by default. Your `logger.info(...)` lines land in the terminal (or wherever the host collects the subprocess's stderr), and the protocol stream stays clean.
3434

3535
!!! tip
36-
Don't `print()` in a stdio server. `print` writes to **stdout**, and stdout belongs to the protocol:
37-
while serving, the SDK diverts stray stdout to stderr so it can't corrupt the wire, but that leaves
38-
your line interleaved raw among the log output, with no level, no logger name, and no way to filter it.
36+
Don't `print()` in a stdio server. `print` writes to **stdout**, and stdout belongs to the protocol.
37+
While serving, the SDK diverts stdout that is actually *flushed* to stderr, so it can't corrupt the
38+
wire, but a `print()` in a block-buffered process usually sits unflushed in `sys.stdout`'s buffer
39+
until the interpreter drains it at exit, straight onto the protocol stream. Even when it is diverted,
40+
the line lands raw among the log output, with no level, no logger name, and no way to filter it.
3941

4042
`logger.debug("got here")` is the same one line of effort and goes to the right place.
4143

@@ -73,7 +75,7 @@ went to standard error: the terminal, not the wire.
7375
* The MCP protocol's logging capability is deprecated by the 2026-07-28 spec and not replaced. Don't build on it.
7476
* `logger = logging.getLogger(__name__)` at module level, `logger.info(...)` in the tool. That's the whole pattern.
7577
* Log output never reaches the model. Only the value you `return` does.
76-
* Standard error is yours; stdout belongs to the protocol. The SDK diverts a stray `print()` to stderr while serving, but it arrives unlabeled; use `logging`.
78+
* Standard error is yours; stdout belongs to the protocol. The SDK diverts flushed stray stdout to stderr while serving, but an unflushed `print()` can still drain onto the wire at exit, and diverted lines arrive unlabeled; use `logging`, whose handler flushes every record.
7779
* `MCPServer(..., log_level="DEBUG")` sets the level, and a logging configuration you made first is left alone.
7880

7981
Telling connected clients that something on your server changed (the tool list, a resource) is **[Subscriptions](subscriptions.md)**.

docs/run/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ python server.py
3939

4040
Nothing prints, and it doesn't return. It is waiting on stdin for a host to speak first.
4141

42-
That also means stdout **is the wire**. While serving, the SDK moves the wire to a private descriptor and diverts stray output (a `print()`, or a subprocess writing to its inherited stdout) to stderr, where it can't corrupt the stream. Output that reaches stdout *before* serving begins (a wrapper script echoing, an unbuffered import-time print) still lands on the wire. For output you actually want, the `logging` module is the right tool. That story is in **[Logging](../handlers/logging.md)**.
42+
That also means stdout **is the wire**. While serving, the SDK moves the wire to a private descriptor and diverts output that is *flushed* to stdout (a subprocess writing to its inherited stdout, a flushed `print()`) to stderr, where it can't corrupt the stream. Output flushed to stdout *before* serving begins (a wrapper script echoing, an unbuffered import-time print) still lands on the wire, and so does a `print()` that stays buffered until the interpreter drains it at exit. For output you actually want, the `logging` module is the right tool: its handler flushes each record to stderr as it happens. That story is in **[Logging](../handlers/logging.md)**.
4343

4444
### Try it
4545

docs/troubleshooting.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ There is no error string for this, which is exactly why it is hard to search. Th
137137
* **Is the tool on the `mcp` the host is running?** A second `MCPServer(...)` in another module is a different, empty server. Check which object the host's command actually imports.
138138
* **Did two tools share a name?** Then one of them is gone. Look for `Tool already exists:` in the server log.
139139
* **Is the host's list stale?** Adding a tool after startup only reaches clients that handle `notifications/tools/list_changed`. Restarting the host is the blunt fix.
140-
* **Did something write to `stdout` before the server started serving?** While serving, the SDK diverts stray stdout to stderr (best-effort: an environment that replaces the standard streams is served as-is), but output flushed to stdout earlier (a wrapper script echoing, an import-time `print()` in an unbuffered process) lands on the protocol stream, and one junk line can make the host drop the connection, which some hosts render as a server with nothing in it. Log with the `logging` module instead. The rest of the host-side checklist is on **[Connect to a real host](get-started/real-host.md)**.
140+
* **Did something write to `stdout` outside the diverted window?** While serving, the SDK diverts *flushed* stray stdout to stderr (best-effort: an environment that replaces the standard streams is served as-is), but output flushed to stdout earlier (a wrapper script echoing, an import-time `print()` in an unbuffered process) or a buffered `print()` drained at interpreter exit lands on the protocol stream, and one junk line can make the host drop the connection, which some hosts render as a server with nothing in it. Log with the `logging` module instead. The rest of the host-side checklist is on **[Connect to a real host](get-started/real-host.md)**.
141141

142142
An "invalid" tool name is *not* on that list: a non-conforming name logs a warning but the tool is registered and listed anyway.
143143

0 commit comments

Comments
 (0)