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)

Offen
#6,822 5 Kommentare 0 Reaktionen 1 zugewiesene Person Beansprucht von @llalitkumarrr Auf GitHub ansehen
mcp request clarification
Vorherrschende Sprache
Python
Sterne
21.5k
Forks
4k
Ø Merge
1 T. 14 Std.
Gemergte PRs (30 T.)
37

Beschreibung

** 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.

Beitragsleitfaden

Beitragsleitfaden öffnen

Bewertung

Dieses Issue wurde noch nicht bewertet.

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.