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..596ae964bf5 --- /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: true +cqStatus: Stable +cqDeprecated: false +cqJvmSince: 3.39.0 +cqNativeSince: 3.39.0 +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/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] 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..96dae1aa262 --- /dev/null +++ b/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc @@ -0,0 +1,301 @@ +// 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: 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: 3.39.0 + +ifeval::[{doc-show-badges} == true] +[.badges] +[.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` +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 + +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] +---- + + 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"); +---- + +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 +---- + +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-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 + +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 +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/mcp-server/deployment/pom.xml b/extensions/mcp-server/deployment/pom.xml new file mode 100644 index 00000000000..908eb319600 --- /dev/null +++ b/extensions/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/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 new file mode 100644 index 00000000000..f43febbb354 --- /dev/null +++ b/extensions/mcp-server/deployment/src/main/java/org/apache/camel/quarkus/component/mcp/server/deployment/McpServerProcessor.java @@ -0,0 +1,53 @@ +/* + * 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 org.apache.camel.quarkus.component.mcp.server.CamelMcpServerConfig; +import org.apache.camel.quarkus.component.mcp.server.CamelMcpServerRecorder; +import org.apache.camel.quarkus.core.deployment.spi.RuntimeCamelContextCustomizerBuildItem; + +class McpServerProcessor { + + 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())); + } + + static final class McpServerEnabled implements BooleanSupplier { + CamelMcpServerConfig config; + + @Override + public boolean getAsBoolean() { + return config.enabled(); + } + } +} diff --git a/extensions/mcp-server/pom.xml b/extensions/mcp-server/pom.xml new file mode 100644 index 00000000000..f514871de76 --- /dev/null +++ b/extensions/mcp-server/pom.xml @@ -0,0 +1,39 @@ + + + + 4.0.0 + + org.apache.camel.quarkus + camel-quarkus-extensions + 3.39.0-SNAPSHOT + ../pom.xml + + + camel-quarkus-mcp-server-parent + Camel Quarkus :: MCP Server + pom + + + deployment + runtime + + diff --git a/extensions/mcp-server/runtime/pom.xml b/extensions/mcp-server/runtime/pom.xml new file mode 100644 index 00000000000..63af0d6f595 --- /dev/null +++ b/extensions/mcp-server/runtime/pom.xml @@ -0,0 +1,105 @@ + + + + 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 + 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/mcp-server/runtime/src/main/doc/intro.adoc b/extensions/mcp-server/runtime/src/main/doc/intro.adoc new file mode 100644 index 00000000000..7c60ee6f2d4 --- /dev/null +++ b/extensions/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/mcp-server/runtime/src/main/doc/usage.adoc b/extensions/mcp-server/runtime/src/main/doc/usage.adoc new file mode 100644 index 00000000000..595f81e6ed5 --- /dev/null +++ b/extensions/mcp-server/runtime/src/main/doc/usage.adoc @@ -0,0 +1,200 @@ +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"); +---- + +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" + } + } +} +---- + +=== 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 — +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. + +=== 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 +Boot it is the `camel-mcp-server-starter`. diff --git a/extensions/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 new file mode 100644 index 00000000000..5975fc5d86f --- /dev/null +++ b/extensions/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/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 new file mode 100644 index 00000000000..f7d5ef2157c --- /dev/null +++ b/extensions/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/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 new file mode 100644 index 00000000000..2f9a19f6be0 --- /dev/null +++ b/extensions/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/mcp-server/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/mcp-server/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 00000000000..875f203389b --- /dev/null +++ b/extensions/mcp-server/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,38 @@ +# +# 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" + 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/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/mcp-server/pom.xml b/integration-tests/mcp-server/pom.xml new file mode 100644 index 00000000000..b6accb9bdfd --- /dev/null +++ b/integration-tests/mcp-server/pom.xml @@ -0,0 +1,146 @@ + + + + 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 + + + + org.apache.camel.quarkus + camel-quarkus-mcp-server + + + org.apache.camel.quarkus + camel-quarkus-ai-tool + + + io.quarkus + quarkus-resteasy + + + + + io.quarkiverse.mcp + quarkus-mcp-server-test + test + + + org.awaitility + awaitility + test + + + org.assertj + assertj-core + test + + + io.quarkus + quarkus-junit + test + + + io.rest-assured + rest-assured + test + + + + + + native + + + native + + + + true + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + ${rerun.failing.test.count} + + + + + + + + + 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/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 new file mode 100644 index 00000000000..1012e173fad --- /dev/null +++ b/integration-tests/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/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 new file mode 100644 index 00000000000..ab1efba1681 --- /dev/null +++ b/integration-tests/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/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 new file mode 100644 index 00000000000..88a6cf149fb --- /dev/null +++ b/integration-tests/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/mcp-server/src/main/resources/application.properties b/integration-tests/mcp-server/src/main/resources/application.properties new file mode 100644 index 00000000000..91728bf6438 --- /dev/null +++ b/integration-tests/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/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 bf1e32703e6..980459421a4 100644 --- a/pom.xml +++ b/pom.xml @@ -60,6 +60,7 @@ 3.6.2 3.2.0 1.12.2 + 1.13.1 3.5.0 3.9.1 2.4.2 diff --git a/poms/bom-test/pom.xml b/poms/bom-test/pom.xml index ca3cfd8ecf3..5454654acaf 100644 --- a/poms/bom-test/pom.xml +++ b/poms/bom-test/pom.xml @@ -463,6 +463,12 @@ ${microsoft.graph.auth.version} test + + io.quarkiverse.mcp + quarkus-mcp-server-test + ${quarkiverse-mcp-server.version} + test + 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 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 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