This is an automated email from the ASF dual-hosted git repository.

jamesnetherton pushed a commit to branch camel-main
in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git


The following commit(s) were added to refs/heads/camel-main by this push:
     new 6a394090de CAMEL-24369: mcp-server: publish argSchema input schemas 
and MCP tool annotations
6a394090de is described below

commit 6a394090de1c55685f0533db4cb043a09d8def18
Author: Federico Mariani <[email protected]>
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 <[email protected]>
---
 .../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)

Reply via email to