Skip to content

Commit c492865

Browse files
committed
test: cover delayed stdio EOF race
1 parent c894c9e commit c492865

File tree

1 file changed

+45
-1
lines changed

1 file changed

+45
-1
lines changed

tests/issues/test_2328_stdio_invalid_utf8.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55

66
import anyio
77
import pytest
8-
from pydantic import AnyUrl, TypeAdapter
8+
from pydantic import AnyHttpUrl, AnyUrl, TypeAdapter
99

1010
from mcp import types
1111
from mcp.server import ServerRequestContext
1212
from mcp.server.lowlevel.server import Server
13+
from mcp.server.mcpserver import MCPServer
1314
from mcp.server.stdio import stdio_server
1415
from mcp.types import JSONRPCError, JSONRPCResponse, jsonrpc_message_adapter
1516

@@ -70,3 +71,46 @@ async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequ
7071
assert isinstance(responses[1], JSONRPCError)
7172
assert responses[1].id == 3
7273
assert responses[1].error.message
74+
75+
76+
@pytest.mark.anyio
77+
async def test_stdio_server_stays_alive_when_tool_validation_finishes_after_stdin_eof():
78+
"""The MCPServer tool path should not crash if validation loses the response race."""
79+
80+
mcp = MCPServer("test")
81+
82+
@mcp.tool()
83+
async def fetch(url: str) -> str:
84+
# Delay validation so stdin can hit EOF and close the session write
85+
# stream before the tool returns its validation failure.
86+
await anyio.sleep(0.1)
87+
return str(TypeAdapter(AnyHttpUrl).validate_python(url))
88+
89+
raw_stdin = io.BytesIO(
90+
b'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}\n'
91+
b'{"jsonrpc":"2.0","method":"notifications/initialized"}\n'
92+
b'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"fetch","arguments":{"url":"http://x\xff\xfe"}}}\n'
93+
)
94+
raw_stdout = io.BytesIO()
95+
stdout = TextIOWrapper(raw_stdout, encoding="utf-8")
96+
97+
async with stdio_server(
98+
stdin=anyio.wrap_file(TextIOWrapper(raw_stdin, encoding="utf-8", errors="replace")),
99+
stdout=anyio.wrap_file(stdout),
100+
) as (read_stream, write_stream):
101+
with anyio.fail_after(5):
102+
await mcp._lowlevel_server.run(
103+
read_stream,
104+
write_stream,
105+
mcp._lowlevel_server.create_initialization_options(),
106+
)
107+
108+
stdout.flush()
109+
responses = [
110+
jsonrpc_message_adapter.validate_json(line)
111+
for line in raw_stdout.getvalue().decode("utf-8").splitlines()
112+
]
113+
114+
assert responses
115+
assert isinstance(responses[0], JSONRPCResponse)
116+
assert responses[0].id == 1

0 commit comments

Comments
 (0)