Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3.8k
Isolate the stdio server's stdin and stdout from handler subprocesses#3117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
fbe98412b0f3ac2e609e1987b6910ba79d43e321f687dda976a6c5448a076d0a45283bcf0ba4f6990fa60f9695fad9c11557271c2885c6aa116a95fFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1863,6 +1863,36 @@ group (spawned with `start_new_session=True`); the `getpgid()` lookup and the | ||
| per-process terminate/kill fallback are gone. The win32 utilities logger is now | ||
| named `mcp.os.win32.utilities` (was `client.stdio.win32`). | ||
| ### `stdio_server` keeps the protocol streams on private descriptors | ||
| While serving, the stdio transport moves the wire to private descriptors and points | ||
| fd 0 at the null device and fd 1 at stderr, restoring both on exit. Subprocesses and | ||
| handler code can no longer read protocol bytes or write into the stream (the | ||
| [#671](https://github.com/modelcontextprotocol/python-sdk/issues/671) fix). Ordinary | ||
| servers have nothing to do, and code that inspects or manipulates fd 0/1 directly | ||
| during a session now sees the diversions, not the wire. | ||
maxisbey marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| One pattern needs migrating: watchdog threads that watch fd 0 to detect a vanished | ||
| client (a POSIX-specific pattern; `select.poll` does not exist on Windows). The null | ||
| device does not behave like the old pipe: it never reports `POLLHUP` or `POLLERR`, | ||
| and it reports readable immediately and permanently (`POLLIN` from `poll()` on Linux, | ||
| plus `POLLOUT` under the default event mask; ready from `select()`; and macOS can | ||
| report `POLLNVAL` for devices). A watcher waiting for `POLLHUP` or `POLLERR` is | ||
| silently disarmed; a watcher that treats any event as "client gone" now fires at | ||
| startup instead of never. Watch the parent process instead: on POSIX, exit | ||
| when `os.getppid()` changes, which happens when the client dies because orphaned | ||
| processes are reparented. That works on both v1 and v2 and does not depend on | ||
| descriptor layout. | ||
| Also new: a second concurrent `stdio_server()` on the process's default streams now | ||
| raises `RuntimeError` instead of silently contending for stdin, a configuration that | ||
| never worked (there is one stdin). | ||
| Also worth knowing: a child process that streams large output to its inherited | ||
| stdout now streams it into the client's stderr channel. Capture output you do not | ||
| want in the client's logs, and be aware that a client which never drains its stderr | ||
| pipe applies back-pressure to the server (true of stderr logging on v1 as well). | ||
| ### WebSocket transport removed | ||
| 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. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| """Windows-specific functionality for stdio client operations.""" | ||
| """Windows-specific functionality for stdio transport operations.""" | ||
| import logging | ||
| import shutil | ||
| @@ -17,6 +17,8 @@ | ||
| # Windows-specific imports for Job Objects | ||
| if sys.platform == "win32": | ||
| import msvcrt | ||
| import pywintypes | ||
| import win32api | ||
| import win32con | ||
| @@ -25,9 +27,30 @@ | ||
| # Type stubs for non-Windows platforms | ||
| win32api = None | ||
| win32con = None | ||
| msvcrt = None | ||
| win32job = None | ||
| pywintypes = None | ||
| def rebind_std_handle_to_fd(fd: int) -> None: | ||
| """Points the Win32 standard-handle slot for fd 0, 1, or 2 at fd's current OS handle. | ||
| os.dup2 updates only the CRT descriptor table; subprocess handle inheritance | ||
| reads the Win32 slot, so it must be repointed too. | ||
| Raises: | ||
| OSError: The slot could not be set. | ||
| """ | ||
| if sys.platform != "win32" or not win32api or not msvcrt or not pywintypes: | ||
| return | ||
| std_ids = {0: win32api.STD_INPUT_HANDLE, 1: win32api.STD_OUTPUT_HANDLE, 2: win32api.STD_ERROR_HANDLE} | ||
| try: | ||
| win32api.SetStdHandle(std_ids[fd], msvcrt.get_osfhandle(fd)) | ||
| except pywintypes.error as exc: | ||
| # Normalized so callers' OSError-based best-effort handling covers it. | ||
| raise OSError(f"SetStdHandle failed for fd {fd}") from exc | ||
maxisbey marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| # How often FallbackProcess polls the underlying Popen for exit. | ||
| _EXIT_POLL_INTERVAL = 0.01 | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.