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