This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch feature/CAMEL-23793-mcp-component-headers in repository https://gitbox.apache.org/repos/asf/camel.git
commit 391f85d390f07d14b3be83d762733bd2c25e7a28 Author: Claus Ibsen <[email protected]> AuthorDate: Thu Jun 18 14:31:11 2026 +0200 CAMEL-23793: Expose component headers from catalog in MCP server Add an optional includeHeaders parameter to the camel_catalog_component_doc MCP tool. When set to true, the response includes message header metadata (CamelXxx names, Java constants, types, and consumer/producer group) from the component catalog. Headers are excluded by default to keep responses lean when agents only need options. Co-Authored-By: Claude <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../dsl/jbang/core/commands/mcp/CatalogTools.java | 28 ++++++++++++-- .../jbang/core/commands/mcp/CatalogToolsTest.java | 44 +++++++++++++++++++--- .../commands/mcp/McpJsonSerializationTest.java | 3 +- 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogTools.java index c7773de51fc9..fd0c79aa95cb 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogTools.java @@ -93,6 +93,7 @@ public class CatalogTools { @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false), description = "Get documentation for a Camel component: URI syntax and endpoint options. " + "Default returns common options only (excludes deprecated and advanced). " + + "Set includeHeaders=true to also return message header metadata (CamelXxx names, constants, types). " + "Use camel_catalog_component_maven for groupId/artifactId/version.") public ComponentDetailResult camel_catalog_component_doc( @ToolArg(description = "Component name (e.g., kafka, http, file, timer)") String component, @@ -101,6 +102,9 @@ public class CatalogTools { @ToolArg(description = "Which options to include: required | common | all (default: common). " + "'common' excludes deprecated and advanced options.", required = false) String includeOptions, + @ToolArg(description = "Whether to include message headers in the response (default: false). " + + "Headers show the CamelXxx header names, their Java constants, types, and consumer/producer group.", + required = false) Boolean includeHeaders, @ToolArg(description = ToolArgDocs.RUNTIME) String runtime, @ToolArg(description = ToolArgDocs.VERSION_QUERY) String camelVersion, @ToolArg(description = ToolArgDocs.PLATFORM_BOM) String platformBom) { @@ -143,7 +147,7 @@ public class CatalogTools { throw new ToolCallException(hint.toString(), null); } - return toComponentDetailResult(model, scope, optionsFilter); + return toComponentDetailResult(model, scope, optionsFilter, Boolean.TRUE.equals(includeHeaders)); } catch (ToolCallException e) { throw e; } catch (Throwable e) { @@ -422,7 +426,8 @@ public class CatalogTools { model.getSupportLevel() != null ? model.getSupportLevel().name() : null); } - private ComponentDetailResult toComponentDetailResult(ComponentModel model, OptionScope scope, String optionsFilter) { + private ComponentDetailResult toComponentDetailResult( + ComponentModel model, OptionScope scope, String optionsFilter, boolean includeHeaders) { Predicate<BaseOptionModel> filter = scope.asPredicate().and(nameFilter(optionsFilter)); List<OptionInfo> componentOptions = new ArrayList<>(); @@ -451,6 +456,15 @@ public class CatalogTools { opt.getGroup()))); } + List<HeaderInfo> headers = List.of(); + if (includeHeaders && model.getEndpointHeaders() != null) { + headers = model.getEndpointHeaders().stream() + .map(h -> new HeaderInfo( + h.getName(), h.getDescription(), h.getJavaType(), + h.getGroup(), h.getConstantName(), h.isRequired())) + .collect(Collectors.toList()); + } + return new ComponentDetailResult( model.getScheme(), model.getTitle(), @@ -463,7 +477,8 @@ public class CatalogTools { model.isConsumerOnly(), model.isProducerOnly(), componentOptions, - endpointOptions); + endpointOptions, + headers); } private static Predicate<BaseOptionModel> nameFilter(String optionsFilter) { @@ -640,7 +655,8 @@ public class CatalogTools { public record ComponentDetailResult(String name, String title, String description, String label, boolean deprecated, String supportLevel, String syntax, boolean async, boolean consumerOnly, boolean producerOnly, - List<OptionInfo> componentOptions, List<OptionInfo> endpointOptions) { + List<OptionInfo> componentOptions, List<OptionInfo> endpointOptions, + List<HeaderInfo> headers) { } public record ComponentMavenResult(String name, String groupId, String artifactId, String version) { @@ -650,6 +666,10 @@ public class CatalogTools { String defaultValue, String group) { } + public record HeaderInfo(String name, String description, String javaType, + String group, String constantName, boolean required) { + } + public record DataFormatListResult(int count, List<DataFormatInfo> dataFormats) { } diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogToolsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogToolsTest.java index b9c53ce47102..0ef1dbd0ac8f 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogToolsTest.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogToolsTest.java @@ -97,7 +97,7 @@ class CatalogToolsTest { CatalogTools tools = createTools("https://maven.repository.redhat.com/ga/"); CatalogTools.ComponentDetailResult result - = tools.camel_catalog_component_doc("timer", null, null, null, null, null); + = tools.camel_catalog_component_doc("timer", null, null, null, null, null, null); assertThat(result).isNotNull(); assertThat(result.name()).isEqualTo("timer"); @@ -108,9 +108,9 @@ class CatalogToolsTest { CatalogTools tools = createTools(null); CatalogTools.ComponentDetailResult defaultResult - = tools.camel_catalog_component_doc("kafka", null, null, null, null, null); + = tools.camel_catalog_component_doc("kafka", null, null, null, null, null, null); CatalogTools.ComponentDetailResult allResult - = tools.camel_catalog_component_doc("kafka", null, "all", null, null, null); + = tools.camel_catalog_component_doc("kafka", null, "all", null, null, null, null); assertThat(defaultResult.endpointOptions()).isNotEmpty(); assertThat(allResult.endpointOptions()).isNotEmpty(); @@ -130,7 +130,7 @@ class CatalogToolsTest { CatalogTools tools = createTools(null); CatalogTools.ComponentDetailResult result - = tools.camel_catalog_component_doc("kafka", null, "required", null, null, null); + = tools.camel_catalog_component_doc("kafka", null, "required", null, null, null, null); assertThat(result.endpointOptions()).allMatch(CatalogTools.OptionInfo::required); } @@ -140,7 +140,7 @@ class CatalogToolsTest { CatalogTools tools = createTools(null); CatalogTools.ComponentDetailResult result - = tools.camel_catalog_component_doc("kafka", "topic", "all", null, null, null); + = tools.camel_catalog_component_doc("kafka", "topic", "all", null, null, null, null); assertThat(result.endpointOptions()).isNotEmpty(); assertThat(result.endpointOptions()) @@ -151,11 +151,43 @@ class CatalogToolsTest { void componentDocInvalidIncludeOptionsThrows() { CatalogTools tools = createTools(null); - assertThatThrownBy(() -> tools.camel_catalog_component_doc("timer", null, "bogus", null, null, null)) + assertThatThrownBy(() -> tools.camel_catalog_component_doc("timer", null, "bogus", null, null, null, null)) .isInstanceOf(ToolCallException.class) .hasMessageContaining("Invalid includeOptions"); } + @Test + void componentDocDefaultExcludesHeaders() { + CatalogTools tools = createTools(null); + + CatalogTools.ComponentDetailResult result + = tools.camel_catalog_component_doc("kafka", null, null, null, null, null, null); + + assertThat(result.headers()).isEmpty(); + } + + @Test + void componentDocIncludeHeadersReturnsHeaders() { + CatalogTools tools = createTools(null); + + CatalogTools.ComponentDetailResult result + = tools.camel_catalog_component_doc("kafka", null, null, true, null, null, null); + + assertThat(result.headers()).isNotEmpty(); + assertThat(result.headers()) + .anyMatch(h -> "CamelKafkaKey".equals(h.name()) && h.constantName() != null); + } + + @Test + void componentDocIncludeHeadersFalseExcludesHeaders() { + CatalogTools tools = createTools(null); + + CatalogTools.ComponentDetailResult result + = tools.camel_catalog_component_doc("kafka", null, null, false, null, null, null); + + assertThat(result.headers()).isEmpty(); + } + @Test void componentMavenReturnsCoordinates() { CatalogTools tools = createTools(null); diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpJsonSerializationTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpJsonSerializationTest.java index e312f394e5e8..40f964c7f4e7 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpJsonSerializationTest.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpJsonSerializationTest.java @@ -85,7 +85,7 @@ class McpJsonSerializationTest { CatalogTools.ComponentDetailResult detail = new CatalogTools.ComponentDetailResult( "timer", "Timer", null, null, false, null, null, - false, false, false, null, null); + false, false, false, null, null, null); String json = mapper.writeValueAsString(detail); @@ -96,6 +96,7 @@ class McpJsonSerializationTest { assertThat(json).doesNotContain("\"groupId\""); assertThat(json).doesNotContain("\"componentOptions\""); assertThat(json).doesNotContain("\"endpointOptions\""); + assertThat(json).doesNotContain("\"headers\""); } @Test
