🌐 AI搜索 & 代理 主页
Skip to content

Conversation

@dannovikov
Copy link

Link to an existing issue (if applicable):

Problem:

When using AgentTool to call a sub-agent that uses MCP tools, ADK creates multiple duplicate MCP connections that all execute the same tool call, resulting in massive API call duplication (3-9x observed). This causes 80% of API calls to be unnecessary duplicates, wasting resources and hitting rate limits faster.

The root cause is that get_tools_with_prefix() is called multiple times during agent preprocessing (in base_llm_flow._preprocess_async), and each call triggers get_tools() which creates new MCP connections and fetches tools from the server.

Solution:

Add caching to BaseToolset.get_tools_with_prefix() to prevent redundant get_tools() calls. The cache stores tools per readonly_context identity, so:

  1. First call with a context → fetches tools and caches them
  2. Subsequent calls with the same context → returns cached tools (skips get_tools())
  3. Different context → cache miss, fetches tools again (correct behavior for context-specific filtering)

This solution is placed at get_tools_with_prefix() because it's the single public API entry point that the ADK framework uses to load tools from toolsets (called from _convert_tool_union_to_tools in llm_agent.py).

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.
= 3529 passed, 2066 warnings in 31.31s =

Added tests:

  • test_get_tools_caching - verifies get_tools() is called only once when get_tools_with_prefix() is called multiple times
  • test_get_tools_caching_with_different_contexts - verifies cache correctly invalidates for different contexts

Manual End-to-End (E2E) Tests:

Run the following reproduction script to verify the fix:

#!/usr/bin/env python3
"""
Test to reproduce issue #3763: AgentTool creates multiple duplicate MCP connections
"""

import asyncio
from typing import Optional

from google.adk.agents.readonly_context import ReadonlyContext
from google.adk.tools.base_tool import BaseTool
from google.adk.tools.base_toolset import BaseToolset


class MockMCPTool(BaseTool):
    """Simulates an MCP tool."""
    
    async def run_async(self, *, args, tool_context):
        return f"Result from {self.name}"


class MockMCPToolset(BaseToolset):
    """Simulates McpToolset behavior - creates tools on each get_tools() call."""
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.get_tools_call_count = 0
        self.connection_count = 0
    
    async def get_tools(
        self, readonly_context: Optional[ReadonlyContext] = None
    ) -> list[BaseTool]:
        self.get_tools_call_count += 1
        self.connection_count += 1
        print(f"  📡 Creating MCP connection #{self.connection_count}")
        print(f"  🔧 Fetching tools from MCP server (call #{self.get_tools_call_count})")
        
        return [
            MockMCPTool(name='github_search', description='Search GitHub'),
            MockMCPTool(name='github_create_issue', description='Create GitHub issue'),
            MockMCPTool(name='github_list_repos', description='List repositories'),
        ]


async def simulate_agent_preprocessing(toolset: MockMCPToolset):
    """Simulates what happens during agent preprocessing in base_llm_flow.py."""
    print("\n🚀 Starting agent preprocessing...")
    print("=" * 70)
    
    print("\n📝 Call 1: Processing toolset for LLM request...")
    tools1 = await toolset.get_tools_with_prefix()
    print(f"   Got {len(tools1)} tools")
    
    print("\n📝 Call 2: Building tools_dict...")
    tools2 = await toolset.get_tools_with_prefix()
    print(f"   Got {len(tools2)} tools")
    
    print("\n📝 Call 3: Additional processing...")
    tools3 = await toolset.get_tools_with_prefix()
    print(f"   Got {len(tools3)} tools")
    
    print("\n" + "=" * 70)
    print(f"📊 RESULTS:")
    print(f"   Total get_tools() calls: {toolset.get_tools_call_count}")
    print(f"   Total MCP connections: {toolset.connection_count}")
    
    if toolset.get_tools_call_count > 1:
        print(f"   ⚠️  ISSUE: get_tools() called {toolset.get_tools_call_count} times!")
    else:
        print(f"   ✅ FIXED: get_tools() called only once (cached)")


async def main():
    toolset = MockMCPToolset(tool_name_prefix='mcp')
    await simulate_agent_preprocessing(toolset)


if __name__ == '__main__':
    asyncio.run(main())

Before fix:

🚀 Starting agent preprocessing...
======================================================================

📝 Call 1: Processing toolset for LLM request...
  📡 Creating MCP connection #1
  🔧 Fetching tools from MCP server (call #1)
   Got 3 tools

📝 Call 2: Building tools_dict...
  📡 Creating MCP connection #2
  🔧 Fetching tools from MCP server (call #2)
   Got 3 tools

📝 Call 3: Additional processing...
  📡 Creating MCP connection #3
  🔧 Fetching tools from MCP server (call #3)
   Got 3 tools

======================================================================
📊 RESULTS:
   Total get_tools() calls: 3
   Total MCP connections: 3
   ⚠️  ISSUE: get_tools() called 3 times!

After fix:

🚀 Starting agent preprocessing...
======================================================================

📝 Call 1: Processing toolset for LLM request...
  📡 Creating MCP connection #1
  🔧 Fetching tools from MCP server (call #1)
   Got 3 tools

📝 Call 2: Building tools_dict...
   Got 3 tools

📝 Call 3: Additional processing...
   Got 3 tools

======================================================================
📊 RESULTS:
   Total get_tools() calls: 1
   Total MCP connections: 1
   ✅ FIXED: get_tools() called only once (cached)

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

…ctions

When get_tools_with_prefix() is called multiple times during agent
preprocessing, it now caches the result per readonly_context to avoid
redundant get_tools() calls.

This fixes issue google#3763 where AgentTool with MCP sub-agents created
multiple duplicate MCP connections, causing 3-9x API call duplication.

Changes:
- Add _tools_cache and _cache_context_id to BaseToolset
- Cache tools in get_tools_with_prefix() based on context identity
- Add tests for caching behavior
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @dannovikov, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly optimizes the ADK framework by introducing a caching layer to the BaseToolset.get_tools_with_prefix() method. This change resolves a performance bottleneck where repeated calls to fetch tools from MCP servers led to redundant connections and API calls during agent preprocessing. By caching tools based on the readonly_context, the system now avoids unnecessary resource consumption and improves overall efficiency when agents interact with sub-agents and their associated tools.

Highlights

  • Problem Identification: The pull request addresses a critical issue where AgentTool calling sub-agents with MCP tools resulted in multiple duplicate MCP connections and excessive API calls (3-9x observed), wasting resources and hitting rate limits faster.
  • Root Cause: The problem stemmed from get_tools_with_prefix() being called multiple times during agent preprocessing, with each call triggering get_tools() to create new MCP connections and fetch tools from the server.
  • Caching Solution: A caching mechanism has been implemented in BaseToolset.get_tools_with_prefix() to prevent redundant get_tools() calls. Tools are now cached per readonly_context identity.
  • Cache Behavior: The first call with a specific context fetches and caches tools. Subsequent calls with the same context retrieve tools from the cache. Calls with a different context correctly result in a cache miss and a new fetch.
  • Testing: New unit tests (test_get_tools_caching and test_get_tools_caching_with_different_contexts) were added to verify the caching logic. Manual end-to-end tests confirmed the fix, showing a reduction from 3 to 1 get_tools() calls and MCP connections.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@adk-bot adk-bot added the mcp [Component] Issues about MCP support label Dec 11, 2025
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request introduces a caching mechanism to BaseToolset.get_tools_with_prefix() to prevent redundant calls to get_tools() and reduce duplicate MCP connections. This is a valuable improvement addressing a significant efficiency issue. The changes correctly initialize cache variables and update the docstring to reflect the new behavior. The added unit tests verify the basic caching functionality for single and different contexts.

@ryanaiagent ryanaiagent self-assigned this Dec 12, 2025
@ryanaiagent ryanaiagent added the needs-review [Status] The PR is awaiting review from the maintainer label Dec 12, 2025
@ryanaiagent
Copy link
Collaborator

Hi @dannovikov, Thank you for your contribution! We appreciate you taking the time to submit this pull request. Your PR has been received by the team and is currently under review. We will provide feedback as soon as we have an update to share.

@ryanaiagent
Copy link
Collaborator

Hi @seanzhou1023 , can you please review this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mcp [Component] Issues about MCP support needs-review [Status] The PR is awaiting review from the maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AgentTool creates multiple duplicate MCP connections causing massive API call duplication

3 participants