Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 36 additions & 17 deletions src/mcp/server/stdio.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
"""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.
that can be used to communicate with an MCP client through standard input/output streams.

Example:
```python
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)
```python
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)
```
anyio.run(run_server)
```
"""

import os
import sys
from contextlib import asynccontextmanager
from io import TextIOWrapper
Expand All @@ -30,28 +30,45 @@ async def run_server():


@asynccontextmanager
async def stdio_server(stdin: anyio.AsyncFile[str] | None = None, stdout: anyio.AsyncFile[str] | None = None):
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.
"""
# 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.
#
# Fix #1933: Use os.dup() to avoid closing the original stdin/stdout
# when the wrapper is closed, preventing "I/O operation on closed file" errors.
if not stdin:
stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace"))
stdin_fd = os.dup(sys.stdin.fileno())
stdin_bin = os.fdopen(stdin_fd, "rb", closefd=True)
stdin = anyio.wrap_file(
TextIOWrapper(stdin_bin, encoding="utf-8", errors="replace")
)

if not stdout:
stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8"))
stdout_fd = os.dup(sys.stdout.fileno())
stdout_bin = os.fdopen(stdout_fd, "wb", closefd=True)
stdout = anyio.wrap_file(TextIOWrapper(stdout_bin, encoding="utf-8"))

read_stream_writer, read_stream = create_context_streams[SessionMessage | Exception](0)
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)
message = types.jsonrpc_message_adapter.validate_json(
line, by_name=False
)
except Exception as exc:
await read_stream_writer.send(exc)
continue
Expand All @@ -65,7 +82,9 @@ 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)
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
Expand Down
Loading