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

davsclaus pushed a commit to branch feature/CAMEL-25379-kamelet-ai-tools
in repository https://gitbox.apache.org/repos/asf/camel.git

commit fea5be60d1e4c1e550f8b9cb3bf475bd1140a03c
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 6 17:13:31 2026 +0200

    CAMEL-25379: camel-jbang - AI tools: Kamelets in camel_catalog_doc and 
camel_catalog_find, and kamelet: endpoints checked by the validator
    
    camel_catalog_doc answers a Kamelet with its properties, and 
camel_catalog_find
    lists Kamelets. The validator checks kamelet: endpoints against the Kamelet
    catalog and the project's own Kamelet files: an unknown Kamelet, an unknown
    property, a required property left out. A Kamelet file gets a hint where a
    property is written as a Simple function instead of its placeholder. The 
Kamelet
    catalog helper no longer prints defaults as AnyType(value=...).
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
---
 .../dsl/jbang/core/commands/ai/AuthoringTools.java |   8 +-
 .../dsl/jbang/core/commands/ai/CatalogDocs.java    |  97 ++++++
 .../dsl/jbang/core/commands/ai/KameletChecks.java  | 282 +++++++++++++++
 .../jbang/core/commands/ai/KameletDefinitions.java | 311 +++++++++++++++++
 .../jbang/core/commands/ai/SourceValidator.java    |   6 +-
 .../commands/catalog/KameletCatalogHelper.java     |  21 +-
 .../jbang/core/commands/ai/KameletChecksTest.java  | 388 +++++++++++++++++++++
 .../jbang/core/commands/mcp/AuthoringTools.java    |   8 +-
 8 files changed, 1109 insertions(+), 12 deletions(-)

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 4bce96d071f9..6f65a77f9ba2 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
@@ -83,13 +83,13 @@ public final class AuthoringTools {
     /** Registers the authoring tools; called once by the {@link 
ToolRegistry}. */
     static void register(Consumer<ToolDescriptor> registry) {
         registry.accept(tool("camel_catalog_doc",
-                "Catalog documentation of a component, data format, language, 
EIP, built-in bean or the Java API: description, options, Maven coordinates, 
the URI rules of a component; for simple its functions and operators 
(optionsFilter narrows them). endpoint validates a URI.")
+                "Catalog documentation of a component, data format, language, 
EIP, Kamelet, built-in bean or the Java API: description, options, Maven 
coordinates, the URI rules of a component; for simple its functions and 
operators (optionsFilter narrows them). endpoint validates a URI.")
                 .param("name", "string",
                         "Name, e.g. kafka, json (a data format by its YAML 
name or artifact), simple, timer, choice, split, Exchange",
                         false)
                 .param("endpoint", "string", "Endpoint URI to check, e.g. 
kafka:orders?brokers=host:9092", false)
                 .param("kind", "string",
-                        "component, dataformat, language, eip, bean or api 
(auto-detected; a bean is a built-in class such as StringAggregationStrategy, 
with how to declare and use it; api is the Java API to call from a bean or 
script before writing it: Exchange, Message, CamelContext, Registry, 
ProducerTemplate, Processor, AggregationStrategy, Predicate, Expression, 
TypeConverter, or the variables of groovy, js, python, java scripts)",
+                        "component, dataformat, language, eip, kamelet, bean 
or api (auto-detected; a bean is a built-in class such as 
StringAggregationStrategy, with how to declare and use it; api is the Java API 
to call from a bean or script before writing it: Exchange, Message, 
CamelContext, Registry, ProducerTemplate, Processor, AggregationStrategy, 
Predicate, Expression, TypeConverter, or the variables of groovy, js, python, 
java scripts)",
                         false)
                 .param("includeOptions", "string",
                         "common (default: no deprecated or advanced), 
required, all or false", false)
@@ -112,13 +112,13 @@ public final class AuthoringTools {
                 }));
 
         registry.accept(tool("camel_catalog_find",
-                "Finds Camel components, data formats, languages and EIPs by a 
protocol, product, alias or other term "
+                "Finds Camel components, data formats, languages, EIPs and 
Kamelets by a protocol, product, alias or other term "
                                                    + "that is not the exact 
name (mqtt, s3, snowflake, csv, fan-out, dedup): best "
                                                    + "match first with title 
and description. camel_catalog_doc then gives the "
                                                    + "options of one.")
                 .param("term", "string", "What to look for, e.g. mqtt, s3, 
database, csv, fan-out", true)
                 .param("kind", "string",
-                        "component, dataformat, language, eip or bean 
(default: all); bean with an interface name such as AggregationStrategy lists 
the built-in implementations",
+                        "component, dataformat, language, eip, kamelet or bean 
(default: all); bean with an interface name such as AggregationStrategy lists 
the built-in implementations",
                         false)
                 .param("limit", "integer", "Maximum matches per kind (default 
10)", false)
                 .param("camelVersion", "string", VERSION_DESC, false)
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
index 7a5f8351cb91..723d49b6b020 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
@@ -309,6 +309,16 @@ public final class CatalogDocs {
                 return notFound("Bean", name, findBeans(catalog, 
name).stream().map(PojoBeanModel::getName).limit(5).toList());
             }
         }
+        if (kind == null || "kamelet".equals(kind)) {
+            KameletDefinitions.Definition def = 
KameletDefinitions.catalog().get(name);
+            if (def != null) {
+                return kameletDoc(def);
+            }
+            if (kind != null) {
+                return notFound("Kamelet", name,
+                        KameletDefinitions.suggest(name, 
KameletDefinitions.catalog().keySet(), 5));
+            }
+        }
         JsonObject group = mainOptionsGroup(catalog, name);
         if (group != null) {
             return group;
@@ -323,9 +333,78 @@ public final class CatalogDocs {
         suggestions.addAll(catalog.suggestLanguageNames(name, 3));
         suggestions.addAll(catalog.suggestEipNames(name, 3));
         suggestions.addAll(findBeans(catalog, 
name).stream().map(PojoBeanModel::getName).limit(3).toList());
+        suggestions.addAll(KameletDefinitions.suggest(name, 
KameletDefinitions.catalog().keySet(), 3));
         return notFound("Artifact", name, suggestions);
     }
 
+    /** The Kamelets of the catalog whose name has the term, then those whose 
description has it. */
+    private static List<KameletDefinitions.Definition> kameletsMatching(String 
lowerTerm) {
+        List<KameletDefinitions.Definition> byName = new ArrayList<>();
+        List<KameletDefinitions.Definition> byDescription = new ArrayList<>();
+        String dashed = lowerTerm.trim().replace(' ', '-');
+        for (KameletDefinitions.Definition def : 
KameletDefinitions.catalog().values()) {
+            if (def.name().contains(dashed)) {
+                byName.add(def);
+            } else if (def.description() != null && 
def.description().toLowerCase(Locale.ROOT).contains(lowerTerm)) {
+                byDescription.add(def);
+            }
+        }
+        byName.sort((a, b) -> Integer.compare(a.name().length(), 
b.name().length()));
+        byName.addAll(byDescription);
+        return byName;
+    }
+
+    /**
+     * A Kamelet of the catalog: its properties, which go under parameters of 
the kamelet: endpoint, and how to write
+     * it. A model writes the options of the component a Kamelet wraps instead 
(brokers on kafka-sink, which has
+     * bootstrapServers), as it knows those from years of examples.
+     */
+    public static JsonObject kameletDoc(KameletDefinitions.Definition def) {
+        JsonObject result = new JsonObject();
+        result.put("kind", "kamelet");
+        result.put("name", def.name());
+        if (def.type() != null) {
+            result.put("type", def.type());
+        }
+        if (def.description() != null) {
+            result.put("description", def.description());
+        }
+        result.put("from", def.source());
+        StringBuilder params = new StringBuilder();
+        for (KameletDefinitions.Property p : def.properties()) {
+            if (p.required() && p.defaultValue() == null) {
+                params.append(params.isEmpty() ? "" : ", 
").append(p.name()).append(": <").append(p.name()).append(">");
+            }
+        }
+        String endpoint = "{uri: kamelet:" + def.name() + (params.isEmpty() ? 
"" : ", parameters: {" + params + "}") + "}";
+        result.put("yaml", ("source".equals(def.type()) ? "from: " : "- to: ") 
+ endpoint
+                           + " (its properties go under parameters: they are 
the Kamelet's, not the options of the"
+                           + " component it uses)");
+        JsonArray props = new JsonArray();
+        for (KameletDefinitions.Property p : def.properties()) {
+            JsonObject o = new JsonObject();
+            o.put("name", p.name());
+            if (p.required() && p.defaultValue() == null) {
+                o.put("required", true);
+            }
+            if (p.type() != null) {
+                o.put("type", p.type());
+            }
+            if (p.defaultValue() != null) {
+                o.put("defaultValue", p.defaultValue());
+            }
+            if (!p.enumValues().isEmpty()) {
+                o.put("enum", new JsonArray(p.enumValues()));
+            }
+            if (p.description() != null) {
+                o.put("description", p.description());
+            }
+            props.add(o);
+        }
+        result.put("properties", props);
+        return result;
+    }
+
     /**
      * The documentation of the EIP a term such as an alias (fan-out, dedup, 
rate-limit) or a word of the title names,
      * with the term the caller used; null when no EIP matches.
@@ -534,6 +613,24 @@ public final class CatalogDocs {
                 matches.add(o);
             }
         }
+        if (kind == null || "kamelet".equals(kind)) {
+            // the Kamelets whose name or description has the term: a few next 
to the components, all when asked for
+            int n = 0;
+            int maxKamelets = kind == null ? Math.min(max, 5) : max;
+            String lower = term.toLowerCase(Locale.ROOT);
+            for (KameletDefinitions.Definition def : kameletsMatching(lower)) {
+                if (n++ >= maxKamelets) {
+                    break;
+                }
+                JsonObject o = summary("kamelet", def.name(), def.title() != 
null ? def.title() : def.name(),
+                        def.description(), null);
+                if (def.type() != null) {
+                    o.put("type", def.type());
+                }
+                o.put("uri", "kamelet:" + def.name());
+                matches.add(o);
+            }
+        }
         result.put("matches", matches);
         result.put("count", matches.size());
         if (matches.isEmpty()) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecks.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecks.java
new file mode 100644
index 000000000000..cd9491798374
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecks.java
@@ -0,0 +1,282 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.ai;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
+import org.yaml.snakeyaml.nodes.MappingNode;
+import org.yaml.snakeyaml.nodes.Node;
+import org.yaml.snakeyaml.nodes.NodeTuple;
+import org.yaml.snakeyaml.nodes.ScalarNode;
+import org.yaml.snakeyaml.nodes.SequenceNode;
+
+/**
+ * The kamelet: endpoints of a YAML route against the Kamelets they name 
({@link KameletDefinitions}): a Kamelet that
+ * does not exist, a property it does not have, a required property left out. 
The catalog validates the options of a
+ * component's endpoint; the options of a kamelet: endpoint are the properties 
of a template it does not know, so these
+ * went unchecked until the route failed to start ("mandatory parameters must 
be provided: message") or, for a
+ * misspelled optional one, never failed and did nothing.
+ */
+public final class KameletChecks {
+
+    /** The keys whose value is an endpoint uri, written as uri: under them or 
as their own value. */
+    private static final Set<String> ENDPOINT_KEYS = Set.of("from", "to", 
"toD", "to-d", "wireTap", "wire-tap", "enrich",
+            "pollEnrich", "poll-enrich");
+
+    private KameletChecks() {
+    }
+
+    /** The errors of the kamelet: endpoints of a YAML route, each with its 
line; empty when it has none. */
+    public static List<String> validateYaml(String content, Path directory) {
+        return validateYaml(content, directory, true);
+    }
+
+    /**
+     * As {@link #validateYaml(String, Path)}; unknownNames false for a write, 
where a Kamelet no catalog or project has
+     * may be the project's own one not written yet: then only a name close to 
a known one is an error (the route and
+     * its Kamelet could otherwise only be written in one order, as for 
direct: consumers in CAMEL-24955).
+     */
+    public static List<String> validateYaml(String content, Path directory, 
boolean unknownNames) {
+        List<String> errors = new ArrayList<>();
+        if (content == null || !content.contains("kamelet:")) {
+            return errors;
+        }
+        List<Node> roots = new ArrayList<>();
+        try {
+            for (Node n : new Yaml(new SafeConstructor(new 
LoaderOptions())).composeAll(new StringReader(content))) {
+                roots.add(n);
+            }
+        } catch (Exception e) {
+            // not YAML: the schema check says so
+            return errors;
+        }
+        Map<String, String> properties = directory != null ? 
projectProperties(directory) : Map.of();
+        for (Node root : roots) {
+            walk(root, null, errors, directory, properties, unknownNames);
+        }
+        return errors;
+    }
+
+    private static void walk(
+            Node node, String parentKey, List<String> errors, Path directory, 
Map<String, String> properties,
+            boolean unknownNames) {
+        if (node instanceof SequenceNode seq) {
+            for (Node n : seq.getValue()) {
+                walk(n, parentKey, errors, directory, properties, 
unknownNames);
+            }
+        } else if (node instanceof MappingNode map) {
+            ScalarNode uri = null;
+            MappingNode parameters = null;
+            for (NodeTuple t : map.getValue()) {
+                String key = t.getKeyNode() instanceof ScalarNode k ? 
k.getValue() : null;
+                if ("uri".equals(key) && t.getValueNode() instanceof 
ScalarNode s) {
+                    uri = s;
+                } else if ("parameters".equals(key) && t.getValueNode() 
instanceof MappingNode p) {
+                    parameters = p;
+                } else if (ENDPOINT_KEYS.contains(key) && t.getValueNode() 
instanceof ScalarNode s) {
+                    // to: kamelet:log-sink, the short form
+                    check(s, null, errors, directory, properties, 
unknownNames);
+                }
+            }
+            if (uri != null && (parentKey == null || 
ENDPOINT_KEYS.contains(parentKey) || "from".equals(parentKey))) {
+                check(uri, parameters, errors, directory, properties, 
unknownNames);
+            }
+            for (NodeTuple t : map.getValue()) {
+                String key = t.getKeyNode() instanceof ScalarNode k ? 
k.getValue() : null;
+                if (!"parameters".equals(key)) {
+                    walk(t.getValueNode(), key, errors, directory, properties, 
unknownNames);
+                }
+            }
+        }
+    }
+
+    private static void check(
+            ScalarNode uriNode, MappingNode parameters, List<String> errors, 
Path directory,
+            Map<String, String> properties, boolean unknownNames) {
+        String uri = uriNode.getValue();
+        if (uri == null || !uri.startsWith("kamelet:")) {
+            return;
+        }
+        String rest = uri.substring("kamelet:".length());
+        String query = null;
+        int q = rest.indexOf('?');
+        if (q >= 0) {
+            query = rest.substring(q + 1);
+            rest = rest.substring(0, q);
+        }
+        String name = rest.contains("/") ? rest.substring(0, 
rest.indexOf('/')) : rest;
+        if (name.isEmpty() || "source".equals(name) || "sink".equals(name) || 
name.contains("{{")
+                || name.contains("${")) {
+            // kamelet:source and kamelet:sink are the ends of a Kamelet's own 
template
+            return;
+        }
+        int line = uriNode.getStartMark().getLine();
+        KameletDefinitions.Definition def = KameletDefinitions.find(name, 
directory);
+        if (def == null) {
+            Map<String, KameletDefinitions.Definition> catalog = 
KameletDefinitions.catalog();
+            if (catalog.isEmpty()) {
+                // the catalog cannot be read: nothing to check against
+                return;
+            }
+            List<String> names = new ArrayList<>(catalog.keySet());
+            if (directory != null) {
+                
names.addAll(KameletDefinitions.projectKamelets(directory).keySet());
+            }
+            List<String> suggestions = KameletDefinitions.suggest(name, names, 
3);
+            if (!unknownNames && suggestions.isEmpty()) {
+                return;
+            }
+            StringBuilder sb = new 
StringBuilder(EndpointChecks.linePrefix(line)).append("kamelet:").append(name)
+                    .append(": no Kamelet named ").append(name).append(" in 
the Kamelet catalog or the project");
+            if (!suggestions.isEmpty()) {
+                sb.append(". Did you mean: ").append(String.join(", ", 
suggestions));
+            }
+            sb.append("; camel_catalog_doc name=<kamelet> has the properties 
of one (a Kamelet of the project is")
+                    .append(" a ").append(name).append(".kamelet.yaml file 
beside the route)");
+            errors.add(sb.toString());
+            return;
+        }
+        Map<String, Integer> given = new LinkedHashMap<>();
+        if (query != null) {
+            for (String part : query.split("&")) {
+                String key = part.contains("=") ? part.substring(0, 
part.indexOf('=')) : part;
+                if (!key.isBlank()) {
+                    given.put(key, line);
+                }
+            }
+        }
+        if (parameters != null) {
+            for (NodeTuple t : parameters.getValue()) {
+                if (t.getKeyNode() instanceof ScalarNode k) {
+                    given.put(k.getValue(), k.getStartMark().getLine());
+                }
+            }
+        }
+        String where = def.source().startsWith("the project") ? " (" + 
def.source() + ")" : "";
+        for (Map.Entry<String, Integer> e : given.entrySet()) {
+            if (def.property(e.getKey()) == null) {
+                List<String> close = 
KameletDefinitions.suggestProperty(e.getKey(), def);
+                StringBuilder sb = new 
StringBuilder(EndpointChecks.linePrefix(e.getValue())).append("kamelet:")
+                        .append(name).append(": unknown property 
'").append(e.getKey()).append("'");
+                if (!close.isEmpty()) {
+                    sb.append(". Did you mean: ").append(String.join(", ", 
close));
+                }
+                sb.append(". The properties of 
").append(name).append(where).append(": ")
+                        .append(def.properties().isEmpty() ? "none" : 
KameletDefinitions.propertyList(def));
+                if (!where.isEmpty()) {
+                    // the project's own Kamelet: the property may be what the 
Kamelet lacks, not the route
+                    sb.append("; a property of a Kamelet is declared in its 
file under spec.definition.properties")
+                            .append(" (and listed under required: when it must 
be given)");
+                }
+                errors.add(sb.toString());
+            }
+        }
+        Set<String> missing = new TreeSet<>();
+        for (KameletDefinitions.Property p : def.properties()) {
+            if (p.required() && p.defaultValue() == null && 
!given.containsKey(p.name())
+                    && !inProperties(properties, name, p.name())) {
+                missing.add(p.name());
+            }
+        }
+        if (!missing.isEmpty()) {
+            errors.add(EndpointChecks.linePrefix(line) + "kamelet:" + name + 
": the required "
+                       + (missing.size() == 1 ? "property " : "properties ") + 
String.join(", ", missing)
+                       + (missing.size() == 1 ? " is" : " are")
+                       + " missing (the runtime says 'mandatory parameters 
must be provided: "
+                       + String.join(",", missing) + "'): add "
+                       + (missing.size() == 1 ? "it" : "them") + " under 
parameters. The properties of " + name
+                       + where + ": " + KameletDefinitions.propertyList(def));
+        }
+    }
+
+    private static final Pattern UNKNOWN_FUNCTION
+            = Pattern.compile(
+                    "Unknown function: 
(?:properties\\.|property\\.|header\\.|exchangeProperty\\.|variable\\.)?([\\w-]+)");
+
+    /**
+     * The messages of a Kamelet file with what a model gets wrong in its 
template: a property of the Kamelet written as
+     * a Simple function (${properties.tag}, ${header.tag}) where the template 
has it as the placeholder {{tag}}.
+     */
+    public static List<String> withTemplateHints(String fileName, String 
content, List<String> msgs) {
+        if (msgs.isEmpty() || fileName == null || 
!fileName.endsWith(".kamelet.yaml")) {
+            return msgs;
+        }
+        KameletDefinitions.Definition def = KameletDefinitions.parse(content, 
fileName, fileName);
+        if (def == null || def.properties().isEmpty()) {
+            return msgs;
+        }
+        List<String> answer = new ArrayList<>();
+        for (String m : msgs) {
+            Matcher matcher = UNKNOWN_FUNCTION.matcher(m);
+            if (matcher.find() && def.property(matcher.group(1)) != null) {
+                String p = matcher.group(1);
+                m = m + " (in a Kamelet's template its property " + p + " is 
the placeholder {{" + p
+                    + "}}, as in simple: \"${body} {{" + p + "}}\"; it is not 
a header or an exchange property)";
+            }
+            answer.add(m);
+        }
+        return answer;
+    }
+
+    /** Whether application properties set the property of the Kamelet: 
camel.kamelet.name.prop or name.routeId.prop. */
+    static boolean inProperties(Map<String, String> properties, String 
kamelet, String property) {
+        String prefix = "camel.kamelet." + kamelet + ".";
+        for (String key : properties.keySet()) {
+            if (key.startsWith(prefix) && (key.equals(prefix + property) || 
key.endsWith("." + property))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static Map<String, String> projectProperties(Path directory) {
+        Map<String, String> answer = new LinkedHashMap<>();
+        try (Stream<Path> files = Files.list(directory)) {
+            files.filter(p -> 
p.getFileName().toString().endsWith(".properties")).forEach(p -> {
+                try {
+                    for (String l : Files.readAllLines(p)) {
+                        String s = l.trim();
+                        int eq = s.indexOf('=');
+                        if (s.startsWith("camel.kamelet.") && eq > 0) {
+                            answer.put(s.substring(0, eq).trim(), 
s.substring(eq + 1).trim());
+                        }
+                    }
+                } catch (IOException e) {
+                    // an unreadable file sets nothing
+                }
+            });
+        } catch (IOException e) {
+            // no directory listing: no properties
+        }
+        return answer;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletDefinitions.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletDefinitions.java
new file mode 100644
index 000000000000..38a48dbf396c
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletDefinitions.java
@@ -0,0 +1,311 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.ai;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Stream;
+
+import org.apache.camel.dsl.jbang.core.commands.catalog.KameletCatalogHelper;
+import org.apache.camel.dsl.jbang.core.commands.catalog.KameletModel;
+import org.apache.camel.dsl.jbang.core.commands.catalog.KameletOptionModel;
+import org.apache.camel.dsl.jbang.core.common.VersionHelper;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
+
+/**
+ * The Kamelets an agent can use: those of the Kamelet catalog and the 
project's own {@code <name>.kamelet.yaml} files,
+ * with their properties. The catalog tools and the validator read them, so 
that a Kamelet is looked up and checked as a
+ * component is: a model knows the components from years of examples, but 
writes a Kamelet's properties from the
+ * component's options (brokers on kafka-sink) or uses one that is gone 
(kafka-not-secured-source).
+ */
+public final class KameletDefinitions {
+
+    /** A Kamelet and its properties. */
+    public record Definition(String name, String type, String title, String 
description, String source,
+            List<Property> properties) {
+
+        public Property property(String name) {
+            for (Property p : properties) {
+                if (p.name().equals(name)) {
+                    return p;
+                }
+            }
+            return null;
+        }
+    }
+
+    /** A property of a Kamelet. */
+    public record Property(String name, boolean required, String type, String 
defaultValue, String description,
+            List<String> enumValues) {
+    }
+
+    private static final Map<String, Map<String, Definition>> CATALOGS = new 
ConcurrentHashMap<>();
+    private static volatile Map<String, Definition> testCatalog;
+
+    private KameletDefinitions() {
+    }
+
+    /** The Kamelet of the project directory, else of the catalog, or null. */
+    public static Definition find(String name, Path directory) {
+        if (name == null || name.isBlank()) {
+            return null;
+        }
+        Definition def = directory != null ? 
projectKamelets(directory).get(name) : null;
+        return def != null ? def : catalog().get(name);
+    }
+
+    /** The Kamelets of the catalog by name; empty when the catalog cannot be 
read (offline, say). */
+    public static Map<String, Definition> catalog() {
+        Map<String, Definition> fixed = testCatalog;
+        if (fixed != null) {
+            return fixed;
+        }
+        String version;
+        try {
+            version = VersionHelper.extractKameletsVersion();
+        } catch (Exception e) {
+            version = null;
+        }
+        if (version == null) {
+            return Map.of();
+        }
+        return CATALOGS.computeIfAbsent(version, 
KameletDefinitions::loadCatalog);
+    }
+
+    /** The Kamelets the catalog has instead of the real catalog, for tests; 
null for the real one. */
+    static void setTestCatalog(Map<String, Definition> kamelets) {
+        testCatalog = kamelets;
+    }
+
+    private static Map<String, Definition> loadCatalog(String version) {
+        Map<String, Definition> answer = new LinkedHashMap<>();
+        try {
+            for (Object o : KameletCatalogHelper.loadKamelets(version, 
null).values()) {
+                KameletModel km = KameletCatalogHelper.createModel(o, true);
+                List<Property> props = new ArrayList<>();
+                if (km.properties != null) {
+                    for (KameletOptionModel om : km.properties.values()) {
+                        props.add(new Property(
+                                om.name, om.required, om.type, 
om.defaultValue, om.description,
+                                om.enumValues != null ? om.enumValues : 
List.of()));
+                    }
+                }
+                answer.put(km.name, new Definition(
+                        km.name, km.type, null, km.description,
+                        "the Kamelet catalog " + version, props));
+            }
+        } catch (Exception e) {
+            // no catalog: the tools say nothing about Kamelets rather than 
the wrong thing
+        }
+        return answer;
+    }
+
+    /** The project's own Kamelets: the *.kamelet.yaml files of the directory 
and its subdirectories. */
+    public static Map<String, Definition> projectKamelets(Path directory) {
+        Map<String, Definition> answer = new LinkedHashMap<>();
+        if (directory == null || !Files.isDirectory(directory)) {
+            return answer;
+        }
+        try (Stream<Path> files = Files.walk(directory, 4)) {
+            files.filter(p -> 
p.getFileName().toString().endsWith(".kamelet.yaml"))
+                    .filter(p -> !hidden(directory.relativize(p)))
+                    .sorted()
+                    .forEach(p -> {
+                        Definition def = parse(p);
+                        if (def != null) {
+                            answer.putIfAbsent(def.name(), def);
+                        }
+                    });
+        } catch (IOException e) {
+            // an unreadable directory has no Kamelets
+        }
+        return answer;
+    }
+
+    private static boolean hidden(Path relative) {
+        for (Path part : relative) {
+            String s = part.toString();
+            if (s.startsWith(".") || "target".equals(s) || 
"node_modules".equals(s)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** A Kamelet file as a definition, or null when it is not one (yet): a 
half-written file has no Kamelet. */
+    static Definition parse(Path file) {
+        String fileName = file.getFileName().toString();
+        String fallback = fileName.substring(0, fileName.length() - 
".kamelet.yaml".length());
+        try {
+            return parse(Files.readString(file), fallback, fileName);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    static Definition parse(String content, String fallbackName, String 
fileName) {
+        Object root;
+        try {
+            root = new Yaml(new SafeConstructor(new 
LoaderOptions())).load(content);
+        } catch (Exception e) {
+            return null;
+        }
+        if (!(root instanceof Map<?, ?> map) || 
!"Kamelet".equals(map.get("kind"))) {
+            return null;
+        }
+        String name = fallbackName;
+        String type = null;
+        if (map.get("metadata") instanceof Map<?, ?> meta) {
+            if (meta.get("name") instanceof String n && !n.isBlank()) {
+                name = n;
+            }
+            if (meta.get("labels") instanceof Map<?, ?> labels
+                    && labels.get("camel.apache.org/kamelet.type") instanceof 
String t) {
+                type = t;
+            }
+        }
+        String title = null;
+        String description = null;
+        List<Property> props = new ArrayList<>();
+        if (map.get("spec") instanceof Map<?, ?> spec && 
spec.get("definition") instanceof Map<?, ?> def) {
+            title = def.get("title") instanceof String t ? t : null;
+            description = def.get("description") instanceof String d ? d : 
null;
+            Set<String> required = new HashSet<>();
+            if (def.get("required") instanceof List<?> req) {
+                req.forEach(r -> required.add(String.valueOf(r)));
+            }
+            if (def.get("properties") instanceof Map<?, ?> properties) {
+                for (Map.Entry<?, ?> e : properties.entrySet()) {
+                    String pname = String.valueOf(e.getKey());
+                    Map<String, Object> p = e.getValue() instanceof Map<?, ?> 
m ? (Map<String, Object>) m : Map.of();
+                    Object dv = p.get("default");
+                    List<String> enums = new ArrayList<>();
+                    if (p.get("enum") instanceof List<?> en) {
+                        en.forEach(v -> enums.add(String.valueOf(v)));
+                    }
+                    props.add(new Property(
+                            pname, required.contains(pname),
+                            p.get("type") != null ? 
String.valueOf(p.get("type")) : null,
+                            dv != null ? String.valueOf(dv) : null,
+                            p.get("description") != null ? 
String.valueOf(p.get("description")) : null, enums));
+                }
+            }
+        }
+        return new Definition(name, type, title, description, "the project 
file " + fileName, props);
+    }
+
+    /**
+     * The Kamelets whose name is close to the given one: those sharing its 
words (kafka-not-secured-source has kafka
+     * and source, so kafka-source), then those a few edits away. Best first.
+     */
+    public static List<String> suggest(String name, Collection<String> names, 
int limit) {
+        if (name == null || name.isBlank()) {
+            return List.of();
+        }
+        String lower = name.toLowerCase(Locale.ROOT);
+        Set<String> words = Set.of(lower.split("[-_.]"));
+        record Scored(String name, int score) {
+        }
+        List<Scored> scored = new ArrayList<>();
+        for (String candidate : names) {
+            String c = candidate.toLowerCase(Locale.ROOT);
+            String[] cw = c.split("-");
+            int shared = 0;
+            for (String w : cw) {
+                if (words.contains(w)) {
+                    shared++;
+                }
+            }
+            int distance = distance(lower, c);
+            // all words of the candidate in the name, or one edit apart for a 
short name
+            if (shared == cw.length && shared >= 2) {
+                scored.add(new Scored(candidate, 100 + shared * 10 - 
distance));
+            } else if (distance <= Math.max(2, lower.length() / 4)) {
+                scored.add(new Scored(candidate, 50 - distance));
+            } else if (shared >= 2 && shared * 2 >= cw.length) {
+                scored.add(new Scored(candidate, shared * 10 - distance));
+            }
+        }
+        return scored.stream()
+                
.sorted(Comparator.comparingInt(Scored::score).reversed().thenComparing(Scored::name))
+                .limit(limit).map(Scored::name).toList();
+    }
+
+    /** The property names close to the given one: same name in another case 
or form, or a few edits away. */
+    public static List<String> suggestProperty(String name, Definition def) {
+        String lower = name.toLowerCase(Locale.ROOT).replace("-", 
"").replace("_", "");
+        List<String> answer = new ArrayList<>();
+        for (Property p : def.properties()) {
+            String c = p.name().toLowerCase(Locale.ROOT);
+            if (c.equals(lower) || distance(lower, c) <= Math.max(2, 
c.length() / 5) || c.contains(lower)
+                    || (lower.length() >= 5 && lower.contains(c))) {
+                answer.add(p.name());
+            }
+        }
+        return answer;
+    }
+
+    static int distance(String a, String b) {
+        int[] prev = new int[b.length() + 1];
+        int[] cur = new int[b.length() + 1];
+        for (int j = 0; j <= b.length(); j++) {
+            prev[j] = j;
+        }
+        for (int i = 1; i <= a.length(); i++) {
+            cur[0] = i;
+            for (int j = 1; j <= b.length(); j++) {
+                int cost = a.charAt(i - 1) == b.charAt(j - 1) ? 0 : 1;
+                cur[j] = Math.min(Math.min(cur[j - 1] + 1, prev[j] + 1), 
prev[j - 1] + cost);
+            }
+            int[] t = prev;
+            prev = cur;
+            cur = t;
+        }
+        return prev[b.length()];
+    }
+
+    /** The properties in one line each: name (required), type, default; for a 
message that lists them. */
+    public static String propertyList(Definition def) {
+        StringBuilder sb = new StringBuilder();
+        for (Property p : def.properties()) {
+            if (!sb.isEmpty()) {
+                sb.append(", ");
+            }
+            sb.append(p.name());
+            if (p.required() && p.defaultValue() == null) {
+                sb.append(" (required)");
+            } else if (p.defaultValue() != null) {
+                sb.append(" (default ").append(p.defaultValue()).append(")");
+            }
+        }
+        return sb.toString();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
index 3d572d282043..cab4549e326d 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
@@ -121,6 +121,10 @@ public final class SourceValidator {
             // route: read it, so that a GET operation is known to carry no 
body (CAMEL-24844)
             List<String> msgs = validateCamelYaml(content, catalog, 
schemaValidator,
                     directory != null ? 
OpenApiVerbs.bodylessEndpoints(content, directory) : Set.of());
+            if (msgs.isEmpty()) {
+                // the kamelet: endpoints against the Kamelet catalog and the 
project's own Kamelets
+                msgs = new ArrayList<>(KameletChecks.validateYaml(content, 
directory, checkConsumers));
+            }
             if (directory != null && msgs.isEmpty()) {
                 msgs = new ArrayList<>(msgs);
                 BeanDeclarations declarations = 
BeanDeclarations.scan(directory, fileName);
@@ -132,7 +136,7 @@ public final class SourceValidator {
                     
msgs.addAll(EndpointConsumerChecks.validateYamlConsumers(content, directory, 
fileName, catalog));
                 }
             }
-            return msgs;
+            return KameletChecks.withTemplateHints(name, content, msgs);
         }
         if (name.endsWith(".properties")) {
             return validateProperties(content, catalog, extraPropertyLine);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletCatalogHelper.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletCatalogHelper.java
index dd7961ef8894..9096ed45a2f5 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletCatalogHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletCatalogHelper.java
@@ -101,7 +101,7 @@ public final class KameletCatalogHelper {
 
     private static String getPropertyExample(Object prop) throws Exception {
         Method m = prop.getClass().getMethod("getExample");
-        Object en = ObjectHelper.invokeMethod(m, prop);
+        Object en = unwrap(ObjectHelper.invokeMethod(m, prop));
         if (en != null) {
             String t = en.toString();
             return StringHelper.removeLeadingAndEndingQuotes(t);
@@ -111,7 +111,7 @@ public final class KameletCatalogHelper {
 
     private static String getPropertyDefaultValue(Object prop) throws 
Exception {
         Method m = prop.getClass().getMethod("get_default");
-        Object dn = ObjectHelper.invokeMethod(m, prop);
+        Object dn = unwrap(ObjectHelper.invokeMethod(m, prop));
         if (dn != null) {
             String t = dn.toString();
             return StringHelper.removeLeadingAndEndingQuotes(t);
@@ -119,13 +119,28 @@ public final class KameletCatalogHelper {
         return null;
     }
 
+    /**
+     * The value of a default, example or enum value of the Kamelet model, 
which wraps it in an AnyType whose toString
+     * is AnyType(value=1000): the docs showed that instead of 1000.
+     */
+    private static Object unwrap(Object value) {
+        if (value != null && 
value.getClass().getSimpleName().equals("AnyType")) {
+            try {
+                return 
ObjectHelper.invokeMethod(value.getClass().getMethod("getValue"), value);
+            } catch (Exception e) {
+                // not the AnyType we know: its text
+            }
+        }
+        return value;
+    }
+
     private static List<String> getPropertyEnum(Object prop) throws Exception {
         List<String> answer = new ArrayList<>();
         Method m = prop.getClass().getMethod("get_enum");
         List<Object> list = (List<Object>) ObjectHelper.invokeMethod(m, prop);
         if (list != null && !list.isEmpty()) {
             for (var en : list) {
-                String t = en.toString();
+                String t = String.valueOf(unwrap(en));
                 t = StringHelper.removeLeadingAndEndingQuotes(t);
                 answer.add(t);
             }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecksTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecksTest.java
new file mode 100644
index 000000000000..4b7de4993365
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecksTest.java
@@ -0,0 +1,388 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.ai;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The kamelet: endpoints of a route against the Kamelets they name, and the 
Kamelets in the catalog tools: what a model
+ * got wrong writing Kamelets in the Kamelet side check of the local model 
benchmark.
+ */
+class KameletChecksTest {
+
+    private static CamelCatalog catalog;
+
+    @BeforeAll
+    static void setUp() {
+        catalog = new DefaultCamelCatalog();
+        Map<String, KameletDefinitions.Definition> kamelets = new 
LinkedHashMap<>();
+        kamelets.put("timer-source", new KameletDefinitions.Definition(
+                "timer-source", "source", null,
+                "Produces periodic messages with a custom payload.", "the 
Kamelet catalog test", List.of(
+                        prop("period", false, "integer", "1000"),
+                        prop("message", true, "string", null),
+                        prop("contentType", false, "string", "text/plain"),
+                        prop("repeatCount", false, "integer", null))));
+        kamelets.put("kafka-source", new KameletDefinitions.Definition(
+                "kafka-source", "source", null,
+                "Receive data from Kafka topics.", "the Kamelet catalog test", 
List.of(
+                        prop("topic", true, "string", null),
+                        prop("bootstrapServers", true, "string", null),
+                        prop("saslAuthType", false, "string", "NONE"))));
+        kamelets.put("kafka-sink", new KameletDefinitions.Definition(
+                "kafka-sink", "sink", null,
+                "Send data to Kafka topics.", "the Kamelet catalog test", 
List.of(
+                        prop("topic", true, "string", null),
+                        prop("bootstrapServers", true, "string", null))));
+        kamelets.put("log-sink", new KameletDefinitions.Definition(
+                "log-sink", "sink", null,
+                "Log data to the console.", "the Kamelet catalog test", 
List.of(
+                        prop("showHeaders", false, "boolean", "false"),
+                        prop("showStreams", false, "boolean", "false"))));
+        KameletDefinitions.setTestCatalog(kamelets);
+    }
+
+    @AfterAll
+    static void tearDown() {
+        KameletDefinitions.setTestCatalog(null);
+    }
+
+    private static KameletDefinitions.Property prop(String name, boolean 
required, String type, String defaultValue) {
+        return new KameletDefinitions.Property(name, required, type, 
defaultValue, null, List.of());
+    }
+
+    @Test
+    void rightKameletsHaveNoErrors() {
+        String yaml = """
+                - route:
+                    from:
+                      uri: kamelet:timer-source
+                      parameters:
+                        period: 2000
+                        message: hello
+                      steps:
+                        - to:
+                            uri: kamelet:kafka-sink
+                            parameters:
+                              topic: orders
+                              bootstrapServers: localhost:9092
+                        - to: kamelet:log-sink
+                """;
+        assertThat(KameletChecks.validateYaml(yaml, null)).isEmpty();
+    }
+
+    @Test
+    void aKameletThatIsGone() {
+        // kafka-not-secured-source was folded into kafka-source (saslAuthType 
NONE); a model still writes it
+        String yaml = """
+                - route:
+                    from:
+                      uri: kamelet:kafka-not-secured-source
+                      parameters:
+                        topic: orders
+                        bootstrapServers: localhost:9092
+                      steps:
+                        - to: kamelet:log-sink
+                """;
+        List<String> errors = KameletChecks.validateYaml(yaml, null);
+        assertThat(errors).hasSize(1);
+        assertThat(errors.get(0)).startsWith("Line 3: ")
+                .contains("no Kamelet named kafka-not-secured-source")
+                .contains("Did you mean: kafka-source");
+    }
+
+    @Test
+    void anUnknownNameOnAWriteOnlyWhenCloseToAKnownOne() {
+        // the project's own Kamelet may be the next file the model writes
+        String yaml = """
+                - route:
+                    from:
+                      uri: timer:tick
+                      steps:
+                        - to: kamelet:tag-order-action
+                        - to: kamelet:kafka-not-secured-sink
+                """;
+        List<String> errors = KameletChecks.validateYaml(yaml, null, false);
+        assertThat(errors).hasSize(1);
+        assertThat(errors.get(0)).startsWith("Line 6: ").contains("Did you 
mean: kafka-sink");
+        assertThat(KameletChecks.validateYaml(yaml, null, true)).hasSize(2);
+    }
+
+    @Test
+    void theOptionsOfTheComponentInsteadOfTheKamelet() {
+        String yaml = """
+                - route:
+                    from:
+                      uri: timer:tick
+                      steps:
+                        - to:
+                            uri: kamelet:kafka-sink
+                            parameters:
+                              topic: orders
+                              brokers: localhost:9092
+                """;
+        List<String> errors = KameletChecks.validateYaml(yaml, null);
+        assertThat(errors).hasSize(2);
+        assertThat(errors.get(0)).startsWith("Line 9: ")
+                .contains("kamelet:kafka-sink: unknown property 'brokers'")
+                .contains("The properties of kafka-sink: topic (required), 
bootstrapServers (required)");
+        assertThat(errors.get(1)).startsWith("Line 6: ")
+                .contains("the required property bootstrapServers is missing")
+                .contains("mandatory parameters must be provided: 
bootstrapServers");
+    }
+
+    @Test
+    void aMisspelledOptionalProperty() {
+        String yaml = """
+                - route:
+                    from:
+                      uri: timer:tick
+                      steps:
+                        - to: kamelet:log-sink?showHeader=true
+                """;
+        List<String> errors = KameletChecks.validateYaml(yaml, null);
+        assertThat(errors).hasSize(1);
+        assertThat(errors.get(0)).contains("unknown property 
'showHeader'").contains("Did you mean: showHeaders");
+    }
+
+    @Test
+    void aRequiredPropertyLeftOut() {
+        String yaml = """
+                - route:
+                    from:
+                      uri: kamelet:timer-source
+                      parameters:
+                        period: 2000
+                        jsonBody: '{"a": 1}'
+                      steps:
+                        - to: kamelet:log-sink
+                """;
+        List<String> errors = KameletChecks.validateYaml(yaml, null);
+        assertThat(errors).hasSize(2);
+        assertThat(errors.get(0)).contains("unknown property 'jsonBody'");
+        assertThat(errors.get(1)).startsWith("Line 3: ").contains("the 
required property message is missing");
+    }
+
+    @Test
+    void aRequiredPropertyFromApplicationProperties(@TempDir Path dir) throws 
Exception {
+        Files.writeString(dir.resolve("application.properties"), 
"camel.kamelet.timer-source.message=hello\n");
+        String yaml = """
+                - route:
+                    from:
+                      uri: kamelet:timer-source
+                      steps:
+                        - to: kamelet:log-sink
+                """;
+        assertThat(KameletChecks.validateYaml(yaml, dir)).isEmpty();
+    }
+
+    @Test
+    void theProjectsOwnKamelet(@TempDir Path dir) throws Exception {
+        Files.writeString(dir.resolve("tag-order-action.kamelet.yaml"), """
+                apiVersion: camel.apache.org/v1
+                kind: Kamelet
+                metadata:
+                  name: tag-order-action
+                  labels:
+                    camel.apache.org/kamelet.type: action
+                spec:
+                  definition:
+                    title: Tag Order
+                    required:
+                      - tag
+                    properties:
+                      tag:
+                        type: string
+                      prefix:
+                        type: string
+                        default: "#"
+                  template:
+                    from:
+                      uri: kamelet:source
+                      steps:
+                        - setBody:
+                            simple: "${body} [{{prefix}}{{tag}}]"
+                """);
+        String ok = """
+                - route:
+                    from:
+                      uri: timer:tick
+                      steps:
+                        - to:
+                            uri: kamelet:tag-order-action
+                            parameters:
+                              tag: priority
+                """;
+        assertThat(KameletChecks.validateYaml(ok, dir)).isEmpty();
+        String wrong = """
+                - route:
+                    from:
+                      uri: timer:tick
+                      steps:
+                        - to:
+                            uri: kamelet:tag-order-action
+                            parameters:
+                              label: priority
+                """;
+        List<String> errors = KameletChecks.validateYaml(wrong, dir);
+        assertThat(errors).hasSize(2);
+        assertThat(errors.get(0)).contains("unknown property 'label'")
+                .contains("The properties of tag-order-action (the project 
file tag-order-action.kamelet.yaml): "
+                          + "tag (required), prefix (default #)");
+        assertThat(errors.get(1)).contains("the required property tag is 
missing");
+    }
+
+    private static final String TAG_KAMELET = """
+            apiVersion: camel.apache.org/v1
+            kind: Kamelet
+            metadata:
+              name: tag-order-action
+              labels:
+                camel.apache.org/kamelet.type: action
+            spec:
+              definition:
+                title: Tag Order
+                required:
+                  - tag
+                properties:
+                  tag:
+                    title: Tag
+                    type: string
+              template:
+                from:
+                  uri: kamelet:source
+                  steps:
+                    - setBody:
+                        expression:
+                          simple:
+                            expression: "%s"
+            """;
+
+    @Test
+    void aRightKameletFileIsValid(@TempDir Path dir) {
+        String content = TAG_KAMELET.formatted("${body} [{{tag}}]");
+        assertThat(SourceValidator.validate("tag-order-action.kamelet.yaml", 
content, catalog, null, dir)).isEmpty();
+    }
+
+    @Test
+    void aKameletPropertyWrittenAsAFunction(@TempDir Path dir) {
+        String content = TAG_KAMELET.formatted("${body} [${properties.tag}]");
+        List<String> errors = 
SourceValidator.validate("tag-order-action.kamelet.yaml", content, catalog, 
null, dir);
+        assertThat(errors).anySatisfy(e -> assertThat(e).contains("Unknown 
function: properties.tag")
+                .contains("its property tag is the placeholder {{tag}}"));
+    }
+
+    @Test
+    void aPropertyTheProjectsKameletLacks(@TempDir Path dir) throws Exception {
+        Files.writeString(dir.resolve("tag-order-action.kamelet.yaml"), """
+                apiVersion: camel.apache.org/v1
+                kind: Kamelet
+                metadata:
+                  name: tag-order-action
+                spec:
+                  template:
+                    from:
+                      uri: kamelet:source
+                      steps:
+                        - to: kamelet:sink
+                """);
+        String yaml = """
+                - route:
+                    from:
+                      uri: timer:tick
+                      steps:
+                        - to:
+                            uri: kamelet:tag-order-action
+                            parameters:
+                              tag: priority
+                """;
+        List<String> errors = KameletChecks.validateYaml(yaml, dir);
+        assertThat(errors).hasSize(1);
+        assertThat(errors.get(0)).contains("unknown property 
'tag'").contains(": none")
+                .contains("declared in its file under 
spec.definition.properties");
+    }
+
+    @Test
+    void theEndsOfAKameletTemplateAreNotKamelets() {
+        String yaml = """
+                apiVersion: camel.apache.org/v1
+                kind: Kamelet
+                metadata:
+                  name: my-action
+                spec:
+                  template:
+                    from:
+                      uri: kamelet:source
+                      steps:
+                        - to: kamelet:sink
+                """;
+        assertThat(KameletChecks.validateYaml(yaml, null)).isEmpty();
+    }
+
+    @Test
+    void theSourceValidatorChecksKamelets(@TempDir Path dir) {
+        String yaml = """
+                - route:
+                    from:
+                      uri: kamelet:timer-source
+                      parameters:
+                        period: 2000
+                      steps:
+                        - to: kamelet:log-sink
+                """;
+        List<String> errors = SourceValidator.validate("orders.camel.yaml", 
yaml, catalog, null, dir);
+        assertThat(errors).anySatisfy(e -> assertThat(e).contains("the 
required property message is missing"));
+    }
+
+    @Test
+    void theCatalogDocOfAKamelet() {
+        JsonObject doc = CatalogDocs.catalogDoc(catalog, "kafka-source", null, 
null, null, null, false, false, null);
+        assertThat(doc.getString("kind")).isEqualTo("kamelet");
+        assertThat(doc.getString("yaml"))
+                .startsWith("from: {uri: kamelet:kafka-source, parameters: 
{topic: <topic>, bootstrapServers:");
+        
assertThat(doc.toJson()).contains("\"name\":\"saslAuthType\"").contains("\"defaultValue\":\"NONE\"");
+    }
+
+    @Test
+    void theCatalogDocSuggestsAKamelet() {
+        JsonObject doc = CatalogDocs.catalogDoc(catalog, 
"kafka-not-secured-source", null, null, null, null, false,
+                false, null);
+        assertThat(doc.toJson()).contains("kafka-source");
+    }
+
+    @Test
+    void theCatalogFindsKamelets() {
+        JsonObject found = CatalogDocs.find(catalog, "kafka", "kamelet", 10);
+        
assertThat(found.toJson()).contains("\"kamelet:kafka-sink\"").contains("\"kamelet:kafka-source\"");
+        JsonObject all = CatalogDocs.find(catalog, "timer source", null, 10);
+        assertThat(all.toJson()).contains("\"kamelet:timer-source\"");
+    }
+}
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 a4b20b6be28a..80327185eb75 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
@@ -53,7 +53,7 @@ public class AuthoringTools {
     RepeatedCallSessions repeatedCalls;
 
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
-          description = "Camel catalog documentation of a component, data 
format, language, EIP, built-in bean or the "
+          description = "Camel catalog documentation of a component, data 
format, language, EIP, Kamelet, built-in bean or the "
                         + "Java API (description, options, Maven coordinates), 
with the URI rules of a component. For "
                         + "simple also its syntax rules, functions and 
operators: count and names by group, or with "
                         + "optionsFilter the matching ones with parameters and 
examples. kind=api is the Java API to "
@@ -69,7 +69,7 @@ public class AuthoringTools {
                      required = false) String name,
             @ToolArg(description = "Endpoint URI to check, e.g. 
kafka:orders?brokers=host:9092",
                      required = false) String endpoint,
-            @ToolArg(description = "component, dataformat, language, eip, bean 
or api (auto-detected; a bean is a built-in class such as 
StringAggregationStrategy, with how to declare and use it; api is the Java API 
to call from a bean or script before writing it: Exchange, Message, 
CamelContext, Registry, ProducerTemplate, Processor, AggregationStrategy, 
Predicate, Expression, TypeConverter, or the variables of groovy, js, python, 
java scripts)",
+            @ToolArg(description = "component, dataformat, language, eip, 
kamelet, bean or api (auto-detected; a bean is a built-in class such as 
StringAggregationStrategy, with how to declare and use it; api is the Java API 
to call from a bean or script before writing it: Exchange, Message, 
CamelContext, Registry, ProducerTemplate, Processor, AggregationStrategy, 
Predicate, Expression, TypeConverter, or the variables of groovy, js, python, 
java scripts)",
                      required = false) String kind,
             @ToolArg(description = "common (default: no deprecated or 
advanced), required, all or false",
                      required = false) String includeOptions,
@@ -88,14 +88,14 @@ public class AuthoringTools {
     }
 
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
-          description = "Finds Camel components, data formats, languages and 
EIPs by a protocol, product, alias or "
+          description = "Finds Camel components, data formats, languages, EIPs 
and Kamelets by a protocol, product, alias or "
                         + "other term that is not the exact name (mqtt, s3, 
snowflake, csv, fan-out, dedup): best "
                         + "match first with title and description. 
camel_catalog_doc then gives the options of one.")
     @MetaField(prefix = "camel.apache.org/", name = "deterministic", type = 
MetaField.Type.BOOLEAN, value = "true")
     public JsonObject camel_catalog_find(
             McpConnection connection,
             @ToolArg(description = "What to look for, e.g. mqtt, s3, database, 
csv, fan-out", required = true) String term,
-            @ToolArg(description = "component, dataformat, language, eip or 
bean (default: all); bean with an interface name such as AggregationStrategy 
lists the built-in implementations",
+            @ToolArg(description = "component, dataformat, language, eip, 
kamelet or bean (default: all); bean with an interface name such as 
AggregationStrategy lists the built-in implementations",
                      required = false) String kind,
             @ToolArg(description = "Maximum matches per kind (default 10)", 
required = false) Integer limit,
             @ToolArg(description = VERSION_DESC, required = false) String 
camelVersion) {

Reply via email to