Fix stderr forwarding for Jupyter and non-fd environments - #3405
Fix stderr forwarding for Jupyter and non-fd environments#3405SushantTusharJoshi wants to merge 1 commit into
Conversation
…descriptor (modelcontextprotocol#156) When running in Jupyter notebooks, sys.stderr is replaced by ipykernel's OutStream which has no OS file descriptor. Passing it directly to subprocess as the child's stderr fails silently -- the server's diagnostics are lost and crashes produce no visible error. Detect this at spawn time by probing errlog.fileno(). When the probe fails (StringIO, ipykernel stream, or any wrapper without a real fd), spawn the child with stderr=PIPE and forward its output into errlog through an async reader task. The direct fd-inheritance path is unchanged for normal file descriptors. The stderr drain runs before _stop_server_process closes the subprocess transport, so no final bytes from a dying server are lost.
|
This PR has been closed automatically. This repo only keeps pull requests open when they come from a maintainer, or from a contributor a maintainer has assigned to the linked issue, and you aren't currently assigned to #156. If a maintainer assigns you to #156, this PR reopens on its own and there's nothing more you need to do here. Assignment is a maintainer call based on capacity; comments that only ask to be assigned don't factor in. What does help is engaging on the issue itself by confirming the repro, explaining why it matters for your use case, or describing the approach you'd take. You're welcome to keep pushing commits here (just avoid force-pushing, since GitHub can't reopen a rewritten branch), but that on its own won't get the PR reviewed or the issue assigned, and realistically most auto-closed PRs stay closed. There's no need to open a new PR either way. CONTRIBUTING.md has the full reasoning, but in short:
Maintainers: reopen, remove |
Summary
Fixes #156.
When running MCP servers from Jupyter notebooks, server stderr output silently vanishes because
ipykernel.iostream.OutStreamlacks a real OS file descriptor. The SDK tries to passerrlogdirectly to the subprocess, which silently fails.How it works
Detection: Added
_lacks_file_descriptor(errlog)helper that callserrlog.fileno(). Real stderr returns a number; Jupyter streams andio.StringIOraise an error.Piping: When
errloghas no file descriptor, spawns the server withstderr=subprocess.PIPEinstead of inheritingerrlog.Forwarding: An async
stderr_reader()task reads chunks from the pipe and writes them toerrlog. Handles pipe close and errlog close gracefully.No-op path: When
errloghas a real file descriptor, nothing changes.Changes
src/mcp/client/stdio.py— fd detection, conditional piping, async stderr reader, drain before closesrc/mcp/os/win32/utilities.py—stderrattribute onFallbackProcess, updated type signaturestests/client/test_stdio.py— 5 new testsTest plan
io.StringIOas errlog (simulates Jupyter)sys.stderrstill takes the direct inheritance path