Spawns an MCP server subprocess and connects to it over stdin/stdout. Raises: OSError: If the server process cannot be spawned. ValueError: If the spawn parameters are invalid (embedded NUL bytes).
(
server: StdioServerParameters, errlog: TextIO = sys.stderr
)
| 112 | |
| 113 | @asynccontextmanager |
| 114 | async def stdio_client( |
| 115 | server: StdioServerParameters, errlog: TextIO = sys.stderr |
| 116 | ) -> AsyncGenerator[TransportStreams, None]: |
| 117 | """Spawns an MCP server subprocess and connects to it over stdin/stdout. |
| 118 | |
| 119 | Raises: |
| 120 | OSError: If the server process cannot be spawned. |
| 121 | ValueError: If the spawn parameters are invalid (embedded NUL bytes). |
| 122 | """ |
| 123 | command = _get_executable_command(server.command) |
| 124 | |
| 125 | process = await _create_platform_compatible_process( |
| 126 | command=command, |
| 127 | args=server.args, |
| 128 | env=get_default_environment() | (server.env or {}), |
| 129 | errlog=errlog, |
| 130 | cwd=server.cwd, |
| 131 | ) |
| 132 | |
| 133 | # The spawn succeeded; no awaits until the task group is entered, or a |
| 134 | # cancellation delivered in the gap would leak the live process. |
| 135 | read_stream_writer, read_stream = anyio.create_memory_object_stream[SessionMessage | Exception](0) |
| 136 | write_stream, write_stream_reader = anyio.create_memory_object_stream[SessionMessage](0) |
| 137 | |
| 138 | shutting_down = False |
| 139 | writer_done = anyio.Event() |
| 140 | |
| 141 | async def stdout_reader() -> None: |
| 142 | assert process.stdout, "Opened process is missing stdout" |
| 143 | |
| 144 | stdout = TextReceiveStream(process.stdout, encoding=server.encoding, errors=server.encoding_error_handler) |
| 145 | try: |
| 146 | async with read_stream_writer: |
| 147 | try: |
| 148 | # One line at a time; no read-ahead while a delivery is blocked. |
| 149 | buffer = "" |
| 150 | async for chunk in stdout: |
| 151 | lines = (buffer + chunk).split("\n") |
| 152 | buffer = lines.pop() |
| 153 | for line in lines: |
| 154 | try: |
| 155 | await read_stream_writer.send(_parse_line(line)) |
| 156 | except (anyio.ClosedResourceError, anyio.BrokenResourceError): |
| 157 | return # the session is gone; only the drain below remains |
| 158 | finally: |
| 159 | await _drain_stdout(process) |
| 160 | except anyio.ClosedResourceError: |
| 161 | pass # our own shutdown closed the stdout stream under the read |
| 162 | except (anyio.BrokenResourceError, ConnectionError): |
| 163 | # Teardown noise during shutdown, a real failure otherwise; either way |
| 164 | # the session sees clean closure when the read stream closes. |
| 165 | if not shutting_down: |
| 166 | logger.exception("Reading from the MCP server's stdout failed mid-session") |
| 167 | |
| 168 | async def stdin_writer() -> None: |
| 169 | assert process.stdin, "Opened process is missing stdin" |
| 170 | |
| 171 | try: |