11import logging
22import os
33import sys
4- from contextlib import asynccontextmanager
4+ from contextlib import AsyncExitStack , asynccontextmanager
55from pathlib import Path
66from typing import Literal , TextIO
77
88import anyio
99import anyio .lowlevel
1010from anyio .abc import Process
11- from anyio .streams .memory import MemoryObjectReceiveStream , MemoryObjectSendStream
1211from anyio .streams .text import TextReceiveStream
1312from pydantic import BaseModel , Field
1413
@@ -107,33 +106,19 @@ async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stder
107106 Client transport for stdio: this will connect to a server by spawning a
108107 process and communicating with it over stdin/stdout.
109108 """
110- read_stream : MemoryObjectReceiveStream [SessionMessage | Exception ]
111- read_stream_writer : MemoryObjectSendStream [ SessionMessage | Exception ]
109+ read_stream_writer , read_stream = anyio . create_memory_object_stream [SessionMessage | Exception ]( 0 )
110+ write_stream , write_stream_reader = anyio . create_memory_object_stream [ SessionMessage ]( 0 )
112111
113- write_stream : MemoryObjectSendStream [SessionMessage ]
114- write_stream_reader : MemoryObjectReceiveStream [SessionMessage ]
112+ command = _get_executable_command (server .command )
115113
116- read_stream_writer , read_stream = anyio .create_memory_object_stream (0 )
117- write_stream , write_stream_reader = anyio .create_memory_object_stream (0 )
118-
119- try :
120- command = _get_executable_command (server .command )
121-
122- # Open process with stderr piped for capture
123- process = await _create_platform_compatible_process (
124- command = command ,
125- args = server .args ,
126- env = ({** get_default_environment (), ** server .env } if server .env is not None else get_default_environment ()),
127- errlog = errlog ,
128- cwd = server .cwd ,
129- )
130- except OSError :
131- # Clean up streams if process creation fails
132- await read_stream .aclose ()
133- await write_stream .aclose ()
134- await read_stream_writer .aclose ()
135- await write_stream_reader .aclose ()
136- raise
114+ # Open process with stderr piped for capture
115+ process = await _create_platform_compatible_process (
116+ command = command ,
117+ args = server .args ,
118+ env = ({** get_default_environment (), ** server .env } if server .env is not None else get_default_environment ()),
119+ errlog = errlog ,
120+ cwd = server .cwd ,
121+ )
137122
138123 async def stdout_reader ():
139124 assert process .stdout , "Opened process is missing stdout"
@@ -177,14 +162,13 @@ async def stdin_writer():
177162 except anyio .ClosedResourceError :
178163 await anyio .lowlevel .checkpoint ()
179164
180- async with (
181- anyio .create_task_group () as tg ,
182- process ,
183- ):
165+ async with anyio .create_task_group () as tg , process :
184166 tg .start_soon (stdout_reader )
185167 tg .start_soon (stdin_writer )
168+
186169 try :
187- yield read_stream , write_stream
170+ async with read_stream , write_stream :
171+ yield read_stream , write_stream
188172 finally :
189173 # MCP spec: stdio shutdown sequence
190174 # 1. Close input stream to server
@@ -208,10 +192,6 @@ async def stdin_writer():
208192 except ProcessLookupError :
209193 # Process already exited, which is fine
210194 pass
211- await read_stream .aclose ()
212- await write_stream .aclose ()
213- await read_stream_writer .aclose ()
214- await write_stream_reader .aclose ()
215195
216196
217197def _get_executable_command (command : str ) -> str :
0 commit comments