From 28810cdc0165b1cb71b24b9f98c154da9f2b88a7 Mon Sep 17 00:00:00 2001 From: "HenD.YA" Date: Fri, 9 Oct 2026 18:01:31 -0500 Subject: [PATCH] feat(mcp): allow rejecting config-supplied stdio MCP servers McpToolset.fromConfig() accepts stdioServerParams / stdioConnectionParams straight from an agent config. Those carry a command and args that the MCP stdio transport launches as a local process, so loading an agent config starts the configured process while tools are resolved on the first turn, before the model is contacted. Agent configs are not necessarily written by the person running the agent: they are shared as bundles, templates, samples and registry entries. Add a switch that makes fromConfig() reject both stdio parameter shapes. The default keeps the current behaviour (the one restored in #1360), so existing configs work unchanged. The rejection is turned on either from the embedding application with McpToolset.setAllowConfigStdioServers(false) or, without changing code, at launch with the system property -Dadk.mcp.allowConfigStdioServers=false. Remote transports (sseServerParams) and McpToolset instances built directly in Java are unaffected. --- .../com/google/adk/tools/mcp/McpToolset.java | 39 +++++++++++ .../google/adk/tools/mcp/McpToolsetTest.java | 69 +++++++++++++++++++ 2 files changed, 108 insertions(+) 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..7bf0a7ced 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 @@ -61,6 +61,22 @@ public class McpToolset implements BaseToolset { private static final long RETRY_DELAY_MILLIS = 100; protected static final Class CONFIG_TYPE = McpToolsetConfig.class; + /** + * System property that lets an operator forbid agent configs from declaring local (stdio) MCP + * servers without modifying the embedding application. Setting it to {@code false} turns the + * rejection on; any other value leaves the historical behaviour in place. + */ + private static final String ALLOW_CONFIG_STDIO_PROPERTY = "adk.mcp.allowConfigStdioServers"; + + /** + * Whether an agent config may declare a local (stdio) MCP server. Defaults to {@code true}, the + * behaviour restored in #1360, so existing configs keep working unchanged. It can be turned off + * at launch with {@code -Dadk.mcp.allowConfigStdioServers=false}, or at runtime with {@link + * #setAllowConfigStdioServers}. + */ + private static volatile boolean allowConfigStdioServers = + !"false".equalsIgnoreCase(System.getProperty(ALLOW_CONFIG_STDIO_PROPERTY)); + /** * Initializes the McpToolset with SSE server parameters. * @@ -416,6 +432,19 @@ public static McpToolset fromConfig(BaseTool.ToolConfig config, String configAbs + " for McpToolset"); } + if ((mcpToolsetConfig.stdioServerParams() != null + || mcpToolsetConfig.stdioConnectionParams() != null) + && !allowConfigStdioServers) { + throw new ConfigurationException( + "Refusing to start a local MCP server declared in an agent config." + + " stdioServerParams and stdioConnectionParams launch the config-supplied command" + + " as a local process while tools are resolved, before the model is contacted." + + " This rejection is disabled by default; it is active because" + + " McpToolset.setAllowConfigStdioServers(false) was called or -D" + + ALLOW_CONFIG_STDIO_PROPERTY + + "=false was set. Remote transports (sseServerParams) are unaffected."); + } + List toolNames = mcpToolsetConfig.toolFilter(); Object connectionParameters = resolveConnectionParameters(mcpToolsetConfig); @@ -430,6 +459,16 @@ public static McpToolset fromConfig(BaseTool.ToolConfig config, String configAbs } } + /** + * Allows or forbids agent configs to declare local (stdio) MCP servers. The default is {@code + * true}, matching the behaviour of previous releases. Applications that load agent configs they + * did not author call this with {@code false} at startup, so a config cannot launch a local + * process during tool resolution. + */ + public static void setAllowConfigStdioServers(boolean allow) { + allowConfigStdioServers = allow; + } + /** * Resolves the single connection-parameters object from an already-validated config. {@code * stdioServerParams} is converted to the MCP SDK {@link ServerParameters}, the type {@link 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 b0ca6a002..7cc1eff5c 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 @@ -36,6 +36,7 @@ import io.modelcontextprotocol.json.McpJsonMapper; import io.modelcontextprotocol.spec.McpSchema; import java.util.List; +import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -52,6 +53,11 @@ public class McpToolsetTest { @Mock private McpSyncClient mockMcpSyncClient; @Mock private ReadonlyContext mockReadonlyContext; + @After + public void restoreConfigStdioDefault() { + McpToolset.setAllowConfigStdioServers(true); + } + private static final McpJsonMapper jsonMapper = McpJsonDefaults.getMapper(); private static final ImmutableMap STDIO_SERVER_PARAMS = @@ -211,6 +217,69 @@ public void testFromConfig_validStdioConnectionParams_createsToolset() assertThat(toolset).isNotNull(); } + @Test + public void testFromConfig_stdioServerParams_rejectedWhenDisallowed() { + McpToolset.setAllowConfigStdioServers(false); + BaseTool.ToolArgsConfig args = new BaseTool.ToolArgsConfig(); + args.put("stdioServerParams", STDIO_SERVER_PARAMS); + + BaseTool.ToolConfig config = new BaseTool.ToolConfig("mcp_toolset", args); + String configPath = "/path/to/config.yaml"; + + ConfigurationException exception = + assertThrows(ConfigurationException.class, () -> McpToolset.fromConfig(config, configPath)); + + assertThat(exception) + .hasMessageThat() + .contains("Refusing to start a local MCP server declared in an agent config"); + } + + @Test + public void testFromConfig_stdioConnectionParams_rejectedWhenDisallowed() { + McpToolset.setAllowConfigStdioServers(false); + BaseTool.ToolArgsConfig args = new BaseTool.ToolArgsConfig(); + args.put( + "stdioConnectionParams", + ImmutableMap.of("timeout", 10f, "serverParams", STDIO_SERVER_PARAMS)); + + BaseTool.ToolConfig config = new BaseTool.ToolConfig("mcp_toolset", args); + String configPath = "/path/to/config.yaml"; + + ConfigurationException exception = + assertThrows(ConfigurationException.class, () -> McpToolset.fromConfig(config, configPath)); + + assertThat(exception) + .hasMessageThat() + .contains("Refusing to start a local MCP server declared in an agent config"); + } + + @Test + public void testFromConfig_stdioServerParams_allowedByDefault() throws ConfigurationException { + BaseTool.ToolArgsConfig args = new BaseTool.ToolArgsConfig(); + args.put("stdioServerParams", STDIO_SERVER_PARAMS); + + BaseTool.ToolConfig config = new BaseTool.ToolConfig("mcp_toolset", args); + String configPath = "/path/to/config.yaml"; + + McpToolset toolset = McpToolset.fromConfig(config, configPath); + + assertThat(toolset).isNotNull(); + } + + @Test + public void testFromConfig_sseParams_unaffectedByStdioRejection() throws ConfigurationException { + McpToolset.setAllowConfigStdioServers(false); + BaseTool.ToolArgsConfig args = new BaseTool.ToolArgsConfig(); + args.put("sseServerParams", ImmutableMap.of("url", "http://localhost:8080")); + + BaseTool.ToolConfig config = new BaseTool.ToolConfig("mcp_toolset", args); + String configPath = "/path/to/config.yaml"; + + McpToolset toolset = McpToolset.fromConfig(config, configPath); + + assertThat(toolset).isNotNull(); + } + @Test public void testFromConfig_onlySseParams_doesNotUseStdioBranch() throws ConfigurationException { // This test ensures that when only SSE params are provided, the SSE branch is taken