Skip to content

Commit 5df03f1

Browse files
seanzhougooglecopybara-github
authored andcommitted
fix(tools): handle toolset errors gracefully in canonical_tools
When an MCP server (or any toolset) is unavailable or raises an exception during get_tools(), the error previously propagated uncaught through _convert_tool_union_to_tools() and canonical_tools(), crashing the entire agent silently with no log output. This change wraps the toolset.get_tools_with_prefix() call in a try-except that catches exceptions, logs a warning with the toolset class name and error details, and returns an empty tool list. This allows the agent to continue operating with tools from other working toolsets. The fix preserves the existing retry behavior — McpToolset's @retry_on_errors decorator still attempts reconnection before the error reaches this handler. On subsequent agent invocations, get_tools() is called again, naturally retrying the connection. Fixes: #3341 Co-authored-by: Xiang (Sean) Zhou <seanzhougoogle@google.com> PiperOrigin-RevId: 890512146
1 parent fe41817 commit 5df03f1

File tree

2 files changed

+43
-1
lines changed

2 files changed

+43
-1
lines changed

src/google/adk/agents/llm_agent.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,15 @@ async def _convert_tool_union_to_tools(
181181
return [FunctionTool(func=tool_union)]
182182

183183
# At this point, tool_union must be a BaseToolset
184-
return await tool_union.get_tools_with_prefix(ctx)
184+
try:
185+
return await tool_union.get_tools_with_prefix(ctx)
186+
except Exception as e:
187+
logger.warning(
188+
'Failed to get tools from toolset %s: %s',
189+
type(tool_union).__name__,
190+
e,
191+
)
192+
return []
185193

186194

187195
class LlmAgent(BaseAgent):

tests/unittests/agents/test_llm_agent_fields.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,40 @@ def _tool_2():
472472
assert tools[0].name == '_tool_1'
473473
assert tools[1].name == '_tool_2'
474474

475+
async def test_canonical_tools_graceful_degradation_on_toolset_error(self):
476+
"""Test that canonical_tools returns tools from working toolsets when one fails."""
477+
from google.adk.tools.base_tool import BaseTool
478+
from google.adk.tools.base_toolset import BaseToolset
479+
480+
class FailingToolset(BaseToolset):
481+
482+
async def get_tools(self, readonly_context=None):
483+
raise ConnectionError('MCP server unavailable')
484+
485+
class WorkingToolset(BaseToolset):
486+
487+
async def get_tools(self, readonly_context=None):
488+
tool = mock.MagicMock(spec=BaseTool)
489+
tool.name = 'working_tool'
490+
tool._get_declaration = mock.MagicMock(return_value=None)
491+
return [tool]
492+
493+
def _regular_tool():
494+
pass
495+
496+
agent = LlmAgent(
497+
name='test_agent',
498+
model='gemini-pro',
499+
tools=[_regular_tool, FailingToolset(), WorkingToolset()],
500+
)
501+
ctx = await _create_readonly_context(agent)
502+
tools = await agent.canonical_tools(ctx)
503+
504+
# Should have the regular tool + working toolset tool, but not crash
505+
assert len(tools) == 2
506+
assert tools[0].name == '_regular_tool'
507+
assert tools[1].name == 'working_tool'
508+
475509

476510
# Tests for multi-provider model support via string model names
477511
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)