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
Refuse abnormal processes instead of repairing them in stdio isolation
The stdin/stdout claim now engages only in a normal process: the sys
stream backed by its real descriptor and fds 0-2 all open, which makes
it impossible for the private wire duplicates to land in the standard
range. Anything else - replaced streams, an incomplete descriptor
table, a failed dup - is served in place exactly as v1 was, and a
second concurrent stdio_server() raises RuntimeError instead of
contending for the streams.
This replaces the previous hardening (the dup-above-standard-range
loop, stderr-merge detection, and nested transports serving into the
diversion) with guards, and removes the migration entry: no working
code changes behavior, so there is nothing to migrate. Comments and
docstrings trimmed throughout the diff.
Copy file name to clipboardExpand all lines: docs/migration.md
-35Lines changed: 0 additions & 35 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1855,41 +1855,6 @@ group (spawned with `start_new_session=True`); the `getpgid()` lookup and the
1855
1855
per-process terminate/kill fallback are gone. The win32 utilities logger is now
1856
1856
named `mcp.os.win32.utilities` (was `client.stdio.win32`).
1857
1857
1858
-
### `stdio_server` keeps the protocol streams on private descriptors
1859
-
1860
-
While serving on the process's real stdin and stdout, the stdio server transport now
1861
-
duplicates each protocol pipe to a private descriptor and points the standard
1862
-
descriptors — with their Windows standard handles — away from the wire, restoring
1863
-
both when the transport exits: fd 0 reads the null device, and fd 1 writes to stderr
1864
-
(the null device if stderr is unusable). Subprocesses started by handler code
1865
-
therefore inherit the diversions instead of the protocol pipes, and a stray
1866
-
`print()` lands on stderr rather than the wire. (The claim is best-effort: in the
1867
-
rare process whose descriptor table cannot be rearranged, the transport serves in
1868
-
place, exactly as v1 did.)
1869
-
1870
-
In v1 a child inheriting the pipes could consume protocol bytes or corrupt the
1871
-
outgoing stream with its own output, and on Windows it hung inside interpreter
1872
-
startup — behind the transport's pending read on the shared pipe
1873
-
([CPython gh-78961](https://github.com/python/cpython/issues/78961)) — until
1874
-
the next request arrived: the
1875
-
[#671](https://github.com/modelcontextprotocol/python-sdk/issues/671) hang, for
1876
-
which passing `stdin=subprocess.DEVNULL` on every spawn (and capturing the child's
1877
-
stdout) was the required workaround. On v2 the workaround is no longer needed, for
1878
-
any spawn API, redirected or not.
1879
-
1880
-
To migrate: nothing, unless handler code used the standard streams directly during a
1881
-
stdio session. Reading `sys.stdin` (or calling `input()`) now sees end-of-file
1882
-
instead of racing the transport for protocol bytes; there was never a meaningful
1883
-
value to read there. `print()` and other `sys.stdout` writes reach stderr instead of
1884
-
corrupting the wire — code that deliberately wrote protocol frames to `sys.stdout`
1885
-
must send them through the transport's write stream instead. A child that streams
1886
-
a lot of output to its inherited stdout now streams it into the client's stderr
1887
-
channel; capture output you don't want in the client's logs. Likewise, anything
1888
-
that read ahead from `sys.stdin` before the server started keeps those bytes; they
1889
-
no longer reach the transport (they never reliably did). Passing an explicit `stdin=`/`stdout=` stream to
1890
-
`stdio_server(...)` skips the descriptor changes for that stream, as does any
1891
-
environment where the sys stream is not backed by the process's real descriptor.
1892
-
1893
1858
### WebSocket transport removed
1894
1859
1895
1860
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/troubleshooting.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
@@ -137,7 +137,7 @@ 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` before the server started serving?** While serving, the SDK diverts stray stdout to stderr (best-effort: an environment that replaces `sys.stdout`, or merges stderr into stdout on Windows, 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` 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)**.
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.
0 commit comments