You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ji...@apache.org on 2026/08/10 05:30:22 UTC
(camel-quarkus) 03/06: CAMEL-24369: mcp-server: publish argSchema input schemas and MCP tool annotations
This is an automated email from the ASF dual-hosted git repository.
JiriOndrusek pushed a commit to branch camel-4.22.0-upgrade
in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
commit add5a8189a74ee07004b60417296e9b889a7a43b
Author: Federico Mariani <34...@users.noreply.github.com>
AuthorDate: Fri Aug 7 11:28:02 2026 +0200
CAMEL-24369: mcp-server: publish argSchema input schemas and MCP tool annotations
QuarkusMcpServerEngine rebuilt the tool input schema from the flat
addArgument calls, so tools defined with a nested argSchema advertised an
empty inputSchema to MCP clients (and flat parameters lost their enum
constraints). The engine now publishes the bridge's pre-built JSON Schema
via ToolDefinition.setInputSchema, which quarkiverse serves verbatim in
tools/list.
The engine also maps the ai-tool annotation hints (title, readOnlyHint,
destructiveHint, idempotentHint, openWorldHint) via setTitle and
setAnnotations, matching the Vert.x and Spring AI engines. The quarkiverse
annotations record uses primitive booleans, so unset hints are published
with the MCP specification's client-side defaults; when no hint is set at
all, no annotations are published.
Covered by three new conformance scenarios in the integration test
(annotations, argSchema in tools/list, argSchema execution with nested
Map/List arguments).
Co-authored-by: Claude Fable 5 <no...@anthropic.com>
---
.../mcp/server/QuarkusMcpServerEngine.java | 34 ++++++++++++++++
.../component/mcp/server/it/McpServerRoutes.java | 17 ++++++++
.../src/main/resources/application.properties | 2 +
.../src/main/resources/schema/create-order.json | 27 +++++++++++++
.../component/mcp/server/it/McpServerTest.java | 47 ++++++++++++++++++++++
5 files changed, 127 insertions(+)
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
index 2f9a19f6be..8c953f5a2a 100644
--- 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
@@ -22,7 +22,9 @@ import java.util.Map;
import io.quarkiverse.mcp.server.TextContent;
import io.quarkiverse.mcp.server.ToolManager;
import io.quarkiverse.mcp.server.ToolResponse;
+import io.vertx.core.json.JsonObject;
import org.apache.camel.CamelContext;
+import org.apache.camel.component.ai.tool.AiToolAnnotations;
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;
@@ -72,6 +74,13 @@ public class QuarkusMcpServerEngine extends ServiceSupport implements McpServerE
ParameterDef def = parameter.getValue();
definition.addArgument(parameter.getKey(), def.getDescription(), def.isRequired(), javaType(def));
}
+ // publish the bridge's pre-built JSON Schema as-is so nested argSchema structures and
+ // constraints such as enum survive; without this the schema shown to clients would be
+ // regenerated from the flat arguments above (empty for argSchema-based tools)
+ if (tool.inputSchemaJson() != null) {
+ definition.setInputSchema(new JsonObject(tool.inputSchemaJson()));
+ }
+ applyAnnotations(definition, tool.annotations());
definition.setHandler(arguments -> {
Map<String, Object> args = arguments.args() != null ? arguments.args() : Map.of();
McpToolCallResult result = tool.handler().call(args);
@@ -92,6 +101,31 @@ public class QuarkusMcpServerEngine extends ServiceSupport implements McpServerE
}
}
+ /**
+ * Maps the ai-tool annotation hints to the quarkiverse tool definition: {@code title} to the tool's top-level
+ * title and the boolean hints to {@link ToolManager.ToolAnnotations}. The quarkiverse annotations record uses
+ * primitive booleans, so hints left unset on the ai-tool endpoint are published with the MCP specification's
+ * client-side defaults (readOnly=false, destructive=true, idempotent=false, openWorld=true); when no hint is set
+ * at all, no annotations are published.
+ */
+ private static void applyAnnotations(ToolManager.ToolDefinition definition, AiToolAnnotations annotations) {
+ if (annotations == null) {
+ return;
+ }
+ if (annotations.title() != null) {
+ definition.setTitle(annotations.title());
+ }
+ if (annotations.readOnlyHint() != null || annotations.destructiveHint() != null
+ || annotations.idempotentHint() != null || annotations.openWorldHint() != null) {
+ definition.setAnnotations(new ToolManager.ToolAnnotations(
+ null,
+ annotations.readOnlyHint() != null ? annotations.readOnlyHint() : false,
+ annotations.destructiveHint() != null ? annotations.destructiveHint() : true,
+ annotations.idempotentHint() != null ? annotations.idempotentHint() : false,
+ annotations.openWorldHint() != null ? annotations.openWorldHint() : true));
+ }
+ }
+
private static Type javaType(ParameterDef def) {
String type = def.getType() != null ? def.getType() : "string";
return switch (type) {
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
index ab1efba168..a350864e75 100644
--- 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
@@ -16,6 +16,9 @@
*/
package org.apache.camel.quarkus.component.mcp.server.it;
+import java.util.List;
+import java.util.Map;
+
import jakarta.enterprise.context.ApplicationScoped;
import org.apache.camel.builder.RouteBuilder;
@@ -43,5 +46,19 @@ public class McpServerRoutes extends RouteBuilder {
from("ai-tool:other_tool?tags=untrusted&description=Not a selected tag, must not be exposed")
.setBody(constant("other"));
+
+ from("ai-tool:annotated_tool?tags=conformance&description=Tool with annotation hints"
+ + "&title=Annotated tool"
+ + "&readOnlyHint=true"
+ + "&idempotentHint=true")
+ .setBody(constant("annotated"));
+
+ from("ai-tool:create_order?tags=conformance&description=Create an order"
+ + "&argSchema=classpath:schema/create-order.json")
+ .process(e -> {
+ Map<?, ?> customer = e.getMessage().getHeader("customer", Map.class);
+ List<?> items = e.getMessage().getHeader("items", List.class);
+ e.getMessage().setBody("order for customer " + customer.get("id") + " with " + items.size() + " item(s)");
+ });
}
}
diff --git a/integration-tests/mcp-server/src/main/resources/application.properties b/integration-tests/mcp-server/src/main/resources/application.properties
index 91728bf643..92abd5f8cb 100644
--- a/integration-tests/mcp-server/src/main/resources/application.properties
+++ b/integration-tests/mcp-server/src/main/resources/application.properties
@@ -16,3 +16,5 @@
## ---------------------------------------------------------------------------
quarkus.camel.mcp-server.tags=conformance
quarkus.camel.mcp-server.tool-timeout=2000
+# the argSchema resource must be reachable in native mode
+quarkus.native.resources.includes=schema/create-order.json
diff --git a/integration-tests/mcp-server/src/main/resources/schema/create-order.json b/integration-tests/mcp-server/src/main/resources/schema/create-order.json
new file mode 100644
index 0000000000..c822f6f31e
--- /dev/null
+++ b/integration-tests/mcp-server/src/main/resources/schema/create-order.json
@@ -0,0 +1,27 @@
+{
+ "type": "object",
+ "properties": {
+ "customer": {
+ "type": "object",
+ "description": "The customer placing the order",
+ "properties": {
+ "id": { "type": "string", "description": "Customer id" }
+ },
+ "required": ["id"]
+ },
+ "items": {
+ "type": "array",
+ "description": "The items to order",
+ "items": {
+ "type": "object",
+ "properties": {
+ "sku": { "type": "string" },
+ "qty": { "type": "integer" }
+ },
+ "required": ["sku", "qty"]
+ }
+ }
+ },
+ "required": ["customer", "items"],
+ "additionalProperties": false
+}
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
index efa0e8945a..f629eec894 100644
--- 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
@@ -75,6 +75,46 @@ class McpServerTest {
.thenAssertResults();
}
+ @Test
+ void testToolAnnotationHintsArePublished() {
+ client().when()
+ .toolsList(page -> {
+ ToolInfo tool = toolByName(page, "annotated_tool");
+ assertThat(tool.title()).isEqualTo("Annotated tool");
+ assertThat(tool.annotations()).isPresent();
+ assertThat(tool.annotations().orElseThrow().readOnlyHint()).isTrue();
+ assertThat(tool.annotations().orElseThrow().idempotentHint()).isTrue();
+ })
+ .thenAssertResults();
+ }
+
+ @Test
+ void testArgSchemaIsPublishedAsInputSchema() {
+ client().when()
+ .toolsList(page -> {
+ ToolInfo tool = toolByName(page, "create_order");
+ assertThat(tool.inputSchema().getJsonObject("properties").getJsonObject("customer")
+ .getString("type")).isEqualTo("object");
+ assertThat(tool.inputSchema().getJsonObject("properties").getJsonObject("items")
+ .getString("type")).isEqualTo("array");
+ assertThat(tool.inputSchema().getJsonArray("required")).contains("customer", "items");
+ })
+ .thenAssertResults();
+ }
+
+ @Test
+ void testArgSchemaToolExecutesWithNestedArguments() {
+ client().when()
+ .toolsCall("create_order",
+ Map.of("customer", Map.of("id", "C-1"),
+ "items", List.of(Map.of("sku", "BOOK", "qty", 2))),
+ response -> {
+ assertThat(response.isError()).isFalse();
+ assertThat(textOf(response)).isEqualTo("order for customer C-1 with 1 item(s)");
+ })
+ .thenAssertResults();
+ }
+
@Test
void testQuarkusAnnotatedToolsCoexistWithCamelTools() {
// both tool sources are served by the same MCP server
@@ -161,6 +201,13 @@ class McpServerTest {
return page.tools().stream().map(ToolInfo::name).toList();
}
+ private static ToolInfo toolByName(ToolsPage page, String name) {
+ return page.tools().stream()
+ .filter(t -> name.equals(t.name()))
+ .findFirst()
+ .orElseThrow();
+ }
+
private static String textOf(ToolResponse response) {
return response.content().stream()
.filter(TextContent.class::isInstance)