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 d4bfc1191ce6 CAMEL-25535: camel-jbang-mcp - camel_catalog_example_file 
reads a bundled example by its short name (#27817)
d4bfc1191ce6 is described below

commit d4bfc1191ce6839f60e4b25802a3dfc1168d41e9
Author: Federico Mariani <[email protected]>
AuthorDate: Wed Oct 14 21:19:47 2026 +0200

    CAMEL-25535: camel-jbang-mcp - camel_catalog_example_file reads a bundled 
example by its short name (#27817)
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/catalog/docs/throwException-eip.adoc     |   8 +-
 .../camel/catalog/models/throwException.json       |   2 +-
 .../camel/catalog/schemas/camelYamlDsl-model.json  |   4 +-
 .../modules/eips/examples/json/throwException.json |   1 +
 .../src/main/docs/modules/eips/nav.adoc            |   2 +-
 .../modules/eips/pages/throwException-eip.adoc     |   8 +-
 .../org/apache/camel/model/throwException.json     |   2 +-
 .../camel/model/ThrowExceptionDefinition.java      |   2 +-
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |  17 ++-
 .../dsl/jbang/core/commands/ai/ToolRegistry.java   | 138 +++++++++++++------
 .../camel/dsl/jbang/core/common/ExampleHelper.java |  31 +++++
 .../core/commands/ai/ExampleToolsRegistryTest.java | 153 +++++++++++++++++++++
 .../dsl/jbang/core/commands/mcp/ExampleTools.java  | 143 +++----------------
 .../jbang/core/commands/mcp/ExampleToolsTest.java  |  84 +++++++----
 .../resources/schema/camelYamlDsl-model.json       |   4 +-
 15 files changed, 384 insertions(+), 215 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/throwException-eip.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/throwException-eip.adoc
index 637c1ab60227..cece8b1ba4a9 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/throwException-eip.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/throwException-eip.adoc
@@ -1,7 +1,7 @@
-= Stop EIP
-:doctitle: Stop
-:shortname: stop
-:description: Stops the processing of the current message
+= Throw Exception EIP
+:doctitle: Throw Exception
+:shortname: throwException
+:description: Throws an exception during route processing, either creating a 
new exception instance or re-throwing an existing one
 :since: 
 :supportlevel: Stable
 :tabs-sync-option:
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/throwException.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/throwException.json
index 8203c2384a2d..7ffb7e35602e 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/throwException.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/throwException.json
@@ -5,7 +5,7 @@
     "title": "Throw Exception",
     "description": "Throws an exception during route processing, either 
creating a new exception instance or re-throwing an existing one",
     "deprecated": false,
-    "label": "error,errorhandling",
+    "label": "eip,error,errorhandling",
     "javaType": "org.apache.camel.model.ThrowExceptionDefinition",
     "abstract": false,
     "input": true,
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
index 8bee214ea736..60af524015b7 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
@@ -532,7 +532,7 @@
         "ref" : "throwException",
         "title" : "Throw Exception",
         "description" : "Throws an exception during route processing, either 
creating a new exception instance or re-throwing an existing one",
-        "label" : "error,errorhandling"
+        "label" : "eip,error,errorhandling"
       }, {
         "name" : "to",
         "type" : "object",
@@ -8944,7 +8944,7 @@
     "throwException" : {
       "title" : "Throw Exception",
       "description" : "Throws an exception during route processing, either 
creating a new exception instance or re-throwing an existing one",
-      "label" : "error,errorhandling",
+      "label" : "eip,error,errorhandling",
       "input" : true,
       "isListItem" : true,
       "children" : [ {
diff --git 
a/core/camel-core-engine/src/main/docs/modules/eips/examples/json/throwException.json
 
b/core/camel-core-engine/src/main/docs/modules/eips/examples/json/throwException.json
new file mode 120000
index 000000000000..cdff8d7ccba6
--- /dev/null
+++ 
b/core/camel-core-engine/src/main/docs/modules/eips/examples/json/throwException.json
@@ -0,0 +1 @@
+../../../../../../../../camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/throwException.json
\ No newline at end of file
diff --git a/core/camel-core-engine/src/main/docs/modules/eips/nav.adoc 
b/core/camel-core-engine/src/main/docs/modules/eips/nav.adoc
index 2fc8aa9654e3..b1b3e04fa256 100644
--- a/core/camel-core-engine/src/main/docs/modules/eips/nav.adoc
+++ b/core/camel-core-engine/src/main/docs/modules/eips/nav.adoc
@@ -103,11 +103,11 @@
 ** xref:step-eip.adoc[Step]
 ** xref:stickyLoadBalancer-eip.adoc[Sticky Load Balancer]
 ** xref:stop-eip.adoc[Stop]
-** xref:throwException-eip.adoc[Stop]
 ** xref:streamConfig-eip.adoc[StreamConfig]
 ** xref:switch-eip.adoc[Switch]
 ** xref:threads-eip.adoc[Threads]
 ** xref:throttle-eip.adoc[Throttle]
+** xref:throwException-eip.adoc[Throw Exception]
 ** xref:to-eip.adoc[To]
 ** xref:toD-eip.adoc[To D]
 ** xref:topicLoadBalancer-eip.adoc[Topic Load Balancer]
diff --git 
a/core/camel-core-engine/src/main/docs/modules/eips/pages/throwException-eip.adoc
 
b/core/camel-core-engine/src/main/docs/modules/eips/pages/throwException-eip.adoc
index 637c1ab60227..cece8b1ba4a9 100644
--- 
a/core/camel-core-engine/src/main/docs/modules/eips/pages/throwException-eip.adoc
+++ 
b/core/camel-core-engine/src/main/docs/modules/eips/pages/throwException-eip.adoc
@@ -1,7 +1,7 @@
-= Stop EIP
-:doctitle: Stop
-:shortname: stop
-:description: Stops the processing of the current message
+= Throw Exception EIP
+:doctitle: Throw Exception
+:shortname: throwException
+:description: Throws an exception during route processing, either creating a 
new exception instance or re-throwing an existing one
 :since: 
 :supportlevel: Stable
 :tabs-sync-option:
diff --git 
a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/throwException.json
 
b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/throwException.json
index 8203c2384a2d..7ffb7e35602e 100644
--- 
a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/throwException.json
+++ 
b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/throwException.json
@@ -5,7 +5,7 @@
     "title": "Throw Exception",
     "description": "Throws an exception during route processing, either 
creating a new exception instance or re-throwing an existing one",
     "deprecated": false,
-    "label": "error,errorhandling",
+    "label": "eip,error,errorhandling",
     "javaType": "org.apache.camel.model.ThrowExceptionDefinition",
     "abstract": false,
     "input": true,
diff --git 
a/core/camel-core-model/src/main/java/org/apache/camel/model/ThrowExceptionDefinition.java
 
b/core/camel-core-model/src/main/java/org/apache/camel/model/ThrowExceptionDefinition.java
index 48aee5f0cb97..90eac05839cc 100644
--- 
a/core/camel-core-model/src/main/java/org/apache/camel/model/ThrowExceptionDefinition.java
+++ 
b/core/camel-core-model/src/main/java/org/apache/camel/model/ThrowExceptionDefinition.java
@@ -28,7 +28,7 @@ import org.apache.camel.spi.annotations.DslArg;
 /**
  * Throws an exception
  */
-@Metadata(label = "error,errorhandling",
+@Metadata(label = "eip,error,errorhandling",
           description = "Throws an exception during route processing, either 
creating a new exception instance or re-throwing an existing one")
 @XmlRootElement(name = "throwException")
 @XmlAccessorType(XmlAccessType.FIELD)
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 889424d6b4f1..8e2b9bd1962d 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -202,7 +202,7 @@ Here are example prompts you can give your AI assistant. 
The assistant automatic
   EIP aliases such as `fan-out`, `dedup` and `rate-limit` match)
 * _"Show me all AWS source kamelets"_ — uses `camel_catalog_kamelets`
 * _"What options does aws-s3-source accept?"_ — uses 
`camel_catalog_kamelet_doc`
-* _"Show me beginner REST examples"_ — uses `camel_catalog_examples`
+* _"Show me the quick-start examples"_ — uses `camel_catalog_examples`
 
 === Building routes
 
@@ -519,13 +519,18 @@ client to the next.
 
 | `camel_catalog_examples`
 | List available Camel CLI examples with filtering by name, description, or 
tag (case-insensitive substring match).
-  Supports filtering by difficulty level (`beginner`, `intermediate`, 
`advanced`) and limiting the number of results.
-  Returns name, title, description, level, tags, and file list for each 
example.
+  The examples are grouped by level, the steps of the examples ladder in 
reading order: `quick-start`, `run`,
+  `transform`, `route`, `fail-well`, `connect`, `connect-service`, 
`contracts`, `ai`, `cloud` and `showcase`.
+  Supports filtering by one level and limiting the number of results; an 
unknown level is answered with the levels
+  that exist. Returns the groups, and for each example its name, title, 
description, level, tags, what it teaches,
+  the infra services it needs, whether it is bundled and its files.
 
 | `camel_catalog_example_file`
-| Get the content of a specific file from a Camel CLI example. For bundled 
examples, returns the file content
-  directly. For non-bundled examples, returns a GitHub URL where the file can 
be found. Use `camel_catalog_examples`
-  first to discover example names and their files.
+| Get the content of a specific file from a Camel CLI example, named by its 
full name such as
+  `quick-start/timer-log` or by its short name `timer-log`, for example the 
file `timer-log.camel.yaml`. For bundled
+  examples, returns the file content directly. For non-bundled examples, 
returns a GitHub URL where the file can be
+  found. The answer gives the full name of the example; an unknown name is 
answered with the closest example names.
+  Use `camel_catalog_examples` first to discover example names and their files.
 |===
 
 === Route Understanding
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
index 2b9549d6f1ce..66214b408afd 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.ai;
 
+import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -1051,6 +1052,17 @@ public final class ToolRegistry {
 
     // ---- Example tools ----
 
+    /** Argument descriptions of the example tools, shared with the MCP server 
(CAMEL-25535). */
+    public static final String EXAMPLE_LEVEL_DESC
+            = "Only the examples of one group (level): quick-start, run, 
transform, route, fail-well, connect, "
+              + "connect-service, contracts, ai, cloud or showcase";
+    public static final String EXAMPLE_NAME_DESC
+            = "Example name as listed, e.g. quick-start/timer-log or 
fail-well/circuit-breaker (the short name without "
+              + "the group, such as timer-log, works too)";
+    public static final String EXAMPLE_FILE_DESC
+            = "File name within the example, as in its files, e.g. 
timer-log.camel.yaml or application.properties "
+              + "for quick-start/timer-log";
+
     private static void registerExampleTools() {
         register(tool("list_examples",
                 "List the Camel CLI examples, grouped as the ladder of the 
examples (quick-start, run, transform, "
@@ -1061,10 +1073,7 @@ public final class ToolRegistry {
                                        + "Call it without arguments for the 
whole ladder, with level for one group.")
                 .param("filter", "string",
                         "Filter by name, description, or tag 
(case-insensitive)", false)
-                .param("level", "string",
-                        "Only the examples of one group: quick-start, run, 
transform, route, fail-well, connect, "
-                                          + "connect-service, contracts, ai, 
cloud or showcase",
-                        false)
+                .param("level", "string", EXAMPLE_LEVEL_DESC, false)
                 .param("limit", "integer", "Maximum number of examples to 
return (default: 50)", false)
                 .executor((ctx, args) -> {
                     String filter = args.get("filter");
@@ -1082,6 +1091,16 @@ public final class ToolRegistry {
                         }
                     }
                     List<JsonObject> catalog2 = ExampleHelper.loadCatalog();
+                    if (level != null && !level.isBlank()) {
+                        List<String> levels = new 
ArrayList<>(ExampleHelper.groupByLevel(catalog2).keySet());
+                        String wanted = level.trim();
+                        if 
(levels.stream().noneMatch(wanted::equalsIgnoreCase)) {
+                            // an old level such as beginner answered 0 
examples otherwise (CAMEL-25535)
+                            throw new ToolExecutionException(
+                                    "No example at level " + wanted + "; the 
levels are: " + String.join(", ", levels));
+                        }
+                        level = wanted;
+                    }
                     List<JsonObject> filtered = 
ExampleHelper.filterExamples(catalog2, filter);
                     List<JsonObject> groups = new ArrayList<>();
                     List<JsonObject> results = new ArrayList<>();
@@ -1117,6 +1136,7 @@ public final class ToolRegistry {
                                 jo.put("needs", needs);
                             }
                             jo.put("bundled", ExampleHelper.isBundled(entry));
+                            jo.put("requiresDocker", 
ExampleHelper.requiresDocker(entry));
                             jo.put("files", ExampleHelper.getFiles(entry));
                             results.add(jo);
                         }
@@ -1130,45 +1150,77 @@ public final class ToolRegistry {
                 }));
 
         register(tool("get_example_file",
-                "Get the content of a file from a bundled Camel CLI example.")
-                .param("example", "string",
-                        "Example name (e.g., timer-log, rest-api, 
circuit-breaker)", true)
-                .param("file", "string",
-                        "File name within the example (e.g., 
route.camel.yaml)", true)
-                .executor((ctx, args) -> {
-                    String example = args.get("example");
-                    String file = args.get("file");
-                    if (example == null || example.isBlank()) {
-                        throw new ToolExecutionException("example name is 
required");
-                    }
-                    if (file == null || file.isBlank()) {
-                        throw new ToolExecutionException("file name is 
required");
-                    }
-                    List<JsonObject> catalog2 = ExampleHelper.loadCatalog();
-                    JsonObject entry = ExampleHelper.findExample(catalog2, 
example);
-                    if (entry == null) {
-                        return "Example not found: " + example;
-                    }
-                    List<String> files = ExampleHelper.getFiles(entry);
-                    if (!files.contains(file)) {
-                        return "File '" + file + "' not found in example '" + 
example + "'. Available: " + files;
-                    }
-                    if (ExampleHelper.isBundled(entry)) {
-                        String resourcePath = "examples/" + example + "/" + 
file;
-                        try (InputStream is
-                                = 
ExampleHelper.class.getClassLoader().getResourceAsStream(resourcePath)) {
-                            if (is != null) {
-                                return IOHelper.loadText(is);
-                            }
-                        } catch (Exception e) {
-                            return "Error reading file: " + e.getMessage();
-                        }
-                        return "Could not read bundled file: " + resourcePath;
-                    } else {
-                        return "This example is not bundled. View it on 
GitHub: "
-                               + ExampleHelper.getGithubUrl(entry) + "/" + 
file;
-                    }
-                }));
+                "Get the content of a file from a Camel CLI example: the 
content of a bundled example, the GitHub "
+                                          + "URL of the file for the others. 
Returns the full name of the example.")
+                .param("example", "string", EXAMPLE_NAME_DESC, true)
+                .param("file", "string", EXAMPLE_FILE_DESC, true)
+                .executor((ctx, args) -> exampleFile(args.get("example"), 
args.get("file")).toJson()));
+    }
+
+    /**
+     * A file of an example: the catalog entry is found by its full or short 
name and the bundled resource is read from
+     * the entry's name, so a short name reads the file as well (CAMEL-25535).
+     */
+    static JsonObject exampleFile(String example, String file) {
+        if (example == null || example.isBlank()) {
+            throw new ToolExecutionException("example name is required");
+        }
+        if (file == null || file.isBlank()) {
+            throw new ToolExecutionException("file name is required");
+        }
+        String wanted = example.trim();
+        String fileName = file.trim();
+        JsonObject entry = resolveExample(ExampleHelper.loadCatalog(), wanted);
+        String name = entry.getString("name");
+        List<String> files = ExampleHelper.getFiles(entry);
+        if (!files.contains(fileName)) {
+            throw new ToolExecutionException(
+                    "File '" + fileName + "' not found in example '" + name + 
"'. Available files: " + files);
+        }
+        JsonObject answer = new JsonObject();
+        answer.put("example", name);
+        answer.put("file", fileName);
+        if (ExampleHelper.isBundled(entry)) {
+            // the resource path comes from the catalog entry, not from the 
name the caller gave
+            String resourcePath = "examples/" + name + "/" + fileName;
+            try (InputStream is = 
ExampleHelper.class.getClassLoader().getResourceAsStream(resourcePath)) {
+                if (is == null) {
+                    throw new ToolExecutionException("Could not read bundled 
file: " + resourcePath);
+                }
+                answer.put("content", IOHelper.loadText(is));
+            } catch (IOException e) {
+                throw new ToolExecutionException("Could not read bundled file: 
" + resourcePath + ": " + e.getMessage());
+            }
+        } else {
+            answer.put("githubUrl", ExampleHelper.getGithubUrl(entry) + "/" + 
fileName);
+        }
+        return answer;
+    }
+
+    /**
+     * The catalog entry of an example by its full name, or by its short name 
when only one example has it, as camel run
+     * resolves it; an ambiguous short name answers with the candidates, an 
unknown one with the closest names.
+     */
+    static JsonObject resolveExample(List<JsonObject> catalog, String example) 
{
+        if (!example.contains("/")) {
+            List<JsonObject> same = 
ExampleHelper.findExamplesByShortName(catalog, example);
+            if (same.size() > 1) {
+                throw new ToolExecutionException(
+                        "Ambiguous example: " + example + ". Use one of: "
+                                                 + same.stream().map(e -> 
e.getString("name"))
+                                                         
.collect(Collectors.joining(", ")));
+            }
+        }
+        JsonObject entry = ExampleHelper.findExample(catalog, example);
+        if (entry == null) {
+            List<String> similar = ExampleHelper.suggestExamples(catalog, 
example);
+            throw new ToolExecutionException(
+                    "Example not found: " + example
+                                             + (similar.isEmpty()
+                                                     ? ". Use list_examples to 
see the examples."
+                                                     : ". Did you mean: " + 
String.join(", ", similar) + "?"));
+        }
+        return entry;
     }
 
     // ---- Processor detail enrichment ----
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java
index 357113f3d11c..97bdf84b2097 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java
@@ -29,10 +29,13 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Comparator;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
 
+import org.apache.camel.main.util.SuggestSimilarHelper;
 import org.apache.camel.util.FileUtil;
 import org.apache.camel.util.IOHelper;
 import org.apache.camel.util.json.JsonArray;
@@ -97,6 +100,34 @@ public final class ExampleHelper {
         return matches;
     }
 
+    /**
+     * The full names of the examples closest to a name that matches none 
(CAMEL-25535): first the nearest by edit
+     * distance to a full or short name, then the ones whose name contains it 
or whose short name it contains (so an old
+     * group such as beginner/timer-log points at quick-start/timer-log), at 
most five.
+     */
+    public static List<String> suggestExamples(List<JsonObject> catalog, 
String name) {
+        if (name == null || name.isBlank()) {
+            return List.of();
+        }
+        Set<String> result = new LinkedHashSet<>();
+        for (String similar : 
SuggestSimilarHelper.didYouMean(getExampleNames(catalog), name)) {
+            JsonObject entry = findExample(catalog, similar);
+            if (entry != null) {
+                result.add(entry.getString("name"));
+            }
+        }
+        String key = name.trim().toLowerCase(Locale.ROOT);
+        for (JsonObject entry : catalog) {
+            String full = entry.getString("name");
+            String shortName = getShortName(entry).toLowerCase(Locale.ROOT);
+            if (full != null && (full.toLowerCase(Locale.ROOT).contains(key)
+                    || shortName.length() >= 3 && key.contains(shortName))) {
+                result.add(full);
+            }
+        }
+        return result.stream().limit(5).toList();
+    }
+
     public static List<String> getExampleNames(List<JsonObject> catalog) {
         List<String> names = new ArrayList<>();
         for (JsonObject entry : catalog) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExampleToolsRegistryTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExampleToolsRegistryTest.java
new file mode 100644
index 000000000000..843caf7bab0b
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExampleToolsRegistryTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.util.IOHelper;
+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;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * The example tools of the shared registry (list_examples, get_example_file) 
as an AI agent calls them (CAMEL-25535).
+ */
+class ExampleToolsRegistryTest {
+
+    @Test
+    void readsOneFileOfEveryBundledExampleByItsShortName() throws Exception {
+        List<JsonObject> catalog = rawCatalog();
+        List<String> bundled = new ArrayList<>();
+        for (JsonObject entry : catalog) {
+            String name = entry.getString("name");
+            String shortName = name.substring(name.indexOf('/') + 1);
+            List<String> files = entry.getCollection("files");
+            assertThat(files).as("files of %s", name).isNotEmpty();
+            // a route file when there is one, the file an agent asks for first
+            String file = files.stream().filter(f -> 
f.contains(".camel.")).findFirst().orElse(files.get(0));
+
+            JsonObject answer = exampleFile(shortName, file);
+
+            assertThat(answer.getString("example")).as("example of %s", 
shortName).isEqualTo(name);
+            assertThat(answer.getString("file")).isEqualTo(file);
+            if (entry.getBooleanOrDefault("bundled", false)) {
+                bundled.add(name);
+                assertThat(answer.getString("content")).as("content of %s/%s", 
name, file)
+                        .isEqualTo(resource("examples/" + name + "/" + file));
+                assertThat(answer.getString("githubUrl")).isNull();
+            } else {
+                assertThat(answer.getString("content")).isNull();
+                assertThat(answer.getString("githubUrl")).as("githubUrl of 
%s", name).endsWith("/" + file);
+            }
+        }
+        assertThat(bundled).as("bundled 
examples").contains("quick-start/timer-log").hasSizeGreaterThan(10);
+    }
+
+    @Test
+    void anUnknownExampleListsTheClosestNames() {
+        // the group of 4.22 (beginner), a typo, and a name that is nowhere 
near an example
+        assertThatThrownBy(() -> exampleFile("beginner/timer-log", 
"timer-log.camel.yaml"))
+                .isInstanceOf(ToolExecutionException.class)
+                .hasMessageContaining("Example not found: beginner/timer-log")
+                .hasMessageContaining("Did you mean: ")
+                .hasMessageContaining("quick-start/timer-log");
+        assertThatThrownBy(() -> exampleFile("timer-lgo", 
"timer-log.camel.yaml"))
+                .isInstanceOf(ToolExecutionException.class)
+                .hasMessageContaining("quick-start/timer-log");
+        assertThatThrownBy(() -> exampleFile("zzzzzzzzzzzz", "x.yaml"))
+                .isInstanceOf(ToolExecutionException.class)
+                .hasMessageContaining("Example not found: zzzzzzzzzzzz")
+                .hasMessageContaining("list_examples")
+                .hasMessageNotContaining("Did you mean");
+    }
+
+    @Test
+    void anAmbiguousShortNameListsTheCandidates() {
+        List<JsonObject> catalog = List.of(new JsonObject(Map.of("name", 
"route/x")),
+                new JsonObject(Map.of("name", "transform/x")));
+
+        assertThatThrownBy(() -> ToolRegistry.resolveExample(catalog, "x"))
+                .isInstanceOf(ToolExecutionException.class)
+                .hasMessage("Ambiguous example: x. Use one of: route/x, 
transform/x");
+        assertThat(ToolRegistry.resolveExample(catalog, 
"transform/x").getString("name")).isEqualTo("transform/x");
+    }
+
+    @Test
+    void anUnknownFileListsTheFilesOfTheExample() {
+        assertThatThrownBy(() -> exampleFile("timer-log", "route.camel.yaml"))
+                .isInstanceOf(ToolExecutionException.class)
+                .hasMessageContaining("File 'route.camel.yaml' not found in 
example 'quick-start/timer-log'")
+                .hasMessageContaining("timer-log.camel.yaml");
+    }
+
+    @Test
+    void anUnknownLevelSaysWhichLevelsExist() {
+        assertThatThrownBy(() -> ToolRegistry.execute("list_examples", new 
ToolContext(), Map.of("level", "beginner")))
+                .isInstanceOf(ToolExecutionException.class)
+                .hasMessageContaining("No example at level beginner")
+                .hasMessageContaining("quick-start")
+                .hasMessageContaining("showcase");
+
+        Object fine = ToolRegistry.execute("list_examples", new ToolContext(), 
Map.of("level", "Quick-Start"));
+        assertThat(fine.toString()).contains("\"level\":\"quick-start\"");
+    }
+
+    @Test
+    void theArgumentDescriptionsNameAnExampleAndAFileThatExist() {
+        ToolDescriptor tool = ToolRegistry.findTool("get_example_file");
+        assertThat(tool).isNotNull();
+        
assertThat(ToolRegistry.EXAMPLE_NAME_DESC).contains("quick-start/timer-log");
+        
assertThat(ToolRegistry.EXAMPLE_FILE_DESC).contains("timer-log.camel.yaml");
+        JsonObject answer = exampleFile("quick-start/timer-log", 
"timer-log.camel.yaml");
+        
assertThat(answer.getString("example")).isEqualTo("quick-start/timer-log");
+        assertThat(answer.getString("content")).contains("uri: timer");
+    }
+
+    private static JsonObject exampleFile(String example, String file) {
+        Object result = ToolRegistry.execute("get_example_file", new 
ToolContext(),
+                Map.of("example", example, "file", file));
+        try {
+            return (JsonObject) Jsoner.deserialize(result.toString());
+        } catch (Exception e) {
+            throw new AssertionError("not a JSON object: " + result, e);
+        }
+    }
+
+    private static List<JsonObject> rawCatalog() throws Exception {
+        JsonArray array = (JsonArray) 
Jsoner.deserialize(resource("examples/camel-jbang-example-catalog.json"));
+        List<JsonObject> catalog = new ArrayList<>();
+        for (Object o : array) {
+            catalog.add((JsonObject) o);
+        }
+        assertThat(catalog).isNotEmpty();
+        return catalog;
+    }
+
+    private static String resource(String path) throws Exception {
+        try (InputStream is = 
ExampleToolsRegistryTest.class.getClassLoader().getResourceAsStream(path)) {
+            assertThat(is).as("resource %s", path).isNotNull();
+            return IOHelper.loadText(is);
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java
index d857ff0b2737..2b5c0b735456 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java
@@ -16,23 +16,20 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.mcp;
 
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-
 import jakarta.enterprise.context.ApplicationScoped;
 
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
-import io.quarkiverse.mcp.server.ToolCallException;
-import org.apache.camel.dsl.jbang.core.common.ExampleHelper;
-import org.apache.camel.util.IOHelper;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolRegistry;
 import org.apache.camel.util.json.JsonObject;
 
+import static org.apache.camel.dsl.jbang.core.commands.mcp.AuthoringTools.args;
+import static org.apache.camel.dsl.jbang.core.commands.mcp.AuthoringTools.call;
+
 /**
- * MCP Tools for browsing Camel CLI examples.
+ * MCP Tools for browsing Camel CLI examples: thin wrappers over the example 
tools of the {@link ToolRegistry}
+ * (list_examples and get_example_file), so the MCP server, {@code camel tui} 
and {@code camel ask} answer from one
+ * implementation (CAMEL-25535).
  */
 @McpSecured
 @ApplicationScoped
@@ -48,132 +45,24 @@ public class ExampleTools {
                         + "Call it without arguments for the whole ladder, 
with level for one group, "
                         + "or with filter to search by name, description or 
tag. "
                         + "Use camel_catalog_example_file to read a file of an 
example.")
-    public ExampleListResult camel_catalog_examples(
+    public JsonObject camel_catalog_examples(
             @ToolArg(description = "Filter examples by name, description, or 
tag (case-insensitive substring match)",
                      required = false) String filter,
-            @ToolArg(description = "Only the examples of one group (level): 
quick-start, run, transform, route, "
-                                   + "fail-well, connect, connect-service, 
contracts, ai, cloud or showcase",
-                     required = false) String level,
+            @ToolArg(description = ToolRegistry.EXAMPLE_LEVEL_DESC, required = 
false) String level,
             @ToolArg(description = "Maximum number of examples to return 
(default: 50)",
                      required = false) Integer limit) {
-
-        int maxResults = limit != null && limit > 0 ? limit : 50;
-
-        try {
-            List<JsonObject> catalog = ExampleHelper.loadCatalog();
-            List<JsonObject> filtered = ExampleHelper.filterExamples(catalog, 
filter);
-
-            List<GroupInfo> groups = new ArrayList<>();
-            List<ExampleInfo> result = new ArrayList<>();
-            int total = 0;
-            for (Map.Entry<String, List<JsonObject>> group : 
ExampleHelper.groupByLevel(filtered).entrySet()) {
-                if (level != null && !level.isBlank() && 
!group.getKey().equalsIgnoreCase(level)) {
-                    continue;
-                }
-                total += group.getValue().size();
-                groups.add(new GroupInfo(
-                        group.getKey(),
-                        ExampleHelper.getGroupTitle(group.getKey()),
-                        ExampleHelper.getGroupIntro(group.getKey()),
-                        group.getValue().size()));
-                for (JsonObject entry : group.getValue()) {
-                    if (result.size() < maxResults) {
-                        result.add(toExampleInfo(entry));
-                    }
-                }
-            }
-
-            return new ExampleListResult(result.size(), total, groups, result);
-        } catch (Throwable e) {
-            throw new ToolCallException(
-                    "Failed to list examples (" + e.getClass().getName() + "): 
" + e.getMessage(), null);
-        }
+        return call("list_examples", args("filter", filter, "level", level, 
"limit", limit));
     }
 
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Get the content of a specific file from a Camel CLI 
example. "
                         + "Use camel_catalog_examples first to find the 
example name and its files. "
                         + "Only bundled examples can return file contents 
directly; "
-                        + "for non-bundled examples, a GitHub URL is returned 
instead.")
-    public ExampleFileResult camel_catalog_example_file(
-            @ToolArg(description = "Example name (e.g., timer-log, rest-api, 
circuit-breaker)") String example,
-            @ToolArg(description = "File name within the example (e.g., 
route.camel.yaml, application.properties)") String file) {
-
-        if (example == null || example.isBlank()) {
-            throw new ToolCallException("Example name is required", null);
-        }
-        if (file == null || file.isBlank()) {
-            throw new ToolCallException("File name is required", null);
-        }
-
-        try {
-            List<JsonObject> catalog = ExampleHelper.loadCatalog();
-            JsonObject entry = ExampleHelper.findExample(catalog, example);
-            if (entry == null) {
-                throw new ToolCallException("Example not found: " + example, 
null);
-            }
-
-            List<String> files = ExampleHelper.getFiles(entry);
-            if (!files.contains(file)) {
-                throw new ToolCallException(
-                        "File '" + file + "' not found in example '" + example
-                                            + "'. Available files: " + files,
-                        null);
-            }
-
-            if (ExampleHelper.isBundled(entry)) {
-                String resourcePath = "examples/" + example + "/" + file;
-                try (InputStream is = 
ExampleHelper.class.getClassLoader().getResourceAsStream(resourcePath)) {
-                    if (is != null) {
-                        String content = IOHelper.loadText(is);
-                        return new ExampleFileResult(example, file, content, 
null);
-                    }
-                }
-                throw new ToolCallException("Could not read bundled file: " + 
resourcePath, null);
-            } else {
-                String githubUrl = ExampleHelper.getGithubUrl(entry) + "/" + 
file;
-                return new ExampleFileResult(example, file, null, githubUrl);
-            }
-        } catch (ToolCallException e) {
-            throw e;
-        } catch (Throwable e) {
-            throw new ToolCallException(
-                    "Failed to read example file (" + e.getClass().getName() + 
"): " + e.getMessage(), null);
-        }
-    }
-
-    @SuppressWarnings("unchecked")
-    private ExampleInfo toExampleInfo(JsonObject entry) {
-        Collection<String> tags = (Collection<String>) entry.get("tags");
-        return new ExampleInfo(
-                entry.getString("name"),
-                entry.getString("title"),
-                entry.getString("description"),
-                entry.getString("level"),
-                ExampleHelper.getOrder(entry) == Integer.MAX_VALUE ? null : 
ExampleHelper.getOrder(entry),
-                tags != null ? new ArrayList<>(tags) : List.of(),
-                ExampleHelper.getTeaches(entry),
-                ExampleHelper.getInfraServices(entry),
-                ExampleHelper.getNeeds(entry),
-                ExampleHelper.isBundled(entry),
-                ExampleHelper.requiresDocker(entry),
-                ExampleHelper.getFiles(entry));
-    }
-
-    // Result records
-
-    public record ExampleListResult(int count, int total, List<GroupInfo> 
groups, List<ExampleInfo> examples) {
-    }
-
-    public record GroupInfo(String level, String title, String intro, int 
count) {
-    }
-
-    public record ExampleInfo(String name, String title, String description, 
String level, Integer order,
-            List<String> tags, Map<String, List<String>> teaches, List<String> 
infraServices,
-            // what the user does before the run, beyond the infra services: a 
model to pull, a key to set
-            String needs, boolean bundled, boolean requiresDocker, 
List<String> files) {
-    }
-
-    public record ExampleFileResult(String example, String file, String 
content, String githubUrl) {
+                        + "for non-bundled examples, a GitHub URL is returned 
instead. "
+                        + "The answer names the example by its full name 
(group/name).")
+    public JsonObject camel_catalog_example_file(
+            @ToolArg(description = ToolRegistry.EXAMPLE_NAME_DESC) String 
example,
+            @ToolArg(description = ToolRegistry.EXAMPLE_FILE_DESC) String 
file) {
+        return call("get_example_file", args("example", example, "file", 
file));
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleToolsTest.java
index 05a14ce4e299..dc3ab34208c7 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleToolsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleToolsTest.java
@@ -19,54 +19,92 @@ package org.apache.camel.dsl.jbang.core.commands.mcp;
 import java.util.Comparator;
 import java.util.List;
 
+import io.quarkiverse.mcp.server.ToolCallException;
 import org.apache.camel.dsl.jbang.core.common.ExampleHelper;
+import org.apache.camel.util.json.JsonObject;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 class ExampleToolsTest {
 
     @Test
     void listsTheWholeLadderInReadingOrderWithoutArguments() {
-        ExampleTools.ExampleListResult all = new 
ExampleTools().camel_catalog_examples(null, null, null);
+        JsonObject all = new ExampleTools().camel_catalog_examples(null, null, 
null);
 
-        assertThat(all.count()).isEqualTo(all.total());
-        assertThat(all.groups()).isNotEmpty();
-        List<String> levels = 
all.groups().stream().map(ExampleTools.GroupInfo::level).toList();
+        assertThat(all.getInteger("count")).isEqualTo(all.getInteger("total"));
+        List<JsonObject> groups = objects(all, "groups");
+        assertThat(groups).isNotEmpty();
+        List<String> levels = groups.stream().map(g -> 
g.getString("level")).toList();
         assertThat(levels).isSubsetOf(ExampleHelper.getGroupOrder());
         assertThat(levels).isSortedAccordingTo((a, b) -> Integer.compare(
                 ExampleHelper.getGroupOrder().indexOf(a), 
ExampleHelper.getGroupOrder().indexOf(b)));
         assertThat(levels.get(0)).isEqualTo("quick-start");
-        ExampleTools.GroupInfo first = all.groups().get(0);
-        
assertThat(first.title()).isEqualTo(ExampleHelper.getGroupTitle("quick-start"));
-        assertThat(first.intro()).isNotBlank();
+        JsonObject first = groups.get(0);
+        
assertThat(first.getString("title")).isEqualTo(ExampleHelper.getGroupTitle("quick-start"));
+        assertThat(first.getString("intro")).isNotBlank();
 
         // the examples follow the groups in order
-        List<String> exampleLevels = 
all.examples().stream().map(ExampleTools.ExampleInfo::level).distinct().toList();
+        List<JsonObject> examples = objects(all, "examples");
+        List<String> exampleLevels = examples.stream().map(e -> 
e.getString("level")).distinct().toList();
         assertThat(exampleLevels).isEqualTo(levels);
-        
assertThat(all.examples().stream().map(ExampleTools.ExampleInfo::name)).contains("quick-start/timer-log");
+        assertThat(examples.stream().map(e -> 
e.getString("name"))).contains("quick-start/timer-log");
+        assertThat(examples).allMatch(e -> e.get("requiresDocker") instanceof 
Boolean);
     }
 
     @Test
     void filtersOneGroupAndReturnsWhatTheExamplesTeach() {
-        ExampleTools.ExampleListResult run = new 
ExampleTools().camel_catalog_examples(null, "run", null);
-
-        assertThat(run.groups()).hasSize(1);
-        assertThat(run.groups().get(0).level()).isEqualTo("run");
-        assertThat(run.groups().get(0).count()).isEqualTo(run.total());
-        assertThat(run.examples()).allMatch(e -> "run".equals(e.level()));
-        assertThat(run.examples()).isSortedAccordingTo(
-                Comparator.comparingInt(e -> e.order() != null ? e.order() : 
Integer.MAX_VALUE));
-        assertThat(run.examples()).anyMatch(e -> !e.teaches().isEmpty());
+        JsonObject run = new ExampleTools().camel_catalog_examples(null, 
"run", null);
+
+        List<JsonObject> groups = objects(run, "groups");
+        assertThat(groups).hasSize(1);
+        assertThat(groups.get(0).getString("level")).isEqualTo("run");
+        
assertThat(groups.get(0).getInteger("count")).isEqualTo(run.getInteger("total"));
+        List<JsonObject> examples = objects(run, "examples");
+        assertThat(examples).allMatch(e -> "run".equals(e.getString("level")));
+        assertThat(examples).isSortedAccordingTo(
+                Comparator.comparingInt(e -> e.getInteger("order") != null ? 
e.getInteger("order") : Integer.MAX_VALUE));
+        assertThat(examples).anyMatch(e -> !e.getMap("teaches").isEmpty());
     }
 
     @Test
     void searchesByNameAndHonoursTheLimit() {
-        ExampleTools.ExampleListResult kafka = new 
ExampleTools().camel_catalog_examples("kafka", null, 1);
+        JsonObject kafka = new ExampleTools().camel_catalog_examples("kafka", 
null, 1);
+
+        assertThat(kafka.getInteger("total")).isGreaterThanOrEqualTo(1);
+        assertThat(kafka.getInteger("count")).isEqualTo(1);
+        JsonObject first = objects(kafka, "examples").get(0);
+        assertThat(first.getString("name")).contains("kafka");
+        assertThat(first.<List<String>> 
getCollection("infraServices")).contains("kafka");
+    }
+
+    @Test
+    void readsAFileOfABundledExampleByItsShortName() {
+        // CAMEL-25535: the short name of the argument description used to end 
in "Could not read bundled file"
+        JsonObject answer = new 
ExampleTools().camel_catalog_example_file("timer-log", "timer-log.camel.yaml");
+
+        
assertThat(answer.getString("example")).isEqualTo("quick-start/timer-log");
+        assertThat(answer.getString("file")).isEqualTo("timer-log.camel.yaml");
+        assertThat(answer.getString("content")).contains("uri: timer");
+        assertThat(answer.getString("githubUrl")).isNull();
+
+        JsonObject full = new 
ExampleTools().camel_catalog_example_file("quick-start/timer-log", 
"timer-log.camel.yaml");
+        
assertThat(full.getString("content")).isEqualTo(answer.getString("content"));
+    }
+
+    @Test
+    void errorsAreToolErrors() {
+        // the messages are tested on the registry tools; here only that they 
reach the client as tool errors
+        ExampleTools tools = new ExampleTools();
+        assertThatThrownBy(() -> tools.camel_catalog_examples(null, 
"beginner", null))
+                
.isInstanceOf(ToolCallException.class).hasMessageContaining("No example at 
level beginner");
+        assertThatThrownBy(() -> 
tools.camel_catalog_example_file("beginner/timer-log", "timer-log.camel.yaml"))
+                
.isInstanceOf(ToolCallException.class).hasMessageContaining("Example not 
found");
+    }
 
-        assertThat(kafka.total()).isGreaterThanOrEqualTo(1);
-        assertThat(kafka.count()).isEqualTo(1);
-        assertThat(kafka.examples().get(0).name()).contains("kafka");
-        assertThat(kafka.examples().get(0).infraServices()).contains("kafka");
+    @SuppressWarnings("unchecked")
+    private static List<JsonObject> objects(JsonObject json, String key) {
+        return (List<JsonObject>) json.get(key);
     }
 }
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
index 8bee214ea736..60af524015b7 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
@@ -532,7 +532,7 @@
         "ref" : "throwException",
         "title" : "Throw Exception",
         "description" : "Throws an exception during route processing, either 
creating a new exception instance or re-throwing an existing one",
-        "label" : "error,errorhandling"
+        "label" : "eip,error,errorhandling"
       }, {
         "name" : "to",
         "type" : "object",
@@ -8944,7 +8944,7 @@
     "throwException" : {
       "title" : "Throw Exception",
       "description" : "Throws an exception during route processing, either 
creating a new exception instance or re-throwing an existing one",
-      "label" : "error,errorhandling",
+      "label" : "eip,error,errorhandling",
       "input" : true,
       "isListItem" : true,
       "children" : [ {

Reply via email to