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

davsclaus pushed a commit to branch fix/CAMEL-24810
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 581fc778a8a4188b7cc9995b1a7173dd38d28e4c
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Sep 18 13:47:33 2026 +0200

    CAMEL-24810: camel-jbang-mcp - camel_dependency_for_class: which Maven 
dependency provides a class, and how to declare it
    
    A new shared authoring tool (camel-jbang-core, wrapped in camel-jbang-mcp) 
answers the
    question a bean of a third-party type raises: which dependency provides 
this class, and how
    do I declare it. Local first, online last:
    
    1. 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 
(the lookup the
       validator uses since #26574). A hit also means camel run needs no 
declaration.
    2. A Camel component, data format or language class answers its camel- 
artifact for each
       runtime: camel-<name> with camel-bom on Camel Main, camel-<name>-starter 
with
       camel-spring-boot-bom on Spring Boot, camel-quarkus-<name> with 
camel-quarkus-bom on
       Quarkus. A third-party library has the same coordinates in every runtime.
    3. Only with mavenCentral=true: Maven Central's class search. 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 and only then run unqualified (the unqualified 
search for
       org.apache.commons.text.StringSubstitutor has 20,737 hits and its first 
page holds only
       repackaged copies). Within the hits the artifact whose name is in the 
package wins and
       shaded or uber jars lose; the version comes from the artifact's 
date-ordered version list
       (core=gav), the newest release, skipping pre-releases (okhttp 4.12.0, 
not 5.0.0-alpha.16).
       The answer is marked as a guess with the candidate artifacts listed. The 
JDK HttpClient
       with the JVM proxy settings is used, as the CLI's other HTTP calls do; 
Central throttles
       repeated queries by holding a request for about thirty seconds, so the 
timeout is 40 s,
       there is no retry, and answers are cached per class.
    
    The answer carries camel.jbang.dependencies, --dep, and the pom.xml 
dependency for the three
    runtimes (or one with runtime=main|spring-boot|quarkus), the source that 
produced it, and
    whether camel run downloads it by itself. Five tests cover the three 
sources with Central
    mocked; the MCP wrapper test checks the wrapper and its hints. Documented 
in the MCP page's
    tool table and examples and in the running page's advanced section.
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
---
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |   9 +
 .../modules/ROOT/pages/camel-jbang-running.adoc    |   4 +-
 .../dsl/jbang/core/commands/ai/AuthoringTools.java |  18 +
 .../jbang/core/commands/ai/DependencyLookup.java   | 369 +++++++++++++++++++++
 .../jbang/core/commands/ai/AuthoringToolsTest.java |   4 +-
 .../core/commands/ai/DependencyLookupTest.java     | 138 ++++++++
 .../jbang/core/commands/mcp/AuthoringTools.java    |  19 ++
 7 files changed, 558 insertions(+), 3 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/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-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