This is an automated email from the ASF dual-hosted git repository. acosentino pushed a commit to branch 23168-comp in repository https://gitbox.apache.org/repos/asf/camel.git
commit 5b11633ba6105854f4792997366bbeb82fca0958 Author: Andrea Cosentino <[email protected]> AuthorDate: Tue Mar 10 12:48:37 2026 +0100 CAMEL-23168 - Camel-Jbang-mcp: Make MCP exception catalog resource doc links version-aware 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(); + } }
