fix(mcp): refuse MCP tools that shadow reserved framework names - #1515
sushant-me wants to merge 3 commits into
Conversation
In-model built-ins (google_search, google_maps, url_context, vertex_ai_search, code_execution) append only to the request's config tools and never occupy their name in the tool map, so the duplicate-name guard in LlmRequest.Builder.appendTools never sees them. A server advertising one of those names was therefore accepted and won dispatch in place of the framework's own tool. A server tool named set_model_response also aborted the whole run. Refuse reserved names when McpToolset loads server tools. Fixes google#1513
ef4d0b0 to
6d3088b
Compare
|
Hi @sushant-me , thank you for your contribution. We appreciate you taking the time to submit this pull request. Currently this PR is under review by our team, we will keep you posted if any additional information is required. thank you. |
|
One scoping note on the residual, so the trade-off is on the record — the same class of note as the one on the Go port, but the reasoning differs here and I did not want to copy it across. This guard closes the path where a remote MCP server advertises a reserved name. It does not change the underlying property that an in-model tool never occupies its name in the tool map:
So an in-process tool registered under Where Java differs from Go: in Happy to prepare that broader change if you would prefer the invariant enforced in one place instead of at each boundary — it touches high-fan-in classes, so I did not want to fold it in unasked. |
Two names in the set were not ADK Java tool names:
finish_task 0 string literals in any non-test Java source
task_completed 0 string literals in any non-test Java source
Neither tool exists in this port; both came from the Go list, which was carried
over wholesale. A server advertising either name was refused as a collision
against a tool this framework does not have.
`load_memory` was also wrong, in the other direction. This port names the tool
from the method name: LoadMemoryTool#loadMemory carries no @Annotations.Schema,
only its parameter does, and FunctionTool falls back to func.getName(). So the
wire name here is `loadMemory`, not the `load_memory` the other ports use, and
the guard was not watching for it.
The set is now the nine names verified in this codebase, and the javadoc
records both corrections and the derivation for `loadMemory` so the next person
does not have to re-derive it.
Tests: `getTools_refusesDerivedLoadMemoryName` pins the corrected spelling, and
`getTools_acceptsNamesOtherPortsDefineButThisOneDoesNot` pins the inverse — the
three names from the other ports must be accepted, because refusing them reports
a collision against a tool this framework does not have. Both guards reverted
and re-run:
- adding "finish_task" back fails the accepted-names test
- reverting McpToolset.java to origin/main fails both refusal tests with
`No errors (latch = 0, values = 1, errors = 0, completions = 1)`
25 tests in the class pass, google-java-format 1.27.0 reports 0 non-complying.
Four framework-owned names were missing: exit_loop, list_skills, load_skill, load_skill_resource Each is a tool this framework ships and a caller can add, with the same standing as `load_artifacts`, which was already listed. All four are present in the Java sources: `ExitLoopTool` declares `exit_loop`, `ListSkillsTool` and `LoadSkillTool` pass theirs to `super(...)`, and `LoadSkillResourceTool` is registered alongside them. They were missed because the set had been assembled from names reported one at a time rather than from what the framework declares. The new test iterates all eight reserved names instead of pinning one, which is the shape that would have caught this. Reverted and re-run: deleting "exit_loop" fails it with `No errors (latch = 0, values = 1, errors = 0, completions = 1)`. Test count 25 -> 26; google-java-format 1.27.0 reports 0 non-complying. This is the second correction to this list. The reason is the approach rather than either edit: a hand-maintained enumeration ships a snapshot that the next tool addition invalidates. Making the in-model tools occupy their name would let the existing duplicate-name guard apply and would not need maintaining.
Fixes #1513
Problem
McpToolsetaccepted any tool name an MCP server advertised. Names that theADK framework reserves for its own built-ins were therefore installable by a
remote server, and the duplicate-name guard in
LlmRequest.Builder.appendToolsnever noticed:google_search,google_maps,url_context,vertex_ai_search,code_execution) are appended only to the request'sconfig.toolsand are never placed in the tool map, so they never collidewith the MCP entry.
set_model_responseis consumed internally bySingleTurnAgentExecutor/LlmFlow; a server tool of that name makes theflow abort the whole run where it expects its own response model.
The result is that an MCP server silently wins dispatch for a framework tool
name — a tool-boundary bug, not a naming nit.
Fix
Reserve the names in
McpToolset.getTools()and fail loading withMcpToolLoadingExceptionwhen a server advertises one:Correction (later push). An earlier revision of this PR listed the Go set
verbatim, which was wrong in both directions.
finish_taskandtask_completeddo not exist anywhere in this port. Theywere refused as collisions against tools this framework does not have.
loadMemoryhere, notload_memory. This port takesa tool's name from the method name when the method carries no
@Annotations.Schema—LoadMemoryTool#loadMemoryannotates only itsparameter — so the other ports'
load_memoryspelling did not match, and theguard was not watching for the name it actually uses.
Two tests now pin this:
getTools_refusesDerivedLoadMemoryName, andgetTools_acceptsNamesOtherPortsDefineButThisOneDoesNot, which asserts that thethree names from the other ports are accepted rather than refused.
McpToolLoadingExceptionis used deliberately:retryWhentreatsIllegalArgumentExceptionas fatal, and a config error of this kind shouldsurface once rather than be retried as a transient server fault.
Tests
getTools_refusesReservedToolNamemocks anMcpSyncClientreturningMcpSchema.Tool.builder().name("google_search")and asserts the load failswith
McpToolLoadingException. It also verifies the client is called exactlyonce, pinning the no-retry behaviour.
Verified in both directions:
McpToolset.java:Tests run: 1, Failures: 1This mirrors the equivalent
google/adk-gofix in #1606.Why a list at all, and why I would rather not keep one
This is the second correction to
RESERVED_TOOL_NAMES, and the reason is theapproach rather than either edit. The set was assembled from names reported one
at a time, so each fix addressed the name in front of it and left the rest. Only
after enumerating the tools this framework actually declares — rather than
transcribing a list — did the remaining four appear.
The structural alternative is better and I would rather implement it: give the
in-model tools their name in the request's tool map, so the existing duplicate-name
check in
LlmRequest.appendToolsrefuses a server that takes one. That guardalready exists and already has the right semantics; the in-model tools currently
bypass it by appending only to the config tools. A list needs maintaining on every
tool the framework adds, and its completeness is not checkable by the same
mechanism that uses it.
If you would rather have the list, it is here and tested. If you would rather have
the guard apply at the point where names are already being checked, I am happy to
rework it that way instead.