google / google/adk-python

MCPSessionManager pools a server-terminated streamable-HTTP session forever: `_is_session_disconnected` only checks local stream flags (scope of closed #3321, still broken in v2.7.1)

オープン
#6,822 コメント 5 件 リアクション 0 件 担当者 1 名 @llalitkumarrr が担当を希望しています GitHub で見る
mcp request clarification
主要言語
Python
スター
21.5k
フォーク
4k
平均マージ
1日 14時間
マージ済み PR(30日)
37

説明

** Please make sure you read the contribution guide and file the issues in the right place. **

**Describe the bug**

When a streamable-HTTP MCP server terminates a session server-side (session eviction, server restart, or any condition that makes the server answer HTTP 404 "Session not found" for the stored `mcp-session-id`), `MCPSessionManager` never detects it. `_is_session_disconnected()` checks only the local stream flags:

```python
return session._read_stream._closed or session._write_stream._closed
```

A server-side termination arrives as a `JSONRPCError` ("Session terminated") delivered into the request's read stream — the local streams stay open and the background task stays alive, so the pooled session still looks healthy. `retry_on_errors` then retries the call through the same dead session, which fails identically. From that point every tool call on the toolset fails forever; the session is never re-created.

With `_MCP_GRACEFUL_ERROR_HANDLING` (experimental, default on), the effect is worse in practice: `McpTool.run_async` converts the raised `McpError` into a returned dict `{"error": "MCP tool execution failed: Session terminated"}`. Callers get no exception, so application code cannot distinguish a permanent transport failure from an ordinary tool result, and nothing upstream can trigger a reconnect. In our production system this combination caused a silent multi-day outage: every call "succeeded" with an error dict and no signal fired.

Issue #3321 described this exact scenario (server restart) and named `_is_session_disconnected`; it was closed in Nov 2025, but the promised handling of `McpError`/"Session terminated" never landed — the method is byte-identical in v2.7.1. Related: the MCP Python SDK does not re-initialize on the 404 either (modelcontextprotocol/python-sdk#1676, PR #1818 pending), so an ADK-side fix is needed regardless.

**To Reproduce**

Steps to reproduce the behavior:

1. Run any MCP server over streamable HTTP with stateful sessions. Two easy ways to kill the session server-side: (a) use a server that evicts idle sessions (e.g. Apollo MCP Server v1.17 evicts after ~5 idle minutes), or (b) simply restart the server container between calls.
2. Create an `McpToolset` with `StreamableHTTPConnectionParams`, fetch tools, and make one successful `run_async` call.
3. Terminate the session server-side (wait past the idle eviction, or restart the server).
4. Call `run_async` again — and any number of times after.

```python
import asyncio
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset, StreamableHTTPConnectionParams

async def main():
ts = McpToolset(connection_params=StreamableHTTPConnectionParams(
url="http://localhost:5000/mcp"))
tools = await ts.get_tools()
tool = tools[0]

print(await tool.run_async(args={}, tool_context=None)) # works

input("Now restart the MCP server (or wait past its idle-session eviction), then press Enter...")

print(await tool.run_async(args={}, tool_context=None)) # {'error': 'MCP tool execution failed: Session terminated'}
print(await tool.run_async(args={}, tool_context=None)) # same, forever — never recovers

asyncio.run(main())
```

**Expected behavior**

The session pool detects the server-side termination (the `McpError`/404 on a pooled session) and re-creates the session, so the `retry_on_errors` retry runs against a fresh session and succeeds. At minimum, the error-dict result produced by graceful error handling should be distinguishable as a transport-level failure so callers can force a reconnect themselves.

**Observed behavior**

The dead session is returned from the pool indefinitely; every retry goes through it; every call returns `{"error": "MCP tool execution failed: Session terminated"}` (or raises `McpError` with `ADK_DISABLE_MCP_GRACEFUL_ERROR_HANDLING=1`). The only recovery is tearing down and re-creating the whole `McpToolset` from application code.

**Environment**

- ADK version: reproduced on 2.2.0; code unchanged on v2.7.1 and current main (`mcp_session_manager.py`)
- mcp (python-sdk): 1.29.0
- Python: 3.12
- OS: Linux (Cloud Run) and macOS, identical behavior
- Transport: streamable HTTP, stateful server sessions

**Additional context**

- #3321 (closed): same failure mode via server restart; the fix discussed there (invalidate on `McpError: Session terminated`) does not appear in the codebase.
- #4300: the origin of graceful error handling — the swallow is intentional there; this report is about the session pool never recovering, with the swallow as the reason the failure is also invisible to callers.
- modelcontextprotocol/python-sdk#1676 / PR #1818: client-side re-init on 404 is also missing at the SDK layer.

コントリビューションガイド

コントリビューションガイドを開く

評価

この issue はまだ評価されていません。

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。