From ae8a5019f53c5caa816afd897f0a15feadd38841 Mon Sep 17 00:00:00 2001 From: croway Date: Mon, 3 Aug 2026 17:27:39 +0200 Subject: [PATCH 1/9] Fixes #8949. Add camel-quarkus-mcp-server extension Expose ai-tool routes as MCP tools over streamable HTTP through the quarkiverse quarkus-mcp-server extension (CAMEL-24308): - JVM-only extension depending on camel-mcp-server-api (the runtime-agnostic bridge + McpServerEngine SPI) - deliberately NOT the camel-mcp-server Vert.x engine used on Camel Main/JBang, so the MCP Java SDK and Reactor never reach the Quarkus classpath. - QuarkusMcpServerEngine publishes tools through the programmatic ToolManager API (list_changed emitted by quarkus-mcp-server); tool arguments are mapped from the ai-tool ParameterDef metadata. - quarkus.camel.mcp-server.* config (enabled default true, tags opt-in, tool-timeout 20s); serving concerns (path, transports, dev UI, auth, guardrails) stay owned by quarkus.mcp.server.*. - Integration tests run the engine conformance scenarios with the official MCP SDK client over streamable HTTP: tag filtering (untagged pool excluded), call success, sanitized execution errors, timeout, tools list following route stop/start. Co-Authored-By: Claude Fable 5 --- catalog/pom.xml | 13 ++ .../ROOT/examples/others/mcp-server.yml | 13 ++ .../reference/extensions/mcp-server.adoc | 119 +++++++++++++++++ extensions-jvm/mcp-server/deployment/pom.xml | 67 ++++++++++ .../server/deployment/McpServerProcessor.java | 67 ++++++++++ extensions-jvm/mcp-server/pom.xml | 39 ++++++ extensions-jvm/mcp-server/runtime/pom.xml | 107 +++++++++++++++ .../runtime/src/main/doc/intro.adoc | 13 ++ .../runtime/src/main/doc/usage.adoc | 28 ++++ .../mcp/server/CamelMcpServerConfig.java | 58 ++++++++ .../mcp/server/CamelMcpServerRecorder.java | 54 ++++++++ .../mcp/server/QuarkusMcpServerEngine.java | 104 +++++++++++++++ .../resources/META-INF/quarkus-extension.yaml | 39 ++++++ extensions-jvm/pom.xml | 1 + integration-tests-jvm/mcp-server/pom.xml | 124 +++++++++++++++++ .../mcp/server/it/McpServerRoutes.java | 47 +++++++ .../src/main/resources/application.properties | 18 +++ .../mcp/server/it/McpServerTest.java | 126 ++++++++++++++++++ integration-tests-jvm/pom.xml | 1 + pom.xml | 1 + poms/bom/pom.xml | 25 ++++ 21 files changed, 1064 insertions(+) create mode 100644 docs/modules/ROOT/examples/others/mcp-server.yml create mode 100644 docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc create mode 100644 extensions-jvm/mcp-server/deployment/pom.xml create mode 100644 extensions-jvm/mcp-server/deployment/src/main/java/org/apache/camel/quarkus/component/mcp/server/deployment/McpServerProcessor.java create mode 100644 extensions-jvm/mcp-server/pom.xml create mode 100644 extensions-jvm/mcp-server/runtime/pom.xml create mode 100644 extensions-jvm/mcp-server/runtime/src/main/doc/intro.adoc create mode 100644 extensions-jvm/mcp-server/runtime/src/main/doc/usage.adoc create mode 100644 extensions-jvm/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerConfig.java create mode 100644 extensions-jvm/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerRecorder.java create mode 100644 extensions-jvm/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/QuarkusMcpServerEngine.java create mode 100644 extensions-jvm/mcp-server/runtime/src/main/resources/META-INF/quarkus-extension.yaml create mode 100644 integration-tests-jvm/mcp-server/pom.xml create mode 100644 integration-tests-jvm/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerRoutes.java create mode 100644 integration-tests-jvm/mcp-server/src/main/resources/application.properties create mode 100644 integration-tests-jvm/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java diff --git a/catalog/pom.xml b/catalog/pom.xml index 4da6127a695..82cbaf373af 100644 --- a/catalog/pom.xml +++ b/catalog/pom.xml @@ -2827,6 +2827,19 @@ + + org.apache.camel.quarkus + camel-quarkus-mcp-server + ${project.version} + pom + test + + + * + * + + + org.apache.camel.quarkus camel-quarkus-mdc diff --git a/docs/modules/ROOT/examples/others/mcp-server.yml b/docs/modules/ROOT/examples/others/mcp-server.yml new file mode 100644 index 00000000000..c24f8f1eaac --- /dev/null +++ b/docs/modules/ROOT/examples/others/mcp-server.yml @@ -0,0 +1,13 @@ +# Do not edit directly! +# This file was generated by camel-quarkus-maven-plugin:update-extension-doc-page +cqArtifactId: camel-quarkus-mcp-server +cqArtifactIdBase: mcp-server +cqNativeSupported: false +cqStatus: Preview +cqDeprecated: false +cqJvmSince: 3.39.0 +cqNativeSince: n/a +cqCamelPartName: mcp-server +cqCamelPartTitle: MCP Server +cqCamelPartDescription: Expose ai-tool routes as MCP tools over streamable HTTP +cqExtensionPageTitle: MCP Server diff --git a/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc b/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc new file mode 100644 index 00000000000..cc786cb942c --- /dev/null +++ b/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc @@ -0,0 +1,119 @@ +// Do not edit directly! +// This file was generated by camel-quarkus-maven-plugin:update-extension-doc-page +[id="extensions-mcp-server"] += MCP Server +:linkattrs: +:cq-artifact-id: camel-quarkus-mcp-server +:cq-native-supported: false +:cq-status: Preview +:cq-status-deprecation: Preview +:cq-description: Expose ai-tool routes as MCP tools over streamable HTTP +:cq-deprecated: false +:cq-jvm-since: 3.39.0 +:cq-native-since: n/a + +ifeval::[{doc-show-badges} == true] +[.badges] +[.badge-key]##JVM since##[.badge-supported]##3.39.0## [.badge-key]##Native##[.badge-unsupported]##unsupported## +endif::[] + +The MCP Server extension exposes Camel routes registered via the `ai-tool` +component as tools of a Model Context Protocol (MCP) server, served through +the quarkiverse https://docs.quarkiverse.io/quarkus-mcp-server/dev/index.html[quarkus-mcp-server] +extension. No route is needed for the server itself: add the extension, tag +the `ai-tool` routes to expose, and any MCP client (another Camel +application, an IDE, a coding agent) can discover and call them. + +Tool semantics — tag-based opt-in (the untagged default pool is never +exposed), flat-namespace collision refusal, per-call timeout and error +sanitization — are owned by the runtime-agnostic `camel-mcp-server-api` +bridge and are identical on every Camel runtime. Serving concerns (endpoint +path, transports, authentication) are owned by quarkus-mcp-server and +configured via `quarkus.mcp.server.*`. + + +[id="extensions-mcp-server-whats-inside"] +== What's inside + +* xref:{cq-camel-components}:others:mcp-server.adoc[MCP Server] + +Please refer to the above link for usage and configuration details. + +[id="extensions-mcp-server-maven-coordinates"] +== Maven coordinates + +[source,xml] +---- + + org.apache.camel.quarkus + camel-quarkus-mcp-server + +---- +ifeval::[{doc-show-user-guide-link} == true] +Check the xref:user-guide/index.adoc[User guide] for more information about writing Camel Quarkus applications. +endif::[] + +[id="extensions-mcp-server-usage"] +== Usage +Define tools as regular `ai-tool` routes and give them tags: + +[source,java] +---- +from("ai-tool:query_db?tags=crm&description=Query customer database" + + "¶meter.customerId=string¶meter.customerId.required=true") + .to("jdbc:dataSource"); +---- + +Select the tags to expose in `application.properties`: + +[source,properties] +---- +quarkus.camel.mcp-server.tags = crm,notify +---- + +The MCP endpoint is served by quarkus-mcp-server over streamable HTTP +(default path `/mcp`) — see the +https://docs.quarkiverse.io/quarkus-mcp-server/dev/index.html[quarkus-mcp-server documentation] +for the `quarkus.mcp.server.*` serving options (path, transports, dev UI, +authentication, guardrails). Tools appearing or disappearing (routes +starting and stopping) emit `notifications/tools/list_changed` to connected +clients, and Camel tools coexist with tools defined natively with +quarkus-mcp-server's `@Tool` annotation on the same server. + +On Camel Main and Camel JBang the equivalent setup is the +`camel-mcp-server` module with the `camel.server.mcp-*` options; on Spring +Boot it is the `camel-mcp-server-starter`. + + +[id="extensions-mcp-server-additional-camel-quarkus-configuration"] +== Additional Camel Quarkus configuration + +[width="100%",cols="80,5,15",options="header"] +|=== +| Configuration property | Type | Default + + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-mcp-server-enabled]]`link:#quarkus-camel-mcp-server-enabled[quarkus.camel.mcp-server.enabled]` + +Whether to expose ai-tool routes as MCP tools through the quarkus-mcp-server extension. +| `boolean` +| `true` + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-mcp-server-tags]]`link:#quarkus-camel-mcp-server-tags[quarkus.camel.mcp-server.tags]` + +Comma-separated list of ai-tool tags to expose as MCP tools. Only tools registered under one of these tags are +exposed; the untagged default pool is never exposed. When not set, no tools are exposed. +| `string` +| + +a|icon:lock[title=Fixed at build time] [[quarkus-camel-mcp-server-tool-timeout]]`link:#quarkus-camel-mcp-server-tool-timeout[quarkus.camel.mcp-server.tool-timeout]` + +Per-call tool execution timeout in milliseconds. A call exceeding the timeout returns an error result to the +MCP client; the underlying route keeps running until it completes on its own. +| `long` +| `20000` +|=== + +[.configuration-legend] +{doc-link-icon-lock}[title=Fixed at build time] Configuration property fixed at build time. All other configuration properties are overridable at runtime. + diff --git a/extensions-jvm/mcp-server/deployment/pom.xml b/extensions-jvm/mcp-server/deployment/pom.xml new file mode 100644 index 00000000000..908eb319600 --- /dev/null +++ b/extensions-jvm/mcp-server/deployment/pom.xml @@ -0,0 +1,67 @@ + + + + 4.0.0 + + org.apache.camel.quarkus + camel-quarkus-mcp-server-parent + 3.39.0-SNAPSHOT + ../pom.xml + + + camel-quarkus-mcp-server-deployment + Camel Quarkus :: MCP Server :: Deployment + + + + org.apache.camel.quarkus + camel-quarkus-core-deployment + + + org.apache.camel.quarkus + camel-quarkus-mcp-server + + + io.quarkiverse.mcp + quarkus-mcp-server-http-deployment + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + + diff --git a/extensions-jvm/mcp-server/deployment/src/main/java/org/apache/camel/quarkus/component/mcp/server/deployment/McpServerProcessor.java b/extensions-jvm/mcp-server/deployment/src/main/java/org/apache/camel/quarkus/component/mcp/server/deployment/McpServerProcessor.java new file mode 100644 index 00000000000..dcf34ea75a6 --- /dev/null +++ b/extensions-jvm/mcp-server/deployment/src/main/java/org/apache/camel/quarkus/component/mcp/server/deployment/McpServerProcessor.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.mcp.server.deployment; + +import java.util.function.BooleanSupplier; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; +import org.apache.camel.quarkus.component.mcp.server.CamelMcpServerConfig; +import org.apache.camel.quarkus.component.mcp.server.CamelMcpServerRecorder; +import org.apache.camel.quarkus.core.JvmOnlyRecorder; +import org.apache.camel.quarkus.core.deployment.spi.RuntimeCamelContextCustomizerBuildItem; +import org.jboss.logging.Logger; + +class McpServerProcessor { + + private static final Logger LOG = Logger.getLogger(McpServerProcessor.class); + private static final String FEATURE = "camel-mcp-server"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep(onlyIf = McpServerEnabled.class) + @Record(ExecutionTime.RUNTIME_INIT) + RuntimeCamelContextCustomizerBuildItem mcpServerBridge(CamelMcpServerRecorder recorder, CamelMcpServerConfig config) { + return new RuntimeCamelContextCustomizerBuildItem( + recorder.createContextCustomizer(config.tags().orElse(null), config.toolTimeout())); + } + + /** + * Remove this once this extension starts supporting the native mode. + */ + @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) + @Record(value = ExecutionTime.RUNTIME_INIT) + void warnJvmInNative(JvmOnlyRecorder recorder) { + JvmOnlyRecorder.warnJvmInNative(LOG, FEATURE); // warn at build time + recorder.warnJvmInNative(FEATURE); // warn at runtime + } + + static final class McpServerEnabled implements BooleanSupplier { + CamelMcpServerConfig config; + + @Override + public boolean getAsBoolean() { + return config.enabled(); + } + } +} diff --git a/extensions-jvm/mcp-server/pom.xml b/extensions-jvm/mcp-server/pom.xml new file mode 100644 index 00000000000..2db7aa1ac53 --- /dev/null +++ b/extensions-jvm/mcp-server/pom.xml @@ -0,0 +1,39 @@ + + + + 4.0.0 + + org.apache.camel.quarkus + camel-quarkus-extensions-jvm + 3.39.0-SNAPSHOT + ../pom.xml + + + camel-quarkus-mcp-server-parent + Camel Quarkus :: MCP Server + pom + + + deployment + runtime + + diff --git a/extensions-jvm/mcp-server/runtime/pom.xml b/extensions-jvm/mcp-server/runtime/pom.xml new file mode 100644 index 00000000000..01fbd9ecc83 --- /dev/null +++ b/extensions-jvm/mcp-server/runtime/pom.xml @@ -0,0 +1,107 @@ + + + + 4.0.0 + + org.apache.camel.quarkus + camel-quarkus-mcp-server-parent + 3.39.0-SNAPSHOT + ../pom.xml + + + camel-quarkus-mcp-server + Camel Quarkus :: MCP Server :: Runtime + Expose ai-tool routes as MCP tools over streamable HTTP + + + 3.39.0 + + + + + org.apache.camel.quarkus + camel-quarkus-core + + + + org.apache.camel + camel-mcp-server-api + + + io.quarkiverse.mcp + quarkus-mcp-server-http + + + + + + + io.quarkus + quarkus-extension-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + + + + + full + + + !quickly + + + + + + org.apache.camel.quarkus + camel-quarkus-maven-plugin + + + update-extension-doc-page + + update-extension-doc-page + + process-classes + + + + + + + + diff --git a/extensions-jvm/mcp-server/runtime/src/main/doc/intro.adoc b/extensions-jvm/mcp-server/runtime/src/main/doc/intro.adoc new file mode 100644 index 00000000000..7c60ee6f2d4 --- /dev/null +++ b/extensions-jvm/mcp-server/runtime/src/main/doc/intro.adoc @@ -0,0 +1,13 @@ +The MCP Server extension exposes Camel routes registered via the `ai-tool` +component as tools of a Model Context Protocol (MCP) server, served through +the quarkiverse https://docs.quarkiverse.io/quarkus-mcp-server/dev/index.html[quarkus-mcp-server] +extension. No route is needed for the server itself: add the extension, tag +the `ai-tool` routes to expose, and any MCP client (another Camel +application, an IDE, a coding agent) can discover and call them. + +Tool semantics — tag-based opt-in (the untagged default pool is never +exposed), flat-namespace collision refusal, per-call timeout and error +sanitization — are owned by the runtime-agnostic `camel-mcp-server-api` +bridge and are identical on every Camel runtime. Serving concerns (endpoint +path, transports, authentication) are owned by quarkus-mcp-server and +configured via `quarkus.mcp.server.*`. diff --git a/extensions-jvm/mcp-server/runtime/src/main/doc/usage.adoc b/extensions-jvm/mcp-server/runtime/src/main/doc/usage.adoc new file mode 100644 index 00000000000..58f128b96b7 --- /dev/null +++ b/extensions-jvm/mcp-server/runtime/src/main/doc/usage.adoc @@ -0,0 +1,28 @@ +Define tools as regular `ai-tool` routes and give them tags: + +[source,java] +---- +from("ai-tool:query_db?tags=crm&description=Query customer database" + + "¶meter.customerId=string¶meter.customerId.required=true") + .to("jdbc:dataSource"); +---- + +Select the tags to expose in `application.properties`: + +[source,properties] +---- +quarkus.camel.mcp-server.tags = crm,notify +---- + +The MCP endpoint is served by quarkus-mcp-server over streamable HTTP +(default path `/mcp`) — see the +https://docs.quarkiverse.io/quarkus-mcp-server/dev/index.html[quarkus-mcp-server documentation] +for the `quarkus.mcp.server.*` serving options (path, transports, dev UI, +authentication, guardrails). Tools appearing or disappearing (routes +starting and stopping) emit `notifications/tools/list_changed` to connected +clients, and Camel tools coexist with tools defined natively with +quarkus-mcp-server's `@Tool` annotation on the same server. + +On Camel Main and Camel JBang the equivalent setup is the +`camel-mcp-server` module with the `camel.server.mcp-*` options; on Spring +Boot it is the `camel-mcp-server-starter`. diff --git a/extensions-jvm/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerConfig.java b/extensions-jvm/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerConfig.java new file mode 100644 index 00000000000..5975fc5d86f --- /dev/null +++ b/extensions-jvm/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerConfig.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.mcp.server; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +/** + * Bridge-owned configuration of the Camel MCP server. Serving concerns (endpoint path, transports, authentication) + * are owned by the quarkiverse quarkus-mcp-server extension and configured via {@code quarkus.mcp.server.*}. + */ +@ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) +@ConfigMapping(prefix = "quarkus.camel.mcp-server") +public interface CamelMcpServerConfig { + + /** + * Whether to expose ai-tool routes as MCP tools through the quarkus-mcp-server extension. + * + * @asciidoclet + */ + @WithDefault("true") + boolean enabled(); + + /** + * Comma-separated list of ai-tool tags to expose as MCP tools. Only tools registered under one of these tags are + * exposed; the untagged default pool is never exposed. When not set, no tools are exposed. + * + * @asciidoclet + */ + Optional tags(); + + /** + * Per-call tool execution timeout in milliseconds. A call exceeding the timeout returns an error result to the + * MCP client; the underlying route keeps running until it completes on its own. + * + * @asciidoclet + */ + @WithDefault("20000") + long toolTimeout(); +} diff --git a/extensions-jvm/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerRecorder.java b/extensions-jvm/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerRecorder.java new file mode 100644 index 00000000000..f7d5ef2157c --- /dev/null +++ b/extensions-jvm/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerRecorder.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.mcp.server; + +import io.quarkiverse.mcp.server.ToolManager; +import io.quarkus.arc.Arc; +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; +import org.apache.camel.CamelContext; +import org.apache.camel.RuntimeCamelException; +import org.apache.camel.component.mcp.server.McpServerBridge; +import org.apache.camel.component.mcp.server.McpServerConfiguration; +import org.apache.camel.component.mcp.server.McpServerEngine; +import org.apache.camel.spi.CamelContextCustomizer; + +@Recorder +public class CamelMcpServerRecorder { + + public RuntimeValue createContextCustomizer(String tags, long toolTimeout) { + return new RuntimeValue<>(new CamelContextCustomizer() { + @Override + public void configure(CamelContext camelContext) { + ToolManager toolManager = Arc.container().instance(ToolManager.class).get(); + QuarkusMcpServerEngine engine = new QuarkusMcpServerEngine(toolManager); + + McpServerConfiguration configuration = new McpServerConfiguration(); + configuration.setTags(tags); + configuration.setToolTimeout(toolTimeout); + McpServerBridge bridge = new McpServerBridge(configuration); + try { + // the bridge resolves the engine registry-first + camelContext.getRegistry().bind("quarkusCamelMcpServerEngine", McpServerEngine.class, engine); + camelContext.addService(bridge); + } catch (Exception e) { + throw RuntimeCamelException.wrapRuntimeCamelException(e); + } + } + }); + } +} diff --git a/extensions-jvm/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/QuarkusMcpServerEngine.java b/extensions-jvm/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/QuarkusMcpServerEngine.java new file mode 100644 index 00000000000..2f9a19f6be0 --- /dev/null +++ b/extensions-jvm/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/QuarkusMcpServerEngine.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.mcp.server; + +import java.lang.reflect.Type; +import java.util.Map; + +import io.quarkiverse.mcp.server.TextContent; +import io.quarkiverse.mcp.server.ToolManager; +import io.quarkiverse.mcp.server.ToolResponse; +import org.apache.camel.CamelContext; +import org.apache.camel.component.ai.tool.AiToolParameterHelper.ParameterDef; +import org.apache.camel.component.mcp.server.McpServerEngine; +import org.apache.camel.component.mcp.server.McpServerInfo; +import org.apache.camel.component.mcp.server.McpServerTool; +import org.apache.camel.component.mcp.server.McpToolCallResult; +import org.apache.camel.support.service.ServiceSupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link McpServerEngine} publishing tools into the quarkiverse quarkus-mcp-server through its programmatic + * {@link ToolManager} API. Serving concerns (endpoint path, transports, authentication) are owned by + * quarkus-mcp-server and configured via {@code quarkus.mcp.server.*}. + */ +public class QuarkusMcpServerEngine extends ServiceSupport implements McpServerEngine { + + private static final Logger LOG = LoggerFactory.getLogger(QuarkusMcpServerEngine.class); + + private final ToolManager toolManager; + private CamelContext camelContext; + + public QuarkusMcpServerEngine(ToolManager toolManager) { + this.toolManager = toolManager; + } + + @Override + public CamelContext getCamelContext() { + return camelContext; + } + + @Override + public void setCamelContext(CamelContext camelContext) { + this.camelContext = camelContext; + } + + @Override + public void initialize(McpServerInfo info) { + // serving identity is owned by quarkus.mcp.server.*; the hint is deliberately ignored + LOG.debug("quarkus-mcp-server engine initialized; serving configuration is owned by quarkus.mcp.server.*"); + } + + @Override + public void toolAdded(McpServerTool tool) { + ToolManager.ToolDefinition definition = toolManager.newTool(tool.name()) + .setDescription(tool.description()); + for (Map.Entry parameter : tool.parameters().entrySet()) { + ParameterDef def = parameter.getValue(); + definition.addArgument(parameter.getKey(), def.getDescription(), def.isRequired(), javaType(def)); + } + definition.setHandler(arguments -> { + Map args = arguments.args() != null ? arguments.args() : Map.of(); + McpToolCallResult result = tool.handler().call(args); + return result.isError() + ? ToolResponse.error(result.text()) : ToolResponse.success(new TextContent(result.text())); + }); + definition.register(); + LOG.debug("MCP tool added: {}", tool.name()); + } + + @Override + public void toolRemoved(String toolName) { + try { + toolManager.removeTool(toolName); + LOG.debug("MCP tool removed: {}", toolName); + } catch (Exception e) { + LOG.debug("Failed to remove MCP tool {}: {}", toolName, e.getMessage()); + } + } + + private static Type javaType(ParameterDef def) { + String type = def.getType() != null ? def.getType() : "string"; + return switch (type) { + case "integer", "int", "long" -> Long.class; + case "number", "double", "float" -> Double.class; + case "boolean", "bool" -> Boolean.class; + default -> String.class; + }; + } +} diff --git a/extensions-jvm/mcp-server/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions-jvm/mcp-server/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 00000000000..d9a2566e733 --- /dev/null +++ b/extensions-jvm/mcp-server/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,39 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# This is a generated file. Do not edit directly! +# To re-generate, run the following command from the top level directory: +# +# mvn -N cq:update-quarkus-metadata +# +--- +name: "Camel MCP Server" +description: "Expose ai-tool routes as MCP tools over streamable HTTP" +metadata: + icon-url: "https://raw.githubusercontent.com/apache/camel-website/main/antora-ui-camel/src/img/logo-d.svg" + sponsor: "Apache Software Foundation" + unlisted: true + guide: "https://camel.apache.org/camel-quarkus/latest/reference/extensions/mcp-server.html" + categories: + - "integration" + config: + - "quarkus.camel.mcp-server" + status: "stable" + integrates: + - name: "Camel" + artifact: "org.apache.camel:camel-base" + version: "${camel.version}" \ No newline at end of file diff --git a/extensions-jvm/pom.xml b/extensions-jvm/pom.xml index ce94a9b29be..0a090c24f0f 100644 --- a/extensions-jvm/pom.xml +++ b/extensions-jvm/pom.xml @@ -74,6 +74,7 @@ jsonapi ldif lucene + mcp-server mvel opensearch printer diff --git a/integration-tests-jvm/mcp-server/pom.xml b/integration-tests-jvm/mcp-server/pom.xml new file mode 100644 index 00000000000..b8d1aa3ef19 --- /dev/null +++ b/integration-tests-jvm/mcp-server/pom.xml @@ -0,0 +1,124 @@ + + + + 4.0.0 + + org.apache.camel.quarkus + camel-quarkus-build-parent-it + 3.39.0-SNAPSHOT + ../../poms/build-parent-it/pom.xml + + + camel-quarkus-integration-test-mcp-server + Camel Quarkus :: Integration Tests :: MCP Server + Integration tests for Camel Quarkus MCP Server extension + + + 2.0.0 + + + + + org.apache.camel.quarkus + camel-quarkus-mcp-server + + + org.apache.camel.quarkus + camel-quarkus-ai-tool + + + + + + io.modelcontextprotocol.sdk + mcp-core + ${mcp-java-sdk.version} + test + + + io.modelcontextprotocol.sdk + mcp-json-jackson2 + ${mcp-java-sdk.version} + test + + + org.awaitility + awaitility + test + + + org.assertj + assertj-core + test + + + io.quarkus + quarkus-junit + test + + + io.rest-assured + rest-assured + test + + + + + + virtualDependencies + + + !noVirtualDependencies + + + + + + org.apache.camel.quarkus + camel-quarkus-ai-tool-deployment + ${project.version} + pom + test + + + * + * + + + + + org.apache.camel.quarkus + camel-quarkus-mcp-server-deployment + ${project.version} + pom + test + + + * + * + + + + + + + diff --git a/integration-tests-jvm/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerRoutes.java b/integration-tests-jvm/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerRoutes.java new file mode 100644 index 00000000000..ab1efba1681 --- /dev/null +++ b/integration-tests-jvm/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerRoutes.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.mcp.server.it; + +import jakarta.enterprise.context.ApplicationScoped; +import org.apache.camel.builder.RouteBuilder; + +@ApplicationScoped +public class McpServerRoutes extends RouteBuilder { + + @Override + public void configure() { + from("ai-tool:say_hello?tags=conformance&description=Say hello" + + "¶meter.name=string¶meter.name.description=Who to greet¶meter.name.required=true") + .routeId("say-hello-route") + .setBody(simple("Hello ${header.name}")); + + from("ai-tool:fail_tool?tags=conformance&description=Always fails") + .process(e -> { + throw new IllegalStateException("secret internal detail"); + }); + + from("ai-tool:slow_tool?tags=conformance&description=Exceeds the tool timeout") + .delay(6000) + .setBody(constant("done")); + + from("ai-tool:hidden_tool?description=Untagged tool, must not be exposed") + .setBody(constant("hidden")); + + from("ai-tool:other_tool?tags=untrusted&description=Not a selected tag, must not be exposed") + .setBody(constant("other")); + } +} diff --git a/integration-tests-jvm/mcp-server/src/main/resources/application.properties b/integration-tests-jvm/mcp-server/src/main/resources/application.properties new file mode 100644 index 00000000000..91728bf6438 --- /dev/null +++ b/integration-tests-jvm/mcp-server/src/main/resources/application.properties @@ -0,0 +1,18 @@ +## --------------------------------------------------------------------------- +## Licensed to the Apache Software Foundation (ASF) under one or more +## contributor license agreements. See the NOTICE file distributed with +## this work for additional information regarding copyright ownership. +## The ASF licenses this file to You under the Apache License, Version 2.0 +## (the "License"); you may not use this file except in compliance with +## the License. You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## --------------------------------------------------------------------------- +quarkus.camel.mcp-server.tags=conformance +quarkus.camel.mcp-server.tool-timeout=2000 diff --git a/integration-tests-jvm/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java b/integration-tests-jvm/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java new file mode 100644 index 00000000000..c731bb9b3ee --- /dev/null +++ b/integration-tests-jvm/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.mcp.server.it; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import io.modelcontextprotocol.client.McpClient; +import io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; +import io.modelcontextprotocol.spec.McpSchema; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.apache.camel.CamelContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +/** + * The engine conformance scenarios (CAMEL-24313) against the quarkus-mcp-server engine, driven by the official MCP SDK + * client over streamable HTTP. Mirrors {@code McpServerConformanceTestSupport} from camel-mcp-server-api, which cannot + * be reused as-is here because it manages its own CamelContext outside Quarkus. + */ +@QuarkusTest +class McpServerTest { + + @Inject + CamelContext camelContext; + + private McpSyncClient client; + + private McpSyncClient client() { + if (client == null) { + client = McpClient + .sync(HttpClientStreamableHttpTransport.builder("http://localhost:8081").build()) + .requestTimeout(Duration.ofSeconds(10)) + .initializationTimeout(Duration.ofSeconds(10)) + .build(); + client.initialize(); + } + return client; + } + + @AfterEach + void closeClient() { + if (client != null) { + client.closeGracefully(); + client = null; + } + } + + @Test + void testListToolsExposesOnlySelectedTags() { + List tools = client().listTools().tools(); + + assertThat(tools).extracting(McpSchema.Tool::name) + .contains("say_hello", "fail_tool", "slow_tool") + .doesNotContain("hidden_tool", "other_tool"); + } + + @Test + void testCallToolSuccess() { + McpSchema.CallToolResult result = client() + .callTool(new McpSchema.CallToolRequest("say_hello", Map.of("name", "World"))); + + assertThat(result.isError()).isNotEqualTo(Boolean.TRUE); + assertThat(textOf(result)).isEqualTo("Hello World"); + } + + @Test + void testCallToolExecutionErrorIsSanitized() { + McpSchema.CallToolResult result = client().callTool(new McpSchema.CallToolRequest("fail_tool", Map.of())); + + assertThat(result.isError()).isEqualTo(Boolean.TRUE); + assertThat(textOf(result)) + .doesNotContain("secret internal detail") + .isEqualTo("Tool execution failed"); + } + + @Test + void testCallToolTimeout() { + McpSchema.CallToolResult result = client().callTool(new McpSchema.CallToolRequest("slow_tool", Map.of())); + + assertThat(result.isError()).isEqualTo(Boolean.TRUE); + assertThat(textOf(result)).contains("timed out"); + } + + @Test + void testToolsListReflectsRouteStopAndStart() throws Exception { + assertThat(client().listTools().tools()).extracting(McpSchema.Tool::name).contains("say_hello"); + + camelContext.getRouteController().stopRoute("say-hello-route"); + await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> assertThat(client().listTools().tools()) + .extracting(McpSchema.Tool::name).doesNotContain("say_hello")); + + camelContext.getRouteController().startRoute("say-hello-route"); + await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> assertThat(client().listTools().tools()) + .extracting(McpSchema.Tool::name).contains("say_hello")); + } + + private static String textOf(McpSchema.CallToolResult result) { + return result.content().stream() + .filter(McpSchema.TextContent.class::isInstance) + .map(c -> ((McpSchema.TextContent) c).text()) + .collect(Collectors.joining()); + } +} diff --git a/integration-tests-jvm/pom.xml b/integration-tests-jvm/pom.xml index fb2cbd3412e..2e3c5a8ed69 100644 --- a/integration-tests-jvm/pom.xml +++ b/integration-tests-jvm/pom.xml @@ -75,6 +75,7 @@ ldif lucene main-devmode + mcp-server mvel opensearch printer diff --git a/pom.xml b/pom.xml index bf1e32703e6..b842437ec5b 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,7 @@ 1.3.0 1.4.0 3.38.0 + 1.13.1 2.5.2 3.6.2 3.2.0 diff --git a/poms/bom/pom.xml b/poms/bom/pom.xml index ef15e3593ed..f2c20ce769f 100644 --- a/poms/bom/pom.xml +++ b/poms/bom/pom.xml @@ -2310,6 +2310,11 @@ camel-master ${camel.version} + + org.apache.camel + camel-mcp-server-api + ${camel.version} + org.apache.camel camel-mdc @@ -5650,6 +5655,16 @@ camel-quarkus-master-deployment ${camel-quarkus.version} + + org.apache.camel.quarkus + camel-quarkus-mcp-server + ${camel-quarkus.version} + + + org.apache.camel.quarkus + camel-quarkus-mcp-server-deployment + ${camel-quarkus.version} + org.apache.camel.quarkus camel-quarkus-mdc @@ -7932,6 +7947,16 @@ quarkus-jsch-deployment ${quarkiverse-jsch.version} + + io.quarkiverse.mcp + quarkus-mcp-server-http + ${quarkiverse-mcp-server.version} + + + io.quarkiverse.mcp + quarkus-mcp-server-http-deployment + ${quarkiverse-mcp-server.version} + io.quarkiverse.messaginghub quarkus-pooled-jms From 49fbda5f5c3604aab610f6ff809610674cf18b23 Mon Sep 17 00:00:00 2001 From: croway Date: Mon, 3 Aug 2026 17:36:02 +0200 Subject: [PATCH 2/9] Add quarkus-mcp-server annotated tool coexistence test and richer usage docs - Prove @Tool-annotated business methods coexist with Camel ai-tool routes on the same MCP server. - Usage docs: combined quarkus.camel.mcp-server.* / quarkus.mcp.server.* configuration example with ownership split, link to the quarkus-mcp-server configuration reference, YAML DSL tool definition, MCP client configuration examples, dynamic tools and error handling. Co-Authored-By: Claude Fable 5 --- .../reference/extensions/mcp-server.adoc | 138 ++++++++++++++++-- .../runtime/src/main/doc/usage.adoc | 133 +++++++++++++++-- .../mcp/server/it/QuarkusDefinedTools.java | 33 +++++ .../mcp/server/it/McpServerTest.java | 12 ++ 4 files changed, 298 insertions(+), 18 deletions(-) create mode 100644 integration-tests-jvm/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java diff --git a/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc b/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc index cc786cb942c..e5b80f3047f 100644 --- a/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc +++ b/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc @@ -64,21 +64,141 @@ from("ai-tool:query_db?tags=crm&description=Query customer database" .to("jdbc:dataSource"); ---- -Select the tags to expose in `application.properties`: +or in YAML DSL: + +[source,yaml] +---- +- route: + from: + uri: "ai-tool:send_email" + parameters: + description: "Send email notification" + tags: "notify" + parameter.to: string + parameter.to.description: "Recipient address" + parameter.to.required: "true" + parameter.subject: string + parameter.priority: string + parameter.priority.enum: "low,normal,high" + steps: + - to: "smtp://mail.example.com" +---- + +Tool parameters are declared with the `parameter.NAME` options: the value is +the JSON type (`string`, `integer`, `number`, `boolean`), and the +`parameter.NAME.description`, `parameter.NAME.required` and +`parameter.NAME.enum` options refine the generated input schema. Arguments +arrive as message headers in the route (`${header.customerId}`). + +[id="extensions-mcp-server-usage-combining-the-camel-and-quarkus-mcp-server-configuration"] +=== Combining the Camel and quarkus-mcp-server configuration + +The configuration is split by ownership: `quarkus.camel.mcp-server.*` +decides *which tools exist and how they execute* (identical semantics on +every Camel runtime), while `quarkus.mcp.server.*` decides *how the server +is exposed*. Both are set side by side in `application.properties`: [source,properties] ---- +# Camel-owned: tool selection and execution quarkus.camel.mcp-server.tags = crm,notify +quarkus.camel.mcp-server.tool-timeout = 10000 + +# quarkus-mcp-server-owned: serving and server identity +quarkus.mcp.server.server-info.name = my-integration-app +quarkus.mcp.server.server-info.version = 1.0.0 +quarkus.mcp.server.http.root-path = /mcp + +# quarkus-mcp-server-owned: diagnostics while developing +quarkus.mcp.server.traffic-logging.enabled = true +quarkus.mcp.server.traffic-logging.text-limit = 200 ---- -The MCP endpoint is served by quarkus-mcp-server over streamable HTTP -(default path `/mcp`) — see the -https://docs.quarkiverse.io/quarkus-mcp-server/dev/index.html[quarkus-mcp-server documentation] -for the `quarkus.mcp.server.*` serving options (path, transports, dev UI, -authentication, guardrails). Tools appearing or disappearing (routes -starting and stopping) emit `notifications/tools/list_changed` to connected -clients, and Camel tools coexist with tools defined natively with -quarkus-mcp-server's `@Tool` annotation on the same server. +See the +https://docs.quarkiverse.io/quarkus-mcp-server/dev/index.html#configuration-reference[quarkus-mcp-server configuration reference] +for the full list of `quarkus.mcp.server.*` properties (transports, dev UI, +authentication, guardrails, pagination, timeouts). + +[id="extensions-mcp-server-usage-connecting-mcp-clients"] +=== Connecting MCP clients + +Any MCP client can connect over streamable HTTP. Another Camel integration +can consume the tools with the camel-openai MCP client and automatic tool +execution: + +[source,java] +---- +from("direct:agent") + .to("openai:chat-completion" + + "?model={{llm.model}}" + + "&autoToolExecution=true" + + "&mcpServer.myCamelTools.transportType=streamableHttp" + + "&mcpServer.myCamelTools.url=http://localhost:8080/mcp"); +---- + +A coding agent or IDE is configured with the same URL, e.g. in an +`mcp.json`-style client configuration: + +[source,json] +---- +{ + "mcpServers": { + "my-integration-app": { + "type": "http", + "url": "http://localhost:8080/mcp" + } + } +} +---- + +[id="extensions-mcp-server-usage-mixing-with-quarkus-mcp-server-annotated-tools"] +=== Mixing with quarkus-mcp-server annotated tools + +Camel tools coexist with tools defined natively with quarkus-mcp-server — +both are served by the same MCP server and appear in the same `tools/list`. +For example, a `@Tool`-annotated business method: + +[source,java] +---- +public class CalculatorTools { + + @Tool(name = "add_numbers", description = "Add two numbers") + String add( + @ToolArg(description = "First addend") long a, + @ToolArg(description = "Second addend") long b) { + return String.valueOf(a + b); + } +} +---- + +is exposed alongside the `ai-tool` routes. Annotated tools are registered at +build time; Camel tools are added and removed dynamically with the route +lifecycle. Choose distinct tool names — MCP has a flat tool namespace. + +[id="extensions-mcp-server-usage-dynamic-tools"] +=== Dynamic tools + +The exposed tool list follows the route lifecycle: stopping or suspending an +`ai-tool` route removes its tool, starting or resuming it publishes the tool +again, and connected clients are notified via +`notifications/tools/list_changed`: + +[source,java] +---- +camelContext.getRouteController().stopRoute("query-db-route"); // tool disappears +camelContext.getRouteController().startRoute("query-db-route"); // tool is back +---- + +[id="extensions-mcp-server-usage-error-handling"] +=== Error handling + +Results returned to MCP clients are sanitized by the bridge: a route +exception produces an error result with the generic message +`Tool execution failed` (the cause is logged server-side and never sent to +the client), a missing or invalid argument returns the validation message, +and a call exceeding `quarkus.camel.mcp-server.tool-timeout` returns +`Tool execution timed out` while the route keeps running until it completes +on its own. On Camel Main and Camel JBang the equivalent setup is the `camel-mcp-server` module with the `camel.server.mcp-*` options; on Spring diff --git a/extensions-jvm/mcp-server/runtime/src/main/doc/usage.adoc b/extensions-jvm/mcp-server/runtime/src/main/doc/usage.adoc index 58f128b96b7..e16eaf13209 100644 --- a/extensions-jvm/mcp-server/runtime/src/main/doc/usage.adoc +++ b/extensions-jvm/mcp-server/runtime/src/main/doc/usage.adoc @@ -7,21 +7,136 @@ from("ai-tool:query_db?tags=crm&description=Query customer database" .to("jdbc:dataSource"); ---- -Select the tags to expose in `application.properties`: +or in YAML DSL: + +[source,yaml] +---- +- route: + from: + uri: "ai-tool:send_email" + parameters: + description: "Send email notification" + tags: "notify" + parameter.to: string + parameter.to.description: "Recipient address" + parameter.to.required: "true" + parameter.subject: string + parameter.priority: string + parameter.priority.enum: "low,normal,high" + steps: + - to: "smtp://mail.example.com" +---- + +Tool parameters are declared with the `parameter.NAME` options: the value is +the JSON type (`string`, `integer`, `number`, `boolean`), and the +`parameter.NAME.description`, `parameter.NAME.required` and +`parameter.NAME.enum` options refine the generated input schema. Arguments +arrive as message headers in the route (`${header.customerId}`). + +=== Combining the Camel and quarkus-mcp-server configuration + +The configuration is split by ownership: `quarkus.camel.mcp-server.*` +decides *which tools exist and how they execute* (identical semantics on +every Camel runtime), while `quarkus.mcp.server.*` decides *how the server +is exposed*. Both are set side by side in `application.properties`: [source,properties] ---- +# Camel-owned: tool selection and execution quarkus.camel.mcp-server.tags = crm,notify +quarkus.camel.mcp-server.tool-timeout = 10000 + +# quarkus-mcp-server-owned: serving and server identity +quarkus.mcp.server.server-info.name = my-integration-app +quarkus.mcp.server.server-info.version = 1.0.0 +quarkus.mcp.server.http.root-path = /mcp + +# quarkus-mcp-server-owned: diagnostics while developing +quarkus.mcp.server.traffic-logging.enabled = true +quarkus.mcp.server.traffic-logging.text-limit = 200 +---- + +See the +https://docs.quarkiverse.io/quarkus-mcp-server/dev/index.html#configuration-reference[quarkus-mcp-server configuration reference] +for the full list of `quarkus.mcp.server.*` properties (transports, dev UI, +authentication, guardrails, pagination, timeouts). + +=== Connecting MCP clients + +Any MCP client can connect over streamable HTTP. Another Camel integration +can consume the tools with the camel-openai MCP client and automatic tool +execution: + +[source,java] +---- +from("direct:agent") + .to("openai:chat-completion" + + "?model={{llm.model}}" + + "&autoToolExecution=true" + + "&mcpServer.myCamelTools.transportType=streamableHttp" + + "&mcpServer.myCamelTools.url=http://localhost:8080/mcp"); +---- + +A coding agent or IDE is configured with the same URL, e.g. in an +`mcp.json`-style client configuration: + +[source,json] +---- +{ + "mcpServers": { + "my-integration-app": { + "type": "http", + "url": "http://localhost:8080/mcp" + } + } +} +---- + +=== Mixing with quarkus-mcp-server annotated tools + +Camel tools coexist with tools defined natively with quarkus-mcp-server — +both are served by the same MCP server and appear in the same `tools/list`. +For example, a `@Tool`-annotated business method: + +[source,java] +---- +public class CalculatorTools { + + @Tool(name = "add_numbers", description = "Add two numbers") + String add( + @ToolArg(description = "First addend") long a, + @ToolArg(description = "Second addend") long b) { + return String.valueOf(a + b); + } +} ---- -The MCP endpoint is served by quarkus-mcp-server over streamable HTTP -(default path `/mcp`) — see the -https://docs.quarkiverse.io/quarkus-mcp-server/dev/index.html[quarkus-mcp-server documentation] -for the `quarkus.mcp.server.*` serving options (path, transports, dev UI, -authentication, guardrails). Tools appearing or disappearing (routes -starting and stopping) emit `notifications/tools/list_changed` to connected -clients, and Camel tools coexist with tools defined natively with -quarkus-mcp-server's `@Tool` annotation on the same server. +is exposed alongside the `ai-tool` routes. Annotated tools are registered at +build time; Camel tools are added and removed dynamically with the route +lifecycle. Choose distinct tool names — MCP has a flat tool namespace. + +=== Dynamic tools + +The exposed tool list follows the route lifecycle: stopping or suspending an +`ai-tool` route removes its tool, starting or resuming it publishes the tool +again, and connected clients are notified via +`notifications/tools/list_changed`: + +[source,java] +---- +camelContext.getRouteController().stopRoute("query-db-route"); // tool disappears +camelContext.getRouteController().startRoute("query-db-route"); // tool is back +---- + +=== Error handling + +Results returned to MCP clients are sanitized by the bridge: a route +exception produces an error result with the generic message +`Tool execution failed` (the cause is logged server-side and never sent to +the client), a missing or invalid argument returns the validation message, +and a call exceeding `quarkus.camel.mcp-server.tool-timeout` returns +`Tool execution timed out` while the route keeps running until it completes +on its own. On Camel Main and Camel JBang the equivalent setup is the `camel-mcp-server` module with the `camel.server.mcp-*` options; on Spring diff --git a/integration-tests-jvm/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java b/integration-tests-jvm/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java new file mode 100644 index 00000000000..88a6cf149fb --- /dev/null +++ b/integration-tests-jvm/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.mcp.server.it; + +import io.quarkiverse.mcp.server.Tool; +import io.quarkiverse.mcp.server.ToolArg; + +/** + * A plain quarkus-mcp-server annotated tool living on the same MCP server as the Camel ai-tool routes. + */ +public class QuarkusDefinedTools { + + @Tool(name = "add_numbers", description = "Add two numbers") + String add( + @ToolArg(description = "First addend") long a, + @ToolArg(description = "Second addend") long b) { + return String.valueOf(a + b); + } +} diff --git a/integration-tests-jvm/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java b/integration-tests-jvm/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java index c731bb9b3ee..e43091afebb 100644 --- a/integration-tests-jvm/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java +++ b/integration-tests-jvm/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java @@ -77,6 +77,18 @@ void testListToolsExposesOnlySelectedTags() { .doesNotContain("hidden_tool", "other_tool"); } + @Test + void testQuarkusAnnotatedToolsCoexistWithCamelTools() { + // both tool sources are served by the same MCP server + assertThat(client().listTools().tools()).extracting(McpSchema.Tool::name) + .contains("add_numbers", "say_hello"); + + McpSchema.CallToolResult result = client() + .callTool(new McpSchema.CallToolRequest("add_numbers", Map.of("a", 17, "b", 25))); + assertThat(result.isError()).as(textOf(result)).isNotEqualTo(Boolean.TRUE); + assertThat(textOf(result)).isEqualTo("42"); + } + @Test void testCallToolSuccess() { McpSchema.CallToolResult result = client() From 86b772718ae9824d61c88ba1772b49f6c38514fc Mon Sep 17 00:00:00 2001 From: croway Date: Tue, 4 Aug 2026 09:56:11 +0200 Subject: [PATCH 3/9] Fixes #8949. Address review feedback on the camel-quarkus-mcp-server extension - Move the MCP Java SDK test dependency version to the root pom.xml test version properties, manage the artifacts in camel-quarkus-bom-test and drop the explicit versions from the integration test pom - Register io.modelcontextprotocol.sdk:mcp-core and mcp-json-jackson2 with the dependabot test dependencies - Replace the CamelContext injection in McpServerTest with an McpServerResource JAX-RS resource driven via RestAssured, so the test also works if the extension is promoted to native mode support - Keep quarkiverse-mcp-server.version in alphabetical order - Remove a redundant comment from the runtime pom - Regenerate the flattened BOMs Co-Authored-By: Claude Opus 5 (1M context) --- .github/dependabot.yml | 2 + extensions-jvm/mcp-server/runtime/pom.xml | 3 -- integration-tests-jvm/mcp-server/pom.xml | 10 ++-- .../mcp/server/it/McpServerResource.java | 52 +++++++++++++++++++ .../mcp/server/it/McpServerTest.java | 33 +++++++++--- pom.xml | 3 +- poms/bom-test/pom.xml | 12 +++++ .../src/main/generated/flattened-full-pom.xml | 25 +++++++++ .../main/generated/flattened-reduced-pom.xml | 25 +++++++++ .../flattened-reduced-verbose-pom.xml | 25 +++++++++ 10 files changed, 172 insertions(+), 18 deletions(-) create mode 100644 integration-tests-jvm/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerResource.java diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e85f10ad3ee..3b84b3c329c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -58,6 +58,8 @@ updates: - dependency-name: "org.zeroturnaround:zt-exec" - dependency-name: "com.microsoft.graph:microsoft-graph" - dependency-name: "com.microsoft.graph:microsoft-graph-auth" + - dependency-name: "io.modelcontextprotocol.sdk:mcp-core" + - dependency-name: "io.modelcontextprotocol.sdk:mcp-json-jackson2" # Maven extensions - dependency-name: "com.gradle:develocity-maven-extension" - dependency-name: "com.gradle:common-custom-user-data-maven-extension" diff --git a/extensions-jvm/mcp-server/runtime/pom.xml b/extensions-jvm/mcp-server/runtime/pom.xml index 01fbd9ecc83..1d628de640b 100644 --- a/extensions-jvm/mcp-server/runtime/pom.xml +++ b/extensions-jvm/mcp-server/runtime/pom.xml @@ -41,9 +41,6 @@ org.apache.camel.quarkus camel-quarkus-core - org.apache.camel camel-mcp-server-api diff --git a/integration-tests-jvm/mcp-server/pom.xml b/integration-tests-jvm/mcp-server/pom.xml index b8d1aa3ef19..e9e08ee87e3 100644 --- a/integration-tests-jvm/mcp-server/pom.xml +++ b/integration-tests-jvm/mcp-server/pom.xml @@ -32,10 +32,6 @@ Camel Quarkus :: Integration Tests :: MCP Server Integration tests for Camel Quarkus MCP Server extension - - 2.0.0 - - org.apache.camel.quarkus @@ -45,19 +41,21 @@ org.apache.camel.quarkus camel-quarkus-ai-tool + + io.quarkus + quarkus-resteasy + io.modelcontextprotocol.sdk mcp-core - ${mcp-java-sdk.version} test io.modelcontextprotocol.sdk mcp-json-jackson2 - ${mcp-java-sdk.version} test diff --git a/integration-tests-jvm/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerResource.java b/integration-tests-jvm/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerResource.java new file mode 100644 index 00000000000..1012e173fad --- /dev/null +++ b/integration-tests-jvm/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerResource.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.mcp.server.it; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.apache.camel.CamelContext; + +@Path("/mcp-server") +public class McpServerResource { + + @Inject + CamelContext camelContext; + + @Path("/route/{routeId}/start") + @POST + public void startRoute(@PathParam("routeId") String routeId) throws Exception { + camelContext.getRouteController().startRoute(routeId); + } + + @Path("/route/{routeId}/stop") + @POST + public void stopRoute(@PathParam("routeId") String routeId) throws Exception { + camelContext.getRouteController().stopRoute(routeId); + } + + @Path("/route/{routeId}/status") + @GET + @Produces(MediaType.TEXT_PLAIN) + public String routeStatus(@PathParam("routeId") String routeId) { + return camelContext.getRouteController().getRouteStatus(routeId).name(); + } +} diff --git a/integration-tests-jvm/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java b/integration-tests-jvm/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java index e43091afebb..d2d0a84ee6f 100644 --- a/integration-tests-jvm/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java +++ b/integration-tests-jvm/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java @@ -27,8 +27,7 @@ import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; import io.modelcontextprotocol.spec.McpSchema; import io.quarkus.test.junit.QuarkusTest; -import jakarta.inject.Inject; -import org.apache.camel.CamelContext; +import io.restassured.RestAssured; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -43,9 +42,6 @@ @QuarkusTest class McpServerTest { - @Inject - CamelContext camelContext; - private McpSyncClient client; private McpSyncClient client() { @@ -117,18 +113,39 @@ void testCallToolTimeout() { } @Test - void testToolsListReflectsRouteStopAndStart() throws Exception { + void testToolsListReflectsRouteStopAndStart() { assertThat(client().listTools().tools()).extracting(McpSchema.Tool::name).contains("say_hello"); - camelContext.getRouteController().stopRoute("say-hello-route"); + controlRoute("stop"); + assertThat(routeStatus()).isEqualTo("Stopped"); await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> assertThat(client().listTools().tools()) .extracting(McpSchema.Tool::name).doesNotContain("say_hello")); - camelContext.getRouteController().startRoute("say-hello-route"); + controlRoute("start"); + assertThat(routeStatus()).isEqualTo("Started"); await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> assertThat(client().listTools().tools()) .extracting(McpSchema.Tool::name).contains("say_hello")); } + private static void controlRoute(String action) { + RestAssured.given() + .when() + .post("/mcp-server/route/say-hello-route/" + action) + .then() + .statusCode(204); + } + + private static String routeStatus() { + return RestAssured.given() + .when() + .get("/mcp-server/route/say-hello-route/status") + .then() + .statusCode(200) + .extract() + .body() + .asString(); + } + private static String textOf(McpSchema.CallToolResult result) { return result.content().stream() .filter(McpSchema.TextContent.class::isInstance) diff --git a/pom.xml b/pom.xml index b842437ec5b..bf6488c8b47 100644 --- a/pom.xml +++ b/pom.xml @@ -56,11 +56,11 @@ 1.3.0 1.4.0 3.38.0 - 1.13.1 2.5.2 3.6.2 3.2.0 1.12.2 + 1.13.1 3.5.0 3.9.1 2.4.2 @@ -190,6 +190,7 @@ ${ftpserver-version} 3.0 4.13.0 + 2.0.0 6.67.0 0.3.0 ${pdfbox-version} diff --git a/poms/bom-test/pom.xml b/poms/bom-test/pom.xml index ca3cfd8ecf3..b618103349f 100644 --- a/poms/bom-test/pom.xml +++ b/poms/bom-test/pom.xml @@ -463,6 +463,18 @@ ${microsoft.graph.auth.version} test + + io.modelcontextprotocol.sdk + mcp-core + ${mcp-java-sdk.version} + test + + + io.modelcontextprotocol.sdk + mcp-json-jackson2 + ${mcp-java-sdk.version} + test + diff --git a/poms/bom/src/main/generated/flattened-full-pom.xml b/poms/bom/src/main/generated/flattened-full-pom.xml index ec5e8408aa7..c75a85ed05a 100644 --- a/poms/bom/src/main/generated/flattened-full-pom.xml +++ b/poms/bom/src/main/generated/flattened-full-pom.xml @@ -2223,6 +2223,11 @@ camel-master 4.22.0-SNAPSHOT + + org.apache.camel + camel-mcp-server-api + 4.22.0-SNAPSHOT + org.apache.camel camel-mdc @@ -5535,6 +5540,16 @@ camel-quarkus-master-deployment 3.39.0-SNAPSHOT + + org.apache.camel.quarkus + camel-quarkus-mcp-server + 3.39.0-SNAPSHOT + + + org.apache.camel.quarkus + camel-quarkus-mcp-server-deployment + 3.39.0-SNAPSHOT + org.apache.camel.quarkus camel-quarkus-mdc @@ -7814,6 +7829,16 @@ quarkus-jsch-deployment 3.2.0 + + io.quarkiverse.mcp + quarkus-mcp-server-http + 1.13.1 + + + io.quarkiverse.mcp + quarkus-mcp-server-http-deployment + 1.13.1 + io.quarkiverse.messaginghub quarkus-pooled-jms diff --git a/poms/bom/src/main/generated/flattened-reduced-pom.xml b/poms/bom/src/main/generated/flattened-reduced-pom.xml index 1b36331e8c3..0a66188c965 100644 --- a/poms/bom/src/main/generated/flattened-reduced-pom.xml +++ b/poms/bom/src/main/generated/flattened-reduced-pom.xml @@ -1177,6 +1177,16 @@ quarkus-jsch-deployment 3.2.0 + + io.quarkiverse.mcp + quarkus-mcp-server-http + 1.13.1 + + + io.quarkiverse.mcp + quarkus-mcp-server-http-deployment + 1.13.1 + io.quarkiverse.messaginghub quarkus-pooled-jms @@ -3551,6 +3561,11 @@ camel-master 4.22.0-SNAPSHOT + + org.apache.camel + camel-mcp-server-api + 4.22.0-SNAPSHOT + org.apache.camel camel-mdc @@ -6868,6 +6883,16 @@ camel-quarkus-master-deployment 3.39.0-SNAPSHOT + + org.apache.camel.quarkus + camel-quarkus-mcp-server + 3.39.0-SNAPSHOT + + + org.apache.camel.quarkus + camel-quarkus-mcp-server-deployment + 3.39.0-SNAPSHOT + org.apache.camel.quarkus camel-quarkus-mdc diff --git a/poms/bom/src/main/generated/flattened-reduced-verbose-pom.xml b/poms/bom/src/main/generated/flattened-reduced-verbose-pom.xml index 4882931d0d7..66614a6368b 100644 --- a/poms/bom/src/main/generated/flattened-reduced-verbose-pom.xml +++ b/poms/bom/src/main/generated/flattened-reduced-verbose-pom.xml @@ -1177,6 +1177,16 @@ quarkus-jsch-deployment 3.2.0 + + io.quarkiverse.mcp + quarkus-mcp-server-http + 1.13.1 + + + io.quarkiverse.mcp + quarkus-mcp-server-http-deployment + 1.13.1 + io.quarkiverse.messaginghub quarkus-pooled-jms @@ -3551,6 +3561,11 @@ camel-master 4.22.0-SNAPSHOT + + org.apache.camel + camel-mcp-server-api + 4.22.0-SNAPSHOT + org.apache.camel camel-mdc @@ -6868,6 +6883,16 @@ camel-quarkus-master-deployment 3.39.0-SNAPSHOT + + org.apache.camel.quarkus + camel-quarkus-mcp-server + 3.39.0-SNAPSHOT + + + org.apache.camel.quarkus + camel-quarkus-mcp-server-deployment + 3.39.0-SNAPSHOT + org.apache.camel.quarkus camel-quarkus-mdc From 4144802236977945536166d9348b1cd66ace549e Mon Sep 17 00:00:00 2001 From: croway Date: Tue, 4 Aug 2026 11:32:25 +0200 Subject: [PATCH 4/9] Fixes #8949. Resolve the MCP test client port from quarkus.http.test-port The hardcoded 8081 broke the test whenever the HTTP test port was configured to something else. Co-Authored-By: Claude Opus 5 (1M context) --- .../quarkus/component/mcp/server/it/McpServerTest.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/integration-tests-jvm/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java b/integration-tests-jvm/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java index d2d0a84ee6f..3526d1906aa 100644 --- a/integration-tests-jvm/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java +++ b/integration-tests-jvm/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java @@ -28,6 +28,7 @@ import io.modelcontextprotocol.spec.McpSchema; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; +import org.eclipse.microprofile.config.ConfigProvider; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -47,7 +48,7 @@ class McpServerTest { private McpSyncClient client() { if (client == null) { client = McpClient - .sync(HttpClientStreamableHttpTransport.builder("http://localhost:8081").build()) + .sync(HttpClientStreamableHttpTransport.builder(serverUrl()).build()) .requestTimeout(Duration.ofSeconds(10)) .initializationTimeout(Duration.ofSeconds(10)) .build(); @@ -127,6 +128,11 @@ void testToolsListReflectsRouteStopAndStart() { .extracting(McpSchema.Tool::name).contains("say_hello")); } + private static String serverUrl() { + return "http://localhost:%d".formatted( + ConfigProvider.getConfig().getValue("quarkus.http.test-port", Integer.class)); + } + private static void controlRoute(String action) { RestAssured.given() .when() From 0693b58443606a1292b30b5a61394587ce6088b5 Mon Sep 17 00:00:00 2001 From: croway Date: Tue, 4 Aug 2026 13:22:26 +0200 Subject: [PATCH 5/9] Fixes #8949. Promote camel-quarkus-mcp-server to native mode support The engine needed no changes to work in native mode: the native image builds without any reflection or resource registration and all engine conformance scenarios pass against the native binary. - Move the extension to extensions/ and the tests to integration-tests/, drop the JVM only warning build step and declare nativeSince - Add a native profile and McpServerIT to the test module - Assign mcp-server to the group-12 test category - Use McpAssured from quarkus-mcp-server-test to drive the server instead of the MCP Java SDK client, which removes the mcp-core and mcp-json-jackson2 test dependencies together with their version property, test BOM and dependabot entries - Regenerate the extension metadata, documentation and flattened BOMs Co-Authored-By: Claude Opus 5 (1M context) --- .github/dependabot.yml | 2 - .../ROOT/examples/others/mcp-server.yml | 6 +- .../reference/extensions/mcp-server.adoc | 14 +- extensions-jvm/pom.xml | 1 - .../mcp-server/deployment/pom.xml | 0 .../server/deployment/McpServerProcessor.java | 14 -- .../mcp-server/pom.xml | 2 +- .../mcp-server/runtime/pom.xml | 1 + .../runtime/src/main/doc/intro.adoc | 0 .../runtime/src/main/doc/usage.adoc | 0 .../mcp/server/CamelMcpServerConfig.java | 0 .../mcp/server/CamelMcpServerRecorder.java | 0 .../mcp/server/QuarkusMcpServerEngine.java | 0 .../resources/META-INF/quarkus-extension.yaml | 1 - extensions/pom.xml | 1 + .../mcp/server/it/McpServerTest.java | 161 ----------------- integration-tests-jvm/pom.xml | 1 - .../mcp-server/pom.xml | 40 ++++- .../mcp/server/it/McpServerResource.java | 0 .../mcp/server/it/McpServerRoutes.java | 0 .../mcp/server/it/QuarkusDefinedTools.java | 0 .../src/main/resources/application.properties | 0 .../component/mcp/server/it/McpServerIT.java | 24 +++ .../mcp/server/it/McpServerTest.java | 170 ++++++++++++++++++ integration-tests/pom.xml | 1 + pom.xml | 1 - poms/bom-test/pom.xml | 12 +- tooling/scripts/test-categories.yaml | 1 + 28 files changed, 246 insertions(+), 207 deletions(-) rename {extensions-jvm => extensions}/mcp-server/deployment/pom.xml (100%) rename {extensions-jvm => extensions}/mcp-server/deployment/src/main/java/org/apache/camel/quarkus/component/mcp/server/deployment/McpServerProcessor.java (77%) rename {extensions-jvm => extensions}/mcp-server/pom.xml (96%) rename {extensions-jvm => extensions}/mcp-server/runtime/pom.xml (98%) rename {extensions-jvm => extensions}/mcp-server/runtime/src/main/doc/intro.adoc (100%) rename {extensions-jvm => extensions}/mcp-server/runtime/src/main/doc/usage.adoc (100%) rename {extensions-jvm => extensions}/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerConfig.java (100%) rename {extensions-jvm => extensions}/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerRecorder.java (100%) rename {extensions-jvm => extensions}/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/QuarkusMcpServerEngine.java (100%) rename {extensions-jvm => extensions}/mcp-server/runtime/src/main/resources/META-INF/quarkus-extension.yaml (98%) delete mode 100644 integration-tests-jvm/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java rename {integration-tests-jvm => integration-tests}/mcp-server/pom.xml (77%) rename {integration-tests-jvm => integration-tests}/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerResource.java (100%) rename {integration-tests-jvm => integration-tests}/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerRoutes.java (100%) rename {integration-tests-jvm => integration-tests}/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java (100%) rename {integration-tests-jvm => integration-tests}/mcp-server/src/main/resources/application.properties (100%) create mode 100644 integration-tests/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerIT.java create mode 100644 integration-tests/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3b84b3c329c..e85f10ad3ee 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -58,8 +58,6 @@ updates: - dependency-name: "org.zeroturnaround:zt-exec" - dependency-name: "com.microsoft.graph:microsoft-graph" - dependency-name: "com.microsoft.graph:microsoft-graph-auth" - - dependency-name: "io.modelcontextprotocol.sdk:mcp-core" - - dependency-name: "io.modelcontextprotocol.sdk:mcp-json-jackson2" # Maven extensions - dependency-name: "com.gradle:develocity-maven-extension" - dependency-name: "com.gradle:common-custom-user-data-maven-extension" diff --git a/docs/modules/ROOT/examples/others/mcp-server.yml b/docs/modules/ROOT/examples/others/mcp-server.yml index c24f8f1eaac..596ae964bf5 100644 --- a/docs/modules/ROOT/examples/others/mcp-server.yml +++ b/docs/modules/ROOT/examples/others/mcp-server.yml @@ -2,11 +2,11 @@ # This file was generated by camel-quarkus-maven-plugin:update-extension-doc-page cqArtifactId: camel-quarkus-mcp-server cqArtifactIdBase: mcp-server -cqNativeSupported: false -cqStatus: Preview +cqNativeSupported: true +cqStatus: Stable cqDeprecated: false cqJvmSince: 3.39.0 -cqNativeSince: n/a +cqNativeSince: 3.39.0 cqCamelPartName: mcp-server cqCamelPartTitle: MCP Server cqCamelPartDescription: Expose ai-tool routes as MCP tools over streamable HTTP diff --git a/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc b/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc index e5b80f3047f..96a9c934835 100644 --- a/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc +++ b/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc @@ -4,17 +4,17 @@ = MCP Server :linkattrs: :cq-artifact-id: camel-quarkus-mcp-server -:cq-native-supported: false -:cq-status: Preview -:cq-status-deprecation: Preview +:cq-native-supported: true +:cq-status: Stable +:cq-status-deprecation: Stable :cq-description: Expose ai-tool routes as MCP tools over streamable HTTP :cq-deprecated: false :cq-jvm-since: 3.39.0 -:cq-native-since: n/a +:cq-native-since: 3.39.0 ifeval::[{doc-show-badges} == true] [.badges] -[.badge-key]##JVM since##[.badge-supported]##3.39.0## [.badge-key]##Native##[.badge-unsupported]##unsupported## +[.badge-key]##JVM since##[.badge-supported]##3.39.0## [.badge-key]##Native since##[.badge-supported]##3.39.0## endif::[] The MCP Server extension exposes Camel routes registered via the `ai-tool` @@ -42,6 +42,10 @@ Please refer to the above link for usage and configuration details. [id="extensions-mcp-server-maven-coordinates"] == Maven coordinates +https://{link-quarkus-code-generator}/?extension-search=camel-quarkus-mcp-server[Create a new project with this extension on {link-quarkus-code-generator}, window="_blank"] + +Or add the coordinates to your existing project: + [source,xml] ---- diff --git a/extensions-jvm/pom.xml b/extensions-jvm/pom.xml index 0a090c24f0f..ce94a9b29be 100644 --- a/extensions-jvm/pom.xml +++ b/extensions-jvm/pom.xml @@ -74,7 +74,6 @@ jsonapi ldif lucene - mcp-server mvel opensearch printer diff --git a/extensions-jvm/mcp-server/deployment/pom.xml b/extensions/mcp-server/deployment/pom.xml similarity index 100% rename from extensions-jvm/mcp-server/deployment/pom.xml rename to extensions/mcp-server/deployment/pom.xml diff --git a/extensions-jvm/mcp-server/deployment/src/main/java/org/apache/camel/quarkus/component/mcp/server/deployment/McpServerProcessor.java b/extensions/mcp-server/deployment/src/main/java/org/apache/camel/quarkus/component/mcp/server/deployment/McpServerProcessor.java similarity index 77% rename from extensions-jvm/mcp-server/deployment/src/main/java/org/apache/camel/quarkus/component/mcp/server/deployment/McpServerProcessor.java rename to extensions/mcp-server/deployment/src/main/java/org/apache/camel/quarkus/component/mcp/server/deployment/McpServerProcessor.java index dcf34ea75a6..f43febbb354 100644 --- a/extensions-jvm/mcp-server/deployment/src/main/java/org/apache/camel/quarkus/component/mcp/server/deployment/McpServerProcessor.java +++ b/extensions/mcp-server/deployment/src/main/java/org/apache/camel/quarkus/component/mcp/server/deployment/McpServerProcessor.java @@ -22,16 +22,12 @@ import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.FeatureBuildItem; -import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; import org.apache.camel.quarkus.component.mcp.server.CamelMcpServerConfig; import org.apache.camel.quarkus.component.mcp.server.CamelMcpServerRecorder; -import org.apache.camel.quarkus.core.JvmOnlyRecorder; import org.apache.camel.quarkus.core.deployment.spi.RuntimeCamelContextCustomizerBuildItem; -import org.jboss.logging.Logger; class McpServerProcessor { - private static final Logger LOG = Logger.getLogger(McpServerProcessor.class); private static final String FEATURE = "camel-mcp-server"; @BuildStep @@ -46,16 +42,6 @@ RuntimeCamelContextCustomizerBuildItem mcpServerBridge(CamelMcpServerRecorder re recorder.createContextCustomizer(config.tags().orElse(null), config.toolTimeout())); } - /** - * Remove this once this extension starts supporting the native mode. - */ - @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) - @Record(value = ExecutionTime.RUNTIME_INIT) - void warnJvmInNative(JvmOnlyRecorder recorder) { - JvmOnlyRecorder.warnJvmInNative(LOG, FEATURE); // warn at build time - recorder.warnJvmInNative(FEATURE); // warn at runtime - } - static final class McpServerEnabled implements BooleanSupplier { CamelMcpServerConfig config; diff --git a/extensions-jvm/mcp-server/pom.xml b/extensions/mcp-server/pom.xml similarity index 96% rename from extensions-jvm/mcp-server/pom.xml rename to extensions/mcp-server/pom.xml index 2db7aa1ac53..f514871de76 100644 --- a/extensions-jvm/mcp-server/pom.xml +++ b/extensions/mcp-server/pom.xml @@ -23,7 +23,7 @@ 4.0.0 org.apache.camel.quarkus - camel-quarkus-extensions-jvm + camel-quarkus-extensions 3.39.0-SNAPSHOT ../pom.xml diff --git a/extensions-jvm/mcp-server/runtime/pom.xml b/extensions/mcp-server/runtime/pom.xml similarity index 98% rename from extensions-jvm/mcp-server/runtime/pom.xml rename to extensions/mcp-server/runtime/pom.xml index 1d628de640b..63af0d6f595 100644 --- a/extensions-jvm/mcp-server/runtime/pom.xml +++ b/extensions/mcp-server/runtime/pom.xml @@ -34,6 +34,7 @@ 3.39.0 + 3.39.0 diff --git a/extensions-jvm/mcp-server/runtime/src/main/doc/intro.adoc b/extensions/mcp-server/runtime/src/main/doc/intro.adoc similarity index 100% rename from extensions-jvm/mcp-server/runtime/src/main/doc/intro.adoc rename to extensions/mcp-server/runtime/src/main/doc/intro.adoc diff --git a/extensions-jvm/mcp-server/runtime/src/main/doc/usage.adoc b/extensions/mcp-server/runtime/src/main/doc/usage.adoc similarity index 100% rename from extensions-jvm/mcp-server/runtime/src/main/doc/usage.adoc rename to extensions/mcp-server/runtime/src/main/doc/usage.adoc diff --git a/extensions-jvm/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerConfig.java b/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerConfig.java similarity index 100% rename from extensions-jvm/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerConfig.java rename to extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerConfig.java diff --git a/extensions-jvm/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerRecorder.java b/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerRecorder.java similarity index 100% rename from extensions-jvm/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerRecorder.java rename to extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerRecorder.java diff --git a/extensions-jvm/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/QuarkusMcpServerEngine.java b/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/QuarkusMcpServerEngine.java similarity index 100% rename from extensions-jvm/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/QuarkusMcpServerEngine.java rename to extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/QuarkusMcpServerEngine.java diff --git a/extensions-jvm/mcp-server/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/mcp-server/runtime/src/main/resources/META-INF/quarkus-extension.yaml similarity index 98% rename from extensions-jvm/mcp-server/runtime/src/main/resources/META-INF/quarkus-extension.yaml rename to extensions/mcp-server/runtime/src/main/resources/META-INF/quarkus-extension.yaml index d9a2566e733..875f203389b 100644 --- a/extensions-jvm/mcp-server/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/mcp-server/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -26,7 +26,6 @@ description: "Expose ai-tool routes as MCP tools over streamable HTTP" metadata: icon-url: "https://raw.githubusercontent.com/apache/camel-website/main/antora-ui-camel/src/img/logo-d.svg" sponsor: "Apache Software Foundation" - unlisted: true guide: "https://camel.apache.org/camel-quarkus/latest/reference/extensions/mcp-server.html" categories: - "integration" diff --git a/extensions/pom.xml b/extensions/pom.xml index bfb301db5eb..023c0b2bca7 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -204,6 +204,7 @@ management mapstruct master + mcp-server mdc micrometer micrometer-observability diff --git a/integration-tests-jvm/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java b/integration-tests-jvm/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java deleted file mode 100644 index 3526d1906aa..00000000000 --- a/integration-tests-jvm/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.camel.quarkus.component.mcp.server.it; - -import java.time.Duration; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -import io.modelcontextprotocol.client.McpClient; -import io.modelcontextprotocol.client.McpSyncClient; -import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; -import io.modelcontextprotocol.spec.McpSchema; -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; -import org.eclipse.microprofile.config.ConfigProvider; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - -/** - * The engine conformance scenarios (CAMEL-24313) against the quarkus-mcp-server engine, driven by the official MCP SDK - * client over streamable HTTP. Mirrors {@code McpServerConformanceTestSupport} from camel-mcp-server-api, which cannot - * be reused as-is here because it manages its own CamelContext outside Quarkus. - */ -@QuarkusTest -class McpServerTest { - - private McpSyncClient client; - - private McpSyncClient client() { - if (client == null) { - client = McpClient - .sync(HttpClientStreamableHttpTransport.builder(serverUrl()).build()) - .requestTimeout(Duration.ofSeconds(10)) - .initializationTimeout(Duration.ofSeconds(10)) - .build(); - client.initialize(); - } - return client; - } - - @AfterEach - void closeClient() { - if (client != null) { - client.closeGracefully(); - client = null; - } - } - - @Test - void testListToolsExposesOnlySelectedTags() { - List tools = client().listTools().tools(); - - assertThat(tools).extracting(McpSchema.Tool::name) - .contains("say_hello", "fail_tool", "slow_tool") - .doesNotContain("hidden_tool", "other_tool"); - } - - @Test - void testQuarkusAnnotatedToolsCoexistWithCamelTools() { - // both tool sources are served by the same MCP server - assertThat(client().listTools().tools()).extracting(McpSchema.Tool::name) - .contains("add_numbers", "say_hello"); - - McpSchema.CallToolResult result = client() - .callTool(new McpSchema.CallToolRequest("add_numbers", Map.of("a", 17, "b", 25))); - assertThat(result.isError()).as(textOf(result)).isNotEqualTo(Boolean.TRUE); - assertThat(textOf(result)).isEqualTo("42"); - } - - @Test - void testCallToolSuccess() { - McpSchema.CallToolResult result = client() - .callTool(new McpSchema.CallToolRequest("say_hello", Map.of("name", "World"))); - - assertThat(result.isError()).isNotEqualTo(Boolean.TRUE); - assertThat(textOf(result)).isEqualTo("Hello World"); - } - - @Test - void testCallToolExecutionErrorIsSanitized() { - McpSchema.CallToolResult result = client().callTool(new McpSchema.CallToolRequest("fail_tool", Map.of())); - - assertThat(result.isError()).isEqualTo(Boolean.TRUE); - assertThat(textOf(result)) - .doesNotContain("secret internal detail") - .isEqualTo("Tool execution failed"); - } - - @Test - void testCallToolTimeout() { - McpSchema.CallToolResult result = client().callTool(new McpSchema.CallToolRequest("slow_tool", Map.of())); - - assertThat(result.isError()).isEqualTo(Boolean.TRUE); - assertThat(textOf(result)).contains("timed out"); - } - - @Test - void testToolsListReflectsRouteStopAndStart() { - assertThat(client().listTools().tools()).extracting(McpSchema.Tool::name).contains("say_hello"); - - controlRoute("stop"); - assertThat(routeStatus()).isEqualTo("Stopped"); - await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> assertThat(client().listTools().tools()) - .extracting(McpSchema.Tool::name).doesNotContain("say_hello")); - - controlRoute("start"); - assertThat(routeStatus()).isEqualTo("Started"); - await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> assertThat(client().listTools().tools()) - .extracting(McpSchema.Tool::name).contains("say_hello")); - } - - private static String serverUrl() { - return "http://localhost:%d".formatted( - ConfigProvider.getConfig().getValue("quarkus.http.test-port", Integer.class)); - } - - private static void controlRoute(String action) { - RestAssured.given() - .when() - .post("/mcp-server/route/say-hello-route/" + action) - .then() - .statusCode(204); - } - - private static String routeStatus() { - return RestAssured.given() - .when() - .get("/mcp-server/route/say-hello-route/status") - .then() - .statusCode(200) - .extract() - .body() - .asString(); - } - - private static String textOf(McpSchema.CallToolResult result) { - return result.content().stream() - .filter(McpSchema.TextContent.class::isInstance) - .map(c -> ((McpSchema.TextContent) c).text()) - .collect(Collectors.joining()); - } -} diff --git a/integration-tests-jvm/pom.xml b/integration-tests-jvm/pom.xml index 2e3c5a8ed69..fb2cbd3412e 100644 --- a/integration-tests-jvm/pom.xml +++ b/integration-tests-jvm/pom.xml @@ -75,7 +75,6 @@ ldif lucene main-devmode - mcp-server mvel opensearch printer diff --git a/integration-tests-jvm/mcp-server/pom.xml b/integration-tests/mcp-server/pom.xml similarity index 77% rename from integration-tests-jvm/mcp-server/pom.xml rename to integration-tests/mcp-server/pom.xml index e9e08ee87e3..b6accb9bdfd 100644 --- a/integration-tests-jvm/mcp-server/pom.xml +++ b/integration-tests/mcp-server/pom.xml @@ -47,15 +47,9 @@ - - io.modelcontextprotocol.sdk - mcp-core - test - - - io.modelcontextprotocol.sdk - mcp-json-jackson2 + io.quarkiverse.mcp + quarkus-mcp-server-test test @@ -81,6 +75,36 @@ + + native + + + native + + + + true + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + ${rerun.failing.test.count} + + + + + + + virtualDependencies diff --git a/integration-tests-jvm/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerResource.java b/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerResource.java similarity index 100% rename from integration-tests-jvm/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerResource.java rename to integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerResource.java diff --git a/integration-tests-jvm/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerRoutes.java b/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerRoutes.java similarity index 100% rename from integration-tests-jvm/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerRoutes.java rename to integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerRoutes.java diff --git a/integration-tests-jvm/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java b/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java similarity index 100% rename from integration-tests-jvm/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java rename to integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java diff --git a/integration-tests-jvm/mcp-server/src/main/resources/application.properties b/integration-tests/mcp-server/src/main/resources/application.properties similarity index 100% rename from integration-tests-jvm/mcp-server/src/main/resources/application.properties rename to integration-tests/mcp-server/src/main/resources/application.properties diff --git a/integration-tests/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerIT.java b/integration-tests/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerIT.java new file mode 100644 index 00000000000..2775bf31837 --- /dev/null +++ b/integration-tests/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerIT.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.mcp.server.it; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class McpServerIT extends McpServerTest { + +} diff --git a/integration-tests/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java b/integration-tests/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java new file mode 100644 index 00000000000..efa0e8945ae --- /dev/null +++ b/integration-tests/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java @@ -0,0 +1,170 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.mcp.server.it; + +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import io.quarkiverse.mcp.server.TextContent; +import io.quarkiverse.mcp.server.ToolResponse; +import io.quarkiverse.mcp.server.test.McpAssured; +import io.quarkiverse.mcp.server.test.McpAssured.McpStreamableTestClient; +import io.quarkiverse.mcp.server.test.McpAssured.ToolInfo; +import io.quarkiverse.mcp.server.test.McpAssured.ToolsPage; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import org.eclipse.microprofile.config.ConfigProvider; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +/** + * The engine conformance scenarios (CAMEL-24313) against the quarkus-mcp-server engine, driven by McpAssured over + * streamable HTTP. Mirrors {@code McpServerConformanceTestSupport} from camel-mcp-server-api, which cannot be reused + * as-is here because it manages its own CamelContext outside Quarkus. + */ +@QuarkusTest +class McpServerTest { + + private McpStreamableTestClient client; + + private McpStreamableTestClient client() { + if (client == null) { + client = McpAssured.newStreamableClient() + .setBaseUri(URI.create("http://localhost:%d".formatted( + ConfigProvider.getConfig().getValue("quarkus.http.test-port", Integer.class)))) + .build() + .connect(); + } + return client; + } + + @AfterEach + void closeClient() { + if (client != null) { + client.disconnect(); + client = null; + } + } + + @Test + void testListToolsExposesOnlySelectedTags() { + client().when() + .toolsList(page -> assertThat(toolNames(page)) + .contains("say_hello", "fail_tool", "slow_tool") + .doesNotContain("hidden_tool", "other_tool")) + .thenAssertResults(); + } + + @Test + void testQuarkusAnnotatedToolsCoexistWithCamelTools() { + // both tool sources are served by the same MCP server + client().when() + .toolsList(page -> assertThat(toolNames(page)).contains("add_numbers", "say_hello")) + .toolsCall("add_numbers", Map.of("a", 17, "b", 25), response -> { + assertThat(response.isError()).isNotEqualTo(Boolean.TRUE); + assertThat(textOf(response)).isEqualTo("42"); + }) + .thenAssertResults(); + } + + @Test + void testCallToolSuccess() { + client().when() + .toolsCall("say_hello", Map.of("name", "World"), response -> { + assertThat(response.isError()).isNotEqualTo(Boolean.TRUE); + assertThat(textOf(response)).isEqualTo("Hello World"); + }) + .thenAssertResults(); + } + + @Test + void testCallToolExecutionErrorIsSanitized() { + client().when() + .toolsCall("fail_tool", response -> { + assertThat(response.isError()).isTrue(); + assertThat(textOf(response)) + .doesNotContain("secret internal detail") + .isEqualTo("Tool execution failed"); + }) + .thenAssertResults(); + } + + @Test + void testCallToolTimeout() { + client().when() + .toolsCall("slow_tool", response -> { + assertThat(response.isError()).isTrue(); + assertThat(textOf(response)).contains("timed out"); + }) + .thenAssertResults(); + } + + @Test + void testToolsListReflectsRouteStopAndStart() { + client().when() + .toolsList(page -> assertThat(toolNames(page)).contains("say_hello")) + .thenAssertResults(); + + controlRoute("stop"); + assertThat(routeStatus()).isEqualTo("Stopped"); + await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> client().when() + .toolsList(page -> assertThat(toolNames(page)).doesNotContain("say_hello")) + .thenAssertResults()); + + controlRoute("start"); + assertThat(routeStatus()).isEqualTo("Started"); + await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> client().when() + .toolsList(page -> assertThat(toolNames(page)).contains("say_hello")) + .thenAssertResults()); + } + + private static void controlRoute(String action) { + RestAssured.given() + .when() + .post("/mcp-server/route/say-hello-route/" + action) + .then() + .statusCode(204); + } + + private static String routeStatus() { + return RestAssured.given() + .when() + .get("/mcp-server/route/say-hello-route/status") + .then() + .statusCode(200) + .extract() + .body() + .asString(); + } + + private static List toolNames(ToolsPage page) { + return page.tools().stream().map(ToolInfo::name).toList(); + } + + private static String textOf(ToolResponse response) { + return response.content().stream() + .filter(TextContent.class::isInstance) + .map(content -> ((TextContent) content).text()) + .collect(Collectors.joining()); + } +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 8ffee5a4dd6..3eac678682e 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -179,6 +179,7 @@ master-file master-infinispan master-openshift + mcp-server mdc micrometer micrometer-observability diff --git a/pom.xml b/pom.xml index bf6488c8b47..980459421a4 100644 --- a/pom.xml +++ b/pom.xml @@ -190,7 +190,6 @@ ${ftpserver-version} 3.0 4.13.0 - 2.0.0 6.67.0 0.3.0 ${pdfbox-version} diff --git a/poms/bom-test/pom.xml b/poms/bom-test/pom.xml index b618103349f..5454654acaf 100644 --- a/poms/bom-test/pom.xml +++ b/poms/bom-test/pom.xml @@ -464,15 +464,9 @@ test - io.modelcontextprotocol.sdk - mcp-core - ${mcp-java-sdk.version} - test - - - io.modelcontextprotocol.sdk - mcp-json-jackson2 - ${mcp-java-sdk.version} + io.quarkiverse.mcp + quarkus-mcp-server-test + ${quarkiverse-mcp-server.version} test diff --git a/tooling/scripts/test-categories.yaml b/tooling/scripts/test-categories.yaml index a4df3902653..98d19bc52c9 100644 --- a/tooling/scripts/test-categories.yaml +++ b/tooling/scripts/test-categories.yaml @@ -239,6 +239,7 @@ group-12: - knative - langchain4j-web-search - langchain4j-tools + - mcp-server - openapi-java - openapi-validator - paho-mqtt5 From 5263d20823cf8c56fd451f8a0c782b835cc5e576 Mon Sep 17 00:00:00 2001 From: croway Date: Tue, 4 Aug 2026 15:55:18 +0200 Subject: [PATCH 6/9] Fixes #8949. Document serving the MCP tools over stdio The extension keeps streamable HTTP out of the box. Adding the quarkus-mcp-server-stdio transport serves the same Camel tools over stdin/stdout, and the HTTP transport of the default MCP server can be turned off to serve over stdio only. Co-Authored-By: Claude Opus 5 (1M context) --- .../reference/extensions/mcp-server.adoc | 58 +++++++++++++++++++ .../runtime/src/main/doc/usage.adoc | 57 ++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc b/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc index 96a9c934835..96dae1aa262 100644 --- a/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc +++ b/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc @@ -155,6 +155,64 @@ A coding agent or IDE is configured with the same URL, e.g. in an } ---- +[id="extensions-mcp-server-usage-serving-over-stdio"] +=== Serving over stdio + +Out of the box the tools are served over streamable HTTP. MCP clients that +launch the server as a subprocess speak over stdin/stdout instead — add the +stdio transport alongside the extension: + +[source,xml] +---- + + io.quarkiverse.mcp + quarkus-mcp-server-stdio + + 1.13.1 + +---- + +The stdio transport is active as soon as it is on the classpath +(`quarkus.mcp.server.stdio.enabled` defaults to `true`), and the same Camel +tools are then served over both transports. To serve over stdio *only*, +disable the HTTP transport of the default MCP server: + +[source,properties] +---- +quarkus.mcp.server."".http.enabled = false +---- + +The server name of the default MCP server is the literal string +``, so it has to be quoted. Conversely, +`quarkus.mcp.server.stdio.enabled = false` keeps the stdio extension on the +classpath without activating it, which is useful when the same application +is deployed both ways. + +Because stdout carries the MCP protocol, nothing else may be written to it; +`quarkus-mcp-server-stdio` takes care of this by routing console logging to +stderr. A stdio server is typically built as a native executable and +launched by the client: + +[source,json] +---- +{ + "mcpServers": { + "my-integration-app": { + "command": "/path/to/target/my-integration-app-1.0.0-runner" + } + } +} +---- + +See the +https://docs.quarkiverse.io/quarkus-mcp-server/dev/getting-started-stdio.html[quarkus-mcp-server stdio guide] +and the +https://docs.quarkiverse.io/quarkus-mcp-server/dev/reference-configuration.html[configuration reference] +for the transport options. On Spring Boot the equivalent setup is described +in the +https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html[Spring AI MCP Server Boot Starter documentation]. + [id="extensions-mcp-server-usage-mixing-with-quarkus-mcp-server-annotated-tools"] === Mixing with quarkus-mcp-server annotated tools diff --git a/extensions/mcp-server/runtime/src/main/doc/usage.adoc b/extensions/mcp-server/runtime/src/main/doc/usage.adoc index e16eaf13209..595f81e6ed5 100644 --- a/extensions/mcp-server/runtime/src/main/doc/usage.adoc +++ b/extensions/mcp-server/runtime/src/main/doc/usage.adoc @@ -92,6 +92,63 @@ A coding agent or IDE is configured with the same URL, e.g. in an } ---- +=== Serving over stdio + +Out of the box the tools are served over streamable HTTP. MCP clients that +launch the server as a subprocess speak over stdin/stdout instead — add the +stdio transport alongside the extension: + +[source,xml] +---- + + io.quarkiverse.mcp + quarkus-mcp-server-stdio + + 1.13.1 + +---- + +The stdio transport is active as soon as it is on the classpath +(`quarkus.mcp.server.stdio.enabled` defaults to `true`), and the same Camel +tools are then served over both transports. To serve over stdio *only*, +disable the HTTP transport of the default MCP server: + +[source,properties] +---- +quarkus.mcp.server."".http.enabled = false +---- + +The server name of the default MCP server is the literal string +``, so it has to be quoted. Conversely, +`quarkus.mcp.server.stdio.enabled = false` keeps the stdio extension on the +classpath without activating it, which is useful when the same application +is deployed both ways. + +Because stdout carries the MCP protocol, nothing else may be written to it; +`quarkus-mcp-server-stdio` takes care of this by routing console logging to +stderr. A stdio server is typically built as a native executable and +launched by the client: + +[source,json] +---- +{ + "mcpServers": { + "my-integration-app": { + "command": "/path/to/target/my-integration-app-1.0.0-runner" + } + } +} +---- + +See the +https://docs.quarkiverse.io/quarkus-mcp-server/dev/getting-started-stdio.html[quarkus-mcp-server stdio guide] +and the +https://docs.quarkiverse.io/quarkus-mcp-server/dev/reference-configuration.html[configuration reference] +for the transport options. On Spring Boot the equivalent setup is described +in the +https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html[Spring AI MCP Server Boot Starter documentation]. + === Mixing with quarkus-mcp-server annotated tools Camel tools coexist with tools defined natively with quarkus-mcp-server — From 5b031c83b681ad6a2d100bd97764f28cb3d2572d Mon Sep 17 00:00:00 2001 From: croway Date: Tue, 4 Aug 2026 16:31:38 +0200 Subject: [PATCH 7/9] Fixes #8949. Use the flat form for disabling the default MCP server HTTP transport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The servers map is annotated @WithUnnamedKey(""), so quarkus.mcp.server.http.enabled addresses the default server directly — the quoted "" key works but is not required, and the flat form is what the quarkus-mcp-server configuration reference documents. Co-Authored-By: Claude Fable 5 --- .../ROOT/pages/reference/extensions/mcp-server.adoc | 10 ++++------ extensions/mcp-server/runtime/src/main/doc/usage.adoc | 10 ++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc b/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc index 96dae1aa262..2f8b86a6617 100644 --- a/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc +++ b/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc @@ -180,14 +180,12 @@ disable the HTTP transport of the default MCP server: [source,properties] ---- -quarkus.mcp.server."".http.enabled = false +quarkus.mcp.server.http.enabled = false ---- -The server name of the default MCP server is the literal string -``, so it has to be quoted. Conversely, -`quarkus.mcp.server.stdio.enabled = false` keeps the stdio extension on the -classpath without activating it, which is useful when the same application -is deployed both ways. +Conversely, `quarkus.mcp.server.stdio.enabled = false` keeps the stdio +extension on the classpath without activating it, which is useful when the +same application is deployed both ways. Because stdout carries the MCP protocol, nothing else may be written to it; `quarkus-mcp-server-stdio` takes care of this by routing console logging to diff --git a/extensions/mcp-server/runtime/src/main/doc/usage.adoc b/extensions/mcp-server/runtime/src/main/doc/usage.adoc index 595f81e6ed5..4949a2be842 100644 --- a/extensions/mcp-server/runtime/src/main/doc/usage.adoc +++ b/extensions/mcp-server/runtime/src/main/doc/usage.adoc @@ -116,14 +116,12 @@ disable the HTTP transport of the default MCP server: [source,properties] ---- -quarkus.mcp.server."".http.enabled = false +quarkus.mcp.server.http.enabled = false ---- -The server name of the default MCP server is the literal string -``, so it has to be quoted. Conversely, -`quarkus.mcp.server.stdio.enabled = false` keeps the stdio extension on the -classpath without activating it, which is useful when the same application -is deployed both ways. +Conversely, `quarkus.mcp.server.stdio.enabled = false` keeps the stdio +extension on the classpath without activating it, which is useful when the +same application is deployed both ways. Because stdout carries the MCP protocol, nothing else may be written to it; `quarkus-mcp-server-stdio` takes care of this by routing console logging to From 7ab41592df5f8b5074bd60f6a0633b0bc430b51e Mon Sep 17 00:00:00 2001 From: croway Date: Tue, 4 Aug 2026 16:34:26 +0200 Subject: [PATCH 8/9] Revert "Fixes #8949. Use the flat form for disabling the default MCP server HTTP transport" This reverts commit 9f6fdd438d189d5eb1a1c968f432bfb30a709647. --- .../ROOT/pages/reference/extensions/mcp-server.adoc | 10 ++++++---- extensions/mcp-server/runtime/src/main/doc/usage.adoc | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc b/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc index 2f8b86a6617..96dae1aa262 100644 --- a/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc +++ b/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc @@ -180,12 +180,14 @@ disable the HTTP transport of the default MCP server: [source,properties] ---- -quarkus.mcp.server.http.enabled = false +quarkus.mcp.server."".http.enabled = false ---- -Conversely, `quarkus.mcp.server.stdio.enabled = false` keeps the stdio -extension on the classpath without activating it, which is useful when the -same application is deployed both ways. +The server name of the default MCP server is the literal string +``, so it has to be quoted. Conversely, +`quarkus.mcp.server.stdio.enabled = false` keeps the stdio extension on the +classpath without activating it, which is useful when the same application +is deployed both ways. Because stdout carries the MCP protocol, nothing else may be written to it; `quarkus-mcp-server-stdio` takes care of this by routing console logging to diff --git a/extensions/mcp-server/runtime/src/main/doc/usage.adoc b/extensions/mcp-server/runtime/src/main/doc/usage.adoc index 4949a2be842..595f81e6ed5 100644 --- a/extensions/mcp-server/runtime/src/main/doc/usage.adoc +++ b/extensions/mcp-server/runtime/src/main/doc/usage.adoc @@ -116,12 +116,14 @@ disable the HTTP transport of the default MCP server: [source,properties] ---- -quarkus.mcp.server.http.enabled = false +quarkus.mcp.server."".http.enabled = false ---- -Conversely, `quarkus.mcp.server.stdio.enabled = false` keeps the stdio -extension on the classpath without activating it, which is useful when the -same application is deployed both ways. +The server name of the default MCP server is the literal string +``, so it has to be quoted. Conversely, +`quarkus.mcp.server.stdio.enabled = false` keeps the stdio extension on the +classpath without activating it, which is useful when the same application +is deployed both ways. Because stdout carries the MCP protocol, nothing else may be written to it; `quarkus-mcp-server-stdio` takes care of this by routing console logging to From afe210d57a0bc0f8c413bb887046f4d94de91927 Mon Sep 17 00:00:00 2001 From: croway Date: Wed, 5 Aug 2026 10:51:48 +0200 Subject: [PATCH 9/9] Fixes #8949. Add the MCP Server extension page to the docs navigation docs/modules/ROOT/nav.adoc is generated by the check-extension-pages goal of the catalog module and was missed when the extension was added, leaving the reference page unreachable from the navigation tree. Co-Authored-By: Claude Opus 5 (1M context) --- docs/modules/ROOT/nav.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index d0cd2289b1a..e1ef9f5e511 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -226,6 +226,7 @@ *** xref:reference/extensions/log.adoc[Log] *** xref:reference/extensions/lucene.adoc[Lucene] *** xref:reference/extensions/lumberjack.adoc[Lumberjack] +*** xref:reference/extensions/mcp-server.adoc[MCP Server] *** xref:reference/extensions/mina-sftp.adoc[MINA SFTP] *** xref:reference/extensions/mllp.adoc[MLLP] *** xref:reference/extensions/mvel.adoc[MVEL]