You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/get-started/real-host.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -165,7 +165,7 @@ Once that command sits and waits, what's left is almost always one of three thin
165
165
166
166
***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.
167
167
***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.** On stdio, stdout *is* the protocol. One stray `print()`and the host reads a corrupt message and 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 default `logging`configuration, whose stderr handler flushes each record; custom handlers must also avoid stdout. **[Logging](../handlers/logging.md)** has the whole story.
169
169
170
170
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.
Copy file name to clipboardExpand all lines: docs/handlers/logging.md
+6-3Lines changed: 6 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -33,8 +33,11 @@ For a **stdio** server this question matters more than usual. The host launched
33
33
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.
34
34
35
35
!!! tip
36
-
Never `print()` in a stdio server. `print` writes to **stdout**, and stdout *is* the wire: one stray
37
-
line and the client is trying to parse it as JSON-RPC.
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.
38
41
39
42
`logger.debug("got here")` is the same one line of effort and goes to the right place.
40
43
@@ -72,7 +75,7 @@ went to standard error: the terminal, not the wire.
72
75
* The MCP protocol's logging capability is deprecated by the 2026-07-28 spec and not replaced. Don't build on it.
73
76
*`logger = logging.getLogger(__name__)` at module level, `logger.info(...)` in the tool. That's the whole pattern.
74
77
* Log output never reaches the model. Only the value you `return` does.
75
-
* Standard error is yours; stdout belongs to the protocol. Never `print()`in a stdio server.
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.
76
79
*`MCPServer(..., log_level="DEBUG")` sets the level, and a logging configuration you made first is left alone.
77
80
78
81
Telling connected clients that something on your server changed (the tool list, a resource) is **[Subscriptions](subscriptions.md)**.
servers have nothing to do, and code that inspects or manipulates fd 0/1 directly
1873
+
during a session now sees the diversions, not the wire.
1874
+
1875
+
One pattern needs migrating: watchdog threads that watch fd 0 to detect a vanished
1876
+
client (a POSIX-specific pattern; `select.poll` does not exist on Windows). The null
1877
+
device does not behave like the old pipe: it never reports `POLLHUP` or `POLLERR`,
1878
+
and it reports readable immediately and permanently (`POLLIN` from `poll()` on Linux,
1879
+
plus `POLLOUT` under the default event mask; ready from `select()`; and macOS can
1880
+
report `POLLNVAL` for devices). A watcher waiting for `POLLHUP` or `POLLERR` is
1881
+
silently disarmed; a watcher that treats any event as "client gone" now fires at
1882
+
startup instead of never. Watch the parent process instead: on POSIX, exit
1883
+
when `os.getppid()` changes, which happens when the client dies because orphaned
1884
+
processes are reparented. That works on both v1 and v2 and does not depend on
1885
+
descriptor layout.
1886
+
1887
+
Also new: a second concurrent `stdio_server()` on the process's default streams now
1888
+
raises `RuntimeError` instead of silently contending for stdin, a configuration that
1889
+
never worked (there is one stdin).
1890
+
1891
+
Also worth knowing: a child process that streams large output to its inherited
1892
+
stdout now streams it into the client's stderr channel. Capture output you do not
1893
+
want in the client's logs, and be aware that a client which never drains its stderr
1894
+
pipe applies back-pressure to the server (true of stderr logging on v1 as well).
1895
+
1866
1896
### WebSocket transport removed
1867
1897
1868
1898
The WebSocket transport has been removed: `mcp.client.websocket.websocket_client`, `mcp.server.websocket.websocket_server`, and the `ws` optional dependency extra (`mcp[ws]`) no longer exist. WebSocket was never part of the MCP specification. Use the streamable HTTP transport instead (`mcp.client.streamable_http.streamable_http_client` on the client, `streamable_http_app()` on the server), which supports bidirectional communication with server-to-client streaming over standard HTTP.
Copy file name to clipboardExpand all lines: docs/run/index.md
+1-25Lines changed: 1 addition & 25 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -39,31 +39,7 @@ python server.py
39
39
40
40
Nothing prints, and it doesn't return. It is waiting on stdin for a host to speak first.
41
41
42
-
That also means stdout **is the wire**. A stray `print()` corrupts the stream; the `logging` module writes to stderr and is the right tool. That story is in **[Logging](../handlers/logging.md)**.
43
-
44
-
On Windows, the same rule applies to child processes your tools start. A child
45
-
that inherits the stdio server's stdin can block behind the server's protocol
46
-
reader. If your tool starts a subprocess and you do not intend to feed it input,
47
-
redirect the child's stdin:
48
-
49
-
```python
50
-
import asyncio
51
-
import subprocess
52
-
import sys
53
-
54
-
55
-
asyncdefrun_script() -> tuple[bytes, bytes]:
56
-
process =await asyncio.create_subprocess_exec(
57
-
sys.executable,
58
-
"script.py",
59
-
stdin=subprocess.DEVNULL,
60
-
stdout=subprocess.PIPE,
61
-
stderr=subprocess.PIPE,
62
-
)
63
-
returnawait process.communicate()
64
-
```
65
-
66
-
The matching troubleshooting entry is **[My stdio tool hangs when it starts a subprocess on Windows](../troubleshooting.md#my-stdio-tool-hangs-when-it-starts-a-subprocess-on-windows)**.
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)**.
Copy file name to clipboardExpand all lines: docs/troubleshooting.md
+1-36Lines changed: 1 addition & 36 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -137,45 +137,10 @@ There is no error string for this, which is exactly why it is hard to search. Th
137
137
***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.
138
138
***Did two tools share a name?** Then one of them is gone. Look for `Tool already exists:` in the server log.
139
139
***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`?**On a stdio transport, stdout *is* the protocol: one stray `print()` and the host drops 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)**.
141
141
142
142
An "invalid" tool name is *not* on that list: a non-conforming name logs a warning but the tool is registered and listed anyway.
143
143
144
-
## My stdio tool hangs when it starts a subprocess on Windows
145
-
146
-
Your server is running over `stdio`, and a tool starts another process with
147
-
`asyncio.create_subprocess_exec`, `asyncio.create_subprocess_shell`, or
148
-
`subprocess.Popen`. The tool call never returns on Windows, while the same code
149
-
works over an HTTP transport.
150
-
151
-
The child inherited the server's stdin. In a stdio server, stdin is the protocol
152
-
pipe and the server is already waiting on it for the next JSON-RPC message. A
153
-
Python child process on Windows can block during startup when it inherits that
154
-
same pipe.
155
-
156
-
If you do not intend to send input to the child, redirect its stdin:
157
-
158
-
```python
159
-
import asyncio
160
-
import subprocess
161
-
import sys
162
-
163
-
164
-
asyncdefrun_script() -> tuple[bytes, bytes]:
165
-
process =await asyncio.create_subprocess_exec(
166
-
sys.executable,
167
-
"script.py",
168
-
stdin=subprocess.DEVNULL,
169
-
stdout=subprocess.PIPE,
170
-
stderr=subprocess.PIPE,
171
-
)
172
-
returnawait process.communicate()
173
-
```
174
-
175
-
Use the same idea with `subprocess.Popen(..., stdin=subprocess.DEVNULL)`. Also
176
-
capture or redirect the child's stdout. The stdio server's stdout is the MCP
177
-
wire, so a child that writes there can corrupt the connection.
178
-
179
144
## `MCPError: Server returned an error response`
180
145
181
146
The server refused the HTTP request outright, with a body that is not JSON-RPC, so the python `Client` has nothing better to show you than this stand-in.
0 commit comments