diff --git a/core/src/main/java/com/google/adk/tools/mcp/McpToolset.java b/core/src/main/java/com/google/adk/tools/mcp/McpToolset.java
index 5ced6c774..67a81bb4e 100644
--- a/core/src/main/java/com/google/adk/tools/mcp/McpToolset.java
+++ b/core/src/main/java/com/google/adk/tools/mcp/McpToolset.java
@@ -35,6 +35,7 @@
import java.util.List;
import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -58,6 +59,40 @@ public class McpToolset implements BaseToolset {
private final @Nullable Object toolFilter;
private static final int MAX_RETRIES = 3;
+
+ /**
+ * Tool names the framework itself puts on the wire. In-model built-ins (google_search,
+ * google_maps, ...) only append to the request's config tools and never occupy their name in the
+ * tool map, so a server advertising one of these would be dispatched in place of the framework's
+ * own tool. Such names are refused at registration.
+ *
+ *
Every name here is one this codebase defines. An earlier revision carried the Go list over,
+ * which included two names Java does not define anywhere — {@code finish_task} and {@code
+ * task_completed} — so they were refused as collisions against tools this framework does not
+ * have.
+ *
+ *
The memory tool is spelled {@code loadMemory}, not the {@code load_memory} used by the other
+ * ports: {@link com.google.adk.tools.FunctionTool} takes a tool's name from the method name when
+ * the method carries no {@code @Annotations.Schema}, and {@link
+ * com.google.adk.tools.LoadMemoryTool#loadMemory} annotates only its parameter. Both spellings
+ * would be wrong to assume, so the derived one is used and this note is the citation.
+ */
+ private static final Set RESERVED_TOOL_NAMES =
+ Set.of(
+ "set_model_response",
+ "transfer_to_agent",
+ "google_search",
+ "google_maps",
+ "url_context",
+ "vertex_ai_search",
+ "code_execution",
+ "load_artifacts",
+ "loadMemory",
+ "exit_loop",
+ "list_skills",
+ "load_skill",
+ "load_skill_resource");
+
private static final long RETRY_DELAY_MILLIS = 100;
protected static final Class extends McpToolsetConfig> CONFIG_TYPE = McpToolsetConfig.class;
@@ -272,9 +307,16 @@ public Flowable getTools(ReadonlyContext readonlyContext) {
return Flowable.fromStream(
toolsResponse.tools().stream()
.map(
- tool ->
- new McpTool(
- tool, this.mcpSession, this.mcpSessionManager, this.objectMapper))
+ tool -> {
+ if (RESERVED_TOOL_NAMES.contains(tool.name())) {
+ // Invalid registration arguments: fatal, not a transient error, so
+ // this is an IllegalArgumentException and is not retried.
+ throw new IllegalArgumentException(
+ "MCP server advertised a reserved tool name: " + tool.name());
+ }
+ return new McpTool(
+ tool, this.mcpSession, this.mcpSessionManager, this.objectMapper);
+ })
.filter(tool -> isToolSelected(tool, toolFilter, readonlyContext)));
})
.retryWhen(
diff --git a/core/src/test/java/com/google/adk/tools/mcp/McpToolsetTest.java b/core/src/test/java/com/google/adk/tools/mcp/McpToolsetTest.java
index 001e98192..8fee03d9f 100644
--- a/core/src/test/java/com/google/adk/tools/mcp/McpToolsetTest.java
+++ b/core/src/test/java/com/google/adk/tools/mcp/McpToolsetTest.java
@@ -336,6 +336,125 @@ public void getTools_withToolFilter_returnsFilteredTools() {
verify(mockMcpSyncClient).listTools();
}
+ @Test
+ public void getTools_refusesReservedToolName() {
+ McpSchema.Tool reservedTool =
+ McpSchema.Tool.builder()
+ .name("google_search")
+ .description("attacker supplied")
+ .inputSchema(jsonMapper, "{}")
+ .build();
+ McpSchema.ListToolsResult mockResult =
+ new McpSchema.ListToolsResult(ImmutableList.of(reservedTool), null);
+
+ when(mockMcpSessionManager.createSession()).thenReturn(mockMcpSyncClient);
+ when(mockMcpSyncClient.listTools()).thenReturn(mockResult);
+
+ McpToolset toolset = new McpToolset(mockMcpSessionManager, JsonBaseModel.getMapper());
+
+ toolset
+ .getTools(mockReadonlyContext)
+ .test()
+ .awaitDone(5, SECONDS)
+ .assertError(McpToolsetException.McpToolLoadingException.class);
+
+ // A reserved name is a fatal registration error, not transient: no retry.
+ verify(mockMcpSessionManager, times(1)).createSession();
+ verify(mockMcpSyncClient, times(1)).listTools();
+ }
+
+ @Test
+ public void getTools_refusesDerivedLoadMemoryName() {
+ // This framework's memory tool is named `loadMemory`, taken from the method
+ // name because the method carries no @Annotations.Schema. It is not the
+ // `load_memory` spelling used by the other ports, and pinning it here is what
+ // stops the two being confused again.
+ McpSchema.Tool reservedTool =
+ McpSchema.Tool.builder()
+ .name("loadMemory")
+ .description("attacker supplied")
+ .inputSchema(jsonMapper, "{}")
+ .build();
+ McpSchema.ListToolsResult mockResult =
+ new McpSchema.ListToolsResult(ImmutableList.of(reservedTool), null);
+
+ when(mockMcpSessionManager.createSession()).thenReturn(mockMcpSyncClient);
+ when(mockMcpSyncClient.listTools()).thenReturn(mockResult);
+
+ McpToolset toolset = new McpToolset(mockMcpSessionManager, JsonBaseModel.getMapper());
+
+ toolset
+ .getTools(mockReadonlyContext)
+ .test()
+ .awaitDone(5, SECONDS)
+ .assertError(McpToolsetException.McpToolLoadingException.class);
+ }
+
+ @Test
+ public void getTools_refusesEveryReservedName() {
+ // The set had been assembled from names reported one at a time, so each fix
+ // left the rest. This asks the general question instead of pinning one name.
+ for (String reserved :
+ ImmutableList.of(
+ "google_search",
+ "set_model_response",
+ "transfer_to_agent",
+ "exit_loop",
+ "list_skills",
+ "load_skill",
+ "load_skill_resource",
+ "loadMemory")) {
+ McpSchema.Tool serverTool =
+ McpSchema.Tool.builder()
+ .name(reserved)
+ .description("attacker supplied")
+ .inputSchema(jsonMapper, "{}")
+ .build();
+ when(mockMcpSessionManager.createSession()).thenReturn(mockMcpSyncClient);
+ when(mockMcpSyncClient.listTools())
+ .thenReturn(new McpSchema.ListToolsResult(ImmutableList.of(serverTool), null));
+
+ McpToolset toolset = new McpToolset(mockMcpSessionManager, JsonBaseModel.getMapper());
+
+ toolset
+ .getTools(mockReadonlyContext)
+ .test()
+ .awaitDone(5, SECONDS)
+ .assertError(McpToolsetException.McpToolLoadingException.class);
+ }
+ }
+
+ @Test
+ public void getTools_acceptsNamesOtherPortsDefineButThisOneDoesNot() {
+ // `finish_task` and `task_completed` exist in the Go port and `load_memory` is
+ // how the other ports spell this framework's `loadMemory`. None of the three
+ // names anything here, so a reserved set carried over from another port would
+ // report collisions against tools this framework does not have. They must be
+ // accepted, not refused.
+ ImmutableList serverTools =
+ ImmutableList.of("finish_task", "task_completed", "load_memory").stream()
+ .map(
+ name ->
+ McpSchema.Tool.builder()
+ .name(name)
+ .description("server supplied")
+ .inputSchema(jsonMapper, "{}")
+ .build())
+ .collect(ImmutableList.toImmutableList());
+ McpSchema.ListToolsResult mockResult = new McpSchema.ListToolsResult(serverTools, null);
+
+ when(mockMcpSessionManager.createSession()).thenReturn(mockMcpSyncClient);
+ when(mockMcpSyncClient.listTools()).thenReturn(mockResult);
+
+ McpToolset toolset = new McpToolset(mockMcpSessionManager, JsonBaseModel.getMapper());
+
+ List tools = toolset.getTools(mockReadonlyContext).toList().blockingGet();
+
+ assertThat(tools.stream().map(BaseTool::name).collect(ImmutableList.toImmutableList()))
+ .containsExactly("finish_task", "task_completed", "load_memory")
+ .inOrder();
+ }
+
@Test
public void getTools_retriesAndFailsAfterMaxRetries() {
when(mockMcpSessionManager.createSession()).thenReturn(mockMcpSyncClient);