From 2c7c99bc9044350d1c1a1ad06823095c243876a2 Mon Sep 17 00:00:00 2001 From: croway Date: Tue, 4 Aug 2026 17:29:13 +0200 Subject: [PATCH] CAMEL-24353: camel-mcp-server - selectable target server type for VertxMcpServerEngine The engine hardwired its router lookup to the main HTTP server. The new targetServerType property (default 'server') lets it register the MCP endpoint on the management HTTP server router instead, which ManagementHttpServer already binds in the registry with SERVER_TYPE_MANAGEMENT. An explicit management target never falls back to the public server; the bare-VertxPlatformHttpServer single-router fallback is kept for the default target only. Unblocks CAMEL-23853: the JBang dev-tools MCP can drive this SDK-backed engine directly (tools built from its ToolRegistry) instead of hand-rolling the streamable HTTP protocol. Co-Authored-By: Claude Fable 5 --- .../server/vertx/VertxMcpServerEngine.java | 39 +++- ...txMcpServerEngineTargetServerTypeTest.java | 174 ++++++++++++++++++ 2 files changed, 206 insertions(+), 7 deletions(-) create mode 100644 components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerEngineTargetServerTypeTest.java 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; + } + }; + } +}