fix(runtime): avoid RuntimeError when calling get_tool_specs with MCP tools from running event loop

Offen Anfängerfreundlich
#709 0 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Maintainer antworten meist innerhalb von 1 Tag

Dieses Issue hat noch niemand übernommen.

Bewertung

Schwierigkeit
2/5
Geschätzter Aufwand
1-3 Stunden
Anfängerfreundlichkeit
74/100
Issue-Typ
Bug
Klarheit
Klar beschrieben
Aktivitätsstatus
Aktiv
Tech-Stack
python
Bereich
backend

Rechercherichtung

Beginne in runtime/node/agent/tool/tool_manager.py, wo die zwei asyncio.run()-Aufrufe in etwa den Zeilen 198-204 (der HTTP-MCP-Abruf) und Zeile 227 (der stdio-MCP-Abruf) stehen, die von get_tool_specs aus erreicht werden. Reproduziere das Problem mit dem minimalen Beispiel aus dem Issue, indem du get_tool_specs aus einer async-Funktion heraus aufrufst. Fertig ist es, wenn dieser Aufruf MCP-Tool-Spezifikationen zurückgibt, ohne RuntimeError, sowohl außerhalb als auch innerhalb einer laufenden Event-Loop.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Beschreibung

Summary & Context

In runtime/node/agent/tool/tool_manager.py, ToolManager.get_tool_specs() provides synchronous tool specification discovery for agent nodes (runtime/node/executor/agent_executor.py:77).

When configuring MCP tools (mcp_remote or mcp_local), _build_mcp_remote_specs() and _build_mcp_local_specs() invoke asyncio.run() to execute coroutines synchronously:

# Lines 198-204:
tools = asyncio.run(
    self._fetch_mcp_tools_http(
        config.server,
        headers=config.headers,
        timeout=config.timeout,
    )
)

# Line 227:
tools = asyncio.run(self._fetch_mcp_tools_stdio(config, launch_key))
Problem & Impact

When an agent node with MCP tools is executed inside an asynchronous environment—such as FastAPI routes (server/routes/websocket.py, server/routes/execute.py), an async workflow executor, or an async test—an asyncio event loop is already active in the current thread.

Calling asyncio.run() while an event loop is active immediately raises:

RuntimeError: asyncio.run() cannot be called from a running event loop

This crashes the node execution and prevents MCP tools from being fetched or registered.

Minimal Reproducible Example
import asyncio
from runtime.node.agent.tool.tool_manager import ToolManager
from entity.configs.node.tooling import McpRemoteConfig, ToolingConfig

tool_manager = ToolManager()
mcp_cfg = ToolingConfig(
    type="mcp_remote",
    config=McpRemoteConfig(server="http://localhost:8000/sse", path="node.tooling.mcp_remote"),
    path="node.tooling"
)

# Inside any async function (e.g. FastAPI endpoint or async runner):
async def run_step():
    # Calling get_tool_specs invokes asyncio.run() internally:
    return tool_manager.get_tool_specs([mcp_cfg])

asyncio.run(run_step())
# Result: RuntimeError: asyncio.run() cannot be called from a running event loop
Proposed Fix

Implement a thread-safe coroutine runner helper that detects if an event loop is already active in the current thread. If an event loop is running, delegate the coroutine execution to a worker thread via concurrent.futures.ThreadPoolExecutor to avoid RuntimeError:

import concurrent.futures

def _run_sync(coro):
    try:
        loop = asyncio.get_running_loop()
    except RuntimeError:
        loop = None

    if loop is not None and loop.is_running():
        with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
            return pool.submit(asyncio.run, coro).result()
    return asyncio.run(coro)

And update lines 198 and 227 in tool_manager.py to use _run_sync(...).

Empirical Verification

Tested with local reproduction script:

  • Gate 1 (Outside event loop): Tools retrieved synchronously with 100% success.
  • Gate 2 (Inside running event loop): Successfully executes coroutine in worker thread and returns tool specs without raising RuntimeError.
Vorherrschende Sprache
Python
Sterne
34.7k
Forks
4.3k
Ø Merge
3 Std. 23 Min.
Gemergte PRs (30 T.)
3

Entwicklungsumgebung

  • Enthält ein Dockerfile oder eine Docker-Compose-Datei
  • Keine Pull-Request-Vorlage
  • Kein Beitragsleitfaden

Erste Schritte

  1. Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
  3. Forken Sie das Repository und arbeiten Sie in einem Branch.
  4. Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.

Mehr aus OpenBMB/ChatDev

Alle Issues in OpenBMB/ChatDev

Ähnliche Issues

Weitere Issues zu Python

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.