fix(mcp): make run_tool wait_for timeout configurable, raise floor to… (#12996)

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 <MANSURAH@ie.ibm.com>
This commit is contained in:
Mansura Habiba
2026-05-06 15:33:47 +01:00
committed by GitHub
parent 4b46b57e5c
commit deecfc68fa

View File

@ -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__