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.git
The following commit(s) were added to refs/heads/main by this push:
new e96c0dc42c95 CAMEL-25258: camel-jbang-mcp - camel_transform_route
converts with the route DSL converter (#27261)
e96c0dc42c95 is described below
commit e96c0dc42c95f448e35f52fe561c8244754d0c51
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 2 10:27:42 2026 +0200
CAMEL-25258: camel-jbang-mcp - camel_transform_route converts with the
route DSL converter (#27261)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 10 +
.../modules/ROOT/pages/camel-jbang-mcp.adoc | 5 +-
.../dsl/jbang/core/commands/RouteDslConverter.java | 13 +-
.../jbang/core/commands/RouteDslConverterTest.java | 5 +-
dsl/camel-jbang/camel-jbang-mcp/pom.xml | 9 -
.../jbang/core/commands/mcp/TransformTools.java | 214 ++++-----------------
.../core/commands/mcp/TransformToolsTest.java | 77 ++++++--
7 files changed, 124 insertions(+), 209 deletions(-)
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 37d5b442c237..3118cfab8d0e 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -4235,3 +4235,13 @@ as Java, the error handler of an XML or YAML route is
now `.errorHandler(...)` o
The `timeout(long)` of the Resequencer EIP in Java keeps the millis as given
(`2000`), instead of a duration
(`2s0ms`).
+
+=== camel-jbang-mcp - camel_transform_route
+
+The `camel_transform_route` tool now converts with the same route DSL
converter as `camel transform route
+--format=java`. It reads the routes without running them, so a Java route is
no longer compiled. Instead it is read
+by the Java DSL parser, and a route the parser cannot read (such as a
processor lambda) is refused with the reason.
+Property placeholders are kept as written instead of being resolved.
+
+In return, Java is also a target format, and rests, route templates, route
configurations and beans are converted
+instead of only routes. A new `notes` field of the result lists what differs
or is not carried over.
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index e4f19d052b4c..e95cae788727 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -574,7 +574,10 @@ The advisories are also browseable as MCP resources:
`camel://security/advisorie
| Tool | Description
| `camel_transform_route`
-| Assists with route DSL format transformation between YAML and XML.
+| Converts routes between the YAML, XML and Java DSLs, as `camel transform
route` does, without running them:
+ routes, rests, route templates, route configurations and beans. The result
is read back and compared with the
+ source, and what differs or is not carried over is returned in `notes`. A
route that cannot be converted without
+ running it (such as a processor lambda) is refused with the reason.
| `camel_configuration_validate`
| Validate Camel configuration property lines (e.g., from
`application.properties`). Detects misspelled option
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverter.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverter.java
index b5fa941e030a..e10491a9a2e9 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverter.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverter.java
@@ -351,6 +351,9 @@ public final class RouteDslConverter {
DefaultCamelContext context, String fileName, String content,
Map<String, Supplier<String>> javaSources,
Resource resource, List<String> notes)
throws Exception {
+ if (JAVA_SEMANTIC.matcher(content).find()) {
+ return SEMANTIC_REFUSED;
+ }
CamelCatalog catalog = new DefaultCamelCatalog();
JavaParseResult r = ProjectRoutes.parseJava(content, javaSources,
catalog);
for (JavaParseResult.Unresolved u : r.unresolved()) {
@@ -430,12 +433,16 @@ public final class RouteDslConverter {
}
private static final Pattern YAML_SEMANTIC =
Pattern.compile("(?m)^-\\s+semantic\\s*:");
+ private static final Pattern JAVA_SEMANTIC =
Pattern.compile("\\bsemanticQuestions\\s*\\(");
+
+ // they live outside the route model the writers export: converting would
lose them without a word
+ private static final String SEMANTIC_REFUSED
+ = "semantic declarations cannot be converted with the routes: keep
them in a separate declaration resource"
+ + " and convert only the routes";
private static String loadYaml(DefaultCamelContext context, String
content, Resource resource) throws Exception {
if (YAML_SEMANTIC.matcher(content).find()) {
- // they live outside the route model the writers export:
converting would lose them without a word
- return "semantic declarations cannot be converted with the routes:
keep them in a separate declaration"
- + " resource and convert only the routes";
+ return SEMANTIC_REFUSED;
}
// the beans are taken out and read as definitions: the YAML DSL would
create them, running code of the project
StringBuilder routes = new StringBuilder();
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverterTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverterTest.java
index c23a8877ba8c..bc952e5cd541 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverterTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverterTest.java
@@ -172,9 +172,8 @@ class RouteDslConverterTest {
}
}
""";
- // the parser cannot read the declaration: refused at its line, not
dropped
- assertThat(convert("SemanticRoute.java", java,
"yaml").refused()).contains("SemanticRoute.java:5",
- "cannot be converted without running it");
+ // refused for the same reason as in YAML, not dropped
+ assertThat(convert("SemanticRoute.java", java,
"yaml").refused()).contains("semantic declarations");
}
@Test
diff --git a/dsl/camel-jbang/camel-jbang-mcp/pom.xml
b/dsl/camel-jbang/camel-jbang-mcp/pom.xml
index 40786dbc0ab9..602d5c4dd3ce 100644
--- a/dsl/camel-jbang/camel-jbang-mcp/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-mcp/pom.xml
@@ -111,10 +111,6 @@
<groupId>org.apache.camel</groupId>
<artifactId>camel-yaml-dsl</artifactId>
</dependency>
- <dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-java-joor-dsl</artifactId>
- </dependency>
<!-- Swagger/OpenAPI parser for contract-first OpenAPI tools -->
<dependency>
@@ -147,11 +143,6 @@
</dependency>
<!-- test dependencies -->
- <dependency>
- <groupId>org.apache.camel</groupId>
- <artifactId>camel-semantic</artifactId>
- <scope>test</scope>
- </dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-xml-io-dsl</artifactId>
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java
index d068f2403245..e7c64364d8e8 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java
@@ -16,11 +16,9 @@
*/
package org.apache.camel.dsl.jbang.core.commands.mcp;
-import java.io.ByteArrayInputStream;
-import java.io.StringWriter;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
+import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -29,22 +27,11 @@ import jakarta.enterprise.context.ApplicationScoped;
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.ToolArg;
import io.quarkiverse.mcp.server.ToolCallException;
-import org.apache.camel.impl.DefaultCamelContext;
-import org.apache.camel.model.ExpressionNode;
-import org.apache.camel.model.ProcessorDefinitionHelper;
-import org.apache.camel.model.RouteDefinition;
-import org.apache.camel.model.RoutesDefinition;
-import org.apache.camel.spi.Resource;
-import org.apache.camel.support.PluginHelper;
-import org.apache.camel.support.ResourceHelper;
-import org.apache.camel.util.ObjectHelper;
-import org.apache.camel.util.json.JsonObject;
-import org.apache.camel.xml.in.ModelParser;
-import org.apache.camel.yaml.out.YamlModelWriter;
+import org.apache.camel.dsl.jbang.core.commands.RouteDslConverter;
/**
- * MCP tool transforming Camel routes between DSL formats using Quarkus MCP
Server. Validation is the shared
- * {@code camel_validate_source} tool.
+ * MCP tool transforming Camel routes between DSL formats with the route DSL
converter of camel-jbang, which reads the
+ * routes without running them. Validation is the shared {@code
camel_validate_source} tool.
*/
@McpSecured
@ApplicationScoped
@@ -56,13 +43,16 @@ public class TransformTools {
* Tool to transform routes between DSL formats.
*/
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
- description = "Transform a Camel route between different DSL formats
(YAML, XML). " +
- "Note: Java to YAML/XML transformation has
limitations."
- + " Java DSL can only be used as source format, not as
target format.")
+ description = "Transform Camel routes between the YAML, XML and Java
DSLs, without running them."
+ + " Routes, rests, route templates, route
configurations and beans are converted."
+ + " The result is read back and compared with the
source: what differs, or is not carried"
+ + " over (such as comments, or the beans when the
target is Java), is listed in notes."
+ + " What cannot be converted without running it (a
processor lambda) is refused with the reason.")
public TransformResult camel_transform_route(
- @ToolArg(description = "Route definition to transform") String
route,
+ @ToolArg(description = "Route definition to transform: a YAML or
XML routes file, a Java RouteBuilder class,"
+ + " or Java route statements such as
from(\"timer:tick\").to(\"log:out\");") String route,
@ToolArg(description = "Source format (yaml, xml, java)") String
fromFormat,
- @ToolArg(description = "Target format (yaml, xml)") String
toFormat) {
+ @ToolArg(description = "Target format (yaml, xml, java)") String
toFormat) {
if (route == null || fromFormat == null || toFormat == null) {
throw new ToolCallException("route, fromFormat, and toFormat are
required", null);
@@ -72,175 +62,40 @@ public class TransformTools {
result.fromFormat = fromFormat;
result.toFormat = toFormat;
- String from = fromFormat.toLowerCase();
- String to = toFormat.toLowerCase();
+ String from = fromFormat.toLowerCase(Locale.ROOT);
+ String to = toFormat.toLowerCase(Locale.ROOT);
if (from.equals(to)) {
result.supported = true;
result.result = route;
return result;
}
-
- try {
- if ("xml".equals(from) && "yaml".equals(to)) {
- result.result = transformXmlToYaml(route);
- result.supported = true;
- } else if ("yaml".equals(from) && "xml".equals(to)) {
- result.result = transformYamlToXml(route);
- result.supported = true;
- } else if ("java".equals(from) && "yaml".equals(to)) {
- result.result = transformJavaToFormat(route, "yaml");
- result.supported = true;
- } else if ("java".equals(from) && "xml".equals(to)) {
- result.result = transformJavaToFormat(route, "xml");
- result.supported = true;
- } else {
- result.supported = false;
- result.note = "Unsupported transformation: " + fromFormat + "
to " + toFormat;
- }
- } catch (Throwable e) {
- Throwable cause = ObjectHelper.createExceptionIterator(e).next();
- String message = e.getMessage();
- if (cause != e) {
- message += ": " + cause.getMessage();
- }
- throw new ToolCallException(
- "Failed to transform route (" + e.getClass().getName() +
"): " + message, null);
- }
-
- return result;
- }
-
- /**
- * Transform an XML route definition to YAML format.
- */
- private String transformXmlToYaml(String xml) throws Exception {
- // Try Spring namespace first (most common), then fall back to no
namespace
- RoutesDefinition routes = null;
- try (ByteArrayInputStream is = new
ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))) {
- routes = new ModelParser(is,
"http://camel.apache.org/schema/spring")
- .parseRoutesDefinition().orElse(null);
- }
- if (routes == null) {
- try (ByteArrayInputStream is = new
ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))) {
- routes = new ModelParser(is)
- .parseRoutesDefinition().orElse(null);
- }
- }
- if (routes == null) {
- throw new IllegalArgumentException(
- "Could not parse XML route. Ensure it contains a valid
<routes> or <route> element.");
- }
-
- YamlModelWriter writer = new YamlModelWriter();
- List<JsonObject> roots = new ArrayList<>();
- for (RouteDefinition route : routes.getRoutes()) {
- roots.add(writer.writeRouteDefinition(route));
- }
- return writer.printAsYaml(roots);
- }
-
- /**
- * Transform a YAML route definition to XML format.
- */
- private String transformYamlToXml(String yaml) throws Exception {
- DefaultCamelContext ctx = new DefaultCamelContext();
- try {
- ctx.build();
-
- Resource resource = ResourceHelper.fromString("route.yaml", yaml);
- PluginHelper.getRoutesLoader(ctx).loadRoutes(resource);
-
- List<RouteDefinition> routeDefs = ctx.getRouteDefinitions();
- if (routeDefs == null || routeDefs.isEmpty()) {
- throw new IllegalArgumentException(
- "Could not parse YAML route. Ensure it contains a
valid route definition.");
- }
-
- RoutesDefinition rd = new RoutesDefinition();
- rd.setRoutes(routeDefs);
- requireSeparateDeclarations(ctx);
-
- StringWriter sw = new StringWriter();
- new
org.apache.camel.xml.out.ModelWriter(sw).writeRoutesDefinition(rd);
- return sw.toString();
- } finally {
- ctx.stop();
+ if (!RouteDslConverter.FORMATS.contains(from) ||
!RouteDslConverter.FORMATS.contains(to)) {
+ result.supported = false;
+ result.note = "Unsupported transformation: " + fromFormat + " to "
+ toFormat;
+ return result;
}
- }
- private static void requireSeparateDeclarations(DefaultCamelContext
context) throws ReflectiveOperationException {
- // Semantic declarations are optional and live outside the model
exported by this converter.
- Class<?> type =
context.getClassResolver().resolveClass("org.apache.camel.semantic.SemanticQuestions");
- if (type == null) {
- return;
+ RouteDslConverter.Result r = RouteDslConverter.convert(fileName(route,
from), route, to, Map.of());
+ if (!r.converted()) {
+ throw new ToolCallException("Cannot transform route: " +
r.refused(), null);
}
- Object questions =
context.getCamelContextExtension().getContextPlugin(type);
- if (questions != null && !(boolean)
type.getMethod("isEmpty").invoke(questions)) {
- throw new IllegalArgumentException(
- "Semantic declarations cannot be exported by the generic
route converter. "
- + "Keep them in a separate
declaration resource and convert only the routes.");
- }
- }
-
- private String transformJavaToFormat(String java, String targetFormat)
throws Exception {
- DefaultCamelContext ctx = new DefaultCamelContext();
- try {
- ctx.build();
-
- String source = wrapSnippetIfNeeded(java);
- String className = extractClassName(source);
- Resource resource = ResourceHelper.fromString(className + ".java",
source);
- PluginHelper.getRoutesLoader(ctx).loadRoutes(resource);
-
- List<RouteDefinition> routeDefs = ctx.getRouteDefinitions();
- if (routeDefs == null || routeDefs.isEmpty()) {
- throw new IllegalArgumentException(
- "Could not parse Java route. Ensure it contains a
valid route definition.");
- }
-
- // Java expression clauses are normally materialized when
processors are created.
- routeDefs.forEach(route ->
ProcessorDefinitionHelper.filterTypeInOutputs(route.getOutputs(),
ExpressionNode.class)
- .forEach(ExpressionNode::preCreateProcessor));
-
- if ("yaml".equals(targetFormat)) {
- YamlModelWriter writer = new YamlModelWriter();
- List<JsonObject> roots = new ArrayList<>();
- requireSeparateDeclarations(ctx);
- for (RouteDefinition route : routeDefs) {
- roots.add(writer.writeRouteDefinition(route));
- }
- return writer.printAsYaml(roots);
- } else {
- RoutesDefinition rd = new RoutesDefinition();
- rd.setRoutes(routeDefs);
- requireSeparateDeclarations(ctx);
-
- StringWriter sw = new StringWriter();
- new
org.apache.camel.xml.out.ModelWriter(sw).writeRoutesDefinition(rd);
- return sw.toString();
- }
- } finally {
- ctx.stop();
+ result.supported = true;
+ result.result = r.content();
+ result.notes = r.notes();
+ if (!r.notes().isEmpty()) {
+ result.note = String.join("; ", r.notes());
}
+ return result;
}
- private static String wrapSnippetIfNeeded(String source) {
- if (CLASS_NAME_PATTERN.matcher(source).find()) {
- return source;
+ /** A file name for the route as the converter tells the DSLs apart: Java
by its class. */
+ private static String fileName(String route, String format) {
+ if ("java".equals(format)) {
+ Matcher m = CLASS_NAME_PATTERN.matcher(route);
+ return (m.find() ? m.group(1) : "Route") + ".java";
}
- return "import org.apache.camel.builder.RouteBuilder;\n\n"
- + "public class SnippetRoute extends RouteBuilder {\n"
- + " @Override\n"
- + " public void configure() {\n"
- + " " + source + "\n"
- + " }\n"
- + "}\n";
- }
-
- private static String extractClassName(String source) {
- Matcher m = CLASS_NAME_PATTERN.matcher(source);
- return m.find() ? m.group(1) : "Route";
+ return "route.camel." + format;
}
// Result class for Jackson serialization
@@ -249,6 +104,7 @@ public class TransformTools {
public String fromFormat;
public String toFormat;
public String note;
+ public List<String> notes = List.of();
public boolean supported;
public String result;
}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java
index a5dc8ed53fb3..fcee87765f55 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java
@@ -66,12 +66,12 @@ class TransformToolsTest {
@CsvSource({ "yaml,xml", "java,xml", "java,yaml" })
void genericConversionDoesNotSilentlyLoseSemanticDeclarations(String
source, String target) {
assertThatThrownBy(() ->
createTools().camel_transform_route(semanticRoute(source, "0.8"), source,
target))
-
.isInstanceOf(ToolCallException.class).hasMessageContaining("Keep them in a
separate declaration resource");
+
.isInstanceOf(ToolCallException.class).hasMessageContaining("keep them in a
separate declaration resource");
}
@ParameterizedTest
@ValueSource(strings = { "xml", "yaml" })
- void emptySemanticRegistryDoesNotPreventConversion(String target) {
+ void anEmptySemanticDeclarationIsRefusedToo(String target) {
String route = """
import org.apache.camel.builder.RouteBuilder;
import static
org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions;
@@ -82,20 +82,23 @@ class TransformToolsTest {
}
}
""";
- var result = createTools().camel_transform_route(route, "java",
target);
-
- assertThat(result.supported).isTrue();
-
assertThat(result.result).contains("direct:input").doesNotContain("semantic");
+ // read without running it, so whether it declares anything is not
known: refused, not dropped
+ assertThatThrownBy(() -> createTools().camel_transform_route(route,
"java", target))
+
.isInstanceOf(ToolCallException.class).hasMessageContaining("semantic
declarations");
}
- @ParameterizedTest
- @CsvSource({ "yaml,xml", "java,xml", "java,yaml" })
- void semanticConversionReportsMissingNumericProperty(String source, String
target) {
- String route = semanticRoute(source,
"{{semantic.export.missing.threshold}}");
-
- assertThatThrownBy(() -> createTools().camel_transform_route(route,
source, target))
- .isInstanceOf(ToolCallException.class)
- .hasMessageContaining("Property with key
[semantic.export.missing.threshold] not found");
+ @Test
+ void placeholdersAreNotResolved() {
+ // the routes are read, not run: a property that is not there is no
error, the placeholder is kept
+ var result = createTools().camel_transform_route("""
+ - route:
+ from:
+ uri: "timer:{{my.timer.name}}"
+ steps:
+ - log: "{{my.message}}"
+ """, "yaml", "xml");
+ assertThat(result.supported).isTrue();
+ assertThat(result.result).contains("timer:{{my.timer.name}}",
"{{my.message}}");
}
private static String semanticRoute(String source, String threshold) {
@@ -251,6 +254,52 @@ class TransformToolsTest {
assertThat(result.result).contains("log:foo");
}
+ @Test
+ void transformXmlToJava() {
+ String xml = """
+ <routes xmlns="http://camel.apache.org/schema/xml-io">
+ <!-- greetings -->
+ <route id="hello">
+ <from uri="timer:tick"/>
+ <log message="Hello ${body}" loggingLevel="WARN"/>
+ </route>
+ </routes>
+ """;
+ TransformTools.TransformResult result =
createTools().camel_transform_route(xml, "xml", "java");
+ assertThat(result.supported).isTrue();
+ assertThat(result.result).contains("public class Route extends
RouteBuilder", "from(\"timer:tick\")",
+ ".routeId(\"hello\")", ".log(LoggingLevel.WARN, \"Hello
${body}\")");
+ // what is not carried over is said
+ assertThat(result.notes).anyMatch(n -> n.contains("comments"));
+ assertThat(result.note).contains("comments");
+ }
+
+ @Test
+ void restsAreCarriedOver() {
+ String xml = """
+ <rests xmlns="http://camel.apache.org/schema/xml-io">
+ <rest path="/api">
+ <get path="/orders">
+ <to uri="direct:orders"/>
+ </get>
+ </rest>
+ </rests>
+ """;
+ TransformTools.TransformResult result =
createTools().camel_transform_route(xml, "xml", "yaml");
+ assertThat(result.supported).isTrue();
+ assertThat(result.result).contains("- rest:", "path: /api", "get:",
"path: /orders", "direct:orders");
+ assertThat(result.notes).isEmpty();
+ }
+
+ @Test
+ void whatOnlyRunsIsRefusedWithTheReason() {
+ String java = """
+ from("timer:tick").process(e ->
e.getIn().setBody("x")).to("log:out");
+ """;
+ assertThatThrownBy(() -> createTools().camel_transform_route(java,
"java", "yaml"))
+
.isInstanceOf(ToolCallException.class).hasMessageContaining("cannot be
converted without running it");
+ }
+
@Test
void sameFormatReturnsInput() {
TransformTools tools = createTools();