11import io
2+ import json
23import sys
34from io import TextIOWrapper
45
78
89from mcp .server .stdio import stdio_server
910from mcp .shared .message import SessionMessage
10- from mcp .types import JSONRPCMessage , JSONRPCRequest , JSONRPCResponse , jsonrpc_message_adapter
11+ from mcp .types import (
12+ PARSE_ERROR ,
13+ JSONRPCError ,
14+ JSONRPCMessage ,
15+ JSONRPCRequest ,
16+ JSONRPCResponse ,
17+ jsonrpc_message_adapter ,
18+ )
1119
1220
1321@pytest .mark .anyio
@@ -68,8 +76,8 @@ async def test_stdio_server_invalid_utf8(monkeypatch: pytest.MonkeyPatch):
6876 """Non-UTF-8 bytes on stdin must not crash the server.
6977
7078 Invalid bytes are replaced with U+FFFD, which then fails JSON parsing and
71- is delivered as an in-stream exception . Subsequent valid messages must
72- still be processed.
79+ is returned as a JSON-RPC parse error . Subsequent valid messages must still
80+ be processed.
7381 """
7482 # \xff\xfe are invalid UTF-8 start bytes.
7583 valid = JSONRPCRequest (jsonrpc = "2.0" , id = 1 , method = "ping" )
@@ -78,17 +86,54 @@ async def test_stdio_server_invalid_utf8(monkeypatch: pytest.MonkeyPatch):
7886 # Replace sys.stdin with a wrapper whose .buffer is our raw bytes, so that
7987 # stdio_server()'s default path wraps it with errors='replace'.
8088 monkeypatch .setattr (sys , "stdin" , TextIOWrapper (raw_stdin , encoding = "utf-8" ))
81- monkeypatch . setattr ( sys , " stdout" , TextIOWrapper ( io .BytesIO (), encoding = "utf-8" ) )
89+ stdout = io .StringIO ( )
8290
8391 with anyio .fail_after (5 ):
84- async with stdio_server () as (read_stream , write_stream ):
85- await write_stream .aclose ()
92+ async with stdio_server (stdout = anyio .AsyncFile (stdout )) as (read_stream , write_stream ):
8693 async with read_stream : # pragma: no branch
87- # First line: \xff\xfe -> U+FFFD U+FFFD -> JSON parse fails -> exception in stream
94+ # First line: \xff\xfe -> U+FFFD U+FFFD -> JSON parse fails -> error response on stdout
8895 first = await read_stream .receive ()
89- assert isinstance (first , Exception )
9096
9197 # Second line: valid message still comes through
92- second = await read_stream .receive ()
93- assert isinstance (second , SessionMessage )
94- assert second .message == valid
98+ assert isinstance (first , SessionMessage )
99+ assert first .message == valid
100+
101+ await write_stream .aclose ()
102+
103+ stdout .seek (0 )
104+ output = stdout .read ()
105+ error = jsonrpc_message_adapter .validate_json (output .strip ())
106+ assert isinstance (error , JSONRPCError )
107+ assert error .id is None
108+ assert error .error .code == PARSE_ERROR
109+
110+
111+ @pytest .mark .anyio
112+ async def test_stdio_server_parse_error_completes_id_bearing_request ():
113+ params : object = {"leaf" : True }
114+ for index in reversed (range (256 )):
115+ params = {f"p{ index } " : params }
116+ line = json .dumps ({"jsonrpc" : "2.0" , "id" : 900256 , "method" : "ping" , "params" : params }) + "\n "
117+
118+ stdin = io .StringIO (line )
119+ stdout = io .StringIO ()
120+
121+ with anyio .fail_after (5 ):
122+ async with stdio_server (stdin = anyio .AsyncFile (stdin ), stdout = anyio .AsyncFile (stdout )) as (
123+ read_stream ,
124+ write_stream ,
125+ ):
126+ async with read_stream :
127+ with pytest .raises (anyio .EndOfStream ):
128+ await read_stream .receive ()
129+ await write_stream .aclose ()
130+
131+ stdout .seek (0 )
132+ output_lines = stdout .readlines ()
133+ assert len (output_lines ) == 1
134+
135+ response = jsonrpc_message_adapter .validate_json (output_lines [0 ].strip ())
136+ assert isinstance (response , JSONRPCError )
137+ assert response .id == 900256
138+ assert response .error .code == PARSE_ERROR
139+ assert "Parse error" in response .error .message
0 commit comments