Repository navigation
support logging to stderr in Jupyter Notebook Environments. #156
Description
Activity
@dsp-ant if that sounds reasonable to you happy to put together a PR
actually another simpler solution is also just to explicitly set stderr to None:
process = await anyio.open_process( [server.command, *server.args], env=server.env if server.env is not None else get_default_environment(), stderr=None, )
@grll Thanks for reporting this. I am curious as to 'why' jupyter notebooks have problems with capturing sys.stderr? I am open for fixes assuming they are not breaking backwards compatibility.
Notable, jupyter integration should probably rely on Logging to correctly capture and display logs. Now sadly, no server actually correctly implements this and we likely need better integration with standard logging facilities in Python.
- addedready for workEnough information for someone to start working onEnough information for someone to start working on
on Oct 7, 2025 - addedgood first issueGood for newcomersGood for newcomersand removed
on Nov 12, 2025 - added a commit that references this issue
on Nov 30, 2025 Hi! I'd like to work on this issue if it's still available. Improved logging integration in Jupyter environments (and generally) sounds very useful. I can investigate and submit a PR to allow better logging configuration, including stderr support as suggested.
- added a commit that references this issue
on Feb 8, 2026 - added a commit that references this issue
on Feb 9, 2026 Hi — I opened PR #1707 to address this, but it's now stale due to the
stdiomodule restructuring (package → single file). I'd like to open a fresh, simpler PR if contributions are still welcome here.Proposed approach: Instead of the
StderrCaptureclass from my original PR, I'd take a minimal approach:- Pipe
stderrfrom the subprocess (instead of inheriting or suppressing it) - Spawn an async task to read stderr lines and forward them to
errlog(the existing stderr stream parameter) - This preserves stderr output in Jupyter/notebook environments where
sys.stderrmay not behave like a real file descriptor
This keeps the change small and focused on the core problem: stderr is lost in environments like Jupyter where
subprocess.PIPE+ explicit forwarding is needed.Would love to hear if this direction works for you before I start the new PR. Thanks!
- Pipe
I'd like to work on this. The issue is that passing to doesn't surface server stderr in Jupyter because Jupyter replaces sys.stderr with an ipykernel stream that isn't inherited by the child process. I'll change the client to leave stderr attached to the process and forward into the parent (or Jupyter's display hook) with an async reader. I'll include tests for both normal and Jupyter-like environments.
Stepping back from this one to take on a higher-impact client reliability issue instead. Thanks!
I hit this outside Jupyter too, which may be worth adding to the repro:
io.StringIOaserrlogfails the same way. Anything without a real file descriptor does, becausestdio_clientpasseserrlogstraight toanyio.open_process(stderr=...)and the child needs a descriptor to inherit. Under ipykernel,fileno()raisesio.UnsupportedOperation. It bit me while capturing server logs in a test, and the failure mode is bad in the way @grll describes: when a server crashes on startup you get a hang and no output.The approach I'd take is the minimal one @dgenio outlined above: check whether
errloghas a usable descriptor, inherit it when it does so the existing path is untouched, and otherwise spawn withsubprocess.PIPEand forward intoerrlogfrom a reader task.Two things I'd want a maintainer's read on:
- Shutdown ordering. Once the server dies its stderr pipe is at EOF with at most a buffer left, and those last lines are exactly the ones you want when a server dies. Is a bounded wait (0.5s) on the reader before the task group cancel acceptable, or would you rather drop them and keep shutdown unconditional?
- ipykernel with
capture_fd_outputenabled does expose a descriptor, and that path already routes back to the notebook, so I'd keep it on the inherit path rather than detecting Jupyter explicitly. Does that match your intent?
I have this written with tests, including one that reproduces this with a real subprocess and fails on main. Given the
ready for worklabel I'm happy to leave it if this is queued for a maintainer; if you'd rather take it from outside, I'd be glad to pick it up.Disclosure: AI tooling was used to write and push the changes. The diagnosis, the approach, and the findings are my own, and I can speak to any of it.
Hi — I can also reproduce the io.StringIO case from the latest comment: any errlog without a real file descriptor fails the same way, because stdio_client passes it straight to open_process(stderr=...).
My plan: when errlog has no inheritable file descriptor, spawn the server with stderr as a pipe and forward decoded lines to errlog from an async reader task, keeping the current direct-inheritance behavior for real file descriptors. I've implemented this locally with tests for io.StringIO and for the Windows SelectorEventLoop fallback path. Happy to open a PR once this is assigned.Implemented in PR #3403.
- Changed:
stdio_clientnow pipes and forwards stderr whenerrloghas no usable file descriptor, while preserving direct inheritance for normal file-backed streams. - Tests: focused stdio transport tests pass; Ruff and diff checks pass.
- Validation: the full stdio test module has one unrelated Windows process-tree teardown timeout.
- Changed:
i hit this running mcp servers from notebooks during development. stderr just vanishes completely which makes debugging painful. you get no startup errors, no connection failures, nothing. eventually figured out it's because jupyter's OutStream doesn't have a real file descriptor so the child process can't inherit it.
the fix i landed on (branch
fix/jupyter-stderr-logging) checks iferrlog.fileno()raises, and if so spawns the server withstderr=PIPEand runs an async reader task that forwards chunks back to errlog. the important bit was making sure the normal path (real fd, real terminal) stays completely untouched. no pipe, no extra task, just the existing direct inheritance.one edge case worth noting: the errlog can close before the server exits (like when a notebook cell finishes), so the reader needs to handle that gracefully instead of crashing the transport. i have a test for that specific case.
code is on #3405 if it gets reopened, otherwise hopefully the approach helps.
I looked into why this still happens on current main, and it turns out there are two different failures.
On Linux and macOS, ipykernel's
sys.stderr.fileno()returns a copy of the kernel's original fd 2, so the server inherits the terminal Jupyter was started from and nothing shows up in the cell. I checked this with a real kernel driven throughjupyter_client: the server's stderr landed in that terminal and never reached the iopub stream. On Windows,fileno()raises instead, so the spawn itself fails. The same failure hitsio.StringIO, pytest'scapsys, and logging shims like the one in #854.Because of the first case, checking whether
fileno()works (the approach in #3268) wouldn't fix the default Jupyter setup on Linux and macOS. The descriptor is valid there, it just points past the notebook.What I'd suggest:
- Keep handing the descriptor to the server when
errlogis a real file (aTextIOWrapperwith a workingfileno()), so nothing changes for people logging to the terminal or to a file. - For any other stream, spawn with
stderr=PIPEand copy the output intoerrlog.write()from a task. - On shutdown, wait a short, bounded time for that pipe to reach EOF before closing it. Closing it first drops whatever the server writes while exiting, which is what cubic flagged on Forward server stderr when errlog has no file descriptor #3268.
I have this on a branch with tests: leogrox/python-sdk@main...fix/stdio-stderr-relay-156
The full suite passes with 100% coverage, the stdio tests pass on 3.10 through 3.14, and I also confirmed the fix end to end in a real Jupyter kernel. If you're open to an outside PR for this, I'm happy to open it. If you'd rather write the fix yourselves, I hope the notes above save some time.
I used Claude Code to help investigate and write the change, and I've reviewed it myself.
- Keep handing the descriptor to the server when
I see there's a proposed fix from @leogrox. If that's not going to be opened as a PR, I'd like to work on this. Is it still available?
Reacted by Leandro Gonzalez
Is your feature request related to a problem? Please describe.
I want to make mcp python-sdk jupyter notebook compatible. When running in a notebook environment, MCP work but do not output stderr as it does normally.
For instance in jupyter notebook:
Outputs:
while running the server without jupyter notebook:
❯ uv run --quiet src/echo.py starting echo serverstderr is correctly displayed.
This is a big problem mostly because if the server is crashing or the command is wrong you have no way to know what's wrong: nothing is logged and the jupyter notebook cell just hangs.
Describe the solution you'd like
I found the culprit being the use of:
In particular sys.stderr here is not working in the jupyter / ipython context. Instead I would suggest a working change as follow:
stderr_readerasync function like the following:This would result in the same behavior as before while allowing the stderr to be logged in the jupyter notebook context.
Additional context
Jupyter notebook support support is also requested for mcpadapt which bring MCP server tools in any agentic framework, as many agentic framework demonstrate usage in jupyter notebooks.
https://github.com/grll/mcpadapt