Server transport for stdio: this communicates with an MCP client by reading
from the current process' stdin and writing to stdout.
While serving on the process's real stdin and stdout, the transport claims
them: each protocol pipe moves to a private descriptor, fd 0 (with the
Windows standard input handle) reads the null device, and fd 1 (with the
standard output handle) is diverted to stderr — the null device if stderr
is unusable. Handler code and the child processes it spawns can therefore
neither consume protocol bytes nor corrupt the outgoing stream: reads see
end-of-file, and stray writes (a print(), a child's inherited stdout)
land on stderr. Both descriptors are restored when the context exits.
Passing an explicit stream skips the claim for that side.
Source code in src/mcp/server/stdio.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251 | @asynccontextmanager
async def stdio_server(stdin: anyio.AsyncFile[str] | None = None, stdout: anyio.AsyncFile[str] | None = None):
"""Server transport for stdio: this communicates with an MCP client by reading
from the current process' stdin and writing to stdout.
While serving on the process's real stdin and stdout, the transport claims
them: each protocol pipe moves to a private descriptor, fd 0 (with the
Windows standard input handle) reads the null device, and fd 1 (with the
standard output handle) is diverted to stderr — the null device if stderr
is unusable. Handler code and the child processes it spawns can therefore
neither consume protocol bytes nor corrupt the outgoing stream: reads see
end-of-file, and stray writes (a `print()`, a child's inherited stdout)
land on stderr. Both descriptors are restored when the context exits.
Passing an explicit stream skips the claim for that side.
"""
# Purposely not using context managers for these, as we don't want to close
# standard process handles. Encoding of stdin/stdout as text streams on
# python is platform-dependent (Windows is particularly problematic), so we
# re-wrap the underlying binary stream to ensure UTF-8.
restore_stdin: Callable[[], None] | None = None
restore_stdout: Callable[[], None] | None = None
try:
if not stdin:
stdin_buffer, restore_stdin = _claim_fd(0, sys.stdin, "rb", _open_stdin_diversion)
stdin = anyio.wrap_file(TextIOWrapper(stdin_buffer, encoding="utf-8", errors="replace"))
if not stdout:
stdout_buffer, restore_stdout = _claim_fd(1, sys.stdout, "wb", _open_stdout_diversion)
stdout = anyio.wrap_file(TextIOWrapper(stdout_buffer, encoding="utf-8"))
read_stream_writer, read_stream = create_context_streams[SessionMessage | Exception](0)
write_stream, write_stream_reader = create_context_streams[SessionMessage](0)
async def stdin_reader():
try:
async with read_stream_writer:
async for line in stdin:
try:
message = types.jsonrpc_message_adapter.validate_json(line, by_name=False)
except Exception as exc:
await read_stream_writer.send(exc)
continue
session_message = SessionMessage(message)
await read_stream_writer.send(session_message)
except anyio.ClosedResourceError: # pragma: no cover
await anyio.lowlevel.checkpoint()
async def stdout_writer():
try:
async with write_stream_reader:
async for session_message in write_stream_reader:
json = session_message.message.model_dump_json(by_alias=True, exclude_unset=True)
await stdout.write(json + "\n")
await stdout.flush()
except anyio.ClosedResourceError: # pragma: no cover
await anyio.lowlevel.checkpoint()
async with anyio.create_task_group() as tg:
tg.start_soon(stdin_reader)
tg.start_soon(stdout_writer)
yield read_stream, write_stream
finally:
if restore_stdout is not None:
restore_stdout()
if restore_stdin is not None:
restore_stdin()
|