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 27a7e566b83f CAMEL-24810: camel-jbang-mcp - 
camel_dependency_for_class: which Maven dependency provides a class, and how to 
declare it
27a7e566b83f is described below

commit 27a7e566b83f29382ed5aacb73618fb1cf63c51c
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Sep 18 15:16:04 2026 +0200

    CAMEL-24810: camel-jbang-mcp - camel_dependency_for_class: which Maven 
dependency provides a class, and how to declare it
    
    A shared authoring tool (camel-jbang-core, wrapped in camel-jbang-mcp) 
answers which
    dependency provides a class and how to declare it. Local first, online 
last, and the answer
    names its source: the known dependencies camel run downloads on demand 
(nothing to declare
    there), a Camel component's artifact for each runtime (camel-<name> with 
camel-bom on Main,
    the -starter with camel-spring-boot-bom on Spring Boot, 
camel-quarkus-<name> with
    camel-quarkus-bom on Quarkus), and only with mavenCentral=true a Maven 
Central class search:
    narrowed to the package prefix as group first, the artifact named by the 
package preferred,
    shaded jars penalised, the newest release from the date-ordered version 
list with
    pre-releases skipped, marked as a guess with the candidates listed. The JDK 
HttpClient with
    the JVM proxy settings is used; Central throttles repeated queries, so a 40 
s timeout, no
    retry, and a per-class cache. The answer carries camel.jbang.dependencies, 
--dep and the
    pom.xml dependency for the three runtimes. Also carries the two review 
notes left on
    #26574: the lazy initialiser of the mapping states its benign-race intent, 
and a test shows a
    class loader without the mapping files answers nothing.
    
    Closes #26594
---
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |   9 +
 .../modules/ROOT/pages/camel-jbang-running.adoc    |   4 +-
 .../dsl/jbang/core/commands/ai/AuthoringTools.java |  18 +
 .../dsl/jbang/core/commands/ai/BeanRefChecks.java  |  29 +-
 .../jbang/core/commands/ai/DependencyLookup.java   | 369 +++++++++++++++++++++
 .../jbang/core/commands/ai/AuthoringToolsTest.java |   4 +-
 .../core/commands/ai/DependencyLookupTest.java     | 138 ++++++++
 .../commands/ai/SourceValidatorBeanRefsTest.java   |   8 +
 .../jbang/core/commands/mcp/AuthoringTools.java    |  19 ++
 9 files changed, 592 insertions(+), 6 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 23a95e7f3ad6..ec63967b8187 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -213,6 +213,8 @@ The assistant uses `camel_error_diagnose` to identify the 
exception chain, extra
 
 * _"Check my pom.xml for missing or outdated dependencies"_ — uses 
`camel_dependency_check`
 * _"What are the latest LTS versions for Spring Boot?"_ — uses 
`camel_version_list`
+* _"Which dependency do I need for org.postgresql.ds.PGSimpleDataSource, and 
how do I add it on Quarkus?"_ —
+  uses `camel_dependency_for_class`
 
 === Migration
 
@@ -356,6 +358,13 @@ project `directory` as an argument, the runtime tools take 
the integration `name
   and returns the value (true/false for a predicate) or the syntax error, so 
an agent can check a simple
   expression before writing it into a route.
 
+| `camel_dependency_for_class`
+| Which Maven dependency provides a class, and how to declare it. Local first: 
the known dependencies
+  `camel run` downloads by itself (nothing to declare there), then a Camel 
component's artifact for each
+  runtime. Only with `mavenCentral=true` it searches Maven Central by class 
name, groups the hits by artifact,
+  takes the newest version, and marks the answer as a guess. Returns 
`camel.jbang.dependencies`, `--dep`, and
+  the `pom.xml` dependency for Camel Main, Spring Boot and Quarkus, or one 
runtime with `runtime`. Uses the
+  JVM proxy settings for the search.
 | `camel_error_diagnose`
 | See <<_error_diagnosis,Error Diagnosis>>; the same shared tool.
 |===
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc
index 5ef0ed5f678f..faf644de04ca 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc
@@ -272,7 +272,9 @@ mapped library is not the one you want, for example the 
ActiveMQ 5 client where
 picks the ActiveMQ 6 client.
 
 `camel validate` and the write tools of the Camel MCP server consult the same 
three files, so a
-bean whose class Camel CLI would download is not reported as missing.
+bean whose class Camel CLI would download is not reported as missing, and the 
MCP tool
+`camel_dependency_for_class` answers the coordinates and the declaration for a 
class from the same
+mapping, with an optional Maven Central search for the rest.
 
 To add a library to the mapping, add one line to 
`known-third-party-libraries.properties`
 in `camel-kamelet-main`: the library's own package, its `groupId:artifactId`, 
and a version
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 3a841b26faa5..56f5c5b87a49 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
@@ -304,6 +304,24 @@ public final class AuthoringTools {
                             args.get("body")).toJson();
                 }));
 
+        registry.accept(tool("camel_dependency_for_class",
+                "Which Maven dependency provides a class, and how to declare 
it: the known dependencies camel run "
+                                                           + "downloads by 
itself (nothing to declare there), a Camel component's "
+                                                           + "artifact per 
runtime, or with mavenCentral=true a Maven Central search "
+                                                           + "by class name (a 
guess, marked as such). Answers camel.jbang.dependencies, "
+                                                           + "--dep, and the 
pom.xml dependency for Camel Main, Spring Boot and Quarkus.")
+                .param("className", "string", "Fully qualified class name, 
e.g. org.postgresql.ds.PGSimpleDataSource",
+                        true)
+                .param("runtime", "string", "main, spring-boot or quarkus 
(default: all three pom forms)", false)
+                .param("mavenCentral", "boolean",
+                        "Search Maven Central when the class is not in the 
known dependencies (default false; needs network, can take up to 40 s)",
+                        false)
+                .param("camelVersion", "string", VERSION_DESC, false)
+                .executor((ctx, args) -> {
+                    applyVersion(ctx, args);
+                    return DependencyLookup.lookup(ctx, required(args, 
"className"), args.get("runtime"),
+                            bool(args, "mavenCentral", false));
+                }));
         registry.accept(tool("camel_error_diagnose",
                 "Diagnoses a Camel error from a stack trace or error message: 
the known exceptions in it with common "
                                                      + "causes and suggested 
fixes, the components and EIPs it mentions with "
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/BeanRefChecks.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/BeanRefChecks.java
index d20270a3f34a..f8803cf9c69f 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/BeanRefChecks.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/BeanRefChecks.java
@@ -348,17 +348,37 @@ final class BeanRefChecks {
      * bean is fine without a dependency declared even though the class is not 
on the CLI classpath. Matched the way the
      * runtime matches: the class name, then each enclosing package.
      */
+    /**
+     * Lazily loaded without a lock, on purpose: two threads that both find it 
null both build the same immutable-in-use
+     * map from the same resources and both publish it through the volatile 
write; the last one wins and every caller
+     * sees a complete map. A benign race, cheaper than synchronising every 
lookup.
+     */
     private static volatile Map<String, String> knownDependencies;
 
     static String knownDependency(String fqcn) {
         Map<String, String> known = knownDependencies;
         if (known == null) {
-            known = new HashMap<>();
+            known = 
loadKnownDependencies(BeanRefChecks.class.getClassLoader());
+            knownDependencies = known;
+        }
+        return findKnown(known, fqcn);
+    }
+
+    /**
+     * The lookup against the mapping files of one class loader, uncached; a 
loader without the files answers nothing.
+     */
+    static String knownDependency(String fqcn, ClassLoader loader) {
+        return findKnown(loadKnownDependencies(loader), fqcn);
+    }
+
+    private static Map<String, String> loadKnownDependencies(ClassLoader 
loader) {
+        Map<String, String> known = new HashMap<>();
+        {
             for (String name : new String[] {
                     "camel-main-known-dependencies.properties", 
"camel-component-known-dependencies.properties",
                     "camel-thirdparty-known-dependencies.properties" }) {
                 try {
-                    Enumeration<URL> resources = 
BeanRefChecks.class.getClassLoader().getResources(name);
+                    Enumeration<URL> resources = loader.getResources(name);
                     while (resources.hasMoreElements()) {
                         try (InputStream is = 
resources.nextElement().openStream()) {
                             Properties prop = new Properties();
@@ -372,8 +392,11 @@ final class BeanRefChecks {
                     // the mapping is an optimisation of the message, not a 
requirement
                 }
             }
-            knownDependencies = known;
         }
+        return known;
+    }
+
+    private static String findKnown(Map<String, String> known, String fqcn) {
         String prefix = fqcn;
         String gav = known.get(prefix);
         while (gav == null && prefix.lastIndexOf('.') != -1) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/DependencyLookup.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/DependencyLookup.java
new file mode 100644
index 000000000000..68d5e3c2729a
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/DependencyLookup.java
@@ -0,0 +1,369 @@
+/*
+ * 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.net.ProxySelector;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+/**
+ * Answers which Maven dependency provides a class, and how to declare it, for 
the {@code camel_dependency_for_class}
+ * tool (CAMEL-24810). Local first, online last:
+ * <ol>
+ * <li>the known dependencies camel run downloads on demand (the three mapping 
files of camel-kamelet-main, matched by
+ * the class and then each enclosing package, see {@link 
BeanRefChecks#knownDependency}); a hit also means camel run
+ * needs no declaration;</li>
+ * <li>a Camel component, data format or language class answers its {@code 
camel-} artifact for each runtime;</li>
+ * <li>only when asked ({@code mavenCentral=true}): Maven Central's class 
search, grouped by artifact, the newest
+ * version fetched separately, and marked as a guess.</li>
+ * </ol>
+ * The declaration forms cover camel run ({@code camel.jbang.dependencies}, 
{@code --dep}) and the pom.xml of the three
+ * runtimes: Camel Main, Spring Boot and Quarkus. A third-party library has 
the same coordinates in every runtime; a
+ * Camel component does not.
+ */
+public final class DependencyLookup {
+
+    static final String CENTRAL_SEARCH = 
"https://search.maven.org/solrsearch/select";;
+    private static final Map<String, JsonObject> CENTRAL_CACHE = new 
ConcurrentHashMap<>();
+
+    private DependencyLookup() {
+    }
+
+    /**
+     * Fetches a URL as text; replaced in tests. Uses the JVM proxy settings 
(http.proxyHost, https.proxyHost,
+     * java.net.useSystemProxies) like the other CLI callers. Maven Central's 
search answers in under a second, but
+     * throttles a client that repeats queries by holding each request for 
about thirty seconds before answering, so the
+     * timeout is set just beyond that and the request is not repeated; 
answers are cached per class.
+     */
+    public static String fetch(String url) {
+        try {
+            HttpClient hc = 
HttpClient.newBuilder().proxy(ProxySelector.getDefault())
+                    .connectTimeout(Duration.ofSeconds(10)).build();
+            HttpResponse<String> res = hc.send(HttpRequest.newBuilder(new 
URI(url))
+                    .header("User-Agent", "Apache Camel 
JBang").timeout(Duration.ofSeconds(40)).build(),
+                    HttpResponse.BodyHandlers.ofString());
+            if (res.statusCode() != 200) {
+                throw new ToolExecutionException("Maven Central search 
answered HTTP " + res.statusCode());
+            }
+            return res.body();
+        } catch (ToolExecutionException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new ToolExecutionException(
+                    "Maven Central search failed (" + e.getMessage() + "): try 
again later, or declare the dependency "
+                                             + "yourself with 
camel.jbang.dependencies=<groupId>:<artifactId>:<version>",
+                    e);
+        }
+    }
+
+    public static JsonObject lookup(ToolContext ctx, String className, String 
runtime, boolean mavenCentral) {
+        return lookup(ctx, className, runtime, mavenCentral, 
DependencyLookup::fetch);
+    }
+
+    static JsonObject lookup(
+            ToolContext ctx, String className, String runtime, boolean 
mavenCentral, Function<String, String> fetcher) {
+        String name = className.trim();
+        if (name.startsWith("#class:")) {
+            name = name.substring(7);
+        }
+        if (name.endsWith(".class")) {
+            name = name.substring(0, name.length() - 6);
+        }
+        String rt = runtime == null || runtime.isBlank() ? null : 
runtime.trim().toLowerCase(Locale.ROOT);
+        if (rt != null && !rt.equals("main") && !rt.equals("spring-boot") && 
!rt.equals("quarkus")) {
+            throw new ToolExecutionException("runtime must be main, 
spring-boot or quarkus");
+        }
+        JsonObject answer = new JsonObject();
+        answer.put("class", name);
+
+        String known = BeanRefChecks.knownDependency(name);
+        if (known != null) {
+            if (known.startsWith("camel:")) {
+                // the context carries a version only when one was selected; 
the catalog's is the CLI's own
+                String version = ctx.camelVersion() != null ? 
ctx.camelVersion() : ctx.catalog().getCatalogVersion();
+                return camelArtifact(answer, known.substring(6), version, rt, 
"camel-component");
+            }
+            String[] parts = known.split(":");
+            if (parts.length >= 3) {
+                answer.put("source", "known-dependencies");
+                answer.put("autoDownload", true);
+                answer.put("note", "camel run downloads this dependency when 
the class is used; nothing to declare "
+                                   + "there. Declare it in a Maven project.");
+                return declare(answer, parts[0], parts[1], parts[2], rt);
+            }
+        }
+        if (!mavenCentral) {
+            answer.put("source", "unknown");
+            answer.put("autoDownload", false);
+            answer.put("note", "Not in the known dependencies. Call again with 
mavenCentral=true to search Maven "
+                               + "Central by class name, or declare the 
dependency yourself: "
+                               + 
"camel.jbang.dependencies=<groupId>:<artifactId>:<version> in 
application.properties, "
+                               + "--dep on camel run, or a pom.xml 
dependency.");
+            return answer;
+        }
+        if (!name.contains(".") || 
Character.isLowerCase(name.charAt(name.lastIndexOf('.') + 1))) {
+            throw new ToolExecutionException(
+                    "Maven Central is searched by fully qualified class name; 
give a class, not a package");
+        }
+        JsonObject hit = centralSearch(name, fetcher);
+        if (hit == null) {
+            answer.put("source", "unknown");
+            answer.put("autoDownload", false);
+            answer.put("note", "Not in the known dependencies and Maven 
Central has no artifact with this class.");
+            return answer;
+        }
+        answer.put("source", "maven-central");
+        answer.put("autoDownload", false);
+        answer.put("note", "A Maven Central search result: the artifact whose 
group and name best match the package, "
+                           + "and its newest release (pre-releases skipped). 
Check it before adding it to a project; "
+                           + "a shaded copy of the class may exist in other 
artifacts.");
+        answer.put("candidates", hit.get("candidates"));
+        return declare(answer, hit.getString("groupId"), 
hit.getString("artifactId"), hit.getString("version"), rt);
+    }
+
+    /** A Camel component, data format or language: the artifact differs per 
runtime, the version is the BOM's. */
+    static JsonObject camelArtifact(JsonObject answer, String shortName, 
String camelVersion, String rt, String source) {
+        answer.put("source", source);
+        answer.put("autoDownload", true);
+        answer.put("groupId", "org.apache.camel");
+        answer.put("artifactId", "camel-" + shortName);
+        answer.put("version", camelVersion);
+        answer.put("note", "A Camel artifact: camel run resolves it by itself. 
In a Maven project use the runtime's "
+                           + "artifact and BOM below; the BOM manages the 
version.");
+        JsonObject declare = new JsonObject();
+        declare.put("jbang", "camel.jbang.dependencies=camel:" + shortName);
+        declare.put("cli", "--dep=camel:" + shortName);
+        if (rt == null || rt.equals("main")) {
+            declare.put("pomMain", pom("org.apache.camel", "camel-" + 
shortName, null,
+                    "org.apache.camel:camel-bom:" + camelVersion));
+        }
+        if (rt == null || rt.equals("spring-boot")) {
+            declare.put("pomSpringBoot", pom("org.apache.camel.springboot", 
"camel-" + shortName + "-starter", null,
+                    "org.apache.camel.springboot:camel-spring-boot-bom:" + 
camelVersion));
+        }
+        if (rt == null || rt.equals("quarkus")) {
+            declare.put("pomQuarkus", pom("org.apache.camel.quarkus", 
"camel-quarkus-" + shortName, null,
+                    "org.apache.camel.quarkus:camel-quarkus-bom:<camel-quarkus 
version>"));
+        }
+        answer.put("declare", declare);
+        return answer;
+    }
+
+    /** A third-party library: the same coordinates in every runtime. */
+    static JsonObject declare(JsonObject answer, String groupId, String 
artifactId, String version, String rt) {
+        answer.put("groupId", groupId);
+        answer.put("artifactId", artifactId);
+        answer.put("version", version);
+        boolean placeholder = version.startsWith("${");
+        if (placeholder) {
+            answer.put("versionNote", "The version is a property of Camel's 
own build; camel run resolves it from "
+                                      + "the camel-dependencies POM. In a 
project pick the version you want.");
+        }
+        String gav = groupId + ":" + artifactId + ":" + version;
+        JsonObject declare = new JsonObject();
+        declare.put("jbang", "camel.jbang.dependencies=" + gav);
+        declare.put("cli", "--dep=" + gav);
+        String v = placeholder ? "<version>" : version;
+        if (rt == null || rt.equals("main")) {
+            declare.put("pomMain", pom(groupId, artifactId, v, null));
+        }
+        if (rt == null || rt.equals("spring-boot")) {
+            declare.put("pomSpringBoot", pom(groupId, artifactId, v, null));
+        }
+        if (rt == null || rt.equals("quarkus")) {
+            declare.put("pomQuarkus", pom(groupId, artifactId, v, null));
+        }
+        answer.put("declare", declare);
+        return answer;
+    }
+
+    static String pom(String groupId, String artifactId, String version, 
String bom) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("<dependency>\n  
<groupId>").append(groupId).append("</groupId>\n  
<artifactId>").append(artifactId)
+                .append("</artifactId>\n");
+        if (version != null) {
+            sb.append("  <version>").append(version).append("</version>\n");
+        }
+        sb.append("</dependency>");
+        if (bom != null) {
+            sb.append("\n(version managed by the BOM ").append(bom).append(" 
imported in dependencyManagement)");
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Maven Central's class search returns one document per version and 
shaded copy, newest score first, and a common
+     * class has tens of thousands of them (20,737 for 
org.apache.commons.text.StringSubstitutor), so the first page may
+     * hold only repackaged copies. The publisher's group id is almost always 
a prefix of the package, so the search is
+     * first narrowed to each package prefix as the group 
("org.apache.commons.text", then "org.apache.commons", ...)
+     * and only then run unqualified. Within the hits the artifact whose name 
is in the package wins, shaded and uber
+     * jars lose, and the version is the newest stable one seen (a pre-release 
latestVersion such as 5.0.0-alpha.16
+     * loses to the newest release).
+     */
+    static JsonObject centralSearch(String className, Function<String, String> 
fetcher) {
+        JsonObject cached = CENTRAL_CACHE.get(className);
+        if (cached != null) {
+            return cached;
+        }
+        String pkg = className.substring(0, className.lastIndexOf('.'));
+        List<Object> docs = new ArrayList<>();
+        String prefix = pkg;
+        while (docs.isEmpty() && prefix.contains(".")) {
+            docs = docs(parse(fetcher.apply(CENTRAL_SEARCH + "?q="
+                                            + URLEncoder.encode("fc:\"" + 
className + "\" AND g:\"" + prefix + "\"",
+                                                    StandardCharsets.UTF_8)
+                                            + "&rows=50&wt=json")));
+            prefix = prefix.substring(0, prefix.lastIndexOf('.'));
+        }
+        if (docs.isEmpty()) {
+            docs = docs(parse(fetcher.apply(CENTRAL_SEARCH + "?q="
+                                            + URLEncoder.encode("fc:\"" + 
className + "\"", StandardCharsets.UTF_8)
+                                            + "&rows=50&wt=json")));
+        }
+        Map<String, Integer> counts = new LinkedHashMap<>();
+        Map<String, List<String>> versions = new LinkedHashMap<>();
+        for (Object o : docs) {
+            JsonObject d = (JsonObject) o;
+            String ga = d.getString("g") + ":" + d.getString("a");
+            counts.merge(ga, 1, Integer::sum);
+            if (d.getString("v") != null) {
+                versions.computeIfAbsent(ga, k -> new 
ArrayList<>()).add(d.getString("v"));
+            }
+        }
+        if (counts.isEmpty()) {
+            return null;
+        }
+        String lowerPkg = pkg.toLowerCase(Locale.ROOT);
+        String best = null;
+        int bestScore = Integer.MIN_VALUE;
+        for (Map.Entry<String, Integer> e : counts.entrySet()) {
+            String[] ga = e.getKey().split(":");
+            String g = ga[0].toLowerCase(Locale.ROOT);
+            String a = ga[1].toLowerCase(Locale.ROOT);
+            int score = 0;
+            if (lowerPkg.startsWith(g)) {
+                score += 4;
+            }
+            if (lowerPkg.contains(a.replace('-', '.')) || lowerPkg.contains(a) 
|| lowerPkg.endsWith(a)) {
+                score += 2;
+            }
+            if (a.contains("shaded") || a.contains("uber") || 
a.endsWith("-all") || a.contains("bundle")
+                    || a.contains("-sdk")) {
+                score -= 3;
+            }
+            if (score > bestScore) {
+                bestScore = score;
+                best = e.getKey();
+            }
+        }
+        String[] ga = best.split(":");
+        // the artifact's versions newest first (core=gav sorts by release 
date): the newest release wins over a
+        // newer pre-release, and over whatever old versions the class search 
happened to return
+        String vq = URLEncoder.encode("g:\"" + ga[0] + "\" AND a:\"" + ga[1] + 
"\"", StandardCharsets.UTF_8);
+        List<String> newest = new ArrayList<>();
+        for (Object o : docs(parse(fetcher.apply(CENTRAL_SEARCH + "?q=" + vq + 
"&core=gav&rows=25&wt=json")))) {
+            String v = ((JsonObject) o).getString("v");
+            if (v != null) {
+                newest.add(v);
+            }
+        }
+        String version = newestStable(newest, versions.getOrDefault(best, 
List.of()));
+        JsonObject hit = new JsonObject();
+        hit.put("groupId", ga[0]);
+        hit.put("artifactId", ga[1]);
+        hit.put("version", version);
+        JsonArray candidates = new JsonArray();
+        int n = 0;
+        for (Map.Entry<String, Integer> e : counts.entrySet()) {
+            if (n++ < 5) {
+                candidates.add(e.getKey() + " (" + e.getValue() + " 
versions)");
+            }
+        }
+        hit.put("candidates", candidates);
+        CENTRAL_CACHE.put(className, hit);
+        return hit;
+    }
+
+    private static final java.util.regex.Pattern PRERELEASE
+            = 
java.util.regex.Pattern.compile("(?i)(alpha|beta|rc|snapshot|preview|milestone|-m\\d|\\.m\\d|-ea|cr\\d)");
+
+    /**
+     * The first release in the date-ordered list (newest first); a 
pre-release is skipped when a release follows it
+     * (5.0.0-alpha.16 loses to 4.12.0). With no release in that list the 
newest one seen anywhere, else the newest
+     * entry whatever it is.
+     */
+    static String newestStable(List<String> newestFirst, List<String> seen) {
+        for (String v : newestFirst) {
+            if (!PRERELEASE.matcher(v).find()) {
+                return v;
+            }
+        }
+        String best = null;
+        for (String v : seen) {
+            if (!PRERELEASE.matcher(v).find() && (best == null || 
compareVersions(v, best) > 0)) {
+                best = v;
+            }
+        }
+        if (best != null) {
+            return best;
+        }
+        return newestFirst.isEmpty() ? (seen.isEmpty() ? null : seen.get(0)) : 
newestFirst.get(0);
+    }
+
+    static int compareVersions(String a, String b) {
+        String[] x = a.split("[^0-9]+");
+        String[] y = b.split("[^0-9]+");
+        for (int i = 0; i < Math.max(x.length, y.length); i++) {
+            long p = i < x.length && !x[i].isEmpty() ? Long.parseLong(x[i]) : 
0;
+            long q = i < y.length && !y[i].isEmpty() ? Long.parseLong(y[i]) : 
0;
+            if (p != q) {
+                return Long.compare(p, q);
+            }
+        }
+        return 0;
+    }
+
+    private static JsonObject parse(String body) {
+        try {
+            return (JsonObject) Jsoner.deserialize(body);
+        } catch (Exception e) {
+            throw new ToolExecutionException("Maven Central search returned no 
JSON", e);
+        }
+    }
+
+    private static List<Object> docs(JsonObject response) {
+        JsonObject r = response == null ? null : response.getMap("response");
+        JsonArray docs = r == null ? null : r.getCollection("docs");
+        return docs == null ? new ArrayList<>() : docs;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
index 8a48d3a9ea53..416f099b0f67 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
@@ -76,7 +76,7 @@ class AuthoringToolsTest {
         assertEquals(List.of("camel_catalog_doc", "camel_catalog_find", 
"camel_catalog_sample", "camel_validate_source",
                 "camel_get_files",
                 "camel_write_file", "camel_run", "camel_control", 
"camel_get_log", "camel_get_errors",
-                "camel_eval_expression", "camel_error_diagnose"), names);
+                "camel_eval_expression", "camel_dependency_for_class", 
"camel_error_diagnose"), names);
         for (ToolDescriptor td : shared) {
             assertTrue(td.name().startsWith("camel_"), td.name());
             assertFalse(td.description().isBlank(), td.name());
@@ -88,7 +88,7 @@ class AuthoringToolsTest {
         }
         for (String reading : List.of("camel_catalog_doc", 
"camel_catalog_sample", "camel_get_files", "camel_get_log",
                 "camel_get_errors",
-                "camel_eval_expression", "camel_error_diagnose", 
"camel_validate_source")) {
+                "camel_eval_expression", "camel_dependency_for_class", 
"camel_error_diagnose", "camel_validate_source")) {
             assertTrue(ToolRegistry.findTool(reading).isReadOnly(), reading);
         }
         assertTrue(ToolRegistry.findTool("camel_control").isDestructive());
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/DependencyLookupTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/DependencyLookupTest.java
new file mode 100644
index 000000000000..955a999ef531
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/DependencyLookupTest.java
@@ -0,0 +1,138 @@
+/*
+ * 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.util.Map;
+import java.util.function.Function;
+
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class DependencyLookupTest {
+
+    private final ToolContext ctx = new ToolContext();
+
+    @Test
+    void 
aKnownThirdPartyClassAnswersItsCoordinatesAndNeedsNoDeclarationForCamelRun() {
+        JsonObject a = DependencyLookup.lookup(ctx, 
"org.postgresql.ds.PGSimpleDataSource", null, false);
+        assertThat(a.getString("source")).isEqualTo("known-dependencies");
+        assertThat(a.getBoolean("autoDownload")).isTrue();
+        assertThat(a.getString("groupId")).isEqualTo("org.postgresql");
+        assertThat(a.getString("artifactId")).isEqualTo("postgresql");
+        assertThat(a.getString("version")).doesNotStartWith("${");
+        JsonObject d = a.getMap("declare");
+        
assertThat(d.getString("jbang")).startsWith("camel.jbang.dependencies=org.postgresql:postgresql:");
+        
assertThat(d.getString("cli")).startsWith("--dep=org.postgresql:postgresql:");
+        
assertThat(d.getString("pomMain")).contains("<artifactId>postgresql</artifactId>").contains("<version>");
+        
assertThat(d.getString("pomSpringBoot")).isEqualTo(d.getString("pomMain"));
+        
assertThat(d.getString("pomQuarkus")).isEqualTo(d.getString("pomMain"));
+    }
+
+    @Test
+    void aCamelComponentClassAnswersTheArtifactOfEachRuntime() {
+        JsonObject a = DependencyLookup.lookup(ctx, 
"org.apache.camel.component.kafka.KafkaComponent", null, false);
+        assertThat(a.getString("source")).isEqualTo("camel-component");
+        assertThat(a.getString("artifactId")).isEqualTo("camel-kafka");
+        assertThat(a.getString("version")).isNotNull().doesNotContain("null");
+        JsonObject d = a.getMap("declare");
+        assertThat(d.getString("cli")).isEqualTo("--dep=camel:kafka");
+        
assertThat(d.getString("pomMain")).contains("<groupId>org.apache.camel</groupId>")
+                
.contains("<artifactId>camel-kafka</artifactId>").contains("camel-bom");
+        
assertThat(d.getString("pomSpringBoot")).contains("org.apache.camel.springboot")
+                
.contains("camel-kafka-starter").contains("camel-spring-boot-bom");
+        
assertThat(d.getString("pomQuarkus")).contains("org.apache.camel.quarkus").contains("camel-quarkus-kafka")
+                .contains("camel-quarkus-bom");
+
+        JsonObject only = DependencyLookup.lookup(ctx, 
"org.apache.camel.component.kafka.KafkaComponent",
+                "spring-boot", false);
+        JsonObject onlyDeclare = only.getMap("declare");
+        assertThat(onlyDeclare.keySet().stream().map(String::valueOf).toList())
+                .containsExactlyInAnyOrder("jbang", "cli", "pomSpringBoot");
+    }
+
+    @Test
+    void anUnknownClassWithoutMavenCentralSaysHowToDeclareIt() {
+        JsonObject a = DependencyLookup.lookup(ctx, 
"com.example.pool.NoSuchDataSource", null, false);
+        assertThat(a.getString("source")).isEqualTo("unknown");
+        assertThat(a.getBoolean("autoDownload")).isFalse();
+        
assertThat(a.getString("note")).contains("mavenCentral=true").contains("camel.jbang.dependencies");
+        assertThat(a.containsKey("declare")).isFalse();
+    }
+
+    @Test
+    void 
mavenCentralIsSearchedOnlyWhenAskedAndTheBestArtifactAndNewestVersionArePicked()
 {
+        // canned answers: the class search lists old versions first and a 
shaded copy; the second query gives the
+        // newest version of the chosen artifact
+        Function<String, String> fetcher = url -> {
+            if (url.contains("fc%3A") && url.contains("g%3A")) {
+                // the group-prefix narrowed searches: nothing under 
com.example.pool or com.example, as on Central
+                return "{\"response\":{\"numFound\":0,\"docs\":[]}}";
+            }
+            if (url.contains("fc%3A")) {
+                return """
+                        {"response":{"numFound":3,"docs":[
+                          {"g":"com.example","a":"example-pool","v":"1.3.3"},
+                          {"g":"org.acme","a":"acme-shaded-all","v":"9.9"},
+                          
{"g":"com.example","a":"example-pool","v":"1.3.2"}]}}""";
+            }
+            // the artifact's versions newest first: two pre-releases ahead of 
the newest release
+            return """
+                    {"response":{"numFound":4,"docs":[
+                      
{"g":"com.example","a":"example-pool","v":"8.0.0-alpha.2"},
+                      
{"g":"com.example","a":"example-pool","v":"8.0.0-alpha.1"},
+                      {"g":"com.example","a":"example-pool","v":"7.0.2"},
+                      {"g":"com.example","a":"example-pool","v":"7.0.1"}]}}""";
+        };
+        JsonObject a = DependencyLookup.lookup(ctx, 
"com.example.pool.PoolDataSource", "main", true, fetcher);
+        assertThat(a.getString("source")).isEqualTo("maven-central");
+        assertThat(a.getBoolean("autoDownload")).isFalse();
+        assertThat(a.getString("groupId")).isEqualTo("com.example");
+        assertThat(a.getString("artifactId")).isEqualTo("example-pool");
+        assertThat(a.getString("version")).isEqualTo("7.0.2");
+        assertThat(a.getString("note")).contains("Check it");
+        assertThat(((java.util.Collection<?>) 
a.get("candidates")).size()).isEqualTo(2);
+        JsonObject declare = a.getMap("declare");
+        
assertThat(declare.getString("pomMain")).contains("<version>7.0.2</version>");
+
+        
assertThat(DependencyLookup.newestStable(java.util.List.of("5.0.0-alpha.16", 
"4.12.0", "4.11.0"),
+                java.util.List.of())).isEqualTo("4.12.0");
+        assertThat(DependencyLookup.newestStable(java.util.List.of(), 
java.util.List.of("1.0", "1.2", "1.1")))
+                .isEqualTo("1.2");
+        assertThat(DependencyLookup.newestStable(java.util.List.of("2.0-RC1"), 
java.util.List.of("2.0-RC1")))
+                .isEqualTo("2.0-RC1");
+        assertThat(DependencyLookup.compareVersions("4.12.0", 
"4.9.1")).isPositive();
+
+        assertThatThrownBy(() -> DependencyLookup.lookup(ctx, 
"com.example.pool", null, true, fetcher))
+                
.isInstanceOf(ToolExecutionException.class).hasMessageContaining("class, not a 
package");
+        assertThatThrownBy(() -> DependencyLookup.lookup(ctx, "x.Y", 
"wildfly", false))
+                
.isInstanceOf(ToolExecutionException.class).hasMessageContaining("runtime must 
be");
+    }
+
+    @Test
+    void theToolIsInTheSharedRegistryAndReadOnly() {
+        ToolDescriptor td = 
ToolRegistry.findTool("camel_dependency_for_class");
+        assertThat(td).isNotNull();
+        assertThat(td.isReadOnly()).isTrue();
+        assertThat(td.isCore()).isFalse();
+        Object result = ToolRegistry.execute("camel_dependency_for_class", ctx,
+                Map.of("className", 
"#class:org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory"));
+        assertThat(((JsonObject) 
result).getString("artifactId")).isEqualTo("artemis-jakarta-client-all");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorBeanRefsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorBeanRefsTest.java
index d9994e238ee2..aa5a2dbcb8a6 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorBeanRefsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorBeanRefsTest.java
@@ -376,6 +376,14 @@ public class SourceValidatorBeanRefsTest {
         
assertThat(BeanRefChecks.knownDependency("com.example.NoSuchThing")).isNull();
     }
 
+    @Test
+    void withoutTheMappingFilesOnTheClasspathNothingIsKnownAndNothingFails() {
+        // a class loader with no resources at all: the check falls back to 
"unknown" instead of failing
+        ClassLoader empty = new java.net.URLClassLoader(new java.net.URL[0], 
null);
+        
assertThat(BeanRefChecks.knownDependency("org.postgresql.ds.PGSimpleDataSource",
 empty)).isNull();
+        
assertThat(BeanRefChecks.knownDependency("org.postgresql.ds.PGSimpleDataSource")).isNotNull();
+    }
+
     @Test
     void aClassFromAnUnknownLibrarySaysHowToDeclareTheDependency(@TempDir Path 
dir) throws IOException {
         // a package no mapping will ever name: com.zaxxer.hikari is mapped as 
a package by CAMEL-24809
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 c525c9fe0a7e..f84d670c26b2 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
@@ -208,6 +208,25 @@ public class AuthoringTools {
                 "name", name));
     }
 
+    @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = true),
+          description = "Which Maven dependency provides a class, and how to 
declare it: the known dependencies "
+                        + "camel run downloads by itself (nothing to declare 
there), a Camel component's artifact per "
+                        + "runtime, or with mavenCentral=true a Maven Central 
search by class name (a guess, marked as "
+                        + "such). Answers camel.jbang.dependencies, --dep, and 
the pom.xml dependency for Camel Main, "
+                        + "Spring Boot and Quarkus.")
+    public JsonObject camel_dependency_for_class(
+            @ToolArg(description = "Fully qualified class name, e.g. 
org.postgresql.ds.PGSimpleDataSource",
+                     required = true) String className,
+            @ToolArg(description = "main, spring-boot or quarkus (default: all 
three pom forms)",
+                     required = false) String runtime,
+            @ToolArg(description = "Search Maven Central when the class is not 
in the known dependencies (default "
+                                   + "false; needs network, can take up to 40 
s)",
+                     required = false) Boolean mavenCentral,
+            @ToolArg(description = VERSION_DESC, required = false) String 
camelVersion) {
+        return call("camel_dependency_for_class", args("className", className, 
"runtime", runtime, "mavenCentral",
+                mavenCentral, "camelVersion", camelVersion));
+    }
+
     /** Runs the registry tool of the same name and hands its JSON back; a 
tool error becomes an MCP tool error. */
     static JsonObject call(String tool, Map<String, String> args) {
         try {

Reply via email to