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