This is an automated email from the ASF dual-hosted git repository.
Croway pushed a commit to branch camel-spring-boot-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel-spring-boot.git
The following commit(s) were added to refs/heads/camel-spring-boot-4.22.x by
this push:
new a8ebb48298a CAMEL-26782: camel-ai-tool-starter - do not publish unset
MCP tool hints as false
a8ebb48298a is described below
commit a8ebb48298abe121a43f64411da6c32a3914f378
Author: Salvatore Mongiardo <[email protected]>
AuthorDate: Tue Nov 10 17:28:39 2026 +0100
CAMEL-26782: camel-ai-tool-starter - do not publish unset MCP tool hints as
false
On the 4.22.x branch the starter copies every option of the generated
configuration class onto the component. AiToolComponentConfiguration carries
the catalog default false for readOnlyHint, destructiveHint, idempotentHint,
openWorldHint and returnDirect, so every tool was published with explicit
false MCP annotations although its route set none.
Clear these options on the configuration class after Spring Boot binds it,
unless they are configured. Values set on a user's own component bean or by
a user customizer are kept.
This is option C of CAMEL-26782 for 4.22.x. Main does not need it: since
CAMEL-24501 the starter copies only the options the application configured.
---
.../AiToolUnsetHintsAutoConfiguration.java | 68 ++++++++++++++++++++++
...rk.boot.autoconfigure.AutoConfiguration.imports | 1 +
.../AiToolComponentAutoConfigurationTest.java | 13 ++++-
...omponentExplicitHintsAutoConfigurationTest.java | 56 ++++++++++++++++++
.../AiToolComponentUserBeanHintsTest.java | 63 ++++++++++++++++++++
.../AiToolComponentUserCustomizerHintsTest.java | 65 +++++++++++++++++++++
.../camel-mcp-server-starter/pom.xml | 6 ++
7 files changed, 271 insertions(+), 1 deletion(-)
diff --git
a/components-starter/camel-ai-tool-starter/src/main/java/org/apache/camel/component/ai/tool/springboot/AiToolUnsetHintsAutoConfiguration.java
b/components-starter/camel-ai-tool-starter/src/main/java/org/apache/camel/component/ai/tool/springboot/AiToolUnsetHintsAutoConfiguration.java
new file mode 100644
index 00000000000..e839248e91e
--- /dev/null
+++
b/components-starter/camel-ai-tool-starter/src/main/java/org/apache/camel/component/ai/tool/springboot/AiToolUnsetHintsAutoConfiguration.java
@@ -0,0 +1,68 @@
+/*
+ * 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.component.ai.tool.springboot;
+
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.env.Environment;
+
+/**
+ * Keeps ai-tool MCP hints and returnDirect unset unless they are explicitly
configured.
+ * <p>
+ * The Camel catalog reports a default of false for Boolean options without a
declared default, so the generated
+ * {@link AiToolComponentConfiguration} initializes these options to false and
the generated customizer would publish
+ * them on every tool. This clears them on the configuration class after
Spring Boot binds it, leaving values set on a
+ * component or by a user customizer untouched.
+ * <p>
+ * Only needed on the 4.22.x branch: from 4.23 the starter copies only the
options the application configured
+ * (CAMEL-24501), so these options stay unset without it.
+ */
+@AutoConfiguration
+public class AiToolUnsetHintsAutoConfiguration {
+
+ private static final String PREFIX = "camel.component.ai-tool.";
+
+ private static final Map<String, BiConsumer<AiToolComponentConfiguration,
Boolean>> OPTIONS = Map.of(
+ "read-only-hint", AiToolComponentConfiguration::setReadOnlyHint,
+ "destructive-hint",
AiToolComponentConfiguration::setDestructiveHint,
+ "idempotent-hint", AiToolComponentConfiguration::setIdempotentHint,
+ "open-world-hint", AiToolComponentConfiguration::setOpenWorldHint,
+ "return-direct", AiToolComponentConfiguration::setReturnDirect);
+
+ @Bean
+ static BeanPostProcessor clearUnsetAiToolHints(Environment environment) {
+ Binder binder = Binder.get(environment);
+ return new BeanPostProcessor() {
+ @Override
+ public Object postProcessAfterInitialization(Object bean, String
beanName) {
+ if (bean instanceof AiToolComponentConfiguration
configuration) {
+ OPTIONS.forEach((option, setter) -> {
+ if (!binder.bind(PREFIX + option,
Boolean.class).isBound()) {
+ setter.accept(configuration, null);
+ }
+ });
+ }
+ return bean;
+ }
+ };
+ }
+}
diff --git
a/components-starter/camel-ai-tool-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
b/components-starter/camel-ai-tool-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index ecbc5ac146c..db784abeab0 100644
---
a/components-starter/camel-ai-tool-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++
b/components-starter/camel-ai-tool-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -16,3 +16,4 @@
## ---------------------------------------------------------------------------
org.apache.camel.component.ai.tool.springboot.AiToolComponentConverter
org.apache.camel.component.ai.tool.springboot.AiToolComponentAutoConfiguration
+org.apache.camel.component.ai.tool.springboot.AiToolUnsetHintsAutoConfiguration
diff --git
a/components-starter/camel-ai-tool-starter/src/test/java/org/apache/camel/component/ai/tool/springboot/AiToolComponentAutoConfigurationTest.java
b/components-starter/camel-ai-tool-starter/src/test/java/org/apache/camel/component/ai/tool/springboot/AiToolComponentAutoConfigurationTest.java
index bc1457118d6..d24d0709555 100644
---
a/components-starter/camel-ai-tool-starter/src/test/java/org/apache/camel/component/ai/tool/springboot/AiToolComponentAutoConfigurationTest.java
+++
b/components-starter/camel-ai-tool-starter/src/test/java/org/apache/camel/component/ai/tool/springboot/AiToolComponentAutoConfigurationTest.java
@@ -18,6 +18,7 @@ package org.apache.camel.component.ai.tool.springboot;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Set;
@@ -41,7 +42,7 @@ import org.springframework.test.annotation.DirtiesContext;
@DirtiesContext
@CamelSpringBootTest
@SpringBootTest(classes = { CamelAutoConfiguration.class,
AiToolComponentConverter.class,
- AiToolComponentAutoConfiguration.class,
+ AiToolComponentAutoConfiguration.class,
AiToolUnsetHintsAutoConfiguration.class,
AiToolComponentAutoConfigurationTest.TestConfiguration.class },
properties = {
"camel.component.ai-tool.tags = demo" })
public class AiToolComponentAutoConfigurationTest {
@@ -77,6 +78,16 @@ public class AiToolComponentAutoConfigurationTest {
assertEquals("demo", component.getConfiguration().getTags());
}
+ @Test
+ public void unsetMcpHintsStayUnsetOnTheComponent() {
+ AiToolComponent component = assertInstanceOf(AiToolComponent.class,
context.getComponent("ai-tool"));
+ assertNull(component.getConfiguration().getReadOnlyHint());
+ assertNull(component.getConfiguration().getDestructiveHint());
+ assertNull(component.getConfiguration().getIdempotentHint());
+ assertNull(component.getConfiguration().getOpenWorldHint());
+ assertNull(component.getConfiguration().getReturnDirect());
+ }
+
@Test
public void routeIsRegisteredAsToolUnderTheConfiguredTag() {
AiToolRegistry registry = AiToolRegistry.getOrCreate(context);
diff --git
a/components-starter/camel-ai-tool-starter/src/test/java/org/apache/camel/component/ai/tool/springboot/AiToolComponentExplicitHintsAutoConfigurationTest.java
b/components-starter/camel-ai-tool-starter/src/test/java/org/apache/camel/component/ai/tool/springboot/AiToolComponentExplicitHintsAutoConfigurationTest.java
new file mode 100644
index 00000000000..26d8bef4577
--- /dev/null
+++
b/components-starter/camel-ai-tool-starter/src/test/java/org/apache/camel/component/ai/tool/springboot/AiToolComponentExplicitHintsAutoConfigurationTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.component.ai.tool.springboot;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.component.ai.tool.AiToolComponent;
+import org.apache.camel.spring.boot.CamelAutoConfiguration;
+import org.apache.camel.test.spring.junit6.CamelSpringBootTest;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.annotation.DirtiesContext;
+
+@DirtiesContext
+@CamelSpringBootTest
+@SpringBootTest(classes = { CamelAutoConfiguration.class,
AiToolComponentConverter.class,
+ AiToolComponentAutoConfiguration.class,
AiToolUnsetHintsAutoConfiguration.class }, properties = {
+ "camel.component.ai-tool.read-only-hint = true",
+ "camel.component.ai-tool.destructive-hint = false" })
+public class AiToolComponentExplicitHintsAutoConfigurationTest {
+
+ @Autowired
+ CamelContext context;
+
+ @Test
+ public void explicitHintsAreStillCopiedOntoTheComponent() {
+ AiToolComponent component = assertInstanceOf(AiToolComponent.class,
context.getComponent("ai-tool"));
+ assertEquals(Boolean.TRUE,
component.getConfiguration().getReadOnlyHint());
+ // an explicit false is a real value and must be kept
+ assertEquals(Boolean.FALSE,
component.getConfiguration().getDestructiveHint());
+ // hints that were not configured stay unset
+ assertNull(component.getConfiguration().getIdempotentHint());
+ assertNull(component.getConfiguration().getOpenWorldHint());
+ assertNull(component.getConfiguration().getReturnDirect());
+ }
+}
diff --git
a/components-starter/camel-ai-tool-starter/src/test/java/org/apache/camel/component/ai/tool/springboot/AiToolComponentUserBeanHintsTest.java
b/components-starter/camel-ai-tool-starter/src/test/java/org/apache/camel/component/ai/tool/springboot/AiToolComponentUserBeanHintsTest.java
new file mode 100644
index 00000000000..e37c3fef20c
--- /dev/null
+++
b/components-starter/camel-ai-tool-starter/src/test/java/org/apache/camel/component/ai/tool/springboot/AiToolComponentUserBeanHintsTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.component.ai.tool.springboot;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.component.ai.tool.AiToolComponent;
+import org.apache.camel.spring.boot.CamelAutoConfiguration;
+import org.apache.camel.test.spring.junit6.CamelSpringBootTest;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
+
+@DirtiesContext
+@CamelSpringBootTest
+@SpringBootTest(classes = { CamelAutoConfiguration.class,
AiToolComponentConverter.class,
+ AiToolComponentAutoConfiguration.class,
AiToolUnsetHintsAutoConfiguration.class,
+ AiToolComponentUserBeanHintsTest.TestConfiguration.class }, properties
= {
+ "camel.component.ai-tool.tags=demo" })
+public class AiToolComponentUserBeanHintsTest {
+
+ @Autowired
+ CamelContext context;
+
+ @Test
+ public void hintSetInJavaOnUserBeanIsKept() {
+ AiToolComponent component = assertInstanceOf(AiToolComponent.class,
context.getComponent("ai-tool"));
+ assertEquals(Boolean.TRUE,
component.getConfiguration().getReadOnlyHint());
+ assertNull(component.getConfiguration().getDestructiveHint());
+ }
+
+ @Configuration
+ static class TestConfiguration {
+ @Bean("ai-tool")
+ AiToolComponent aiTool() {
+ AiToolComponent component = new AiToolComponent();
+ component.getConfiguration().setReadOnlyHint(true);
+ return component;
+ }
+ }
+}
diff --git
a/components-starter/camel-ai-tool-starter/src/test/java/org/apache/camel/component/ai/tool/springboot/AiToolComponentUserCustomizerHintsTest.java
b/components-starter/camel-ai-tool-starter/src/test/java/org/apache/camel/component/ai/tool/springboot/AiToolComponentUserCustomizerHintsTest.java
new file mode 100644
index 00000000000..a692fff840d
--- /dev/null
+++
b/components-starter/camel-ai-tool-starter/src/test/java/org/apache/camel/component/ai/tool/springboot/AiToolComponentUserCustomizerHintsTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.component.ai.tool.springboot;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.component.ai.tool.AiToolComponent;
+import org.apache.camel.spi.ComponentCustomizer;
+import org.apache.camel.spring.boot.CamelAutoConfiguration;
+import org.apache.camel.test.spring.junit6.CamelSpringBootTest;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
+
+@DirtiesContext
+@CamelSpringBootTest
+@SpringBootTest(classes = { CamelAutoConfiguration.class,
AiToolComponentConverter.class,
+ AiToolComponentAutoConfiguration.class,
AiToolUnsetHintsAutoConfiguration.class,
+ AiToolComponentUserCustomizerHintsTest.TestConfiguration.class },
properties = {
+ "camel.component.ai-tool.tags=demo" })
+public class AiToolComponentUserCustomizerHintsTest {
+
+ @Autowired
+ CamelContext context;
+
+ @Test
+ public void hintSetByUserCustomizerIsKept() {
+ AiToolComponent component = assertInstanceOf(AiToolComponent.class,
context.getComponent("ai-tool"));
+ assertEquals(Boolean.TRUE,
component.getConfiguration().getReadOnlyHint());
+ assertNull(component.getConfiguration().getDestructiveHint());
+ }
+
+ @Configuration
+ static class TestConfiguration {
+ // Runs after the generated customizer, which is how a user can
override starter values.
+ @Bean
+ ComponentCustomizer userAiToolHints() {
+ return ComponentCustomizer.builder(AiToolComponent.class)
+ .withOrder(100)
+ .build(component ->
component.getConfiguration().setReadOnlyHint(true));
+ }
+ }
+}
diff --git a/components-starter/camel-mcp-server-starter/pom.xml
b/components-starter/camel-mcp-server-starter/pom.xml
index 919fbe87d70..22de5653c1a 100644
--- a/components-starter/camel-mcp-server-starter/pom.xml
+++ b/components-starter/camel-mcp-server-starter/pom.xml
@@ -70,6 +70,12 @@
<artifactId>camel-ai-tool</artifactId>
<version>${camel-version}</version>
</dependency>
+ <!-- puts the ai-tool starter auto-configuration on the test classpath, so
SpringBootMcpServerTest checks that a tool without hints has no annotations -->
+ <dependency>
+ <groupId>org.apache.camel.springboot</groupId>
+ <artifactId>camel-ai-tool-starter</artifactId>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>