Skip to content

Commit aa6a7e6

Browse files
committed
fix(server): opt-in drain on read EOF
1 parent f6be89c commit aa6a7e6

4 files changed

Lines changed: 17 additions & 3 deletions

File tree

src/mcp/server/lowlevel/server.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,10 @@ async def run(
406406
# the initialization lifecycle, but can do so with any available node
407407
# rather than requiring initialization for each connection.
408408
stateless: bool = False,
409+
# When True, treat read EOF as a half-close and allow in-flight handlers
410+
# to drain their responses via the still-open write stream (e.g. stdio
411+
# with bash-redirected stdin).
412+
drain_on_read_close: bool = False,
409413
) -> None:
410414
async with self.lifespan(self) as lifespan_context:
411415
dispatcher: JSONRPCDispatcher[TransportContext] = JSONRPCDispatcher(
@@ -416,7 +420,7 @@ async def run(
416420
# the next request (spec says SHOULD NOT, not MUST NOT) sees
417421
# the initialized state instead of failing the init-gate.
418422
inline_methods=frozenset({"initialize"}),
419-
close_write_stream_on_read_close=False,
423+
close_write_stream_on_read_close=not drain_on_read_close,
420424
)
421425
runner = ServerRunner(
422426
server=self,

src/mcp/server/mcpserver/server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,7 @@ async def run_stdio_async(self) -> None:
848848
read_stream,
849849
write_stream,
850850
self._lowlevel_server.create_initialization_options(),
851+
drain_on_read_close=True,
851852
)
852853

853854
async def run_sse_async( # pragma: no cover

tests/server/test_cancel_handling.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ async def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestPar
120120
server_write, from_server = anyio.create_memory_object_stream[SessionMessage](10)
121121

122122
async def run_server():
123-
await server.run(server_read, server_write, server.create_initialization_options())
123+
await server.run(server_read, server_write, server.create_initialization_options(), drain_on_read_close=True)
124124
server_run_returned.set()
125125

126126
init_req = JSONRPCRequest(

tests/server/test_stdio.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,16 @@ async def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestPar
247247
):
248248
with anyio.fail_after(5):
249249
async with anyio.create_task_group() as tg: # pragma: no branch
250-
tg.start_soon(server.run, read_stream, write_stream, server.create_initialization_options())
250+
251+
async def run_server() -> None:
252+
await server.run(
253+
read_stream,
254+
write_stream,
255+
server.create_initialization_options(),
256+
drain_on_read_close=True,
257+
)
258+
259+
tg.start_soon(run_server)
251260
await both_tools_started.wait()
252261
allow_tools_to_finish.set()
253262

0 commit comments

Comments
 (0)