Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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() {
Expand All @@ -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;
Expand Down Expand Up @@ -154,16 +170,25 @@ public void toolRemoved(String toolName) {
}

private VertxPlatformHttpRouter lookupRouter() {
boolean mainTarget = VertxPlatformHttpRouter.SERVER_TYPE_SERVER.equals(targetServerType);
Set<VertxPlatformHttpRouter> 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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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<String, ParameterDef> parameters() {
return Map.of();
}

@Override
public McpToolCallHandler handler() {
return handler;
}
};
}
}