1010
1111import httpx
1212
13- from agentrun .tool .model import ToolInfo , ToolSchema
13+ from agentrun .tool .model import ToolInfo
1414from agentrun .utils .config import Config
1515from agentrun .utils .log import logger
16+ from agentrun .utils .ram_signature import get_agentrun_signed_headers
17+
18+ _MCP_METADATA_TIMEOUT_SECONDS = 30.0
1619
1720
1821def _get_or_create_event_loop () -> asyncio .AbstractEventLoop :
@@ -30,9 +33,6 @@ def _get_or_create_event_loop() -> asyncio.AbstractEventLoop:
3033 return loop
3134
3235
33- from agentrun .utils .ram_signature import get_agentrun_signed_headers
34-
35-
3636class _AgentrunRamAuth (httpx .Auth ):
3737 """httpx Auth handler:为每次请求动态生成 RAM 签名。
3838
@@ -144,6 +144,54 @@ def is_streamable(self) -> bool:
144144 """是否使用 Streamable HTTP 传输 / Whether to use Streamable HTTP transport"""
145145 return self .session_affinity == "MCP_STREAMABLE"
146146
147+ def _metadata_timeout_seconds (self ) -> float :
148+ timeout = self .config .get_timeout ()
149+ if timeout and timeout > 0 :
150+ return min (float (timeout ), _MCP_METADATA_TIMEOUT_SECONDS )
151+ return _MCP_METADATA_TIMEOUT_SECONDS
152+
153+ def _invoke_timeout_seconds (self ) -> float :
154+ timeout = self .config .get_timeout ()
155+ if timeout and timeout > 0 :
156+ return float (timeout )
157+ return 600.0
158+
159+ async def _wait_for_mcp_request (
160+ self ,
161+ awaitable : Any ,
162+ operation : str ,
163+ timeout : float ,
164+ ) -> Any :
165+ try :
166+ return await asyncio .wait_for (awaitable , timeout = timeout )
167+ except asyncio .TimeoutError as exc :
168+ raise TimeoutError (
169+ f"MCP { operation } timed out after { timeout :g} s for endpoint"
170+ f" { self .endpoint } "
171+ ) from exc
172+
173+ def _find_mcp_timeout_error (
174+ self , exc : BaseException
175+ ) -> Optional [TimeoutError ]:
176+ if isinstance (exc , TimeoutError ) and str (exc ).startswith ("MCP " ):
177+ return exc
178+
179+ nested_exceptions = getattr (exc , "exceptions" , None )
180+ if not nested_exceptions :
181+ return None
182+
183+ for nested_exc in nested_exceptions :
184+ timeout_error = self ._find_mcp_timeout_error (nested_exc )
185+ if timeout_error is not None :
186+ return timeout_error
187+
188+ return None
189+
190+ def _raise_mcp_timeout_if_present (self , exc : BaseException ) -> None :
191+ timeout_error = self ._find_mcp_timeout_error (exc )
192+ if timeout_error is not None :
193+ raise timeout_error
194+
147195 def _build_ram_auth (self , url : str ) -> tuple :
148196 """当目标是 agentrun-data 域名时,改写 URL 并返回 httpx Auth handler。
149197
@@ -199,8 +247,17 @@ async def list_tools_async(self) -> List[ToolInfo]:
199247 async with ClientSession (
200248 read_stream , write_stream
201249 ) as session :
202- await session .initialize ()
203- result = await session .list_tools ()
250+ metadata_timeout = self ._metadata_timeout_seconds ()
251+ await self ._wait_for_mcp_request (
252+ session .initialize (),
253+ "initialize" ,
254+ metadata_timeout ,
255+ )
256+ result = await self ._wait_for_mcp_request (
257+ session .list_tools (),
258+ "list_tools" ,
259+ metadata_timeout ,
260+ )
204261 return [
205262 ToolInfo .from_mcp_tool (tool )
206263 for tool in result .tools
@@ -215,8 +272,17 @@ async def list_tools_async(self) -> List[ToolInfo]:
215272 async with ClientSession (
216273 read_stream , write_stream
217274 ) as session :
218- await session .initialize ()
219- result = await session .list_tools ()
275+ metadata_timeout = self ._metadata_timeout_seconds ()
276+ await self ._wait_for_mcp_request (
277+ session .initialize (),
278+ "initialize" ,
279+ metadata_timeout ,
280+ )
281+ result = await self ._wait_for_mcp_request (
282+ session .list_tools (),
283+ "list_tools" ,
284+ metadata_timeout ,
285+ )
220286 return [
221287 ToolInfo .from_mcp_tool (tool )
222288 for tool in result .tools
@@ -226,6 +292,9 @@ async def list_tools_async(self) -> List[ToolInfo]:
226292 "mcp package is not installed. Install it with: pip install mcp"
227293 )
228294 return []
295+ except Exception as exc :
296+ self ._raise_mcp_timeout_if_present (exc )
297+ raise
229298
230299 def list_tools (self ) -> List [ToolInfo ]:
231300 """同步获取工具列表 / Get tool list synchronously
@@ -266,9 +335,15 @@ async def call_tool_async(
266335 async with ClientSession (
267336 read_stream , write_stream
268337 ) as session :
269- await session .initialize ()
270- result = await session .call_tool (
271- name , arguments = arguments or {}
338+ await self ._wait_for_mcp_request (
339+ session .initialize (),
340+ "initialize" ,
341+ self ._metadata_timeout_seconds (),
342+ )
343+ result = await self ._wait_for_mcp_request (
344+ session .call_tool (name , arguments = arguments or {}),
345+ f"call_tool { name } " ,
346+ self ._invoke_timeout_seconds (),
272347 )
273348 return result
274349 else :
@@ -281,16 +356,25 @@ async def call_tool_async(
281356 async with ClientSession (
282357 read_stream , write_stream
283358 ) as session :
284- await session .initialize ()
285- result = await session .call_tool (
286- name , arguments = arguments or {}
359+ await self ._wait_for_mcp_request (
360+ session .initialize (),
361+ "initialize" ,
362+ self ._metadata_timeout_seconds (),
363+ )
364+ result = await self ._wait_for_mcp_request (
365+ session .call_tool (name , arguments = arguments or {}),
366+ f"call_tool { name } " ,
367+ self ._invoke_timeout_seconds (),
287368 )
288369 return result
289370 except ImportError :
290371 raise ImportError (
291372 "mcp package is required for MCP tool calls. "
292373 "Install it with: pip install mcp"
293374 )
375+ except Exception as exc :
376+ self ._raise_mcp_timeout_if_present (exc )
377+ raise
294378
295379 def call_tool (
296380 self ,
0 commit comments