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:")