π΄ Required Information
Describe the Bug:
SkillToolset._resolve_additional_tools_from_state gathers tools from every provided toolset with a bare asyncio.gather(...) β no return_exceptions=True:
https://github.com/google/adk-python/blob/main/src/google/adk/tools/skill_toolset.py#L1096-L1099
ts_results = await asyncio.gather(*(
ts.get_tools_with_prefix(readonly_context)
for ts in self._provided_toolsets
))
Because asyncio.gather propagates the first exception, a single toolset that raises while listing its tools aborts resolution of ALL skill additional tools β including healthy individual tools and healthy toolsets that have nothing to do with the failing one.
This is easy to hit in practice with MCP-backed toolsets: McpToolset.get_tools opens a live session to a remote MCP server, so any restart / 503 / network blip makes listing raise. The blast radius is not limited to that MCP's tools β every adk_additional_tools entry of every activated skill disappears for that invocation, including tools that have no MCP dependency at all. The user-visible symptom is simply that the tool does not exist, which makes this hard to trace back to the MCP server.
Steps to Reproduce:
- Install
google-adk==2.5.0.
- Construct a
SkillToolset with two entries in additional_tools: one plain BaseTool (say good_tool), and one BaseToolset whose get_tools raises β an McpToolset pointed at an unreachable server, or any stub that raises ConnectionError.
- Register one skill whose
adk_additional_tools metadata names a tool from each (good_tool plus one from the failing toolset), and mark that skill activated for the agent (state key _adk_activated_skill_<agent_name>).
- Trigger additional-tools resolution for an invocation in which that skill is activated.
Expected Behavior:
The toolset that fails to list its tools degrades to "no tools from this toolset", and every other additional tool still resolves β good_tool is returned.
Observed Behavior:
Resolution aborts entirely and the failing toolset's exception propagates out of _resolve_additional_tools_from_state. good_tool is never resolved, even though it is a plain individual tool that cannot fail:
File ".../google/adk/tools/skill_toolset.py", line 1096, in _resolve_additional_tools_from_state
ts_results = await asyncio.gather(*(
^^^^^^^^^^^^^^^^^^^^^^^
ConnectionError: MCP server unreachable (503)
Environment Details:
- ADK Library Version: 2.5.0 (also verified present in 2.4.0 and on
main @ skill_toolset.py:1096)
- Desktop OS: macOS
- Python Version: 3.13 (also observed on 3.12)
Model Information:
- Are you using LiteLLM: N/A β the bug is in toolset resolution, before any model call
- Which model is being used: N/A (model-independent)
π‘ Optional Information
Regression:
Not a regression β the bare gather has been there since SkillToolset gained _resolve_additional_tools_from_state.
Possible Direction (description only β I am not submitting a patch):
Passing return_exceptions=True to that gather, then skipping (with a warning log) any toolset whose listing raised while re-raising asyncio.CancelledError so cancellation still propagates, would confine the failure to the toolset that actually failed. Individual tools and healthy toolsets would then resolve normally.
Verification:
I made that change locally against an unmodified google-adk==2.5.0 install and re-ran my local reproduction. The healthy tool resolved (['good_tool']), and the failing toolset was logged and skipped β contributing no tools for that invocation and recovering on the next one, since tool listing is cached per invocation_id.
Note: this is filed as a defect report and is conspicuously designated "Not a Contribution" as that term is used in the Google CLA. I am not submitting any code for inclusion in the Work; please implement any fix independently.
π΄ Required Information
Describe the Bug:
SkillToolset._resolve_additional_tools_from_stategathers tools from every provided toolset with a bareasyncio.gather(...)β noreturn_exceptions=True:https://github.com/google/adk-python/blob/main/src/google/adk/tools/skill_toolset.py#L1096-L1099
Because
asyncio.gatherpropagates the first exception, a single toolset that raises while listing its tools aborts resolution of ALL skill additional tools β including healthy individual tools and healthy toolsets that have nothing to do with the failing one.This is easy to hit in practice with MCP-backed toolsets:
McpToolset.get_toolsopens a live session to a remote MCP server, so any restart / 503 / network blip makes listing raise. The blast radius is not limited to that MCP's tools β everyadk_additional_toolsentry of every activated skill disappears for that invocation, including tools that have no MCP dependency at all. The user-visible symptom is simply that the tool does not exist, which makes this hard to trace back to the MCP server.Steps to Reproduce:
google-adk==2.5.0.SkillToolsetwith two entries inadditional_tools: one plainBaseTool(saygood_tool), and oneBaseToolsetwhoseget_toolsraises β anMcpToolsetpointed at an unreachable server, or any stub that raisesConnectionError.adk_additional_toolsmetadata names a tool from each (good_toolplus one from the failing toolset), and mark that skill activated for the agent (state key_adk_activated_skill_<agent_name>).Expected Behavior:
The toolset that fails to list its tools degrades to "no tools from this toolset", and every other additional tool still resolves β
good_toolis returned.Observed Behavior:
Resolution aborts entirely and the failing toolset's exception propagates out of
_resolve_additional_tools_from_state.good_toolis never resolved, even though it is a plain individual tool that cannot fail:Environment Details:
main@skill_toolset.py:1096)Model Information:
π‘ Optional Information
Regression:
Not a regression β the bare
gatherhas been there sinceSkillToolsetgained_resolve_additional_tools_from_state.Possible Direction (description only β I am not submitting a patch):
Passing
return_exceptions=Trueto thatgather, then skipping (with a warning log) any toolset whose listing raised while re-raisingasyncio.CancelledErrorso cancellation still propagates, would confine the failure to the toolset that actually failed. Individual tools and healthy toolsets would then resolve normally.Verification:
I made that change locally against an unmodified
google-adk==2.5.0install and re-ran my local reproduction. The healthy tool resolved (['good_tool']), and the failing toolset was logged and skipped β contributing no tools for that invocation and recovering on the next one, since tool listing is cached perinvocation_id.Note: this is filed as a defect report and is conspicuously designated "Not a Contribution" as that term is used in the Google CLA. I am not submitting any code for inclusion in the Work; please implement any fix independently.