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

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


The following commit(s) were added to refs/heads/main by this push:
     new de43e58e512 CAMEL-24369: camel-mcp-server-starter: publish MCP tool 
annotations and fix json-schema-validator resolution (#1881)
de43e58e512 is described below

commit de43e58e5126e04acd24eeb3d62806790445b300
Author: Federico Mariani <[email protected]>
AuthorDate: Fri Aug 7 12:05:12 2026 +0200

    CAMEL-24369: camel-mcp-server-starter: publish MCP tool annotations and fix 
json-schema-validator resolution (#1881)
    
    SpringAiMcpServerEngine now maps the ai-tool annotation hints (title,
    readOnlyHint, destructiveHint, idempotentHint, openWorldHint) to the MCP
    tool, mirroring the Vert.x engine; unset hints are not published. This
    required promoting the starter's camel-ai-tool dependency from test to
    compile scope (the direct test-scope declaration overrode the transitive
    compile scope from camel-mcp-server-api).
    
    Also declare the Jackson3-based com.networknt:json-schema-validator 3.0.0
    required by the MCP SDK's mcp-json-jackson3 tool-argument validation:
    without it, nearest-wins resolution can pick the Jackson2-based 2.0.x line
    and the application fails at startup with
    NoSuchMethodError: Schema.validate(tools.jackson.databind.JsonNode).
    
    Co-authored-by: Claude Fable 5 <[email protected]>
---
 .../camel-mcp-server-starter/pom.xml               | 11 +++++-
 .../mcp/server/SpringAiMcpServerEngine.java        | 43 ++++++++++++++++++++--
 .../mcp/server/SpringBootMcpServerTest.java        | 29 +++++++++++++++
 3 files changed, 79 insertions(+), 4 deletions(-)

diff --git a/components-starter/camel-mcp-server-starter/pom.xml 
b/components-starter/camel-mcp-server-starter/pom.xml
index 527872d230b..c143963ac93 100644
--- a/components-starter/camel-mcp-server-starter/pom.xml
+++ b/components-starter/camel-mcp-server-starter/pom.xml
@@ -48,6 +48,16 @@
       <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
       <version>${spring-ai-version}</version>
     </dependency>
+    <!-- direct dependency so applications resolve the Jackson3-based 
json-schema-validator
+             required by the MCP SDK's mcp-json-jackson3 tool-argument 
validation: without it,
+             nearest-wins can pick the Jackson2-based 2.0.x line (Camel 
manages 2.0.1 for other
+             components) and the application fails at startup with
+             NoSuchMethodError: 
Schema.validate(tools.jackson.databind.JsonNode) -->
+    <dependency>
+      <groupId>com.networknt</groupId>
+      <artifactId>json-schema-validator</artifactId>
+      <version>3.0.0</version>
+    </dependency>
     <!-- test dependencies -->
     <dependency>
       <groupId>org.springframework.boot</groupId>
@@ -59,7 +69,6 @@
       <groupId>org.apache.camel</groupId>
       <artifactId>camel-ai-tool</artifactId>
       <version>${camel-version}</version>
-      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.awaitility</groupId>
diff --git 
a/components-starter/camel-mcp-server-starter/src/main/java/org/apache/camel/springboot/mcp/server/SpringAiMcpServerEngine.java
 
b/components-starter/camel-mcp-server-starter/src/main/java/org/apache/camel/springboot/mcp/server/SpringAiMcpServerEngine.java
index aeb5bd24f3e..1728fd4fd9a 100644
--- 
a/components-starter/camel-mcp-server-starter/src/main/java/org/apache/camel/springboot/mcp/server/SpringAiMcpServerEngine.java
+++ 
b/components-starter/camel-mcp-server-starter/src/main/java/org/apache/camel/springboot/mcp/server/SpringAiMcpServerEngine.java
@@ -24,6 +24,7 @@ import io.modelcontextprotocol.server.McpServerFeatures;
 import io.modelcontextprotocol.server.McpSyncServer;
 import io.modelcontextprotocol.spec.McpSchema;
 import org.apache.camel.CamelContext;
+import org.apache.camel.component.ai.tool.AiToolAnnotations;
 import org.apache.camel.component.mcp.server.McpServerEngine;
 import org.apache.camel.component.mcp.server.McpServerInfo;
 import org.apache.camel.component.mcp.server.McpServerTool;
@@ -77,9 +78,10 @@ public class SpringAiMcpServerEngine extends ServiceSupport 
implements McpServer
     @Override
     public void toolAdded(McpServerTool tool) {
         String schema = tool.inputSchemaJson() != null ? 
tool.inputSchemaJson() : EMPTY_OBJECT_SCHEMA;
-        McpSchema.Tool mcpTool = McpSchema.Tool.builder(tool.name(), 
jsonMapper, schema)
-                .description(tool.description())
-                .build();
+        McpSchema.Tool.Builder builder = McpSchema.Tool.builder(tool.name(), 
jsonMapper, schema)
+                .description(tool.description());
+        applyAnnotations(builder, tool.annotations());
+        McpSchema.Tool mcpTool = builder.build();
         McpServerFeatures.SyncToolSpecification spec = 
McpServerFeatures.SyncToolSpecification.builder()
                 .tool(mcpTool)
                 .callHandler((exchange, request) -> {
@@ -104,4 +106,39 @@ public class SpringAiMcpServerEngine extends 
ServiceSupport implements McpServer
             LOG.debug("Failed to remove MCP tool {}: {}", toolName, 
e.getMessage());
         }
     }
+
+    /**
+     * Maps the ai-tool annotation hints to the MCP tool: {@code title} to the 
tool's top-level title and the boolean
+     * hints to {@code ToolAnnotations}. Unset hints are not published, 
mirroring the Vert.x engine, so MCP clients
+     * apply their own spec defaults.
+     */
+    private static void applyAnnotations(McpSchema.Tool.Builder builder, 
AiToolAnnotations annotations) {
+        if (annotations == null) {
+            return;
+        }
+        if (annotations.title() != null) {
+            builder.title(annotations.title());
+        }
+        McpSchema.ToolAnnotations.Builder hintBuilder = 
McpSchema.ToolAnnotations.builder();
+        boolean hasHints = false;
+        if (annotations.readOnlyHint() != null) {
+            hintBuilder.readOnlyHint(annotations.readOnlyHint());
+            hasHints = true;
+        }
+        if (annotations.destructiveHint() != null) {
+            hintBuilder.destructiveHint(annotations.destructiveHint());
+            hasHints = true;
+        }
+        if (annotations.idempotentHint() != null) {
+            hintBuilder.idempotentHint(annotations.idempotentHint());
+            hasHints = true;
+        }
+        if (annotations.openWorldHint() != null) {
+            hintBuilder.openWorldHint(annotations.openWorldHint());
+            hasHints = true;
+        }
+        if (hasHints) {
+            builder.annotations(hintBuilder.build());
+        }
+    }
 }
diff --git 
a/components-starter/camel-mcp-server-starter/src/test/java/org/apache/camel/springboot/mcp/server/SpringBootMcpServerTest.java
 
b/components-starter/camel-mcp-server-starter/src/test/java/org/apache/camel/springboot/mcp/server/SpringBootMcpServerTest.java
index d240924a0bc..3f075e8414c 100644
--- 
a/components-starter/camel-mcp-server-starter/src/test/java/org/apache/camel/springboot/mcp/server/SpringBootMcpServerTest.java
+++ 
b/components-starter/camel-mcp-server-starter/src/test/java/org/apache/camel/springboot/mcp/server/SpringBootMcpServerTest.java
@@ -115,6 +115,12 @@ public class SpringBootMcpServerTest {
 
                     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"));
                 }
             };
         }
@@ -149,6 +155,29 @@ public class SpringBootMcpServerTest {
                 .doesNotContain("hidden_tool", "other_tool");
     }
 
+    @Test
+    void testToolAnnotationHintsArePublished() {
+        McpSchema.Tool tool = client().listTools().tools().stream()
+                .filter(t -> "annotated_tool".equals(t.name()))
+                .findFirst()
+                .orElseThrow();
+
+        assertThat(tool.title()).isEqualTo("Annotated tool");
+        assertThat(tool.annotations()).isNotNull();
+        assertThat(tool.annotations().readOnlyHint()).isTrue();
+        assertThat(tool.annotations().idempotentHint()).isTrue();
+    }
+
+    @Test
+    void testToolWithoutHintsHasNoAnnotations() {
+        McpSchema.Tool tool = client().listTools().tools().stream()
+                .filter(t -> "say_hello".equals(t.name()))
+                .findFirst()
+                .orElseThrow();
+
+        assertThat(tool.annotations()).isNull();
+    }
+
     @Test
     void testSpringAnnotatedToolsCoexistWithCamelTools() {
         // both tool sources are served by the same MCP server

Reply via email to