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)
- Lenguaje dominante
- Python
- Estrellas
- 21.5k
- Forks
- 4k
- Merge medio
- 1 d 14 h
- PR fusionados (30 d)
- 37
Descripción
** 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.
Guía de contribución
Evaluación
Este issue todavía no se ha evaluado.