|
1 | 1 | import logging |
2 | 2 | import os |
| 3 | +import subprocess |
3 | 4 | import sys |
4 | 5 | from contextlib import asynccontextmanager |
5 | 6 | from pathlib import Path |
|
20 | 21 | get_windows_executable_command, |
21 | 22 | terminate_windows_process_tree, |
22 | 23 | ) |
| 24 | +from mcp.shared.jupyter import is_jupyter |
23 | 25 | from mcp.shared.message import SessionMessage |
24 | 26 |
|
25 | 27 | logger = logging.getLogger(__name__) |
@@ -118,12 +120,13 @@ async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stder |
118 | 120 | try: |
119 | 121 | command = _get_executable_command(server.command) |
120 | 122 |
|
121 | | - # Open process with stderr piped for capture |
| 123 | + # Pipe stderr so we can route it through a reader task. |
| 124 | + # This enables Jupyter-compatible stderr output (#156). |
122 | 125 | process = await _create_platform_compatible_process( |
123 | 126 | command=command, |
124 | 127 | args=server.args, |
125 | 128 | env=({**get_default_environment(), **server.env} if server.env is not None else get_default_environment()), |
126 | | - errlog=errlog, |
| 129 | + errlog=subprocess.PIPE, |
127 | 130 | cwd=server.cwd, |
128 | 131 | ) |
129 | 132 | except OSError: |
@@ -177,9 +180,28 @@ async def stdin_writer(): |
177 | 180 | except anyio.ClosedResourceError: # pragma: no cover |
178 | 181 | await anyio.lowlevel.checkpoint() |
179 | 182 |
|
| 183 | + async def stderr_reader(): |
| 184 | + """Read stderr from the subprocess and route to errlog or Jupyter output.""" |
| 185 | + if process.stderr is None: # pragma: no cover |
| 186 | + return |
| 187 | + try: |
| 188 | + async for chunk in TextReceiveStream( |
| 189 | + process.stderr, |
| 190 | + encoding=server.encoding, |
| 191 | + errors=server.encoding_error_handler, |
| 192 | + ): |
| 193 | + if is_jupyter(): |
| 194 | + # In Jupyter, stderr isn't visible — use print() with ANSI red |
| 195 | + print(f"\033[91m{chunk}\033[0m", end="", flush=True) |
| 196 | + else: |
| 197 | + print(chunk, file=errlog, end="", flush=True) |
| 198 | + except anyio.ClosedResourceError: |
| 199 | + await anyio.lowlevel.checkpoint() |
| 200 | + |
180 | 201 | async with anyio.create_task_group() as tg, process: |
181 | 202 | tg.start_soon(stdout_reader) |
182 | 203 | tg.start_soon(stdin_writer) |
| 204 | + tg.start_soon(stderr_reader) |
183 | 205 | try: |
184 | 206 | yield read_stream, write_stream |
185 | 207 | finally: |
@@ -230,7 +252,7 @@ async def _create_platform_compatible_process( |
230 | 252 | command: str, |
231 | 253 | args: list[str], |
232 | 254 | env: dict[str, str] | None = None, |
233 | | - errlog: TextIO = sys.stderr, |
| 255 | + errlog: TextIO | int = sys.stderr, |
234 | 256 | cwd: Path | str | None = None, |
235 | 257 | ): |
236 | 258 | """Creates a subprocess in a platform-compatible way. |
|
0 commit comments