Skip to content

contrib.google_adk_agents: MCP toolset connections are never closed (resource/subprocess leak), unlike sibling contribs #1663

Description

@wankhede04

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:

  1. 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.
  2. 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.
  3. On any call failure, evict and let the next call reconnect (same "drop the broken session" behavior already in strands/google_genai).
  4. 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.
  5. Ensure worker shutdown drains/closes any open records (mirroring strands's aclose()).
  6. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ai-sdkRelated to AI integrationsenhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions