Skip to content

[Bug] MCP Tool freezes at __aexit__ on Windows #3240

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
cutekibry opened this issue May 19, 2025 · 3 comments
Open

[Bug] MCP Tool freezes at __aexit__ on Windows #3240

cutekibry opened this issue May 19, 2025 · 3 comments
Labels
bug Something isn't working

Comments

@cutekibry
Copy link

cutekibry commented May 19, 2025

Description

I wrote:

  • A mongo_agent: agno.agent.Agent with MCPTools(command=<run mongodb-mcp-server>) and Reasontool
  • A team: agno.team.team.Team with mode="coordinate" and Reasontool

Then I call team.print_response("Check the mongo database list for me", stream=True).

The passing from team to mongo_agent is successful and mongo_agent can call list_database().

However, mongo_agent cannot receive the data from list_database() and the program finished with output "Sorry, I cannot ...".

Furthermore, it freezes when exiting async with MCPTools() context. I have to enter Ctrl+C to force it stopping, then it tells me that RuntimeWarning: coroutine 'get_entrypoint_for_tool.<locals>.call_tool' was never awaited.

I believe it is not the MCP's problem, since I have tested it manually by calling the API and it worked.


This is maybe relevant to #3238.

Steps to Reproduce

mongo_agent.py:

from agno.tools.mcp import MCPTools
from agno.agent import Agent
from xd_agno.utils.azure import get_azure_openai
from agno.tools.reasoning import ReasoningTools


def create_mongo_agent(mcp_mongo_tools: MCPTools) -> Agent:
    """
    Create a MongoDB agent from the given tools.

    Example:
        >>> async with MCPTools(command="npx mongo-mcp mongodb://localhost:27017") as mcp_mongo_tools:
        >>>     mongo_agent = create_mongo_agent(mcp_mongo_tools)
    """

    return Agent(
        model=get_azure_openai(),
        name="MongoDB Agent",
        role="Reads or Writes to the MongoDB database",
        tools=[
            ReasoningTools(add_instructions=True),
            mcp_mongo_tools,
        ],
        description=(
            "You are a helpful assistant that can answer questions or"
            "do database operations about the MongoDB database."
        ),
        instructions=[
            "The user might ask you to find some information or do some operations in native language.",
            "",
            "You need to always remember:",
            "1. Think twice before you do any operations.",
            "2. Remember to check the actual fields in the database, since the user might not know the exact field names.",
            "3. You can use the tools provided to do the operations.",
            "4. You SHOULD always add all necessary information from tool result into your response.",
            "",
            "A standard workflow for you to follow:",
            "First, call `list-databases` to list all the databases.",
            "Second, call `list-collections` to list all the collections in the database.",
            "Third, call `find` to find the documents with filter or projection.",
            "You should ALWAYS remember to add filters and projections to the query",
            "if necessary in order to get the most accurate and relevant results.",
            "",
        ],
        show_tool_calls=True,
        markdown=True,
    )

team.py:

from agno.agent import Agent
from agno.team.team import Team
from agno.tools.reasoning import ReasoningTools
from xd_agno.utils.azure import get_azure_openai


def create_team(members: list[Agent]) -> Team:
    return Team(
        name="Leader",
        model=get_azure_openai(),
        mode="coordinate",
        members=list(members),
        description=(
            "You are the leader of the team."
            "You are responsible for the overall plan and the big picture."
            "As you are talking to a Chinese user, you need to always use Chinese."
        ),
        instructions=[
            "Remember:",
            "1. Always think twice before you make any decisions.",
            "2. You need to be very careful when you make any decisions.",
            "3. Always remember to use Chinese.",
        ],
        tools=[
            ReasoningTools(add_instructions=True),
        ],
        add_datetime_to_instructions=True,
        add_member_tools_to_system_message=False,  # This can be tried to make the agent more consistently get the transfer tool call correct
        enable_agentic_context=True,  # Allow the agent to maintain a shared context and send that to members.
        share_member_interactions=True,  # Share all member responses with subsequent member requests.
        show_members_responses=True,
        markdown=True,
    )

__init__.py:

import asyncio

from agno.tools.mcp import MCPTools

from xd_agno.agents.jira_db_agent import create_jira_db_agent
from xd_agno.agents.mongo_agent import create_mongo_agent
from xd_agno.agents.team import create_team

import logging

logging.basicConfig(level=logging.DEBUG)


async def async_main():
    print("Hello from test-agno!")
    async with MCPTools(
        command="npx -y mongodb-mcp-server --connectionString mongodb://localhost:27017",
        timeout_seconds=10,
    ) as mcp_mongo_tools:
        team = create_team(
            [
                create_jira_db_agent(),
                create_mongo_agent(mcp_mongo_tools),
            ]
        )
        print("Team created")
        team.print_response("Check the mongo database list for me", stream=True)


def main():
    """Entry point for the package."""
    asyncio.run(async_main())


if __name__ == "__main__":
    main()

Agent Configuration (if applicable)

No response

Expected Behavior

None

Actual Behavior

None

Screenshots or Logs (if applicable)

After I force it stop using Ctrl+C:

DEBUG:pymongo.topology:{"message": "Server heartbeat succeeded", "topologyId": {"$oid": "682b07813eddacaf9cc40e92"}, "driverConnectionId": 1, "serverConnectionI
d": 144, "serverHost": "localhost", "serverPort": 27017, "awaited": true, "durationMS": 10000.0, "reply": "{\"isWritablePrimary\": true, \"topologyVersion\": {\
"processId\": {\"$oid\": \"6811d1e60704aeeb9e4f9f46\"}}, \"maxBsonObjectSize\": 16777216, \"maxMessageSizeBytes\": 48000000, \"maxWriteBatchSize\": 100000, \"lo
calTime\": {\"$date\": \"2025-05-19T10:27:43.382Z\"}, \"logicalSessionTimeoutMinutes\": 30, \"connectionId\": 144, \"maxWireVersion\": 25, \"ok\": 1.0}"}
DEBUG:pymongo.topology:{"message": "Server heartbeat started", "topologyId": {"$oid": "682b07813eddacaf9cc40e92"}, "driverConnectionId": 1, "serverConnectionId"
: 144, "serverHost": "localhost", "serverPort": 27017, "awaited": true}
  + Exception Group Traceback (most recent call last):
  |   File "<frozen runpy>", line 198, in _run_module_as_main
  |   File "<frozen runpy>", line 88, in _run_code
  |   File "E:\work\test\test-agno\.venv\Scripts\xd-agno.exe\__main__.py", line 10, in <module>
  |   File "E:\work\test\test-agno\src\xd_agno\__init__.py", line 32, in main
  |     asyncio.run(async_main())
  |   File "C:\Users\A-AAA-202109-83\AppData\Roaming\uv\python\cpython-3.12.9-windows-x86_64-none\Lib\asyncio\runners.py", line 195, in run
  |     return runner.run(main)
  |            ^^^^^^^^^^^^^^^^
  |   File "C:\Users\A-AAA-202109-83\AppData\Roaming\uv\python\cpython-3.12.9-windows-x86_64-none\Lib\asyncio\runners.py", line 118, in run
  |     return self._loop.run_until_complete(task)
  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "C:\Users\A-AAA-202109-83\AppData\Roaming\uv\python\cpython-3.12.9-windows-x86_64-none\Lib\asyncio\base_events.py", line 691, in run_until_complete
  |     return future.result()
  |            ^^^^^^^^^^^^^^^
  |   File "E:\work\test\test-agno\src\xd_agno\__init__.py", line 16, in async_main
  |     async with MCPTools(
  |                ^^^^^^^^^
  |   File "E:\work\test\test-agno\.venv\Lib\site-packages\agno\tools\mcp.py", line 196, in __aexit__
  |     await self._context.__aexit__(exc_type, exc_val, exc_tb)
  |   File "C:\Users\A-AAA-202109-83\AppData\Roaming\uv\python\cpython-3.12.9-windows-x86_64-none\Lib\contextlib.py", line 217, in __aexit__
  |     await anext(self.gen)
  |   File "E:\work\test\test-agno\.venv\Lib\site-packages\mcp\client\stdio\__init__.py", line 171, in stdio_client
  |     anyio.create_task_group() as tg,
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "E:\work\test\test-agno\.venv\Lib\site-packages\anyio\_backends\_asyncio.py", line 772, in __aexit__
  |     raise BaseExceptionGroup(
  | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "E:\work\test\test-agno\.venv\Lib\site-packages\mcp\client\stdio\__init__.py", line 148, in stdout_reader
    |     await read_stream_writer.send(session_message)
    |   File "E:\work\test\test-agno\.venv\Lib\site-packages\anyio\streams\memory.py", line 242, in send
    |     self.send_nowait(item)
    |   File "E:\work\test\test-agno\.venv\Lib\site-packages\anyio\streams\memory.py", line 213, in send_nowait
    |     raise BrokenResourceError
    | anyio.BrokenResourceError
    +------------------------------------
DEBUG:pymongo.topology:{"message": "Server heartbeat failed", "topologyId": {"$oid": "682b07813eddacaf9cc40e92"}, "serverHost": "localhost", "serverPort": 27017
, "awaited": true, "durationMS": 7641.000000061467, "failure": "\"_OperationCancelled('operation cancelled')\"", "driverConnectionId": 1}
Exception ignored in: <function BaseSubprocessTransport.__del__ at 0x000001485A1D8860>
Traceback (most recent call last):
  File "C:\Users\A-AAA-202109-83\AppData\Roaming\uv\python\cpython-3.12.9-windows-x86_64-none\Lib\asyncio\base_subprocess.py", line 126, in __del__
  File "C:\Users\A-AAA-202109-83\AppData\Roaming\uv\python\cpython-3.12.9-windows-x86_64-none\Lib\asyncio\base_subprocess.py", line 104, in close
  File "C:\Users\A-AAA-202109-83\AppData\Roaming\uv\python\cpython-3.12.9-windows-x86_64-none\Lib\asyncio\proactor_events.py", line 109, in close
  File "C:\Users\A-AAA-202109-83\AppData\Roaming\uv\python\cpython-3.12.9-windows-x86_64-none\Lib\asyncio\base_events.py", line 799, in call_soon
  File "C:\Users\A-AAA-202109-83\AppData\Roaming\uv\python\cpython-3.12.9-windows-x86_64-none\Lib\asyncio\base_events.py", line 545, in _check_closed
RuntimeError: Event loop is closed
DEBUG:httpcore.connection:close.started
DEBUG:httpcore.connection:close.complete
DEBUG:httpcore.connection:close.started
DEBUG:httpcore.connection:close.complete
sys:1: RuntimeWarning: coroutine 'get_entrypoint_for_tool.<locals>.call_tool' was never awaited
(test-agno)

Environment

- OS: Windows 10
- Agno Version: v1.5.1
- Python Version: 3.12.9

Possible Solutions (optional)

Check the using of Function.entrypoint(...).

In mcp.py:

# Get an entrypoint for the tool
entrypoint = get_entrypoint_for_tool(tool, self.session)
# Create a Function for the tool
f = Function(
name=tool.name,
description=tool.description,
parameters=tool.inputSchema,
entrypoint=entrypoint,
# Set skip_entrypoint_processing to True to avoid processing the entrypoint
skip_entrypoint_processing=True,
)

get_entrypoint_for_tool(...) -> AsyncCallable is assigned to Function.entrypoint: Optional[Callable].

However, Function.entrypoint is called sync in some cases:

def execute_entrypoint(name, func, args):
"""Execute the entrypoint function synchronously."""
arguments = entrypoint_args.copy()
if self.arguments is not None:
arguments.update(self.arguments)
return self.function.entrypoint(**arguments) # type: ignore

It might need to check if these cases are calling correctly.

Additional Context

No response

@cutekibry cutekibry added the bug Something isn't working label May 19, 2025
Copy link

linear bot commented May 19, 2025

@cutekibry
Copy link
Author

Further exploring shows that it stopped at:

  • In agno.tools.mcp.MCPTools.__aexit__():
    • Calling await self._context.__aexit__(exc_type, exc_val, exc_tb)
    • Which is mcp.client.stddio.stdio_client.__aexit__()
    • In mcp.client.stddio.stdio_client.__aexit__():
      • Freezed when calling await mcp.client.stdio.win32.terminate_windows_process(process)
      • This function is supposed to force to kill the process after 2s, but strangely it did not

Details

At agno.tools.mcp.MCPTools.__aexit__():

async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Exit the async context manager."""
if self._session_context is not None:
await self._session_context.__aexit__(exc_type, exc_val, exc_tb)
self.session = None
self._session_context = None
if self._context is not None:
await self._context.__aexit__(exc_type, exc_val, exc_tb)
self._context = None
self._initialized = False

The function stopped at Line 196 calling await self._context.__aexit__(exc_type, exc_val, exc_tb).

self._context is assigned as mcp.client.stddio.stdio_client(self.server_params) at agno.tools.mcp.MCPTools.__aenter__():

async def __aenter__(self) -> "MCPTools":
"""Enter the async context manager."""
if self.session is not None:
# Already has a session, just initialize
if not self._initialized:
await self.initialize()
return self
# Create a new session using stdio_client, sse_client or streamablehttp_client based on transport
if self.transport == "sse":
sse_params = asdict(self.server_params) if self.server_params is not None else {}
if "url" not in sse_params:
sse_params["url"] = self.url
self._context = sse_client(**sse_params)
elif self.transport == "streamable-http":
streamable_http_params = asdict(self.server_params) if self.server_params is not None else {}
if "url" not in streamable_http_params:
streamable_http_params["url"] = self.url
self._context = streamablehttp_client(**streamable_http_params)
elif self.transport == "stdio":
if self.server_params is None:
raise ValueError("server_params must be provided when using stdio transport.")
self._context = stdio_client(self.server_params) # type: ignore

@cutekibry
Copy link
Author

I try to insert debug prints into the mcp.client.stdio.win32.terminate_windows_process, but the debug output is unreasonable for me. In mcp/client/stdio/win32.py:

async def terminate_windows_process(process: Process):
    """
    Terminate a Windows process.

    Note: On Windows, terminating a process with process.terminate() doesn't
    always guarantee immediate process termination.
    So we give it 2s to exit, or we call process.kill()
    which sends a SIGKILL equivalent signal.

    Args:
        process: The process to terminate
    """
    try:
        print("Calling process.terminate()")
        process.terminate()
        print("process.terminate() returned")
        with anyio.fail_after(2.0):
            print("Calling process.wait()")
            await process.wait()
            print("process.wait() returned")
    except TimeoutError:
        # Force kill if it doesn't terminate
        print("Calling process.kill()")
        process.kill()
        print("process.kill() returned")
    except Exception as e:
        print("Unexpected error:", type(e), e)
    finally:
        print("finally!")

The output when I run team.print_response and freeze:

Terminating windows process Process(_process=<Process 20840>, _stdin=StreamWriterWrapper(_stream=<StreamWriter transport=<_ProactorWritePipeTransport fd=900 read=<_OverlappedFuture pending cb=[_ProactorWritePipeTransport._pipe_closed()]>>>), _stdout=StreamReaderWrapper(_stream=<StreamReader waiter=<Future pending cb=[Task.task_wakeup()]> transport=<_ProactorReadPipeTransport fd=1016 read=<_OverlappedFuture pending cb=[_ProactorReadPipeTransport._loop_reading()]>>>), _stderr=None)
Calling process.terminate()
process.terminate() returned
Calling process.wait()
finally!
DEBUG:pymongo.topology:{"message": "Server heartbeat succeeded", "topologyId": {"$oid": "682c23cd4ba86765ec169fef"}, "driverConnectionId": 1, "serverConnectionId": 231, "serverHost": "localhost", "serverPort": 27017, "awaited": true, "durationMS": 10000.0, "reply": "{\"isWritablePrimary\": true, \"topologyVersion\": {\"processId\": {\"$oid\": \"6811d1e60704aeeb9e4f9f46\"}}, \"maxBsonObjectSize\": 16777216, \"maxMessageSizeBytes\": 48000000, \"maxWriteBatchSize\": 100000, \"localTime\": {\"$date\": \"2025-05-20T06:40:23.515Z\"}, \"logicalSessionTimeoutMinutes\": 30, \"connectionId\": 231, \"maxWireVersion\": 25, \"ok\": 1.0}"}  
DEBUG:pymongo.topology:{"message": "Server heartbeat started", "topologyId": {"$oid": "682c23cd4ba86765ec169fef"}, "driverConnectionId": 1, "serverConnectionId": 231, "serverHost": "localhost", "serverPort": 27017, "awaited": true}
  • The code in try block is interrupted.
    • It outputs Calling process.wait() but no process.kill() returned.
  • However, it did not raise an Exception neither.
    • It outputs finally! but no Calling process.kill() or Unexpected error.

This is quite confusing for me, as it is working in the simple test. In test.py:

import asyncio

# run npx -y mongodb-mcp-server --connectionString mongodb://localhost:27017

from mcp.client.stdio.win32 import terminate_windows_process
from mcp.client.stdio import _create_platform_compatible_process


async def main():
    print("hello main")
    process = await _create_platform_compatible_process(
        command="npx.cmd",
        args=[
            "-y",
            "mongodb-mcp-server",
            "--connectionString",
            "mongodb://localhost:27017",
        ],
    )
    await terminate_windows_process(process)


asyncio.run(main())

Output:

hello main
Calling process.terminate()
process.terminate() returned
Calling process.wait()
process.wait() returned
finally!

@cutekibry cutekibry changed the title [Bug] MCP Tool stop responding at call_tool [Bug] MCP Tool freezes at __aexit__ on Windows May 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

1 participant