** 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:
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:
- 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.
- Create an
McpToolset with StreamableHTTPConnectionParams, fetch tools, and make one successful run_async call.
- Terminate the session server-side (wait past the idle eviction, or restart the server).
- Call
run_async again — and any number of times after.
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
** 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),MCPSessionManagernever detects it._is_session_disconnected()checks only the local stream flags: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_errorsthen 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_asyncconverts the raisedMcpErrorinto 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 ofMcpError/"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:
McpToolsetwithStreamableHTTPConnectionParams, fetch tools, and make one successfulrun_asynccall.run_asyncagain — and any number of times after.Expected behavior
The session pool detects the server-side termination (the
McpError/404 on a pooled session) and re-creates the session, so theretry_on_errorsretry 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 raisesMcpErrorwithADK_DISABLE_MCP_GRACEFUL_ERROR_HANDLING=1). The only recovery is tearing down and re-creating the wholeMcpToolsetfrom application code.Environment
mcp_session_manager.py)Additional context
McpError: Session terminated) does not appear in the codebase.