This is an automated email from the ASF dual-hosted git repository.
oscerd 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 7047210cdcf4 CAMEL-23475: camel-jbang-mcp - make ComponentDetailResult
lean by default with option filtering (#23359)
7047210cdcf4 is described below
commit 7047210cdcf403bb4cbf907b116af2b99faf220b
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed May 20 12:59:45 2026 +0200
CAMEL-23475: camel-jbang-mcp - make ComponentDetailResult lean by default
with option filtering (#23359)
Add option filtering to camel_catalog_component_doc and split out a
small camel_catalog_component_maven tool so callers only pay the
maven-coordinate token cost when they actually need it.
* optionsFilter: case-insensitive substring match on option name.
* includeOptions: required | common | all (default: common, which
excludes deprecated and advanced options).
* ComponentDetailResult no longer carries groupId/artifactId/version;
use camel_catalog_component_maven for those.
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
---
.../ROOT/pages/camel-4x-upgrade-guide-4_21.adoc | 14 +++
.../modules/ROOT/pages/camel-jbang-mcp.adoc | 11 +-
.../dsl/jbang/core/commands/mcp/CatalogTools.java | 140 +++++++++++++++++----
.../jbang/core/commands/mcp/CatalogToolsTest.java | 95 ++++++++++++--
.../commands/mcp/McpJsonSerializationTest.java | 17 ++-
5 files changed, 241 insertions(+), 36 deletions(-)
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
index 9830b267a59f..7327faf48188 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
@@ -63,6 +63,20 @@ auto-disables `contentCache` on resource-based components
(such as `xslt`) whose
the route. Set `camel.component.<name>.contentCache=true` (or pass
`?contentCache=true` on the
URI) to opt back in to caching during dev mode.
+==== camel-jbang-mcp
+
+The `camel_catalog_component_doc` tool no longer returns every component
option by default. Two new
+arguments make the response lean by default to reduce LLM context-window
pressure:
+
+* `optionsFilter` (optional) — case-insensitive substring match on option name.
+* `includeOptions` (optional) — one of `required`, `common`, or `all`;
defaults to `common`, which
+ excludes deprecated options and options whose label contains `advanced`.
Pass `all` to restore the
+ previous behaviour.
+
+The `groupId`, `artifactId`, and `version` fields have been removed from the
`camel_catalog_component_doc`
+response. A new `camel_catalog_component_maven` tool returns just those Maven
coordinates and should be
+called only when you actually need to add the component as a dependency.
+
==== camel-jbang plugins
Plugins are now loaded lazily. Built-in commands that do not consume plugins
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index 8103d14cbc62..a65c413b0d96 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -34,7 +34,7 @@ over the MCP protocol.
== Available Tools
-The server exposes 27 tools organized into eleven functional areas, plus 3
prompts that provide structured
+The server exposes 28 tools organized into eleven functional areas, plus 3
prompts that provide structured
multi-step workflows.
=== Catalog Exploration
@@ -48,8 +48,13 @@ multi-step workflows.
runtime type (`main`, `spring-boot`, `quarkus`). Supports querying specific
Camel versions.
| `camel_catalog_component_doc`
-| Get detailed documentation for a specific component including all endpoint
options, component-level options,
- Maven coordinates, and URI syntax.
+| Get documentation for a specific component: URI syntax, component-level and
endpoint options. Supports
+ `optionsFilter` (case-insensitive substring on option name) and
`includeOptions` (`required`, `common`, or
+ `all`; default `common`, which excludes deprecated and advanced options) to
control payload size.
+
+| `camel_catalog_component_maven`
+| Get the Maven coordinates (`groupId`, `artifactId`, `version`) of a specific
component, for adding it as a
+ project dependency.
| `camel_catalog_dataformats`
| List available data formats (JSON, XML, CSV, Avro, Protobuf, and others).
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 ea3bc1a8885a..3242c1beabe2 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
@@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.mcp;
import java.util.ArrayList;
import java.util.List;
+import java.util.function.Predicate;
import java.util.stream.Collectors;
import jakarta.enterprise.context.ApplicationScoped;
@@ -27,6 +28,7 @@ import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.ToolArg;
import io.quarkiverse.mcp.server.ToolCallException;
import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.tooling.model.BaseOptionModel;
import org.apache.camel.tooling.model.ComponentModel;
import org.apache.camel.tooling.model.DataFormatModel;
import org.apache.camel.tooling.model.EipModel;
@@ -88,10 +90,16 @@ public class CatalogTools {
* Tool to get detailed documentation for a specific component.
*/
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
- description = "Get detailed documentation for a Camel component
including all options, " +
- "endpoint parameters, and usage examples.")
+ description = "Get documentation for a Camel component: URI syntax
and endpoint options. "
+ + "Default returns common options only (excludes
deprecated and advanced). "
+ + "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,
+ @ToolArg(description = "Filter options by name (case-insensitive
substring match)",
+ required = false) String optionsFilter,
+ @ToolArg(description = "Which options to include: required |
common | all (default: common). "
+ + "'common' excludes deprecated and
advanced options.",
+ required = false) String includeOptions,
@ToolArg(description = ToolArgDocs.RUNTIME) String runtime,
@ToolArg(description = ToolArgDocs.VERSION_QUERY) String
camelVersion,
@ToolArg(description = ToolArgDocs.PLATFORM_BOM) String
platformBom) {
@@ -100,6 +108,8 @@ public class CatalogTools {
throw new ToolCallException("Component name is required", null);
}
+ OptionScope scope = OptionScope.parse(includeOptions);
+
try {
CamelCatalog cat = catalogService.loadCatalog(runtime,
camelVersion, platformBom);
ComponentModel model = cat.componentModel(component);
@@ -132,7 +142,42 @@ public class CatalogTools {
throw new ToolCallException(hint.toString(), null);
}
- return toComponentDetailResult(model);
+ return toComponentDetailResult(model, scope, optionsFilter);
+ } catch (ToolCallException e) {
+ throw e;
+ } catch (Throwable e) {
+ throw new ToolCallException(
+ "Component not found: " + component + " (" +
e.getClass().getName() + "): " + e.getMessage(), null);
+ }
+ }
+
+ /**
+ * Tool to get the Maven coordinates (groupId, artifactId, version) for a
specific component.
+ */
+ @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
+ description = "Get Maven coordinates (groupId, artifactId, version)
for a Camel component, "
+ + "for adding it as a dependency.")
+ public ComponentMavenResult camel_catalog_component_maven(
+ @ToolArg(description = "Component name (e.g., kafka, http, file,
timer)") String component,
+ @ToolArg(description = ToolArgDocs.RUNTIME) String runtime,
+ @ToolArg(description = ToolArgDocs.VERSION_QUERY) String
camelVersion,
+ @ToolArg(description = ToolArgDocs.PLATFORM_BOM) String
platformBom) {
+
+ if (component == null || component.isBlank()) {
+ throw new ToolCallException("Component name is required", null);
+ }
+
+ try {
+ CamelCatalog cat = catalogService.loadCatalog(runtime,
camelVersion, platformBom);
+ ComponentModel model = cat.componentModel(component);
+ if (model == null) {
+ throw new ToolCallException("Component not found: " +
component, null);
+ }
+ return new ComponentMavenResult(
+ model.getScheme(),
+ model.getGroupId(),
+ model.getArtifactId(),
+ model.getVersion());
} catch (ToolCallException e) {
throw e;
} catch (Throwable e) {
@@ -377,27 +422,33 @@ public class CatalogTools {
model.getSupportLevel() != null ?
model.getSupportLevel().name() : null);
}
- private ComponentDetailResult toComponentDetailResult(ComponentModel
model) {
+ private ComponentDetailResult toComponentDetailResult(ComponentModel
model, OptionScope scope, String optionsFilter) {
+ Predicate<BaseOptionModel> filter =
scope.asPredicate().and(nameFilter(optionsFilter));
+
List<OptionInfo> componentOptions = new ArrayList<>();
if (model.getComponentOptions() != null) {
- model.getComponentOptions().forEach(opt ->
componentOptions.add(new OptionInfo(
- opt.getName(),
- opt.getDescription(),
- opt.getType(),
- opt.isRequired(),
- opt.getDefaultValue() != null ?
opt.getDefaultValue().toString() : null,
- null)));
+ model.getComponentOptions().stream()
+ .filter(filter)
+ .forEach(opt -> componentOptions.add(new OptionInfo(
+ opt.getName(),
+ opt.getDescription(),
+ opt.getType(),
+ opt.isRequired(),
+ opt.getDefaultValue() != null ?
opt.getDefaultValue().toString() : null,
+ null)));
}
List<OptionInfo> endpointOptions = new ArrayList<>();
if (model.getEndpointOptions() != null) {
- model.getEndpointOptions().forEach(opt -> endpointOptions.add(new
OptionInfo(
- opt.getName(),
- opt.getDescription(),
- opt.getType(),
- opt.isRequired(),
- opt.getDefaultValue() != null ?
opt.getDefaultValue().toString() : null,
- opt.getGroup())));
+ model.getEndpointOptions().stream()
+ .filter(filter)
+ .forEach(opt -> endpointOptions.add(new OptionInfo(
+ opt.getName(),
+ opt.getDescription(),
+ opt.getType(),
+ opt.isRequired(),
+ opt.getDefaultValue() != null ?
opt.getDefaultValue().toString() : null,
+ opt.getGroup())));
}
return new ComponentDetailResult(
@@ -407,9 +458,6 @@ public class CatalogTools {
model.getLabel(),
model.isDeprecated(),
model.getSupportLevel() != null ?
model.getSupportLevel().name() : null,
- model.getGroupId(),
- model.getArtifactId(),
- model.getVersion(),
model.getSyntax(),
model.isAsync(),
model.isConsumerOnly(),
@@ -418,6 +466,49 @@ public class CatalogTools {
endpointOptions);
}
+ private static Predicate<BaseOptionModel> nameFilter(String optionsFilter)
{
+ if (optionsFilter == null || optionsFilter.isBlank()) {
+ return opt -> true;
+ }
+ String lower = optionsFilter.toLowerCase();
+ return opt -> opt.getName() != null &&
opt.getName().toLowerCase().contains(lower);
+ }
+
+ private static boolean isAdvanced(BaseOptionModel opt) {
+ String label = opt.getLabel();
+ return label != null && label.contains("advanced");
+ }
+
+ /**
+ * Subset of options to include in {@code camel_catalog_component_doc}
responses.
+ */
+ private enum OptionScope {
+ REQUIRED,
+ COMMON,
+ ALL;
+
+ Predicate<BaseOptionModel> asPredicate() {
+ return switch (this) {
+ case REQUIRED -> BaseOptionModel::isRequired;
+ case COMMON -> opt -> !opt.isDeprecated() && !isAdvanced(opt);
+ case ALL -> opt -> true;
+ };
+ }
+
+ static OptionScope parse(String value) {
+ if (value == null || value.isBlank()) {
+ return COMMON;
+ }
+ return switch (value.trim().toLowerCase()) {
+ case "required" -> REQUIRED;
+ case "common" -> COMMON;
+ case "all" -> ALL;
+ default -> throw new ToolCallException(
+ "Invalid includeOptions value: '" + value + "'.
Expected one of: required, common, all.", null);
+ };
+ }
+ }
+
private DataFormatInfo toDataFormatInfo(DataFormatModel model) {
return new DataFormatInfo(
model.getName(),
@@ -523,11 +614,14 @@ public class CatalogTools {
}
public record ComponentDetailResult(String name, String title, String
description, String label,
- boolean deprecated, String supportLevel, String groupId, String
artifactId,
- String version, String syntax, boolean async, boolean
consumerOnly, boolean producerOnly,
+ boolean deprecated, String supportLevel, String syntax,
+ boolean async, boolean consumerOnly, boolean producerOnly,
List<OptionInfo> componentOptions, List<OptionInfo>
endpointOptions) {
}
+ public record ComponentMavenResult(String name, String groupId, String
artifactId, String version) {
+ }
+
public record OptionInfo(String name, String description, String type,
boolean required,
String defaultValue, String group) {
}
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 1face11c0145..4f2c3c3a19fd 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
@@ -84,12 +84,89 @@ class CatalogToolsTest {
void componentDocWithRepos() {
CatalogTools tools =
createTools("https://maven.repository.redhat.com/ga/");
- CatalogTools.ComponentDetailResult result =
tools.camel_catalog_component_doc("timer", null, null, null);
+ CatalogTools.ComponentDetailResult result
+ = tools.camel_catalog_component_doc("timer", null, null, null,
null, null);
assertThat(result).isNotNull();
assertThat(result.name()).isEqualTo("timer");
}
+ @Test
+ void componentDocDefaultsToCommonScope() {
+ CatalogTools tools = createTools(null);
+
+ CatalogTools.ComponentDetailResult defaultResult
+ = tools.camel_catalog_component_doc("kafka", null, null, null,
null, null);
+ CatalogTools.ComponentDetailResult allResult
+ = tools.camel_catalog_component_doc("kafka", null, "all",
null, null, null);
+
+ assertThat(defaultResult.endpointOptions()).isNotEmpty();
+ assertThat(allResult.endpointOptions()).isNotEmpty();
+ assertThat(defaultResult.endpointOptions().size())
+ .as("default 'common' scope must filter out
advanced/deprecated options")
+ .isLessThan(allResult.endpointOptions().size());
+
+ // 'common' must not include any deprecated option (advanced options
are not exposed in the
+ // returned OptionInfo, but deprecated is hidden internally via the
label/deprecated flags)
+ assertThat(defaultResult.endpointOptions())
+ .as("default 'common' scope must not contain options whose
name starts with 'synchronous'")
+ .noneMatch(o -> "synchronous".equals(o.name()));
+ }
+
+ @Test
+ void componentDocRequiredScopeOnlyReturnsRequiredOptions() {
+ CatalogTools tools = createTools(null);
+
+ CatalogTools.ComponentDetailResult result
+ = tools.camel_catalog_component_doc("kafka", null, "required",
null, null, null);
+
+
assertThat(result.endpointOptions()).allMatch(CatalogTools.OptionInfo::required);
+ }
+
+ @Test
+ void componentDocOptionsFilterByName() {
+ CatalogTools tools = createTools(null);
+
+ CatalogTools.ComponentDetailResult result
+ = tools.camel_catalog_component_doc("kafka", "topic", "all",
null, null, null);
+
+ assertThat(result.endpointOptions()).isNotEmpty();
+ assertThat(result.endpointOptions())
+ .allMatch(o -> o.name().toLowerCase().contains("topic"));
+ }
+
+ @Test
+ void componentDocInvalidIncludeOptionsThrows() {
+ CatalogTools tools = createTools(null);
+
+ assertThatThrownBy(() -> tools.camel_catalog_component_doc("timer",
null, "bogus", null, null, null))
+ .isInstanceOf(ToolCallException.class)
+ .hasMessageContaining("Invalid includeOptions");
+ }
+
+ @Test
+ void componentMavenReturnsCoordinates() {
+ CatalogTools tools = createTools(null);
+
+ CatalogTools.ComponentMavenResult result
+ = tools.camel_catalog_component_maven("timer", null, null,
null);
+
+ assertThat(result).isNotNull();
+ assertThat(result.name()).isEqualTo("timer");
+ assertThat(result.groupId()).isEqualTo("org.apache.camel");
+ assertThat(result.artifactId()).isEqualTo("camel-timer");
+ assertThat(result.version()).isNotBlank();
+ }
+
+ @Test
+ void componentMavenUnknownComponentThrows() {
+ CatalogTools tools = createTools(null);
+
+ assertThatThrownBy(() ->
tools.camel_catalog_component_maven("does-not-exist", null, null, null))
+ .isInstanceOf(ToolCallException.class)
+ .hasMessageContaining("Component not found");
+ }
+
// platformBom validation tests
@Test
@@ -206,11 +283,11 @@ class CatalogToolsTest {
assertThat(listResult.camelVersion()).isEqualTo(requestedVersion);
- CatalogTools.ComponentDetailResult docResult
- = tools.camel_catalog_component_doc("timer", "main",
requestedVersion, null);
+ CatalogTools.ComponentMavenResult mavenResult
+ = tools.camel_catalog_component_maven("timer", "main",
requestedVersion, null);
- assertThat(docResult.version()).isEqualTo(requestedVersion);
- assertThat(docResult.version()).isNotEqualTo(BUILTIN_VERSION);
+ assertThat(mavenResult.version()).isEqualTo(requestedVersion);
+ assertThat(mavenResult.version()).isNotEqualTo(BUILTIN_VERSION);
}
@Test
@@ -226,10 +303,10 @@ class CatalogToolsTest {
assertThat(listResult.camelVersion()).isEqualTo(requestedVersion);
- CatalogTools.ComponentDetailResult docResult
- = tools.camel_catalog_component_doc("timer", "main", null,
bom);
+ CatalogTools.ComponentMavenResult mavenResult
+ = tools.camel_catalog_component_maven("timer", "main", null,
bom);
- assertThat(docResult.version()).isEqualTo(requestedVersion);
- assertThat(docResult.version()).isNotEqualTo(BUILTIN_VERSION);
+ assertThat(mavenResult.version()).isEqualTo(requestedVersion);
+ assertThat(mavenResult.version()).isNotEqualTo(BUILTIN_VERSION);
}
}
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 066504a2ca2c..196b5edfb8b6 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
@@ -82,7 +82,7 @@ class McpJsonSerializationTest {
ObjectMapper mapper = newConfiguredObjectMapper();
CatalogTools.ComponentDetailResult detail = new
CatalogTools.ComponentDetailResult(
- "timer", "Timer", null, null, false, null, null, null, null,
null,
+ "timer", "Timer", null, null, false, null, null,
false, false, false, null, null);
String json = mapper.writeValueAsString(detail);
@@ -96,6 +96,21 @@ class McpJsonSerializationTest {
assertThat(json).doesNotContain("\"endpointOptions\"");
}
+ @Test
+ void componentMavenResultSerializesNonNullFields() throws Exception {
+ ObjectMapper mapper = newConfiguredObjectMapper();
+
+ CatalogTools.ComponentMavenResult maven = new
CatalogTools.ComponentMavenResult(
+ "kafka", "org.apache.camel", "camel-kafka", "4.21.0");
+
+ String json = mapper.writeValueAsString(maven);
+
+ assertThat(json).contains("\"name\":\"kafka\"");
+ assertThat(json).contains("\"groupId\":\"org.apache.camel\"");
+ assertThat(json).contains("\"artifactId\":\"camel-kafka\"");
+ assertThat(json).contains("\"version\":\"4.21.0\"");
+ }
+
private static ObjectMapper newConfiguredObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
// Mirrors the configuration applied by Quarkus'
ConfigurationCustomizer when