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 e0f63be5a64d CAMEL-23168 - Camel-Jbang-mcp: Make MCP exception catalog
resource doc links version-aware (#21908)
e0f63be5a64d is described below
commit e0f63be5a64da3bc28750391b7525a57e1e75a62
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Mar 10 13:23:38 2026 +0100
CAMEL-23168 - Camel-Jbang-mcp: Make MCP exception catalog resource doc
links version-aware (#21908)
Signed-off-by: Andrea Cosentino <[email protected]>
---
.../dsl/jbang/core/commands/mcp/DiagnoseData.java | 58 ++++++++++++-
.../jbang/core/commands/mcp/DiagnoseResources.java | 66 +++++++++++++++
.../core/commands/mcp/DiagnoseResourcesTest.java | 99 ++++++++++++++++++++++
3 files changed, 221 insertions(+), 2 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseData.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseData.java
index 30265ce3575d..f65da48865db 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseData.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseData.java
@@ -16,6 +16,7 @@
*/
package org.apache.camel.dsl.jbang.core.commands.mcp;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
@@ -41,6 +42,41 @@ public class DiagnoseData {
public static final String CAMEL_MANUAL_DOC = CAMEL_DOC_BASE + "manual/";
public static final String CAMEL_EIP_DOC = CAMEL_COMPONENT_DOC + "eips/";
+ /**
+ * Component doc base URL for a specific Camel version.
+ *
+ * @param version the version segment (e.g., "4.18.x", "4.14.x"), or
null/"next" for the latest development docs
+ */
+ public static String componentDocBase(String version) {
+ String v = (version == null || version.isBlank() ||
"next".equals(version)) ? "next" : version;
+ return CAMEL_DOC_BASE + "components/" + v + "/";
+ }
+
+ /**
+ * EIP doc base URL for a specific Camel version.
+ */
+ public static String eipDocBase(String version) {
+ return componentDocBase(version) + "eips/";
+ }
+
+ /**
+ * Resolve documentation links for a specific version by replacing the
"next" version segment in component/EIP doc
+ * URLs with the specified version. Manual URLs (which have no version
segment) are left unchanged.
+ *
+ * @param links the original documentation links (using "next")
+ * @param version the target version (e.g., "4.18.x"), or null/"next" to
keep defaults
+ */
+ public static List<String> resolveDocLinks(List<String> links, String
version) {
+ if (version == null || version.isBlank() || "next".equals(version)) {
+ return links;
+ }
+ List<String> resolved = new ArrayList<>(links.size());
+ for (String link : links) {
+ resolved.add(link.replace("components/next/", "components/" +
version + "/"));
+ }
+ return resolved;
+ }
+
private static final Map<String, ExceptionInfo> KNOWN_EXCEPTIONS;
static {
@@ -379,11 +415,20 @@ public class DiagnoseData {
* Convert this exception info to a full JSON object with all fields.
*/
public JsonObject toJson() {
+ return toJson(null);
+ }
+
+ /**
+ * Convert this exception info to a full JSON object, resolving doc
links for the given version.
+ *
+ * @param version the Camel doc version (e.g., "4.18.x"), or null for
"next"
+ */
+ public JsonObject toJson(String version) {
JsonObject json = new JsonObject();
json.put("description", description);
json.put("commonCauses", toJsonArray(commonCauses));
json.put("suggestedFixes", toJsonArray(suggestedFixes));
- json.put("documentationLinks", toJsonArray(documentationLinks));
+ json.put("documentationLinks",
toJsonArray(resolveDocLinks(documentationLinks, version)));
return json;
}
@@ -391,9 +436,18 @@ public class DiagnoseData {
* Convert this exception info to a summary JSON object (description
and doc links only).
*/
public JsonObject toSummaryJson() {
+ return toSummaryJson(null);
+ }
+
+ /**
+ * Convert this exception info to a summary JSON object, resolving doc
links for the given version.
+ *
+ * @param version the Camel doc version (e.g., "4.18.x"), or null for
"next"
+ */
+ public JsonObject toSummaryJson(String version) {
JsonObject json = new JsonObject();
json.put("description", description);
- json.put("documentationLinks", toJsonArray(documentationLinks));
+ json.put("documentationLinks",
toJsonArray(resolveDocLinks(documentationLinks, version)));
return json;
}
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
index ebcd5794b357..f8650009c29d 100644
---
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
@@ -68,6 +68,37 @@ public class DiagnoseResources {
return new TextResourceContents("camel://error/exception-catalog",
result.toJson(), "application/json");
}
+ /**
+ * All known Camel exceptions with version-specific documentation links.
+ */
+ @ResourceTemplate(uriTemplate =
"camel://error/exception-catalog/{version}",
+ name = "camel_error_exception_catalog_versioned",
+ title = "Camel Exception Catalog (Versioned)",
+ description = "Registry of all known Camel exceptions
with documentation links resolved "
+ + "for a specific Camel version (e.g.,
'4.18.x', '4.14.x'). "
+ + "Use 'next' for the latest development
docs.",
+ mimeType = "application/json")
+ public TextResourceContents exceptionCatalogVersioned(
+ @ResourceTemplateArg(name = "version") String version) {
+
+ String uri = "camel://error/exception-catalog/" + version;
+
+ JsonObject result = new JsonObject();
+ result.put("version", version);
+
+ JsonArray exceptions = new JsonArray();
+ for (Map.Entry<String, DiagnoseData.ExceptionInfo> entry :
diagnoseData.getKnownExceptions().entrySet()) {
+ JsonObject exJson = entry.getValue().toSummaryJson(version);
+ exJson.put("name", entry.getKey());
+ exceptions.add(exJson);
+ }
+
+ result.put("exceptions", exceptions);
+ result.put("totalCount", exceptions.size());
+
+ return new TextResourceContents(uri, result.toJson(),
"application/json");
+ }
+
/**
* Detail for a specific Camel exception by name.
*/
@@ -98,4 +129,39 @@ public class DiagnoseResources {
return new TextResourceContents(uri, result.toJson(),
"application/json");
}
+
+ /**
+ * Detail for a specific Camel exception with version-specific
documentation links.
+ */
+ @ResourceTemplate(uriTemplate = "camel://error/exception/{name}/{version}",
+ name = "camel_error_exception_detail_versioned",
+ title = "Exception Detail (Versioned)",
+ description = "Full diagnostic detail for a specific
Camel exception with documentation links "
+ + "resolved for a specific Camel version
(e.g., '4.18.x', '4.14.x'). "
+ + "Use 'next' for the latest development
docs.",
+ mimeType = "application/json")
+ public TextResourceContents exceptionDetailVersioned(
+ @ResourceTemplateArg(name = "name") String name,
+ @ResourceTemplateArg(name = "version") String version) {
+
+ String uri = "camel://error/exception/" + name + "/" + version;
+
+ DiagnoseData.ExceptionInfo info = diagnoseData.getException(name);
+ if (info == null) {
+ JsonObject result = new JsonObject();
+ result.put("name", name);
+ result.put("version", version);
+ 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(version);
+ result.put("name", name);
+ result.put("version", version);
+ result.put("found", true);
+
+ return new TextResourceContents(uri, result.toJson(),
"application/json");
+ }
}
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
index 9a9575c1b753..b31259a71fc7 100644
---
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
@@ -132,4 +132,103 @@ class DiagnoseResourcesTest {
assertThat(result.getBoolean("found")).isFalse();
assertThat(result.getString("message")).contains("not in the known
exceptions catalog");
}
+
+ // ---- Versioned catalog ----
+
+ @Test
+ void versionedCatalogReturnsVersionSpecificDocLinks() throws Exception {
+ TextResourceContents contents =
resources.exceptionCatalogVersioned("4.18.x");
+
+
assertThat(contents.uri()).isEqualTo("camel://error/exception-catalog/4.18.x");
+ assertThat(contents.mimeType()).isEqualTo("application/json");
+
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+ assertThat(result.getString("version")).isEqualTo("4.18.x");
+ assertThat(result.getInteger("totalCount")).isGreaterThan(0);
+
+ // Verify doc links use 4.18.x instead of next
+ JsonArray exceptions = result.getCollection("exceptions");
+ for (Object obj : exceptions) {
+ JsonObject entry = (JsonObject) obj;
+ JsonArray docs = entry.getCollection("documentationLinks");
+ for (Object doc : docs) {
+ String link = (String) doc;
+ if (link.contains("/components/")) {
+ assertThat(link).contains("components/4.18.x/");
+ assertThat(link).doesNotContain("components/next/");
+ }
+ }
+ }
+ }
+
+ @Test
+ void versionedCatalogWithNextReturnsDefaultLinks() throws Exception {
+ TextResourceContents contents =
resources.exceptionCatalogVersioned("next");
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+
+ JsonArray exceptions = result.getCollection("exceptions");
+ for (Object obj : exceptions) {
+ JsonObject entry = (JsonObject) obj;
+ JsonArray docs = entry.getCollection("documentationLinks");
+ for (Object doc : docs) {
+ String link = (String) doc;
+ if (link.contains("/components/")) {
+ assertThat(link).contains("components/next/");
+ }
+ }
+ }
+ }
+
+ // ---- Versioned detail ----
+
+ @Test
+ void versionedDetailReturnsVersionSpecificDocLinks() throws Exception {
+ TextResourceContents contents =
resources.exceptionDetailVersioned("DirectConsumerNotAvailableException",
"4.14.x");
+
+
assertThat(contents.uri()).isEqualTo("camel://error/exception/DirectConsumerNotAvailableException/4.14.x");
+
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+ assertThat(result.getBoolean("found")).isTrue();
+ assertThat(result.getString("version")).isEqualTo("4.14.x");
+
+ // DirectConsumerNotAvailableException has component doc links
(direct-component.html, seda-component.html)
+ JsonArray docs = result.getCollection("documentationLinks");
+ for (Object doc : docs) {
+ String link = (String) doc;
+ if (link.contains("/components/")) {
+ assertThat(link).contains("components/4.14.x/");
+ assertThat(link).doesNotContain("components/next/");
+ }
+ }
+ }
+
+ @Test
+ void versionedDetailNotFoundIncludesVersion() throws Exception {
+ TextResourceContents contents =
resources.exceptionDetailVersioned("NonExistentException", "4.18.x");
+
+
assertThat(contents.uri()).isEqualTo("camel://error/exception/NonExistentException/4.18.x");
+
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+ assertThat(result.getBoolean("found")).isFalse();
+ assertThat(result.getString("version")).isEqualTo("4.18.x");
+ }
+
+ @Test
+ void versionedDetailPreservesManualLinks() throws Exception {
+ // CamelExecutionException has manual doc links
(manual/exception-clause.html, etc.)
+ TextResourceContents contents =
resources.exceptionDetailVersioned("CamelExecutionException", "4.18.x");
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+
+ JsonArray docs = result.getCollection("documentationLinks");
+ boolean hasManualLink = false;
+ for (Object doc : docs) {
+ String link = (String) doc;
+ if (link.contains("/manual/")) {
+ hasManualLink = true;
+ // Manual links should NOT have a version segment inserted
+ assertThat(link).doesNotContain("components/");
+ }
+ }
+ assertThat(hasManualLink).as("CamelExecutionException should have at
least one manual doc link").isTrue();
+ }
}