Is your feature request related to a problem? Please describe.
In temporalio/contrib/google_adk_agents/_mcp.py, TemporalMcpToolSetProvider._get_activities() defines the {name}-list-tools and {name}-call-tool activities like this:
@activity.defn(name=self._name + "-list-tools")
async def get_tools(args):
toolset = self._toolset_factory(args.factory_argument) # new McpToolset every call
tools = await toolset.get_tools()
...
@activity.defn(name=self._name + "-call-tool")
async def call_tool(args):
toolset = self._toolset_factory(args.factory_argument) # new McpToolset every call
tools = await toolset.get_tools()
...
result = await tool.run_async(...)
...
Every single activity invocation constructs a brand-new ADK McpToolset (and therefore a brand-new MCPSessionManager), which internally spawns a fresh MCP transport — a new subprocess for stdio servers, or a new SSE/streamable-HTTP session — via MCPSessionManager.create_session(). No .close()/cleanup is ever called on these ephemeral toolsets in _get_activities(). For stdio-transport MCP servers, this leaks a spawned child process on every activity execution, relying only on GC/process exit for eventual reclamation.
This is a real regression relative to how the rest of this repo already handles the identical problem:
temporalio/contrib/openai_agents/_mcp.py's StatelessMCPServerProvider at least wraps every call in try/finally: await server.cleanup() — no leak, just no reuse.
temporalio/contrib/openai_agents/_mcp.py also ships a StatefulMCPServerProvider (dedicated worker + persistent session for the workflow's lifetime).
temporalio/contrib/strands/_temporal_mcp_client.py implements a fully pooled, idle-evicted, worker-process-shared connection (_ConnectionRecord, get_connection, _MCP_CONNECTION_IDLE default of 5 minutes, acquire/release refcounting, error-triggered eviction).
temporalio/contrib/google_genai/_mcp.py reimplements essentially the same pooling pattern, and its own docstring states it is "modeled on the strands plugin's _temporal_mcp_client."
google_adk_agents (merged earliest, PR #1353) is the one AI-agent contrib package that never received this treatment — it's both slower (reconnect every call) and, unlike the others, actually leaks resources rather than just being inefficient.
Describe the solution you'd like
Port the already-proven, already-shipped pooling pattern from temporalio/contrib/strands/_temporal_mcp_client.py (and its near-identical sibling temporalio/contrib/google_genai/_mcp.py) into temporalio/contrib/google_adk_agents/_mcp.py:
- Add a connection-record/pool keyed by toolset name, holding one live
McpToolset + underlying MCP session open per worker process, opened lazily on first use — matching strands's _ConnectionRecord/get_connection shape.
- Add acquire/release refcounting so idle eviction (default
timedelta(minutes=5), overridable) only fires when no calls are in flight, matching strands's _MCP_CONNECTION_IDLE default for consistency across contribs.
- On any call failure, evict and let the next call reconnect (same "drop the broken session" behavior already in
strands/google_genai).
- Rewrite
TemporalMcpToolSetProvider._get_activities()'s get_tools/call_tool activities to call get_connection(...) instead of self._toolset_factory(...) fresh each time, preserving the existing factory_argument parameterization semantics.
- Ensure worker shutdown drains/closes any open records (mirroring
strands's aclose()).
- Add regression tests asserting that N sequential
call_tool activity executions against the same toolset name result in exactly one underlying MCPSessionManager.create_session()/subprocess spawn, not N — the direct, testable manifestation of the leak.
This is pure Python/asyncio work inside temporalio/contrib/google_adk_agents/ — no Rust, no core bridge, no protocol changes.
Describe alternatives you've considered
- Minimal fix: just add
try/finally: await toolset.close() (matching openai_agents's StatelessMCPServerProvider) without pooling — stops the leak but keeps the reconnect-per-call overhead. Happy to start with this as a smaller first PR if maintainers prefer incremental steps, then follow up with full pooling to reach parity with strands/google_genai.
- Leaving as-is and relying on GC/process exit — not acceptable for stdio-transport servers under sustained load; this is a genuine leak, not just a missed optimization.
Additional context
Not aware of any existing issue or PR covering this — searched for google_adk_agents + connection/toolset/mcp terms (including an exact-phrase search) with no relevant hits; the handful of existing google_adk_agents-related issues/PRs are about tool_context serialization, activity_tool metadata, streaming, and codeowners — none touch MCP connection lifecycle.
One secondary observation, not part of this request: the pooling logic is now duplicated near-verbatim across strands and google_genai. A maintainer might prefer that this fix (or a follow-up) extract a shared internal pooling utility (e.g. temporalio.contrib._mcp_pool) rather than adding a third near-identical copy — happy to go either direction based on your preference.
Happy to contribute a PR for this if the team is open to the approach.
Is your feature request related to a problem? Please describe.
In
temporalio/contrib/google_adk_agents/_mcp.py,TemporalMcpToolSetProvider._get_activities()defines the{name}-list-toolsand{name}-call-toolactivities like this:Every single activity invocation constructs a brand-new ADK
McpToolset(and therefore a brand-newMCPSessionManager), which internally spawns a fresh MCP transport — a new subprocess for stdio servers, or a new SSE/streamable-HTTP session — viaMCPSessionManager.create_session(). No.close()/cleanup is ever called on these ephemeral toolsets in_get_activities(). For stdio-transport MCP servers, this leaks a spawned child process on every activity execution, relying only on GC/process exit for eventual reclamation.This is a real regression relative to how the rest of this repo already handles the identical problem:
temporalio/contrib/openai_agents/_mcp.py'sStatelessMCPServerProviderat least wraps every call intry/finally: await server.cleanup()— no leak, just no reuse.temporalio/contrib/openai_agents/_mcp.pyalso ships aStatefulMCPServerProvider(dedicated worker + persistent session for the workflow's lifetime).temporalio/contrib/strands/_temporal_mcp_client.pyimplements a fully pooled, idle-evicted, worker-process-shared connection (_ConnectionRecord,get_connection,_MCP_CONNECTION_IDLEdefault of 5 minutes, acquire/release refcounting, error-triggered eviction).temporalio/contrib/google_genai/_mcp.pyreimplements essentially the same pooling pattern, and its own docstring states it is "modeled on the strands plugin's_temporal_mcp_client."google_adk_agents(merged earliest, PR #1353) is the one AI-agent contrib package that never received this treatment — it's both slower (reconnect every call) and, unlike the others, actually leaks resources rather than just being inefficient.Describe the solution you'd like
Port the already-proven, already-shipped pooling pattern from
temporalio/contrib/strands/_temporal_mcp_client.py(and its near-identical siblingtemporalio/contrib/google_genai/_mcp.py) intotemporalio/contrib/google_adk_agents/_mcp.py:McpToolset+ underlying MCP session open per worker process, opened lazily on first use — matchingstrands's_ConnectionRecord/get_connectionshape.timedelta(minutes=5), overridable) only fires when no calls are in flight, matchingstrands's_MCP_CONNECTION_IDLEdefault for consistency across contribs.strands/google_genai).TemporalMcpToolSetProvider._get_activities()'sget_tools/call_toolactivities to callget_connection(...)instead ofself._toolset_factory(...)fresh each time, preserving the existingfactory_argumentparameterization semantics.strands'saclose()).call_toolactivity executions against the same toolset name result in exactly one underlyingMCPSessionManager.create_session()/subprocess spawn, not N — the direct, testable manifestation of the leak.This is pure Python/asyncio work inside
temporalio/contrib/google_adk_agents/— no Rust, no core bridge, no protocol changes.Describe alternatives you've considered
try/finally: await toolset.close()(matchingopenai_agents'sStatelessMCPServerProvider) without pooling — stops the leak but keeps the reconnect-per-call overhead. Happy to start with this as a smaller first PR if maintainers prefer incremental steps, then follow up with full pooling to reach parity withstrands/google_genai.Additional context
Not aware of any existing issue or PR covering this — searched for
google_adk_agents+ connection/toolset/mcp terms (including an exact-phrase search) with no relevant hits; the handful of existinggoogle_adk_agents-related issues/PRs are about tool_context serialization, activity_tool metadata, streaming, and codeowners — none touch MCP connection lifecycle.One secondary observation, not part of this request: the pooling logic is now duplicated near-verbatim across
strandsandgoogle_genai. A maintainer might prefer that this fix (or a follow-up) extract a shared internal pooling utility (e.g.temporalio.contrib._mcp_pool) rather than adding a third near-identical copy — happy to go either direction based on your preference.Happy to contribute a PR for this if the team is open to the approach.