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

davsclaus pushed a commit to branch feature/CAMEL-24828-examples-mcp-tools
in repository https://gitbox.apache.org/repos/asf/camel.git

commit b63d3f470542fabf3767fd70bf4a3cbbf0ee3745
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Sep 18 22:48:03 2026 +0200

    CAMEL-24828: the examples tools of camel-jbang-mcp list the examples by 
group
    
    camel_catalog_examples (and list_examples in the core AI tool registry) now 
take
    optional arguments, return the groups of the ladder with title and intro 
and the
    examples in reading order with order, what they teach, the infra services 
they
    need and whether they are bundled. The stale beginner/intermediate/advanced
    descriptions are replaced by the group names, and the cap of 20 results in 
the
    core registry is replaced by a limit argument (default 50).
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
---
 .../dsl/jbang/core/commands/ai/ToolRegistry.java   | 68 +++++++++++++++------
 .../camel/dsl/jbang/core/common/ExampleHelper.java | 28 ++++++---
 .../jbang/core/commands/ai/ToolRegistryTest.java   | 25 ++++++++
 .../dsl/jbang/core/commands/mcp/ExampleTools.java  | 67 ++++++++++++++-------
 .../jbang/core/commands/mcp/ExampleToolsTest.java  | 70 ++++++++++++++++++++++
 5 files changed, 209 insertions(+), 49 deletions(-)

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 ca4b617cc884..907b64c77299 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
@@ -942,39 +942,71 @@ public final class ToolRegistry {
 
     private static void registerExampleTools() {
         register(tool("list_examples",
-                "List available Camel CLI examples. Returns name, title, 
description, difficulty level, and tags.")
+                "List the Camel CLI examples, grouped as the ladder of the 
examples (quick-start, run, transform, "
+                                       + "route, fail-well, connect, 
connect-service, contracts, ai, cloud, showcase). "
+                                       + "Returns the groups (level, title, 
intro) and the examples in reading order with "
+                                       + "name, title, description, level, 
order, tags, what they teach, the infra "
+                                       + "services they need, whether they are 
bundled and their files. "
+                                       + "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",
-                        "Filter by difficulty: beginner, intermediate, or 
advanced", false)
+                        "Only the examples of one group: quick-start, run, 
transform, route, fail-well, connect, "
+                                          + "connect-service, contracts, ai, 
cloud or showcase",
+                        false)
+                .param("limit", "integer", "Maximum number of examples to 
return (default: 50)", false)
                 .executor((ctx, args) -> {
                     String filter = args.get("filter");
                     String level = args.get("level");
+                    int limit = 50;
+                    String limitArg = args.get("limit");
+                    if (limitArg != null && !limitArg.isBlank()) {
+                        try {
+                            limit = Integer.parseInt(limitArg.trim());
+                        } catch (NumberFormatException e) {
+                            throw new ToolExecutionException("limit must be a 
number: " + limitArg);
+                        }
+                    }
                     List<JsonObject> catalog2 = ExampleHelper.loadCatalog();
                     List<JsonObject> filtered = 
ExampleHelper.filterExamples(catalog2, filter);
+                    List<JsonObject> groups = new ArrayList<>();
                     List<JsonObject> results = new ArrayList<>();
-                    for (JsonObject entry : filtered) {
-                        if (level != null && !level.isBlank()) {
-                            String entryLevel = entry.getString("level");
-                            if (entryLevel == null || 
!entryLevel.equalsIgnoreCase(level)) {
+                    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();
+                        JsonObject g = new JsonObject();
+                        g.put("level", group.getKey());
+                        g.put("title", 
ExampleHelper.getGroupTitle(group.getKey()));
+                        g.put("intro", 
ExampleHelper.getGroupIntro(group.getKey()));
+                        g.put("count", group.getValue().size());
+                        groups.add(g);
+                        for (JsonObject entry : group.getValue()) {
+                            if (results.size() >= limit) {
                                 continue;
                             }
-                        }
-                        JsonObject jo = new JsonObject();
-                        jo.put("name", entry.getString("name"));
-                        jo.put("title", entry.getString("title"));
-                        jo.put("description", entry.getString("description"));
-                        jo.put("level", entry.getString("level"));
-                        jo.put("tags", entry.get("tags"));
-                        jo.put("bundled", ExampleHelper.isBundled(entry));
-                        jo.put("files", ExampleHelper.getFiles(entry));
-                        results.add(jo);
-                        if (results.size() >= 20) {
-                            break;
+                            JsonObject jo = new JsonObject();
+                            jo.put("name", entry.getString("name"));
+                            jo.put("title", entry.getString("title"));
+                            jo.put("description", 
entry.getString("description"));
+                            jo.put("level", entry.getString("level"));
+                            if (ExampleHelper.getOrder(entry) != 
Integer.MAX_VALUE) {
+                                jo.put("order", ExampleHelper.getOrder(entry));
+                            }
+                            jo.put("tags", entry.get("tags"));
+                            jo.put("teaches", ExampleHelper.getTeaches(entry));
+                            jo.put("infraServices", 
ExampleHelper.getInfraServices(entry));
+                            jo.put("bundled", ExampleHelper.isBundled(entry));
+                            jo.put("files", ExampleHelper.getFiles(entry));
+                            results.add(jo);
                         }
                     }
                     JsonObject response = new JsonObject();
                     response.put("count", results.size());
+                    response.put("total", total);
+                    response.put("groups", groups);
                     response.put("examples", results);
                     return response.toJson();
                 }));
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 bfdc7cb4aafc..febdca5fd094 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
@@ -255,14 +255,15 @@ public final class ExampleHelper {
     }
 
     /**
-     * What the example teaches, as one line: the components and the EIPs from 
its metadata, or an empty string.
+     * What the example teaches from its metadata: the components, EIPs, 
languages and data formats, each as a list of
+     * names in that order; keys without names are left out, so an example 
without metadata gives an empty map.
      */
-    public static String getTeachesSummary(JsonObject entry) {
+    public static Map<String, List<String>> getTeaches(JsonObject entry) {
+        Map<String, List<String>> answer = new LinkedHashMap<>();
         JsonObject teaches = entry.getMap("teaches");
         if (teaches == null || teaches.isEmpty()) {
-            return "";
+            return answer;
         }
-        StringBuilder sb = new StringBuilder();
         for (String key : new String[] { "components", "eips", "languages", 
"dataformats" }) {
             // the catalog holds string arrays here; anything else in a 
hand-edited metadata file is skipped
             if (!(teaches.get(key) instanceof Collection<?> values) || 
values.isEmpty()) {
@@ -275,11 +276,22 @@ public final class ExampleHelper {
                 }
             }
             if (!names.isEmpty()) {
-                if (sb.length() > 0) {
-                    sb.append(" · ");
-                }
-                sb.append(key).append(": ").append(String.join(", ", names));
+                answer.put(key, names);
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * What the example teaches, as one line: the components and the EIPs from 
its metadata, or an empty string.
+     */
+    public static String getTeachesSummary(JsonObject entry) {
+        StringBuilder sb = new StringBuilder();
+        for (Map.Entry<String, List<String>> e : getTeaches(entry).entrySet()) 
{
+            if (sb.length() > 0) {
+                sb.append(" · ");
             }
+            sb.append(e.getKey()).append(": ").append(String.join(", ", 
e.getValue()));
         }
         return sb.toString();
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
index 514e2d77c0a9..e3aaf4c88b4e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
@@ -88,6 +88,31 @@ class ToolRegistryTest {
         assertTrue(json.contains("split"), "Should find split EIP");
     }
 
+    @Test
+    void listExamplesGroupsTheLadderWithoutArguments() {
+        ToolContext ctx = new ToolContext();
+        String json = ToolRegistry.execute("list_examples", ctx, 
Map.of()).toString();
+        assertTrue(json.contains("\"groups\""), "Should return the groups");
+        assertTrue(json.contains("\"level\":\"quick-start\""), "Should start 
with the quick-start group");
+        assertTrue(json.contains("timer-log"), "Should list the examples");
+        assertTrue(json.contains("\"teaches\""), "Should tell what the 
examples teach");
+        // more than the old cap of 20 examples
+        assertTrue(json.indexOf("\"total\":") > 0);
+        String total = json.replaceAll(".*\"total\":(\\d+).*", "$1");
+        assertTrue(Integer.parseInt(total) > 20, "Should count all examples, 
got " + total);
+    }
+
+    @Test
+    void listExamplesFiltersOneGroupAndLimits() {
+        ToolContext ctx = new ToolContext();
+        String json = ToolRegistry.execute("list_examples", ctx, 
Map.of("level", "run", "limit", "1")).toString();
+        assertTrue(json.contains("\"count\":1"), "Should honour the limit: " + 
json);
+        assertTrue(json.contains("\"level\":\"run\""));
+        assertFalse(json.contains("\"level\":\"quick-start\""), "Should only 
return the run group");
+        assertThrows(ToolExecutionException.class,
+                () -> ToolRegistry.execute("list_examples", ctx, 
Map.of("limit", "many")));
+    }
+
     @Test
     void runtimeToolThrowsWithoutProcess() {
         ToolContext ctx = new ToolContext();
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 c2c2d3d5c6e6..7811acb5a0df 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
@@ -20,6 +20,7 @@ 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;
 
@@ -38,38 +39,51 @@ import org.apache.camel.util.json.JsonObject;
 public class ExampleTools {
 
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
-          description = "List available Camel CLI examples. "
-                        + "Returns name, title, description, difficulty level, 
and tags. "
-                        + "Use filter to search by name, description, or tag. "
-                        + "Use level to filter by difficulty (beginner, 
intermediate, advanced).")
+          description = "List the Camel CLI examples, grouped as the ladder of 
the examples: "
+                        + "quick-start, run, transform, route, fail-well, 
connect, connect-service, contracts, ai, "
+                        + "cloud and showcase, in that reading order. Returns 
the groups (level, title, introduction) "
+                        + "and the examples in reading order with name, title, 
description, level, order, tags, "
+                        + "what they teach (components, EIPs, languages, data 
formats), the infra services they need "
+                        + "(start them with camel infra run), whether they are 
bundled and their files. "
+                        + "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(
-            @ToolArg(description = "Filter examples by name, description, or 
tag (case-insensitive substring match)") String filter,
-            @ToolArg(description = "Filter by difficulty level: beginner, 
intermediate, or advanced") String level,
-            @ToolArg(description = "Maximum number of results to return 
(default: 50)") Integer limit) {
+            @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 = "Maximum number of examples to return 
(default: 50)",
+                     required = false) Integer limit) {
 
-        int maxResults = limit != null ? limit : 50;
+        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<>();
-            for (JsonObject entry : filtered) {
-                if (level != null && !level.isBlank()) {
-                    String entryLevel = entry.getString("level");
-                    if (entryLevel == null || 
!entryLevel.equalsIgnoreCase(level)) {
-                        continue;
-                    }
+            int total = 0;
+            for (Map.Entry<String, List<JsonObject>> group : 
ExampleHelper.groupByLevel(filtered).entrySet()) {
+                if (level != null && !level.isBlank() && 
!group.getKey().equalsIgnoreCase(level)) {
+                    continue;
                 }
-
-                result.add(toExampleInfo(entry));
-
-                if (result.size() >= maxResults) {
-                    break;
+                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(), result);
+            return new ExampleListResult(result.size(), total, groups, result);
         } catch (Throwable e) {
             throw new ToolCallException(
                     "Failed to list examples (" + e.getClass().getName() + "): 
" + e.getMessage(), null);
@@ -136,7 +150,10 @@ public class ExampleTools {
                 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.isBundled(entry),
                 ExampleHelper.requiresDocker(entry),
                 ExampleHelper.getFiles(entry));
@@ -144,11 +161,15 @@ public class ExampleTools {
 
     // Result records
 
-    public record ExampleListResult(int count, List<ExampleInfo> examples) {
+    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,
-            List<String> tags, boolean bundled, boolean requiresDocker, 
List<String> files) {
+    public record ExampleInfo(String name, String title, String description, 
String level, Integer order,
+            List<String> tags, Map<String, List<String>> teaches, List<String> 
infraServices,
+            boolean bundled, boolean requiresDocker, List<String> files) {
     }
 
     public record ExampleFileResult(String example, String file, String 
content, String githubUrl) {
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
new file mode 100644
index 000000000000..9bb61e1d7e6f
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleToolsTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.mcp;
+
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.common.ExampleHelper;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ExampleToolsTest {
+
+    @Test
+    void listsTheWholeLadderInReadingOrderWithoutArguments() {
+        ExampleTools.ExampleListResult 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(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();
+
+        // the examples follow the groups in order
+        List<String> exampleLevels = 
all.examples().stream().map(ExampleTools.ExampleInfo::level).distinct().toList();
+        assertThat(exampleLevels).isEqualTo(levels);
+        
assertThat(all.examples().stream().map(ExampleTools.ExampleInfo::name)).contains("quick-start/timer-log");
+    }
+
+    @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((a, b) -> 
Integer.compare(a.order(), b.order()));
+        assertThat(run.examples()).anyMatch(e -> !e.teaches().isEmpty());
+    }
+
+    @Test
+    void searchesByNameAndHonoursTheLimit() {
+        ExampleTools.ExampleListResult kafka = new 
ExampleTools().camel_catalog_examples("kafka", null, 1);
+
+        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");
+    }
+}

Reply via email to