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 5432fc78c4d1 CAMEL-25586: camel-jbang-mcp - the route tools read the 
components of a route from its endpoints instead of searching its text (#27722)
5432fc78c4d1 is described below

commit 5432fc78c4d1a25ba77ba2adda81c18c322f50a2
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Oct 12 20:12:57 2026 +0200

    CAMEL-25586: camel-jbang-mcp - the route tools read the components of a 
route from its endpoints instead of searching its text (#27722)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../camel/dsl/jbang/core/commands/Explain.java     |  24 ++--
 .../camel/dsl/jbang/core/commands/Harden.java      |  20 +--
 .../jbang/core/commands/ai/RouteComponents.java    | 143 +++++++++++++++++++++
 .../core/commands/ai/RouteComponentsTest.java      | 120 +++++++++++++++++
 .../core/commands/mcp/DependencyCheckTools.java    |  15 +--
 .../commands/mcp/DependencySecurityAuditTools.java |  22 +---
 .../dsl/jbang/core/commands/mcp/ExplainTools.java  |  25 +---
 .../dsl/jbang/core/commands/mcp/HardenTools.java   |  26 +---
 8 files changed, 293 insertions(+), 102 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Explain.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Explain.java
index a37f0bee4cae..eaf7da84ad64 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Explain.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Explain.java
@@ -26,6 +26,7 @@ import java.util.Optional;
 
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.jbang.core.commands.ai.RouteComponents;
 import org.apache.camel.tooling.model.ComponentModel;
 import org.apache.camel.tooling.model.EipModel;
 import org.apache.camel.util.FileUtil;
@@ -286,7 +287,7 @@ public class Explain extends CamelCommand {
         StringBuilder prompt = new StringBuilder();
 
         if (catalogContext) {
-            appendCatalogContext(prompt, routeContent);
+            appendCatalogContext(prompt, routeContent, fileExtension);
         }
 
         prompt.append("File: ").append(fileName).append("\n");
@@ -298,28 +299,29 @@ public class Explain extends CamelCommand {
         return prompt.toString();
     }
 
-    private void appendCatalogContext(StringBuilder prompt, String 
routeContent) {
-        String catalogInfo = buildCatalogContext(routeContent);
+    private void appendCatalogContext(StringBuilder prompt, String 
routeContent, String fileExtension) {
+        String catalogInfo = buildCatalogContext(routeContent, fileExtension);
         if (catalogInfo.isEmpty()) {
             return;
         }
         prompt.append("Reference 
information:\n").append(catalogInfo).append("\n");
     }
 
-    private String buildCatalogContext(String routeContent) {
+    private String buildCatalogContext(String routeContent, String 
fileExtension) {
         StringBuilder context = new StringBuilder();
         CamelCatalog catalog = new DefaultCamelCatalog();
         String lowerContent = routeContent.toLowerCase();
 
-        appendComponentDescriptions(context, catalog, lowerContent);
+        appendComponentDescriptions(context, catalog, routeContent, 
fileExtension);
         appendEipDescriptions(context, catalog, lowerContent);
 
         return context.toString();
     }
 
-    private void appendComponentDescriptions(StringBuilder context, 
CamelCatalog catalog, String content) {
-        COMMON_COMPONENTS.stream()
-                .filter(comp -> containsComponent(content, comp))
+    private void appendComponentDescriptions(
+            StringBuilder context, CamelCatalog catalog, String content, 
String fileExtension) {
+        // the components of the route's endpoints, not every name in the text 
(CAMEL-25586)
+        RouteComponents.of(content, fileExtension, catalog, COMMON_COMPONENTS)
                 .forEach(comp -> appendModelDescription(context, 
catalog.componentModel(comp), comp, ""));
     }
 
@@ -329,12 +331,6 @@ public class Explain extends CamelCommand {
                 .forEach(eip -> appendModelDescription(context, 
catalog.eipModel(eip), eip, " (EIP)"));
     }
 
-    private boolean containsComponent(String content, String comp) {
-        return content.contains(comp + ":")
-                || content.contains("\"" + comp + "\"")
-                || content.contains("'" + comp + "'");
-    }
-
     private void appendModelDescription(StringBuilder context, Object model, 
String name, String suffix) {
         String description = getModelDescription(model);
         if (description != null) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Harden.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Harden.java
index 0b130ad11e17..380043e18731 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Harden.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Harden.java
@@ -36,6 +36,7 @@ import java.util.stream.Stream;
 
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.jbang.core.commands.ai.RouteComponents;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.tooling.model.ComponentModel;
 import org.apache.camel.util.FileUtil;
@@ -472,7 +473,7 @@ public class Harden extends CamelCommand {
         StringBuilder prompt = new StringBuilder();
 
         if (catalogContext) {
-            appendCatalogContext(prompt, routeContent);
+            appendCatalogContext(prompt, routeContent, fileExtension);
         }
 
         prompt.append("File: ").append(fileName).append("\n");
@@ -484,21 +485,20 @@ public class Harden extends CamelCommand {
         return prompt.toString();
     }
 
-    private void appendCatalogContext(StringBuilder prompt, String 
routeContent) {
-        String catalogInfo = buildCatalogContext(routeContent);
+    private void appendCatalogContext(StringBuilder prompt, String 
routeContent, String fileExtension) {
+        String catalogInfo = buildCatalogContext(routeContent, fileExtension);
         if (catalogInfo.isEmpty()) {
             return;
         }
         prompt.append("Security-relevant component 
information:\n").append(catalogInfo).append("\n");
     }
 
-    private String buildCatalogContext(String routeContent) {
+    private String buildCatalogContext(String routeContent, String 
fileExtension) {
         StringBuilder context = new StringBuilder();
         CamelCatalog catalog = new DefaultCamelCatalog();
-        String lowerContent = routeContent.toLowerCase();
 
-        SECURITY_SENSITIVE_COMPONENTS.stream()
-                .filter(comp -> containsComponent(lowerContent, comp))
+        // the components of the route's endpoints, not every name in the text 
(CAMEL-25586)
+        RouteComponents.of(routeContent, fileExtension, catalog, 
SECURITY_SENSITIVE_COMPONENTS)
                 .forEach(comp -> {
                     ComponentModel model = catalog.componentModel(comp);
                     if (model != null) {
@@ -536,12 +536,6 @@ public class Harden extends CamelCommand {
         };
     }
 
-    private boolean containsComponent(String content, String comp) {
-        return content.contains(comp + ":")
-                || content.contains("\"" + comp + "\"")
-                || content.contains("'" + comp + "'");
-    }
-
     String callOllama(String endpoint, String sysPrompt, String userPrompt) {
         JsonObject request = new JsonObject();
         request.put("model", model);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteComponents.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteComponents.java
new file mode 100644
index 000000000000..1be4b9b3c4ab
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteComponents.java
@@ -0,0 +1,143 @@
+/*
+ * 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.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.dsl.jbang.core.commands.ai.ProjectRoutes.Endpoint;
+import org.apache.camel.dsl.jbang.core.commands.ai.ProjectRoutes.Route;
+
+/**
+ * The components a route source uses: the components of its endpoints, read 
as routes of its DSL by the reader of
+ * {@code camel overview} (YAML and XML structurally, Java with the Java DSL 
parser, the endpoint DSL and constants
+ * resolved). A {@code log} step is not the log component, and a name in a 
comment or a description is no endpoint.
+ * Content that cannot be read as routes, such as a fragment of a route, falls 
back to looking for the component names
+ * in the text (CAMEL-25586).
+ */
+public final class RouteComponents {
+
+    private RouteComponents() {
+    }
+
+    /**
+     * The components the route uses, in the order of the source.
+     *
+     * @param  content the route source: YAML, XML or Java DSL
+     * @param  format  yaml, xml or java; null or blank to tell from the 
content
+     * @param  catalog the catalog the components are looked up in
+     * @return         the names of the components the catalog knows
+     */
+    public static List<String> of(String content, String format, CamelCatalog 
catalog) {
+        if (content == null || content.isBlank()) {
+            return List.of();
+        }
+        List<Route> routes;
+        try {
+            routes = ProjectRoutes.parse("route." + extension(format, 
content), content, catalog);
+        } catch (RuntimeException e) {
+            routes = List.of();
+        }
+        if (routes.isEmpty()) {
+            return mentioned(content, catalog.findComponentNames());
+        }
+        Set<String> answer = new LinkedHashSet<>();
+        for (Route r : routes) {
+            if ("rest".equals(r.kind())) {
+                // the rest DSL runs on the rest component
+                add(answer, "rest", catalog);
+            }
+            add(answer, r.from(), catalog);
+            r.consumes().forEach(e -> add(answer, e, catalog));
+            r.produces().forEach(e -> add(answer, e, catalog));
+        }
+        return new ArrayList<>(answer);
+    }
+
+    /**
+     * The given components the route uses (as {@link #of(String, String, 
CamelCatalog)}), in the order they are given.
+     *
+     * @param  content    the route source: YAML, XML or Java DSL
+     * @param  format     yaml, xml or java; null or blank to tell from the 
content
+     * @param  catalog    the catalog the components are looked up in
+     * @param  components the names of the components to look for
+     * @return            the names of the given components the route uses
+     */
+    public static List<String> of(String content, String format, CamelCatalog 
catalog, List<String> components) {
+        if (components == null || components.isEmpty()) {
+            return List.of();
+        }
+        List<String> used = of(content, format, catalog);
+        return components.stream().filter(used::contains).toList();
+    }
+
+    /**
+     * The components whose name the text has as an endpoint ({@code name:}) 
or a quoted name: the fallback for content
+     * that cannot be read as routes.
+     */
+    static List<String> mentioned(String content, List<String> components) {
+        String lower = content.toLowerCase(Locale.ROOT);
+        List<String> answer = new ArrayList<>();
+        for (String comp : components) {
+            if ((lower.contains(comp + ":") || lower.contains("\"" + comp + 
"\"") || lower.contains("'" + comp + "'"))
+                    && !answer.contains(comp)) {
+                answer.add(comp);
+            }
+        }
+        return answer;
+    }
+
+    private static void add(Set<String> answer, Endpoint e, CamelCatalog 
catalog) {
+        if (e != null && e.scheme() != null) {
+            add(answer, e.scheme().toLowerCase(Locale.ROOT), catalog);
+        }
+    }
+
+    private static void add(Set<String> answer, String scheme, CamelCatalog 
catalog) {
+        if (!answer.contains(scheme) && isComponent(scheme, catalog)) {
+            answer.add(scheme);
+        }
+    }
+
+    private static boolean isComponent(String scheme, CamelCatalog catalog) {
+        try {
+            return catalog.componentModel(scheme) != null;
+        } catch (RuntimeException e) {
+            return false;
+        }
+    }
+
+    /** The file extension of the route source: the given format, else what 
the content looks like. */
+    static String extension(String format, String content) {
+        if (format != null && !format.isBlank()) {
+            String f = format.strip().toLowerCase(Locale.ROOT);
+            return "yml".equals(f) ? "yaml" : f;
+        }
+        String s = content.stripLeading();
+        if (s.startsWith("<")) {
+            return "xml";
+        }
+        if (content.contains("RouteBuilder") || content.contains("void 
configure()")) {
+            return "java";
+        }
+        return "yaml";
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteComponentsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteComponentsTest.java
new file mode 100644
index 000000000000..7517a0d38a47
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteComponentsTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.List;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-25586: the components of a route are the components of its endpoints, 
not every component name in its text.
+ */
+class RouteComponentsTest {
+
+    private static final CamelCatalog CATALOG = new DefaultCamelCatalog();
+
+    @Test
+    void yamlRouteHasTheComponentsOfItsEndpoints() {
+        String yaml = """
+                - route:
+                    description: "reads from kafka: no, from a timer"
+                    from:
+                      uri: timer:tick
+                      steps:
+                        - log: "sent to the jms: queue"
+                        - to:
+                            uri: kafka
+                            parameters:
+                              topic: orders
+                """;
+        // the log EIP is not the log component, and names in a description or 
a message are no endpoints
+        assertThat(RouteComponents.of(yaml, null, 
CATALOG)).containsExactly("timer", "kafka");
+    }
+
+    @Test
+    void javaRouteReadsTheEndpointDslAndConstants() {
+        String java = """
+                import org.apache.camel.builder.endpoint.EndpointRouteBuilder;
+
+                public class Orders extends EndpointRouteBuilder {
+                    private static final String QUEUE = "sql:select * from 
orders";
+
+                    @Override
+                    public void configure() {
+                        // to("ftp:old") is commented out
+                        from(timer("tick"))
+                            .to(kafka("orders"))
+                            .to(QUEUE);
+                    }
+                }
+                """;
+        assertThat(RouteComponents.of(java, null, 
CATALOG)).containsExactly("timer", "kafka", "sql");
+    }
+
+    @Test
+    void xmlRouteAndTheRestDsl() {
+        String xml = """
+                <routes xmlns="http://camel.apache.org/schema/xml-io";>
+                    <route>
+                        <from uri="platform-http:/orders"/>
+                        <to uri="https://example.com/api"/>
+                    </route>
+                </routes>
+                """;
+        assertThat(RouteComponents.of(xml, "xml", 
CATALOG)).containsExactly("platform-http", "https");
+
+        String rest = """
+                - rest:
+                    get:
+                      - path: /orders
+                        to: direct:orders
+                """;
+        assertThat(RouteComponents.of(rest, "yaml", CATALOG)).contains("rest", 
"direct");
+    }
+
+    @Test
+    void textThatIsNoRouteFallsBackToTheNamesInIt() {
+        // a fragment, not a route file: look for the component names in the 
text as before
+        assertThat(RouteComponents.of("send it to \"kafka:orders\" and then 
'sql'", null, CATALOG))
+                .contains("kafka", "sql");
+    }
+
+    @Test
+    void onlyTheGivenComponents() {
+        String yaml = """
+                - from:
+                    uri: timer:tick
+                    steps:
+                      - to: exec:ls
+                      - to: kafka:orders
+                """;
+        assertThat(RouteComponents.of(yaml, null, CATALOG, List.of("kafka", 
"exec", "sql")))
+                .containsExactly("kafka", "exec");
+    }
+
+    @Test
+    void formatFromTheContent() {
+        assertThat(RouteComponents.extension(null, "  
<routes/>")).isEqualTo("xml");
+        assertThat(RouteComponents.extension(null, "class A extends 
RouteBuilder {}")).isEqualTo("java");
+        assertThat(RouteComponents.extension(null, "- from: 
timer:x")).isEqualTo("yaml");
+        assertThat(RouteComponents.extension("YML", "")).isEqualTo("yaml");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckTools.java
index 1622dbb21c45..0e9a419773ea 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckTools.java
@@ -26,6 +26,7 @@ import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
 import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.dsl.jbang.core.commands.ai.RouteComponents;
 import org.apache.camel.tooling.model.ComponentModel;
 
 /**
@@ -158,13 +159,9 @@ public class DependencyCheckTools {
     private List<MissingDependency> checkMissingDependencies(
             String routes, List<String> existingDeps, CamelCatalog catalog) {
         List<MissingDependency> missing = new ArrayList<>();
-        String lowerRoutes = routes.toLowerCase();
-
-        for (String comp : catalog.findComponentNames()) {
-            if (!containsComponent(lowerRoutes, comp)) {
-                continue;
-            }
 
+        // the components of the routes' endpoints, not every name in the text 
(CAMEL-25586)
+        for (String comp : RouteComponents.of(routes, null, catalog)) {
             ComponentModel model = catalog.componentModel(comp);
             if (model == null) {
                 continue;
@@ -268,12 +265,6 @@ public class DependencyCheckTools {
 
     // --- Helpers ---
 
-    private boolean containsComponent(String content, String comp) {
-        return content.contains(comp + ":")
-                || content.contains("\"" + comp + "\"")
-                || content.contains("'" + comp + "'");
-    }
-
     /**
      * Compare two version strings. Returns negative if v1 < v2, 0 if equal, 
positive if v1 > v2.
      */
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencySecurityAuditTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencySecurityAuditTools.java
index f204062d963f..8a8c3f522e2f 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencySecurityAuditTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencySecurityAuditTools.java
@@ -28,6 +28,7 @@ import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
 import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.dsl.jbang.core.commands.ai.RouteComponents;
 import org.apache.camel.tooling.model.ComponentModel;
 import org.apache.camel.tooling.model.SecurityAdvisoryModel;
 
@@ -162,25 +163,12 @@ public class DependencySecurityAuditTools {
     }
 
     /**
-     * Detect which components a route uses by walking the known catalog 
component names, the same way
-     * {@link DependencyCheckTools} and {@code HardenTools} do. Tokenizing the 
route text instead would treat the DSL's
-     * own structural keywords ({@code from:}, {@code to:}, {@code steps:}, 
{@code uri:}) as component schemes.
+     * The components the routes use: the components of their endpoints, read 
as routes of their DSL (CAMEL-25586).
+     * Tokenizing the route text instead would treat the DSL's own structural 
keywords ({@code from:}, {@code to:},
+     * {@code steps:}, {@code uri:}) as component schemes.
      */
     private List<String> extractUsedSchemes(String routes, CamelCatalog 
catalog) {
-        List<String> schemes = new ArrayList<>();
-        String lower = routes.toLowerCase();
-        for (String comp : catalog.findComponentNames()) {
-            if (containsComponent(lower, comp) && !schemes.contains(comp)) {
-                schemes.add(comp);
-            }
-        }
-        return schemes;
-    }
-
-    private boolean containsComponent(String content, String comp) {
-        return content.contains(comp + ":")
-                || content.contains("\"" + comp + "\"")
-                || content.contains("'" + comp + "'");
+        return RouteComponents.of(routes, null, catalog);
     }
 
     private boolean isReachable(String artifactId, List<String> usedSchemes, 
CamelCatalog catalog) {
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExplainTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExplainTools.java
index fc7c3584a9a7..87971f87e75d 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExplainTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExplainTools.java
@@ -26,6 +26,7 @@ import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
 import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.dsl.jbang.core.commands.ai.RouteComponents;
 import org.apache.camel.tooling.model.ComponentModel;
 import org.apache.camel.tooling.model.EipModel;
 
@@ -67,7 +68,7 @@ public class ExplainTools {
             String resolvedFormat = format != null && !format.isBlank() ? 
format.toLowerCase() : "yaml";
 
             // Extract and document components
-            List<String> componentNames = extractComponents(route, catalog);
+            List<String> componentNames = RouteComponents.of(route, format, 
catalog);
             List<RouteComponent> components = new ArrayList<>();
             for (String comp : componentNames) {
                 ComponentModel model = catalog.componentModel(comp);
@@ -99,22 +100,6 @@ public class ExplainTools {
         }
     }
 
-    /**
-     * Extract component names from route content.
-     */
-    private List<String> extractComponents(String route, CamelCatalog catalog) 
{
-        List<String> found = new ArrayList<>();
-        String lowerRoute = route.toLowerCase();
-
-        for (String comp : catalog.findComponentNames()) {
-            if (containsComponent(lowerRoute, comp)) {
-                found.add(comp);
-            }
-        }
-
-        return found;
-    }
-
     /**
      * Extract EIP names from route content.
      */
@@ -136,12 +121,6 @@ public class ExplainTools {
         return found;
     }
 
-    private boolean containsComponent(String content, String comp) {
-        return content.contains(comp + ":")
-                || content.contains("\"" + comp + "\"")
-                || content.contains("'" + comp + "'");
-    }
-
     private String camelCaseToDash(String text) {
         StringBuilder sb = new StringBuilder();
         for (char c : text.toCharArray()) {
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenTools.java
index e2ba938a7666..9c6eba1bbe9a 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenTools.java
@@ -30,6 +30,7 @@ import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
 import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.dsl.jbang.core.commands.ai.RouteComponents;
 import org.apache.camel.tooling.model.ComponentModel;
 import org.apache.camel.tooling.model.SecurityAdvisoryModel;
 
@@ -82,7 +83,8 @@ public class HardenTools {
             String resolvedFormat = format != null && !format.isBlank() ? 
format.toLowerCase() : "yaml";
 
             // Analyze security-sensitive components
-            List<String> securityComponentNames = 
extractSecurityComponents(route);
+            List<String> securityComponentNames = RouteComponents.of(
+                    route, format, catalog, 
List.copyOf(securityData.getSecuritySensitiveComponents()));
             List<SecurityComponent> securityComponents = new ArrayList<>();
             for (String comp : securityComponentNames) {
                 ComponentModel model = catalog.componentModel(comp);
@@ -140,22 +142,6 @@ public class HardenTools {
         }
     }
 
-    /**
-     * Extract security-sensitive components from route content.
-     */
-    private List<String> extractSecurityComponents(String route) {
-        List<String> found = new ArrayList<>();
-        String lowerRoute = route.toLowerCase();
-
-        for (String comp : securityData.getSecuritySensitiveComponents()) {
-            if (containsComponent(lowerRoute, comp)) {
-                found.add(comp);
-            }
-        }
-
-        return found;
-    }
-
     /**
      * Match published CVE advisories against the components used by the 
route, keeping only advisories that affect the
      * given Camel version (advisories whose affected ranges cannot be parsed 
are kept for the LLM to judge). Components
@@ -302,12 +288,6 @@ public class HardenTools {
                 && !route.contains("{{") && !route.contains("$");
     }
 
-    private boolean containsComponent(String content, String comp) {
-        return content.contains(comp + ":")
-                || content.contains("\"" + comp + "\"")
-                || content.contains("'" + comp + "'");
-    }
-
     private boolean hasExternalConnections(String route) {
         String lowerRoute = route.toLowerCase();
         return lowerRoute.contains("http:") || lowerRoute.contains("https:")

Reply via email to