@@ -83,14 +83,38 @@ for line in sys.stdin:
8383 "error": {"code": -32601, "message": "method not found"}})
8484"# ;
8585
86+ const STDIO_STALL_AFTER_INIT_PY : & str = r#"
87+ import sys, json, time
88+
89+ def send(obj):
90+ sys.stdout.write(json.dumps(obj) + "\n")
91+ sys.stdout.flush()
92+
93+ line = sys.stdin.readline()
94+ msg = json.loads(line)
95+ send({"jsonrpc": "2.0", "id": msg.get("id"), "result": {
96+ "protocolVersion": "2024-11-05",
97+ "capabilities": {},
98+ "serverInfo": {"name": "stall", "version": "0.0.1"},
99+ }})
100+
101+ # Do not read stdin again. A large client request can fill the pipe while the
102+ # transport is still in write_json, before its per-request response timeout is
103+ # armed.
104+ time.sleep(60)
105+ "# ;
106+
86107/// Write the python fixture into a tempdir and return its path. The tempdir is
87108/// kept alive by the returned guard.
88109fn write_stdio_fixture ( ) -> ( tempfile:: TempDir , std:: path:: PathBuf ) {
110+ write_stdio_fixture_source ( STDIO_FIXTURE_PY )
111+ }
112+
113+ fn write_stdio_fixture_source ( source : & str ) -> ( tempfile:: TempDir , std:: path:: PathBuf ) {
89114 let dir = tempfile:: tempdir ( ) . expect ( "tempdir" ) ;
90115 let path = dir. path ( ) . join ( "server.py" ) ;
91116 let mut f = std:: fs:: File :: create ( & path) . expect ( "create fixture" ) ;
92- f. write_all ( STDIO_FIXTURE_PY . as_bytes ( ) )
93- . expect ( "write fixture" ) ;
117+ f. write_all ( source. as_bytes ( ) ) . expect ( "write fixture" ) ;
94118 f. flush ( ) . expect ( "flush fixture" ) ;
95119 ( dir, path)
96120}
@@ -161,6 +185,34 @@ async fn stdio_error_result_maps_is_error() {
161185 assert_eq ! ( mcp_result_tool_content( & result. into_seam( ) ) , "kaboom" ) ;
162186}
163187
188+ #[ tokio:: test]
189+ #[ ignore = "diagnostic repro: stdio MCP timeout does not cover a blocked stdin write" ]
190+ async fn repro_stdio_call_can_remain_pending_before_response_timeout_is_armed ( ) {
191+ let ( _dir, script) = write_stdio_fixture_source ( STDIO_STALL_AFTER_INIT_PY ) ;
192+ let transport = StdioTransport :: connect (
193+ "python3" ,
194+ & [ script. to_string_lossy ( ) . to_string ( ) ] ,
195+ & HashMap :: new ( ) ,
196+ None ,
197+ Duration :: from_secs ( 2 ) ,
198+ Duration :: from_millis ( 50 ) ,
199+ )
200+ . await
201+ . expect ( "connect stalling stdio fixture" ) ;
202+
203+ let large_arg = "x" . repeat ( 32 * 1024 * 1024 ) ;
204+ let result = tokio:: time:: timeout (
205+ Duration :: from_secs ( 2 ) ,
206+ transport. call_tool ( "stall" , Some ( json ! ( { "blob" : large_arg } ) ) ) ,
207+ )
208+ . await ;
209+
210+ assert ! (
211+ result. is_err( ) ,
212+ "MCP call returned inside the outer watchdog; this repro expects write_json to remain pending before the tool timeout"
213+ ) ;
214+ }
215+
164216// ---------------------------------------------------------------------------
165217// http transport (loopback TcpListener)
166218// ---------------------------------------------------------------------------
0 commit comments