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

Croway 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 0bd78692be89 CAMEL-26400: camel-jbang - AI tools: 
camel_eval_expression takes headers, variables, resultType and predicate 
(#28683)
0bd78692be89 is described below

commit 0bd78692be89d647faac60d2053550dce2729fcb
Author: Federico Mariani <[email protected]>
AuthorDate: Thu Oct 29 17:07:52 2026 +0100

    CAMEL-26400: camel-jbang - AI tools: camel_eval_expression takes headers, 
variables, resultType and predicate (#28683)
    
    * CAMEL-26400: camel-jbang - AI tools: camel_eval_expression takes headers, 
variables, resultType and predicate
    
    camel_eval_expression took only the expression, language, body and 
integration name, so it could not evaluate an
    expression the way the route runs it: an XPath function such as string(), 
local-name(), boolean() or count() failed
    with "Can not convert #STRING to a NodeList!" (the xpath language gives 
nodes unless resultType says otherwise),
    predicate mode was guessed for simple only, and a header or variable an 
earlier step sets was null. A headers
    argument was silently ignored, and a local XPath that worked showed the 
class of its node list.
    
    The tool (shared registry and MCP wrapper) now takes headers and variables 
(a JSON object, whose values keep their
    JSON type, or key=value lines as camel_runtime_send takes headers), 
predicate for any language (simple keeps its
    guess when it is absent) and resultType, a class name passed to the 
language as its resultType option as the
    reifiers pass it. Locally they are set on the scratch exchange and the 
value is converted to String by the
    context's type converter, as the integration does. In the integration the 
eval action carries them;
    EvalLanguageDevConsole gets a resultType option and LocalCliConnector 
passes it on and keeps the JSON type of a
    header or variable value. An integration before Camel 4.24 gets a list or 
object value as JSON text, and the answer
    notes that it ignores resultType.
    
    An XPath that fails with "Can not convert #STRING/#BOOLEAN/#NUMBER to a 
NodeList" gets a hint with the argument to
    pass and the route form (resultType java.lang.String, predicate=true, 
java.lang.Integer); in the integration, where
    the failed eval action is a tool error, the hint is appended to the error 
message.
    
    camel-jbang-core gets camel-xpath, camel-groovy and camel-jq as test 
dependencies, so the tests evaluate them
    locally without a download. The prompt budgets of the TUI AI panel are 
raised by the ~114 tokens of the four
    arguments.
    
    * CAMEL-26400: regenerate dev-consoles-openapi.json for the resultType 
option of eval-language
    
    * CAMEL-26400: camel-jbang - AI tools: the elvis rewrite also goes with the 
tool error of an integration before 4.24
    
    An integration before Camel 4.24 refuses ${header.missing ?: 10}, and a 
failed
    eval action is a tool error (CAMEL-25398), so the rewrite that CAMEL-26401 
adds
    to the answer never reached the model for the integration it was meant for. 
It
    now goes with the error, as the XPath hint does.
    
    * CAMEL-26400: camel-jbang - AI tools: the JSON of headers and variables is 
held to the argument length limit, and a predicate is true or false
    
    McpSecurityInterceptor checks the arguments that are strings; headers and
    variables sent as a JSON object are not, so their JSON is now held to the 
same
    max-argument-length. A predicate other than true or false (such as yes) is
    refused instead of being taken for false.
    
    * Update 
dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
    
    Co-authored-by: Guillaume Nodet - AI Bot <[email protected]>
    
    * CAMEL-26400: camel-console - the text output of eval-language reports an 
error instead of throwing
    
    Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    ---------
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Guillaume Nodet - AI Bot <[email protected]>
    Co-authored-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Sonnet 5.5 <[email protected]>
---
 .../apache/camel/catalog/dev-consoles-openapi.json |   4 +
 .../camel/catalog/dev-consoles/eval-language.json  |  18 +-
 .../apache/camel/dev-console/eval-language.json    |  18 +-
 .../camel/impl/console/EvalLanguageDevConsole.java |  46 +++-
 .../impl/console/EvalLanguageDevConsoleTest.java   |  51 ++++
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |  10 +-
 .../ROOT/pages/camel-jbang-tui-ai-agents.adoc      |   3 +-
 .../camel/cli/connector/LocalCliConnector.java     |  13 +-
 .../connector/LocalCliConnectorEvalActionTest.java |  93 +++++++
 dsl/camel-jbang/camel-jbang-core/pom.xml           |  17 ++
 .../dsl/jbang/core/commands/ai/AuthoringTools.java |  35 ++-
 .../core/commands/ai/ExpressionEvaluator.java      | 287 ++++++++++++++++++---
 .../camel/dsl/jbang/core/common/ActionClient.java  |   7 +-
 .../camel/dsl/jbang/core/common/RuntimeHelper.java |  26 +-
 .../core/commands/ai/EvalInTheIntegrationTest.java | 146 ++++++++++-
 .../ai/ExpressionEvaluatorRequestTest.java         | 209 +++++++++++++++
 .../jbang/core/commands/mcp/AuthoringTools.java    |  48 +++-
 .../core/commands/mcp/AuthoringToolsTest.java      |  51 +++-
 .../core/commands/tui/AiPanelPromptBudgetTest.java |  16 +-
 19 files changed, 1023 insertions(+), 75 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
index c6f2f1b1939f..d5d9c43a81a0 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
@@ -1888,6 +1888,10 @@
                                                                                
"default": false,
                                                                                
"description": "Whether to execute as predicate (use expression by default)"
                                                                        },
+                                                                       
"resultType": {
+                                                                               
"type": "string",
+                                                                               
"description": "Optional type of the result, as a class name such as 
java.lang.String, passed to the language as its resultType option, as in a 
route (not used for a predicate)"
+                                                                       },
                                                                        
"template": {
                                                                                
"type": "string",
                                                                                
"description": "Template to use for executing simple language function"
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/eval-language.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/eval-language.json
index cf657a2c2247..70723c97a8e1 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/eval-language.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/eval-language.json
@@ -71,9 +71,23 @@
       "defaultValue": false,
       "description": "Whether to execute as predicate (use expression by 
default)"
     },
-    "template": {
+    "resultType": {
       "index": 4,
       "kind": "option",
+      "displayName": "Result Type",
+      "group": "query",
+      "label": "query",
+      "required": false,
+      "type": "string",
+      "javaType": "java.lang.String",
+      "deprecated": false,
+      "autowired": false,
+      "secret": false,
+      "description": "Optional type of the result, as a class name such as 
java.lang.String, passed to the language as its resultType option, as in a 
route (not used for a predicate)"
+    },
+    "template": {
+      "index": 5,
+      "kind": "option",
       "displayName": "Template",
       "group": "query",
       "label": "query",
@@ -86,7 +100,7 @@
       "description": "Template to use for executing simple language function"
     },
     "variables": {
-      "index": 5,
+      "index": 6,
       "kind": "option",
       "displayName": "Variables",
       "group": "query",
diff --git 
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/eval-language.json
 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/eval-language.json
index cf657a2c2247..70723c97a8e1 100644
--- 
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/eval-language.json
+++ 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/eval-language.json
@@ -71,9 +71,23 @@
       "defaultValue": false,
       "description": "Whether to execute as predicate (use expression by 
default)"
     },
-    "template": {
+    "resultType": {
       "index": 4,
       "kind": "option",
+      "displayName": "Result Type",
+      "group": "query",
+      "label": "query",
+      "required": false,
+      "type": "string",
+      "javaType": "java.lang.String",
+      "deprecated": false,
+      "autowired": false,
+      "secret": false,
+      "description": "Optional type of the result, as a class name such as 
java.lang.String, passed to the language as its resultType option, as in a 
route (not used for a predicate)"
+    },
+    "template": {
+      "index": 5,
+      "kind": "option",
       "displayName": "Template",
       "group": "query",
       "label": "query",
@@ -86,7 +100,7 @@
       "description": "Template to use for executing simple language function"
     },
     "variables": {
-      "index": 5,
+      "index": 6,
       "kind": "option",
       "displayName": "Variables",
       "group": "query",
diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/EvalLanguageDevConsole.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/EvalLanguageDevConsole.java
index 3effec949507..4743d45ccb4a 100644
--- 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/EvalLanguageDevConsole.java
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/EvalLanguageDevConsole.java
@@ -21,6 +21,7 @@ import java.util.Map;
 import org.apache.camel.Exchange;
 import org.apache.camel.Expression;
 import org.apache.camel.Predicate;
+import org.apache.camel.spi.Language;
 import org.apache.camel.spi.Metadata;
 import org.apache.camel.spi.annotations.DevConsole;
 import org.apache.camel.support.DefaultExchange;
@@ -58,6 +59,12 @@ public class EvalLanguageDevConsole extends 
AbstractDevConsole {
     @Metadata(label = "query", description = "Optional exchange variables", 
javaType = "java.lang.String")
     public static final String VARIABLES = "variables";
 
+    @Metadata(label = "query",
+              description = "Optional type of the result, as a class name such 
as java.lang.String, passed to the language"
+                            + " as its resultType option, as in a route (not 
used for a predicate)",
+              javaType = "java.lang.String")
+    public static final String RESULT_TYPE = "resultType";
+
     public EvalLanguageDevConsole() {
         super("camel", "eval-language", "Evaluate Language", "Evaluate 
Language and display result");
     }
@@ -84,14 +91,18 @@ public class EvalLanguageDevConsole extends 
AbstractDevConsole {
             }
 
             String out;
-            boolean predicate = optionBoolean(options, PREDICATE, false);
-            template = resolvePlaceholders(template);
-            if (predicate) {
-                Predicate pre = 
getCamelContext().resolveLanguage(language).createPredicate(template);
-                out = pre.matches(dummy) ? "true" : "false";
-            } else {
-                Expression exp = 
getCamelContext().resolveLanguage(language).createExpression(template);
-                out = exp.evaluate(dummy, String.class);
+            try {
+                boolean predicate = optionBoolean(options, PREDICATE, false);
+                template = resolvePlaceholders(template);
+                if (predicate) {
+                    Predicate pre = 
getCamelContext().resolveLanguage(language).createPredicate(template);
+                    out = pre.matches(dummy) ? "true" : "false";
+                } else {
+                    Expression exp = createExpression(language, template, 
options);
+                    out = exp.evaluate(dummy, String.class);
+                }
+            } catch (Exception e) {
+                out = "Error: " + e.getMessage();
             }
             sb.append(String.format("%nEvaluating (%s): %s", language, 
template));
             sb.append("\n");
@@ -100,6 +111,23 @@ public class EvalLanguageDevConsole extends 
AbstractDevConsole {
         return sb.toString();
     }
 
+    /**
+     * The expression, with the resultType option of the language when one is 
given, as a route passes it: an XPath
+     * function such as string() or count() then gives its value, where the 
default result of xpath is nodes.
+     */
+    private Expression createExpression(String language, String template, 
Map<String, Object> options) {
+        Language lan = getCamelContext().resolveLanguage(language);
+        String resultType = optionString(options, RESULT_TYPE);
+        if (resultType == null || resultType.isBlank()) {
+            return lan.createExpression(template);
+        }
+        Class<?> type = 
getCamelContext().getClassResolver().resolveClass(resultType.strip());
+        if (type == null) {
+            throw new IllegalArgumentException("resultType " + resultType + " 
is not a class on the classpath");
+        }
+        return lan.createExpression(template, new Object[] { type });
+    }
+
     /**
      * Resolves property placeholders in the template first, as the route 
loaders do before a language parses the text,
      * so that {{myPeriod}} or ${body} > {{myThreshold}} evaluate as they 
would in a route (CAMEL-24698).
@@ -144,7 +172,7 @@ public class EvalLanguageDevConsole extends 
AbstractDevConsole {
                     Predicate pre = 
getCamelContext().resolveLanguage(language).createPredicate(template);
                     out = pre.matches(dummy) ? "true" : "false";
                 } else {
-                    Expression exp = 
getCamelContext().resolveLanguage(language).createExpression(template);
+                    Expression exp = createExpression(language, template, 
options);
                     out = exp.evaluate(dummy, String.class);
                 }
             } catch (Exception e) {
diff --git 
a/core/camel-console/src/test/java/org/apache/camel/impl/console/EvalLanguageDevConsoleTest.java
 
b/core/camel-console/src/test/java/org/apache/camel/impl/console/EvalLanguageDevConsoleTest.java
index 9e2c4e881aff..2f7fef177473 100644
--- 
a/core/camel-console/src/test/java/org/apache/camel/impl/console/EvalLanguageDevConsoleTest.java
+++ 
b/core/camel-console/src/test/java/org/apache/camel/impl/console/EvalLanguageDevConsoleTest.java
@@ -114,6 +114,57 @@ public class EvalLanguageDevConsoleTest extends 
AbstractDevConsoleTest {
         Assertions.assertEquals("true", out.getString("result"));
     }
 
+    @Test
+    public void testEvalWithResultType() {
+        DevConsole con = assertConsoleExists("eval-language", "camel");
+
+        // the resultType goes to the language as in a route: the body as an 
Integer
+        JsonObject out = callJson(con, Map.of(
+                "language", "simple",
+                "template", "${body}",
+                "body", "0042",
+                "resultType", "java.lang.Integer"));
+        Assertions.assertEquals("success", out.getString("status"));
+        Assertions.assertEquals("42", out.getString("result"));
+
+        // the short name of a java.lang class, as a route resolves it
+        out = callJson(con, Map.of(
+                "language", "simple",
+                "template", "${body}",
+                "body", "0042",
+                "resultType", "Integer"));
+        Assertions.assertEquals("42", out.getString("result"));
+
+        out = callJson(con, Map.of(
+                "language", "simple",
+                "template", "${body}",
+                "body", "0042",
+                "resultType", "com.example.NoSuchType"));
+        Assertions.assertEquals("failed", out.getString("status"));
+        JsonObject exception = out.getMap("exception");
+        
Assertions.assertTrue(exception.getString("message").contains("com.example.NoSuchType"));
+
+        String text = callText(con, Map.of(
+                "language", "simple",
+                "template", "${body}",
+                "body", "0042",
+                "resultType", "java.lang.Integer"));
+        Assertions.assertTrue(text.endsWith("\n42"), text);
+    }
+
+    @Test
+    public void testEvalTextWithUnknownResultType() {
+        DevConsole con = assertConsoleExists("eval-language", "camel");
+
+        // the text output reports the error, as the json output does, instead 
of throwing
+        String text = callText(con, Map.of(
+                "language", "simple",
+                "template", "${body}",
+                "body", "0042",
+                "resultType", "com.example.NoSuchType"));
+        Assertions.assertTrue(text.contains("com.example.NoSuchType"), text);
+    }
+
     @Test
     public void testEvalWithBodyAndVariables() {
         DevConsole con = assertConsoleExists("eval-language", "camel");
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 e007e01e0a3f..8278cf1b8668 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -478,7 +478,15 @@ passed as the file name, is answered with the argument the 
source goes in.
   and returns the value (true/false for a predicate) or the syntax error with 
the position where it breaks,
   so an agent can check an expression before writing it into a route. Any 
language works: one that is not on
   the server's own classpath, `jsonpath`, `jq`, `xpath`, `groovy`, has its 
component downloaded on first use
-  and the answer says so in `downloaded`. A null value is answered with 
`nullResult: true` and a note, since the
+  and the answer says so in `downloaded`. The message can carry what an 
earlier step of the route sets:
+  `headers` and `variables`, as a JSON object (its values keep their JSON 
type, such as the rows of a query for
+  a groovy `headers.rows[0].total`) or as `key=value` lines. `predicate=true` 
evaluates a predicate in any
+  language (for simple it is guessed from an operator), and `resultType` is 
the `resultType` option of the
+  language, as in a route: `java.lang.String` for an XPath function such as 
`string()` or `local-name()`,
+  which the `xpath` language otherwise evaluates as nodes. An XPath that fails 
that way
+  (`Can not convert #STRING to a NodeList`) gets a hint with the argument to 
pass. An integration before
+  Camel 4.24 ignores `resultType`, which the answer notes.
+  A null value is answered with `nullResult: true` and a note, since the
   answer leaves out null fields; an elvis operator without a space on both 
sides inside `${...}` (`${header.limit ?:10}`
   reads a header named `limit ?:10`), or inside `${...}` for an integration 
older than Camel 4.24, gets a note with
   the expression rewritten.
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai-agents.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai-agents.adoc
index 6f6c9c1e2e4f..44fd28832b43 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai-agents.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai-agents.adoc
@@ -234,7 +234,8 @@ together with the catalog's functions and operators (their 
count and names by gr
 the matching ones with parameters and examples), and `docPage` serves the 
operators, functions, OGNL and
 advanced documentation pages. `camel_eval_expression` evaluates an expression 
inside the running integration,
 like `camel cmd eval`, or locally when no integration is selected, and returns 
the value or the parser error,
-so the agent can try an expression before it answers or writes it. Component 
lookups get the same treatment for endpoint URIs: every endpoint option
+so the agent can try an expression before it answers or writes it; it takes 
the headers, variables, `resultType`
+and predicate mode the expression has in the route. Component lookups get the 
same treatment for endpoint URIs: every endpoint option
 says whether it is part of the URI path or a query parameter, and the result 
spells out the URI rules for
 that component (its path options, the `?option=value&option=value` form, the 
YAML `uri` plus `parameters`
 form, placeholders, `RAW()`, and that component options belong in 
`application.properties`). With
diff --git 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index dfb6d5abaff3..3675eb725d2c 100644
--- 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++ 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -1213,18 +1213,19 @@ public class LocalCliConnector extends ServiceSupport
             boolean predicate = root.getBooleanOrDefault("predicate", false);
             String template = root.getStringOrDefault("template", "");
             String body = root.getStringOrDefault("body", "");
-            Map<String, String> headerMap = new LinkedHashMap<>();
+            // a value as it is in the JSON: text, or a number, a list or an 
object (the rows of a header for groovy)
+            Map<String, Object> headerMap = new LinkedHashMap<>();
             Collection<JsonObject> headers = root.getCollection("headers");
             if (headers != null) {
                 for (JsonObject jo : headers) {
-                    headerMap.put(jo.getString("key"), jo.getString("value"));
+                    headerMap.put(jo.getString("key"), jo.get("value"));
                 }
             }
-            Map<String, String> variableMap = new LinkedHashMap<>();
+            Map<String, Object> variableMap = new LinkedHashMap<>();
             Collection<JsonObject> variables = root.getCollection("variables");
             if (variables != null) {
                 for (JsonObject jo : variables) {
-                    variableMap.put(jo.getString("key"), 
jo.getString("value"));
+                    variableMap.put(jo.getString("key"), jo.get("value"));
                 }
             }
             Map<String, Object> params = new HashMap<>();
@@ -1234,6 +1235,10 @@ public class LocalCliConnector extends ServiceSupport
             params.put("body", body);
             params.put("headers", headerMap);
             params.put("variables", variableMap);
+            String resultType = root.getString("resultType");
+            if (resultType != null && !resultType.isBlank()) {
+                params.put("resultType", resultType);
+            }
             JsonObject json = (JsonObject) dc.call(DevConsole.MediaType.JSON, 
params);
             writeOutput(json);
         } else {
diff --git 
a/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/LocalCliConnectorEvalActionTest.java
 
b/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/LocalCliConnectorEvalActionTest.java
new file mode 100644
index 000000000000..ca7bb3aadbf7
--- /dev/null
+++ 
b/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/LocalCliConnectorEvalActionTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.cli.connector;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.CliConnectorFactory;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-26400: the eval action passes the resultType of the call to the 
eval-language console, and a header or variable
+ * keeps the JSON type of its value (a list of rows), as camel_eval_expression 
sends them.
+ */
+class LocalCliConnectorEvalActionTest {
+
+    @Test
+    void theResultTypeGoesToTheConsole() throws Exception {
+        JsonObject request = new JsonObject();
+        request.put("action", "eval");
+        request.put("language", "simple");
+        request.put("template", "${body}");
+        request.put("body", "0042");
+        request.put("resultType", "java.lang.Integer");
+
+        JsonObject answer = dispatch(request);
+
+        
assertThat(answer.getString("status")).as(answer.toJson()).isEqualTo("success");
+        assertThat(answer.getString("result")).isEqualTo("42");
+    }
+
+    @Test
+    void aHeaderAndAVariableKeepTheTypeOfTheirValue() throws Exception {
+        JsonArray rows = new JsonArray();
+        JsonObject row = new JsonObject();
+        row.put("total", 3);
+        rows.add(row);
+        JsonObject request = new JsonObject();
+        request.put("action", "eval");
+        request.put("language", "simple");
+        request.put("template", "${header.rows.size()} ${variable.count}");
+        request.put("headers", new JsonArray(List.of(keyValue("rows", rows))));
+        request.put("variables", new JsonArray(List.of(keyValue("count", 7))));
+
+        // as the action file carries it: JSON text
+        JsonObject answer = dispatch((JsonObject) 
Jsoner.deserialize(request.toJson()));
+
+        
assertThat(answer.getString("status")).as(answer.toJson()).isEqualTo("success");
+        assertThat(answer.getString("result")).isEqualTo("1 7");
+    }
+
+    private static JsonObject dispatch(JsonObject request) throws Exception {
+        try (var context = new DefaultCamelContext()) {
+            // the action is dispatched here: a connector of the context would 
poll the {pid} files of other tests
+            var disabled = new DefaultCliConnectorFactory();
+            disabled.setEnabled(false);
+            
context.getCamelContextExtension().addContextPlugin(CliConnectorFactory.class, 
disabled);
+            context.start();
+            var connector = new LocalCliConnector(new 
DefaultCliConnectorFactory());
+            connector.setCamelContext(context);
+            var answer = new AtomicReference<JsonObject>();
+            assertThat(connector.dispatch(request, answer::set)).isTrue();
+            return answer.get();
+        }
+    }
+
+    private static JsonObject keyValue(String key, Object value) {
+        JsonObject jo = new JsonObject();
+        jo.put("key", key);
+        jo.put("value", value);
+        return jo;
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/pom.xml 
b/dsl/camel-jbang/camel-jbang-core/pom.xml
index 7b921f8eb8f2..a24cbcd917f4 100644
--- a/dsl/camel-jbang/camel-jbang-core/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-core/pom.xml
@@ -236,6 +236,23 @@
             <artifactId>camel-mcp-server</artifactId>
             <scope>test</scope>
         </dependency>
+        <!-- languages camel_eval_expression evaluates locally with headers, 
variables and a resultType; on the
+             classpath of the tests, so they are not downloaded -->
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-xpath</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-groovy</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-jq</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>io.modelcontextprotocol.sdk</groupId>
             <artifactId>mcp-core</artifactId>
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
index 62e249e17336..cbbb5be0abae 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
@@ -78,6 +78,12 @@ public final class AuthoringTools {
               + " half done, e.g. a to: direct:x and the route from direct:x:"
               + " [{\"find\": \"...\", \"replace\": \"...\"}, {\"find\": 
\"...\", \"replace\": \"...\"}]";
     static final String DIRECTORY_DESC = "Project directory with the source 
files (default: the selected integration's)";
+    // the arguments of camel_eval_expression for the message it is evaluated 
against, and how (CAMEL-26400)
+    public static final String EVAL_HEADERS_DESC = "Message headers: a JSON 
object, or key=value lines";
+    public static final String EVAL_VARIABLES_DESC = "Exchange variables: a 
JSON object, or key=value lines";
+    public static final String EVAL_PREDICATE_DESC = "Evaluate as a predicate 
(default: guessed for simple, else false)";
+    public static final String EVAL_RESULT_TYPE_DESC
+            = "Result class as resultType in a route, e.g. java.lang.String 
for an XPath string() or local-name()";
 
     /** Files listed and read by the file tools; more than that and a 
directory is not an integration's sources. */
     static final String FILE_PATH_DESC = "File path relative to the directory, 
e.g. src/main/resources/camel/foo.camel.yaml";
@@ -397,16 +403,26 @@ public final class AuthoringTools {
                 .param("language", "string", "simple (default), jsonpath, 
xpath, jq", false)
                 .param("body", "string", "Message body", false)
                 .param("name", "string", NAME_DESC, false)
+                .param("headers", "string", EVAL_HEADERS_DESC, false)
+                .param("variables", "string", EVAL_VARIABLES_DESC, false)
+                .param("predicate", "boolean", EVAL_PREDICATE_DESC, false)
+                .param("resultType", "string", EVAL_RESULT_TYPE_DESC, false)
                 .core(true)
                 .executor((ctx, args) -> {
+                    String expression = required(args, "expression");
+                    Map<String, Object> headers = 
ExpressionEvaluator.parseValues("headers", args.get("headers"));
+                    Map<String, Object> variables = 
ExpressionEvaluator.parseValues("variables", args.get("variables"));
+                    Boolean predicate = booleanArgument("predicate", 
args.get("predicate"));
                     String name = args.get("name");
                     if (name != null && !name.isBlank()) {
                         ctx.selectProcess(name);
                     } else {
                         ctx.selectSingleProcessIfNone();
                     }
-                    return ExpressionEvaluator.evaluate(ctx, 
args.get("language"), required(args, "expression"),
-                            args.get("body")).toJson();
+                    return ExpressionEvaluator.evaluate(ctx, new 
ExpressionEvaluator.Request(
+                            args.get("language"),
+                            expression, args.get("body"), headers, variables, 
predicate,
+                            args.get("resultType"))).toJson();
                 }));
 
         registry.accept(tool("camel_dependency_for_class",
@@ -2612,6 +2628,21 @@ public final class AuthoringTools {
         return value == null || value.isBlank() ? defaultValue : 
Boolean.parseBoolean(value.trim());
     }
 
+    /**
+     * A boolean argument that may be left out: null when it is, and refused 
when it is not true or false, where
+     * {@link Boolean#valueOf(String)} would take {@code yes} for false 
without a word.
+     */
+    static Boolean booleanArgument(String key, String value) {
+        if (value == null || value.isBlank()) {
+            return null;
+        }
+        String text = value.strip();
+        if ("true".equalsIgnoreCase(text) || "false".equalsIgnoreCase(text)) {
+            return Boolean.valueOf(text);
+        }
+        throw new ToolExecutionException(key + " is true or false, not " + 
text);
+    }
+
     static int integer(Map<String, String> args, String key, int defaultValue) 
{
         String value = args.get(key);
         if (value == null || value.isBlank()) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
index 2e99d3896939..f2926e9aad39 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
@@ -16,14 +16,20 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.ai;
 
+import java.util.Collection;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import org.apache.camel.CamelContext;
 import org.apache.camel.Exchange;
 import org.apache.camel.Expression;
 import org.apache.camel.Predicate;
 import org.apache.camel.catalog.LanguageValidationResult;
+import org.apache.camel.dsl.jbang.core.common.ActionClient;
+import org.apache.camel.dsl.jbang.core.common.RuntimeHelper;
 import org.apache.camel.dsl.jbang.core.common.ScratchContextHelper;
 import org.apache.camel.impl.DefaultCamelContext;
 import org.apache.camel.main.download.DependencyDownloaderClassLoader;
@@ -31,6 +37,8 @@ import 
org.apache.camel.main.download.MavenDependencyDownloader;
 import org.apache.camel.spi.Language;
 import org.apache.camel.support.DefaultExchange;
 import org.apache.camel.tooling.model.LanguageModel;
+import org.apache.camel.util.json.DeserializationException;
+import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.util.json.Jsoner;
 
@@ -50,9 +58,77 @@ public final class ExpressionEvaluator {
     // a path that ends with a filter: $[?(@.sku == 'CAMEL-MUG')]
     private static final Pattern FILTER_ENDS_PATH = 
Pattern.compile("\\)]\\s*$");
 
+    // the JDK's XPath asked for nodes, the default result of the xpath 
language, by a function that gives a value
+    private static final Pattern XPATH_NOT_NODES = Pattern.compile("Can not 
convert #(STRING|BOOLEAN|NUMBER) to a NodeList");
+
     private ExpressionEvaluator() {
     }
 
+    /**
+     * What to evaluate, and the message to evaluate it against.
+     *
+     * @param language   the language, simple by default
+     * @param expression the expression
+     * @param body       the message body, or null for an empty one
+     * @param headers    the message headers, or null for none
+     * @param variables  the exchange variables, or null for none
+     * @param predicate  whether to evaluate it as a predicate; null guesses 
it for simple (an operator) and is false
+     *                   for the other languages
+     * @param resultType the class of the result, such as java.lang.String, as 
the resultType of the language in a
+     *                   route, or null for the language's own (nodes for 
xpath)
+     */
+    public record Request(
+            String language, String expression, String body, Map<String, 
Object> headers,
+            Map<String, Object> variables, Boolean predicate, String 
resultType) {
+
+        public Request(String language, String expression, String body) {
+            this(language, expression, body, null, null, null, null);
+        }
+
+        boolean isPredicate() {
+            return Boolean.TRUE.equals(predicate);
+        }
+    }
+
+    /**
+     * Headers or variables as the tool takes them: a JSON object, whose 
values keep their JSON type (so a list of rows
+     * works for a groovy headers.rows[0].total), or key=value lines as 
camel_runtime_send takes headers, whose values
+     * are text.
+     *
+     * @param  name                   the argument, headers or variables, for 
the error
+     * @param  text                   the argument, or null
+     * @return                        the values by name, or null when there 
are none
+     * @throws ToolExecutionException when the text is neither
+     */
+    public static Map<String, Object> parseValues(String name, String text) {
+        if (text == null || text.isBlank()) {
+            return null;
+        }
+        String trimmed = text.strip();
+        Map<String, Object> answer = new LinkedHashMap<>();
+        if (trimmed.startsWith("{")) {
+            Object json;
+            try {
+                json = Jsoner.deserialize(trimmed);
+            } catch (DeserializationException e) {
+                throw new ToolExecutionException(name + " is not a valid JSON 
object: " + e.getMessage());
+            }
+            if (json instanceof Map<?, ?> map) {
+                map.forEach((k, v) -> answer.put(String.valueOf(k), v));
+                return answer.isEmpty() ? null : answer;
+            }
+        }
+        try {
+            for (Object entry : RuntimeHelper.parseKeyValues(trimmed, "A line 
of " + name)) {
+                JsonObject jo = (JsonObject) entry;
+                answer.put(jo.getString("key"), jo.getString("value"));
+            }
+        } catch (IllegalArgumentException e) {
+            throw new ToolExecutionException(e.getMessage() + " (or pass a 
JSON object)");
+        }
+        return answer.isEmpty() ? null : answer;
+    }
+
     /** Whether a simple expression is a predicate: it has a comparison or 
logical operator and no ternary or elvis. */
     public static boolean looksLikePredicate(String expression) {
         if (expression == null || expression.contains(" ? ") || 
expression.contains(" ?: ")) {
@@ -69,26 +145,48 @@ public final class ExpressionEvaluator {
      * @return            language, expression, predicate, status (ok or 
error) and the result or error
      */
     public static JsonObject evaluate(ToolContext ctx, String language, String 
expression, String body) {
-        String lang = language == null || language.isBlank() ? "simple" : 
language;
-        boolean predicate = "simple".equals(lang) && 
looksLikePredicate(expression);
+        return evaluate(ctx, new Request(language, expression, body));
+    }
+
+    /**
+     * @param  ctx     the context; its selected process evaluates the 
expression when it has one
+     * @param  request the expression, and the message to evaluate it against
+     * @return         language, expression, predicate, status (ok or error) 
and the result or error
+     */
+    public static JsonObject evaluate(ToolContext ctx, Request request) {
+        String lang = request.language() == null || 
request.language().isBlank() ? "simple" : request.language();
+        String expression = request.expression();
+        String body = request.body();
+        boolean predicate = request.predicate() != null
+                ? request.predicate() : "simple".equals(lang) && 
looksLikePredicate(expression);
+        String resultType = request.resultType() == null || 
request.resultType().isBlank()
+                ? null : request.resultType().strip();
         JsonObject result = new JsonObject();
         result.put("language", lang);
         result.put("expression", expression);
         if (predicate) {
             result.put("predicate", true);
+        } else if (resultType != null) {
+            result.put("resultType", resultType);
         }
         if (body == null && expression.contains("${body")) {
             result.put("note", "evaluated with an empty body; pass body to 
evaluate against a value");
         }
+        if (predicate && resultType != null) {
+            addNote(result, "resultType is not used for a predicate, which 
gives true or false");
+            resultType = null;
+        }
+        Request r = new Request(lang, expression, body, request.headers(), 
request.variables(), predicate, resultType);
         if (ctx.hasProcess()) {
             result.put("evaluatedIn", "the running integration (pid " + 
ctx.pid() + ")");
-            evaluateInProcess(ctx, lang, expression, body, predicate, result);
+            evaluateInProcess(ctx, r, result);
         } else {
             result.put("evaluatedIn", "a local scratch context (no integration 
selected)");
-            evaluateLocally(ctx, lang, expression, body, predicate, result);
+            evaluateLocally(ctx, r, result);
         }
         addNullAndElvisNotes(ctx, result, lang, expression);
         addFilterNote(result, lang, expression);
+        addXPathHint(result, lang, expression);
         return result;
     }
 
@@ -107,14 +205,21 @@ public final class ExpressionEvaluator {
             result.put("result", null);
             result.put("nullResult", true);
         }
-        String elvis = "simple".equals(lang) && expression != null && 
expression.contains("?:")
-                ? SimpleChecks.elvisInsideFunction(expression, 
camelVersion(ctx)) : null;
+        String elvis = elvisNote(ctx, lang, expression);
         String note = elvis != null ? elvis : isNull ? NULL_NOTE : null;
         if (note != null) {
             result.put("note", result.containsKey("note") ? 
result.getString("note") + "; " + note : note);
         }
     }
 
+    /**
+     * What to say about an elvis operator inside ${...} that the Camel of the 
evaluation does not read as one, or null.
+     */
+    private static String elvisNote(ToolContext ctx, String lang, String 
expression) {
+        return "simple".equals(lang) && expression != null && 
expression.contains("?:")
+                ? SimpleChecks.elvisInsideFunction(expression, 
camelVersion(ctx)) : null;
+    }
+
     /**
      * The Camel version that evaluates: the selected integration's, else the 
one the tools are for (null for the CLI's
      * own, which evaluates locally).
@@ -133,12 +238,54 @@ public final class ExpressionEvaluator {
         }
     }
 
+    /** Adds a note to the result of an evaluation, after the note it has 
already. */
+    private static void addNote(JsonObject result, String note) {
+        result.put("note", result.containsKey("note") ? 
result.getString("note") + "; " + note : note);
+    }
+
     /** Adds the note about a jsonpath filter to the result of an evaluation, 
after the note it has already. */
     static void addFilterNote(JsonObject result, String lang, String 
expression) {
         String filter = filterNote(lang, expression, 
result.getString("status"));
         if (filter != null) {
-            result.put("note", result.containsKey("note") ? 
result.getString("note") + "; " + filter : filter);
+            addNote(result, filter);
+        }
+    }
+
+    /** Adds what to pass to an XPath function that gives a value instead of 
nodes, to an error of an evaluation. */
+    static void addXPathHint(JsonObject result, String lang, String 
expression) {
+        String hint = xpathHint(lang, expression, result.getString("error"));
+        if (hint != null) {
+            result.put("hint", hint);
+        }
+    }
+
+    /**
+     * The xpath language evaluates to nodes unless told otherwise, so a 
function that gives a value (string(),
+     * local-name(), boolean(), count()) fails with "Can not convert #STRING 
to a NodeList": a model of the eval tried
+     * the tool three times with such an XPath, then gave up on it. The hint 
says what to pass, here and in the route.
+     */
+    static String xpathHint(String lang, String expression, String error) {
+        if (!"xpath".equals(lang) || error == null) {
+            return null;
+        }
+        Matcher m = XPATH_NOT_NODES.matcher(error);
+        if (!m.find()) {
+            return null;
         }
+        String route = "xpath: {expression: " + quoted(expression) + ", 
resultType: ";
+        return switch (m.group(1)) {
+            case "STRING" -> "this XPath returns a string, not nodes: pass 
resultType java.lang.String (in a route: "
+                             + route + "java.lang.String})";
+            case "BOOLEAN" -> "this XPath returns a boolean, not nodes: pass 
predicate=true (in a route: use it as the"
+                              + " predicate of a filter or a when, or as " + 
route + "java.lang.Boolean})";
+            default -> "this XPath returns a number, not nodes: pass 
resultType java.lang.Integer, or"
+                       + " java.lang.Double for a fraction (in a route: " + 
route + "java.lang.Integer})";
+        };
+    }
+
+    /** The text as a quoted YAML value: in single quotes when it holds a 
double quote, as the YAML validator quotes. */
+    private static String quoted(String text) {
+        return text.contains("\"") || text.contains("\\") ? "'" + 
text.replace("'", "''") + "'" : "\"" + text + "\"";
     }
 
     /**
@@ -162,17 +309,49 @@ public final class ExpressionEvaluator {
                + " java.lang.String or java.lang.Integer, gives the one value 
when one element matches";
     }
 
-    private static void evaluateInProcess(
-            ToolContext ctx, String lang, String expression, String body, 
boolean predicate, JsonObject result) {
-        String raw = ctx.executeAction("eval", root -> {
-            root.put("language", lang);
-            root.put("predicate", String.valueOf(predicate));
-            // as they are: the action file is JSON, and the integration does 
not unescape them
-            root.put("template", expression);
-            if (body != null) {
-                root.put("body", body);
+    private static void evaluateInProcess(ToolContext ctx, Request r, 
JsonObject result) {
+        // an integration before Camel 4.24 reads the value of a header or 
variable as text, and has no resultType
+        String version = ActionClient.versionOf(ctx.pid());
+        boolean before424 = version != null && 
!ActionClient.isAtLeast(version, 4, 24);
+        String ignored = before424 && r.resultType() != null
+                ? "the integration runs Camel " + version + ", which ignores 
resultType (taken from Camel 4.24 on)"
+                : null;
+        if (ignored != null) {
+            addNote(result, ignored);
+        }
+        String raw;
+        try {
+            raw = ctx.executeAction("eval", root -> {
+                root.put("language", r.language());
+                root.put("predicate", String.valueOf(r.isPredicate()));
+                // as they are: the action file is JSON, and the integration 
does not unescape them
+                root.put("template", r.expression());
+                if (r.body() != null) {
+                    root.put("body", r.body());
+                }
+                if (r.headers() != null) {
+                    root.put("headers", keyValues(r.headers(), !before424));
+                }
+                if (r.variables() != null) {
+                    root.put("variables", keyValues(r.variables(), 
!before424));
+                }
+                if (r.resultType() != null) {
+                    root.put("resultType", r.resultType());
+                }
+            });
+        } catch (ToolExecutionException e) {
+            // the integration answers an expression that fails as a failed 
action, a tool error (CAMEL-25398): what
+            // to pass for an XPath function, that resultType was not used, 
and the rewrite of an elvis operator inside
+            // ${...} that an integration before Camel 4.24 refuses, go with 
its message
+            String hint = xpathHint(r.language(), r.expression(), 
e.getMessage());
+            String elvis = elvisNote(ctx, r.language(), r.expression());
+            if (hint == null && ignored == null && elvis == null) {
+                throw e;
             }
-        });
+            throw new ToolExecutionException(
+                    e.getMessage() + (hint != null ? "; " + hint : "") + 
(ignored != null ? "; " + ignored : "")
+                                             + (elvis != null ? "; " + elvis : 
""));
+        }
         JsonObject out = null;
         try {
             out = (JsonObject) Jsoner.deserialize(raw);
@@ -195,26 +374,41 @@ public final class ExpressionEvaluator {
         result.put("error", message != null ? Jsoner.unescape(message) : 
"evaluation failed");
     }
 
-    private static void evaluateLocally(
-            ToolContext ctx, String lang, String expression, String body, 
boolean predicate, JsonObject result) {
+    /**
+     * Headers or variables as the eval action of the CLI connector reads 
them: a key and a value for each. A JSON list
+     * or object goes as JSON text to an integration before Camel 4.24, which 
reads every value as text.
+     */
+    private static JsonArray keyValues(Map<String, Object> values, boolean 
typed) {
+        JsonArray list = new JsonArray();
+        values.forEach((k, v) -> {
+            JsonObject jo = new JsonObject();
+            jo.put("key", k);
+            jo.put("value", typed || !(v instanceof Map || v instanceof 
Collection) ? v : Jsoner.serialize(v));
+            list.add(jo);
+        });
+        return list;
+    }
+
+    private static void evaluateLocally(ToolContext ctx, Request r, JsonObject 
result) {
+        String lang = r.language();
         ClassLoader tccl = Thread.currentThread().getContextClassLoader();
         try {
             String gav = languageArtifact(ctx, lang);
             // the languages of this process (simple, constant, header, ...) 
need no class loader of their own
             ClassLoader known = gav != null ? DOWNLOADED.get(gav) : null;
-            if (evaluateWith(known, lang, expression, body, predicate, 
result)) {
+            if (evaluateWith(known, r, result)) {
                 if (known != null) {
                     // downloaded by an earlier call: say so again, so the 
answers of two calls read the same
                     result.put("downloaded", gav);
                 }
-                syntaxCheck(ctx, known, lang, expression, predicate, result);
+                syntaxCheck(ctx, known, lang, r.expression(), r.isPredicate(), 
result);
                 return;
             }
             // the language is not on this process's classpath: download its 
component as camel run does, so a
             // jsonpath, jq or xpath expression can be tried before it is 
written (CAMEL-24907)
             StringBuilder failure = new StringBuilder();
             ClassLoader downloaded = download(gav, failure);
-            if (downloaded == null || !evaluateWith(downloaded, lang, 
expression, body, predicate, result)) {
+            if (downloaded == null || !evaluateWith(downloaded, r, result)) {
                 result.put("status", "error");
                 result.put("error", "The language '" + lang + "' is not on the 
classpath of this process"
                                     + (gav != null ? " and " + gav + " could 
not be downloaded" : "")
@@ -224,7 +418,7 @@ public final class ExpressionEvaluator {
                 return;
             }
             result.put("downloaded", gav);
-            syntaxCheck(ctx, downloaded, lang, expression, predicate, result);
+            syntaxCheck(ctx, downloaded, lang, r.expression(), 
r.isPredicate(), result);
         } finally {
             Thread.currentThread().setContextClassLoader(tccl);
         }
@@ -234,8 +428,7 @@ public final class ExpressionEvaluator {
      * Evaluates with the given class loader (null for this process's own), 
and answers whether the language was found;
      * the result of the evaluation, or its error, is put in {@code result}.
      */
-    private static boolean evaluateWith(
-            ClassLoader loader, String lang, String expression, String body, 
boolean predicate, JsonObject result) {
+    private static boolean evaluateWith(ClassLoader loader, Request r, 
JsonObject result) {
         // without the CLI connector, which would make this process, such as 
the MCP server, a running integration
         try (DefaultCamelContext context = 
ScratchContextHelper.withoutCliConnector(new DefaultCamelContext(false))) {
             if (loader != null) {
@@ -246,24 +439,33 @@ public final class ExpressionEvaluator {
             context.start();
             Language language;
             try {
-                language = context.resolveLanguage(lang);
+                language = context.resolveLanguage(r.language());
             } catch (Exception e) {
                 return false;
             }
             Exchange exchange = new DefaultExchange(context);
-            exchange.getMessage().setBody(body != null ? body : "");
+            exchange.getMessage().setBody(r.body() != null ? r.body() : "");
+            if (r.headers() != null) {
+                r.headers().forEach(exchange.getMessage()::setHeader);
+            }
+            if (r.variables() != null) {
+                r.variables().forEach(exchange::setVariable);
+            }
             Object value;
-            if (predicate) {
-                Predicate p = language.createPredicate(expression);
+            if (r.isPredicate()) {
+                Predicate p = language.createPredicate(r.expression());
                 p.init(context);
                 value = p.matches(exchange);
             } else {
-                Expression e = language.createExpression(expression);
+                // the resultType of a route goes to the language as its first 
property, as the reifiers pass it
+                Expression e = r.resultType() != null
+                        ? language.createExpression(r.expression(), new 
Object[] { resultType(context, r.resultType()) })
+                        : language.createExpression(r.expression());
                 e.init(context);
                 value = e.evaluate(exchange, Object.class);
             }
             result.put("status", "ok");
-            result.put("result", value != null ? value.toString() : null);
+            result.put("result", text(exchange, value));
         } catch (Exception e) {
             result.put("status", "error");
             result.put("error", rootCause(e));
@@ -271,6 +473,29 @@ public final class ExpressionEvaluator {
         return true;
     }
 
+    /** The class of a resultType as a route resolves it, so java.lang.String 
works, and String as well. */
+    private static Class<?> resultType(CamelContext context, String name) {
+        Class<?> type = context.getClassResolver().resolveClass(name);
+        if (type == null) {
+            throw new IllegalArgumentException(
+                    "resultType " + name + " is not a class on the classpath; 
use a class name such as"
+                                               + " java.lang.String, 
java.lang.Integer or java.util.Map");
+        }
+        return type;
+    }
+
+    /**
+     * The value as text, the way the integration answers it: converted by the 
context, so an XPath node list shows its
+     * XML instead of the name of its class.
+     */
+    private static String text(Exchange exchange, Object value) {
+        if (value == null) {
+            return null;
+        }
+        String text = 
exchange.getContext().getTypeConverter().tryConvertTo(String.class, exchange, 
value);
+        return text != null ? text : value.toString();
+    }
+
     /**
      * The catalog's own check of the text, which names where the syntax 
breaks (its index), so an error says more than
      * the evaluation's exception; only added when it finds something the 
evaluation did not.
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ActionClient.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ActionClient.java
index 06744756dda6..090900d1f096 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ActionClient.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ActionClient.java
@@ -316,7 +316,10 @@ public final class ActionClient {
         return request;
     }
 
-    private static String versionOf(long pid) {
+    /**
+     * The Camel version of the integration with the given pid, from its 
status file, or <tt>null</tt> when unknown.
+     */
+    public static String versionOf(long pid) {
         try {
             Path f = CommandLineHelper.getCamelDir().resolve(pid + 
"-status.json");
             JsonObject status = Files.exists(f) ? 
Jsoner.deserialize(Files.readString(f), (JsonObject) null) : null;
@@ -332,7 +335,7 @@ public final class ActionClient {
     /**
      * Whether a Camel version such as <tt>4.24.0-SNAPSHOT</tt> is the given 
major.minor or later.
      */
-    static boolean isAtLeast(String version, int major, int minor) {
+    public static boolean isAtLeast(String version, int major, int minor) {
         Matcher m = VERSION.matcher(version);
         if (!m.find()) {
             // not a version we know how to read: assume a recent one
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
index e7a9d35dd7e0..382f6bfced8e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
@@ -245,19 +245,31 @@ public final class RuntimeHelper {
      * The headers as the send action of the CLI connector reads them: a key 
and a value for each header.
      */
     static JsonArray parseHeaders(String headers) {
+        return parseKeyValues(headers, "Header");
+    }
+
+    /**
+     * Lines of {@code key=value} as the actions of the CLI connector read 
them (the headers of the send action, the
+     * headers and variables of the eval action): a key and a value for each 
line.
+     *
+     * @param  text                     the lines
+     * @param  what                     what a line holds, for the error, such 
as Header
+     * @throws IllegalArgumentException when a line is not in the {@code 
key=value} form
+     */
+    public static JsonArray parseKeyValues(String text, String what) {
         JsonArray list = new JsonArray();
-        for (String line : headers.split("\\R")) {
-            String header = line.strip();
-            if (header.isEmpty()) {
+        for (String line : text.split("\\R")) {
+            String entry = line.strip();
+            if (entry.isEmpty()) {
                 continue;
             }
-            int pos = header.indexOf('=');
+            int pos = entry.indexOf('=');
             if (pos <= 0) {
-                throw new IllegalArgumentException("Header must be in 
key=value format, was: " + header);
+                throw new IllegalArgumentException(what + " must be in 
key=value format, was: " + entry);
             }
             JsonObject jo = new JsonObject();
-            jo.put("key", header.substring(0, pos).strip());
-            jo.put("value", header.substring(pos + 1));
+            jo.put("key", entry.substring(0, pos).strip());
+            jo.put("value", entry.substring(pos + 1));
             list.add(jo);
         }
         return list;
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/EvalInTheIntegrationTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/EvalInTheIntegrationTest.java
index 0f4597721b11..540413bf2529 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/EvalInTheIntegrationTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/EvalInTheIntegrationTest.java
@@ -18,6 +18,10 @@ package org.apache.camel.dsl.jbang.core.commands.ai;
 
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 
 import org.apache.camel.console.DevConsole;
@@ -32,6 +36,7 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 /**
  * The expression tools evaluate in the selected integration: it gets the 
expression and the body as they are, as the
@@ -137,6 +142,121 @@ class EvalInTheIntegrationTest {
         assertThat(json.getString("result")).isEqualTo("\"a/b\"");
     }
 
+    /** CAMEL-26400: the headers, variables, predicate and resultType of the 
call go to the eval action. */
+    @Test
+    void theHeadersVariablesPredicateAndResultTypeGoToTheAction() {
+        JsonObject result = ExpressionEvaluator.evaluate(ctx(), new 
ExpressionEvaluator.Request(
+                "simple",
+                "${header.foo}-${variable.bar}", null, Map.of("foo", "x"), 
Map.of("bar", "y"), null, null));
+
+        assertThat(result.getString("status")).isEqualTo("ok");
+        assertThat(result.getString("result")).isEqualTo("x-y");
+        JsonObject action = integration.lastAction();
+        Collection<JsonObject> headers = action.getCollection("headers");
+        assertThat(headers).containsExactly(keyValue("foo", "x"));
+        Collection<JsonObject> variables = action.getCollection("variables");
+        assertThat(variables).containsExactly(keyValue("bar", "y"));
+        assertThat(action.getString("predicate")).isEqualTo("false");
+        assertThat(action.get("resultType")).isNull();
+
+        JsonObject typed = ExpressionEvaluator.evaluate(ctx(), new 
ExpressionEvaluator.Request(
+                "simple", "${body}",
+                "0042", null, null, null, "java.lang.Integer"));
+        
assertThat(integration.lastAction().getString("resultType")).isEqualTo("java.lang.Integer");
+        assertThat(typed.getString("result")).isEqualTo("42");
+        assertThat(typed.get("note")).isNull();
+
+        // any language can be a predicate: the action says so, and the 
console answers true or false
+        JsonObject predicate = ExpressionEvaluator.evaluate(ctx(), new 
ExpressionEvaluator.Request(
+                "constant",
+                "true", null, null, null, true, null));
+        
assertThat(integration.lastAction().getString("predicate")).isEqualTo("true");
+        assertThat(predicate.getString("result")).isEqualTo("true");
+    }
+
+    /** A JSON list or object keeps its type, so a groovy 
headers.rows[0].total works in the integration too. */
+    @Test
+    void aJsonValueKeepsItsType() {
+        Map<String, Object> headers = 
ExpressionEvaluator.parseValues("headers", "{\"rows\": [{\"total\": 3}]}");
+        ExpressionEvaluator.evaluate(ctx(), new ExpressionEvaluator.Request(
+                "simple", "${header.rows}", null, headers,
+                null, null, null));
+
+        JsonObject header = (JsonObject) 
List.copyOf(integration.lastAction().getCollection("headers")).get(0);
+        assertThat(header.get("value")).isInstanceOf(List.class);
+    }
+
+    /**
+     * An integration before Camel 4.24 reads every value of a header as text, 
and has no resultType: the list goes as
+     * JSON text, and the answer says that resultType is not used there.
+     */
+    @Test
+    void anIntegrationBefore424GetsTextAndANoteOnTheResultType() throws 
Exception {
+        Files.writeString(CommandLineHelper.getCamelDir().resolve(PID + 
"-status.json"),
+                "{\"context\": {\"name\": \"orders\", \"version\": 
\"4.23.0\"}}");
+        Map<String, Object> headers = 
ExpressionEvaluator.parseValues("headers", "{\"rows\": [{\"total\": 3}]}");
+
+        JsonObject result = ExpressionEvaluator.evaluate(ctx(), new 
ExpressionEvaluator.Request(
+                "simple", "${body}",
+                "1", headers, null, null, "java.lang.Integer"));
+
+        assertThat(result.getString("note")).contains("the integration runs 
Camel 4.23.0, which ignores resultType");
+        JsonObject header = (JsonObject) 
List.copyOf(integration.lastAction().getCollection("headers")).get(0);
+        assertThat(header.get("value")).isEqualTo("[{\"total\":3}]");
+    }
+
+    /**
+     * The XPath of a route that fails in the integration gets the same hint 
as a local evaluation, with the error the
+     * tool answers (a failed eval action is a tool error).
+     */
+    @Test
+    void anXPathErrorOfTheIntegrationGetsTheHint() throws Exception {
+        try (FakeIntegration failing = new FakeIntegration(
+                PID + 1, root -> Map.of("status", "failed", "exception",
+                        Map.of("message", "Invalid xpath: boolean(/x). Reason: 
javax.xml.xpath.XPathExpressionException:"
+                                          + " 
com.sun.org.apache.xpath.internal.XPathException: Can not convert #BOOLEAN to a"
+                                          + " NodeList!")))) {
+            ToolContext ctx = new ToolContext();
+            ctx.selectProcess(PID + 1);
+
+            assertThatThrownBy(() -> ExpressionEvaluator.evaluate(ctx, 
"xpath", "boolean(/x)", "<x/>"))
+                    .isInstanceOf(ToolExecutionException.class)
+                    .hasMessageStartingWith("Invalid xpath: boolean(/x).")
+                    .hasMessageContaining("NodeList!; this XPath returns a 
boolean, not nodes: pass predicate=true");
+            assertThat(failing.actions()).hasSize(1);
+        }
+    }
+
+    /**
+     * An integration before Camel 4.24 refuses the elvis operator inside 
${...}: the tool error says how to write it
+     * there, as the answer of an evaluation that works does (CAMEL-26401).
+     */
+    @Test
+    void 
anElvisInsideThePlaceholderRefusedByAnOlderIntegrationGetsTheRewrite() throws 
Exception {
+        Files.writeString(CommandLineHelper.getCamelDir().resolve((PID + 1) + 
"-status.json"),
+                "{\"context\":{\"version\":\"4.22.1\"}}");
+        try (FakeIntegration failing = new FakeIntegration(
+                PID + 1, root -> Map.of("status", "failed", "exception",
+                        Map.of("message", "${ cannot accept ?: at location 
17")))) {
+            ToolContext ctx = new ToolContext();
+            ctx.selectProcess(PID + 1);
+
+            assertThatThrownBy(() -> ExpressionEvaluator.evaluate(ctx, 
"simple", "${header.missing ?: 10}", null))
+                    .isInstanceOf(ToolExecutionException.class)
+                    .hasMessageStartingWith("${ cannot accept ?: at location 
17; ")
+                    .hasMessageContaining("needs Camel 4.24 or later")
+                    .hasMessageContaining("(here Camel 4.22.1)")
+                    .hasMessageEndingWith("write it after the function: 
`${header.missing} ?: 10`");
+        }
+    }
+
+    private static JsonObject keyValue(String key, Object value) {
+        JsonObject jo = new JsonObject();
+        jo.put("key", key);
+        jo.put("value", value);
+        return jo;
+    }
+
     private static ToolContext ctx() {
         ToolContext ctx = new ToolContext();
         ctx.selectProcess(PID);
@@ -146,11 +266,25 @@ class EvalInTheIntegrationTest {
     // the eval action of the CLI connector: the values of the action as they 
are, to the eval-language console
     private Object eval(JsonObject root) {
         Object predicate = root.get("predicate");
-        return console.call(DevConsole.MediaType.JSON, Map.of(
-                "language", root.getStringOrDefault("language", "simple"),
-                "predicate", predicate != null && 
Boolean.parseBoolean(predicate.toString()),
-                "template", root.getStringOrDefault("template", ""),
-                "body", root.getStringOrDefault("body", ""),
-                "headers", Map.of(), "variables", Map.of()));
+        Map<String, Object> options = new HashMap<>();
+        options.put("language", root.getStringOrDefault("language", "simple"));
+        options.put("predicate", predicate != null && 
Boolean.parseBoolean(predicate.toString()));
+        options.put("template", root.getStringOrDefault("template", ""));
+        options.put("body", root.getStringOrDefault("body", ""));
+        options.put("headers", values(root, "headers"));
+        options.put("variables", values(root, "variables"));
+        if (root.getString("resultType") != null) {
+            options.put("resultType", root.getString("resultType"));
+        }
+        return console.call(DevConsole.MediaType.JSON, options);
+    }
+
+    private static Map<String, Object> values(JsonObject root, String name) {
+        Map<String, Object> values = new LinkedHashMap<>();
+        Collection<JsonObject> list = root.getCollection(name);
+        if (list != null) {
+            list.forEach(jo -> values.put(jo.getString("key"), 
jo.get("value")));
+        }
+        return values;
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluatorRequestTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluatorRequestTest.java
new file mode 100644
index 000000000000..7d73edda9cd0
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluatorRequestTest.java
@@ -0,0 +1,209 @@
+/*
+ * 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.dsl.jbang.core.commands.ai;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * CAMEL-26400: camel_eval_expression evaluates an expression the way the 
route will, with the headers and variables an
+ * earlier step sets, the resultType of the language and as a predicate in any 
language. Here in the local scratch
+ * context, with xpath, groovy and jq on the classpath of the tests.
+ */
+class ExpressionEvaluatorRequestTest {
+
+    private static final String XML = "<x><y>1</y><y>2</y></x>";
+
+    @TempDir
+    Path home;
+
+    private Path previousHome;
+
+    // a home of its own: the tool picks the only running integration of the 
machine, when there is one
+    @BeforeEach
+    void useOwnHome() {
+        previousHome = CommandLineHelper.getHomeDir();
+        CommandLineHelper.useHomeDir(home.toString());
+    }
+
+    @AfterEach
+    void restoreHome() {
+        CommandLineHelper.useHomeDir(previousHome.toString());
+    }
+
+    @Test
+    void anXPathBooleanIsEvaluatedAsAPredicate() {
+        String expression = "boolean(/*[local-name()='x'])";
+        JsonObject asNodes = eval("xpath", expression, XML, null, null, null, 
null);
+        assertThat(asNodes.getString("status")).isEqualTo("error");
+        assertThat(asNodes.getString("error")).contains("#BOOLEAN to a 
NodeList");
+        assertThat(asNodes.getString("hint")).startsWith("this XPath returns a 
boolean, not nodes: pass predicate=true")
+                .contains("xpath: {expression: 
\"boolean(/*[local-name()='x'])\", resultType: java.lang.Boolean}");
+
+        JsonObject predicate = eval("xpath", expression, XML, null, null, 
true, null);
+        assertThat(predicate.getString("status")).isEqualTo("ok");
+        assertThat(predicate.getString("result")).isEqualTo("true");
+        assertThat(predicate.getBoolean("predicate")).isTrue();
+        assertThat(predicate.get("hint")).isNull();
+    }
+
+    @Test
+    void anXPathStringFunctionTakesTheResultType() {
+        String expression = "local-name(/*[local-name()='x']/*[1])";
+        JsonObject asNodes = eval("xpath", expression, XML, null, null, null, 
null);
+        assertThat(asNodes.getString("status")).isEqualTo("error");
+        assertThat(asNodes.getString("hint")).startsWith("this XPath returns a 
string, not nodes: pass resultType"
+                                                         + " java.lang.String")
+                .contains("(in a route: xpath: {expression: \"" + expression + 
"\", resultType: java.lang.String})");
+
+        JsonObject typed = eval("xpath", expression, XML, null, null, null, 
"java.lang.String");
+        assertThat(typed.getString("status")).isEqualTo("ok");
+        assertThat(typed.getString("result")).isEqualTo("y");
+        
assertThat(typed.getString("resultType")).isEqualTo("java.lang.String");
+        // the short name works as in a route, where the class resolver knows 
it
+        assertThat(eval("xpath", expression, XML, null, null, null, 
"String").getString("result")).isEqualTo("y");
+    }
+
+    @Test
+    void anXPathNumberTakesANumberType() {
+        JsonObject asNodes = eval("xpath", "count(/x/y)", XML, null, null, 
null, null);
+        assertThat(asNodes.getString("hint")).startsWith("this XPath returns a 
number, not nodes: pass resultType"
+                                                         + " 
java.lang.Integer");
+
+        assertThat(eval("xpath", "count(/x/y)", XML, null, null, null, 
"java.lang.Integer").getString("result"))
+                .isEqualTo("2");
+    }
+
+    @Test
+    void nodesShowTheirXmlAsTheIntegrationAnswers() {
+        JsonObject nodes = eval("xpath", "/x/y[1]", XML, null, null, null, 
null);
+
+        assertThat(nodes.getString("status")).isEqualTo("ok");
+        // not com.sun.org.apache.xml.internal.dtm.ref.DTMNodeList@60b333fd
+        assertThat(nodes.getString("result")).isEqualTo("<y>1</y>");
+    }
+
+    @Test
+    void anUnknownResultTypeSaysWhatToPass() {
+        JsonObject result = eval("xpath", "string(/x/y)", XML, null, null, 
null, "Map");
+
+        assertThat(result.getString("status")).isEqualTo("error");
+        assertThat(result.getString("error")).contains("resultType Map is not 
a class on the classpath")
+                .contains("java.util.Map");
+    }
+
+    @Test
+    void aPredicateSaysThatItHasNoResultType() {
+        JsonObject result = eval("simple", "${body} == 'a'", "a", null, null, 
null, "java.lang.Integer");
+
+        assertThat(result.getString("result")).isEqualTo("true");
+        assertThat(result.get("resultType")).isNull();
+        assertThat(result.getString("note")).contains("resultType is not used 
for a predicate");
+    }
+
+    @Test
+    void groovyReadsTheRowsOfAHeader() {
+        Map<String, Object> headers = 
ExpressionEvaluator.parseValues("headers",
+                "{\"totalOrdersRows\": [{\"total_orders\": 3}]}");
+        JsonObject result = eval("groovy", "def total = 
headers.totalOrdersRows[0].total_orders as long; total + 1",
+                null, headers, null, null, null);
+
+        
assertThat(result.getString("status")).as(result.toJson()).isEqualTo("ok");
+        assertThat(result.getString("result")).isEqualTo("4");
+    }
+
+    @Test
+    void jqReadsAVariable() {
+        Map<String, Object> variables = 
ExpressionEvaluator.parseValues("variables",
+                Jsoner.serialize(Map.of("requestBodyJson", "{\"products\": 
[{\"sku\": \"A\"}, {\"sku\": \"B\"}]}")));
+        JsonObject result = eval("jq", "[ (variable(\"requestBodyJson\") | 
fromjson).products[] | .sku ]", "{}",
+                null, variables, null, null);
+
+        
assertThat(result.getString("status")).as(result.toJson()).isEqualTo("ok");
+        // as the integration answers it: the JSON of the node, which the type 
converter may indent
+        assertThat(result.getString("result").replace(" ", 
"")).isEqualTo("[\"A\",\"B\"]");
+    }
+
+    @Test
+    void headersAndVariablesAreKeyValueLines() {
+        Map<String, Object> headers = 
ExpressionEvaluator.parseValues("headers", "foo=bar\n qty=3\n");
+        Map<String, Object> variables = 
ExpressionEvaluator.parseValues("variables", "greeting=Hi");
+        JsonObject result = eval("simple", "${variable.greeting} ${header.foo} 
${header.qty}", null, headers, variables,
+                null, null);
+
+        assertThat(result.getString("result")).isEqualTo("Hi bar 3");
+        assertThat(ExpressionEvaluator.parseValues("headers", " ")).isNull();
+    }
+
+    @Test
+    void headersThatAreNeitherSayWhy() {
+        assertThatThrownBy(() -> ExpressionEvaluator.parseValues("headers", 
"foo"))
+                .isInstanceOf(ToolExecutionException.class)
+                .hasMessage("A line of headers must be in key=value format, 
was: foo (or pass a JSON object)");
+        assertThatThrownBy(() -> ExpressionEvaluator.parseValues("variables", 
"{\"a\": "))
+                
.isInstanceOf(ToolExecutionException.class).hasMessageStartingWith("variables 
is not a valid JSON object");
+    }
+
+    /** The tool takes the arguments as text, as the MCP servers, the TUI and 
camel ask pass them. */
+    @Test
+    void theToolTakesTheNewArguments() throws Exception {
+        Object answer = ToolRegistry.execute("camel_eval_expression", new 
ToolContext(), Map.of(
+                "language", "xpath", "expression", "boolean(/x/y)", "body", 
XML, "predicate", "true"));
+        JsonObject predicate = (JsonObject) 
Jsoner.deserialize(answer.toString());
+        assertThat(predicate.getString("result")).isEqualTo("true");
+
+        answer = ToolRegistry.execute("camel_eval_expression", new 
ToolContext(), Map.of(
+                "expression", "${header.foo}-${variable.bar}", "headers", 
"{\"foo\": \"x\"}", "variables", "bar=y"));
+        assertThat(((JsonObject) 
Jsoner.deserialize(answer.toString())).getString("result")).isEqualTo("x-y");
+
+        answer = ToolRegistry.execute("camel_eval_expression", new 
ToolContext(), Map.of(
+                "language", "xpath", "expression", "string(/x/y[2])", "body", 
XML, "resultType", "java.lang.String"));
+        assertThat(((JsonObject) 
Jsoner.deserialize(answer.toString())).getString("result")).isEqualTo("2");
+
+        // an explicit false keeps a simple predicate an expression
+        answer = ToolRegistry.execute("camel_eval_expression", new 
ToolContext(), Map.of(
+                "expression", "${body} == 'a'", "body", "a", "predicate", 
"false"));
+        assertThat(((JsonObject) 
Jsoner.deserialize(answer.toString())).get("predicate")).isNull();
+
+        
assertThat(ToolRegistry.findTool("camel_eval_expression").params()).extracting(ToolDescriptor.Param::name)
+                .containsAll(List.of("headers", "variables", "predicate", 
"resultType"));
+
+        // a predicate that is not true or false is refused, not taken for 
false
+        assertThatThrownBy(() -> ToolRegistry.execute("camel_eval_expression", 
new ToolContext(), Map.of(
+                "expression", "${body} == 'a'", "body", "a", "predicate", 
"yes")))
+                
.isInstanceOf(ToolExecutionException.class).hasMessage("predicate is true or 
false, not yes");
+    }
+
+    private static JsonObject eval(
+            String language, String expression, String body, Map<String, 
Object> headers, Map<String, Object> variables,
+            Boolean predicate, String resultType) {
+        return ExpressionEvaluator.evaluate(new ToolContext(),
+                new ExpressionEvaluator.Request(language, expression, body, 
headers, variables, predicate, resultType));
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
index 56392718b63a..d97bfc9affb1 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
@@ -44,6 +44,7 @@ import 
org.apache.camel.dsl.jbang.core.commands.ai.ToolExecutionException;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolRegistry;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
 import org.jboss.logging.Logger;
 
 /**
@@ -397,9 +398,52 @@ public class AuthoringTools {
             @ToolArg(description = "e.g. ${random(1,10)} or ${body} ?: 
'none'", required = true) String expression,
             @ToolArg(description = "simple (default), jsonpath, xpath, jq", 
required = false) String language,
             @ToolArg(description = "Message body", required = false) String 
body,
-            @ToolArg(description = NAME_DESC, required = false) String name) {
+            @ToolArg(description = NAME_DESC, required = false) String name,
+            // Object, so the schema takes any JSON: a client sends a JSON 
object, or the text of key=value lines
+            @ToolArg(description = 
org.apache.camel.dsl.jbang.core.commands.ai.AuthoringTools.EVAL_HEADERS_DESC,
+                     required = false) Object headers,
+            @ToolArg(description = 
org.apache.camel.dsl.jbang.core.commands.ai.AuthoringTools.EVAL_VARIABLES_DESC,
+                     required = false) Object variables,
+            @ToolArg(description = 
org.apache.camel.dsl.jbang.core.commands.ai.AuthoringTools.EVAL_PREDICATE_DESC,
+                     required = false) Boolean predicate,
+            @ToolArg(description = 
org.apache.camel.dsl.jbang.core.commands.ai.AuthoringTools.EVAL_RESULT_TYPE_DESC,
+                     required = false) String resultType) {
         return call("camel_eval_expression", args("expression", expression, 
"language", language, "body", body,
-                "name", name));
+                "name", name, "headers", limited("headers", 
jsonText(headers)), "variables",
+                limited("variables", jsonText(variables)), "predicate", 
predicate, "resultType", resultType));
+    }
+
+    /**
+     * The text of an argument held to the length limit of the string 
arguments: McpSecurityInterceptor checks the
+     * arguments that are strings, and a JSON object is not one.
+     */
+    String limited(String argument, String text) {
+        int max = securityConfig != null
+                ? securityConfig.getMaxArgumentLength() : 
McpSecurityConfig.DEFAULT_MAX_ARGUMENT_LENGTH;
+        if (text != null && text.length() > max) {
+            throw new ToolCallException(
+                    "Argument '" + argument + "' is " + text.length() + " 
characters long as JSON, over the limit of "
+                                        + max + "; nothing was done. Pass 
fewer or shorter values, or raise"
+                                        + " 
camel.mcp.security.max-argument-length");
+        }
+        return text;
+    }
+
+    /**
+     * An argument that is a JSON object or text, as the text the shared tool 
reads. The server hands a JSON object over
+     * as a Map of Maps, Lists, strings, numbers and booleans, which is the 
JSON of the object for Jsoner.
+     */
+    static String jsonText(Object value) {
+        if (value == null || value instanceof String) {
+            return (String) value;
+        }
+        try {
+            return Jsoner.serialize(value);
+        } catch (IllegalArgumentException e) {
+            // not a value Jsoner knows: its own text, which the shared tool 
reads or refuses with the reason
+            LOG.debug("jsonText: Jsoner cannot serialize {}, falling back to 
toString()", value.getClass().getSimpleName(), e);
+            return value.toString();
+        }
     }
 
     @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
index 8b4b05ddb024..31a2216ed4b2 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
@@ -135,7 +135,8 @@ class AuthoringToolsTest {
         Path previousHome = CommandLineHelper.getHomeDir();
         CommandLineHelper.useHomeDir(home.toString());
         try {
-            assertThatThrownBy(() -> tools.camel_eval_expression("${body}", 
null, "camel", "no-such-app-xyz-1"))
+            assertThatThrownBy(
+                    () -> tools.camel_eval_expression("${body}", null, 
"camel", "no-such-app-xyz-1", null, null, null, null))
                     
.isInstanceOf(ToolCallException.class).hasMessageContaining("no-such-app-xyz-1");
             assertThatThrownBy(() -> tools.camel_get_log("no-such-app-xyz-1", 
null, null, null, null, null))
                     
.isInstanceOf(ToolCallException.class).hasMessageContaining("no-such-app-xyz-1");
@@ -144,6 +145,54 @@ class AuthoringToolsTest {
         }
     }
 
+    /**
+     * CAMEL-26400: the headers, variables, predicate and resultType of 
camel_eval_expression reach the shared tool; a
+     * client sends headers as a JSON object (the schema takes any JSON) or as 
key=value lines.
+     */
+    @Test
+    void evalTakesHeadersVariablesPredicateAndResultType(@TempDir Path home) {
+        // a home of its own: without a name the tool picks the only running 
integration of the machine
+        Path previousHome = CommandLineHelper.getHomeDir();
+        CommandLineHelper.useHomeDir(home.toString());
+        try {
+            JsonObject values = 
tools.camel_eval_expression("${header.foo}-${variable.bar}", null, null, null,
+                    Map.of("foo", "x"), "bar=y", null, null);
+            assertThat(values.getString("status")).isEqualTo("ok");
+            assertThat(values.getString("result")).isEqualTo("x-y");
+
+            JsonObject typed = tools.camel_eval_expression("${body}", null, 
"0042", null, null, null, null,
+                    "java.lang.Integer");
+            assertThat(typed.getString("result")).isEqualTo("42");
+            
assertThat(typed.getString("resultType")).isEqualTo("java.lang.Integer");
+
+            JsonObject predicate = tools.camel_eval_expression("true", 
"constant", null, null, null, null, true, null);
+            assertThat(predicate.getBoolean("predicate")).isTrue();
+            assertThat(predicate.getString("result")).isEqualTo("true");
+
+            assertThatThrownBy(() -> 
tools.camel_eval_expression("${header.foo}", null, null, null, "foo", null, 
null,
+                    
null)).isInstanceOf(ToolCallException.class).hasMessageContaining("key=value");
+
+            // a JSON object is not a string argument, which the security 
interceptor checks: its JSON is held to the
+            // same length limit
+            Map<String, Object> large = Map.of("foo", 
"x".repeat(McpSecurityConfig.DEFAULT_MAX_ARGUMENT_LENGTH));
+            assertThatThrownBy(() -> 
tools.camel_eval_expression("${header.foo}", null, null, null, large, null, 
null,
+                    null)).isInstanceOf(ToolCallException.class)
+                    .hasMessageStartingWith("Argument 'headers' is ")
+                    .hasMessageContaining(" characters long as JSON, over the 
limit of "
+                                          + 
McpSecurityConfig.DEFAULT_MAX_ARGUMENT_LENGTH + "; nothing was done");
+        } finally {
+            CommandLineHelper.useHomeDir(previousHome.toString());
+        }
+    }
+
+    @Test
+    void aJsonObjectArgumentIsPassedOnAsItsJson() {
+        assertThat(AuthoringTools.jsonText(null)).isNull();
+        assertThat(AuthoringTools.jsonText("a=b")).isEqualTo("a=b");
+        assertThat(AuthoringTools.jsonText(Map.of("rows", 
List.of(Map.of("total", 3)))))
+                .isEqualTo("{\"rows\":[{\"total\":3}]}");
+    }
+
     @Test
     void everyArgumentIsRequiredExactlyWhenTheSharedDescriptorSaysSo() {
         // the Quarkus server rejects a call that omits an argument declared 
required, so an optional argument with a
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelPromptBudgetTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelPromptBudgetTest.java
index 011c540a65b2..16403c755bae 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelPromptBudgetTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelPromptBudgetTest.java
@@ -52,7 +52,10 @@ class AiPanelPromptBudgetTest {
     // the camel_component_properties tool and the 38k-character answers a 
client spilled to a file
     // raised from 5100 when camel_get_errors said that it answers for an 
integration that is not running (CAMEL-25784),
     // measured ~5119: without it an agent does not ask how the previous run 
ended
-    static final int CORE_BUDGET_TOKENS = 5_200;
+    // raised from 5200 for the headers, variables, predicate and resultType 
arguments of camel_eval_expression
+    // (CAMEL-26400), measured ~5233 (~114 for the four): without them an 
XPath function or a header lookup cannot be
+    // checked the way the route runs it
+    static final int CORE_BUDGET_TOKENS = 5_300;
     /** Measured ~6.9k tokens for 47 tools. */
     // raised from 7500 with tui_write_file and tui_validate_source
     // and from 7900 with the shared camel_* set (camel_catalog_find, 
camel_run and camel_error_diagnose added)
@@ -72,7 +75,8 @@ class AiPanelPromptBudgetTest {
     // raised from 10500 with the core budget for camel_catalog_doc 
(CAMEL-25541), measured ~10485
     // raised from 10600 for tui_diagram_zoom, measured ~10731 (~168 for the 
tool): a model asked to zoom or fold the
     // diagram does it as the keys do; full mode only, its schema cut to the 
bone
-    static final int FULL_BUDGET_TOKENS = 10_750;
+    // raised from 10750 with the core budget for camel_eval_expression 
(CAMEL-26400), measured ~10844
+    static final int FULL_BUDGET_TOKENS = 10_900;
     /** Measured ~5.5k tokens for 28 tools: the core set (~4.7k) plus every 
tool group (CAMEL-24834). */
     // the SQL group adds tui_execute_sql and tui_update_row (~385 tokens), 
each group one guidance line in the prompt
     // (~130 for all three); an integration rarely has all of them, and the 
groups only load for the integration that
@@ -81,7 +85,8 @@ class AiPanelPromptBudgetTest {
     // (~245 tokens) and its guidance line (~35), for the local model that 
otherwise cannot see or call the HTTP API
     // it is building
     // raised from 5750 with the core budget for camel_catalog_doc 
(CAMEL-25541), measured ~5820
-    static final int CORE_WITH_GROUPS_BUDGET_TOKENS = 5_900;
+    // raised from 5900 with the core budget for camel_eval_expression 
(CAMEL-26400), measured ~6011
+    static final int CORE_WITH_GROUPS_BUDGET_TOKENS = 6_050;
 
     record Prefix(String mode, int tools, long promptChars, long toolChars) {
 
@@ -186,8 +191,9 @@ class AiPanelPromptBudgetTest {
             Prefix prefix = measure(mode + "+semantic", panel);
             System.out.println("AI panel semantic prefix: " + prefix);
             // core raised from 6100 with the core budget for 
camel_catalog_doc (CAMEL-25541), measured ~6170; full
-            // raised from 11000 for tui_diagram_zoom, measured ~11068
-            assertTrue(prefix.totalTokens() <= 
(AiPanel.TOOL_MODE_CORE.equals(mode) ? 6250 : 11100));
+            // raised from 11000 for tui_diagram_zoom, measured ~11068; both 
raised by 150 with the core budget for
+            // camel_eval_expression (CAMEL-26400), measured ~6361 and ~11182
+            assertTrue(prefix.totalTokens() <= 
(AiPanel.TOOL_MODE_CORE.equals(mode) ? 6400 : 11250));
         }
     }
 

Reply via email to