Skip to content

On Windows 11 when you initialize an mcp client it hangs indefinitely #552

Description

@BenMawnMahlauNBTC

Describe the bug
On Windows 11 when you initialize an mcp client it hangs indefinitely.

To Reproduce
Code to reproduce:

# ruff: noqaimportasynciofrommcpimportClientSession, StdioServerParametersfrommcp.client.sseimportsse_clientfrommcp.client.stdioimportstdio_clientasyncdefrun():
params=StdioServerParameters(
command='bunx', args=['@playwright/mcp@latest']
)
asyncwithstdio_client(params) as (read, write):
print('inside client')
asyncwithClientSession(read, write) asc:
print('inside ClientSession')
awaitc.initialize()
print('exit ClientSession')
print('exit stdio_client')
asyncdefrun_sse():
asyncwithsse_client('http://localhost:8931/sse') as (read, write):
asyncwithClientSession(read, write) asc:
awaitc.initialize()
print('exit ClientSession')
print('exit sse_client')
if__name__=='__main__':
asyncio.run(run_sse()) # worksasyncio.run(run()) # does not work

Expected behavior
in both cases it should print both exit statements

Desktop (please complete the following information):

  • OS: Windows 11
  • Python Version: Tested on both 3.13.1 and 3.12.7

Additional context
counterintuitively commenting out the code meant to support windows fixes this issue:

importosimportsysfromcontextlibimportasynccontextmanagerfrompathlibimportPathfromtypingimportLiteral, TextIOimportanyioimportanyio.lowlevelfromanyio.streams.memoryimportMemoryObjectReceiveStream, MemoryObjectSendStreamfromanyio.streams.textimportTextReceiveStreamfrompydanticimportBaseModel, Fieldimportmcp.typesastypes# from .win32 import (# create_windows_process,# get_windows_executable_command,# terminate_windows_process,# )# Environment variables to inherit by defaultDEFAULT_INHERITED_ENV_VARS= (
[
"APPDATA",
"HOMEDRIVE",
"HOMEPATH",
"LOCALAPPDATA",
"PATH",
"PROCESSOR_ARCHITECTURE",
"SYSTEMDRIVE",
"SYSTEMROOT",
"TEMP",
"USERNAME",
"USERPROFILE",
]
ifsys.platform=="win32"else ["HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER"]
)
print( sys.platform)
defget_default_environment() ->dict[str, str]:
""" Returns a default environment object including only environment variables deemed safe to inherit. """env: dict[str, str] = {}
forkeyinDEFAULT_INHERITED_ENV_VARS:
value=os.environ.get(key)
ifvalueisNone:
continueifvalue.startswith("()"):
# Skip functions, which are a security riskcontinueenv[key] =valuereturnenvclassStdioServerParameters(BaseModel):
command: str"""The executable to run to start the server."""args: list[str] =Field(default_factory=list)
"""Command line arguments to pass to the executable."""env: dict[str, str] |None=None""" The environment to use when spawning the process. If not specified, the result of get_default_environment() will be used. """cwd: str|Path|None=None"""The working directory to use when spawning the process."""encoding: str="utf-8"""" The text encoding used when sending/receiving messages to the server defaults to utf-8 """encoding_error_handler: Literal["strict", "ignore", "replace"] ="strict"""" The text encoding error handler. See https://docs.python.org/3/library/codecs.html#codec-base-classes for explanations of possible values """@asynccontextmanagerasyncdefstdio_client(server: StdioServerParameters, errlog: TextIO=sys.stderr):
""" Client transport for stdio: this will connect to a server by spawning a process and communicating with it over stdin/stdout. """read_stream: MemoryObjectReceiveStream[types.JSONRPCMessage|Exception]
read_stream_writer: MemoryObjectSendStream[types.JSONRPCMessage|Exception]
write_stream: MemoryObjectSendStream[types.JSONRPCMessage]
write_stream_reader: MemoryObjectReceiveStream[types.JSONRPCMessage]
read_stream_writer, read_stream=anyio.create_memory_object_stream(0)
write_stream, write_stream_reader=anyio.create_memory_object_stream(0)
command=_get_executable_command(server.command)
# Open process with stderr piped for captureprocess=await_create_platform_compatible_process(
command=command,
args=server.args,
env=(
{**get_default_environment(), **server.env}
ifserver.envisnotNoneelseget_default_environment()
),
errlog=errlog,
cwd=server.cwd,
)
asyncdefstdout_reader():
assertprocess.stdout, "Opened process is missing stdout"try:
asyncwithread_stream_writer:
buffer=""asyncforchunkinTextReceiveStream(
process.stdout,
encoding=server.encoding,
errors=server.encoding_error_handler,
):
lines= (buffer+chunk).split("\n")
buffer=lines.pop()
forlineinlines:
try:
message=types.JSONRPCMessage.model_validate_json(line)
exceptExceptionasexc:
awaitread_stream_writer.send(exc)
continueawaitread_stream_writer.send(message)
exceptanyio.ClosedResourceError:
awaitanyio.lowlevel.checkpoint()
asyncdefstdin_writer():
assertprocess.stdin, "Opened process is missing stdin"try:
asyncwithwrite_stream_reader:
asyncformessageinwrite_stream_reader:
json=message.model_dump_json(by_alias=True, exclude_none=True)
awaitprocess.stdin.send(
(json+"\n").encode(
encoding=server.encoding,
errors=server.encoding_error_handler,
)
)
exceptanyio.ClosedResourceError:
awaitanyio.lowlevel.checkpoint()
asyncwith (
anyio.create_task_group() astg,
process,
):
tg.start_soon(stdout_reader)
tg.start_soon(stdin_writer)
try:
yieldread_stream, write_streamfinally:
# Clean up process to prevent any dangling orphaned processes# if sys.platform == "win32":# await terminate_windows_process(process)# else:process.terminate()
def_get_executable_command(command: str) ->str:
""" Get the correct executable command normalized for the current platform. Args: command: Base command (e.g., 'uvx', 'npx') Returns: str: Platform-appropriate command """# if sys.platform == "win32":# return get_windows_executable_command(command)# else:returncommandasyncdef_create_platform_compatible_process(
command: str,
args: list[str],
env: dict[str, str] |None=None,
errlog: TextIO=sys.stderr,
cwd: Path|str|None=None,
):
""" Creates a subprocess in a platform-compatible way. Returns a process handle. """# if sys.platform == "win32":# print('attempting create windows process')# process = await create_windows_process(command, args, env, errlog, cwd)# print('created windows process')# else:process=awaitanyio.open_process(
[command, *args], env=env, stderr=errlog, cwd=cwd
)
returnprocess

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingneeds confirmationNeeds confirmation that the PR is actually required or needed.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions