Title: fix: use non-blocking stdout writes in stdio_server to prevent event loop deadlock by retospect · Pull Request #2070 · modelcontextprotocol/python-sdk · GitHub
Open Graph Title: fix: use non-blocking stdout writes in stdio_server to prevent event loop deadlock by retospect · Pull Request #2070 · modelcontextprotocol/python-sdk
X Title: fix: use non-blocking stdout writes in stdio_server to prevent event loop deadlock by retospect · Pull Request #2070 · modelcontextprotocol/python-sdk
Description: fix: use non-blocking stdout writes in stdio_server to prevent event loop deadlock Problem When an MCP server tool returns a response larger than the OS pipe buffer (64 KB on macOS), stdout_writer blocks the entire event loop on the await stdout.write() call. This happens because anyio.wrap_file delegates to a synchronous write() on a blocking fd — if the pipe buffer is full (client hasn't read yet), the write syscall blocks, and no other async tasks can run. In practice this manifests as: Server hangs indefinitely after returning a large tool result (e.g. list_papers with 500+ entries returning ~74 KB of JSON) The hang is silent — no error, no timeout, no log entry The server process stays alive but is completely unresponsive Only affects macOS (64 KB pipe buffer) in practice; Linux has a 1 MB default Reported in #547. Root cause anyio.wrap_file(TextIOWrapper(sys.stdout.buffer)) wraps the synchronous file in a thread worker, but the underlying write() still blocks when the kernel pipe buffer is full. Since MCP stdio transport is a single pipe between server and client, the client must read before the server can write more — but the server can't process the client's next read request because the event loop is blocked on the write. Fix For the default stdout path (no custom override): Set the stdout fd to non-blocking (os.set_blocking(fd, False)) Write in small chunks (4 KB) directly via os.write(), catching BlockingIOError (EAGAIN) and yielding to the event loop with await anyio.sleep(0.005) before retrying This ensures the event loop never blocks on a pipe-full condition. The 4 KB chunk size is well below the 64 KB macOS pipe buffer, so most writes complete in a single syscall. When the buffer fills, the coroutine yields and retries after the client drains some data. Custom stdout overrides (the stdout parameter) use the original anyio.wrap_file path unchanged. Testing Tested in production with an MCP server managing 500+ research papers, where list_papers regularly returns 60-80 KB responses. Before this fix, the server would hang ~1 in 3 calls. After the fix, zero hangs over weeks of use. Closes #547
Open Graph Description: fix: use non-blocking stdout writes in stdio_server to prevent event loop deadlock Problem When an MCP server tool returns a response larger than the OS pipe buffer (64 KB on macOS), stdout_writer ...
X Description: fix: use non-blocking stdout writes in stdio_server to prevent event loop deadlock Problem When an MCP server tool returns a response larger than the OS pipe buffer (64 KB on macOS), stdout_writer ...
Opengraph URL: https://github.com/modelcontextprotocol/python-sdk/pull/2070
X: @github
Domain: github.com
| route-pattern | /:user_id/:repository/pull/:id/files(.:format) |
| route-controller | pull_requests |
| route-action | files |
| fetch-nonce | v2:398d6061-fef6-3ad1-afe0-ce7a52a0e80f |
| current-catalog-service-hash | ae870bc5e265a340912cde392f23dad3671a0a881730ffdadd82f2f57d81641b |
| request-id | DBC6:2B9CDD:3CC803:4F7E52:6A5BF86F |
| html-safe-nonce | a786fb76f3faa47b5fde71f168033f43723008436e846163f893384a221726ac |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJEQkM2OjJCOUNERDozQ0M4MDM6NEY3RTUyOjZBNUJGODZGIiwidmlzaXRvcl9pZCI6IjI2ODQ0MDgzMTMzMDAxMjE3MTEiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | cc378ccd35a52c281bc489426c33e28093e91adf1862fffae39bac1fd07a546f |
| hovercard-subject-tag | pull_request:3291244124 |
| github-keyboard-shortcuts | repository,pull-request-list,pull-request-conversation,pull-request-files-changed,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/modelcontextprotocol/python-sdk/pull/2070/files |
| twitter:image | https://avatars.githubusercontent.com/u/430279?s=400&v=4 |
| twitter:card | summary_large_image |
| og:image | https://avatars.githubusercontent.com/u/430279?s=400&v=4 |
| og:image:alt | fix: use non-blocking stdout writes in stdio_server to prevent event loop deadlock Problem When an MCP server tool returns a response larger than the OS pipe buffer (64 KB on macOS), stdout_writer ... |
| og:site_name | GitHub |
| og:type | object |
| hostname | github.com |
| expected-hostname | github.com |
| None | 5290d7e14309ad1e76106a9c4237bd1041517e83ea182c8ab756752cb0c6940b |
| turbo-cache-control | no-preview |
| diff-view | unified |
| go-import | github.com/modelcontextprotocol/python-sdk git https://github.com/modelcontextprotocol/python-sdk.git |
| octolytics-dimension-user_id | 182288589 |
| octolytics-dimension-user_login | modelcontextprotocol |
| octolytics-dimension-repository_id | 862584018 |
| octolytics-dimension-repository_nwo | modelcontextprotocol/python-sdk |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 862584018 |
| octolytics-dimension-repository_network_root_nwo | modelcontextprotocol/python-sdk |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | true |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 9c975978430e9ad293956f2bbdaf153b1bd84a99 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width