This is an automated email from the ASF dual-hosted git repository.
apupier pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 994d3ceca58a CAMEL-26897: camel-mcp-server: reduce test suite wall
time by ~55% (90s → 42s)
994d3ceca58a is described below
commit 994d3ceca58a839ece37259c0d519e48097c9950
Author: Aurélien Pupier <[email protected]>
AuthorDate: Thu Nov 12 18:19:58 2026 +0100
CAMEL-26897: camel-mcp-server: reduce test suite wall time by ~55% (90s →
42s)
Four independent improvements to camel-mcp-server and camel-mcp-server-api:
1. Reduce slow-route delays in conformance kits (camel-mcp-server-api)
slow_tool and slow_resource routes used delay(TIMEOUT * 3) = 6 s to
exceed the 2 s tool/resource timeout. When CamelContext shuts down,
DefaultShutdownStrategy waits up to 10 s for the still-running inflight
exchange. Changed to delay(TIMEOUT + 200) = 2.2 s: the route still
exceeds the timeout (so testCallToolTimeout / testReadResourceTimeout
pass), but the residual inflight window shrinks from ~4 s to ≤200 ms,
saving ~4 s of shutdown wait per conformance test class.
2. Shorten Awaitility 'during' windows in VertxMcpSessionEvictionTest
postOnlySessionSurvivesKeepAliveUntilIdle and activeSessionSurvivesIdle
Eviction used during(2 s).atMost(3 s) to assert a session stays alive.
500 ms is enough to confirm no immediate eviction; the idle-TTL is 10 s /
2 s — well beyond that window. Saves ~3 s total.
3. Enable parallel test-class execution (surefire + failsafe)
Every test class creates its own Vert.x server on a fresh
AvailablePortFinder port, its own CamelContext, and its own subprocess
(stdio tests) — no cross-class shared state. Added to pom.xml:
- camel.surefire.parallel=true with factor=1 (JUnit 5 concurrent classes)
- maven-failsafe-plugin configurationParameters block with the same
parallel settings (the parent POM defines the variables but never wires
them into failsafe's configurationParameters)
This turns the serial 19-class unit queue and 5-class IT queue into
fan-outs, letting the Keycloak container boot overlap with the other ITs.
4. Safe port allocation with AvailablePortFinder.find()
All tests that held a port as a static-final field or instance field
initialised with the deprecated getNextAvailable() — which releases the
reservation immediately, before the server binds — are migrated to
AvailablePortFinder.find(). Following the pattern from commit da3c533,
the Port object is allocated as late as possible (first line of
@BeforeAll, or inside createCamelContext()/customizeCamelContext() just
before the server is added to the context) and released in the matching
teardown (@AfterAll or @AfterEach), minimising the TOCTOU window between
probe and bind.
Benchmark (same machine, Keycloak container warm):
mvn verify before: 1m 30s after: 42s (-53%)
mvn test (units) before: ~27s after: ~12s (-56%)
Co-authored-by: IBM Bob 2.2.1 <[email protected]>
Signed-off-by: Aurélien Pupier <[email protected]>
---
.../McpServerConformanceTestSupport.java | 2 +-
.../McpServerResourceConformanceTestSupport.java | 2 +-
components/camel-ai/camel-mcp-server/pom.xml | 31 +++
.../main/McpServerMainAuthenticationTest.java | 8 +-
.../mcp/server/main/McpServerMainMetadataTest.java | 103 +++++----
.../server/main/McpServerMainOAuthKeycloakIT.java | 254 ++++++++++----------
.../mcp/server/main/McpServerMainOAuthTest.java | 56 +++--
.../server/main/McpServerMainPropertiesTest.java | 165 ++++++-------
.../McpServerSessionEvictionPropertiesTest.java | 55 ++---
.../server/vertx/VertxMcpErrorResponseTest.java | 7 +-
.../mcp/server/vertx/VertxMcpOriginTest.java | 9 +-
.../vertx/VertxMcpServerConformanceTest.java | 16 +-
.../VertxMcpServerEngineTargetServerTypeTest.java | 170 +++++++-------
.../vertx/VertxMcpServerOutputSchemaTest.java | 93 ++++----
.../vertx/VertxMcpServerPromptConformanceTest.java | 16 +-
.../VertxMcpServerResourceConformanceTest.java | 16 +-
.../vertx/VertxMcpServerToolAnnotationsTest.java | 257 +++++++++++----------
.../server/vertx/VertxMcpSessionEvictionTest.java | 89 +++----
.../MainHttpServerMcpConformanceIT.java | 16 +-
.../MainHttpServerMcpResourceConformanceIT.java | 16 +-
.../vertx/integration/McpServerOpenAIAgentIT.java | 16 +-
.../integration/OfficialMcpConformanceIT.java | 16 +-
22 files changed, 802 insertions(+), 611 deletions(-)
diff --git
a/components/camel-ai/camel-mcp-server-api/src/test/java/org/apache/camel/component/mcp/server/conformance/McpServerConformanceTestSupport.java
b/components/camel-ai/camel-mcp-server-api/src/test/java/org/apache/camel/component/mcp/server/conformance/McpServerConformanceTestSupport.java
index 119f309b258f..cefe60cbda61 100644
---
a/components/camel-ai/camel-mcp-server-api/src/test/java/org/apache/camel/component/mcp/server/conformance/McpServerConformanceTestSupport.java
+++
b/components/camel-ai/camel-mcp-server-api/src/test/java/org/apache/camel/component/mcp/server/conformance/McpServerConformanceTestSupport.java
@@ -102,7 +102,7 @@ public abstract class McpServerConformanceTestSupport
extends CamelTestSupport {
from("ai-tool:slow_tool?tags=" + CONFORMANCE_TAG +
"&description=Exceeds the tool timeout")
.routeId("slow-tool-route")
- .delay(TOOL_TIMEOUT_MILLIS * 3)
+ .delay(TOOL_TIMEOUT_MILLIS + 1000)
.setBody(constant("done"));
from("ai-tool:hidden_tool?description=Untagged tool, must not
be exposed")
diff --git
a/components/camel-ai/camel-mcp-server-api/src/test/java/org/apache/camel/component/mcp/server/conformance/McpServerResourceConformanceTestSupport.java
b/components/camel-ai/camel-mcp-server-api/src/test/java/org/apache/camel/component/mcp/server/conformance/McpServerResourceConformanceTestSupport.java
index 46e2dc1fc09a..e349788671d0 100644
---
a/components/camel-ai/camel-mcp-server-api/src/test/java/org/apache/camel/component/mcp/server/conformance/McpServerResourceConformanceTestSupport.java
+++
b/components/camel-ai/camel-mcp-server-api/src/test/java/org/apache/camel/component/mcp/server/conformance/McpServerResourceConformanceTestSupport.java
@@ -112,7 +112,7 @@ public abstract class
McpServerResourceConformanceTestSupport extends CamelTestS
from("ai-resource:slow_resource?resourceUri=camel:///slow&tags=" +
CONFORMANCE_TAG
+ "&description=Exceeds the resource timeout")
.routeId("slow-resource-route")
- .delay(RESOURCE_TIMEOUT_MILLIS * 3)
+ .delay(RESOURCE_TIMEOUT_MILLIS + 1000)
.setBody(constant("done"));
from("ai-resource:hidden_resource?resourceUri=camel:///hidden"
diff --git a/components/camel-ai/camel-mcp-server/pom.xml
b/components/camel-ai/camel-mcp-server/pom.xml
index b80aa4f3f3e8..b0cecad172bd 100644
--- a/components/camel-ai/camel-mcp-server/pom.xml
+++ b/components/camel-ai/camel-mcp-server/pom.xml
@@ -37,6 +37,13 @@
<title>MCP Server</title>
<supportLevel>Preview</supportLevel>
<failsafe.rerunFailingTestsCount>3</failsafe.rerunFailingTestsCount>
+ <!-- Run test classes in parallel: each class owns its own Vert.x
server on a
+ dedicated port so there is no shared state between test classes.
+ Use -Dcamel.surefire.parallel=false or
-Dcamel.failsafe.parallel=false to disable. -->
+ <camel.surefire.parallel>true</camel.surefire.parallel>
+ <camel.surefire.parallel.factor>1</camel.surefire.parallel.factor>
+ <camel.failsafe.parallel>true</camel.failsafe.parallel>
+ <camel.failsafe.parallel.factor>1</camel.failsafe.parallel.factor>
</properties>
<dependencies>
@@ -154,6 +161,30 @@
</dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ <configuration combine.children="merge">
+ <!-- Run IT classes in parallel: each class owns its own
Vert.x server on a
+ dedicated port (or its own subprocess), so there is
no shared state.
+ Use -Dcamel.failsafe.parallel=false to disable for
debugging. -->
+ <properties>
+ <configurationParameters>
+ junit.jupiter.execution.parallel.enabled =
${camel.failsafe.parallel}
+ junit.jupiter.execution.parallel.mode.default =
same_thread
+
junit.jupiter.execution.parallel.mode.classes.default = concurrent
+ junit.jupiter.execution.parallel.config.strategy =
${camel.failsafe.parallel.strategy}
+
junit.jupiter.execution.parallel.config.dynamic.factor =
${camel.failsafe.parallel.factor}
+
junit.jupiter.execution.parallel.config.executor-service = FORK_JOIN_POOL
+ </configurationParameters>
+ </properties>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
<profiles>
<!--
Official MCP conformance profile (-Poc).
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainAuthenticationTest.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainAuthenticationTest.java
index c25b8289ef11..62e96e310e84 100644
---
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainAuthenticationTest.java
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainAuthenticationTest.java
@@ -56,11 +56,14 @@ class McpServerMainAuthenticationTest {
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05",
"capabilities":{},"clientInfo":{"name":"test","version":"1"}}}""";
- private static final int PORT = AvailablePortFinder.getNextAvailable();
+ private static AvailablePortFinder.Port portRef;
+ private static int PORT;
private static Main main;
@BeforeAll
static void startMain() {
+ portRef = AvailablePortFinder.find();
+ PORT = portRef.getPort();
main = new Main();
main.configure().addRoutesBuilder(new RouteBuilder() {
@Override
@@ -106,6 +109,9 @@ class McpServerMainAuthenticationTest {
if (main != null) {
main.stop();
}
+ if (portRef != null) {
+ portRef.release();
+ }
}
@Test
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainMetadataTest.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainMetadataTest.java
index 506d9c348b99..03228e8c96bb 100644
---
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainMetadataTest.java
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainMetadataTest.java
@@ -26,6 +26,7 @@ import io.modelcontextprotocol.spec.McpSchema;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.main.Main;
import org.apache.camel.test.AvailablePortFinder;
+import org.apache.camel.test.AvailablePortFinder.Port;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -37,62 +38,64 @@ class McpServerMainMetadataTest {
@Test
void testMcpServerMetadataFromProperties() throws Exception {
- int port = AvailablePortFinder.getNextAvailable();
+ try (Port portHolder = AvailablePortFinder.find()) {
+ int port = portHolder.getPort();
- Main main = new Main();
- main.configure().addRoutesBuilder(new RouteBuilder() {
- @Override
- public void configure() {
- from("ai-tool:say_hello?tags=crm&description=Say hello")
- .setBody(constant("Hello"));
- }
- });
- main.addInitialProperty("camel.server.enabled", "true");
- main.addInitialProperty("camel.server.port", String.valueOf(port));
- main.addInitialProperty("camel.server.mcp-enabled", "true");
- main.addInitialProperty("camel.server.mcp-tags", "crm");
- main.addInitialProperty("camel.server.mcp-server-name",
"wanaku-camel-mcp");
- main.addInitialProperty("camel.server.mcp-server-title", "Wanaku Camel
MCP");
- main.addInitialProperty("camel.server.mcp-server-description",
"Integration tools for Wanaku");
- main.addInitialProperty("camel.server.mcp-server-website-url",
"https://camel.apache.org/");
- main.addInitialProperty("camel.server.mcp-instructions", "Use these
tools to operate the integration.");
- main.addInitialProperty("camel.server.mcp-server-icons", """
-
[{"src":"https://example.com/icon.png","mimeType":"image/png","sizes":["48x48"],"theme":"light"}]
- """);
- main.start();
+ Main main = new Main();
+ main.configure().addRoutesBuilder(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("ai-tool:say_hello?tags=crm&description=Say hello")
+ .setBody(constant("Hello"));
+ }
+ });
+ main.addInitialProperty("camel.server.enabled", "true");
+ main.addInitialProperty("camel.server.port", String.valueOf(port));
+ main.addInitialProperty("camel.server.mcp-enabled", "true");
+ main.addInitialProperty("camel.server.mcp-tags", "crm");
+ main.addInitialProperty("camel.server.mcp-server-name",
"wanaku-camel-mcp");
+ main.addInitialProperty("camel.server.mcp-server-title", "Wanaku
Camel MCP");
+ main.addInitialProperty("camel.server.mcp-server-description",
"Integration tools for Wanaku");
+ main.addInitialProperty("camel.server.mcp-server-website-url",
"https://camel.apache.org/");
+ main.addInitialProperty("camel.server.mcp-instructions", "Use
these tools to operate the integration.");
+ main.addInitialProperty("camel.server.mcp-server-icons", """
+
[{"src":"https://example.com/icon.png","mimeType":"image/png","sizes":["48x48"],"theme":"light"}]
+ """);
+ main.start();
- McpSyncClient client = null;
- try {
- client =
McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" +
port).build())
- .requestTimeout(Duration.ofSeconds(10))
- .initializationTimeout(Duration.ofSeconds(10))
- .build();
+ McpSyncClient client = null;
+ try {
+ client =
McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" +
port).build())
+ .requestTimeout(Duration.ofSeconds(10))
+ .initializationTimeout(Duration.ofSeconds(10))
+ .build();
- McpSchema.InitializeResult init = client.initialize();
- McpSchema.Implementation serverInfo = init.serverInfo();
+ McpSchema.InitializeResult init = client.initialize();
+ McpSchema.Implementation serverInfo = init.serverInfo();
- assertThat(serverInfo.name()).isEqualTo("wanaku-camel-mcp");
- assertThat(serverInfo.title()).isEqualTo("Wanaku Camel MCP");
- assertThat(serverInfo.description()).isEqualTo("Integration tools
for Wanaku");
-
assertThat(serverInfo.websiteUrl()).isEqualTo("https://camel.apache.org/");
- assertThat(serverInfo.version()).isNotBlank();
- assertThat(serverInfo.icons()).hasSize(1);
-
assertThat(serverInfo.icons().get(0).src()).isEqualTo("https://example.com/icon.png");
-
assertThat(serverInfo.icons().get(0).mimeType()).isEqualTo("image/png");
-
assertThat(serverInfo.icons().get(0).sizes()).containsExactly("48x48");
- assertThat(serverInfo.icons().get(0).theme()).isEqualTo("light");
- assertThat(init.instructions()).isEqualTo("Use these tools to
operate the integration.");
+ assertThat(serverInfo.name()).isEqualTo("wanaku-camel-mcp");
+ assertThat(serverInfo.title()).isEqualTo("Wanaku Camel MCP");
+ assertThat(serverInfo.description()).isEqualTo("Integration
tools for Wanaku");
+
assertThat(serverInfo.websiteUrl()).isEqualTo("https://camel.apache.org/");
+ assertThat(serverInfo.version()).isNotBlank();
+ assertThat(serverInfo.icons()).hasSize(1);
+
assertThat(serverInfo.icons().get(0).src()).isEqualTo("https://example.com/icon.png");
+
assertThat(serverInfo.icons().get(0).mimeType()).isEqualTo("image/png");
+
assertThat(serverInfo.icons().get(0).sizes()).containsExactly("48x48");
+
assertThat(serverInfo.icons().get(0).theme()).isEqualTo("light");
+ assertThat(init.instructions()).isEqualTo("Use these tools to
operate the integration.");
- assertThat(client.listTools().tools())
- .extracting(McpSchema.Tool::name)
- .contains("say_hello");
- assertThat(client.callTool(new
McpSchema.CallToolRequest("say_hello", Map.of())).isError())
- .isNotEqualTo(Boolean.TRUE);
- } finally {
- if (client != null) {
- client.closeGracefully();
+ assertThat(client.listTools().tools())
+ .extracting(McpSchema.Tool::name)
+ .contains("say_hello");
+ assertThat(client.callTool(new
McpSchema.CallToolRequest("say_hello", Map.of())).isError())
+ .isNotEqualTo(Boolean.TRUE);
+ } finally {
+ if (client != null) {
+ client.closeGracefully();
+ }
+ main.stop();
}
- main.stop();
}
}
}
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainOAuthKeycloakIT.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainOAuthKeycloakIT.java
index 061c1e8cb921..c44668c34629 100644
---
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainOAuthKeycloakIT.java
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainOAuthKeycloakIT.java
@@ -39,6 +39,7 @@ import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mcp.server.McpToolCallContext;
import org.apache.camel.main.Main;
import org.apache.camel.test.AvailablePortFinder;
+import org.apache.camel.test.AvailablePortFinder.Port;
import org.apache.camel.test.infra.keycloak.services.KeycloakService;
import org.apache.camel.test.infra.keycloak.services.KeycloakServiceFactory;
import org.awaitility.Awaitility;
@@ -75,8 +76,9 @@ class McpServerMainOAuthKeycloakIT {
@RegisterExtension
static KeycloakService keycloakService =
KeycloakServiceFactory.createSingletonService();
+ private static AvailablePortFinder.Port portRef;
private static final HttpClient HTTP = HttpClient.newHttpClient();
- private static final int PORT = AvailablePortFinder.getNextAvailable();
+ private static int PORT;
private static final int MCP_TIMEOUT_SECONDS = 15;
private static final String REALM = "mcp-oauth-" +
UUID.randomUUID().toString().substring(0, 8);
private static final String CLIENT_ID = "mcp-client";
@@ -105,6 +107,8 @@ class McpServerMainOAuthKeycloakIT {
@BeforeAll
static void startKeycloakAndMcpServer() throws Exception {
+ portRef = AvailablePortFinder.find();
+ PORT = portRef.getPort();
awaitKeycloakReady();
adminClient = keycloakService.getKeycloakAdminClient();
createRealm();
@@ -157,6 +161,9 @@ class McpServerMainOAuthKeycloakIT {
if (main != null) {
main.stop();
}
+ if (portRef != null) {
+ portRef.release();
+ }
if (adminClient != null) {
try {
adminClient.realm(REALM).remove();
@@ -307,73 +314,79 @@ class McpServerMainOAuthKeycloakIT {
@Test
void tokenWithUnexpectedIssuerIsRejected() throws Exception {
String token = accessToken(CLIENT_ID, clientSecret, "mcp:read
mcp:write");
- int wrongIssuerPort = AvailablePortFinder.getNextAvailable();
- Main wrongIssuer = newMain(wrongIssuerPort);
- wrongIssuer.addInitialProperty("camel.server.mcp-oauth-profile",
"wrong-issuer");
- wrongIssuer.addInitialProperty("camel.server.mcp-required-scopes",
"mcp:read,mcp:write");
-
wrongIssuer.addInitialProperty("camel.oauth.wrong-issuer.jwks-endpoint",
- issuer + "/protocol/openid-connect/certs");
-
wrongIssuer.addInitialProperty("camel.oauth.wrong-issuer.expected-issuer",
issuer + "/not-the-issuer");
-
wrongIssuer.addInitialProperty("camel.oauth.wrong-issuer.expected-audience",
CLIENT_ID);
-
wrongIssuer.addInitialProperty("camel.oauth.wrong-issuer.allow-insecure-http",
"true");
+ try (Port wrongIssuerPortHolder = AvailablePortFinder.find()) {
+ int wrongIssuerPort = wrongIssuerPortHolder.getPort();
+ Main wrongIssuer = newMain(wrongIssuerPort);
+ wrongIssuer.addInitialProperty("camel.server.mcp-oauth-profile",
"wrong-issuer");
+ wrongIssuer.addInitialProperty("camel.server.mcp-required-scopes",
"mcp:read,mcp:write");
+
wrongIssuer.addInitialProperty("camel.oauth.wrong-issuer.jwks-endpoint",
+ issuer + "/protocol/openid-connect/certs");
+
wrongIssuer.addInitialProperty("camel.oauth.wrong-issuer.expected-issuer",
issuer + "/not-the-issuer");
+
wrongIssuer.addInitialProperty("camel.oauth.wrong-issuer.expected-audience",
CLIENT_ID);
+
wrongIssuer.addInitialProperty("camel.oauth.wrong-issuer.allow-insecure-http",
"true");
- try {
- wrongIssuer.start();
- HttpResponse<String> response = post(wrongIssuerPort, token);
- assertThat(response.statusCode()).isEqualTo(401);
- assertThat(response.headers().firstValue("WWW-Authenticate"))
- .hasValueSatisfying(value ->
assertThat(value).contains("error=\"invalid_token\""));
- } finally {
- wrongIssuer.stop();
+ try {
+ wrongIssuer.start();
+ HttpResponse<String> response = post(wrongIssuerPort, token);
+ assertThat(response.statusCode()).isEqualTo(401);
+ assertThat(response.headers().firstValue("WWW-Authenticate"))
+ .hasValueSatisfying(value ->
assertThat(value).contains("error=\"invalid_token\""));
+ } finally {
+ wrongIssuer.stop();
+ }
}
}
@Test
void tokenWithDisallowedAlgorithmIsRejected() throws Exception {
String token = accessToken(CLIENT_ID, clientSecret, "mcp:read
mcp:write");
- int wrongAlgorithmPort = AvailablePortFinder.getNextAvailable();
- Main wrongAlgorithm = newMain(wrongAlgorithmPort);
- wrongAlgorithm.addInitialProperty("camel.server.mcp-oauth-profile",
"wrong-algorithm");
- wrongAlgorithm.addInitialProperty("camel.server.mcp-required-scopes",
"mcp:read,mcp:write");
-
wrongAlgorithm.addInitialProperty("camel.oauth.wrong-algorithm.jwks-endpoint",
- issuer + "/protocol/openid-connect/certs");
-
wrongAlgorithm.addInitialProperty("camel.oauth.wrong-algorithm.expected-issuer",
issuer);
-
wrongAlgorithm.addInitialProperty("camel.oauth.wrong-algorithm.expected-audience",
CLIENT_ID);
-
wrongAlgorithm.addInitialProperty("camel.oauth.wrong-algorithm.allowed-jws-algorithms",
"HS256");
-
wrongAlgorithm.addInitialProperty("camel.oauth.wrong-algorithm.allow-insecure-http",
"true");
+ try (Port wrongAlgorithmPortHolder = AvailablePortFinder.find()) {
+ int wrongAlgorithmPort = wrongAlgorithmPortHolder.getPort();
+ Main wrongAlgorithm = newMain(wrongAlgorithmPort);
+
wrongAlgorithm.addInitialProperty("camel.server.mcp-oauth-profile",
"wrong-algorithm");
+
wrongAlgorithm.addInitialProperty("camel.server.mcp-required-scopes",
"mcp:read,mcp:write");
+
wrongAlgorithm.addInitialProperty("camel.oauth.wrong-algorithm.jwks-endpoint",
+ issuer + "/protocol/openid-connect/certs");
+
wrongAlgorithm.addInitialProperty("camel.oauth.wrong-algorithm.expected-issuer",
issuer);
+
wrongAlgorithm.addInitialProperty("camel.oauth.wrong-algorithm.expected-audience",
CLIENT_ID);
+
wrongAlgorithm.addInitialProperty("camel.oauth.wrong-algorithm.allowed-jws-algorithms",
"HS256");
+
wrongAlgorithm.addInitialProperty("camel.oauth.wrong-algorithm.allow-insecure-http",
"true");
- try {
- wrongAlgorithm.start();
- HttpResponse<String> response = post(wrongAlgorithmPort, token);
+ try {
+ wrongAlgorithm.start();
+ HttpResponse<String> response = post(wrongAlgorithmPort,
token);
- assertThat(response.statusCode()).isEqualTo(401);
- assertThat(response.headers().firstValue("WWW-Authenticate"))
- .hasValueSatisfying(value ->
assertThat(value).contains("error=\"invalid_token\""));
- } finally {
- wrongAlgorithm.stop();
+ assertThat(response.statusCode()).isEqualTo(401);
+ assertThat(response.headers().firstValue("WWW-Authenticate"))
+ .hasValueSatisfying(value ->
assertThat(value).contains("error=\"invalid_token\""));
+ } finally {
+ wrongAlgorithm.stop();
+ }
}
}
@Test
void protectedResourceMetadataIsNotPublishedWithoutOAuthProfile() throws
Exception {
- int unprotectedPort = AvailablePortFinder.getNextAvailable();
- Main unprotected = new Main();
- unprotected.addInitialProperty("camel.server.enabled", "true");
- unprotected.addInitialProperty("camel.server.port",
String.valueOf(unprotectedPort));
- unprotected.addInitialProperty("camel.server.mcp-enabled", "true");
- unprotected.addInitialProperty("camel.server.mcp-tags", "oauth");
+ try (Port unprotectedPortHolder = AvailablePortFinder.find()) {
+ int unprotectedPort = unprotectedPortHolder.getPort();
+ Main unprotected = new Main();
+ unprotected.addInitialProperty("camel.server.enabled", "true");
+ unprotected.addInitialProperty("camel.server.port",
String.valueOf(unprotectedPort));
+ unprotected.addInitialProperty("camel.server.mcp-enabled", "true");
+ unprotected.addInitialProperty("camel.server.mcp-tags", "oauth");
- try {
- unprotected.start();
- HttpRequest request =
HttpRequest.newBuilder(URI.create("http://localhost:" + unprotectedPort
- +
"/.well-known/oauth-protected-resource/mcp"))
- .timeout(Duration.ofSeconds(MCP_TIMEOUT_SECONDS))
- .GET().build();
- HttpResponse<String> response = HTTP.send(request,
HttpResponse.BodyHandlers.ofString());
-
- assertThat(response.statusCode()).isEqualTo(404);
- } finally {
- unprotected.stop();
+ try {
+ unprotected.start();
+ HttpRequest request =
HttpRequest.newBuilder(URI.create("http://localhost:" + unprotectedPort
+ +
"/.well-known/oauth-protected-resource/mcp"))
+ .timeout(Duration.ofSeconds(MCP_TIMEOUT_SECONDS))
+ .GET().build();
+ HttpResponse<String> response = HTTP.send(request,
HttpResponse.BodyHandlers.ofString());
+
+ assertThat(response.statusCode()).isEqualTo(404);
+ } finally {
+ unprotected.stop();
+ }
}
}
@@ -400,90 +413,99 @@ class McpServerMainOAuthKeycloakIT {
@Test
void incompleteOAuthProfileFailsDuringStartup() {
- Main invalid = newMain(AvailablePortFinder.getNextAvailable());
- invalid.addInitialProperty("camel.server.mcp-oauth-profile",
"incomplete");
- invalid.addInitialProperty("camel.server.mcp-resource-uri",
MCP_RESOURCE_URI);
- invalid.addInitialProperty("camel.oauth.incomplete.expected-audience",
CLIENT_ID);
+ try (Port invalidPortHolder = AvailablePortFinder.find()) {
+ Main invalid = newMain(invalidPortHolder.getPort());
+ invalid.addInitialProperty("camel.server.mcp-oauth-profile",
"incomplete");
+ invalid.addInitialProperty("camel.server.mcp-resource-uri",
MCP_RESOURCE_URI);
+
invalid.addInitialProperty("camel.oauth.incomplete.expected-audience",
CLIENT_ID);
- try {
- assertThatThrownBy(invalid::start)
- .hasStackTraceContaining("No jwks-endpoint,
introspection-endpoint, or base-uri");
- } finally {
- invalid.stop();
+ try {
+ assertThatThrownBy(invalid::start)
+ .hasStackTraceContaining("No jwks-endpoint,
introspection-endpoint, or base-uri");
+ } finally {
+ invalid.stop();
+ }
}
}
@Test
void unreachableIssuerDiscoveryFailsDuringStartup() {
- Main invalid = newMain(AvailablePortFinder.getNextAvailable());
- invalid.addInitialProperty("camel.server.mcp-oauth-profile",
"unreachable");
- invalid.addInitialProperty("camel.server.mcp-resource-uri",
MCP_RESOURCE_URI);
- invalid.addInitialProperty("camel.oauth.unreachable.base-uri",
"http://127.0.0.1:1/realms/unreachable");
-
invalid.addInitialProperty("camel.oauth.unreachable.expected-audience",
CLIENT_ID);
-
invalid.addInitialProperty("camel.oauth.unreachable.allow-insecure-http",
"true");
+ try (Port invalidPortHolder = AvailablePortFinder.find()) {
+ Main invalid = newMain(invalidPortHolder.getPort());
+ invalid.addInitialProperty("camel.server.mcp-oauth-profile",
"unreachable");
+ invalid.addInitialProperty("camel.server.mcp-resource-uri",
MCP_RESOURCE_URI);
+ invalid.addInitialProperty("camel.oauth.unreachable.base-uri",
"http://127.0.0.1:1/realms/unreachable");
+
invalid.addInitialProperty("camel.oauth.unreachable.expected-audience",
CLIENT_ID);
+
invalid.addInitialProperty("camel.oauth.unreachable.allow-insecure-http",
"true");
- try {
- assertThatThrownBy(invalid::start)
- .hasStackTraceContaining("Failed to discover OIDC
endpoints");
- } finally {
- invalid.stop();
+ try {
+ assertThatThrownBy(invalid::start)
+ .hasStackTraceContaining("Failed to discover OIDC
endpoints");
+ } finally {
+ invalid.stop();
+ }
}
}
@Test
void keycloakIntrospectionAcceptsActiveTokenAndRejectsInactiveToken()
throws Exception {
- int introspectionPort = AvailablePortFinder.getNextAvailable();
- Main introspection = newMain(introspectionPort);
- introspection.addInitialProperty("camel.server.mcp-oauth-profile",
"keycloak-introspection");
- introspection.addInitialProperty("camel.server.mcp-required-scopes",
"mcp:read,mcp:write");
-
introspection.addInitialProperty("camel.oauth.keycloak-introspection.introspection-endpoint",
- issuer + "/protocol/openid-connect/token/introspect");
-
introspection.addInitialProperty("camel.oauth.keycloak-introspection.introspection-client-id",
CLIENT_ID);
-
introspection.addInitialProperty("camel.oauth.keycloak-introspection.introspection-client-secret",
clientSecret);
-
introspection.addInitialProperty("camel.oauth.keycloak-introspection.expected-issuer",
issuer);
-
introspection.addInitialProperty("camel.oauth.keycloak-introspection.expected-audience",
CLIENT_ID);
-
introspection.addInitialProperty("camel.oauth.keycloak-introspection.allow-insecure-http",
"true");
+ try (Port introspectionPortHolder = AvailablePortFinder.find()) {
+ int introspectionPort = introspectionPortHolder.getPort();
+ Main introspection = newMain(introspectionPort);
+ introspection.addInitialProperty("camel.server.mcp-oauth-profile",
"keycloak-introspection");
+
introspection.addInitialProperty("camel.server.mcp-required-scopes",
"mcp:read,mcp:write");
+
introspection.addInitialProperty("camel.oauth.keycloak-introspection.introspection-endpoint",
+ issuer + "/protocol/openid-connect/token/introspect");
+
introspection.addInitialProperty("camel.oauth.keycloak-introspection.introspection-client-id",
CLIENT_ID);
+
introspection.addInitialProperty("camel.oauth.keycloak-introspection.introspection-client-secret",
clientSecret);
+
introspection.addInitialProperty("camel.oauth.keycloak-introspection.expected-issuer",
issuer);
+
introspection.addInitialProperty("camel.oauth.keycloak-introspection.expected-audience",
CLIENT_ID);
+
introspection.addInitialProperty("camel.oauth.keycloak-introspection.allow-insecure-http",
"true");
- try {
- introspection.start();
+ try {
+ introspection.start();
- String activeToken = accessToken(CLIENT_ID, clientSecret,
"mcp:read mcp:write");
- HttpResponse<String> activeResponse = post(introspectionPort,
activeToken);
- assertThat(activeResponse.statusCode()).isEqualTo(200);
+ String activeToken = accessToken(CLIENT_ID, clientSecret,
"mcp:read mcp:write");
+ HttpResponse<String> activeResponse = post(introspectionPort,
activeToken);
+ assertThat(activeResponse.statusCode()).isEqualTo(200);
- HttpResponse<String> inactiveResponse = post(introspectionPort,
"not-a-valid-keycloak-token");
- assertThat(inactiveResponse.statusCode()).isEqualTo(401);
-
assertThat(inactiveResponse.headers().firstValue("WWW-Authenticate"))
- .hasValueSatisfying(value ->
assertThat(value).contains("error=\"invalid_token\""));
- } finally {
- introspection.stop();
+ HttpResponse<String> inactiveResponse =
post(introspectionPort, "not-a-valid-keycloak-token");
+ assertThat(inactiveResponse.statusCode()).isEqualTo(401);
+
assertThat(inactiveResponse.headers().firstValue("WWW-Authenticate"))
+ .hasValueSatisfying(value ->
assertThat(value).contains("error=\"invalid_token\""));
+ } finally {
+ introspection.stop();
+ }
}
}
@Test
void
unreachableIntrospectionEndpointReturns503WithoutLeakingTokenOrConfiguration()
throws Exception {
- int unavailablePort = AvailablePortFinder.getNextAvailable();
- String introspectionEndpoint = "http://127.0.0.1:" + unavailablePort +
"/introspect";
- String token = "opaque-test-token";
- int offlineServerPort = AvailablePortFinder.getNextAvailable();
- Main offline = newMain(offlineServerPort);
- offline.addInitialProperty("camel.server.mcp-oauth-profile",
"offline");
- offline.addInitialProperty("camel.server.mcp-resource-uri",
MCP_RESOURCE_URI);
-
offline.addInitialProperty("camel.oauth.offline.introspection-endpoint",
introspectionEndpoint);
-
offline.addInitialProperty("camel.oauth.offline.introspection-client-id",
"offline-client");
-
offline.addInitialProperty("camel.oauth.offline.introspection-client-secret",
"offline-secret");
- offline.addInitialProperty("camel.oauth.offline.expected-issuer",
"https://issuer.example.test");
- offline.addInitialProperty("camel.oauth.offline.expected-audience",
"mcp-server");
- offline.addInitialProperty("camel.oauth.offline.allow-insecure-http",
"true");
+ try (Port unavailablePortHolder = AvailablePortFinder.find();
+ Port offlineServerPortHolder = AvailablePortFinder.find()) {
+ int unavailablePort = unavailablePortHolder.getPort();
+ String introspectionEndpoint = "http://127.0.0.1:" +
unavailablePort + "/introspect";
+ String token = "opaque-test-token";
+ int offlineServerPort = offlineServerPortHolder.getPort();
+ Main offline = newMain(offlineServerPort);
+ offline.addInitialProperty("camel.server.mcp-oauth-profile",
"offline");
+ offline.addInitialProperty("camel.server.mcp-resource-uri",
MCP_RESOURCE_URI);
+
offline.addInitialProperty("camel.oauth.offline.introspection-endpoint",
introspectionEndpoint);
+
offline.addInitialProperty("camel.oauth.offline.introspection-client-id",
"offline-client");
+
offline.addInitialProperty("camel.oauth.offline.introspection-client-secret",
"offline-secret");
+ offline.addInitialProperty("camel.oauth.offline.expected-issuer",
"https://issuer.example.test");
+
offline.addInitialProperty("camel.oauth.offline.expected-audience",
"mcp-server");
+
offline.addInitialProperty("camel.oauth.offline.allow-insecure-http", "true");
- try {
- offline.start();
- HttpResponse<String> response = post(offlineServerPort, token);
- assertThat(response.statusCode()).isEqualTo(503);
- assertThat(response.body()).isEqualTo("Service Unavailable")
- .doesNotContain(token, "offline-secret",
introspectionEndpoint);
- } finally {
- offline.stop();
+ try {
+ offline.start();
+ HttpResponse<String> response = post(offlineServerPort, token);
+ assertThat(response.statusCode()).isEqualTo(503);
+ assertThat(response.body()).isEqualTo("Service Unavailable")
+ .doesNotContain(token, "offline-secret",
introspectionEndpoint);
+ } finally {
+ offline.stop();
+ }
}
}
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainOAuthTest.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainOAuthTest.java
index 70c78ab3a183..9ab628a1b406 100644
---
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainOAuthTest.java
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainOAuthTest.java
@@ -44,6 +44,7 @@ import org.apache.camel.spi.OAuthTokenValidationConfig;
import org.apache.camel.spi.OAuthTokenValidationFactory;
import org.apache.camel.spi.OAuthTokenValidationResult;
import org.apache.camel.test.AvailablePortFinder;
+import org.apache.camel.test.AvailablePortFinder.Port;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -58,15 +59,12 @@ import static
org.assertj.core.api.Assertions.assertThatThrownBy;
class McpServerMainOAuthTest {
private static final HttpClient HTTP = HttpClient.newHttpClient();
- private static final int PORT = AvailablePortFinder.getNextAvailable();
- private static final String RESOURCE_URI = "https://localhost:" + PORT +
"/mcp";
- private static final String RESOURCE_METADATA_URI
- = "https://localhost:" + PORT +
"/.well-known/oauth-protected-resource/mcp";
- private static final String MISSING_TOKEN_CHALLENGE
- = "Bearer resource_metadata=\"" + RESOURCE_METADATA_URI + "\",
scope=\"mcp:read mcp:write\"";
- private static final String INVALID_TOKEN_CHALLENGE
- = "Bearer error=\"invalid_token\", resource_metadata=\"" +
RESOURCE_METADATA_URI
- + "\", scope=\"mcp:read mcp:write\"";
+ private static AvailablePortFinder.Port portRef;
+ private static int PORT;
+ private static String RESOURCE_URI;
+ private static String RESOURCE_METADATA_URI;
+ private static String MISSING_TOKEN_CHALLENGE;
+ private static String INVALID_TOKEN_CHALLENGE;
private static final String INITIALIZE_REQUEST = """
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18",
"capabilities":{},"clientInfo":{"name":"oauth-test","version":"1"}}}
@@ -76,6 +74,13 @@ class McpServerMainOAuthTest {
@BeforeAll
static void startMain() throws Exception {
+ portRef = AvailablePortFinder.find();
+ PORT = portRef.getPort();
+ RESOURCE_URI = "https://localhost:" + PORT + "/mcp";
+ RESOURCE_METADATA_URI = "https://localhost:" + PORT +
"/.well-known/oauth-protected-resource/mcp";
+ MISSING_TOKEN_CHALLENGE = "Bearer resource_metadata=\"" +
RESOURCE_METADATA_URI + "\", scope=\"mcp:read mcp:write\"";
+ INVALID_TOKEN_CHALLENGE = "Bearer error=\"invalid_token\",
resource_metadata=\"" + RESOURCE_METADATA_URI
+ + "\", scope=\"mcp:read mcp:write\"";
main = new Main();
main.configure().addRoutesBuilder(new RouteBuilder() {
@Override
@@ -116,6 +121,9 @@ class McpServerMainOAuthTest {
if (main != null) {
main.stop();
}
+ if (portRef != null) {
+ portRef.release();
+ }
}
@Test
@@ -212,20 +220,22 @@ class McpServerMainOAuthTest {
@Test
void rejectsNonRootHttpContextPathWhenOAuthEngineStarts() {
- Main nonRootMain = new Main();
- nonRootMain.addInitialProperty("camel.server.enabled", "true");
- nonRootMain.addInitialProperty("camel.server.port",
String.valueOf(AvailablePortFinder.getNextAvailable()));
- nonRootMain.addInitialProperty("camel.server.path", "/api");
- nonRootMain.addInitialProperty("camel.server.mcp-enabled", "true");
- nonRootMain.addInitialProperty("camel.server.mcp-oauth-profile",
"mcp-test");
- nonRootMain.addInitialProperty("camel.server.mcp-resource-uri",
"https://localhost/mcp");
-
- try {
- assertThatThrownBy(nonRootMain::start)
- .hasStackTraceContaining(
- "RFC 9728 protected-resource metadata must be
available at the origin root");
- } finally {
- nonRootMain.stop();
+ try (Port portHolder = AvailablePortFinder.find()) {
+ Main nonRootMain = new Main();
+ nonRootMain.addInitialProperty("camel.server.enabled", "true");
+ nonRootMain.addInitialProperty("camel.server.port",
String.valueOf(portHolder.getPort()));
+ nonRootMain.addInitialProperty("camel.server.path", "/api");
+ nonRootMain.addInitialProperty("camel.server.mcp-enabled", "true");
+ nonRootMain.addInitialProperty("camel.server.mcp-oauth-profile",
"mcp-test");
+ nonRootMain.addInitialProperty("camel.server.mcp-resource-uri",
"https://localhost/mcp");
+
+ try {
+ assertThatThrownBy(nonRootMain::start)
+ .hasStackTraceContaining(
+ "RFC 9728 protected-resource metadata must be
available at the origin root");
+ } finally {
+ nonRootMain.stop();
+ }
}
}
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainPropertiesTest.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainPropertiesTest.java
index 8af1cbda8324..def27cc8bc37 100644
---
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainPropertiesTest.java
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainPropertiesTest.java
@@ -33,6 +33,7 @@ import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mcp.server.stdio.StdioMcpServerEngine;
import org.apache.camel.main.Main;
import org.apache.camel.test.AvailablePortFinder;
+import org.apache.camel.test.AvailablePortFinder.Port;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -46,81 +47,85 @@ class McpServerMainPropertiesTest {
@Test
void testMcpServerStartsFromPropertiesAlone() throws Exception {
- int port = AvailablePortFinder.getNextAvailable();
-
- Main main = new Main();
- main.configure().addRoutesBuilder(new RouteBuilder() {
- @Override
- public void configure() {
- from("ai-tool:say_hello?tags=crm&description=Say hello"
- + "¶meter.name=string¶meter.name.required=true")
- .setBody(simple("Hello ${header.name}"));
-
- from("ai-tool:hidden_tool?description=Untagged, not exposed")
- .setBody(constant("hidden"));
- }
- });
- main.addInitialProperty("camel.server.enabled", "true");
- main.addInitialProperty("camel.server.port", String.valueOf(port));
- main.addInitialProperty("camel.server.mcp-enabled", "true");
- main.addInitialProperty("camel.server.mcp-tags", "crm");
- main.addInitialProperty("camel.server.mcp-server-name",
"my-integration-app");
- main.start();
+ try (Port portHolder = AvailablePortFinder.find()) {
+ int port = portHolder.getPort();
+
+ Main main = new Main();
+ main.configure().addRoutesBuilder(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("ai-tool:say_hello?tags=crm&description=Say hello"
+ +
"¶meter.name=string¶meter.name.required=true")
+ .setBody(simple("Hello ${header.name}"));
+
+ from("ai-tool:hidden_tool?description=Untagged, not
exposed")
+ .setBody(constant("hidden"));
+ }
+ });
+ main.addInitialProperty("camel.server.enabled", "true");
+ main.addInitialProperty("camel.server.port", String.valueOf(port));
+ main.addInitialProperty("camel.server.mcp-enabled", "true");
+ main.addInitialProperty("camel.server.mcp-tags", "crm");
+ main.addInitialProperty("camel.server.mcp-server-name",
"my-integration-app");
+ main.start();
- McpSyncClient client = null;
- try {
- client =
McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" +
port).build())
- .requestTimeout(Duration.ofSeconds(10))
- .initializationTimeout(Duration.ofSeconds(10))
- .build();
-
- McpSchema.InitializeResult init = client.initialize();
-
assertThat(init.serverInfo().name()).isEqualTo("my-integration-app");
- HttpResponse<String> metadata = HttpClient.newHttpClient().send(
- HttpRequest.newBuilder(URI.create("http://localhost:" +
port
- +
"/.well-known/oauth-protected-resource/mcp"))
- .GET().build(),
- HttpResponse.BodyHandlers.ofString());
- assertThat(metadata.statusCode()).isEqualTo(404);
-
- assertThat(client.listTools().tools())
- .extracting(McpSchema.Tool::name)
- .contains("say_hello")
- .doesNotContain("hidden_tool");
-
- McpSchema.CallToolResult result
- = client.callTool(new
McpSchema.CallToolRequest("say_hello", Map.of("name", "Camel")));
- assertThat(result.isError()).isNotEqualTo(Boolean.TRUE);
- assertThat(result.content().toString()).contains("Hello Camel");
- } finally {
- if (client != null) {
- client.closeGracefully();
+ McpSyncClient client = null;
+ try {
+ client =
McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" +
port).build())
+ .requestTimeout(Duration.ofSeconds(10))
+ .initializationTimeout(Duration.ofSeconds(10))
+ .build();
+
+ McpSchema.InitializeResult init = client.initialize();
+
assertThat(init.serverInfo().name()).isEqualTo("my-integration-app");
+ HttpResponse<String> metadata =
HttpClient.newHttpClient().send(
+ HttpRequest.newBuilder(URI.create("http://localhost:"
+ port
+ +
"/.well-known/oauth-protected-resource/mcp"))
+ .GET().build(),
+ HttpResponse.BodyHandlers.ofString());
+ assertThat(metadata.statusCode()).isEqualTo(404);
+
+ assertThat(client.listTools().tools())
+ .extracting(McpSchema.Tool::name)
+ .contains("say_hello")
+ .doesNotContain("hidden_tool");
+
+ McpSchema.CallToolResult result
+ = client.callTool(new
McpSchema.CallToolRequest("say_hello", Map.of("name", "Camel")));
+ assertThat(result.isError()).isNotEqualTo(Boolean.TRUE);
+ assertThat(result.content().toString()).contains("Hello
Camel");
+ } finally {
+ if (client != null) {
+ client.closeGracefully();
+ }
+ main.stop();
}
- main.stop();
}
}
@Test
void testMcpAllowedOriginsFromProperties() throws Exception {
- int port = AvailablePortFinder.getNextAvailable();
-
- Main main = new Main();
- main.addInitialProperty("camel.server.enabled", "true");
- main.addInitialProperty("camel.server.port", String.valueOf(port));
- main.addInitialProperty("camel.server.mcp-enabled", "true");
- main.addInitialProperty("camel.server.mcp-tags", "crm");
- main.addInitialProperty("camel.server.mcp-allowed-origins",
"https://App.Example.com:443/, http://localhost:3000");
- main.start();
+ try (Port portHolder = AvailablePortFinder.find()) {
+ int port = portHolder.getPort();
+
+ Main main = new Main();
+ main.addInitialProperty("camel.server.enabled", "true");
+ main.addInitialProperty("camel.server.port", String.valueOf(port));
+ main.addInitialProperty("camel.server.mcp-enabled", "true");
+ main.addInitialProperty("camel.server.mcp-tags", "crm");
+ main.addInitialProperty("camel.server.mcp-allowed-origins",
"https://App.Example.com:443/, http://localhost:3000");
+ main.start();
- try {
- assertThat(initialize(port, null)).isEqualTo(200);
- assertThat(initialize(port,
"https://app.example.com")).isEqualTo(200);
- assertThat(initialize(port,
"http://localhost:3000")).isEqualTo(200);
- // the configured list replaces the loopback default
- assertThat(initialize(port,
"http://localhost:6274")).isEqualTo(403);
- assertThat(initialize(port,
"https://other.example.com")).isEqualTo(403);
- } finally {
- main.stop();
+ try {
+ assertThat(initialize(port, null)).isEqualTo(200);
+ assertThat(initialize(port,
"https://app.example.com")).isEqualTo(200);
+ assertThat(initialize(port,
"http://localhost:3000")).isEqualTo(200);
+ // the configured list replaces the loopback default
+ assertThat(initialize(port,
"http://localhost:6274")).isEqualTo(403);
+ assertThat(initialize(port,
"https://other.example.com")).isEqualTo(403);
+ } finally {
+ main.stop();
+ }
}
}
@@ -163,18 +168,20 @@ class McpServerMainPropertiesTest {
@Test
void testMcpAllowedOriginWildcardFailsFast() {
- Main main = new Main();
- main.addInitialProperty("camel.server.enabled", "true");
- main.addInitialProperty("camel.server.port",
String.valueOf(AvailablePortFinder.getNextAvailable()));
- main.addInitialProperty("camel.server.mcp-enabled", "true");
- main.addInitialProperty("camel.server.mcp-tags", "crm");
- main.addInitialProperty("camel.server.mcp-allowed-origins", "*");
-
- try {
- assertThatThrownBy(main::start)
- .hasStackTraceContaining("MCP allowed origin * is not
supported");
- } finally {
- main.stop();
+ try (Port portHolder = AvailablePortFinder.find()) {
+ Main main = new Main();
+ main.addInitialProperty("camel.server.enabled", "true");
+ main.addInitialProperty("camel.server.port",
String.valueOf(portHolder.getPort()));
+ main.addInitialProperty("camel.server.mcp-enabled", "true");
+ main.addInitialProperty("camel.server.mcp-tags", "crm");
+ main.addInitialProperty("camel.server.mcp-allowed-origins", "*");
+
+ try {
+ assertThatThrownBy(main::start)
+ .hasStackTraceContaining("MCP allowed origin * is not
supported");
+ } finally {
+ main.stop();
+ }
}
}
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/McpServerSessionEvictionPropertiesTest.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/McpServerSessionEvictionPropertiesTest.java
index 0eb27a4e10bf..3ac29c65813b 100644
---
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/McpServerSessionEvictionPropertiesTest.java
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/McpServerSessionEvictionPropertiesTest.java
@@ -27,6 +27,7 @@ import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mcp.server.McpServerBridge;
import org.apache.camel.main.Main;
import org.apache.camel.test.AvailablePortFinder;
+import org.apache.camel.test.AvailablePortFinder.Port;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -45,35 +46,37 @@ class McpServerSessionEvictionPropertiesTest {
@Test
void testSessionIdleTtlPropertyEvictsOrphanSessions() throws Exception {
- int port = AvailablePortFinder.getNextAvailable();
+ try (Port portHolder = AvailablePortFinder.find()) {
+ int port = portHolder.getPort();
- Main main = new Main();
- main.configure().addRoutesBuilder(new RouteBuilder() {
- @Override
- public void configure() {
- from("ai-tool:ping?tags=crm&description=Ping tool")
- .setBody(constant("pong"));
- }
- });
- main.addInitialProperty("camel.server.enabled", "true");
- main.addInitialProperty("camel.server.port", String.valueOf(port));
- main.addInitialProperty("camel.server.mcp-enabled", "true");
- main.addInitialProperty("camel.server.mcp-tags", "crm");
-
main.addInitialProperty("camel.server.mcp-session-keep-alive-interval", "0");
- main.addInitialProperty("camel.server.mcp-session-idle-ttl", "500");
- main.start();
+ Main main = new Main();
+ main.configure().addRoutesBuilder(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("ai-tool:ping?tags=crm&description=Ping tool")
+ .setBody(constant("pong"));
+ }
+ });
+ main.addInitialProperty("camel.server.enabled", "true");
+ main.addInitialProperty("camel.server.port", String.valueOf(port));
+ main.addInitialProperty("camel.server.mcp-enabled", "true");
+ main.addInitialProperty("camel.server.mcp-tags", "crm");
+
main.addInitialProperty("camel.server.mcp-session-keep-alive-interval", "0");
+ main.addInitialProperty("camel.server.mcp-session-idle-ttl",
"500");
+ main.start();
- try {
- initializeSession(port);
- McpServerBridge bridge =
main.getCamelContext().hasService(McpServerBridge.class);
- assertThat(bridge).isNotNull();
-
assertThat(bridge.getEngine()).isInstanceOf(VertxMcpServerEngine.class);
- VertxMcpServerEngine engine = (VertxMcpServerEngine)
bridge.getEngine();
- assertThat(engine.sessionCount()).isEqualTo(1);
+ try {
+ initializeSession(port);
+ McpServerBridge bridge =
main.getCamelContext().hasService(McpServerBridge.class);
+ assertThat(bridge).isNotNull();
+
assertThat(bridge.getEngine()).isInstanceOf(VertxMcpServerEngine.class);
+ VertxMcpServerEngine engine = (VertxMcpServerEngine)
bridge.getEngine();
+ assertThat(engine.sessionCount()).isEqualTo(1);
- await().atMost(5, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(engine.sessionCount()).isZero());
- } finally {
- main.stop();
+ await().atMost(5, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(engine.sessionCount()).isZero());
+ } finally {
+ main.stop();
+ }
}
}
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpErrorResponseTest.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpErrorResponseTest.java
index 1c607952a929..5dfaf550390d 100644
---
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpErrorResponseTest.java
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpErrorResponseTest.java
@@ -46,13 +46,15 @@ class VertxMcpErrorResponseTest {
private static final HttpClient HTTP =
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
+ private static AvailablePortFinder.Port portRef;
private static CamelContext camelContext;
private static VertxMcpServerEngine engine;
private static String url;
@BeforeAll
static void startServer() throws Exception {
- int port = AvailablePortFinder.getNextAvailable();
+ portRef = AvailablePortFinder.find();
+ int port = portRef.getPort();
camelContext = new DefaultCamelContext();
MainHttpServer main = new MainHttpServer();
main.setCamelContext(camelContext);
@@ -73,6 +75,9 @@ class VertxMcpErrorResponseTest {
static void stopServer() {
engine.stop();
camelContext.stop();
+ if (portRef != null) {
+ portRef.release();
+ }
}
@Test
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpOriginTest.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpOriginTest.java
index 0847a3a4a808..c83a1c3f5188 100644
---
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpOriginTest.java
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpOriginTest.java
@@ -37,6 +37,7 @@ import
org.apache.camel.component.mcp.server.McpToolCallResult;
import org.apache.camel.component.platform.http.main.MainHttpServer;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.test.AvailablePortFinder;
+import org.apache.camel.test.AvailablePortFinder.Port;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -66,6 +67,7 @@ class VertxMcpOriginTest {
private CamelContext camelContext;
private VertxMcpServerEngine engine;
private int port;
+ private Port portHolder;
@AfterEach
void stop() {
@@ -75,6 +77,10 @@ class VertxMcpOriginTest {
if (camelContext != null) {
camelContext.stop();
}
+ if (portHolder != null) {
+ portHolder.release();
+ portHolder = null;
+ }
}
@Test
@@ -209,7 +215,8 @@ class VertxMcpOriginTest {
}
private void start(List<String> allowedOrigins) throws Exception {
- port = AvailablePortFinder.getNextAvailable();
+ portHolder = AvailablePortFinder.find();
+ port = portHolder.getPort();
camelContext = new DefaultCamelContext();
MainHttpServer main = new MainHttpServer();
main.setCamelContext(camelContext);
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerConformanceTest.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerConformanceTest.java
index 572cbc15e273..883b7fb986e5 100644
---
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerConformanceTest.java
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerConformanceTest.java
@@ -21,6 +21,7 @@ import
org.apache.camel.component.mcp.server.conformance.McpServerConformanceTes
import org.apache.camel.component.platform.http.vertx.VertxPlatformHttpServer;
import
org.apache.camel.component.platform.http.vertx.VertxPlatformHttpServerConfiguration;
import org.apache.camel.test.AvailablePortFinder;
+import org.junit.jupiter.api.AfterEach;
/**
* Runs the engine conformance kit against {@link VertxMcpServerEngine}
serving through a standalone Vert.x platform
@@ -28,17 +29,26 @@ import org.apache.camel.test.AvailablePortFinder;
*/
class VertxMcpServerConformanceTest extends McpServerConformanceTestSupport {
- private final int port = AvailablePortFinder.getNextAvailable();
+ private AvailablePortFinder.Port portRef;
+
+ @AfterEach
+ void releasePort() {
+ if (portRef != null) {
+ portRef.release();
+ portRef = null;
+ }
+ }
@Override
protected void customizeCamelContext(CamelContext camelContext) throws
Exception {
+ portRef = AvailablePortFinder.find();
VertxPlatformHttpServerConfiguration configuration = new
VertxPlatformHttpServerConfiguration();
- configuration.setBindPort(port);
+ configuration.setBindPort(portRef.getPort());
camelContext.addService(new VertxPlatformHttpServer(configuration));
}
@Override
protected String mcpServerBaseUrl() {
- return "http://localhost:" + port;
+ return "http://localhost:" + portRef.getPort();
}
}
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
index e831a1f44324..19a0b4f2d104 100644
---
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
@@ -38,6 +38,7 @@ 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.apache.camel.test.AvailablePortFinder.Port;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -52,94 +53,99 @@ 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();
+ try (Port mainPortHolder = AvailablePortFinder.find();
+ Port managementPortHolder = AvailablePortFinder.find()) {
+ int mainPort = mainPortHolder.getPort();
+ int managementPort = managementPortHolder.getPort();
+
+ 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();
}
- 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();
+ try (Port mainPortHolder = AvailablePortFinder.find()) {
+ int mainPort = mainPortHolder.getPort();
+
+ 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();
+ }
}
}
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerOutputSchemaTest.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerOutputSchemaTest.java
index 95553c81d9a7..36ac2335bd4c 100644
---
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerOutputSchemaTest.java
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerOutputSchemaTest.java
@@ -32,6 +32,7 @@ import
org.apache.camel.component.mcp.server.McpToolCallResult;
import org.apache.camel.component.platform.http.main.MainHttpServer;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.test.AvailablePortFinder;
+import org.apache.camel.test.AvailablePortFinder.Port;
import org.apache.camel.util.json.JsonObject;
import org.junit.jupiter.api.Test;
@@ -52,52 +53,54 @@ class VertxMcpServerOutputSchemaTest {
@Test
void testEnginePublishesOutputSchemaAndStructuredContent() throws
Exception {
- int port = AvailablePortFinder.getNextAvailable();
- CamelContext camelContext = new DefaultCamelContext();
- VertxMcpServerEngine engine = new VertxMcpServerEngine();
- McpSyncClient client = null;
- try {
- MainHttpServer main = new MainHttpServer();
- main.setCamelContext(camelContext);
- main.setHost("0.0.0.0");
- main.setPort(port);
- camelContext.addService(main);
- camelContext.start();
-
- engine.setCamelContext(camelContext);
- engine.initialize(new McpServerInfo("output-schema", "1.0",
"/mcp"));
- engine.start();
-
- JsonObject structured = new JsonObject();
- structured.put("temperature", 21.5);
- structured.put("unit", "celsius");
- engine.toolAdded(tool("getWeather", "Get weather", OUTPUT_SCHEMA,
- arguments -> new McpToolCallResult(structured.toJson(),
false, structured)));
-
- client =
McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" +
port).build())
- .requestTimeout(Duration.ofSeconds(10))
- .initializationTimeout(Duration.ofSeconds(10))
- .build();
- client.initialize();
-
- List<McpSchema.Tool> tools = client.listTools().tools();
- McpSchema.Tool weather = tools.stream().filter(t ->
"getWeather".equals(t.name())).findFirst().orElseThrow();
-
- assertThat(weather.outputSchema()).isNotNull();
- assertThat(weather.outputSchema()).containsKey("properties");
-
- McpSchema.CallToolResult result = client.callTool(new
McpSchema.CallToolRequest("getWeather", Map.of()));
-
- assertThat(result.isError()).isFalse();
- assertThat(result.structuredContent()).isNotNull();
- assertThat(result.structuredContent()).isInstanceOf(Map.class);
- assertThat(((Map<?, ?>)
result.structuredContent()).get("temperature")).isEqualTo(21.5);
- } finally {
- if (client != null) {
- client.closeGracefully();
+ try (Port portHolder = AvailablePortFinder.find()) {
+ int port = portHolder.getPort();
+ CamelContext camelContext = new DefaultCamelContext();
+ VertxMcpServerEngine engine = new VertxMcpServerEngine();
+ McpSyncClient client = null;
+ try {
+ MainHttpServer main = new MainHttpServer();
+ main.setCamelContext(camelContext);
+ main.setHost("0.0.0.0");
+ main.setPort(port);
+ camelContext.addService(main);
+ camelContext.start();
+
+ engine.setCamelContext(camelContext);
+ engine.initialize(new McpServerInfo("output-schema", "1.0",
"/mcp"));
+ engine.start();
+
+ JsonObject structured = new JsonObject();
+ structured.put("temperature", 21.5);
+ structured.put("unit", "celsius");
+ engine.toolAdded(tool("getWeather", "Get weather",
OUTPUT_SCHEMA,
+ arguments -> new
McpToolCallResult(structured.toJson(), false, structured)));
+
+ client =
McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" +
port).build())
+ .requestTimeout(Duration.ofSeconds(10))
+ .initializationTimeout(Duration.ofSeconds(10))
+ .build();
+ client.initialize();
+
+ List<McpSchema.Tool> tools = client.listTools().tools();
+ McpSchema.Tool weather = tools.stream().filter(t ->
"getWeather".equals(t.name())).findFirst().orElseThrow();
+
+ assertThat(weather.outputSchema()).isNotNull();
+ assertThat(weather.outputSchema()).containsKey("properties");
+
+ McpSchema.CallToolResult result = client.callTool(new
McpSchema.CallToolRequest("getWeather", Map.of()));
+
+ assertThat(result.isError()).isFalse();
+ assertThat(result.structuredContent()).isNotNull();
+ assertThat(result.structuredContent()).isInstanceOf(Map.class);
+ assertThat(((Map<?, ?>)
result.structuredContent()).get("temperature")).isEqualTo(21.5);
+ } finally {
+ if (client != null) {
+ client.closeGracefully();
+ }
+ engine.stop();
+ camelContext.stop();
}
- engine.stop();
- camelContext.stop();
}
}
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerPromptConformanceTest.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerPromptConformanceTest.java
index f338809d37fd..805ed7d11654 100644
---
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerPromptConformanceTest.java
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerPromptConformanceTest.java
@@ -21,21 +21,31 @@ import
org.apache.camel.component.mcp.server.conformance.McpServerPromptConforma
import org.apache.camel.component.platform.http.vertx.VertxPlatformHttpServer;
import
org.apache.camel.component.platform.http.vertx.VertxPlatformHttpServerConfiguration;
import org.apache.camel.test.AvailablePortFinder;
+import org.junit.jupiter.api.AfterEach;
/** Runs the prompt conformance kit against the Vert.x streamable HTTP engine.
*/
class VertxMcpServerPromptConformanceTest extends
McpServerPromptConformanceTestSupport {
- private final int port = AvailablePortFinder.getNextAvailable();
+ private AvailablePortFinder.Port portRef;
+
+ @AfterEach
+ void releasePort() {
+ if (portRef != null) {
+ portRef.release();
+ portRef = null;
+ }
+ }
@Override
protected void customizeCamelContext(CamelContext camelContext) throws
Exception {
+ portRef = AvailablePortFinder.find();
VertxPlatformHttpServerConfiguration configuration = new
VertxPlatformHttpServerConfiguration();
- configuration.setBindPort(port);
+ configuration.setBindPort(portRef.getPort());
camelContext.addService(new VertxPlatformHttpServer(configuration));
}
@Override
protected String mcpServerBaseUrl() {
- return "http://localhost:" + port;
+ return "http://localhost:" + portRef.getPort();
}
}
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerResourceConformanceTest.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerResourceConformanceTest.java
index c1c41d923fe8..08f43efa4b05 100644
---
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerResourceConformanceTest.java
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerResourceConformanceTest.java
@@ -21,6 +21,7 @@ import
org.apache.camel.component.mcp.server.conformance.McpServerResourceConfor
import org.apache.camel.component.platform.http.vertx.VertxPlatformHttpServer;
import
org.apache.camel.component.platform.http.vertx.VertxPlatformHttpServerConfiguration;
import org.apache.camel.test.AvailablePortFinder;
+import org.junit.jupiter.api.AfterEach;
/**
* Runs the resource conformance kit against {@link VertxMcpServerEngine}
serving through a standalone Vert.x platform
@@ -28,17 +29,26 @@ import org.apache.camel.test.AvailablePortFinder;
*/
class VertxMcpServerResourceConformanceTest extends
McpServerResourceConformanceTestSupport {
- private final int port = AvailablePortFinder.getNextAvailable();
+ private AvailablePortFinder.Port portRef;
+
+ @AfterEach
+ void releasePort() {
+ if (portRef != null) {
+ portRef.release();
+ portRef = null;
+ }
+ }
@Override
protected void customizeCamelContext(CamelContext camelContext) throws
Exception {
+ portRef = AvailablePortFinder.find();
VertxPlatformHttpServerConfiguration configuration = new
VertxPlatformHttpServerConfiguration();
- configuration.setBindPort(port);
+ configuration.setBindPort(portRef.getPort());
camelContext.addService(new VertxPlatformHttpServer(configuration));
}
@Override
protected String mcpServerBaseUrl() {
- return "http://localhost:" + port;
+ return "http://localhost:" + portRef.getPort();
}
}
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerToolAnnotationsTest.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerToolAnnotationsTest.java
index cb549b461083..0e7012634ab0 100644
---
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerToolAnnotationsTest.java
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerToolAnnotationsTest.java
@@ -35,6 +35,7 @@ import
org.apache.camel.component.mcp.server.McpToolCallResult;
import org.apache.camel.component.platform.http.main.MainHttpServer;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.test.AvailablePortFinder;
+import org.apache.camel.test.AvailablePortFinder.Port;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -43,144 +44,150 @@ class VertxMcpServerToolAnnotationsTest {
@Test
void testEnginePublishesTitleOnlyHint() throws Exception {
- int port = AvailablePortFinder.getNextAvailable();
- CamelContext camelContext = new DefaultCamelContext();
- VertxMcpServerEngine engine = new VertxMcpServerEngine();
- McpSyncClient client = null;
- try {
- MainHttpServer main = new MainHttpServer();
- main.setCamelContext(camelContext);
- main.setHost("0.0.0.0");
- main.setPort(port);
- camelContext.addService(main);
- camelContext.start();
-
- engine.setCamelContext(camelContext);
- engine.initialize(new McpServerInfo("tool-hints", "1.0", "/mcp"));
- engine.start();
-
- AiToolConfiguration configuration = new AiToolConfiguration();
- configuration.setTitle("Lookup customer");
- AiToolAnnotations annotations =
AiToolAnnotations.fromConfiguration(configuration);
-
- engine.toolAdded(tool("lookup", "Lookup a customer", annotations,
- arguments -> new McpToolCallResult("ok", false)));
-
- client =
McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" +
port).build())
- .requestTimeout(Duration.ofSeconds(10))
- .initializationTimeout(Duration.ofSeconds(10))
- .build();
- client.initialize();
-
- List<McpSchema.Tool> tools = client.listTools().tools();
- McpSchema.Tool lookup = tools.stream().filter(t ->
"lookup".equals(t.name())).findFirst().orElseThrow();
-
- assertThat(lookup.title()).isEqualTo("Lookup customer");
- assertThat(lookup.annotations()).isNull();
- } finally {
- if (client != null) {
- client.closeGracefully();
+ try (Port portHolder = AvailablePortFinder.find()) {
+ int port = portHolder.getPort();
+ CamelContext camelContext = new DefaultCamelContext();
+ VertxMcpServerEngine engine = new VertxMcpServerEngine();
+ McpSyncClient client = null;
+ try {
+ MainHttpServer main = new MainHttpServer();
+ main.setCamelContext(camelContext);
+ main.setHost("0.0.0.0");
+ main.setPort(port);
+ camelContext.addService(main);
+ camelContext.start();
+
+ engine.setCamelContext(camelContext);
+ engine.initialize(new McpServerInfo("tool-hints", "1.0",
"/mcp"));
+ engine.start();
+
+ AiToolConfiguration configuration = new AiToolConfiguration();
+ configuration.setTitle("Lookup customer");
+ AiToolAnnotations annotations =
AiToolAnnotations.fromConfiguration(configuration);
+
+ engine.toolAdded(tool("lookup", "Lookup a customer",
annotations,
+ arguments -> new McpToolCallResult("ok", false)));
+
+ client =
McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" +
port).build())
+ .requestTimeout(Duration.ofSeconds(10))
+ .initializationTimeout(Duration.ofSeconds(10))
+ .build();
+ client.initialize();
+
+ List<McpSchema.Tool> tools = client.listTools().tools();
+ McpSchema.Tool lookup = tools.stream().filter(t ->
"lookup".equals(t.name())).findFirst().orElseThrow();
+
+ assertThat(lookup.title()).isEqualTo("Lookup customer");
+ assertThat(lookup.annotations()).isNull();
+ } finally {
+ if (client != null) {
+ client.closeGracefully();
+ }
+ engine.stop();
+ camelContext.stop();
}
- engine.stop();
- camelContext.stop();
}
}
@Test
void testEnginePublishesPartialBooleanHints() throws Exception {
- int port = AvailablePortFinder.getNextAvailable();
- CamelContext camelContext = new DefaultCamelContext();
- VertxMcpServerEngine engine = new VertxMcpServerEngine();
- McpSyncClient client = null;
- try {
- MainHttpServer main = new MainHttpServer();
- main.setCamelContext(camelContext);
- main.setHost("0.0.0.0");
- main.setPort(port);
- camelContext.addService(main);
- camelContext.start();
-
- engine.setCamelContext(camelContext);
- engine.initialize(new McpServerInfo("tool-hints", "1.0", "/mcp"));
- engine.start();
-
- AiToolConfiguration configuration = new AiToolConfiguration();
- configuration.setDestructiveHint(true);
- AiToolAnnotations annotations =
AiToolAnnotations.fromConfiguration(configuration);
-
- engine.toolAdded(tool("delete", "Delete resource", annotations,
- arguments -> new McpToolCallResult("ok", false)));
-
- client =
McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" +
port).build())
- .requestTimeout(Duration.ofSeconds(10))
- .initializationTimeout(Duration.ofSeconds(10))
- .build();
- client.initialize();
-
- List<McpSchema.Tool> tools = client.listTools().tools();
- McpSchema.Tool delete = tools.stream().filter(t ->
"delete".equals(t.name())).findFirst().orElseThrow();
-
- assertThat(delete.title()).isNull();
- assertThat(delete.annotations()).isNotNull();
- assertThat(delete.annotations().destructiveHint()).isTrue();
- assertThat(delete.annotations().readOnlyHint()).isNull();
- } finally {
- if (client != null) {
- client.closeGracefully();
+ try (Port portHolder = AvailablePortFinder.find()) {
+ int port = portHolder.getPort();
+ CamelContext camelContext = new DefaultCamelContext();
+ VertxMcpServerEngine engine = new VertxMcpServerEngine();
+ McpSyncClient client = null;
+ try {
+ MainHttpServer main = new MainHttpServer();
+ main.setCamelContext(camelContext);
+ main.setHost("0.0.0.0");
+ main.setPort(port);
+ camelContext.addService(main);
+ camelContext.start();
+
+ engine.setCamelContext(camelContext);
+ engine.initialize(new McpServerInfo("tool-hints", "1.0",
"/mcp"));
+ engine.start();
+
+ AiToolConfiguration configuration = new AiToolConfiguration();
+ configuration.setDestructiveHint(true);
+ AiToolAnnotations annotations =
AiToolAnnotations.fromConfiguration(configuration);
+
+ engine.toolAdded(tool("delete", "Delete resource", annotations,
+ arguments -> new McpToolCallResult("ok", false)));
+
+ client =
McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" +
port).build())
+ .requestTimeout(Duration.ofSeconds(10))
+ .initializationTimeout(Duration.ofSeconds(10))
+ .build();
+ client.initialize();
+
+ List<McpSchema.Tool> tools = client.listTools().tools();
+ McpSchema.Tool delete = tools.stream().filter(t ->
"delete".equals(t.name())).findFirst().orElseThrow();
+
+ assertThat(delete.title()).isNull();
+ assertThat(delete.annotations()).isNotNull();
+ assertThat(delete.annotations().destructiveHint()).isTrue();
+ assertThat(delete.annotations().readOnlyHint()).isNull();
+ } finally {
+ if (client != null) {
+ client.closeGracefully();
+ }
+ engine.stop();
+ camelContext.stop();
}
- engine.stop();
- camelContext.stop();
}
}
@Test
void testEnginePublishesToolAnnotationHints() throws Exception {
- int port = AvailablePortFinder.getNextAvailable();
- CamelContext camelContext = new DefaultCamelContext();
- VertxMcpServerEngine engine = new VertxMcpServerEngine();
- McpSyncClient client = null;
- try {
- MainHttpServer main = new MainHttpServer();
- main.setCamelContext(camelContext);
- main.setHost("0.0.0.0");
- main.setPort(port);
- camelContext.addService(main);
- camelContext.start();
-
- engine.setCamelContext(camelContext);
- engine.initialize(new McpServerInfo("tool-hints", "1.0", "/mcp"));
- engine.start();
-
- AiToolConfiguration configuration = new AiToolConfiguration();
- configuration.setTitle("Read only tool");
- configuration.setReadOnlyHint(true);
- configuration.setDestructiveHint(false);
- configuration.setOpenWorldHint(false);
- AiToolAnnotations annotations =
AiToolAnnotations.fromConfiguration(configuration);
-
- engine.toolAdded(tool("read_only", "Read-only lookup", annotations,
- arguments -> new McpToolCallResult("ok", false)));
-
- client =
McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" +
port).build())
- .requestTimeout(Duration.ofSeconds(10))
- .initializationTimeout(Duration.ofSeconds(10))
- .build();
- client.initialize();
-
- List<McpSchema.Tool> tools = client.listTools().tools();
- McpSchema.Tool readOnly = tools.stream().filter(t ->
"read_only".equals(t.name())).findFirst().orElseThrow();
-
- assertThat(readOnly.title()).isEqualTo("Read only tool");
- assertThat(readOnly.annotations()).isNotNull();
- assertThat(readOnly.annotations().readOnlyHint()).isTrue();
- assertThat(readOnly.annotations().destructiveHint()).isFalse();
- assertThat(readOnly.annotations().openWorldHint()).isFalse();
- } finally {
- if (client != null) {
- client.closeGracefully();
+ try (Port portHolder = AvailablePortFinder.find()) {
+ int port = portHolder.getPort();
+ CamelContext camelContext = new DefaultCamelContext();
+ VertxMcpServerEngine engine = new VertxMcpServerEngine();
+ McpSyncClient client = null;
+ try {
+ MainHttpServer main = new MainHttpServer();
+ main.setCamelContext(camelContext);
+ main.setHost("0.0.0.0");
+ main.setPort(port);
+ camelContext.addService(main);
+ camelContext.start();
+
+ engine.setCamelContext(camelContext);
+ engine.initialize(new McpServerInfo("tool-hints", "1.0",
"/mcp"));
+ engine.start();
+
+ AiToolConfiguration configuration = new AiToolConfiguration();
+ configuration.setTitle("Read only tool");
+ configuration.setReadOnlyHint(true);
+ configuration.setDestructiveHint(false);
+ configuration.setOpenWorldHint(false);
+ AiToolAnnotations annotations =
AiToolAnnotations.fromConfiguration(configuration);
+
+ engine.toolAdded(tool("read_only", "Read-only lookup",
annotations,
+ arguments -> new McpToolCallResult("ok", false)));
+
+ client =
McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" +
port).build())
+ .requestTimeout(Duration.ofSeconds(10))
+ .initializationTimeout(Duration.ofSeconds(10))
+ .build();
+ client.initialize();
+
+ List<McpSchema.Tool> tools = client.listTools().tools();
+ McpSchema.Tool readOnly = tools.stream().filter(t ->
"read_only".equals(t.name())).findFirst().orElseThrow();
+
+ assertThat(readOnly.title()).isEqualTo("Read only tool");
+ assertThat(readOnly.annotations()).isNotNull();
+ assertThat(readOnly.annotations().readOnlyHint()).isTrue();
+ assertThat(readOnly.annotations().destructiveHint()).isFalse();
+ assertThat(readOnly.annotations().openWorldHint()).isFalse();
+ } finally {
+ if (client != null) {
+ client.closeGracefully();
+ }
+ engine.stop();
+ camelContext.stop();
}
- engine.stop();
- camelContext.stop();
}
}
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpSessionEvictionTest.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpSessionEvictionTest.java
index d8232a6db1a7..954add31754a 100644
---
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpSessionEvictionTest.java
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/VertxMcpSessionEvictionTest.java
@@ -28,6 +28,7 @@ import org.apache.camel.component.mcp.server.McpServerInfo;
import org.apache.camel.component.platform.http.main.MainHttpServer;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.test.AvailablePortFinder;
+import org.apache.camel.test.AvailablePortFinder.Port;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -49,60 +50,70 @@ class VertxMcpSessionEvictionTest {
@Test
void evictsIdleSessionWithoutDelete() throws Exception {
- int port = AvailablePortFinder.getNextAvailable();
- VertxMcpServerEngine engine = startEngine(port, new
McpServerInfo("eviction", "1.0", "/mcp", 0, 500));
- try {
- initializeSession(port);
- assertThat(engine.sessionCount()).isEqualTo(1);
-
- await().atMost(5, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(engine.sessionCount()).isZero());
- } finally {
- engine.stop();
+ try (Port portHolder = AvailablePortFinder.find()) {
+ int port = portHolder.getPort();
+ VertxMcpServerEngine engine = startEngine(port, new
McpServerInfo("eviction", "1.0", "/mcp", 0, 500));
+ try {
+ initializeSession(port);
+ assertThat(engine.sessionCount()).isEqualTo(1);
+
+ await().atMost(5, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(engine.sessionCount()).isZero());
+ } finally {
+ engine.stop();
+ }
}
}
@Test
void postOnlySessionSurvivesKeepAliveUntilIdle() throws Exception {
- int port = AvailablePortFinder.getNextAvailable();
- VertxMcpServerEngine engine = startEngine(port, new
McpServerInfo("eviction", "1.0", "/mcp", 300, 10_000));
- try {
- String sessionId = initializeSession(port);
-
- await().during(2, TimeUnit.SECONDS).atMost(3,
TimeUnit.SECONDS).untilAsserted(() -> {
- assertThat(postWithSession(port, sessionId,
INITIALIZED_NOTIFICATION)).isEqualTo(202);
- assertThat(engine.sessionCount()).isEqualTo(1);
- });
- } finally {
- engine.stop();
+ try (Port portHolder = AvailablePortFinder.find()) {
+ int port = portHolder.getPort();
+ // keep-alive interval 300 ms, idle TTL 10 s — session must stay
alive while POSTs keep arriving
+ VertxMcpServerEngine engine = startEngine(port, new
McpServerInfo("eviction", "1.0", "/mcp", 300, 10_000));
+ try {
+ String sessionId = initializeSession(port);
+
+ await().during(500, TimeUnit.MILLISECONDS).atMost(1500,
TimeUnit.MILLISECONDS).untilAsserted(() -> {
+ assertThat(postWithSession(port, sessionId,
INITIALIZED_NOTIFICATION)).isEqualTo(202);
+ assertThat(engine.sessionCount()).isEqualTo(1);
+ });
+ } finally {
+ engine.stop();
+ }
}
}
@Test
void activeSessionSurvivesIdleEviction() throws Exception {
- int port = AvailablePortFinder.getNextAvailable();
- VertxMcpServerEngine engine = startEngine(port, new
McpServerInfo("eviction", "1.0", "/mcp", 0, 2_000));
- try {
- String sessionId = initializeSession(port);
-
- await().during(2, TimeUnit.SECONDS).atMost(3,
TimeUnit.SECONDS).untilAsserted(() -> {
- assertThat(postWithSession(port, sessionId,
INITIALIZED_NOTIFICATION)).isEqualTo(202);
- assertThat(engine.sessionCount()).isEqualTo(1);
- });
- } finally {
- engine.stop();
+ try (Port portHolder = AvailablePortFinder.find()) {
+ int port = portHolder.getPort();
+ // TTL 300 ms so each POST must call touch() or the session would
be evicted between polls
+ VertxMcpServerEngine engine = startEngine(port, new
McpServerInfo("eviction", "1.0", "/mcp", 0, 300));
+ try {
+ String sessionId = initializeSession(port);
+
+ await().during(1, TimeUnit.SECONDS).atMost(2,
TimeUnit.SECONDS).untilAsserted(() -> {
+ assertThat(postWithSession(port, sessionId,
INITIALIZED_NOTIFICATION)).isEqualTo(202);
+ assertThat(engine.sessionCount()).isEqualTo(1);
+ });
+ } finally {
+ engine.stop();
+ }
}
}
@Test
void explicitDeleteRemovesSessionImmediately() throws Exception {
- int port = AvailablePortFinder.getNextAvailable();
- VertxMcpServerEngine engine = startEngine(port, new
McpServerInfo("eviction", "1.0", "/mcp", 0, 60_000));
- try {
- String sessionId = initializeSession(port);
- assertThat(deleteSession(port, sessionId)).isEqualTo(200);
- assertThat(engine.sessionCount()).isZero();
- } finally {
- engine.stop();
+ try (Port portHolder = AvailablePortFinder.find()) {
+ int port = portHolder.getPort();
+ VertxMcpServerEngine engine = startEngine(port, new
McpServerInfo("eviction", "1.0", "/mcp", 0, 60_000));
+ try {
+ String sessionId = initializeSession(port);
+ assertThat(deleteSession(port, sessionId)).isEqualTo(200);
+ assertThat(engine.sessionCount()).isZero();
+ } finally {
+ engine.stop();
+ }
}
}
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/integration/MainHttpServerMcpConformanceIT.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/integration/MainHttpServerMcpConformanceIT.java
index 39234e2e3c6e..d0892a9bd96a 100644
---
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/integration/MainHttpServerMcpConformanceIT.java
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/integration/MainHttpServerMcpConformanceIT.java
@@ -20,6 +20,7 @@ import org.apache.camel.CamelContext;
import
org.apache.camel.component.mcp.server.conformance.McpServerConformanceTestSupport;
import org.apache.camel.component.platform.http.main.MainHttpServer;
import org.apache.camel.test.AvailablePortFinder;
+import org.junit.jupiter.api.AfterEach;
/**
* Runs the engine conformance kit against the Camel main HTTP server ({@code
camel-platform-http-main}) — the actual
@@ -28,19 +29,28 @@ import org.apache.camel.test.AvailablePortFinder;
*/
class MainHttpServerMcpConformanceIT extends McpServerConformanceTestSupport {
- private final int port = AvailablePortFinder.getNextAvailable();
+ private AvailablePortFinder.Port portRef;
+
+ @AfterEach
+ void releasePort() {
+ if (portRef != null) {
+ portRef.release();
+ portRef = null;
+ }
+ }
@Override
protected void customizeCamelContext(CamelContext camelContext) throws
Exception {
+ portRef = AvailablePortFinder.find();
MainHttpServer server = new MainHttpServer();
server.setCamelContext(camelContext);
server.setHost("0.0.0.0");
- server.setPort(port);
+ server.setPort(portRef.getPort());
camelContext.addService(server);
}
@Override
protected String mcpServerBaseUrl() {
- return "http://localhost:" + port;
+ return "http://localhost:" + portRef.getPort();
}
}
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/integration/MainHttpServerMcpResourceConformanceIT.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/integration/MainHttpServerMcpResourceConformanceIT.java
index 668dc345505c..0928aee99466 100644
---
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/integration/MainHttpServerMcpResourceConformanceIT.java
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/integration/MainHttpServerMcpResourceConformanceIT.java
@@ -20,6 +20,7 @@ import org.apache.camel.CamelContext;
import
org.apache.camel.component.mcp.server.conformance.McpServerResourceConformanceTestSupport;
import org.apache.camel.component.platform.http.main.MainHttpServer;
import org.apache.camel.test.AvailablePortFinder;
+import org.junit.jupiter.api.AfterEach;
/**
* Runs the resource conformance kit against the Camel main HTTP server
({@code camel-platform-http-main}) — the actual
@@ -27,19 +28,28 @@ import org.apache.camel.test.AvailablePortFinder;
*/
class MainHttpServerMcpResourceConformanceIT extends
McpServerResourceConformanceTestSupport {
- private final int port = AvailablePortFinder.getNextAvailable();
+ private AvailablePortFinder.Port portRef;
+
+ @AfterEach
+ void releasePort() {
+ if (portRef != null) {
+ portRef.release();
+ portRef = null;
+ }
+ }
@Override
protected void customizeCamelContext(CamelContext camelContext) throws
Exception {
+ portRef = AvailablePortFinder.find();
MainHttpServer server = new MainHttpServer();
server.setCamelContext(camelContext);
server.setHost("0.0.0.0");
- server.setPort(port);
+ server.setPort(portRef.getPort());
camelContext.addService(server);
}
@Override
protected String mcpServerBaseUrl() {
- return "http://localhost:" + port;
+ return "http://localhost:" + portRef.getPort();
}
}
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/integration/McpServerOpenAIAgentIT.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/integration/McpServerOpenAIAgentIT.java
index 43f04c98fbad..8af7d1025199 100644
---
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/integration/McpServerOpenAIAgentIT.java
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/integration/McpServerOpenAIAgentIT.java
@@ -31,6 +31,7 @@ import
org.apache.camel.test.infra.ollama.services.OllamaService;
import org.apache.camel.test.infra.ollama.services.OllamaServiceFactory;
import org.apache.camel.test.junit6.CamelTestSupport;
import org.apache.camel.util.ObjectHelper;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -48,7 +49,15 @@ class McpServerOpenAIAgentIT extends CamelTestSupport {
@RegisterExtension
static OllamaService OLLAMA =
OllamaServiceFactory.createSingletonService();
- private final int mcpPort = AvailablePortFinder.getNextAvailable();
+ private AvailablePortFinder.Port mcpPortRef;
+
+ @AfterEach
+ void releasePort() {
+ if (mcpPortRef != null) {
+ mcpPortRef.release();
+ mcpPortRef = null;
+ }
+ }
private String apiKey;
private String baseUrl;
@@ -82,8 +91,9 @@ class McpServerOpenAIAgentIT extends CamelTestSupport {
camelContext.addComponent("openai", component);
// this application serves its own MCP endpoint on the Vert.x platform
HTTP server
+ mcpPortRef = AvailablePortFinder.find();
VertxPlatformHttpServerConfiguration serverConfiguration = new
VertxPlatformHttpServerConfiguration();
- serverConfiguration.setBindPort(mcpPort);
+ serverConfiguration.setBindPort(mcpPortRef.getPort());
camelContext.addService(new
VertxPlatformHttpServer(serverConfiguration));
McpServerConfiguration mcpConfiguration = new McpServerConfiguration();
@@ -121,7 +131,7 @@ class McpServerOpenAIAgentIT extends CamelTestSupport {
.toF("openai:chat-completion"
+
"?mcpServer.camelTools.transportType=streamableHttp"
+
"&mcpServer.camelTools.url=http://localhost:%d/mcp",
- mcpPort)
+ mcpPortRef.getPort())
.to("mock:response");
}
});
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/integration/OfficialMcpConformanceIT.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/integration/OfficialMcpConformanceIT.java
index 151047ad7f14..b59053736642 100644
---
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/integration/OfficialMcpConformanceIT.java
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/vertx/integration/OfficialMcpConformanceIT.java
@@ -33,6 +33,7 @@ import
org.apache.camel.component.mcp.server.McpServerConfiguration;
import org.apache.camel.component.platform.http.main.MainHttpServer;
import org.apache.camel.test.AvailablePortFinder;
import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.slf4j.Logger;
@@ -85,16 +86,25 @@ class OfficialMcpConformanceIT extends CamelTestSupport {
private static final String MINIMAL_WAV_BASE64
= "UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=";
- private final int port = AvailablePortFinder.getNextAvailable();
+ private AvailablePortFinder.Port portRef;
+
+ @AfterEach
+ void releasePort() {
+ if (portRef != null) {
+ portRef.release();
+ portRef = null;
+ }
+ }
@Override
protected CamelContext createCamelContext() throws Exception {
CamelContext camelContext = super.createCamelContext();
+ portRef = AvailablePortFinder.find();
MainHttpServer server = new MainHttpServer();
server.setCamelContext(camelContext);
server.setHost("0.0.0.0");
- server.setPort(port);
+ server.setPort(portRef.getPort());
camelContext.addService(server);
McpServerConfiguration configuration = new McpServerConfiguration();
@@ -218,7 +228,7 @@ class OfficialMcpConformanceIT extends CamelTestSupport {
@Test
void testOfficialMcpConformanceSuite() throws Exception {
- String serverUrl = "http://localhost:" + port + "/mcp";
+ String serverUrl = "http://localhost:" + portRef.getPort() + "/mcp";
Path expectedFailures = expectedFailuresPath();
LOG.info("Running official MCP conformance suite against {}",
serverUrl);