mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 17:25:26 -04:00
fix(code): run stdio MCP server pre-flight check off the event loop (#4434)
Fixes #4433 --- The stdio MCP server pre-flight check calls `shutil.which`, which makes blocking `os.access` calls. When run on the event loop, this triggers a `BlockingError` from blockbuster, preventing the agent from starting. This wraps the `_check_stdio_server` call in `asyncio.to_thread` to execute it off the event loop, resolving the blocking call issue. --------- Co-authored-by: Dhruv <heyparth@Dhruvs-MacBook-Air.local> Co-authored-by: Mason Daugherty <mason@langchain.dev> Co-authored-by: Mason Daugherty <github@mdrxy.com>
This commit is contained in:
@@ -1521,7 +1521,9 @@ async def _load_tools_from_config(
|
||||
if server_type in _SUPPORTED_REMOTE_TYPES:
|
||||
await _check_remote_server(server_name, server_config)
|
||||
elif server_type == "stdio":
|
||||
_check_stdio_server(server_name, server_config)
|
||||
# `shutil.which` makes blocking `os.access` calls; run it
|
||||
# off the event loop so blockbuster doesn't reject it.
|
||||
await asyncio.to_thread(_check_stdio_server, server_name, server_config)
|
||||
except RuntimeError as exc:
|
||||
logger.warning(
|
||||
"MCP server '%s' skipped: pre-flight failed: %s",
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
@@ -2075,6 +2076,38 @@ class TestHealthChecks:
|
||||
):
|
||||
_check_stdio_server("srv", {"command": "missing"})
|
||||
|
||||
async def test_check_stdio_server_runs_off_event_loop(
|
||||
self,
|
||||
write_config: Callable[..., str],
|
||||
) -> None:
|
||||
"""The stdio pre-flight's `shutil.which` runs off the event loop."""
|
||||
path = write_config({"mcpServers": {"srv": {"command": "missing"}}})
|
||||
event_loop_thread = threading.current_thread()
|
||||
which_threads: list[threading.Thread] = []
|
||||
|
||||
def _record_missing_command(_command: str) -> str | None:
|
||||
which_threads.append(threading.current_thread())
|
||||
return None
|
||||
|
||||
with patch(
|
||||
"deepagents_code.mcp_tools.shutil.which",
|
||||
side_effect=_record_missing_command,
|
||||
):
|
||||
tools, manager, server_infos = await get_mcp_tools(path)
|
||||
|
||||
try:
|
||||
assert which_threads
|
||||
assert which_threads[0] is not event_loop_thread
|
||||
assert tools == []
|
||||
assert server_infos[0].name == "srv"
|
||||
assert server_infos[0].status == "error"
|
||||
error = server_infos[0].error or ""
|
||||
assert "command 'missing' not found on PATH" in error
|
||||
assert manager is not None
|
||||
finally:
|
||||
if manager is not None:
|
||||
await manager.cleanup()
|
||||
|
||||
async def test_check_remote_server_transport_error(self) -> None:
|
||||
"""Transport errors are wrapped as `RuntimeError`."""
|
||||
import httpx
|
||||
|
||||
Reference in New Issue
Block a user