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

Aperta Adatta ai principianti
#709 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@DZH166 ci sta già lavorando.

Dal 5/11/2026.

  • #713 di @DZH166 — aperta

Valutazione

Difficoltà
2/5
Tempo stimato
1-3 ore
Idoneità per principianti
74/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
python
Ambito
backend

Direzione di ricerca

Inizia da runtime/node/agent/tool/tool_manager.py, dove si trovano le due chiamate asyncio.run() vicino alle righe 198-204 (il recupero MCP via HTTP) e alla riga 227 (il recupero MCP via stdio), raggiunte da get_tool_specs. Riproduci il problema con l'esempio minimo nell'issue, chiamando get_tool_specs dall'interno di una funzione async. È completato quando quella chiamata restituisce le specifiche degli strumenti MCP senza un RuntimeError, sia al di fuori sia all'interno di un event loop in esecuzione.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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.
Lingua principale
Python
Stelle
34.7k
Fork
4.3k
Merge medio
3h 23m
PR unite (30g)
3

Preparare l'ambiente

  • Include un Dockerfile o un file Docker Compose
  • Nessun modello di pull request
  • Nessuna guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di OpenBMB/ChatDev

Tutte le issue di OpenBMB/ChatDev

Issue simili

Altre issue su Python

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.