Skip to content

stdio

Stdio Server Transport Module

This module provides functionality for creating an stdio-based transport layer that can be used to communicate with an MCP client through standard input/output streams.

Example
async def run_server():
    async with stdio_server() as (read_stream, write_stream):
        # read_stream contains incoming JSONRPCMessages from stdin
        # write_stream allows sending JSONRPCMessages to stdout
        server = await create_my_server()
        await server.run(read_stream, write_stream, init_options)

anyio.run(run_server)

stdio_server async

stdio_server(
    stdin: AsyncFile[str] | None = None,
    stdout: 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.

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()