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 ef06db9cf371 CAMEL-24720: camel-jbang - camel_catalog_sample returns 
samples for components, data formats and languages
ef06db9cf371 is described below

commit ef06db9cf371f2c416b3f31646fe49b6c107ed26
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Sep 17 20:51:08 2026 +0200

    CAMEL-24720: camel-jbang - camel_catalog_sample returns samples for 
components, data formats and languages
    
    camel_catalog_sample served the EIPs and file entries only; an agent
    asking for kafka, csv or jq got "No sample". A component, data format
    or language name now answers from the documentation of the catalog in
    use, the way the EIPs already did.
    
    Components resolve their page and sub-pages, or the page of their
    artifact (smtp on the mail page, https on http), with the examples
    using the component's endpoint ranked first and placement saying from:
    or to:. Data formats resolve by page, artifact or major-version
    variant (jackson on jackson2/jackson3), languages by page. Without
    kind, resolution goes EIPs and file entries, then catalog names, then
    EIP aliases; a name in several kinds returns the kinds to choose from,
    and the new kind argument picks one. Only schema-valid YAML examples
    are returned; a page without an endpoint example carries a note, a
    page without route examples a hint to camel_catalog_doc. A page that
    cannot be read or validated is logged at debug.
    
    Two behavior changes: an exact catalog name beats a fuzzy EIP alias
    (chunk is the component), and the timer intent is removed so the timer
    component answers.
    
    CatalogSamplesTest gains 7 tests; the MCP page documents the kind
    argument.
    
    Closes #26562
    
    Co-Authored-By: Claude <[email protected]>
---
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |  23 +-
 .../dsl/jbang/core/commands/ai/AuthoringTools.java |  17 +-
 .../dsl/jbang/core/commands/ai/CatalogSamples.java | 560 ++++++++++++++++++---
 .../jbang/core/commands/ai/CatalogSamplesTest.java | 165 +++++-
 .../jbang/core/commands/mcp/AuthoringTools.java    |  15 +-
 5 files changed, 681 insertions(+), 99 deletions(-)

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 99637a52a42b..bb95dd07da4a 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -297,15 +297,20 @@ project `directory` as an argument, the runtime tools 
take the integration `name
   `AggregationStrategy` lists the built-in implementations.
 
 | `camel_catalog_sample`
-| Validated YAML DSL samples of an EIP or file entry (`onException`, 
`aggregate`, `split`, `rest`, `beans`) taken
-  from the documentation examples, with where it goes: a top-level entry next 
to the route, or a step inside
-  it. The name can be kebab-case, a part of another EIP (`doCatch`, `when`, 
`onFallback` show the whole
-  construct), an alias of the EIP (`fan-out`, `rate-limit`, from the catalog's 
EIP models) or what to do
-  (`read file`, `call service`, `retry`, `batch`). The EIP samples come from 
the
-  documentation of the catalog in use, so they follow its Camel version; the 
file entries come from the user
-  manual examples that the build validates (`generate-doc-samples` in
-  xref:camel-yaml-dsl-validator-maven-plugin.adoc[]). `limit` is 2 by default, 
at most 5.
-  Use it before writing an EIP the first time, or after a `not defined in the 
schema` validation error.
+| Validated YAML DSL samples of an EIP or file entry (`onException`, 
`aggregate`, `split`, `rest`, `beans`), a
+  component (`kafka`, `file`), a data format (`csv`) or a language (`jq`) 
taken from the documentation
+  examples, with where it goes: a top-level entry next to the route, a step 
inside it, an endpoint uri in
+  `from:` or `to:` (the component's consumer or producer side), a 
`marshal`/`unmarshal` step, or the
+  expression of a step. The name can be kebab-case, a part of another EIP 
(`doCatch`, `when`, `onFallback`
+  show the whole construct), an alias of the EIP (`fan-out`, `rate-limit`, 
from the catalog's EIP models) or
+  what to do (`read file`, `call service`, `retry`, `batch`); `kind` (`eip`, 
`component`, `dataformat`,
+  `language`) is only needed for a name that is in several kinds (`avro`, 
`file`), otherwise the answer names
+  the kinds to choose from. The samples come from the documentation of the 
catalog in use, so they follow its
+  Camel version: the EIP page, the component page and its sub-pages (the 
examples using the component's
+  endpoint first), the data format or language page. The file entries come 
from the user manual examples that
+  the build validates (`generate-doc-samples` in 
xref:camel-yaml-dsl-validator-maven-plugin.adoc[]).
+  `limit` is 2 by default, at most 5. Use it before writing one the first 
time, or after a
+  `not defined in the schema` validation error.
 
 | `camel_validate_source`
 | Validates Camel YAML DSL or `.properties` source without writing: the YAML 
DSL schema (a misspelled option
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
index fcb10cf3b2ec..3a841b26faa5 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
@@ -123,16 +123,23 @@ public final class AuthoringTools {
                 }));
 
         registry.accept(tool("camel_catalog_sample",
-                "A validated YAML DSL sample of an EIP or file entry 
(onException, aggregate, split, rest, beans) from "
-                                                     + "the docs, with where 
it goes (a top-level entry or a step). Use before "
-                                                     + "writing an EIP the 
first time or after a 'not defined in the schema' error.")
-                .param("name", "string", "EIP or entry name, or what to do 
(read file, call service, retry, batch)", true)
+                "A validated YAML DSL sample of an EIP or file entry 
(onException, aggregate, split, rest, beans), a "
+                                                     + "component (kafka, 
file), a data format (csv) or a language (jq) from the "
+                                                     + "docs, with where it 
goes (a top-level entry, a step, an endpoint uri, a "
+                                                     + "marshal step, an 
expression). Use before writing one the first time or "
+                                                     + "after a 'not defined 
in the schema' error.")
+                .param("name", "string",
+                        "EIP, component, data format or language name, or what 
to do (read file, call service, retry, batch)",
+                        true)
+                .param("kind", "string", "eip, component, dataformat or 
language; needed only when a name is in several "
+                                         + "(avro, file)",
+                        false)
                 .param("limit", "integer", "Maximum samples to return (default 
2, max 5)", false)
                 .param("camelVersion", "string", VERSION_DESC, false)
                 .core(true)
                 .executor((ctx, args) -> {
                     applyVersion(ctx, args);
-                    return CatalogSamples.sample(ctx.catalog(), 
args.get("name"),
+                    return CatalogSamples.sample(ctx.catalog(), 
args.get("kind"), args.get("name"),
                             integer(args, "limit", 
CatalogSamples.DEFAULT_LIMIT));
                 }));
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamples.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamples.java
index 71c4e4f20cf0..baeaffe09c3d 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamples.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamples.java
@@ -20,34 +20,56 @@ import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.tooling.model.ComponentModel;
+import org.apache.camel.tooling.model.DataFormatModel;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.util.json.Jsoner;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Validated Camel YAML DSL samples per EIP (and per top-level entry such as 
onException, rest or routeConfiguration),
- * taken from the YAML examples in the Camel documentation, with where each 
goes in a file.
+ * per component, data format and language, taken from the YAML examples in 
the Camel documentation, with where each
+ * goes in a file.
  * <p>
- * The EIP samples are read from the catalog's documentation at lookup time, 
so they follow the Camel version in use.
- * The shipped {@code eip-samples.json} is the fallback (the user manual pages 
the catalog does not bundle, and the EIP
- * pages when the catalog has none): the build generates it from the 
documentation with the
- * {@code camel-yaml-dsl-validator:generate-doc-samples} goal, which fails 
when an example does not validate.
+ * The samples are read from the catalog's documentation at lookup time, so 
they follow the Camel version in use: the
+ * EIP page, or the component page and its sub-pages (the consumer, producer 
or examples pages of a component), the data
+ * format page or the language page. The shipped {@code eip-samples.json} is 
the fallback for the EIPs and the file
+ * entries (the user manual pages the catalog does not bundle, and the EIP 
pages when the catalog has none): the build
+ * generates it from the documentation with the {@code 
camel-yaml-dsl-validator:generate-doc-samples} goal, which fails
+ * when an example does not validate. The component, data format and language 
examples have no shipped fallback: they
+ * come from the catalog in use only, which is the point of asking for a 
version.
  */
 public final class CatalogSamples {
 
+    private static final Logger LOG = 
LoggerFactory.getLogger(CatalogSamples.class);
+
     private static final String RESOURCE = 
"org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json";
     public static final int DEFAULT_LIMIT = 2;
     public static final int MAX_LIMIT = 5;
 
+    /** The kinds a sample can be asked for; eip covers the file entries 
(onException, rest, beans) too. */
+    static final List<String> KINDS = List.of("eip", "component", 
"dataformat", "language");
+
+    private static final List<String> CATALOG_KINDS = List.of("component", 
"dataformat", "language");
+
+    /** The suffixes of the catalog pages of the four families; the other 
pages are sub-pages or manual pages. */
+    private static final List<String> FAMILY_SUFFIXES = List.of("-component", 
"-dataformat", "-language", "-eip");
+
     /** The entries that go at the top of a YAML file, as list items next to 
the route, not inside it. */
     static final Set<String> TOP_LEVEL = Set.of("beans", "dataFormats", 
"errorHandler", "from", "intercept",
             "interceptFrom", "interceptSendToEndpoint", "onCompletion", 
"onException", "rest", "restConfiguration",
@@ -73,6 +95,7 @@ public final class CatalogSamples {
      * What a request is about, in the words a person or a model uses, to the 
EIP that does it. The names and aliases of
      * the EIPs themselves (fan-out, dedup, rate-limit) come from the 
catalog's EIP models, so only what is not an alias
      * is here: a task (read file, call service), a wording of the outcome 
(retry, batch) or a technology (json, cron).
+     * A protocol or a product (mqtt, s3) is what camel_catalog_find turns 
into a component, so it is not here.
      */
     static final Map<String, String> INTENTS = Map.ofEntries(
             Map.entry("read file", "poll"), Map.entry("readfile", "poll"), 
Map.entry("load file", "poll"),
@@ -87,7 +110,7 @@ public final class CatalogSamples {
             Map.entry("parallel", "multicast"), Map.entry("resilience", 
"circuitBreaker"),
             Map.entry("rest api", "rest"), Map.entry("http server", "rest"), 
Map.entry("endpoint", "rest"),
             Map.entry("convert", "convertBodyTo"), Map.entry("json", 
"marshal"),
-            Map.entry("timer", "from"), Map.entry("schedule", "from"), 
Map.entry("cron", "from"));
+            Map.entry("schedule", "from"), Map.entry("cron", "from"));
 
     private static volatile Map<String, List<Map<String, String>>> samples;
 
@@ -125,7 +148,7 @@ public final class CatalogSamples {
         return answer;
     }
 
-    /** The names with samples, sorted. */
+    /** The names with shipped samples (the EIPs and the file entries), 
sorted. */
     public static List<String> names() {
         return new ArrayList<>(samples().keySet());
     }
@@ -134,57 +157,212 @@ public final class CatalogSamples {
      * Samples for the given EIP or entry name (camelCase, kebab-case or any 
case), with where it goes.
      */
     public static JsonObject sample(String name, int limit) {
-        return sample(null, name, limit);
+        return sample(null, null, name, limit);
+    }
+
+    /**
+     * Samples for the given name of any kind, with where it goes; from the 
catalog's documentation for the Camel
+     * version in use when it has the page, else the shipped set.
+     */
+    public static JsonObject sample(CamelCatalog catalog, String name, int 
limit) {
+        return sample(catalog, null, name, limit);
     }
 
     private static final Pattern YAML_BLOCK = 
Pattern.compile("\\[source,yaml\\]\\s*\\n----\\n(.*?)\\n----", Pattern.DOTALL);
-    private static final Map<String, List<Map<String, String>>> DOC_CACHE = 
new java.util.concurrent.ConcurrentHashMap<>();
+    private static final Map<String, Found> DOC_CACHE = new 
ConcurrentHashMap<>();
+
+    /** The validated examples of a page family, and for a component whether 
any of them uses its endpoint. */
+    record Found(List<Map<String, String>> samples, boolean endpoint) {
+        static final Found NONE = new Found(List.of(), false);
+    }
 
     /**
-     * The validated YAML examples of the EIP page in the catalog, or an empty 
list when the catalog has no such page or
-     * none of its examples validate. Cached per name for the default catalog.
+     * The validated YAML examples of the documentation of the given kind and 
name in the catalog, or none when the
+     * catalog has no such page or none of its examples validate. For an EIP 
the page named after it
+     * (circuitBreaker-eip, pollEnrich-eip; a few are kebab-case, and the 
pattern pages have no suffix:
+     * dead-letter-channel, intercept); for a component its page and 
sub-pages, the examples using its endpoint first;
+     * for a data format or a language its page. Cached per kind and name for 
the default catalog.
      */
-    static List<Map<String, String>> fromCatalog(CamelCatalog catalog, String 
key) {
+    static Found fromCatalog(CamelCatalog catalog, String kind, String key) {
         if (catalog == null) {
-            return List.of();
+            return Found.NONE;
         }
-        // the catalog names the pages after the EIP (circuitBreaker-eip, 
pollEnrich-eip); a few are kebab-case, and the
-        // pattern pages have no suffix (dead-letter-channel, intercept)
         boolean cacheable = catalog.getCatalogVersion() != null && 
catalog.getLoadedVersion() == null;
-        if (cacheable && DOC_CACHE.containsKey(key)) {
-            return DOC_CACHE.get(key);
+        String cacheKey = kind + ":" + key;
+        if (cacheable) {
+            Found cached = DOC_CACHE.get(cacheKey);
+            if (cached != null) {
+                return cached;
+            }
         }
-        List<Map<String, String>> answer = new ArrayList<>();
+        Found found;
         try {
-            String page = null;
-            String doc = null;
-            for (String candidate : List.of(key + "-eip", kebab(key) + "-eip", 
kebab(key), key)) {
-                doc = catalog.asciiDoc(candidate);
-                if (doc != null) {
-                    page = candidate;
+            found = switch (kind) {
+                case "component" -> componentSamples(catalog, key);
+                case "dataformat" -> pageSamples(catalog, 
dataFormatPages(catalog, key), null);
+                case "language" -> pageSamples(catalog, List.of(key + 
"-language"), null);
+                default -> eipSamples(catalog, key);
+            };
+        } catch (Exception e) {
+            // a page that cannot be read or validated: the shipped samples 
are the fallback, and the answer says the
+            // page has no example; the cause is in the debug log
+            LOG.debug("Cannot read the {} samples of {} from the catalog 
documentation", kind, key, e);
+            found = Found.NONE;
+        }
+        if (cacheable) {
+            DOC_CACHE.put(cacheKey, found);
+        }
+        return found;
+    }
+
+    private static Found eipSamples(CamelCatalog catalog, String key) {
+        for (String candidate : List.of(key + "-eip", kebab(key) + "-eip", 
kebab(key), key)) {
+            if (catalog.asciiDoc(candidate) != null) {
+                return pageSamples(catalog, List.of(candidate), null);
+            }
+        }
+        return Found.NONE;
+    }
+
+    private static Found componentSamples(CamelCatalog catalog, String name) {
+        String page = componentPage(catalog, name);
+        if (page == null) {
+            return Found.NONE;
+        }
+        List<String> pages = new ArrayList<>();
+        pages.add(page + "-component");
+        pages.addAll(subPages(catalog, page));
+        return pageSamples(catalog, pages, schemePattern(schemesOf(catalog, 
name, page)));
+    }
+
+    /**
+     * The validated route examples of the pages, in page order; with a scheme 
pattern the examples that use the
+     * endpoint come first.
+     */
+    private static Found pageSamples(CamelCatalog catalog, List<String> pages, 
Pattern scheme) {
+        List<Map<String, String>> uses = new ArrayList<>();
+        List<Map<String, String>> others = new ArrayList<>();
+        for (String page : pages) {
+            String doc = catalog.asciiDoc(page);
+            if (doc == null) {
+                continue;
+            }
+            Matcher m = YAML_BLOCK.matcher(doc);
+            while (m.find()) {
+                String yaml = m.group(1).stripTrailing() + "\n";
+                if (!yaml.stripLeading().startsWith("- ")) {
+                    // a fragment (an option list, a manifest), not a route 
file
+                    continue;
+                }
+                if (!SourceValidator.validateYamlSchema(yaml, 
catalog).isEmpty()) {
+                    continue;
+                }
+                Map<String, String> sample
+                        = Map.of("source", page + ".adoc (Camel " + 
catalog.getCatalogVersion() + ")", "yaml", yaml);
+                if (scheme != null && scheme.matcher(yaml).find()) {
+                    uses.add(sample);
+                } else {
+                    others.add(sample);
+                }
+            }
+        }
+        List<Map<String, String>> all = new ArrayList<>(uses);
+        all.addAll(others);
+        return new Found(List.copyOf(all), !uses.isEmpty());
+    }
+
+    /**
+     * The base name of the documentation page of a component: its own name, 
or the page of its artifact when the
+     * component shares a page with its siblings (smtp, imap and pop3 are on 
the mail page, https on the http page).
+     */
+    static String componentPage(CamelCatalog catalog, String name) {
+        if (catalog.asciiDoc(name + "-component") != null) {
+            return name;
+        }
+        ComponentModel model = componentModel(catalog, name);
+        String artifactId = model != null ? model.getArtifactId() : null;
+        if (artifactId != null && artifactId.startsWith("camel-")) {
+            String page = artifactId.substring("camel-".length());
+            if (catalog.asciiDoc(page + "-component") != null) {
+                return page;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * The sub-pages of a component page (aws2-s3-consumer-examples, 
salesforce-streaming, platform-http-vertx): the
+     * catalog pages named with the component's prefix that are not a page of 
one of the four families, nor of another
+     * component with a longer name (mina-sftp-authentication is not a page of 
mina).
+     */
+    static List<String> subPages(CamelCatalog catalog, String page) {
+        List<String> out = new ArrayList<>();
+        String prefix = page + "-";
+        List<String> components = catalog.findComponentNames();
+        for (String doc : catalog.findDocNames()) {
+            if (!doc.startsWith(prefix) || 
FAMILY_SUFFIXES.stream().anyMatch(doc::endsWith)) {
+                continue;
+            }
+            boolean other = false;
+            for (String c : components) {
+                if (c.length() > page.length() && c.startsWith(prefix) && 
(doc.equals(c) || doc.startsWith(c + "-"))) {
+                    other = true;
                     break;
                 }
             }
-            if (doc != null) {
-                Matcher m = YAML_BLOCK.matcher(doc);
-                while (m.find()) {
-                    String yaml = m.group(1).stripTrailing() + "\n";
-                    if (!yaml.stripLeading().startsWith("- ")) {
-                        continue;
-                    }
-                    if (SourceValidator.validateYamlSchema(yaml, 
catalog).isEmpty()) {
-                        answer.add(Map.of("source", page + ".adoc (Camel " + 
catalog.getCatalogVersion() + ")", "yaml", yaml));
-                    }
+            if (!other) {
+                out.add(doc);
+            }
+        }
+        Collections.sort(out);
+        return out;
+    }
+
+    /**
+     * The schemes an example of the component would use in an endpoint uri: 
the component's own and its alternatives
+     * (smtp, smtps, imap...), or for a page that groups components the 
schemes of the components of its artifact.
+     */
+    private static Set<String> schemesOf(CamelCatalog catalog, String name, 
String page) {
+        Set<String> schemes = new LinkedHashSet<>();
+        ComponentModel model = componentModel(catalog, name);
+        if (model != null) {
+            addSchemes(schemes, model);
+        } else {
+            for (String c : catalog.findComponentNames()) {
+                ComponentModel m = catalog.componentModel(c);
+                if (m != null && ("camel-" + page).equals(m.getArtifactId())) {
+                    addSchemes(schemes, m);
                 }
             }
-        } catch (Exception e) {
-            // a page that cannot be read or validated: the shipped samples 
are the fallback
         }
-        List<Map<String, String>> result = List.copyOf(answer);
-        if (cacheable) {
-            DOC_CACHE.put(key, result);
+        if (schemes.isEmpty()) {
+            schemes.add(name);
         }
-        return result;
+        return schemes;
+    }
+
+    private static void addSchemes(Set<String> schemes, ComponentModel model) {
+        if (model.getScheme() != null) {
+            schemes.add(model.getScheme());
+        }
+        String alternatives = model.getAlternativeSchemes();
+        if (alternatives != null) {
+            for (String s : alternatives.split(",")) {
+                if (!s.isBlank()) {
+                    schemes.add(s.trim());
+                }
+            }
+        }
+    }
+
+    /** Matches an endpoint uri of one of the schemes: {@code uri: kafka:...}, 
{@code uri: "kafka:..."}. */
+    private static Pattern schemePattern(Set<String> schemes) {
+        String any = 
schemes.stream().map(Pattern::quote).collect(Collectors.joining("|"));
+        return Pattern.compile("(uri:\\s*[\"']?|[\"'])(" + any + "):");
+    }
+
+    private static ComponentModel componentModel(CamelCatalog catalog, String 
name) {
+        return catalog.findComponentNames().contains(name) ? 
catalog.componentModel(name) : null;
     }
 
     static String kebab(String camel) {
@@ -200,35 +378,99 @@ public final class CatalogSamples {
     }
 
     /**
-     * Samples for the given EIP or entry name (camelCase, kebab-case, any 
case, or what to do), with where it goes;
-     * from the catalog's documentation for the Camel version in use when it 
has the page, else the shipped set.
+     * Samples for the given name, with where it goes: an EIP or entry name 
(camelCase, kebab-case, any case, or what to
+     * do), a component, a data format or a language. Without a kind the EIPs 
and file entries are looked up first, then
+     * the catalog's components, data formats and languages, then the aliases 
of the EIPs; a name that is in several
+     * kinds with samples returns the kinds to choose from. The samples come 
from the catalog's documentation for the
+     * Camel version in use when it has the page, else (EIPs and file entries) 
the shipped set.
+     *
+     * @param catalog the catalog of the Camel version to answer for, or null 
for the shipped samples only
+     * @param kind    eip, component, dataformat or language, or null to 
resolve the name across the kinds
+     * @param name    the name, or what to do
+     * @param limit   the maximum number of samples, at most {@link #MAX_LIMIT}
      */
-    public static JsonObject sample(CamelCatalog catalog, String name, int 
limit) {
+    public static JsonObject sample(CamelCatalog catalog, String kind, String 
name, int limit) {
         JsonObject answer = new JsonObject();
         String given = name != null ? name.trim() : "";
         if (given.isEmpty()) {
-            answer.put("error", "name is required, e.g. onException, 
aggregate, circuitBreaker, split, rest");
+            answer.put("error", "name is required, e.g. onException, 
aggregate, circuitBreaker, split, rest, kafka, csv, jq");
+            return answer;
+        }
+        String wanted = normalizeKind(kind);
+        if (wanted != null && !KINDS.contains(wanted)) {
+            answer.put("error", "kind must be one of " + String.join(", ", 
KINDS) + ", got '" + kind + "'");
             return answer;
         }
         int max = Math.max(1, Math.min(MAX_LIMIT, limit <= 0 ? DEFAULT_LIMIT : 
limit));
-        String key = resolve(catalog, given);
-        if (key == null) {
-            answer.put("error", "No sample for '" + given + "'");
-            answer.put("suggestions", new 
JsonArray(suggest(normalize(given))));
-            // the name may still be an EIP (dedup is idempotentConsumer) 
whose page has no YAML example
-            List<String> eips = catalog != null ? 
catalog.suggestEipNames(given, 1) : List.of();
-            if (!eips.isEmpty()) {
-                answer.put("eip", eips.get(0));
-                answer.put("hint", "'" + given + "' is the " + eips.get(0) + " 
EIP, which has no YAML sample; "
-                                   + "camel_catalog_doc gives its options");
-            } else {
-                answer.put("hint", "camel_catalog_doc gives the options of an 
EIP; the sample tool covers the EIPs and file "
-                                   + "entries documented with YAML examples");
+        boolean eips = wanted == null || wanted.equals("eip");
+
+        // an EIP or a file entry by its name, a part of it, or what to do
+        if (eips) {
+            String key = resolveExact(given);
+            if (key != null) {
+                return eipAnswer(catalog, given, key, max, wanted == null ? 
alsoKinds(catalog, given) : List.of());
             }
-            return answer;
         }
+        // a component, data format or language by its name
+        if (catalog != null && !"eip".equals(wanted)) {
+            Map<String, String> kinds = new TreeMap<>();
+            for (String k : wanted != null ? List.of(wanted) : CATALOG_KINDS) {
+                String canonical = canonical(catalog, k, given);
+                if (canonical != null) {
+                    kinds.put(k, canonical);
+                }
+            }
+            if (kinds.size() > 1) {
+                // the kinds with samples decide (file is a component with 
examples and a language without)
+                Map<String, String> withSamples = new TreeMap<>();
+                kinds.forEach((k, canonical) -> {
+                    if (!fromCatalog(catalog, k, 
canonical).samples().isEmpty()) {
+                        withSamples.put(k, canonical);
+                    }
+                });
+                if (!withSamples.isEmpty()) {
+                    kinds = withSamples;
+                }
+            }
+            if (kinds.size() > 1) {
+                answer.put("name", given);
+                answer.put("kinds", new JsonArray(new 
ArrayList<>(kinds.keySet())));
+                answer.put("hint", "'" + given + "' is a " + String.join(" and 
a ", kinds.keySet())
+                                   + "; ask again with kind");
+                return answer;
+            }
+            if (kinds.size() == 1) {
+                Map.Entry<String, String> only = 
kinds.entrySet().iterator().next();
+                return catalogAnswer(catalog, only.getKey(), only.getValue(), 
max);
+            }
+        }
+        // an alias of an EIP model (fan-out, dedup, rate-limit) or a word of 
its title
+        if (eips) {
+            String key = resolveAlias(catalog, given);
+            if (key != null) {
+                return eipAnswer(catalog, given, key, max, List.of());
+            }
+        }
+        answer.put("error", "No sample for '" + given + "'" + (wanted != null 
? " as a " + wanted : ""));
+        answer.put("suggestions", new JsonArray(suggestions(catalog, wanted, 
given)));
+        // the name may still be an EIP (dedup is idempotentConsumer) whose 
page has no YAML example
+        List<String> eipNames = catalog != null && eips ? 
catalog.suggestEipNames(given, 1) : List.of();
+        if (!eipNames.isEmpty()) {
+            answer.put("eip", eipNames.get(0));
+            answer.put("hint", "'" + given + "' is the " + eipNames.get(0) + " 
EIP, which has no YAML sample; "
+                               + "camel_catalog_doc gives its options");
+        } else {
+            answer.put("hint", "camel_catalog_doc gives the options of an EIP, 
component, data format or language; "
+                               + "the sample tool covers what the 
documentation shows with YAML examples");
+        }
+        return answer;
+    }
+
+    private static JsonObject eipAnswer(CamelCatalog catalog, String given, 
String key, int max, List<String> also) {
+        JsonObject answer = new JsonObject();
         String partOf = PART_OF.get(normalize(given));
         answer.put("name", key);
+        answer.put("kind", "eip");
         if (partOf != null) {
             answer.put("partOf", partOf);
             answer.put("note", given + " is a part of " + partOf + "; the 
sample shows it in place");
@@ -236,10 +478,37 @@ public final class CatalogSamples {
             answer.put("note", "'" + given + "' is done with the " + key + " 
EIP");
         }
         answer.put("placement", placement(key));
-        List<Map<String, String>> list = fromCatalog(catalog, key);
+        List<Map<String, String>> list = fromCatalog(catalog, "eip", 
key).samples();
         if (list.isEmpty()) {
             list = samples().getOrDefault(key, List.of());
         }
+        putSamples(answer, list, max);
+        if (!also.isEmpty()) {
+            answer.put("also", new JsonArray(also));
+            answer.put("hint", "'" + given + "' is also a " + String.join(" 
and a ", also)
+                               + "; ask with kind for that sample");
+        }
+        return answer;
+    }
+
+    private static JsonObject catalogAnswer(CamelCatalog catalog, String kind, 
String name, int max) {
+        JsonObject answer = new JsonObject();
+        answer.put("name", name);
+        answer.put("kind", kind);
+        answer.put("placement", placement(catalog, kind, name));
+        Found found = fromCatalog(catalog, kind, name);
+        putSamples(answer, found.samples(), max);
+        if (found.samples().isEmpty()) {
+            answer.put("hint", "the documentation of " + name + " has no YAML 
route example; camel_catalog_doc gives "
+                               + "its options" + ("component".equals(kind) ? " 
and the uri syntax" : ""));
+        } else if ("component".equals(kind) && !found.endpoint()) {
+            answer.put("note", "none of the examples of the documentation uses 
a " + name + " endpoint; they show the "
+                               + "component in another way (a properties 
function, a policy, a converter)");
+        }
+        return answer;
+    }
+
+    private static void putSamples(JsonObject answer, List<Map<String, 
String>> list, int max) {
         JsonArray arr = new JsonArray();
         for (Map<String, String> s : list.subList(0, Math.min(max, 
list.size()))) {
             JsonObject jo = new JsonObject();
@@ -249,7 +518,91 @@ public final class CatalogSamples {
         }
         answer.put("samples", arr);
         answer.put("count", list.size());
-        return answer;
+    }
+
+    /** The catalog kinds, other than the EIPs, the name has samples in. */
+    private static List<String> alsoKinds(CamelCatalog catalog, String given) {
+        List<String> also = new ArrayList<>();
+        if (catalog != null) {
+            for (String k : CATALOG_KINDS) {
+                String canonical = canonical(catalog, k, given);
+                if (canonical != null && !fromCatalog(catalog, k, 
canonical).samples().isEmpty()) {
+                    also.add(k);
+                }
+            }
+        }
+        return also;
+    }
+
+    /**
+     * The catalog name of the given kind the name stands for, when it has a 
documentation page: the name itself, or the
+     * name in another case; for a component also a page that groups 
components (mail).
+     */
+    static String canonical(CamelCatalog catalog, String kind, String given) {
+        List<String> names = switch (kind) {
+            case "component" -> catalog.findComponentNames();
+            case "dataformat" -> catalog.findDataFormatNames();
+            case "language" -> catalog.findLanguageNames();
+            default -> List.of();
+        };
+        String name = names.contains(given) ? given : null;
+        if (name == null) {
+            for (String n : names) {
+                if (n.equalsIgnoreCase(given)) {
+                    name = n;
+                    break;
+                }
+            }
+        }
+        if ("component".equals(kind)) {
+            if (name != null) {
+                return componentPage(catalog, name) != null ? name : null;
+            }
+            String page = given.toLowerCase(Locale.ROOT);
+            return catalog.asciiDoc(page + "-component") != null ? page : null;
+        }
+        if ("dataformat".equals(kind)) {
+            return name != null && !dataFormatPages(catalog, name).isEmpty() ? 
name : null;
+        }
+        return name != null && catalog.asciiDoc(name + "-" + kind) != null ? 
name : null;
+    }
+
+    /**
+     * The documentation pages of a data format: its own, the page of its 
artifact (bindyCsv, bindyFixed and bindyKvp
+     * are on the bindy page), or the pages of its variants by major version 
(jackson2 and jackson3 for jackson).
+     */
+    static List<String> dataFormatPages(CamelCatalog catalog, String name) {
+        if (catalog.asciiDoc(name + "-dataformat") != null) {
+            return List.of(name + "-dataformat");
+        }
+        DataFormatModel model = catalog.findDataFormatNames().contains(name) ? 
catalog.dataFormatModel(name) : null;
+        String artifactId = model != null ? model.getArtifactId() : null;
+        if (artifactId != null && artifactId.startsWith("camel-")) {
+            String page = artifactId.substring("camel-".length()) + 
"-dataformat";
+            if (catalog.asciiDoc(page) != null) {
+                return List.of(page);
+            }
+        }
+        List<String> variants = new ArrayList<>();
+        Pattern variant = Pattern.compile(Pattern.quote(name) + 
"\\d+-dataformat");
+        for (String doc : catalog.findDocNames()) {
+            if (variant.matcher(doc).matches()) {
+                variants.add(doc);
+            }
+        }
+        Collections.sort(variants);
+        return variants;
+    }
+
+    static String normalizeKind(String kind) {
+        if (kind == null || kind.isBlank()) {
+            return null;
+        }
+        String k = kind.trim().toLowerCase(Locale.ROOT).replace("-", 
"").replace(" ", "").replace("_", "");
+        if (k.endsWith("s")) {
+            k = k.substring(0, k.length() - 1);
+        }
+        return k;
     }
 
     static String placement(String key) {
@@ -259,16 +612,32 @@ public final class CatalogSamples {
         return "a step inside the steps of a route (or of another EIP)";
     }
 
-    /** Resolves a user given name to a sample key: exact, case-insensitive, 
kebab-case, or a part of another EIP. */
-    static String resolve(String given) {
-        return resolve(null, given);
+    static String placement(CamelCatalog catalog, String kind, String name) {
+        switch (kind) {
+            case "component": {
+                ComponentModel model = componentModel(catalog, name);
+                if (model != null && model.isConsumerOnly()) {
+                    return "an endpoint uri in from: only (a consumer; it 
cannot be used in to:)";
+                }
+                if (model != null && model.isProducerOnly()) {
+                    return "an endpoint uri in to: only (a producer; it cannot 
be used in from:)";
+                }
+                return "an endpoint uri in from: (consumer) or to: (producer)";
+            }
+            case "dataformat":
+                return "a marshal or unmarshal step inside the steps of a 
route, with the data format and its options"
+                       + " under it";
+            case "language":
+                return "the expression of a step (setBody, setHeader, filter, 
when, split, ...), under expression:";
+            default:
+                return placement(name);
+        }
     }
 
     /**
-     * Resolves a user given name to a sample key: exact, case-insensitive, 
kebab-case, a part of another EIP, an
-     * intent, or with a catalog an alias of an EIP model (fan-out, dedup, 
rate-limit) or a word of its title.
+     * Resolves a user given name to a shipped sample key: exact, an intent, a 
part of another EIP, or case-insensitive.
      */
-    static String resolve(CamelCatalog catalog, String given) {
+    static String resolveExact(String given) {
         String n = normalize(given);
         if (samples().containsKey(n)) {
             return n;
@@ -286,16 +655,24 @@ public final class CatalogSamples {
                 return k;
             }
         }
-        if (catalog != null) {
-            // the aliases of the EIP models, then the EIP an aliased part 
belongs to (fallback is a part of circuitBreaker)
-            for (String eip : catalog.suggestEipNames(given, 3)) {
-                if (samples().containsKey(eip)) {
-                    return eip;
-                }
-                String whole = PART_OF.get(eip);
-                if (whole != null && samples().containsKey(whole)) {
-                    return whole;
-                }
+        return null;
+    }
+
+    /**
+     * Resolves a user given name through the catalog's EIP models: an alias 
(fan-out, dedup, rate-limit) or a word of
+     * the title, then the EIP an aliased part belongs to (fallback is a part 
of circuitBreaker).
+     */
+    static String resolveAlias(CamelCatalog catalog, String given) {
+        if (catalog == null) {
+            return null;
+        }
+        for (String eip : catalog.suggestEipNames(given, 3)) {
+            if (samples().containsKey(eip)) {
+                return eip;
+            }
+            String whole = PART_OF.get(eip);
+            if (whole != null && samples().containsKey(whole)) {
+                return whole;
             }
         }
         return null;
@@ -319,6 +696,37 @@ public final class CatalogSamples {
         return n;
     }
 
+    /**
+     * Names close to the given one: the shipped keys, and with a catalog its 
components, data formats and languages.
+     */
+    private static List<String> suggestions(CamelCatalog catalog, String 
wanted, String given) {
+        List<String> out = wanted == null || wanted.equals("eip") ? 
suggest(normalize(given)) : new ArrayList<>();
+        if (catalog != null) {
+            for (String kind : CATALOG_KINDS) {
+                if (wanted == null || wanted.equals(kind)) {
+                    suggestCatalog(out, catalog, kind, given, 
kind.equals("component") ? 3 : 2);
+                }
+            }
+        }
+        return out;
+    }
+
+    /** Adds the catalog names of the kind close to the given one, only those 
with a page to come back for. */
+    private static void suggestCatalog(List<String> out, CamelCatalog catalog, 
String kind, String given, int max) {
+        List<String> found = switch (kind) {
+            case "component" -> catalog.suggestComponentNames(given, max * 3);
+            case "dataformat" -> catalog.suggestDataFormatNames(given, max * 
3);
+            default -> catalog.suggestLanguageNames(given, max * 3);
+        };
+        int n = 0;
+        for (String s : found) {
+            if (n < max && canonical(catalog, kind, s) != null) {
+                out.add(s + " (" + kind + ")");
+                n++;
+            }
+        }
+    }
+
     static List<String> suggest(String n) {
         List<String> out = new ArrayList<>();
         String lower = n.toLowerCase(Locale.ROOT);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamplesTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamplesTest.java
index 6e4d0f503e02..8899f5644d75 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamplesTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamplesTest.java
@@ -17,7 +17,11 @@
 package org.apache.camel.dsl.jbang.core.commands.ai;
 
 import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 import org.junit.jupiter.api.Test;
@@ -61,12 +65,14 @@ public class CatalogSamplesTest {
     @Test
     void eipAliasesResolveThroughTheCatalog() {
         // the aliases of the EIP models, so the intent map need not repeat 
them
-        org.apache.camel.catalog.CamelCatalog catalog = new 
org.apache.camel.catalog.DefaultCamelCatalog();
+        CamelCatalog catalog = new DefaultCamelCatalog();
         JsonObject o = CatalogSamples.sample(catalog, "fan-out", 1);
         assertThat(o.getString("name")).isEqualTo("multicast");
         assertThat(o.getString("note")).contains("multicast EIP");
         assertThat(CatalogSamples.sample(catalog, "broadcast", 
1).getString("name")).isEqualTo("multicast");
-        assertThat(CatalogSamples.sample(catalog, "chunk", 
1).getString("name")).isEqualTo("split");
+        // an exact component name (chunk is the Chunk templating component) 
beats an alias of an EIP, unless asked
+        assertThat(CatalogSamples.sample(catalog, "chunk", 
1).getString("kind")).isEqualTo("component");
+        assertThat(CatalogSamples.sample(catalog, "eip", "chunk", 
1).getString("name")).isEqualTo("split");
         // an alias of an EIP whose page has no YAML example still names the 
EIP
         JsonObject dedup = CatalogSamples.sample(catalog, "dedup", 1);
         assertThat(dedup.getString("error")).contains("dedup");
@@ -105,7 +111,7 @@ public class CatalogSamplesTest {
 
     @Test
     void eipSamplesComeFromTheCatalogDocsAndTheRestFromTheShippedSet() {
-        org.apache.camel.catalog.CamelCatalog catalog = new 
org.apache.camel.catalog.DefaultCamelCatalog();
+        CamelCatalog catalog = new DefaultCamelCatalog();
         JsonObject o = CatalogSamples.sample(catalog, "poll", 2);
         JsonArray samples = (JsonArray) o.get("samples");
         assertThat(samples).isNotEmpty();
@@ -158,10 +164,161 @@ public class CatalogSamplesTest {
 
     @Test
     void patternPagesWithoutTheEipSuffixAreReadFromTheCatalog() {
-        org.apache.camel.catalog.CamelCatalog catalog = new 
org.apache.camel.catalog.DefaultCamelCatalog();
+        CamelCatalog catalog = new DefaultCamelCatalog();
         JsonArray samples = (JsonArray) CatalogSamples.sample(catalog, 
"deadLetterChannel", 1).get("samples");
         assertThat(((JsonObject) 
samples.get(0)).getString("source")).startsWith("dead-letter-channel.adoc 
(Camel ");
         samples = (JsonArray) CatalogSamples.sample(catalog, 
"keyValueRepository", 1).get("samples");
         assertThat(((JsonObject) 
samples.get(0)).getString("source")).startsWith("keyValueRepository.adoc (Camel 
");
     }
+
+    // CAMEL-24720: components, data formats and languages from their 
documentation
+
+    private static String yaml(JsonObject answer, int i) {
+        return ((JsonObject) ((JsonArray) 
answer.get("samples")).get(i)).getString("yaml");
+    }
+
+    private static String source(JsonObject answer, int i) {
+        return ((JsonObject) ((JsonArray) 
answer.get("samples")).get(i)).getString("source");
+    }
+
+    @Test
+    void componentSamplesComeFromTheComponentPageWithItsEndpointFirst() {
+        CamelCatalog catalog = new DefaultCamelCatalog();
+        JsonObject o = CatalogSamples.sample(catalog, "kafka", 2);
+        assertThat(o.getString("name")).isEqualTo("kafka");
+        assertThat(o.getString("kind")).isEqualTo("component");
+        assertThat(o.getString("placement")).contains("from:").contains("to:");
+        assertThat((JsonArray) o.get("samples")).hasSize(2);
+        // the first example of the kafka page is an idempotent consumer with 
a SQL repository: a kafka endpoint first
+        assertThat(yaml(o, 0)).contains("kafka:");
+        assertThat(source(o, 0)).startsWith("kafka-component.adoc (Camel ");
+        assertThat(o.get("note")).isNull();
+        assertThat((Integer) o.get("count")).isGreaterThan(5);
+        // any case, and the kind when given
+        assertThat(CatalogSamples.sample(catalog, "component", "Kafka", 
1).getString("name")).isEqualTo("kafka");
+        // without a catalog there is no component page to read
+        assertThat(CatalogSamples.sample("kafka", 
1).getString("error")).contains("kafka");
+    }
+
+    @Test
+    void theSubPagesOfAComponentBelongToIt() {
+        CamelCatalog catalog = new DefaultCamelCatalog();
+        assertThat(CatalogSamples.subPages(catalog, "aws2-s3"))
+                .contains("aws2-s3-consumer-examples", 
"aws2-s3-producer-operations", "aws2-s3-streaming");
+        // mina-sftp is a component of its own: its pages are not mina's
+        assertThat(CatalogSamples.subPages(catalog, "mina")).isEmpty();
+        assertThat(CatalogSamples.subPages(catalog, 
"mina-sftp")).contains("mina-sftp-authentication");
+        // the examples of the sub-pages count, after the ones of the main page
+        Matcher m = 
Pattern.compile("\\[source,yaml\\]").matcher(catalog.asciiDoc("aws2-s3-component"));
+        int onMainPage = 0;
+        while (m.find()) {
+            onMainPage++;
+        }
+        JsonObject o = CatalogSamples.sample(catalog, "aws2-s3", 5);
+        assertThat((Integer) o.get("count")).isGreaterThan(onMainPage);
+        assertThat(source(o, 0)).startsWith("aws2-s3-component.adoc");
+    }
+
+    @Test
+    void aConsumerOrProducerOnlyComponentSaysItsSide() {
+        CamelCatalog catalog = new DefaultCamelCatalog();
+        JsonObject timer = CatalogSamples.sample(catalog, "timer", 1);
+        assertThat(timer.getString("placement")).contains("from: only");
+        assertThat(yaml(timer, 0)).contains("timer:");
+        JsonObject log = CatalogSamples.sample(catalog, "component", "log", 1);
+        assertThat(log.getString("kind")).isEqualTo("component");
+        assertThat(log.getString("placement")).contains("to: only");
+        // without a kind the log EIP answers first, and says the component is 
there too
+        JsonObject eip = CatalogSamples.sample(catalog, "log", 1);
+        assertThat(eip.getString("kind")).isEqualTo("eip");
+        assertThat((JsonArray) eip.get("also")).containsExactly("component");
+        assertThat(eip.getString("hint")).contains("kind");
+        // an EIP that is no component says nothing
+        assertThat(CatalogSamples.sample(catalog, "split", 
1).get("also")).isNull();
+    }
+
+    @Test
+    void dataFormatAndLanguageSamplesShowTheStepAndTheExpression() {
+        CamelCatalog catalog = new DefaultCamelCatalog();
+        JsonObject csv = CatalogSamples.sample(catalog, "csv", 1);
+        assertThat(csv.getString("kind")).isEqualTo("dataformat");
+        assertThat(csv.getString("placement")).contains("marshal");
+        assertThat(yaml(csv, 0)).contains("csv:").containsAnyOf("marshal:", 
"unmarshal:");
+        assertThat(source(csv, 0)).startsWith("csv-dataformat.adoc (Camel ");
+        JsonObject jq = CatalogSamples.sample(catalog, "jq", 1);
+        assertThat(jq.getString("kind")).isEqualTo("language");
+        assertThat(jq.getString("placement")).contains("expression");
+        assertThat(yaml(jq, 0)).contains("jq:");
+        assertThat(source(jq, 0)).startsWith("jq-language.adoc (Camel ");
+    }
+
+    @Test
+    void aNameInSeveralKindsReturnsTheChoiceUnlessOnlyOneHasSamples() {
+        CamelCatalog catalog = new DefaultCamelCatalog();
+        JsonObject avro = CatalogSamples.sample(catalog, "avro", 1);
+        assertThat(avro.get("samples")).isNull();
+        assertThat((JsonArray) avro.get("kinds")).containsExactly("component", 
"dataformat");
+        assertThat(avro.getString("hint")).contains("kind");
+        assertThat(CatalogSamples.sample(catalog, "dataformat", "avro", 
1).getString("kind")).isEqualTo("dataformat");
+        assertThat(CatalogSamples.sample(catalog, "Data-Formats", "avro", 
1).getString("kind")).isEqualTo("dataformat");
+        assertThat(yaml(CatalogSamples.sample(catalog, "component", "avro", 
1), 0)).contains("avro:");
+        // file is a component and a language, and only the component page has 
examples
+        JsonObject file = CatalogSamples.sample(catalog, "file", 1);
+        assertThat(file.getString("kind")).isEqualTo("component");
+        assertThat(yaml(file, 0)).contains("file:");
+        assertThat(CatalogSamples.sample(catalog, "steps", "file", 
1).getString("error")).contains("kind must be one of");
+    }
+
+    @Test
+    void componentsSharingAPageAndPagesWithoutAnEndpointExample() {
+        CamelCatalog catalog = new DefaultCamelCatalog();
+        // smtp, imap and pop3 are documented on the mail page
+        JsonObject smtp = CatalogSamples.sample(catalog, "smtp", 1);
+        assertThat(smtp.getString("kind")).isEqualTo("component");
+        assertThat(source(smtp, 0)).startsWith("mail-component.adoc (Camel ");
+        assertThat(yaml(smtp, 0)).containsAnyOf("smtp:", "imap:", "pop3:");
+        assertThat(smtp.get("note")).isNull();
+        JsonObject mail = CatalogSamples.sample(catalog, "mail", 1);
+        assertThat(mail.getString("name")).isEqualTo("mail");
+        assertThat(mail.get("note")).isNull();
+        // mapstruct is used through convertBodyTo, no mapstruct: endpoint in 
its examples
+        JsonObject mapstruct = CatalogSamples.sample(catalog, "mapstruct", 1);
+        assertThat((Integer) mapstruct.get("count")).isPositive();
+        assertThat(mapstruct.getString("note")).contains("none of the 
examples");
+        // knative shows Kubernetes manifests, no route example
+        JsonObject knative = CatalogSamples.sample(catalog, "knative", 1);
+        assertThat((Integer) knative.get("count")).isZero();
+        assertThat(knative.getString("hint")).contains("no YAML route 
example");
+    }
+
+    @Test
+    void anUnknownNameGetsTheCatalogSuggestionsToo() {
+        CamelCatalog catalog = new DefaultCamelCatalog();
+        JsonObject o = CatalogSamples.sample(catalog, "mqtt", 1);
+        assertThat(o.getString("error")).contains("mqtt");
+        
assertThat(String.valueOf(o.get("suggestions"))).contains("(component)");
+        // a suggestion is a name with a page to come back for, and no EIP is 
guessed for another kind
+        JsonObject typo = CatalogSamples.sample(catalog, "dataformat", 
"univocity", 1);
+        assertThat(typo.getString("error")).contains("as a dataformat");
+        
assertThat(String.valueOf(typo.get("suggestions"))).contains("univocityCsv 
(dataformat)");
+        assertThat(typo.get("eip")).isNull();
+    }
+
+    @Test
+    void aDataFormatDocumentedUnderAnotherPageNameStillAnswers() {
+        CamelCatalog catalog = new DefaultCamelCatalog();
+        // jackson is on the jackson2 and jackson3 pages, one example each
+        assertThat(CatalogSamples.dataFormatPages(catalog, "jackson"))
+                .containsExactly("jackson2-dataformat", "jackson3-dataformat");
+        JsonObject jackson = CatalogSamples.sample(catalog, "jackson", 5);
+        assertThat(jackson.getString("kind")).isEqualTo("dataformat");
+        assertThat(source(jackson, 0)).startsWith("jackson2-dataformat.adoc");
+        assertThat((Integer) jackson.get("count")).isEqualTo(2);
+        // bindyCsv is on the bindy page, which has no YAML example
+        assertThat(CatalogSamples.dataFormatPages(catalog, 
"bindyCsv")).containsExactly("bindy-dataformat");
+        JsonObject bindy = CatalogSamples.sample(catalog, "bindyCsv", 1);
+        assertThat(bindy.getString("kind")).isEqualTo("dataformat");
+        assertThat((Integer) bindy.get("count")).isZero();
+        assertThat(bindy.getString("hint")).contains("no YAML route example");
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
index d689b1acbb5c..c525c9fe0a7e 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
@@ -93,15 +93,20 @@ public class AuthoringTools {
     }
 
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
-          description = "A validated YAML DSL sample of an EIP or file entry 
(onException, aggregate, split, rest, beans) "
-                        + "from the docs, with where it goes (a top-level 
entry or a step). Use before writing an EIP the "
-                        + "first time or after a 'not defined in the schema' 
error.")
+          description = "A validated YAML DSL sample of an EIP or file entry 
(onException, aggregate, split, rest, beans), "
+                        + "a component (kafka, file), a data format (csv) or a 
language (jq) from the docs, with where "
+                        + "it goes (a top-level entry, a step, an endpoint 
uri, a marshal step, an expression). Use "
+                        + "before writing one the first time or after a 'not 
defined in the schema' error.")
     public JsonObject camel_catalog_sample(
-            @ToolArg(description = "EIP or entry name, or what to do (read 
file, call service, retry, batch)",
+            @ToolArg(description = "EIP, component, data format or language 
name, or what to do (read file, call "
+                                   + "service, retry, batch)",
                      required = true) String name,
+            @ToolArg(description = "eip, component, dataformat or language; 
needed only when a name is in several "
+                                   + "(avro, file)",
+                     required = false) String kind,
             @ToolArg(description = "Maximum samples to return (default 2, max 
5)", required = false) Integer limit,
             @ToolArg(description = VERSION_DESC, required = false) String 
camelVersion) {
-        return call("camel_catalog_sample", args("name", name, "limit", limit, 
"camelVersion", camelVersion));
+        return call("camel_catalog_sample", args("name", name, "kind", kind, 
"limit", limit, "camelVersion", camelVersion));
     }
 
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),

Reply via email to