diff --git a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerEngine.java b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerEngine.java index 2dd0e8e0d20cd..2f9b21b2d5480 100644 --- a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerEngine.java +++ b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerEngine.java @@ -40,8 +40,10 @@ /** * {@link McpServerEngine} for Camel Main / JBang: serves MCP streamable HTTP through the Vert.x platform HTTP router - * using the official MCP Java SDK. The MCP endpoint is registered on the main HTTP server's router, so it serves on the - * main server port and inherits its lifecycle, authentication and CORS configuration. + * using the official MCP Java SDK. By default the MCP endpoint is registered on the main HTTP server's router, so it + * serves on the main server port and inherits its lifecycle, authentication and CORS configuration. Set + * {@link #setTargetServerType(String)} to {@link VertxPlatformHttpRouter#SERVER_TYPE_MANAGEMENT} to serve on the + * management HTTP server router instead (e.g. for dev/diagnostics tools that must not be publicly exposed). */ @JdkService(McpServerConstants.MCP_SERVER_ENGINE_FACTORY) public class VertxMcpServerEngine extends ServiceSupport implements McpServerEngine { @@ -62,6 +64,7 @@ public class VertxMcpServerEngine extends ServiceSupport implements McpServerEng private McpJsonMapper jsonMapper; private VertxMcpStreamableServerTransportProvider transport; private McpSyncServer server; + private String targetServerType = VertxPlatformHttpRouter.SERVER_TYPE_SERVER; @Override public CamelContext getCamelContext() { @@ -78,6 +81,19 @@ public void initialize(McpServerInfo info) { this.info = info; } + public String getTargetServerType() { + return targetServerType; + } + + /** + * The server type of the {@link VertxPlatformHttpRouter} to register the MCP endpoint on: + * {@link VertxPlatformHttpRouter#SERVER_TYPE_SERVER} (default) for the main HTTP server, or + * {@link VertxPlatformHttpRouter#SERVER_TYPE_MANAGEMENT} for the management HTTP server. + */ + public void setTargetServerType(String targetServerType) { + this.targetServerType = targetServerType; + } + @Override public boolean consumesServingConfiguration() { return true; @@ -154,16 +170,25 @@ public void toolRemoved(String toolName) { } private VertxPlatformHttpRouter lookupRouter() { + boolean mainTarget = VertxPlatformHttpRouter.SERVER_TYPE_SERVER.equals(targetServerType); Set routers = camelContext.getRegistry().findByType(VertxPlatformHttpRouter.class); VertxPlatformHttpRouter router = routers.stream() - .filter(VertxPlatformHttpRouter::isMainServer) + .filter(r -> targetServerType.equals(r.getServerType())) .findFirst() - .orElseGet(() -> routers.size() == 1 ? routers.iterator().next() : null); + // a bare VertxPlatformHttpServer may carry no server type; only the default target may fall + // back to it — an explicit management target must never silently use the public server + .orElseGet(() -> mainTarget && routers.size() == 1 ? routers.iterator().next() : null); if (router == null) { + if (mainTarget) { + throw new IllegalStateException( + "The MCP server requires the Vert.x platform HTTP server. Enable the Camel main HTTP server " + + "(camel.server.enabled=true with camel-platform-http-main on the classpath) " + + "or add a VertxPlatformHttpServer service to the CamelContext."); + } throw new IllegalStateException( - "The MCP server requires the Vert.x platform HTTP server. Enable the Camel main HTTP server " - + "(camel.server.enabled=true with camel-platform-http-main on the classpath) " - + "or add a VertxPlatformHttpServer service to the CamelContext."); + "The MCP server requires the Vert.x platform HTTP server with server type '" + targetServerType + + "' but no such router was found in the registry. Enable the Camel management HTTP server " + + "(camel.management.enabled=true with camel-platform-http-main on the classpath)."); } return router; } diff --git a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerEngineTargetServerTypeTest.java b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerEngineTargetServerTypeTest.java new file mode 100644 index 0000000000000..e831a1f443243 --- /dev/null +++ b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerEngineTargetServerTypeTest.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.mcp.server.vertx; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Map; + +import io.modelcontextprotocol.client.McpClient; +import io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; +import io.modelcontextprotocol.spec.McpSchema; +import org.apache.camel.CamelContext; +import org.apache.camel.component.ai.tool.AiToolParameterHelper.ParameterDef; +import org.apache.camel.component.mcp.server.McpServerInfo; +import org.apache.camel.component.mcp.server.McpServerTool; +import org.apache.camel.component.mcp.server.McpToolCallHandler; +import org.apache.camel.component.mcp.server.McpToolCallResult; +import org.apache.camel.component.platform.http.main.MainHttpServer; +import org.apache.camel.component.platform.http.main.ManagementHttpServer; +import org.apache.camel.component.platform.http.vertx.VertxPlatformHttpRouter; +import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.test.AvailablePortFinder; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Verifies the selectable target server type (CAMEL-24353): the engine can serve the MCP endpoint on the management + * HTTP server router instead of the main one, and it is driven directly with hand-built tools — no bridge and no + * ai-tool routes — the way an alternative tool source such as the JBang dev tools (CAMEL-23853) uses it. + */ +class VertxMcpServerEngineTargetServerTypeTest { + + @Test + void testEngineServesOnManagementServerOnly() throws Exception { + int mainPort = AvailablePortFinder.getNextAvailable(); + int managementPort = AvailablePortFinder.getNextAvailable(); + + CamelContext camelContext = new DefaultCamelContext(); + ManagementHttpServer management = new ManagementHttpServer(); + VertxMcpServerEngine engine = new VertxMcpServerEngine(); + McpSyncClient client = null; + try { + MainHttpServer main = new MainHttpServer(); + main.setCamelContext(camelContext); + main.setHost("0.0.0.0"); + main.setPort(mainPort); + camelContext.addService(main); + camelContext.start(); + + management.setCamelContext(camelContext); + management.setHost("0.0.0.0"); + management.setPort(managementPort); + management.setPath("/"); + management.start(); + + engine.setCamelContext(camelContext); + engine.setTargetServerType(VertxPlatformHttpRouter.SERVER_TYPE_MANAGEMENT); + engine.initialize(new McpServerInfo("dev-tools", "1.0", "/mcp")); + engine.start(); + engine.toolAdded(tool("current_pid", "The pid of this process", + arguments -> new McpToolCallResult("pid-42", false))); + + client = McpClient.sync( + HttpClientStreamableHttpTransport.builder("http://localhost:" + managementPort).build()) + .requestTimeout(Duration.ofSeconds(10)) + .initializationTimeout(Duration.ofSeconds(10)) + .build(); + McpSchema.InitializeResult init = client.initialize(); + assertThat(init.serverInfo().name()).isEqualTo("dev-tools"); + + assertThat(client.listTools().tools()) + .extracting(McpSchema.Tool::name) + .contains("current_pid"); + + McpSchema.CallToolResult result = client.callTool(new McpSchema.CallToolRequest("current_pid", Map.of())); + assertThat(result.isError()).isNotEqualTo(Boolean.TRUE); + assertThat(result.content().toString()).contains("pid-42"); + + // the main server must not serve the management-targeted MCP endpoint + HttpResponse onMainServer = HttpClient.newHttpClient().send( + HttpRequest.newBuilder(URI.create("http://localhost:" + mainPort + "/mcp")) + .header("Content-Type", "application/json") + .header("Accept", "application/json, text/event-stream") + .POST(HttpRequest.BodyPublishers.ofString("{}")) + .build(), + HttpResponse.BodyHandlers.ofString()); + assertThat(onMainServer.statusCode()).isEqualTo(404); + } finally { + if (client != null) { + client.closeGracefully(); + } + engine.stop(); + management.stop(); + camelContext.stop(); + } + } + + @Test + void testManagementTargetWithoutManagementServerFailsFast() throws Exception { + int mainPort = AvailablePortFinder.getNextAvailable(); + + CamelContext camelContext = new DefaultCamelContext(); + VertxMcpServerEngine engine = new VertxMcpServerEngine(); + try { + MainHttpServer main = new MainHttpServer(); + main.setCamelContext(camelContext); + main.setHost("0.0.0.0"); + main.setPort(mainPort); + camelContext.addService(main); + camelContext.start(); + + engine.setCamelContext(camelContext); + engine.setTargetServerType(VertxPlatformHttpRouter.SERVER_TYPE_MANAGEMENT); + engine.initialize(new McpServerInfo("dev-tools", "1.0", "/mcp")); + + // the main server router is present but an explicit management target must never fall back to it + assertThatThrownBy(engine::start) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("management"); + } finally { + engine.stop(); + camelContext.stop(); + } + } + + private static McpServerTool tool(String name, String description, McpToolCallHandler handler) { + return new McpServerTool() { + @Override + public String name() { + return name; + } + + @Override + public String description() { + return description; + } + + @Override + public String inputSchemaJson() { + return null; + } + + @Override + public Map parameters() { + return Map.of(); + } + + @Override + public McpToolCallHandler handler() { + return handler; + } + }; + } +}