From deecfc68fa7cf7245e12e1056cde4e3de3e156ab Mon Sep 17 00:00:00 2001 From: Mansura Habiba Date: Wed, 6 May 2026 15:33:47 +0100 Subject: [PATCH] =?UTF-8?q?fix(mcp):=20make=20run=5Ftool=20wait=5Ffor=20ti?= =?UTF-8?q?meout=20configurable,=20raise=20floor=20to=E2=80=A6=20(#12996)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(mcp): make run_tool wait_for timeout configurable, raise floor to 180s The hardcoded `timeout=30.0` on `asyncio.wait_for(session.call_tool(...))` in both `MCPSseClient.run_tool` and `MCPStreamableHttpClient.run_tool` was truncating in-flight tool calls whose servers needed longer than 30s to respond — most visibly large MCP composers whose tool responses include sizeable catalogs. Failure mode: LLM → AgentExecutor → run_tool (1 call from the LLM) ├── attempt 1: get_session (probe ..._1 fails → swap ..._2) │ → call_tool → 30s wait_for fires, TimeoutError raised │ (server is still working — response ignored on arrival) └── attempt 2: get_session (probe ..._2 fails → swap ..._3) → call_tool → 30s wait_for fires again → util.py dedup ("Repeated TimeoutError, not retrying") → ValueError("Maximum retries exceeded ...") ← AgentExecutor surfaces error → flow fails The retry loop's TimeoutError dedup correctly stops looping once it sees the same error twice, but every call against a slow server hit the same ceiling and never had a chance to succeed. The session swaps between attempts also produced excess HTTP churn against the server. Both `run_tool` methods now resolve the timeout from `get_settings_service().settings.mcp_server_timeout` (already used for connection setup at lines 1617 / 1887, env: `LANGFLOW_MCP_SERVER_TIMEOUT`) with a `max(setting, 180.0)` floor so default deployments don't end up *shorter* than the previous hardcoded 30s — the Pydantic default for `mcp_server_timeout` is 20. Touched: src/lfx/src/lfx/base/mcp/util.py - L1677, L1689 (stdio run_tool) - L1960, L1972 (streamable HTTP run_tool) Unchanged on purpose: - L1217, L1381 session-creation `timeout=30.0` (separate concern). - L1060 `_validate_session_connectivity` 3.0s probe (separate concern; addressed in follow-up if probe thrash persists). Tuning: - LANGFLOW_MCP_SERVER_TIMEOUT now governs both connection setup and tool-call wait_for. The 180s floor clamps low values; raise the env var above 180 to take effect. Repro / verification: - Trace run that previously failed at 67.08s with "Maximum retries exceeded with repeated TimeoutError errors" against `mcp-guardium_get_service_info` (47KB response payload) now completes on attempt 1. Co-authored-by: MANSURA HABIBA --- src/lfx/src/lfx/base/mcp/util.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/lfx/src/lfx/base/mcp/util.py b/src/lfx/src/lfx/base/mcp/util.py index 2ada0b894b..d9d0f90da8 100644 --- a/src/lfx/src/lfx/base/mcp/util.py +++ b/src/lfx/src/lfx/base/mcp/util.py @@ -1672,6 +1672,9 @@ class MCPStdioClient: param_hash = uuid.uuid4().hex[:8] self._session_context = f"default_{param_hash}" + # Tool-call timeout: env LANGFLOW_MCP_SERVER_TIMEOUT (via settings), with a 180s + # floor so default deployments aren't shorter than the previous hardcoded 30s. + timeout = max(get_settings_service().settings.mcp_server_timeout, 180.0) max_retries = 2 last_error_type = None @@ -1683,7 +1686,7 @@ class MCPStdioClient: result = await asyncio.wait_for( session.call_tool(tool_name, arguments=arguments), - timeout=30.0, # 30 second timeout + timeout=timeout, ) except Exception as e: current_error_type = type(e).__name__ @@ -1952,6 +1955,9 @@ class MCPStreamableHttpClient: param_hash = uuid.uuid4().hex[:8] self._session_context = f"default_http_{param_hash}" + # Tool-call timeout: env LANGFLOW_MCP_SERVER_TIMEOUT (via settings), with a 180s + # floor so default deployments aren't shorter than the previous hardcoded 30s. + timeout = max(get_settings_service().settings.mcp_server_timeout, 180.0) max_retries = 2 last_error_type = None @@ -1963,7 +1969,7 @@ class MCPStreamableHttpClient: result = await asyncio.wait_for( session.call_tool(tool_name, arguments=arguments), - timeout=30.0, # 30 second timeout + timeout=timeout, ) except Exception as e: current_error_type = type(e).__name__