This is an automated email from the ASF dual-hosted git repository.
acosentino 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 0f79384bebd6 CAMEL-23167 - Camel-jbang-mcp - Extract exception catalog
from Diagno… (#21905)
0f79384bebd6 is described below
commit 0f79384bebd6045a7e243ad402e9131845d77895
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Mar 10 12:47:55 2026 +0100
CAMEL-23167 - Camel-jbang-mcp - Extract exception catalog from Diagno…
(#21905)
* CAMEL-23167 - Camel-jbang-mcp - Extract static reference data from tools
into MCP Resources
Signed-off-by: Andrea Cosentino <[email protected]>
* CAMEL-23167 - Camel-jbang-mcp - Extract static reference data from tools
into MCP Resources
Signed-off-by: Andrea Cosentino <[email protected]>
* CAMEL-23167 - Camel-jbang-mcp - Extract static reference data from tools
into MCP Resources
Signed-off-by: Andrea Cosentino <[email protected]>
* CAMEL-23167 - Camel-jbang-mcp - Extract static reference data from tools
into MCP Resources
Signed-off-by: Andrea Cosentino <[email protected]>
---------
Signed-off-by: Andrea Cosentino <[email protected]>
---
.../core/commands/mcp/DependencyCheckTools.java | 49 +--
.../jbang/core/commands/mcp/DependencyData.java | 125 ++++++
.../core/commands/mcp/DependencyResources.java | 119 ++++++
.../mcp/{DiagnoseTools.java => DiagnoseData.java} | 419 ++++++---------------
.../jbang/core/commands/mcp/DiagnoseResources.java | 101 +++++
.../dsl/jbang/core/commands/mcp/DiagnoseTools.java | 362 +-----------------
.../dsl/jbang/core/commands/mcp/TestInfraData.java | 211 +++++++++++
.../core/commands/mcp/TestInfraResources.java | 144 +++++++
.../jbang/core/commands/mcp/TestScaffoldTools.java | 207 ++--------
.../commands/mcp/DependencyCheckToolsTest.java | 7 +-
.../core/commands/mcp/DependencyResourcesTest.java | 142 +++++++
.../core/commands/mcp/DiagnoseResourcesTest.java | 135 +++++++
.../jbang/core/commands/mcp/DiagnoseToolsTest.java | 7 +-
.../core/commands/mcp/TestInfraResourcesTest.java | 159 ++++++++
.../core/commands/mcp/TestScaffoldToolsTest.java | 7 +-
15 files changed, 1328 insertions(+), 866 deletions(-)
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 05b6e0690a93..a22bdef6b185 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
@@ -16,11 +16,10 @@
*/
package org.apache.camel.dsl.jbang.core.commands.mcp;
-import java.util.Arrays;
import java.util.List;
-import java.util.Set;
import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.ToolArg;
@@ -40,20 +39,8 @@ import org.apache.camel.util.json.JsonObject;
@ApplicationScoped
public class DependencyCheckTools {
- /**
- * Artifacts that are transitive dependencies of camel-core and do not
need to be declared explicitly.
- */
- private static final Set<String> CAMEL_CORE_TRANSITIVE_ARTIFACTS =
Set.copyOf(Arrays.asList(
- "camel-core", "camel-core-model", "camel-core-engine",
"camel-core-processor",
- "camel-core-reifier", "camel-core-languages", "camel-core-catalog",
- "camel-bean", "camel-browse", "camel-cluster", "camel-controlbus",
- "camel-dataformat", "camel-dataset", "camel-direct", "camel-file",
- "camel-health", "camel-language", "camel-log", "camel-mock",
"camel-ref",
- "camel-rest", "camel-saga", "camel-scheduler", "camel-seda",
"camel-stub",
- "camel-timer", "camel-validator", "camel-xpath", "camel-xslt",
- "camel-xml-io", "camel-xml-jaxb", "camel-xml-jaxp",
"camel-yaml-io",
- "camel-api", "camel-base", "camel-base-engine",
"camel-management-api",
- "camel-support", "camel-util"));
+ @Inject
+ DependencyData dependencyData;
private final CamelCatalog catalog;
@@ -192,7 +179,7 @@ public class DependencyCheckTools {
}
// Skip core components — they are transitive dependencies of
camel-core
- if (CAMEL_CORE_TRANSITIVE_ARTIFACTS.contains(artifactId)) {
+ if (dependencyData.isCoreTransitive(artifactId)) {
continue;
}
@@ -408,32 +395,6 @@ public class DependencyCheckTools {
}
private String formatBomSnippet(MigrationData.PomAnalysis pom) {
- String bomArtifact;
- String bomGroup = "org.apache.camel";
- String runtimeType = pom.runtimeType();
-
- if ("spring-boot".equals(runtimeType)) {
- bomArtifact = "camel-spring-boot-bom";
- bomGroup = "org.apache.camel.springboot";
- } else if ("quarkus".equals(runtimeType)) {
- bomArtifact = "camel-quarkus-bom";
- bomGroup = "org.apache.camel.quarkus";
- } else {
- bomArtifact = "camel-bom";
- }
-
- String version = pom.camelVersion() != null ? pom.camelVersion() :
"${camel.version}";
-
- return "<dependencyManagement>\n"
- + " <dependencies>\n"
- + " <dependency>\n"
- + " <groupId>" + bomGroup + "</groupId>\n"
- + " <artifactId>" + bomArtifact + "</artifactId>\n"
- + " <version>" + version + "</version>\n"
- + " <type>pom</type>\n"
- + " <scope>import</scope>\n"
- + " </dependency>\n"
- + " </dependencies>\n"
- + "</dependencyManagement>";
+ return dependencyData.formatBomSnippet(pom.runtimeType(),
pom.camelVersion());
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyData.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyData.java
new file mode 100644
index 000000000000..92023349388b
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyData.java
@@ -0,0 +1,125 @@
+/*
+ * 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.mcp;
+
+import java.util.List;
+import java.util.Set;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+/**
+ * Shared holder for Camel dependency reference data used by both {@link
DependencyCheckTools} (MCP Tool) and
+ * {@link DependencyResources} (MCP Resources).
+ * <p>
+ * Contains the set of core transitive artifacts and BOM template generation
for different runtimes.
+ */
+@ApplicationScoped
+public class DependencyData {
+
+ /**
+ * Artifacts that are transitive dependencies of camel-core and do not
need to be declared explicitly.
+ */
+ private static final Set<String> CAMEL_CORE_TRANSITIVE_ARTIFACTS = Set.of(
+ "camel-core", "camel-core-model", "camel-core-engine",
"camel-core-processor",
+ "camel-core-reifier", "camel-core-languages", "camel-core-catalog",
+ "camel-bean", "camel-browse", "camel-cluster", "camel-controlbus",
+ "camel-dataformat", "camel-dataset", "camel-direct", "camel-file",
+ "camel-health", "camel-language", "camel-log", "camel-mock",
"camel-ref",
+ "camel-rest", "camel-saga", "camel-scheduler", "camel-seda",
"camel-stub",
+ "camel-timer", "camel-validator", "camel-xpath", "camel-xslt",
+ "camel-xml-io", "camel-xml-jaxb", "camel-xml-jaxp",
"camel-yaml-io",
+ "camel-api", "camel-base", "camel-base-engine",
"camel-management-api",
+ "camel-support", "camel-util");
+
+ private static final List<BomTemplate> BOM_TEMPLATES = List.of(
+ new BomTemplate(
+ "main", "org.apache.camel", "camel-bom",
+ "Standard Camel BOM for Camel Main runtime"),
+ new BomTemplate(
+ "spring-boot", "org.apache.camel.springboot",
"camel-spring-boot-bom",
+ "Camel Spring Boot BOM for Spring Boot runtime"),
+ new BomTemplate(
+ "quarkus", "org.apache.camel.quarkus", "camel-quarkus-bom",
+ "Camel Quarkus BOM for Quarkus runtime"));
+
+ /**
+ * Get the set of artifacts that are transitive dependencies of camel-core.
+ */
+ public Set<String> getCoreTransitiveArtifacts() {
+ return CAMEL_CORE_TRANSITIVE_ARTIFACTS;
+ }
+
+ /**
+ * Check if an artifact is a transitive dependency of camel-core.
+ */
+ public boolean isCoreTransitive(String artifactId) {
+ return CAMEL_CORE_TRANSITIVE_ARTIFACTS.contains(artifactId);
+ }
+
+ /**
+ * Get all available BOM templates.
+ */
+ public List<BomTemplate> getBomTemplates() {
+ return BOM_TEMPLATES;
+ }
+
+ /**
+ * Get the BOM template for a specific runtime.
+ *
+ * @return the BOM template, or null if the runtime is not recognized
+ */
+ public BomTemplate getBomTemplate(String runtime) {
+ for (BomTemplate template : BOM_TEMPLATES) {
+ if (template.runtime().equals(runtime)) {
+ return template;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Format a BOM dependency management snippet for a given runtime and
version.
+ */
+ public String formatBomSnippet(String runtime, String version) {
+ BomTemplate template = getBomTemplate(runtime);
+ if (template == null) {
+ template = BOM_TEMPLATES.get(0); // default to main
+ }
+ String ver = version != null ? version : "${camel.version}";
+ return "<dependencyManagement>\n"
+ + " <dependencies>\n"
+ + " <dependency>\n"
+ + " <groupId>" + template.groupId() + "</groupId>\n"
+ + " <artifactId>" + template.artifactId() +
"</artifactId>\n"
+ + " <version>" + ver + "</version>\n"
+ + " <type>pom</type>\n"
+ + " <scope>import</scope>\n"
+ + " </dependency>\n"
+ + " </dependencies>\n"
+ + "</dependencyManagement>";
+ }
+
+ /**
+ * Holds BOM template information for a Camel runtime.
+ */
+ public record BomTemplate(
+ String runtime,
+ String groupId,
+ String artifactId,
+ String description) {
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyResources.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyResources.java
new file mode 100644
index 000000000000..9bfc84a61d3b
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyResources.java
@@ -0,0 +1,119 @@
+/*
+ * 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.mcp;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+import io.quarkiverse.mcp.server.Resource;
+import io.quarkiverse.mcp.server.ResourceTemplate;
+import io.quarkiverse.mcp.server.ResourceTemplateArg;
+import io.quarkiverse.mcp.server.TextResourceContents;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * MCP Resources exposing Camel dependency reference data.
+ * <p>
+ * These resources provide browseable dependency information that clients can
pull into context when helping users
+ * manage Camel project dependencies. Includes the set of core transitive
artifacts (no explicit declaration needed) and
+ * BOM templates for different runtimes.
+ */
+@ApplicationScoped
+public class DependencyResources {
+
+ @Inject
+ DependencyData dependencyData;
+
+ /**
+ * Core transitive artifacts that do not need explicit declaration in
pom.xml.
+ */
+ @Resource(uri = "camel://dependency/core-transitive-artifacts",
+ name = "camel_dependency_core_transitive_artifacts",
+ title = "Camel Core Transitive Artifacts",
+ description = "Set of Maven artifact IDs that are transitive
dependencies of camel-core "
+ + "and do not need to be declared explicitly in a
project's pom.xml. "
+ + "Includes core modules (camel-core, camel-api,
camel-support, etc.) "
+ + "and built-in components (camel-direct,
camel-seda, camel-log, camel-timer, etc.).",
+ mimeType = "application/json")
+ public TextResourceContents coreTransitiveArtifacts() {
+ JsonObject result = new JsonObject();
+
+ List<String> sorted = new
ArrayList<>(dependencyData.getCoreTransitiveArtifacts());
+ sorted.sort(String::compareTo);
+
+ JsonArray artifacts = new JsonArray();
+ for (String artifact : sorted) {
+ artifacts.add(artifact);
+ }
+
+ result.put("artifacts", artifacts);
+ result.put("totalCount", artifacts.size());
+ result.put("description",
+ "These artifacts are included transitively via camel-core and
do not need explicit "
+ + "<dependency> declarations. Adding them
explicitly is harmless but unnecessary.");
+
+ return new TextResourceContents(
+ "camel://dependency/core-transitive-artifacts",
result.toJson(),
+ "application/json");
+ }
+
+ /**
+ * BOM template for a specific Camel runtime.
+ */
+ @ResourceTemplate(uriTemplate =
"camel://dependency/bom-template/{runtime}",
+ name = "camel_dependency_bom_template",
+ title = "Camel BOM Template",
+ description = "Maven dependencyManagement snippet for
importing the Camel BOM "
+ + "for a specific runtime (main,
spring-boot, or quarkus). "
+ + "Using a BOM ensures consistent versions
across all Camel dependencies.",
+ mimeType = "application/json")
+ public TextResourceContents bomTemplate(
+ @ResourceTemplateArg(name = "runtime") String runtime) {
+
+ String uri = "camel://dependency/bom-template/" + runtime;
+
+ DependencyData.BomTemplate template =
dependencyData.getBomTemplate(runtime);
+ if (template == null) {
+ JsonObject result = new JsonObject();
+ result.put("runtime", runtime);
+ result.put("found", false);
+
+ JsonArray available = new JsonArray();
+ for (DependencyData.BomTemplate t :
dependencyData.getBomTemplates()) {
+ available.add(t.runtime());
+ }
+ result.put("availableRuntimes", available);
+ result.put("message", "Unknown runtime '" + runtime + "'. "
+ + "Use one of: main, spring-boot, quarkus.");
+ return new TextResourceContents(uri, result.toJson(),
"application/json");
+ }
+
+ JsonObject result = new JsonObject();
+ result.put("runtime", runtime);
+ result.put("found", true);
+ result.put("groupId", template.groupId());
+ result.put("artifactId", template.artifactId());
+ result.put("description", template.description());
+ result.put("snippet", dependencyData.formatBomSnippet(runtime, null));
+
+ return new TextResourceContents(uri, result.toJson(),
"application/json");
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseData.java
similarity index 60%
copy from
dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseTools.java
copy to
dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseData.java
index 879063c6e283..30265ce3575d 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseData.java
@@ -16,564 +16,393 @@
*/
package org.apache.camel.dsl.jbang.core.commands.mcp;
-import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import jakarta.enterprise.context.ApplicationScoped;
-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.catalog.DefaultCamelCatalog;
-import org.apache.camel.tooling.model.ComponentModel;
-import org.apache.camel.tooling.model.EipModel;
import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
/**
- * MCP Tool for diagnosing Camel errors from stack traces or error messages.
+ * Shared holder for Camel exception diagnostic reference data used by both
{@link DiagnoseTools} (MCP Tool) and
+ * {@link DiagnoseResources} (MCP Resources).
* <p>
- * Accepts a Camel stack trace or error message and returns the likely
component/EIP involved, common causes, links to
- * relevant documentation, and suggested fixes.
+ * Contains the registry of known Camel exceptions with their descriptions,
common causes, suggested fixes, and
+ * documentation links.
*/
@ApplicationScoped
-public class DiagnoseTools {
+public class DiagnoseData {
- private static final String CAMEL_DOC_BASE = "https://camel.apache.org/";
- private static final String CAMEL_COMPONENT_DOC = CAMEL_DOC_BASE +
"components/next/";
- private static final String CAMEL_MANUAL_DOC = CAMEL_DOC_BASE + "manual/";
- private static final String CAMEL_EIP_DOC = CAMEL_COMPONENT_DOC + "eips/";
+ public static final String CAMEL_DOC_BASE = "https://camel.apache.org/";
+ public static final String CAMEL_COMPONENT_DOC = CAMEL_DOC_BASE +
"components/next/";
+ public static final String CAMEL_MANUAL_DOC = CAMEL_DOC_BASE + "manual/";
+ public static final String CAMEL_EIP_DOC = CAMEL_COMPONENT_DOC + "eips/";
- /**
- * Pattern to extract component names from endpoint URIs in error messages
(e.g., "kafka:myTopic", "http://host").
- */
- private static final Pattern ENDPOINT_URI_PATTERN = Pattern
-
.compile("(?:endpoint|uri)[:\\s]+['\"]?([a-zA-Z][a-zA-Z0-9+.-]*):(?://)?[^\\s'\"]*",
Pattern.CASE_INSENSITIVE);
-
- /**
- * Pattern to extract component scheme from common error contexts.
- */
- private static final Pattern COMPONENT_SCHEME_PATTERN
- =
Pattern.compile("(?:component|scheme)[:\\s]+['\"]?([a-zA-Z][a-zA-Z0-9+.-]*)['\"]?",
Pattern.CASE_INSENSITIVE);
-
- /**
- * Pattern to extract route IDs from error messages.
- */
- private static final Pattern ROUTE_ID_PATTERN
- = Pattern.compile("route[:\\s]+['\"]?([a-zA-Z0-9_-]+)['\"]?",
Pattern.CASE_INSENSITIVE);
-
- private final CamelCatalog catalog;
- private final Map<String, ExceptionInfo> knownExceptions;
-
- public DiagnoseTools() {
- this.catalog = new DefaultCamelCatalog();
- this.knownExceptions = buildKnownExceptions();
- }
-
- /**
- * Tool to diagnose Camel errors from stack traces or error messages.
- */
- @Tool(description = "Diagnose a Camel error from a stack trace or error
message. "
- + "Returns the identified component/EIP involved,
common causes for the error, "
- + "links to relevant Camel documentation, and
suggested fixes. "
- + "Covers the most common Camel exceptions including
NoSuchEndpointException, "
- + "ResolveEndpointFailedException,
FailedToCreateRouteException, and more.")
- public String camel_error_diagnose(
- @ToolArg(description = "The Camel stack trace or error message to
diagnose") String error) {
-
- if (error == null || error.isBlank()) {
- throw new ToolCallException("Error message or stack trace is
required", null);
- }
-
- try {
- JsonObject result = new JsonObject();
-
- // Identify matching exceptions
- List<String> matchedExceptions = identifyExceptions(error);
- JsonArray exceptionsJson = new JsonArray();
- for (String exceptionName : matchedExceptions) {
- ExceptionInfo info = knownExceptions.get(exceptionName);
- if (info != null) {
- JsonObject exJson = new JsonObject();
- exJson.put("exception", exceptionName);
- exJson.put("description", info.description);
-
- JsonArray causesJson = new JsonArray();
- for (String cause : info.commonCauses) {
- causesJson.add(cause);
- }
- exJson.put("commonCauses", causesJson);
-
- JsonArray fixesJson = new JsonArray();
- for (String fix : info.suggestedFixes) {
- fixesJson.add(fix);
- }
- exJson.put("suggestedFixes", fixesJson);
-
- JsonArray docsJson = new JsonArray();
- for (String doc : info.documentationLinks) {
- docsJson.add(doc);
- }
- exJson.put("documentationLinks", docsJson);
-
- exceptionsJson.add(exJson);
- }
- }
- result.put("identifiedExceptions", exceptionsJson);
-
- // Identify components from the error
- List<String> componentNames = extractComponentNames(error);
- JsonArray componentsJson = new JsonArray();
- for (String comp : componentNames) {
- ComponentModel model = catalog.componentModel(comp);
- if (model != null) {
- JsonObject compJson = new JsonObject();
- compJson.put("name", comp);
- compJson.put("title", model.getTitle());
- compJson.put("description", model.getDescription());
- compJson.put("documentationUrl", CAMEL_COMPONENT_DOC +
comp + "-component.html");
- componentsJson.add(compJson);
- }
- }
- result.put("identifiedComponents", componentsJson);
-
- // Identify EIPs from the error
- List<String> eipNames = extractEipNames(error);
- JsonArray eipsJson = new JsonArray();
- for (String eip : eipNames) {
- EipModel model = catalog.eipModel(eip);
- if (model != null) {
- JsonObject eipJson = new JsonObject();
- eipJson.put("name", eip);
- eipJson.put("title", model.getTitle());
- eipJson.put("description", model.getDescription());
- eipJson.put("documentationUrl", CAMEL_EIP_DOC + eip +
"-eip.html");
- eipsJson.add(eipJson);
- }
- }
- result.put("identifiedEips", eipsJson);
-
- // Extract route ID if present
- Matcher routeMatcher = ROUTE_ID_PATTERN.matcher(error);
- if (routeMatcher.find()) {
- result.put("routeId", routeMatcher.group(1));
- }
-
- // Summary
- JsonObject summary = new JsonObject();
- summary.put("exceptionCount", exceptionsJson.size());
- summary.put("componentCount", componentsJson.size());
- summary.put("eipCount", eipsJson.size());
- summary.put("diagnosed", !exceptionsJson.isEmpty());
- result.put("summary", summary);
-
- return result.toJson();
- } catch (ToolCallException e) {
- throw e;
- } catch (Throwable e) {
- throw new ToolCallException(
- "Failed to diagnose error (" + e.getClass().getName() +
"): " + e.getMessage(), null);
- }
- }
-
- /**
- * Identify known Camel exceptions in the error text.
- */
- private List<String> identifyExceptions(String error) {
- List<String> matched = new ArrayList<>();
- for (String exceptionName : knownExceptions.keySet()) {
- if (error.contains(exceptionName)) {
- matched.add(exceptionName);
- }
- }
- return matched;
- }
+ private static final Map<String, ExceptionInfo> KNOWN_EXCEPTIONS;
- /**
- * Extract component names from endpoint URIs and other patterns in the
error text.
- */
- private List<String> extractComponentNames(String error) {
- List<String> found = new ArrayList<>();
-
- // Try endpoint URI pattern
- Matcher uriMatcher = ENDPOINT_URI_PATTERN.matcher(error);
- while (uriMatcher.find()) {
- String scheme = uriMatcher.group(1).toLowerCase();
- if (catalog.componentModel(scheme) != null &&
!found.contains(scheme)) {
- found.add(scheme);
- }
- }
-
- // Try component scheme pattern
- Matcher schemeMatcher = COMPONENT_SCHEME_PATTERN.matcher(error);
- while (schemeMatcher.find()) {
- String scheme = schemeMatcher.group(1).toLowerCase();
- if (catalog.componentModel(scheme) != null &&
!found.contains(scheme)) {
- found.add(scheme);
- }
- }
-
- // Scan for known component names in the error text
- String lowerError = error.toLowerCase();
- for (String comp : catalog.findComponentNames()) {
- if (!found.contains(comp) && containsComponent(lowerError, comp)) {
- found.add(comp);
- }
- }
-
- return found;
- }
-
- /**
- * Extract EIP names from the error text.
- */
- private List<String> extractEipNames(String error) {
- List<String> found = new ArrayList<>();
- String lowerError = error.toLowerCase();
-
- for (String eip : catalog.findModelNames()) {
- EipModel model = catalog.eipModel(eip);
- if (model != null) {
- String eipLower = eip.toLowerCase();
- if (lowerError.contains(eipLower) && !found.contains(eip)) {
- found.add(eip);
- }
- }
- }
-
- return found;
- }
-
- private boolean containsComponent(String content, String comp) {
- return content.contains(comp + ":")
- || content.contains("\"" + comp + "\"")
- || content.contains("'" + comp + "'");
- }
-
- /**
- * Build the registry of known Camel exceptions with their causes, fixes,
and documentation.
- */
- private Map<String, ExceptionInfo> buildKnownExceptions() {
+ static {
Map<String, ExceptionInfo> exceptions = new LinkedHashMap<>();
exceptions.put("NoSuchEndpointException", new ExceptionInfo(
"The specified endpoint URI could not be resolved to any known
Camel component.",
- Arrays.asList(
+ List.of(
"Typo in the endpoint URI scheme (e.g., 'kafak:'
instead of 'kafka:')",
"Missing component dependency in pom.xml or
build.gradle",
"Component not on the classpath",
"Using a component scheme that does not exist"),
- Arrays.asList(
+ List.of(
"Verify the endpoint URI scheme is spelled correctly",
"Add the required camel-<component> dependency to your
project",
"Check available components with 'camel-catalog' or
the Camel documentation",
"Ensure the component JAR is on the classpath"),
- Arrays.asList(
+ List.of(
CAMEL_MANUAL_DOC + "component.html")));
exceptions.put("ResolveEndpointFailedException", new ExceptionInfo(
"Failed to resolve or create an endpoint from the given URI.
The URI syntax may be invalid or required options may be missing.",
- Arrays.asList(
+ List.of(
"Invalid endpoint URI syntax",
"Missing required endpoint options",
"Unknown or misspelled endpoint options",
"Invalid option values (wrong type or format)",
"Special characters in URI not properly encoded"),
- Arrays.asList(
+ List.of(
"Check the endpoint URI syntax against the component
documentation",
"Ensure all required options are provided",
"Verify option names are spelled correctly",
"URL-encode special characters in the URI",
"Use the endpoint DSL for type-safe endpoint
configuration"),
- Arrays.asList(
+ List.of(
CAMEL_MANUAL_DOC + "endpoint.html",
CAMEL_MANUAL_DOC + "uris.html")));
exceptions.put("FailedToCreateRouteException", new ExceptionInfo(
"A route could not be created. This is typically a
configuration or wiring issue.",
- Arrays.asList(
+ List.of(
"Invalid endpoint URI in from() or to()",
"Missing required component dependency",
"Bean reference that cannot be resolved",
"Invalid route configuration or DSL syntax",
"Circular route dependencies"),
- Arrays.asList(
+ List.of(
"Check the full exception chain for the root cause",
"Verify all endpoint URIs in the route are valid",
"Ensure all referenced beans are available in the
registry",
"Validate the route DSL syntax",
"Check for missing component dependencies"),
- Arrays.asList(
+ List.of(
CAMEL_MANUAL_DOC + "routes.html",
CAMEL_MANUAL_DOC + "route-configuration.html")));
exceptions.put("FailedToStartRouteException", new ExceptionInfo(
"A route was created but could not be started. This often
indicates a connectivity or resource issue.",
- Arrays.asList(
+ List.of(
"Cannot connect to external service (broker, database,
etc.)",
"Port already in use for server-side components",
"Authentication/authorization failure",
"Missing or invalid SSL/TLS configuration",
"Resource not available (queue, topic, table, etc.)"),
- Arrays.asList(
+ List.of(
"Verify network connectivity to external services",
"Check credentials and authentication configuration",
"Ensure the target resource exists (queue, topic,
etc.)",
"Review SSL/TLS configuration if using secure
connections",
"Check if the port is already in use"),
- Arrays.asList(
+ List.of(
CAMEL_MANUAL_DOC + "routes.html",
CAMEL_MANUAL_DOC + "lifecycle.html")));
exceptions.put("NoTypeConversionAvailableException", new ExceptionInfo(
"Camel could not find a type converter to convert between the
required types.",
- Arrays.asList(
+ List.of(
"Trying to convert a message body to an incompatible
type",
"Missing type converter on the classpath",
"Custom type without a registered converter",
"Null body when a non-null type is expected"),
- Arrays.asList(
+ List.of(
"Check the source and target types in the conversion",
"Add appropriate data format or converter dependency",
"Use explicit marshal/unmarshal instead of implicit
conversion",
"Register a custom TypeConverter if needed",
"Check if the message body is null"),
- Arrays.asList(
+ List.of(
CAMEL_MANUAL_DOC + "type-converter.html",
CAMEL_MANUAL_DOC + "data-format.html")));
exceptions.put("CamelExecutionException", new ExceptionInfo(
"A wrapper exception thrown during route execution. The root
cause is in the nested exception.",
- Arrays.asList(
+ List.of(
"Exception thrown by a processor or bean in the route",
"External service failure (HTTP error, broker
disconnect, etc.)",
"Data transformation error",
"Timeout during synchronous processing"),
- Arrays.asList(
+ List.of(
"Inspect the nested/caused-by exception for the actual
error",
"Add error handling (onException, errorHandler,
doTry/doCatch) to the route",
"Check the processor or bean that failed",
"Review the full stack trace for the root cause"),
- Arrays.asList(
+ List.of(
CAMEL_MANUAL_DOC + "exception-clause.html",
CAMEL_MANUAL_DOC + "error-handler.html",
CAMEL_MANUAL_DOC + "try-catch-finally.html")));
exceptions.put("ExchangeTimedOutException", new ExceptionInfo(
"An exchange did not complete within the configured timeout
period.",
- Arrays.asList(
+ List.of(
"Slow downstream service or endpoint",
"Network latency or connectivity issues",
"Timeout value too low for the operation",
"Deadlock or resource contention",
"Direct/SEDA consumer not available"),
- Arrays.asList(
+ List.of(
"Increase the timeout value if appropriate",
"Add circuit breaker pattern for unreliable services",
"Check network connectivity to the target service",
"Use async processing for long-running operations",
"Add timeout error handling in the route"),
- Arrays.asList(
+ List.of(
CAMEL_MANUAL_DOC + "request-reply.html",
CAMEL_EIP_DOC + "circuitBreaker-eip.html")));
exceptions.put("DirectConsumerNotAvailableException", new
ExceptionInfo(
"No consumer is available for a direct endpoint. The direct
component requires an active consumer.",
- Arrays.asList(
+ List.of(
"Target route with the direct endpoint is not started",
"Typo in the direct endpoint name",
"Route with the direct consumer was stopped or
removed",
"Timing issue during startup — producer route started
before consumer route"),
- Arrays.asList(
+ List.of(
"Ensure a route with from(\"direct:name\") exists and
is started",
"Verify the direct endpoint name matches between
producer and consumer",
"Use SEDA instead of direct if startup ordering is
uncertain",
"Configure route startup ordering if needed"),
- Arrays.asList(
+ List.of(
CAMEL_COMPONENT_DOC + "direct-component.html",
CAMEL_COMPONENT_DOC + "seda-component.html")));
exceptions.put("CamelExchangeException", new ExceptionInfo(
"A general exception related to exchange processing.",
- Arrays.asList(
+ List.of(
"Processor failure during exchange handling",
"Invalid exchange pattern (InOnly vs InOut mismatch)",
"Missing required headers or properties",
"Exchange body cannot be processed"),
- Arrays.asList(
+ List.of(
"Check the exchange pattern matches the endpoint
requirements",
"Verify required headers are set on the exchange",
"Add error handling to catch and process the
exception",
"Inspect the exchange body type and content"),
- Arrays.asList(
+ List.of(
CAMEL_MANUAL_DOC + "exchange.html",
CAMEL_MANUAL_DOC + "exchange-pattern.html")));
exceptions.put("InvalidPayloadException", new ExceptionInfo(
"The message payload (body) is not of the expected type and
cannot be converted.",
- Arrays.asList(
+ List.of(
"Message body is null when a value is expected",
"Message body type does not match the expected type",
"Missing type converter for the body type",
"Upstream processor produced unexpected output"),
- Arrays.asList(
+ List.of(
"Check the message body type before the failing
processor",
"Use convertBodyTo() to explicitly convert the body
type",
"Add a null check or default value for the body",
"Add the appropriate data format dependency for
marshalling/unmarshalling"),
- Arrays.asList(
+ List.of(
CAMEL_MANUAL_DOC + "message.html",
CAMEL_MANUAL_DOC + "type-converter.html")));
exceptions.put("PropertyBindingException", new ExceptionInfo(
"Failed to bind a property or option to a Camel component,
endpoint, or bean.",
- Arrays.asList(
+ List.of(
"Property name does not exist on the target object",
"Property value has wrong type (e.g., string for a
boolean)",
"Misspelled property or option name",
"Property placeholder could not be resolved"),
- Arrays.asList(
+ List.of(
"Check the property name spelling against the
component documentation",
"Verify the property value type matches the expected
type",
"Use property placeholders correctly:
{{property.name}}",
"Check application.properties or YAML configuration
for correct keys"),
- Arrays.asList(
+ List.of(
CAMEL_MANUAL_DOC + "using-propertyplaceholder.html",
CAMEL_MANUAL_DOC + "component.html")));
exceptions.put("NoSuchBeanException", new ExceptionInfo(
"A referenced bean could not be found in the Camel registry.",
- Arrays.asList(
+ List.of(
"Bean not registered in the Spring/CDI/Camel registry",
"Typo in the bean name reference",
"Bean class not on the classpath",
"Missing @Named or @Component annotation on the bean
class",
"Bean definition not scanned by component scan"),
- Arrays.asList(
+ List.of(
"Verify the bean is registered with the correct name",
"Check spelling of the bean reference",
"Ensure the bean class has proper annotations (@Named,
@Component, etc.)",
"Verify component scanning includes the bean's
package",
"Register the bean manually in RouteBuilder
configure() if needed"),
- Arrays.asList(
+ List.of(
CAMEL_MANUAL_DOC + "registry.html",
CAMEL_MANUAL_DOC + "bean-binding.html")));
exceptions.put("NoSuchHeaderException", new ExceptionInfo(
"A required header was not found on the message.",
- Arrays.asList(
+ List.of(
"Header not set by upstream processors",
"Header name is misspelled",
"Header was removed by a previous processor",
"Using wrong header constant name"),
- Arrays.asList(
+ List.of(
"Verify the header name matches what upstream
processors set",
"Use header constants from the component's class
(e.g., KafkaConstants)",
"Add a setHeader() before the processor that requires
it",
"Add a null check or default value for the header"),
- Arrays.asList(
+ List.of(
CAMEL_MANUAL_DOC + "message.html")));
exceptions.put("PredicateValidationException", new ExceptionInfo(
"A predicate validation failed. This is typically thrown by
the validate() DSL or a filter condition.",
- Arrays.asList(
+ List.of(
"Message did not match the expected validation
predicate",
"Invalid or unexpected message content",
"Predicate expression has a syntax error",
"Null values in the expression evaluation"),
- Arrays.asList(
+ List.of(
"Review the predicate expression for correctness",
"Check the message content matches the expected
format",
"Add error handling for validation failures",
"Use a more lenient predicate or add default values"),
- Arrays.asList(
+ List.of(
CAMEL_EIP_DOC + "validate-eip.html",
CAMEL_MANUAL_DOC + "predicate.html")));
exceptions.put("NoSuchLanguageException", new ExceptionInfo(
"The specified expression language is not available.",
- Arrays.asList(
+ List.of(
"Missing language dependency (e.g., camel-jsonpath,
camel-xpath)",
"Typo in the language name",
"Using a language that does not exist"),
- Arrays.asList(
+ List.of(
"Add the required camel-<language> dependency",
"Verify the language name is spelled correctly",
"Use 'simple' language which is included in
camel-core"),
- Arrays.asList(
+ List.of(
CAMEL_MANUAL_DOC + "languages.html")));
exceptions.put("FailedToCreateConsumerException", new ExceptionInfo(
"A consumer could not be created for the endpoint.",
- Arrays.asList(
+ List.of(
"Cannot connect to the source system (broker, server,
etc.)",
"Invalid consumer configuration options",
"Authentication failure",
"Missing required consumer options",
"Resource does not exist (topic, queue, file path,
etc.)"),
- Arrays.asList(
+ List.of(
"Verify connectivity to the source system",
"Check the consumer configuration options",
"Ensure credentials are correct",
"Verify the target resource exists",
"Check the full exception chain for the root cause"),
- Arrays.asList(
+ List.of(
CAMEL_MANUAL_DOC + "component.html")));
exceptions.put("FailedToCreateProducerException", new ExceptionInfo(
"A producer could not be created for the endpoint.",
- Arrays.asList(
+ List.of(
"Cannot connect to the target system",
"Invalid producer configuration options",
"Authentication failure",
"Missing required producer options"),
- Arrays.asList(
+ List.of(
"Verify connectivity to the target system",
"Check the producer configuration options",
"Ensure credentials are correct",
"Check the full exception chain for the root cause"),
- Arrays.asList(
+ List.of(
CAMEL_MANUAL_DOC + "component.html")));
exceptions.put("CamelAuthorizationException", new ExceptionInfo(
"An authorization check failed. The current identity does not
have permission to perform the operation.",
- Arrays.asList(
+ List.of(
"Insufficient permissions for the user or service
account",
"Missing or expired authentication token",
"Security policy denying the operation",
"Incorrect RBAC or ACL configuration"),
- Arrays.asList(
+ List.of(
"Check the user/service account permissions",
"Verify the authentication token is valid and not
expired",
"Review the security policies and ACLs",
"Ensure the correct security provider is configured"),
- Arrays.asList(
+ List.of(
CAMEL_MANUAL_DOC + "security.html")));
- return exceptions;
+ KNOWN_EXCEPTIONS = Collections.unmodifiableMap(exceptions);
+ }
+
+ /**
+ * Get all known Camel exceptions with their diagnostic information.
+ */
+ public Map<String, ExceptionInfo> getKnownExceptions() {
+ return KNOWN_EXCEPTIONS;
+ }
+
+ /**
+ * Get diagnostic information for a specific exception by name.
+ *
+ * @return the exception info, or null if not found
+ */
+ public ExceptionInfo getException(String name) {
+ return KNOWN_EXCEPTIONS.get(name);
+ }
+
+ /**
+ * Get all known exception names.
+ */
+ public List<String> getExceptionNames() {
+ return List.copyOf(KNOWN_EXCEPTIONS.keySet());
}
/**
* Holds diagnostic information about a known Camel exception.
*/
- static class ExceptionInfo {
- final String description;
- final List<String> commonCauses;
- final List<String> suggestedFixes;
- final List<String> documentationLinks;
-
- ExceptionInfo(String description, List<String> commonCauses,
List<String> suggestedFixes,
- List<String> documentationLinks) {
- this.description = description;
- this.commonCauses = commonCauses;
- this.suggestedFixes = suggestedFixes;
- this.documentationLinks = documentationLinks;
+ public record ExceptionInfo(
+ String description,
+ List<String> commonCauses,
+ List<String> suggestedFixes,
+ List<String> documentationLinks) {
+
+ /**
+ * Convert this exception info to a full JSON object with all fields.
+ */
+ public JsonObject toJson() {
+ JsonObject json = new JsonObject();
+ json.put("description", description);
+ json.put("commonCauses", toJsonArray(commonCauses));
+ json.put("suggestedFixes", toJsonArray(suggestedFixes));
+ json.put("documentationLinks", toJsonArray(documentationLinks));
+ return json;
+ }
+
+ /**
+ * Convert this exception info to a summary JSON object (description
and doc links only).
+ */
+ public JsonObject toSummaryJson() {
+ JsonObject json = new JsonObject();
+ json.put("description", description);
+ json.put("documentationLinks", toJsonArray(documentationLinks));
+ return json;
+ }
+
+ private static JsonArray toJsonArray(List<String> items) {
+ JsonArray array = new JsonArray();
+ for (String item : items) {
+ array.add(item);
+ }
+ return array;
}
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseResources.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseResources.java
new file mode 100644
index 000000000000..ebcd5794b357
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseResources.java
@@ -0,0 +1,101 @@
+/*
+ * 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.mcp;
+
+import java.util.Map;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+import io.quarkiverse.mcp.server.Resource;
+import io.quarkiverse.mcp.server.ResourceTemplate;
+import io.quarkiverse.mcp.server.ResourceTemplateArg;
+import io.quarkiverse.mcp.server.TextResourceContents;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * MCP Resources exposing Camel exception diagnostic reference data.
+ * <p>
+ * These resources provide browseable exception catalog data that clients can
pull into context independently of error
+ * diagnosis. This allows LLM clients to proactively load exception reference
data without needing a stack trace to
+ * trigger the diagnose tool.
+ */
+@ApplicationScoped
+public class DiagnoseResources {
+
+ @Inject
+ DiagnoseData diagnoseData;
+
+ /**
+ * All known Camel exceptions with descriptions and documentation links.
+ */
+ @Resource(uri = "camel://error/exception-catalog",
+ name = "camel_error_exception_catalog",
+ title = "Camel Exception Catalog",
+ description = "Registry of all known Camel exceptions with
descriptions, common causes, "
+ + "suggested fixes, and links to relevant
documentation. Covers exceptions like "
+ + "NoSuchEndpointException,
ResolveEndpointFailedException, FailedToCreateRouteException, "
+ + "and more.",
+ mimeType = "application/json")
+ public TextResourceContents exceptionCatalog() {
+ JsonObject result = new JsonObject();
+
+ JsonArray exceptions = new JsonArray();
+ for (Map.Entry<String, DiagnoseData.ExceptionInfo> entry :
diagnoseData.getKnownExceptions().entrySet()) {
+ JsonObject exJson = entry.getValue().toSummaryJson();
+ exJson.put("name", entry.getKey());
+ exceptions.add(exJson);
+ }
+
+ result.put("exceptions", exceptions);
+ result.put("totalCount", exceptions.size());
+
+ return new TextResourceContents("camel://error/exception-catalog",
result.toJson(), "application/json");
+ }
+
+ /**
+ * Detail for a specific Camel exception by name.
+ */
+ @ResourceTemplate(uriTemplate = "camel://error/exception/{name}",
+ name = "camel_error_exception_detail",
+ title = "Exception Detail",
+ description = "Full diagnostic detail for a specific
Camel exception including description, "
+ + "common causes, suggested fixes, and
documentation links.",
+ mimeType = "application/json")
+ public TextResourceContents exceptionDetail(
+ @ResourceTemplateArg(name = "name") String name) {
+
+ String uri = "camel://error/exception/" + name;
+
+ DiagnoseData.ExceptionInfo info = diagnoseData.getException(name);
+ if (info == null) {
+ JsonObject result = new JsonObject();
+ result.put("name", name);
+ result.put("found", false);
+ result.put("message", "Exception '" + name + "' is not in the
known exceptions catalog. "
+ + "Use the camel://error/exception-catalog
resource to see all known exceptions.");
+ return new TextResourceContents(uri, result.toJson(),
"application/json");
+ }
+
+ JsonObject result = info.toJson();
+ result.put("name", name);
+ result.put("found", true);
+
+ return new TextResourceContents(uri, result.toJson(),
"application/json");
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseTools.java
index 879063c6e283..ca21d682126a 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseTools.java
@@ -17,14 +17,12 @@
package org.apache.camel.dsl.jbang.core.commands.mcp;
import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.LinkedHashMap;
import java.util.List;
-import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.ToolArg;
@@ -45,10 +43,8 @@ import org.apache.camel.util.json.JsonObject;
@ApplicationScoped
public class DiagnoseTools {
- private static final String CAMEL_DOC_BASE = "https://camel.apache.org/";
- private static final String CAMEL_COMPONENT_DOC = CAMEL_DOC_BASE +
"components/next/";
- private static final String CAMEL_MANUAL_DOC = CAMEL_DOC_BASE + "manual/";
- private static final String CAMEL_EIP_DOC = CAMEL_COMPONENT_DOC + "eips/";
+ private static final String CAMEL_COMPONENT_DOC =
DiagnoseData.CAMEL_COMPONENT_DOC;
+ private static final String CAMEL_EIP_DOC = DiagnoseData.CAMEL_EIP_DOC;
/**
* Pattern to extract component names from endpoint URIs in error messages
(e.g., "kafka:myTopic", "http://host").
@@ -68,12 +64,13 @@ public class DiagnoseTools {
private static final Pattern ROUTE_ID_PATTERN
= Pattern.compile("route[:\\s]+['\"]?([a-zA-Z0-9_-]+)['\"]?",
Pattern.CASE_INSENSITIVE);
+ @Inject
+ DiagnoseData diagnoseData;
+
private final CamelCatalog catalog;
- private final Map<String, ExceptionInfo> knownExceptions;
public DiagnoseTools() {
this.catalog = new DefaultCamelCatalog();
- this.knownExceptions = buildKnownExceptions();
}
/**
@@ -98,30 +95,10 @@ public class DiagnoseTools {
List<String> matchedExceptions = identifyExceptions(error);
JsonArray exceptionsJson = new JsonArray();
for (String exceptionName : matchedExceptions) {
- ExceptionInfo info = knownExceptions.get(exceptionName);
+ DiagnoseData.ExceptionInfo info =
diagnoseData.getException(exceptionName);
if (info != null) {
- JsonObject exJson = new JsonObject();
+ JsonObject exJson = info.toJson();
exJson.put("exception", exceptionName);
- exJson.put("description", info.description);
-
- JsonArray causesJson = new JsonArray();
- for (String cause : info.commonCauses) {
- causesJson.add(cause);
- }
- exJson.put("commonCauses", causesJson);
-
- JsonArray fixesJson = new JsonArray();
- for (String fix : info.suggestedFixes) {
- fixesJson.add(fix);
- }
- exJson.put("suggestedFixes", fixesJson);
-
- JsonArray docsJson = new JsonArray();
- for (String doc : info.documentationLinks) {
- docsJson.add(doc);
- }
- exJson.put("documentationLinks", docsJson);
-
exceptionsJson.add(exJson);
}
}
@@ -187,7 +164,7 @@ public class DiagnoseTools {
*/
private List<String> identifyExceptions(String error) {
List<String> matched = new ArrayList<>();
- for (String exceptionName : knownExceptions.keySet()) {
+ for (String exceptionName :
diagnoseData.getKnownExceptions().keySet()) {
if (error.contains(exceptionName)) {
matched.add(exceptionName);
}
@@ -255,325 +232,4 @@ public class DiagnoseTools {
|| content.contains("\"" + comp + "\"")
|| content.contains("'" + comp + "'");
}
-
- /**
- * Build the registry of known Camel exceptions with their causes, fixes,
and documentation.
- */
- private Map<String, ExceptionInfo> buildKnownExceptions() {
- Map<String, ExceptionInfo> exceptions = new LinkedHashMap<>();
-
- exceptions.put("NoSuchEndpointException", new ExceptionInfo(
- "The specified endpoint URI could not be resolved to any known
Camel component.",
- Arrays.asList(
- "Typo in the endpoint URI scheme (e.g., 'kafak:'
instead of 'kafka:')",
- "Missing component dependency in pom.xml or
build.gradle",
- "Component not on the classpath",
- "Using a component scheme that does not exist"),
- Arrays.asList(
- "Verify the endpoint URI scheme is spelled correctly",
- "Add the required camel-<component> dependency to your
project",
- "Check available components with 'camel-catalog' or
the Camel documentation",
- "Ensure the component JAR is on the classpath"),
- Arrays.asList(
- CAMEL_MANUAL_DOC + "component.html")));
-
- exceptions.put("ResolveEndpointFailedException", new ExceptionInfo(
- "Failed to resolve or create an endpoint from the given URI.
The URI syntax may be invalid or required options may be missing.",
- Arrays.asList(
- "Invalid endpoint URI syntax",
- "Missing required endpoint options",
- "Unknown or misspelled endpoint options",
- "Invalid option values (wrong type or format)",
- "Special characters in URI not properly encoded"),
- Arrays.asList(
- "Check the endpoint URI syntax against the component
documentation",
- "Ensure all required options are provided",
- "Verify option names are spelled correctly",
- "URL-encode special characters in the URI",
- "Use the endpoint DSL for type-safe endpoint
configuration"),
- Arrays.asList(
- CAMEL_MANUAL_DOC + "endpoint.html",
- CAMEL_MANUAL_DOC + "uris.html")));
-
- exceptions.put("FailedToCreateRouteException", new ExceptionInfo(
- "A route could not be created. This is typically a
configuration or wiring issue.",
- Arrays.asList(
- "Invalid endpoint URI in from() or to()",
- "Missing required component dependency",
- "Bean reference that cannot be resolved",
- "Invalid route configuration or DSL syntax",
- "Circular route dependencies"),
- Arrays.asList(
- "Check the full exception chain for the root cause",
- "Verify all endpoint URIs in the route are valid",
- "Ensure all referenced beans are available in the
registry",
- "Validate the route DSL syntax",
- "Check for missing component dependencies"),
- Arrays.asList(
- CAMEL_MANUAL_DOC + "routes.html",
- CAMEL_MANUAL_DOC + "route-configuration.html")));
-
- exceptions.put("FailedToStartRouteException", new ExceptionInfo(
- "A route was created but could not be started. This often
indicates a connectivity or resource issue.",
- Arrays.asList(
- "Cannot connect to external service (broker, database,
etc.)",
- "Port already in use for server-side components",
- "Authentication/authorization failure",
- "Missing or invalid SSL/TLS configuration",
- "Resource not available (queue, topic, table, etc.)"),
- Arrays.asList(
- "Verify network connectivity to external services",
- "Check credentials and authentication configuration",
- "Ensure the target resource exists (queue, topic,
etc.)",
- "Review SSL/TLS configuration if using secure
connections",
- "Check if the port is already in use"),
- Arrays.asList(
- CAMEL_MANUAL_DOC + "routes.html",
- CAMEL_MANUAL_DOC + "lifecycle.html")));
-
- exceptions.put("NoTypeConversionAvailableException", new ExceptionInfo(
- "Camel could not find a type converter to convert between the
required types.",
- Arrays.asList(
- "Trying to convert a message body to an incompatible
type",
- "Missing type converter on the classpath",
- "Custom type without a registered converter",
- "Null body when a non-null type is expected"),
- Arrays.asList(
- "Check the source and target types in the conversion",
- "Add appropriate data format or converter dependency",
- "Use explicit marshal/unmarshal instead of implicit
conversion",
- "Register a custom TypeConverter if needed",
- "Check if the message body is null"),
- Arrays.asList(
- CAMEL_MANUAL_DOC + "type-converter.html",
- CAMEL_MANUAL_DOC + "data-format.html")));
-
- exceptions.put("CamelExecutionException", new ExceptionInfo(
- "A wrapper exception thrown during route execution. The root
cause is in the nested exception.",
- Arrays.asList(
- "Exception thrown by a processor or bean in the route",
- "External service failure (HTTP error, broker
disconnect, etc.)",
- "Data transformation error",
- "Timeout during synchronous processing"),
- Arrays.asList(
- "Inspect the nested/caused-by exception for the actual
error",
- "Add error handling (onException, errorHandler,
doTry/doCatch) to the route",
- "Check the processor or bean that failed",
- "Review the full stack trace for the root cause"),
- Arrays.asList(
- CAMEL_MANUAL_DOC + "exception-clause.html",
- CAMEL_MANUAL_DOC + "error-handler.html",
- CAMEL_MANUAL_DOC + "try-catch-finally.html")));
-
- exceptions.put("ExchangeTimedOutException", new ExceptionInfo(
- "An exchange did not complete within the configured timeout
period.",
- Arrays.asList(
- "Slow downstream service or endpoint",
- "Network latency or connectivity issues",
- "Timeout value too low for the operation",
- "Deadlock or resource contention",
- "Direct/SEDA consumer not available"),
- Arrays.asList(
- "Increase the timeout value if appropriate",
- "Add circuit breaker pattern for unreliable services",
- "Check network connectivity to the target service",
- "Use async processing for long-running operations",
- "Add timeout error handling in the route"),
- Arrays.asList(
- CAMEL_MANUAL_DOC + "request-reply.html",
- CAMEL_EIP_DOC + "circuitBreaker-eip.html")));
-
- exceptions.put("DirectConsumerNotAvailableException", new
ExceptionInfo(
- "No consumer is available for a direct endpoint. The direct
component requires an active consumer.",
- Arrays.asList(
- "Target route with the direct endpoint is not started",
- "Typo in the direct endpoint name",
- "Route with the direct consumer was stopped or
removed",
- "Timing issue during startup — producer route started
before consumer route"),
- Arrays.asList(
- "Ensure a route with from(\"direct:name\") exists and
is started",
- "Verify the direct endpoint name matches between
producer and consumer",
- "Use SEDA instead of direct if startup ordering is
uncertain",
- "Configure route startup ordering if needed"),
- Arrays.asList(
- CAMEL_COMPONENT_DOC + "direct-component.html",
- CAMEL_COMPONENT_DOC + "seda-component.html")));
-
- exceptions.put("CamelExchangeException", new ExceptionInfo(
- "A general exception related to exchange processing.",
- Arrays.asList(
- "Processor failure during exchange handling",
- "Invalid exchange pattern (InOnly vs InOut mismatch)",
- "Missing required headers or properties",
- "Exchange body cannot be processed"),
- Arrays.asList(
- "Check the exchange pattern matches the endpoint
requirements",
- "Verify required headers are set on the exchange",
- "Add error handling to catch and process the
exception",
- "Inspect the exchange body type and content"),
- Arrays.asList(
- CAMEL_MANUAL_DOC + "exchange.html",
- CAMEL_MANUAL_DOC + "exchange-pattern.html")));
-
- exceptions.put("InvalidPayloadException", new ExceptionInfo(
- "The message payload (body) is not of the expected type and
cannot be converted.",
- Arrays.asList(
- "Message body is null when a value is expected",
- "Message body type does not match the expected type",
- "Missing type converter for the body type",
- "Upstream processor produced unexpected output"),
- Arrays.asList(
- "Check the message body type before the failing
processor",
- "Use convertBodyTo() to explicitly convert the body
type",
- "Add a null check or default value for the body",
- "Add the appropriate data format dependency for
marshalling/unmarshalling"),
- Arrays.asList(
- CAMEL_MANUAL_DOC + "message.html",
- CAMEL_MANUAL_DOC + "type-converter.html")));
-
- exceptions.put("PropertyBindingException", new ExceptionInfo(
- "Failed to bind a property or option to a Camel component,
endpoint, or bean.",
- Arrays.asList(
- "Property name does not exist on the target object",
- "Property value has wrong type (e.g., string for a
boolean)",
- "Misspelled property or option name",
- "Property placeholder could not be resolved"),
- Arrays.asList(
- "Check the property name spelling against the
component documentation",
- "Verify the property value type matches the expected
type",
- "Use property placeholders correctly:
{{property.name}}",
- "Check application.properties or YAML configuration
for correct keys"),
- Arrays.asList(
- CAMEL_MANUAL_DOC + "using-propertyplaceholder.html",
- CAMEL_MANUAL_DOC + "component.html")));
-
- exceptions.put("NoSuchBeanException", new ExceptionInfo(
- "A referenced bean could not be found in the Camel registry.",
- Arrays.asList(
- "Bean not registered in the Spring/CDI/Camel registry",
- "Typo in the bean name reference",
- "Bean class not on the classpath",
- "Missing @Named or @Component annotation on the bean
class",
- "Bean definition not scanned by component scan"),
- Arrays.asList(
- "Verify the bean is registered with the correct name",
- "Check spelling of the bean reference",
- "Ensure the bean class has proper annotations (@Named,
@Component, etc.)",
- "Verify component scanning includes the bean's
package",
- "Register the bean manually in RouteBuilder
configure() if needed"),
- Arrays.asList(
- CAMEL_MANUAL_DOC + "registry.html",
- CAMEL_MANUAL_DOC + "bean-binding.html")));
-
- exceptions.put("NoSuchHeaderException", new ExceptionInfo(
- "A required header was not found on the message.",
- Arrays.asList(
- "Header not set by upstream processors",
- "Header name is misspelled",
- "Header was removed by a previous processor",
- "Using wrong header constant name"),
- Arrays.asList(
- "Verify the header name matches what upstream
processors set",
- "Use header constants from the component's class
(e.g., KafkaConstants)",
- "Add a setHeader() before the processor that requires
it",
- "Add a null check or default value for the header"),
- Arrays.asList(
- CAMEL_MANUAL_DOC + "message.html")));
-
- exceptions.put("PredicateValidationException", new ExceptionInfo(
- "A predicate validation failed. This is typically thrown by
the validate() DSL or a filter condition.",
- Arrays.asList(
- "Message did not match the expected validation
predicate",
- "Invalid or unexpected message content",
- "Predicate expression has a syntax error",
- "Null values in the expression evaluation"),
- Arrays.asList(
- "Review the predicate expression for correctness",
- "Check the message content matches the expected
format",
- "Add error handling for validation failures",
- "Use a more lenient predicate or add default values"),
- Arrays.asList(
- CAMEL_EIP_DOC + "validate-eip.html",
- CAMEL_MANUAL_DOC + "predicate.html")));
-
- exceptions.put("NoSuchLanguageException", new ExceptionInfo(
- "The specified expression language is not available.",
- Arrays.asList(
- "Missing language dependency (e.g., camel-jsonpath,
camel-xpath)",
- "Typo in the language name",
- "Using a language that does not exist"),
- Arrays.asList(
- "Add the required camel-<language> dependency",
- "Verify the language name is spelled correctly",
- "Use 'simple' language which is included in
camel-core"),
- Arrays.asList(
- CAMEL_MANUAL_DOC + "languages.html")));
-
- exceptions.put("FailedToCreateConsumerException", new ExceptionInfo(
- "A consumer could not be created for the endpoint.",
- Arrays.asList(
- "Cannot connect to the source system (broker, server,
etc.)",
- "Invalid consumer configuration options",
- "Authentication failure",
- "Missing required consumer options",
- "Resource does not exist (topic, queue, file path,
etc.)"),
- Arrays.asList(
- "Verify connectivity to the source system",
- "Check the consumer configuration options",
- "Ensure credentials are correct",
- "Verify the target resource exists",
- "Check the full exception chain for the root cause"),
- Arrays.asList(
- CAMEL_MANUAL_DOC + "component.html")));
-
- exceptions.put("FailedToCreateProducerException", new ExceptionInfo(
- "A producer could not be created for the endpoint.",
- Arrays.asList(
- "Cannot connect to the target system",
- "Invalid producer configuration options",
- "Authentication failure",
- "Missing required producer options"),
- Arrays.asList(
- "Verify connectivity to the target system",
- "Check the producer configuration options",
- "Ensure credentials are correct",
- "Check the full exception chain for the root cause"),
- Arrays.asList(
- CAMEL_MANUAL_DOC + "component.html")));
-
- exceptions.put("CamelAuthorizationException", new ExceptionInfo(
- "An authorization check failed. The current identity does not
have permission to perform the operation.",
- Arrays.asList(
- "Insufficient permissions for the user or service
account",
- "Missing or expired authentication token",
- "Security policy denying the operation",
- "Incorrect RBAC or ACL configuration"),
- Arrays.asList(
- "Check the user/service account permissions",
- "Verify the authentication token is valid and not
expired",
- "Review the security policies and ACLs",
- "Ensure the correct security provider is configured"),
- Arrays.asList(
- CAMEL_MANUAL_DOC + "security.html")));
-
- return exceptions;
- }
-
- /**
- * Holds diagnostic information about a known Camel exception.
- */
- static class ExceptionInfo {
- final String description;
- final List<String> commonCauses;
- final List<String> suggestedFixes;
- final List<String> documentationLinks;
-
- ExceptionInfo(String description, List<String> commonCauses,
List<String> suggestedFixes,
- List<String> documentationLinks) {
- this.description = description;
- this.commonCauses = commonCauses;
- this.suggestedFixes = suggestedFixes;
- this.documentationLinks = documentationLinks;
- }
- }
}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestInfraData.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestInfraData.java
new file mode 100644
index 000000000000..a5d542b26c85
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestInfraData.java
@@ -0,0 +1,211 @@
+/*
+ * 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.mcp;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+/**
+ * Shared holder for Camel test infrastructure reference data used by both
{@link TestScaffoldTools} (MCP Tool) and
+ * {@link TestInfraResources} (MCP Resources).
+ * <p>
+ * Contains the registry of test-infra services (Testcontainers-based) that
map component schemes to their corresponding
+ * service classes, factory classes, and Maven artifact IDs.
+ */
+@ApplicationScoped
+public class TestInfraData {
+
+ /** Component schemes that are trivial and should not be replaced with
mocks. */
+ private static final Set<String> TRIVIAL_SCHEMES = Set.of("log", "direct",
"seda", "mock", "controlbus", "stub");
+
+ /** Component schemes that are internally triggered (user can send
messages to them). */
+ private static final Set<String> SENDABLE_SCHEMES = Set.of("direct",
"seda");
+
+ private static final Map<String, TestInfraInfo> TEST_INFRA_MAP;
+
+ static {
+ Map<String, TestInfraInfo> map = new LinkedHashMap<>();
+
+ TestInfraInfo kafka = new TestInfraInfo(
+ "KafkaService", "KafkaServiceFactory",
+ "camel-test-infra-kafka",
"org.apache.camel.test.infra.kafka.services");
+ map.put("kafka", kafka);
+
+ TestInfraInfo artemis = new TestInfraInfo(
+ "ArtemisService", "ArtemisServiceFactory",
+ "camel-test-infra-artemis",
"org.apache.camel.test.infra.artemis.services");
+ map.put("jms", artemis);
+ map.put("activemq", artemis);
+ map.put("sjms", artemis);
+ map.put("sjms2", artemis);
+ map.put("amqp", artemis);
+
+ TestInfraInfo mongodb = new TestInfraInfo(
+ "MongoDBService", "MongoDBServiceFactory",
+ "camel-test-infra-mongodb",
"org.apache.camel.test.infra.mongodb.services");
+ map.put("mongodb", mongodb);
+
+ TestInfraInfo postgres = new TestInfraInfo(
+ "PostgresService", "PostgresServiceFactory",
+ "camel-test-infra-postgres",
"org.apache.camel.test.infra.postgres.services");
+ map.put("sql", postgres);
+ map.put("jdbc", postgres);
+
+ TestInfraInfo cassandra = new TestInfraInfo(
+ "CassandraService", "CassandraServiceFactory",
+ "camel-test-infra-cassandra",
"org.apache.camel.test.infra.cassandra.services");
+ map.put("cql", cassandra);
+
+ TestInfraInfo elasticsearch = new TestInfraInfo(
+ "ElasticSearchService", "ElasticSearchServiceFactory",
+ "camel-test-infra-elasticsearch",
"org.apache.camel.test.infra.elasticsearch.services");
+ map.put("elasticsearch", elasticsearch);
+ map.put("elasticsearch-rest", elasticsearch);
+
+ TestInfraInfo redis = new TestInfraInfo(
+ "RedisService", "RedisServiceFactory",
+ "camel-test-infra-redis",
"org.apache.camel.test.infra.redis.services");
+ map.put("spring-redis", redis);
+
+ TestInfraInfo rabbitmq = new TestInfraInfo(
+ "RabbitMQService", "RabbitMQServiceFactory",
+ "camel-test-infra-rabbitmq",
"org.apache.camel.test.infra.rabbitmq.services");
+ map.put("rabbitmq", rabbitmq);
+
+ TestInfraInfo ftp = new TestInfraInfo(
+ "FtpService", "FtpServiceFactory",
+ "camel-test-infra-ftp",
"org.apache.camel.test.infra.ftp.services");
+ map.put("ftp", ftp);
+ map.put("sftp", ftp);
+ map.put("ftps", ftp);
+
+ TestInfraInfo consul = new TestInfraInfo(
+ "ConsulService", "ConsulServiceFactory",
+ "camel-test-infra-consul",
"org.apache.camel.test.infra.consul.services");
+ map.put("consul", consul);
+
+ TestInfraInfo nats = new TestInfraInfo(
+ "NatsService", "NatsServiceFactory",
+ "camel-test-infra-nats",
"org.apache.camel.test.infra.nats.services");
+ map.put("nats", nats);
+
+ TestInfraInfo pulsar = new TestInfraInfo(
+ "PulsarService", "PulsarServiceFactory",
+ "camel-test-infra-pulsar",
"org.apache.camel.test.infra.pulsar.services");
+ map.put("pulsar", pulsar);
+
+ TestInfraInfo couchdb = new TestInfraInfo(
+ "CouchDbService", "CouchDbServiceFactory",
+ "camel-test-infra-couchdb",
"org.apache.camel.test.infra.couchdb.services");
+ map.put("couchdb", couchdb);
+
+ TestInfraInfo infinispan = new TestInfraInfo(
+ "InfinispanService", "InfinispanServiceFactory",
+ "camel-test-infra-infinispan",
"org.apache.camel.test.infra.infinispan.services");
+ map.put("infinispan", infinispan);
+
+ TestInfraInfo minio = new TestInfraInfo(
+ "MinioService", "MinioServiceFactory",
+ "camel-test-infra-minio",
"org.apache.camel.test.infra.minio.services");
+ map.put("minio", minio);
+
+ TestInfraInfo solr = new TestInfraInfo(
+ "SolrService", "SolrServiceFactory",
+ "camel-test-infra-solr",
"org.apache.camel.test.infra.solr.services");
+ map.put("solr", solr);
+
+ TEST_INFRA_MAP = Collections.unmodifiableMap(map);
+ }
+
+ /**
+ * Get the full scheme-to-service mapping. Multiple schemes may map to the
same service (e.g., jms, activemq, sjms
+ * all map to ArtemisService).
+ */
+ public Map<String, TestInfraInfo> getTestInfraMap() {
+ return TEST_INFRA_MAP;
+ }
+
+ /**
+ * Get test infrastructure info for a specific component scheme.
+ *
+ * @return the infra info, or null if no test-infra service is available
for the scheme
+ */
+ public TestInfraInfo getTestInfra(String scheme) {
+ return TEST_INFRA_MAP.get(scheme);
+ }
+
+ /**
+ * Get all unique test-infra services (deduplicated, since multiple
schemes can map to the same service).
+ */
+ public List<TestInfraInfo> getUniqueServices() {
+ return new ArrayList<>(new LinkedHashSet<>(TEST_INFRA_MAP.values()));
+ }
+
+ /**
+ * Get all component schemes that have test-infra services available.
+ */
+ public List<String> getSupportedSchemes() {
+ return List.copyOf(TEST_INFRA_MAP.keySet());
+ }
+
+ /**
+ * Get the set of trivial component schemes that should not be replaced
with mocks.
+ */
+ public Set<String> getTrivialSchemes() {
+ return TRIVIAL_SCHEMES;
+ }
+
+ /**
+ * Get the set of component schemes that are internally triggered (user
can send messages to them).
+ */
+ public Set<String> getSendableSchemes() {
+ return SENDABLE_SCHEMES;
+ }
+
+ /**
+ * Holds test-infra service information for a Camel component.
+ */
+ public record TestInfraInfo(
+ String serviceClass,
+ String factoryClass,
+ String artifactId,
+ String packageName) {
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof TestInfraInfo that)) {
+ return false;
+ }
+ return artifactId.equals(that.artifactId);
+ }
+
+ @Override
+ public int hashCode() {
+ return artifactId.hashCode();
+ }
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestInfraResources.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestInfraResources.java
new file mode 100644
index 000000000000..44a286ee7d3d
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestInfraResources.java
@@ -0,0 +1,144 @@
+/*
+ * 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.mcp;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+import io.quarkiverse.mcp.server.Resource;
+import io.quarkiverse.mcp.server.ResourceTemplate;
+import io.quarkiverse.mcp.server.ResourceTemplateArg;
+import io.quarkiverse.mcp.server.TextResourceContents;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * MCP Resources exposing Camel test infrastructure reference data.
+ * <p>
+ * These resources provide browseable test-infra service mappings that clients
can pull into context when helping users
+ * write tests for Camel routes. Each service maps component schemes to their
Testcontainers-based test infrastructure
+ * classes and Maven dependencies.
+ */
+@ApplicationScoped
+public class TestInfraResources {
+
+ @Inject
+ TestInfraData testInfraData;
+
+ /**
+ * All available Camel test infrastructure services with their supported
component schemes.
+ */
+ @Resource(uri = "camel://testing/infra-services",
+ name = "camel_testing_infra_services",
+ title = "Camel Test Infrastructure Services",
+ description = "Registry of all available Camel
Testcontainers-based test infrastructure services "
+ + "including Kafka, Artemis (JMS), MongoDB,
PostgreSQL, Redis, RabbitMQ, FTP, and more. "
+ + "Each service lists its supported component
schemes, service/factory classes, "
+ + "and Maven artifact ID.",
+ mimeType = "application/json")
+ public TextResourceContents infraServices() {
+ JsonObject result = new JsonObject();
+
+ // Group schemes by unique service (reverse the map)
+ Map<TestInfraData.TestInfraInfo, List<String>> serviceToSchemes = new
LinkedHashMap<>();
+ for (Map.Entry<String, TestInfraData.TestInfraInfo> entry :
testInfraData.getTestInfraMap().entrySet()) {
+ serviceToSchemes
+ .computeIfAbsent(entry.getValue(), k -> new ArrayList<>())
+ .add(entry.getKey());
+ }
+
+ JsonArray services = new JsonArray();
+ for (Map.Entry<TestInfraData.TestInfraInfo, List<String>> entry :
serviceToSchemes.entrySet()) {
+ TestInfraData.TestInfraInfo info = entry.getKey();
+ JsonObject serviceJson = new JsonObject();
+ serviceJson.put("serviceClass", info.serviceClass());
+ serviceJson.put("factoryClass", info.factoryClass());
+ serviceJson.put("artifactId", info.artifactId());
+ serviceJson.put("package", info.packageName());
+
+ JsonArray schemesJson = new JsonArray();
+ for (String scheme : entry.getValue()) {
+ schemesJson.add(scheme);
+ }
+ serviceJson.put("schemes", schemesJson);
+
+ services.add(serviceJson);
+ }
+
+ result.put("services", services);
+ result.put("totalServices", services.size());
+ result.put("totalSchemes", testInfraData.getTestInfraMap().size());
+
+ return new TextResourceContents("camel://testing/infra-services",
result.toJson(), "application/json");
+ }
+
+ /**
+ * Test infrastructure detail for a specific component scheme.
+ */
+ @ResourceTemplate(uriTemplate = "camel://testing/infra-service/{scheme}",
+ name = "camel_testing_infra_service_detail",
+ title = "Test Infrastructure Service Detail",
+ description = "Test infrastructure detail for a specific
Camel component scheme including "
+ + "the Testcontainers service class,
factory class, Maven artifact ID, "
+ + "and a usage snippet with
@RegisterExtension.",
+ mimeType = "application/json")
+ public TextResourceContents infraServiceDetail(
+ @ResourceTemplateArg(name = "scheme") String scheme) {
+
+ String uri = "camel://testing/infra-service/" + scheme;
+
+ TestInfraData.TestInfraInfo info = testInfraData.getTestInfra(scheme);
+ if (info == null) {
+ JsonObject result = new JsonObject();
+ result.put("scheme", scheme);
+ result.put("found", false);
+ result.put("message", "No test infrastructure service is available
for scheme '" + scheme + "'. "
+ + "Use the camel://testing/infra-services
resource to see all supported schemes.");
+ return new TextResourceContents(uri, result.toJson(),
"application/json");
+ }
+
+ JsonObject result = new JsonObject();
+ result.put("scheme", scheme);
+ result.put("found", true);
+ result.put("serviceClass", info.serviceClass());
+ result.put("factoryClass", info.factoryClass());
+ result.put("artifactId", info.artifactId());
+ result.put("package", info.packageName());
+
+ // Usage snippet
+ String fieldName =
Character.toLowerCase(info.serviceClass().charAt(0)) +
info.serviceClass().substring(1);
+ String snippet = "@RegisterExtension\n"
+ + "static " + info.serviceClass() + " " + fieldName
+ + " = " + info.factoryClass() + ".createService();";
+ result.put("usageSnippet", snippet);
+
+ // Maven dependency
+ result.put("mavenDependency",
+ "<dependency>\n"
+ + "
<groupId>org.apache.camel</groupId>\n"
+ + " <artifactId>" + info.artifactId()
+ "</artifactId>\n"
+ + " <scope>test</scope>\n"
+ + "</dependency>");
+
+ return new TextResourceContents(uri, result.toJson(),
"application/json");
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldTools.java
index 39e7cace3d70..0c0090cb94a0 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldTools.java
@@ -17,15 +17,14 @@
package org.apache.camel.dsl.jbang.core.commands.mcp;
import java.util.ArrayList;
-import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
-import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.ToolArg;
@@ -65,13 +64,8 @@ public class TestScaffoldTools {
private static final Pattern XML_TO =
Pattern.compile("<to\\s+uri=[\"']([^\"']+)[\"']", Pattern.CASE_INSENSITIVE);
private static final Pattern XML_TOD =
Pattern.compile("<toD\\s+uri=[\"']([^\"']+)[\"']", Pattern.CASE_INSENSITIVE);
- /** Component schemes that are trivial and should not be replaced with
mocks. */
- private static final Set<String> TRIVIAL_SCHEMES = Set.of("log", "direct",
"seda", "mock", "controlbus", "stub");
-
- /** Component schemes that are internally triggered (user can send
messages to them). */
- private static final Set<String> SENDABLE_SCHEMES = Set.of("direct",
"seda");
-
- private static final Map<String, TestInfraInfo> TEST_INFRA_MAP =
buildTestInfraMap();
+ @Inject
+ TestInfraData testInfraData;
private final CamelCatalog catalog;
@@ -111,12 +105,12 @@ public class TestScaffoldTools {
List<String> mockEndpoints = toEndpoints.stream()
.filter(uri -> {
String scheme = extractScheme(uri);
- return scheme != null &&
!TRIVIAL_SCHEMES.contains(scheme);
+ return scheme != null &&
!testInfraData.getTrivialSchemes().contains(scheme);
})
.toList();
// Determine test-infra services needed (deduplicated)
- List<TestInfraInfo> infraServices =
resolveInfraServices(allSchemes);
+ List<TestInfraData.TestInfraInfo> infraServices =
resolveInfraServices(allSchemes);
// Generate test code
String testCode = "spring-boot".equals(resolvedRuntime)
@@ -250,10 +244,10 @@ public class TestScaffoldTools {
// ---- Test-infra resolution ----
- private List<TestInfraInfo> resolveInfraServices(List<String> schemes) {
- Set<TestInfraInfo> seen = new LinkedHashSet<>();
+ private List<TestInfraData.TestInfraInfo>
resolveInfraServices(List<String> schemes) {
+ Set<TestInfraData.TestInfraInfo> seen = new LinkedHashSet<>();
for (String scheme : schemes) {
- TestInfraInfo info = TEST_INFRA_MAP.get(scheme);
+ TestInfraData.TestInfraInfo info =
testInfraData.getTestInfra(scheme);
if (info != null) {
seen.add(info);
}
@@ -265,7 +259,7 @@ public class TestScaffoldTools {
private String generateMainTest(
List<String> fromEndpoints, List<String> mockEndpoints,
- List<TestInfraInfo> infraServices) {
+ List<TestInfraData.TestInfraInfo> infraServices) {
StringBuilder sb = new StringBuilder();
@@ -281,13 +275,13 @@ public class TestScaffoldTools {
sb.append("import java.util.concurrent.TimeUnit;\n");
}
sb.append("import org.junit.jupiter.api.Test;\n");
- for (TestInfraInfo info : infraServices) {
+ for (TestInfraData.TestInfraInfo info : infraServices) {
sb.append("import
org.junit.jupiter.api.extension.RegisterExtension;\n");
break;
}
- for (TestInfraInfo info : infraServices) {
- sb.append("import
").append(info.packageName).append('.').append(info.serviceClass).append(";\n");
- sb.append("import
").append(info.packageName).append('.').append(info.factoryClass).append(";\n");
+ for (TestInfraData.TestInfraInfo info : infraServices) {
+ sb.append("import
").append(info.packageName()).append('.').append(info.serviceClass()).append(";\n");
+ sb.append("import
").append(info.packageName()).append('.').append(info.factoryClass()).append(";\n");
}
sb.append("\n");
sb.append("import static
org.junit.jupiter.api.Assertions.assertTrue;\n");
@@ -297,11 +291,11 @@ public class TestScaffoldTools {
sb.append("class RouteTest extends CamelTestSupport {\n\n");
// @RegisterExtension fields
- for (TestInfraInfo info : infraServices) {
- String fieldName =
Character.toLowerCase(info.serviceClass.charAt(0)) +
info.serviceClass.substring(1);
+ for (TestInfraData.TestInfraInfo info : infraServices) {
+ String fieldName =
Character.toLowerCase(info.serviceClass().charAt(0)) +
info.serviceClass().substring(1);
sb.append(" @RegisterExtension\n");
- sb.append(" static ").append(info.serviceClass).append('
').append(fieldName);
- sb.append(" =
").append(info.factoryClass).append(".createService();\n\n");
+ sb.append(" static ").append(info.serviceClass()).append('
').append(fieldName);
+ sb.append(" =
").append(info.factoryClass()).append(".createService();\n\n");
}
// createRouteBuilder
@@ -313,7 +307,7 @@ public class TestScaffoldTools {
String fromUri = fromEndpoints.isEmpty() ? "direct:start" :
fromEndpoints.get(0);
String fromScheme = extractScheme(fromUri);
- boolean isSendable = fromScheme != null &&
SENDABLE_SCHEMES.contains(fromScheme);
+ boolean isSendable = fromScheme != null &&
testInfraData.getSendableSchemes().contains(fromScheme);
boolean isTimer = "timer".equals(fromScheme);
if (!isSendable && !isTimer && !fromEndpoints.isEmpty()) {
@@ -380,7 +374,7 @@ public class TestScaffoldTools {
private String generateSpringBootTest(
List<String> fromEndpoints, List<String> mockEndpoints,
- List<TestInfraInfo> infraServices) {
+ List<TestInfraData.TestInfraInfo> infraServices) {
StringBuilder sb = new StringBuilder();
@@ -398,13 +392,13 @@ public class TestScaffoldTools {
sb.append("import org.junit.jupiter.api.Test;\n");
sb.append("import
org.springframework.beans.factory.annotation.Autowired;\n");
sb.append("import
org.springframework.boot.test.context.SpringBootTest;\n");
- for (TestInfraInfo info : infraServices) {
+ for (TestInfraData.TestInfraInfo info : infraServices) {
sb.append("import
org.junit.jupiter.api.extension.RegisterExtension;\n");
break;
}
- for (TestInfraInfo info : infraServices) {
- sb.append("import
").append(info.packageName).append('.').append(info.serviceClass).append(";\n");
- sb.append("import
").append(info.packageName).append('.').append(info.factoryClass).append(";\n");
+ for (TestInfraData.TestInfraInfo info : infraServices) {
+ sb.append("import
").append(info.packageName()).append('.').append(info.serviceClass()).append(";\n");
+ sb.append("import
").append(info.packageName()).append('.').append(info.factoryClass()).append(";\n");
}
sb.append("\n");
sb.append("import static
org.junit.jupiter.api.Assertions.assertTrue;\n");
@@ -422,11 +416,11 @@ public class TestScaffoldTools {
sb.append(" private ProducerTemplate template;\n\n");
// @RegisterExtension fields
- for (TestInfraInfo info : infraServices) {
- String fieldName =
Character.toLowerCase(info.serviceClass.charAt(0)) +
info.serviceClass.substring(1);
+ for (TestInfraData.TestInfraInfo info : infraServices) {
+ String fieldName =
Character.toLowerCase(info.serviceClass().charAt(0)) +
info.serviceClass().substring(1);
sb.append(" @RegisterExtension\n");
- sb.append(" static ").append(info.serviceClass).append('
').append(fieldName);
- sb.append(" =
").append(info.factoryClass).append(".createService();\n\n");
+ sb.append(" static ").append(info.serviceClass()).append('
').append(fieldName);
+ sb.append(" =
").append(info.factoryClass()).append(".createService();\n\n");
}
// Test method
@@ -447,7 +441,7 @@ public class TestScaffoldTools {
// Send or wait
String fromUri = fromEndpoints.isEmpty() ? "direct:start" :
fromEndpoints.get(0);
String fromScheme = extractScheme(fromUri);
- boolean isSendable = fromScheme != null &&
SENDABLE_SCHEMES.contains(fromScheme);
+ boolean isSendable = fromScheme != null &&
testInfraData.getSendableSchemes().contains(fromScheme);
boolean isTimer = "timer".equals(fromScheme);
if (isTimer) {
@@ -517,7 +511,7 @@ public class TestScaffoldTools {
private String buildResult(
String testCode, String format, String runtime,
List<String> allSchemes, List<String> fromEndpoints, List<String>
toEndpoints,
- List<String> mockEndpoints, List<TestInfraInfo> infraServices) {
+ List<String> mockEndpoints, List<TestInfraData.TestInfraInfo>
infraServices) {
JsonObject result = new JsonObject();
result.put("testCode", testCode);
@@ -551,12 +545,12 @@ public class TestScaffoldTools {
// Test-infra services
JsonArray infraJson = new JsonArray();
- for (TestInfraInfo info : infraServices) {
+ for (TestInfraData.TestInfraInfo info : infraServices) {
JsonObject infraObj = new JsonObject();
- infraObj.put("service", info.serviceClass);
- infraObj.put("factory", info.factoryClass);
- infraObj.put("artifactId", info.artifactId);
- infraObj.put("package", info.packageName);
+ infraObj.put("service", info.serviceClass());
+ infraObj.put("factory", info.factoryClass());
+ infraObj.put("artifactId", info.artifactId());
+ infraObj.put("package", info.packageName());
infraJson.add(infraObj);
}
result.put("testInfraServices", infraJson);
@@ -567,8 +561,8 @@ public class TestScaffoldTools {
if (!mockEndpoints.isEmpty()) {
addDependency(depsJson, "camel-mock");
}
- for (TestInfraInfo info : infraServices) {
- addDependency(depsJson, info.artifactId);
+ for (TestInfraData.TestInfraInfo info : infraServices) {
+ addDependency(depsJson, info.artifactId());
}
if ("spring-boot".equals(runtime)) {
addDependency(depsJson, "camel-test-spring-junit5");
@@ -594,133 +588,4 @@ public class TestScaffoldTools {
dep.put("scope", "test");
array.add(dep);
}
-
- // ---- Test-infra mapping ----
-
- private static Map<String, TestInfraInfo> buildTestInfraMap() {
- Map<String, TestInfraInfo> map = new LinkedHashMap<>();
-
- TestInfraInfo kafka = new TestInfraInfo(
- "KafkaService", "KafkaServiceFactory",
- "camel-test-infra-kafka",
"org.apache.camel.test.infra.kafka.services");
- map.put("kafka", kafka);
-
- TestInfraInfo artemis = new TestInfraInfo(
- "ArtemisService", "ArtemisServiceFactory",
- "camel-test-infra-artemis",
"org.apache.camel.test.infra.artemis.services");
- map.put("jms", artemis);
- map.put("activemq", artemis);
- map.put("sjms", artemis);
- map.put("sjms2", artemis);
- map.put("amqp", artemis);
-
- TestInfraInfo mongodb = new TestInfraInfo(
- "MongoDBService", "MongoDBServiceFactory",
- "camel-test-infra-mongodb",
"org.apache.camel.test.infra.mongodb.services");
- map.put("mongodb", mongodb);
-
- TestInfraInfo postgres = new TestInfraInfo(
- "PostgresService", "PostgresServiceFactory",
- "camel-test-infra-postgres",
"org.apache.camel.test.infra.postgres.services");
- map.put("sql", postgres);
- map.put("jdbc", postgres);
-
- TestInfraInfo cassandra = new TestInfraInfo(
- "CassandraService", "CassandraServiceFactory",
- "camel-test-infra-cassandra",
"org.apache.camel.test.infra.cassandra.services");
- map.put("cql", cassandra);
-
- TestInfraInfo elasticsearch = new TestInfraInfo(
- "ElasticSearchService", "ElasticSearchServiceFactory",
- "camel-test-infra-elasticsearch",
"org.apache.camel.test.infra.elasticsearch.services");
- map.put("elasticsearch", elasticsearch);
- map.put("elasticsearch-rest", elasticsearch);
-
- TestInfraInfo redis = new TestInfraInfo(
- "RedisService", "RedisServiceFactory",
- "camel-test-infra-redis",
"org.apache.camel.test.infra.redis.services");
- map.put("spring-redis", redis);
-
- TestInfraInfo rabbitmq = new TestInfraInfo(
- "RabbitMQService", "RabbitMQServiceFactory",
- "camel-test-infra-rabbitmq",
"org.apache.camel.test.infra.rabbitmq.services");
- map.put("rabbitmq", rabbitmq);
-
- TestInfraInfo ftp = new TestInfraInfo(
- "FtpService", "FtpServiceFactory",
- "camel-test-infra-ftp",
"org.apache.camel.test.infra.ftp.services");
- map.put("ftp", ftp);
- map.put("sftp", ftp);
- map.put("ftps", ftp);
-
- TestInfraInfo consul = new TestInfraInfo(
- "ConsulService", "ConsulServiceFactory",
- "camel-test-infra-consul",
"org.apache.camel.test.infra.consul.services");
- map.put("consul", consul);
-
- TestInfraInfo nats = new TestInfraInfo(
- "NatsService", "NatsServiceFactory",
- "camel-test-infra-nats",
"org.apache.camel.test.infra.nats.services");
- map.put("nats", nats);
-
- TestInfraInfo pulsar = new TestInfraInfo(
- "PulsarService", "PulsarServiceFactory",
- "camel-test-infra-pulsar",
"org.apache.camel.test.infra.pulsar.services");
- map.put("pulsar", pulsar);
-
- TestInfraInfo couchdb = new TestInfraInfo(
- "CouchDbService", "CouchDbServiceFactory",
- "camel-test-infra-couchdb",
"org.apache.camel.test.infra.couchdb.services");
- map.put("couchdb", couchdb);
-
- TestInfraInfo infinispan = new TestInfraInfo(
- "InfinispanService", "InfinispanServiceFactory",
- "camel-test-infra-infinispan",
"org.apache.camel.test.infra.infinispan.services");
- map.put("infinispan", infinispan);
-
- TestInfraInfo minio = new TestInfraInfo(
- "MinioService", "MinioServiceFactory",
- "camel-test-infra-minio",
"org.apache.camel.test.infra.minio.services");
- map.put("minio", minio);
-
- TestInfraInfo solr = new TestInfraInfo(
- "SolrService", "SolrServiceFactory",
- "camel-test-infra-solr",
"org.apache.camel.test.infra.solr.services");
- map.put("solr", solr);
-
- return map;
- }
-
- /**
- * Holds test-infra service information for a component.
- */
- static final class TestInfraInfo {
- final String serviceClass;
- final String factoryClass;
- final String artifactId;
- final String packageName;
-
- TestInfraInfo(String serviceClass, String factoryClass, String
artifactId, String packageName) {
- this.serviceClass = serviceClass;
- this.factoryClass = factoryClass;
- this.artifactId = artifactId;
- this.packageName = packageName;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (!(o instanceof TestInfraInfo that)) {
- return false;
- }
- return artifactId.equals(that.artifactId);
- }
-
- @Override
- public int hashCode() {
- return artifactId.hashCode();
- }
- }
}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckToolsTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckToolsTest.java
index b4e578a9da27..a6413338fb0d 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckToolsTest.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckToolsTest.java
@@ -27,7 +27,12 @@ import static
org.assertj.core.api.Assertions.assertThatThrownBy;
class DependencyCheckToolsTest {
- private final DependencyCheckTools tools = new DependencyCheckTools();
+ private final DependencyCheckTools tools;
+
+ DependencyCheckToolsTest() {
+ tools = new DependencyCheckTools();
+ tools.dependencyData = new DependencyData();
+ }
// A pom.xml with Camel BOM and some components
private static final String POM_WITH_BOM = """
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyResourcesTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyResourcesTest.java
new file mode 100644
index 000000000000..6f00b8e295ee
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyResourcesTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.mcp;
+
+import io.quarkiverse.mcp.server.TextResourceContents;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class DependencyResourcesTest {
+
+ private final DependencyResources resources;
+
+ DependencyResourcesTest() {
+ resources = new DependencyResources();
+ resources.dependencyData = new DependencyData();
+ }
+
+ // ---- Core transitive artifacts ----
+
+ @Test
+ void coreTransitiveArtifactsReturnsValidJson() throws Exception {
+ TextResourceContents contents = resources.coreTransitiveArtifacts();
+
+
assertThat(contents.uri()).isEqualTo("camel://dependency/core-transitive-artifacts");
+ assertThat(contents.mimeType()).isEqualTo("application/json");
+
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+ assertThat(result.getInteger("totalCount")).isGreaterThan(0);
+
+ JsonArray artifacts = result.getCollection("artifacts");
+ assertThat(artifacts).isNotEmpty();
+ }
+
+ @Test
+ void coreTransitiveArtifactsContainsKnownEntries() throws Exception {
+ TextResourceContents contents = resources.coreTransitiveArtifacts();
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+ JsonArray artifacts = result.getCollection("artifacts");
+
+ assertThat(artifacts).contains("camel-core", "camel-direct",
"camel-log",
+ "camel-seda", "camel-timer", "camel-api", "camel-support");
+ }
+
+ @Test
+ void coreTransitiveArtifactsAreSorted() throws Exception {
+ TextResourceContents contents = resources.coreTransitiveArtifacts();
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+ JsonArray artifacts = result.getCollection("artifacts");
+
+ for (int i = 1; i < artifacts.size(); i++) {
+ String prev = (String) artifacts.get(i - 1);
+ String curr = (String) artifacts.get(i);
+ assertThat(prev.compareTo(curr))
+ .as("Expected sorted order: '%s' should come before '%s'",
prev, curr)
+ .isLessThanOrEqualTo(0);
+ }
+ }
+
+ @Test
+ void coreTransitiveArtifactsCountMatchesData() throws Exception {
+ TextResourceContents contents = resources.coreTransitiveArtifacts();
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+
+ DependencyData data = new DependencyData();
+
assertThat(result.getInteger("totalCount")).isEqualTo(data.getCoreTransitiveArtifacts().size());
+ }
+
+ // ---- BOM template for known runtimes ----
+
+ @Test
+ void bomTemplateReturnsMainRuntime() throws Exception {
+ TextResourceContents contents = resources.bomTemplate("main");
+
+
assertThat(contents.uri()).isEqualTo("camel://dependency/bom-template/main");
+ assertThat(contents.mimeType()).isEqualTo("application/json");
+
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+ assertThat(result.getString("runtime")).isEqualTo("main");
+ assertThat(result.getBoolean("found")).isTrue();
+ assertThat(result.getString("groupId")).isEqualTo("org.apache.camel");
+ assertThat(result.getString("artifactId")).isEqualTo("camel-bom");
+
assertThat(result.getString("snippet")).contains("<dependencyManagement>");
+ assertThat(result.getString("snippet")).contains("camel-bom");
+ }
+
+ @Test
+ void bomTemplateReturnsSpringBootRuntime() throws Exception {
+ TextResourceContents contents = resources.bomTemplate("spring-boot");
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+
+ assertThat(result.getBoolean("found")).isTrue();
+
assertThat(result.getString("groupId")).isEqualTo("org.apache.camel.springboot");
+
assertThat(result.getString("artifactId")).isEqualTo("camel-spring-boot-bom");
+
assertThat(result.getString("snippet")).contains("camel-spring-boot-bom");
+ }
+
+ @Test
+ void bomTemplateReturnsQuarkusRuntime() throws Exception {
+ TextResourceContents contents = resources.bomTemplate("quarkus");
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+
+ assertThat(result.getBoolean("found")).isTrue();
+
assertThat(result.getString("groupId")).isEqualTo("org.apache.camel.quarkus");
+
assertThat(result.getString("artifactId")).isEqualTo("camel-quarkus-bom");
+ assertThat(result.getString("snippet")).contains("camel-quarkus-bom");
+ }
+
+ // ---- BOM template not found ----
+
+ @Test
+ void bomTemplateReturnsNotFoundForUnknownRuntime() throws Exception {
+ TextResourceContents contents = resources.bomTemplate("unknown");
+
+
assertThat(contents.uri()).isEqualTo("camel://dependency/bom-template/unknown");
+
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+ assertThat(result.getString("runtime")).isEqualTo("unknown");
+ assertThat(result.getBoolean("found")).isFalse();
+ assertThat(result.getString("message")).contains("Unknown runtime");
+
+ JsonArray available = result.getCollection("availableRuntimes");
+ assertThat(available).contains("main", "spring-boot", "quarkus");
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseResourcesTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseResourcesTest.java
new file mode 100644
index 000000000000..9a9575c1b753
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseResourcesTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.mcp;
+
+import io.quarkiverse.mcp.server.TextResourceContents;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class DiagnoseResourcesTest {
+
+ private final DiagnoseResources resources;
+
+ DiagnoseResourcesTest() {
+ resources = new DiagnoseResources();
+ resources.diagnoseData = new DiagnoseData();
+ }
+
+ // ---- Exception catalog ----
+
+ @Test
+ void catalogReturnsValidJson() throws Exception {
+ TextResourceContents contents = resources.exceptionCatalog();
+
+
assertThat(contents.uri()).isEqualTo("camel://error/exception-catalog");
+ assertThat(contents.mimeType()).isEqualTo("application/json");
+
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+ assertThat(result.getInteger("totalCount")).isGreaterThan(0);
+
+ JsonArray exceptions = result.getCollection("exceptions");
+ assertThat(exceptions).isNotEmpty();
+ }
+
+ @Test
+ void catalogContainsAllKnownExceptions() throws Exception {
+ TextResourceContents contents = resources.exceptionCatalog();
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+
+ DiagnoseData data = new DiagnoseData();
+ int expectedCount = data.getKnownExceptions().size();
+
+ assertThat(result.getInteger("totalCount")).isEqualTo(expectedCount);
+
+ JsonArray exceptions = result.getCollection("exceptions");
+ assertThat(exceptions).hasSize(expectedCount);
+ }
+
+ @Test
+ void catalogEntriesHaveRequiredFields() throws Exception {
+ TextResourceContents contents = resources.exceptionCatalog();
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+ JsonArray exceptions = result.getCollection("exceptions");
+
+ for (Object obj : exceptions) {
+ JsonObject entry = (JsonObject) obj;
+ assertThat(entry.getString("name")).isNotBlank();
+ assertThat(entry.getString("description")).isNotBlank();
+ JsonArray entryDocs = entry.getCollection("documentationLinks");
+ assertThat(entryDocs).isNotEmpty();
+ }
+ }
+
+ // ---- Exception detail for known exception ----
+
+ @Test
+ void detailReturnsKnownException() throws Exception {
+ TextResourceContents contents =
resources.exceptionDetail("NoSuchEndpointException");
+
+
assertThat(contents.uri()).isEqualTo("camel://error/exception/NoSuchEndpointException");
+ assertThat(contents.mimeType()).isEqualTo("application/json");
+
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+
assertThat(result.getString("name")).isEqualTo("NoSuchEndpointException");
+ assertThat(result.getBoolean("found")).isTrue();
+ assertThat(result.getString("description")).isNotBlank();
+
+ JsonArray causes = result.getCollection("commonCauses");
+ assertThat(causes).isNotEmpty();
+
+ JsonArray fixes = result.getCollection("suggestedFixes");
+ assertThat(fixes).isNotEmpty();
+
+ JsonArray docs = result.getCollection("documentationLinks");
+ assertThat(docs).isNotEmpty();
+ }
+
+ @Test
+ void detailReturnsAllFieldsForCamelExecutionException() throws Exception {
+ TextResourceContents contents =
resources.exceptionDetail("CamelExecutionException");
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+
+ assertThat(result.getBoolean("found")).isTrue();
+
+ JsonArray causes = result.getCollection("commonCauses");
+ assertThat(causes.size()).isGreaterThanOrEqualTo(3);
+
+ JsonArray fixes = result.getCollection("suggestedFixes");
+ assertThat(fixes.size()).isGreaterThanOrEqualTo(3);
+
+ JsonArray docs = result.getCollection("documentationLinks");
+ assertThat(docs.size()).isGreaterThanOrEqualTo(2);
+ }
+
+ // ---- Exception detail not found ----
+
+ @Test
+ void detailReturnsNotFoundForUnknownException() throws Exception {
+ TextResourceContents contents =
resources.exceptionDetail("NonExistentException");
+
+
assertThat(contents.uri()).isEqualTo("camel://error/exception/NonExistentException");
+
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+ assertThat(result.getString("name")).isEqualTo("NonExistentException");
+ assertThat(result.getBoolean("found")).isFalse();
+ assertThat(result.getString("message")).contains("not in the known
exceptions catalog");
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseToolsTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseToolsTest.java
index 92486b03c223..be45f4d52942 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseToolsTest.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseToolsTest.java
@@ -27,7 +27,12 @@ import static
org.assertj.core.api.Assertions.assertThatThrownBy;
class DiagnoseToolsTest {
- private final DiagnoseTools tools = new DiagnoseTools();
+ private final DiagnoseTools tools;
+
+ DiagnoseToolsTest() {
+ tools = new DiagnoseTools();
+ tools.diagnoseData = new DiagnoseData();
+ }
// ---- Input validation ----
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestInfraResourcesTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestInfraResourcesTest.java
new file mode 100644
index 000000000000..9cf162478899
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestInfraResourcesTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.mcp;
+
+import java.util.List;
+
+import io.quarkiverse.mcp.server.TextResourceContents;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class TestInfraResourcesTest {
+
+ private final TestInfraResources resources;
+
+ TestInfraResourcesTest() {
+ resources = new TestInfraResources();
+ resources.testInfraData = new TestInfraData();
+ }
+
+ // ---- Infra services catalog ----
+
+ @Test
+ void infraServicesReturnsValidJson() throws Exception {
+ TextResourceContents contents = resources.infraServices();
+
+ assertThat(contents.uri()).isEqualTo("camel://testing/infra-services");
+ assertThat(contents.mimeType()).isEqualTo("application/json");
+
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+ assertThat(result.getInteger("totalServices")).isGreaterThan(0);
+ assertThat(result.getInteger("totalSchemes")).isGreaterThan(0);
+
+ JsonArray services = result.getCollection("services");
+ assertThat(services).isNotEmpty();
+ }
+
+ @Test
+ void infraServicesGroupsSchemesByService() throws Exception {
+ TextResourceContents contents = resources.infraServices();
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+ JsonArray services = result.getCollection("services");
+
+ // Find the Artemis service (handles jms, activemq, sjms, sjms2, amqp)
+ JsonObject artemis = services.stream()
+ .map(s -> (JsonObject) s)
+ .filter(s ->
"ArtemisService".equals(s.getString("serviceClass")))
+ .findFirst()
+ .orElse(null);
+
+ assertThat(artemis).isNotNull();
+ JsonArray schemes = artemis.getCollection("schemes");
+ assertThat(schemes).contains("jms", "activemq");
+ assertThat(schemes).hasSizeGreaterThanOrEqualTo(3);
+ }
+
+ @Test
+ void infraServicesEntriesHaveRequiredFields() throws Exception {
+ TextResourceContents contents = resources.infraServices();
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+ JsonArray services = result.getCollection("services");
+
+ for (Object obj : services) {
+ JsonObject entry = (JsonObject) obj;
+ assertThat(entry.getString("serviceClass")).isNotBlank();
+ assertThat(entry.getString("factoryClass")).isNotBlank();
+ assertThat(entry.getString("artifactId")).isNotBlank();
+ assertThat(entry.getString("package")).isNotBlank();
+ JsonArray entrySchemes = entry.getCollection("schemes");
+ assertThat(entrySchemes).isNotEmpty();
+ }
+ }
+
+ @Test
+ void infraServicesTotalSchemeMatchesMap() throws Exception {
+ TextResourceContents contents = resources.infraServices();
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+
+ TestInfraData data = new TestInfraData();
+
assertThat(result.getInteger("totalSchemes")).isEqualTo(data.getTestInfraMap().size());
+ }
+
+ // ---- Infra service detail for known scheme ----
+
+ @Test
+ void detailReturnsKafkaService() throws Exception {
+ TextResourceContents contents = resources.infraServiceDetail("kafka");
+
+
assertThat(contents.uri()).isEqualTo("camel://testing/infra-service/kafka");
+ assertThat(contents.mimeType()).isEqualTo("application/json");
+
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+ assertThat(result.getString("scheme")).isEqualTo("kafka");
+ assertThat(result.getBoolean("found")).isTrue();
+ assertThat(result.getString("serviceClass")).isEqualTo("KafkaService");
+
assertThat(result.getString("factoryClass")).isEqualTo("KafkaServiceFactory");
+
assertThat(result.getString("artifactId")).isEqualTo("camel-test-infra-kafka");
+ assertThat(result.getString("package")).isNotBlank();
+
assertThat(result.getString("usageSnippet")).contains("@RegisterExtension");
+
assertThat(result.getString("usageSnippet")).contains("KafkaServiceFactory.createService()");
+
assertThat(result.getString("mavenDependency")).contains("camel-test-infra-kafka");
+ }
+
+ @Test
+ void detailReturnsArtemisForJmsScheme() throws Exception {
+ TextResourceContents contents = resources.infraServiceDetail("jms");
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+
+ assertThat(result.getBoolean("found")).isTrue();
+
assertThat(result.getString("serviceClass")).isEqualTo("ArtemisService");
+
assertThat(result.getString("artifactId")).isEqualTo("camel-test-infra-artemis");
+ }
+
+ // ---- Infra service detail not found ----
+
+ @Test
+ void detailReturnsNotFoundForUnknownScheme() throws Exception {
+ TextResourceContents contents =
resources.infraServiceDetail("nonexistent");
+
+
assertThat(contents.uri()).isEqualTo("camel://testing/infra-service/nonexistent");
+
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+ assertThat(result.getString("scheme")).isEqualTo("nonexistent");
+ assertThat(result.getBoolean("found")).isFalse();
+ assertThat(result.getString("message")).contains("No test
infrastructure service");
+ }
+
+ // ---- Deduplication ----
+
+ @Test
+ void infraServicesDeduplicatesServices() throws Exception {
+ TextResourceContents contents = resources.infraServices();
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+ JsonArray services = result.getCollection("services");
+
+ // Ensure no duplicate service classes
+ List<String> serviceClasses = services.stream()
+ .map(s -> ((JsonObject) s).getString("serviceClass"))
+ .toList();
+ assertThat(serviceClasses).doesNotHaveDuplicates();
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldToolsTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldToolsTest.java
index 1f680d47aaf9..90705763a219 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldToolsTest.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldToolsTest.java
@@ -29,7 +29,12 @@ import static
org.assertj.core.api.Assertions.assertThatThrownBy;
class TestScaffoldToolsTest {
- private final TestScaffoldTools tools = new TestScaffoldTools();
+ private final TestScaffoldTools tools;
+
+ TestScaffoldToolsTest() {
+ tools = new TestScaffoldTools();
+ tools.testInfraData = new TestInfraData();
+ }
// ---- Input validation ----