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

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


The following commit(s) were added to refs/heads/main by this push:
     new a60992b3efaa chore: camel-jbang - About has the operations of an 
OpenAPI contract the REST DSL serves, an AI tool icon, and tui_send_message 
returns the answer of a direct route (#29596)
a60992b3efaa is described below

commit a60992b3efaa592615dd57e30e304c8115cf9c37
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Nov 11 08:02:27 2026 +0100

    chore: camel-jbang - About has the operations of an OpenAPI contract the 
REST DSL serves, an AI tool icon, and tui_send_message returns the answer of a 
direct route (#29596)
    
    * chore: camel-jbang - About has the operations of an OpenAPI contract the 
REST DSL serves, an AI tool icon, and tui_send_message returns the answer of a 
direct route
    * chore: camel-jbang - In of About has the icon of the kind of system a 
consumer the icons do not know is, as Out has it
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../ROOT/pages/camel-jbang-tui-observe.adoc        |   2 +-
 .../jbang/core/commands/ai/JavaRouteReader.java    |   5 +
 .../jbang/core/commands/ai/ProjectOverview.java    |   4 +-
 .../dsl/jbang/core/commands/ai/ProjectRoutes.java  |   9 ++
 .../dsl/jbang/core/commands/ai/RestContracts.java  | 151 +++++++++++++++++++++
 .../jbang/core/commands/ai/RouteComponents.java    |   4 +
 .../camel/dsl/jbang/core/common/RuntimeHelper.java |  16 +++
 .../jbang/core/commands/ai/RestContractsTest.java  | 129 ++++++++++++++++++
 .../dsl/jbang/core/common/RuntimeHelperTest.java   |  24 ++++
 .../dsl/jbang/core/commands/tui/AboutView.java     |  34 ++++-
 .../dsl/jbang/core/commands/tui/McpFacade.java     |  11 +-
 .../core/commands/tui/TuiToolDefinitions.java      |   3 +-
 .../jbang/core/commands/tui/TuiToolRegistry.java   |   3 +-
 .../src/main/resources/tui/help/overview.md        |   3 +-
 .../dsl/jbang/core/commands/tui/AboutViewTest.java |  55 ++++++++
 15 files changed, 443 insertions(+), 10 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
index 4f1c663294c4..b4f00f5c1ec4 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
@@ -29,7 +29,7 @@ route file (a YAML `#` comment, a Javadoc or `//` comment in 
Java, an XML commen
 README, else the descriptions of the routes, else the overview an AI wrote in 
`camel-summary.md`. *r* opens the README,
 and *s* the summary of the project.
 * *In*, *Routes* and *Out* say what starts it (a timer or a cron with the 
period or schedule of its properties, its
-profile applied), its routes (a route called with `direct` sits under its 
caller with `โ†ณ`,
+profile applied; the operations of an OpenAPI contract the REST DSL serves, 
from the contract), its routes (a route called with `direct` sits under its 
caller with `โ†ณ`,
 one handed off with `seda` or a `wireTap` with `โ‡ข`) and what it talks to, with 
the exchanges since it started and a sparkline of the
 last 5 minutes. *Up* and *Down* select a line, and *Enter* opens a route 
drilled down on the Diagram, or an endpoint with
 its details on the Endpoint tab.
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JavaRouteReader.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JavaRouteReader.java
index 6e0fac5ef8a6..9cbe963ebed2 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JavaRouteReader.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JavaRouteReader.java
@@ -310,6 +310,11 @@ final class JavaRouteReader {
     }
 
     private void rest(RestDefinition rest) {
+        if (rest.getOpenApi() != null && rest.getOpenApi().getSpecification() 
!= null) {
+            // contract first: the operations are in the contract, which the 
overview reads
+            
routes.add(RestContracts.contract(rest.getOpenApi().getSpecification(), 
rest.getPath(), file,
+                    Math.max(1, rest.getLineNumber()), "java"));
+        }
         for (VerbDefinition v : rest.getVerbs()) {
             List<Endpoint> produces = new ArrayList<>();
             if (v.getTo() != null) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverview.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverview.java
index a75c786b7553..61286b446972 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverview.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverview.java
@@ -178,6 +178,8 @@ public final class ProjectOverview {
         for (Map.Entry<String, String> e : sources.entrySet()) {
             routes.addAll(ProjectRoutes.parse(e.getKey(), e.getValue(), 
catalog, constants));
         }
+        // the operations of a contract the REST DSL serves, from the contract
+        routes = new ArrayList<>(RestContracts.expand(dir, routes, catalog));
         if (routes.size() > MAX_ROUTES) {
             routes = new ArrayList<>(routes.subList(0, MAX_ROUTES));
         }
@@ -198,7 +200,7 @@ public final class ProjectOverview {
      * (SaaS, chat, documents), http, cloud, schedule, internal (plumbing 
inside Camel such as direct, seda, log, bean)
      * or other.
      */
-    static String category(String scheme, CamelCatalog catalog) {
+    public static String category(String scheme, CamelCatalog catalog) {
         String s = scheme.toLowerCase(Locale.ROOT);
         if ("direct".equals(s) || IN_MEMORY.contains(s) || "kamelet".equals(s) 
|| s.startsWith("dynamic")) {
             return "internal";
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectRoutes.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectRoutes.java
index 16575063aa77..48fd5aa64754 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectRoutes.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectRoutes.java
@@ -697,6 +697,10 @@ public final class ProjectRoutes {
 
         private void rest(MappingNode m, int line) {
             String base = str(value(m, "path"));
+            if (child(m, "openApi") instanceof MappingNode oa && str(value(oa, 
"specification")) != null) {
+                // contract first: the operations are in the contract, which 
the overview reads
+                routes.add(RestContracts.contract(str(value(oa, 
"specification")), base, file, line, "yaml"));
+            }
             for (NodeTuple t : m.getValue()) {
                 String verb = scalar(t.getKeyNode());
                 if (verb == null || !REST_VERBS.contains(verb) || 
!(t.getValueNode() instanceof SequenceNode ops)) {
@@ -1129,6 +1133,11 @@ public final class ProjectRoutes {
 
         private void rest(Element rest) {
             String base = attr(rest, "path");
+            Element openApi = firstChild(rest, "openApi");
+            if (openApi != null && attr(openApi, "specification") != null) {
+                routes.add(RestContracts.contract(attr(openApi, 
"specification"), base, file, lineOf("rest", null),
+                        "xml"));
+            }
             NodeList children = rest.getChildNodes();
             for (int i = 0; i < children.getLength(); i++) {
                 if (!(children.item(i) instanceof Element op) || 
!REST_VERBS.contains(localName(op))) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RestContracts.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RestContracts.java
new file mode 100644
index 000000000000..830962454978
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RestContracts.java
@@ -0,0 +1,151 @@
+/*
+ * 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.io.IOException;
+import java.io.Reader;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.dsl.jbang.core.commands.ai.ProjectRoutes.Endpoint;
+import org.apache.camel.dsl.jbang.core.commands.ai.ProjectRoutes.Route;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
+
+/**
+ * The operations of a REST DSL that serves an OpenAPI contract (contract 
first: {@code rest: openApi: specification:
+ * stock-api.json}). The routes do not name them: each operation is served by 
the route {@code direct:<operationId>},
+ * and the paths and methods are in the contract. The readers of the sources 
record the contract as one route of kind
+ * {@link #KIND}; the overview, which knows the project directory, reads the 
contract and puts one REST operation per
+ * operation of it in its place, as the REST DSL written out would have.
+ */
+public final class RestContracts {
+
+    /** The kind of the route a reader records for a REST DSL that serves a 
contract. */
+    static final String KIND = "openApiContract";
+
+    private static final Set<String> METHODS = Set.of("get", "post", "put", 
"patch", "delete", "head", "options");
+
+    private RestContracts() {
+    }
+
+    /**
+     * The route a reader records for a contract: its specification as the 
description, the path of the REST DSL as its
+     * path.
+     */
+    static Route contract(String specification, String basePath, String file, 
int line, String format) {
+        return new Route(
+                null, KIND, specification, null, file, line, format, false, 
null, List.of(), List.of(), 1, -1, 0,
+                null, basePath, false, null);
+    }
+
+    /** The routes with each contract in the place of its operations; a 
contract that cannot be read has none. */
+    static List<Route> expand(Path dir, List<Route> routes, CamelCatalog 
catalog) {
+        if (routes.stream().noneMatch(r -> KIND.equals(r.kind()))) {
+            return routes;
+        }
+        List<Route> answer = new ArrayList<>();
+        for (Route r : routes) {
+            if (KIND.equals(r.kind())) {
+                answer.addAll(operations(dir, r, catalog));
+            } else {
+                answer.add(r);
+            }
+        }
+        return answer;
+    }
+
+    private static List<Route> operations(Path dir, Route contract, 
CamelCatalog catalog) {
+        Map<?, ?> spec = read(dir, contract.file(), contract.description());
+        if (spec == null || !(spec.get("paths") instanceof Map<?, ?> paths)) {
+            return List.of();
+        }
+        // the base path of the contract (servers: url: 
http://localhost:8080/api), under the path of the REST DSL
+        String base = ProjectRoutes.joinPath(contract.restPath(), 
serverPath(spec));
+        List<Route> answer = new ArrayList<>();
+        for (Map.Entry<?, ?> p : paths.entrySet()) {
+            if (!(p.getValue() instanceof Map<?, ?> ops)) {
+                continue;
+            }
+            for (Map.Entry<?, ?> o : ops.entrySet()) {
+                String method = 
String.valueOf(o.getKey()).toLowerCase(Locale.ROOT);
+                if (!METHODS.contains(method) || !(o.getValue() instanceof 
Map<?, ?> op)
+                        || !(op.get("operationId") instanceof String 
operationId)) {
+                    continue;
+                }
+                List<Endpoint> produces = new ArrayList<>();
+                produces.add(ProjectRoutes.endpoint("direct:" + operationId, 
null, false, catalog));
+                Object summary = op.get("summary");
+                answer.add(new Route(
+                        null, "rest", summary instanceof String s ? s : null, 
null, contract.file(), contract.line(),
+                        contract.format(), false, null, List.of(), produces, 
1, -1, 0, method.toUpperCase(Locale.ROOT),
+                        ProjectRoutes.joinPath(base, 
String.valueOf(p.getKey())), false, null));
+            }
+        }
+        return answer;
+    }
+
+    /** The path of the first server of the contract (/api of 
http://localhost:8080/api), or none. */
+    static String serverPath(Map<?, ?> spec) {
+        if (spec.get("servers") instanceof List<?> servers && 
!servers.isEmpty()
+                && servers.get(0) instanceof Map<?, ?> server && 
server.get("url") instanceof String url) {
+            try {
+                String path = URI.create(url).getPath();
+                return path == null || "/".equals(path) ? null : path;
+            } catch (IllegalArgumentException e) {
+                return null;
+            }
+        }
+        // Swagger 2
+        return spec.get("basePath") instanceof String b ? b : null;
+    }
+
+    /** The contract, JSON or YAML, by its path in the project or next to the 
route file; null when not found. */
+    private static Map<?, ?> read(Path dir, String routeFile, String 
specification) {
+        if (dir == null || specification == null) {
+            return null;
+        }
+        String name = specification.replaceFirst("^(classpath|file):", "");
+        List<Path> candidates = new ArrayList<>();
+        candidates.add(dir.resolve(name));
+        Path routeDir = routeFile != null ? dir.resolve(routeFile).getParent() 
: null;
+        if (routeDir != null) {
+            candidates.add(routeDir.resolve(name));
+        }
+        for (Path f : candidates) {
+            if (f.normalize().startsWith(dir.normalize()) && 
Files.isRegularFile(f)) {
+                try (Reader r = Files.newBufferedReader(f, 
StandardCharsets.UTF_8)) {
+                    // JSON is YAML too
+                    Object doc = new Yaml(new SafeConstructor(new 
LoaderOptions())).load(r);
+                    return doc instanceof Map<?, ?> m ? m : null;
+                } catch (IOException | RuntimeException e) {
+                    return null;
+                }
+            }
+        }
+        return null;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteComponents.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteComponents.java
index 1be4b9b3c4ab..829081bf71c6 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteComponents.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteComponents.java
@@ -64,6 +64,10 @@ public final class RouteComponents {
             if ("rest".equals(r.kind())) {
                 // the rest DSL runs on the rest component
                 add(answer, "rest", catalog);
+            } else if (RestContracts.KIND.equals(r.kind())) {
+                // and serves a contract with rest-openapi
+                add(answer, "rest", catalog);
+                add(answer, "rest-openapi", catalog);
             }
             add(answer, r.from(), catalog);
             r.consumes().forEach(e -> add(answer, e, catalog));
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 97056c9d448e..f6411741f8bb 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
@@ -228,6 +228,14 @@ public final class RuntimeHelper {
         return sendMessage(pid, endpoint, body, headers, 0, null);
     }
 
+    /**
+     * Sends a message, and with reply, waits for the answer of the route 
(InOut) and has it under message in the
+     * result. A request-reply over a broker (jms:, kafka:) waits for a reply 
that may not come: a reply suits direct:.
+     */
+    public static JsonObject sendMessage(long pid, String endpoint, String 
body, String headers, boolean reply) {
+        return sendMessage(pid, endpoint, body, headers, 0, null, reply);
+    }
+
     /**
      * Sends a message, or with a repeat or a body of {@code file:<folder>} 
several messages, with a pause between them;
      * the answer is then a summary (sent, failed and the errors) rather than 
the reply.
@@ -239,10 +247,18 @@ public final class RuntimeHelper {
      */
     public static JsonObject sendMessage(
             long pid, String endpoint, String body, String headers, int 
repeat, String interval) {
+        return sendMessage(pid, endpoint, body, headers, repeat, interval, 
false);
+    }
+
+    private static JsonObject sendMessage(
+            long pid, String endpoint, String body, String headers, int 
repeat, String interval, boolean reply) {
         JsonArray headerList = headers != null ? parseHeaders(headers) : null;
         long[] pause = RepeatedSend.interval(interval);
         String result = executeAction(pid, "send", root -> {
             root.put("endpoint", endpoint);
+            if (reply) {
+                root.put("exchangePattern", "InOut");
+            }
             if (body != null) {
                 root.put("body", body);
             }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RestContractsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RestContractsTest.java
new file mode 100644
index 000000000000..ac5a8af3b639
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RestContractsTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.Files;
+import java.nio.file.Path;
+import java.util.Map;
+
+import org.apache.camel.catalog.DefaultCamelCatalog;
+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.tuple;
+
+/**
+ * contracts/openapi-server: the REST DSL serves the contract stock-api.json, 
and each operation is the route
+ * direct:operationId. The routes name no operation; the overview reads them 
from the contract.
+ */
+class RestContractsTest {
+
+    private static final DefaultCamelCatalog CATALOG = new 
DefaultCamelCatalog();
+
+    private static final String CONTRACT = """
+            {
+              "openapi": "3.0.3",
+              "servers": [ { "url": "http://localhost:8080/api"; } ],
+              "paths": {
+                "/stock": { "get": { "operationId": "listStock", "summary": 
"All stock levels" } },
+                "/stock/{sku}": { "get": { "operationId": "getStock", 
"summary": "Stock level of one SKU" } },
+                "/stock/{sku}/reserve": { "post": { "operationId": 
"reserveStock" } }
+              }
+            }
+            """;
+
+    private static final String OPERATIONS = """
+            - route:
+                id: listStock
+                from:
+                  uri: direct:listStock
+                  steps:
+                    - log: listed
+            - route:
+                id: getStock
+                from:
+                  uri: direct:getStock
+                  steps:
+                    - log: one
+            - route:
+                id: reserveStock
+                from:
+                  uri: direct:reserveStock
+                  steps:
+                    - log: reserved
+            """;
+
+    @TempDir
+    Path dir;
+
+    @Test
+    void theOperationsOfTheContractAreTheEntryPointsInEveryDsl() throws 
Exception {
+        Files.writeString(dir.resolve("stock-api.json"), CONTRACT);
+        Map<String, String> rests = Map.of(
+                "rest.camel.yaml", """
+                        - rest:
+                            openApi:
+                              specification: stock-api.json
+                        """,
+                "rest.xml", """
+                        <routes xmlns="http://camel.apache.org/schema/xml-io";>
+                          <rest><openApi 
specification="stock-api.json"/></rest>
+                        </routes>
+                        """,
+                "Rest.java", """
+                        public class Rest extends RouteBuilder {
+                            public void configure() {
+                                
rest().openApi().specification("stock-api.json");
+                            }
+                        }
+                        """);
+        for (Map.Entry<String, String> rest : rests.entrySet()) {
+            ProjectOverview.Overview o = ProjectOverview.analyze(dir,
+                    Map.of(rest.getKey(), rest.getValue(), 
"operations.camel.yaml", OPERATIONS), CATALOG);
+
+            assertThat(o.entryPoints()).as(rest.getKey())
+                    .extracting(ProjectOverview.EntryPoint::label, 
ProjectOverview.EntryPoint::route)
+                    .contains(tuple("GET /api/stock", "listStock"), tuple("GET 
/api/stock/{sku}", "getStock"),
+                            tuple("POST /api/stock/{sku}/reserve", 
"reserveStock"));
+            // About has them as what comes in from outside
+            assertThat(About.of(dir, 
o).in()).as(rest.getKey()).extracting(About.In::route)
+                    .containsExactlyInAnyOrder("listStock", "getStock", 
"reserveStock");
+        }
+    }
+
+    @Test
+    void aContractThatIsNotThereHasNoOperations() {
+        ProjectOverview.Overview o = ProjectOverview.analyze(dir, 
Map.of("rest.camel.yaml", """
+                - rest:
+                    openApi:
+                      specification: missing.json
+                """, "operations.camel.yaml", OPERATIONS), CATALOG);
+
+        assertThat(o.routes()).noneMatch(r -> 
RestContracts.KIND.equals(r.kind()));
+        assertThat(o.entryPoints()).isEmpty();
+    }
+
+    @Test
+    void theRestComponentsOfAContract() {
+        assertThat(RouteComponents.of("""
+                - rest:
+                    openApi:
+                      specification: stock-api.json
+                """, "yaml", CATALOG)).contains("rest", "rest-openapi");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelperTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelperTest.java
index 02c9481b5b1f..c6605a0696a6 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelperTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelperTest.java
@@ -114,6 +114,30 @@ class RuntimeHelperTest {
         assertNoRequestFiles();
     }
 
+    @Test
+    void aSendWithReplyAsksForTheAnswerOfTheRoute() throws Exception {
+        // showcase/tui-hello-world: direct:greet answers Hello and the name
+        var result = executor.submit(() -> RuntimeHelper.sendMessage(123, 
"direct:greet", "Claus", null, true));
+        Path action = requestFile();
+        JsonObject request = (JsonObject) 
Jsoner.deserialize(Files.readString(action));
+        assertEquals("send", request.get("action"));
+        assertEquals("InOut", request.get("exchangePattern"));
+        Files.writeString(outputFile(action), 
"{\"status\":\"success\",\"message\":{\"body\":{\"value\":\"Hello Claus!\"}}}");
+        JsonObject message = (JsonObject) result.get(5, 
TimeUnit.SECONDS).get("message");
+        assertEquals("Hello Claus!", ((JsonObject) 
message.get("body")).get("value"));
+    }
+
+    @Test
+    void aSendWithoutReplyDoesNotWaitForAnAnswer() throws Exception {
+        var result = executor.submit(() -> RuntimeHelper.sendMessage(123, 
"kafka:orders", "x", null));
+        Path action = requestFile();
+        JsonObject request = (JsonObject) 
Jsoner.deserialize(Files.readString(action));
+        // over a broker InOut is request-reply, waiting for a reply that may 
not come
+        assertNull(request.get("exchangePattern"));
+        Files.writeString(outputFile(action), "{\"status\":\"success\"}");
+        assertEquals("success", result.get(5, 
TimeUnit.SECONDS).getString("status"));
+    }
+
     @Test
     void jsonApiPropagatesPublicationFailureWhileTextApiKeepsItsDiagnostic() {
         CommandLineHelper.useHomeDir(home.resolve("missing").toString());
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
index fc122d35962e..05bb6aba74dd 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
@@ -1012,7 +1012,7 @@ final class AboutView {
             }
             Long total = e.route() != null ? totals.get(e.route()) : null;
             // with the values of its properties: the period or the schedule 
it runs with, its profile applied
-            in.add(Line.from(Span.raw(" " + icon(e.kind()) + " "),
+            in.add(Line.from(Span.raw(" " + inIcon(e.kind()) + " "),
                     Span.styled(Placeholders.resolve(e.label(), info), 
Style.EMPTY.bold()),
                     Span.styled(total != null ? "  " + total : "", 
Theme.success())));
             inTargets.add(e.route() != null ? new Target(e.route(), null) : 
null);
@@ -1146,7 +1146,19 @@ final class AboutView {
         return AboutReport.baseUri(uri);
     }
 
-    private static String icon(String kind) {
+    /**
+     * The icon of a consumer of In, by its scheme: one the icons do not know 
(aws2-sqs) has the icon of its kind of
+     * system in the catalog (messaging), as Out has it.
+     */
+    static String inIcon(String scheme) {
+        String icon = icon(scheme);
+        if ("โ€ข".equals(icon) && scheme != null && !scheme.isBlank()) {
+            icon = icon(ProjectOverview.category(scheme, 
ArchitectureView.catalog()));
+        }
+        return icon;
+    }
+
+    static String icon(String kind) {
         String k = kind != null ? kind.toLowerCase(Locale.ROOT) : "";
         if (k.contains("file") || k.contains("storage") || k.equals("ftp") || 
k.equals("sftp") || k.equals("s3")) {
             return "๐Ÿ“";
@@ -1154,6 +1166,10 @@ final class AboutView {
         if (k.contains("kafka") || k.contains("jms") || k.contains("amqp") || 
k.contains("mqtt") || k.equals("seda")) {
             return "๐Ÿ“จ";
         }
+        if (k.equals("ai") || k.startsWith("ai-") || 
k.startsWith("langchain4j") || k.equals("openai")) {
+            // a route an AI agent calls as a tool (ai-tool), or a model it 
calls
+            return TuiIcons.MCP;
+        }
         return switch (k) {
             case "schedule" -> "โฑ";
             case "http" -> "๐ŸŒ";
@@ -1257,6 +1273,15 @@ final class AboutView {
         return dot >= 0 ? type.substring(dot + 1) : type;
     }
 
+    /**
+     * Whether the route is the one Camel adds to serve an OpenAPI contract 
(from rest-openapi:, a route the sources do
+     * not have), which hands each request to the route direct:operationId.
+     */
+    private static boolean servesAContract(IntegrationInfo info, String 
routeId) {
+        return info.routes.stream().anyMatch(r -> routeId.equals(r.routeId) && 
r.from != null
+                && r.from.startsWith("rest-openapi:"));
+    }
+
     /** How each route that another one sends to is reached: call (direct) or 
a hand-off (seda); none for the others. */
     private Map<String, String> howCalled() {
         Map<String, String> how = new LinkedHashMap<>();
@@ -1311,8 +1336,9 @@ final class AboutView {
         Map<String, String> how = howCalled();
         for (Timeline.RouteSeries r : series) {
             String called = how.get(r.route());
-            if ("call".equals(called)) {
-                // a route called with direct is part of the exchange of its 
caller, which gets its failure too
+            if ("call".equals(called) || servesAContract(info, r.route())) {
+                // a route called with direct is part of the exchange of its 
caller, which gets its failure too; the
+                // route that serves a contract hands each request on to the 
route of its operation, which counts it
                 continue;
             }
             if (called == null) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index de0649a95274..486c1750122e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -1636,6 +1636,14 @@ class McpFacade {
     // ---- Integration messaging / control ----
 
     JsonObject sendMessage(String endpoint, String body, String headers) {
+        return sendMessage(endpoint, body, headers, null);
+    }
+
+    /**
+     * @param reply whether to wait for the answer of the route and give it 
back; null for the default: the answer of a
+     *              direct: route, which the send waits for anyway, but no 
request-reply over a broker
+     */
+    JsonObject sendMessage(String endpoint, String body, String headers, 
Boolean reply) {
         if (ctx.selectedPid == null) {
             return null;
         }
@@ -1650,7 +1658,8 @@ class McpFacade {
         String fileOutside = ToolConfirms.fileOutsideProject(
                 body, info != null ? info.directory : null, 
getSelectedSourceDirectory());
         JsonObject rejected = confirm(ToolConfirms.sendMessage(endpoint, 
selectedEndpointUris(), fileOutside));
-        return rejected != null ? rejected : RuntimeHelper.sendMessage(pid, 
endpoint, body, headers);
+        boolean inOut = reply != null ? reply : endpoint.startsWith("direct:");
+        return rejected != null ? rejected : RuntimeHelper.sendMessage(pid, 
endpoint, body, headers, inOut);
     }
 
     JsonObject executeSql(String sql, String datasource, int maxRows, int 
queryTimeout) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
index 129b830242c2..965dc9246727 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
@@ -397,7 +397,8 @@ final class TuiToolDefinitions {
                 Map.of("endpoint", propDef("string",
                         "Endpoint URI, e.g. 'direct:myRoute', 
'paho-mqtt5:temperature?brokerUrl=tcp://localhost:1883'"),
                         "body", propDef("string", "Message body to send"),
-                        "headers", propDef("string", "Message headers as 
key=value pairs separated by newlines")),
+                        "headers", propDef("string", "Message headers as 
key=value pairs separated by newlines"),
+                        "reply", propDef("boolean", "Return the route's 
answer; default true for direct:")),
                 List.of("endpoint")), r::callSendMessage, CORE));
         // CAMEL-25307: the http tool group; the shared get_http_endpoints and 
http_request for the selected integration
         tools.add(tool(toolDef(
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
index c71ff3e96107..2761f41ef244 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
@@ -1287,7 +1287,8 @@ class TuiToolRegistry {
         }
         String body = args.get("body") instanceof String s ? s : null;
         String headers = args.get("headers") instanceof String s ? s : null;
-        JsonObject response = facade.sendMessage(endpoint, body, headers);
+        Boolean reply = args.get("reply") instanceof Boolean b ? b : null;
+        JsonObject response = facade.sendMessage(endpoint, body, headers, 
reply);
         if (response == null) {
             return "Error: no integration selected or PID unavailable";
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
index 6c2570b9add7..7ca5cef11bf8 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
@@ -22,7 +22,8 @@ happened to it, with no AI:
   else the overview an AI wrote in its `camel-summary.md`. A timer or a
   cron of In shows the period or schedule it runs with, from its
   properties with its profile applied.
-- **In, Routes, Out** โ€” what starts it (timers, HTTP, consumers), its
+- **In, Routes, Out** โ€” what starts it (timers, HTTP, consumers, the
+  operations of an OpenAPI contract the REST DSL serves), its
   routes (a route called with `direct:` sits under its caller with `โ†ณ`,
   one handed off with `seda:` or a `wireTap` with `โ‡ข`) and what it talks to, 
from its
   sources. Each line has the exchanges since it started (for an
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
index 1f8c6b769ee4..1be80a314444 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
@@ -561,6 +561,61 @@ class AboutViewTest {
                 .contains("10 exchanges ยท 1 failed").doesNotContain("30 
exchanges").doesNotContain("20 exchanges"));
     }
 
+    @Test
+    void theOperationsOfAContractComeInAndAreCountedOnce() throws Exception {
+        // contracts/openapi-server: the REST DSL serves stock-api.json; Camel 
adds route2 to serve it, which hands each
+        // request to the route of its operation
+        Files.writeString(dir.resolve("stock-api.json"), """
+                {"servers": [{"url": "http://localhost:8080/api"}],
+                 "paths": {"/stock/{sku}/reserve": {"post": {"operationId": 
"reserveStock"}}}}
+                """);
+        Files.writeString(dir.resolve("openapi-server.camel.yaml"), """
+                - rest:
+                    openApi:
+                      specification: stock-api.json
+                - route:
+                    id: reserveStock
+                    from:
+                      uri: direct:reserveStock
+                      steps:
+                        - log: reserved
+                """);
+        IntegrationInfo info = new IntegrationInfo();
+        info.pid = "1234";
+        info.name = "openapi-server";
+        info.directory = dir.toString();
+        for (String[] r : List.of(new String[] { "reserveStock", 
"direct://reserveStock" },
+                new String[] { "route2", 
"rest-openapi://stock-api.json?clientRequestValidation=true" })) {
+            RouteInfo route = new RouteInfo();
+            route.routeId = r[0];
+            route.from = r[1];
+            route.state = "Started";
+            info.routes.add(route);
+        }
+        TimelineJson t = new TimelineJson(T0 / 1000 + 59, 60);
+        for (int i = 50; i < 56; i++) {
+            t.route("reserveStock", "completed", i, 1).route("route2", 
"completed", i, 1);
+        }
+        MonitorContext ctx = new MonitorContext(new 
AtomicReference<>(List.of(info)), new AtomicReference<>(List.of()));
+        ctx.timeline.merge("1234", t.json(), true);
+        AboutView about = new AboutView(ctx);
+
+        await().atMost(20, TimeUnit.SECONDS).untilAsserted(() -> 
assertThat(render(about, info, 200, 34))
+                .contains("POST 
/api/stock/{sku}/reserve").doesNotContain("nothing from outside")
+                // 6 reservations, not 12 with the route that served them
+                .contains("6 exchanges").doesNotContain("12 exchanges"));
+    }
+
+    @Test
+    void aRouteAnAiAgentCallsAsAToolHasTheIconOfAi() {
+        // ai/mcp-server: its routes start from ai-tool
+        assertThat(AboutView.icon("ai-tool")).isEqualTo(TuiIcons.MCP);
+        assertThat(AboutView.icon("ai")).isEqualTo(TuiIcons.MCP);
+        assertThat(AboutView.icon("http")).isEqualTo("๐ŸŒ");
+        // cloud/aws-sqs: the queue the courier route consumes has the icon 
Out gives it, from the catalog
+        assertThat(AboutView.inIcon("aws2-sqs")).isEqualTo("๐Ÿ“จ");
+    }
+
     @Test
     void aboutAnIntegrationWithWhatHasHappened() throws Exception {
         Files.writeString(dir.resolve("shop.camel.yaml"), """

Reply via email to