This is an automated email from the ASF dual-hosted git repository.
jamesnetherton pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
The following commit(s) were added to refs/heads/main by this push:
new c4eb7f6e64 Quarkus Agent MCP support: built-in skills and Dev MCP tools
c4eb7f6e64 is described below
commit c4eb7f6e6403a3a020d039c22fa14edfdb176cd2
Author: Zineb BENDHIBA <[email protected]>
AuthorDate: Wed Oct 21 09:42:51 2026 +0200
Quarkus Agent MCP support: built-in skills and Dev MCP tools
Fixes #8706.
* Quarkus Agent MCP support: built-in skills and Dev MCP tools
Ship META-INF/quarkus-skill.md in the camel-quarkus-core and
camel-quarkus-bean deployment JARs. The Quarkus Agent MCP quarkus_skills
tool discovers them automatically and loads prescriptive Camel Quarkus
guidance into AI assistants before they write code.
Expose camelCatalogDoc and camelValidateEndpointUri as Dev MCP tools
from the core Dev UI JSON-RPC service, backed by the application's
RuntimeCamelCatalog. The Quarkus Agent MCP proxies them in dev mode,
giving agents live component documentation and endpoint URI validation
with no extra MCP server to install.
Also adds an AI-assisted development page to the user guide.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Address review comments on Quarkus Agent MCP support
- Delegate endpoint URI validation to
AbstractCamelCatalog.validateEndpointProperties,
honouring lenient components and URI normalization
- Replace the camel-quarkus-<scheme> dependency hint with a generic Quarkus
platform
catalog nudge, in both catalog tools
- Filter apiProperties/apis options for API-based components (box, twilio,
...)
- Extract a shared getContextPlugin(Class<T>) helper for the catalog and
console registry
- Skills: correct @Consume (method) vs @Produce/@EndpointInject (fields),
YAML route
auto-discovery and default routes-include-pattern, @CamelQuarkusTest
testing, plus
Dev Services and backend smoke-test guidance
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Harden camelValidateEndpointUri against malformed and schemeless URIs
Wrap the catalog validation call in a try/catch so an unexpected
RuntimeException returns a structured {"valid": false, "errors": ...}
response instead of failing the JSON-RPC call. Report a schemeless URI
as malformed rather than as an unknown component named after the whole
URI. Add a test covering the schemeless case.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* Align AI-assisted development doc testing guidance with the core skill
The user-guide page recommended CamelQuarkusTestSupport while the core
quarkus-skill.md now leads with @CamelQuarkusTest. Update the page to match
the skill and the canonical testing guide.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
---------
Co-authored-by: Claude Fable 5 <[email protected]>
---
docs/modules/ROOT/nav.adoc | 1 +
.../ROOT/pages/user-guide/quarkus-agent-mcp.adoc | 69 ++++++
.../src/main/resources/META-INF/quarkus-skill.md | 89 +++++++
.../devui/CamelQuarkusCoreDevUITest.java | 75 ++++++
.../camel/quarkus/devui/CamelCoreDevUIService.java | 261 +++++++++++++++++++--
.../src/main/resources/META-INF/quarkus-skill.md | 45 ++++
6 files changed, 525 insertions(+), 15 deletions(-)
diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc
index 648e78b823..6cfbb1bae3 100644
--- a/docs/modules/ROOT/nav.adoc
+++ b/docs/modules/ROOT/nav.adoc
@@ -12,6 +12,7 @@
*** xref:user-guide/testing/external-services.adoc[External services]
*** xref:user-guide/testing/camel-quarkus-test.adoc[Annotation based testing]
*** xref:user-guide/testing/camel-quarkus-test-support.adoc[CamelTestSupport
style]
+** xref:user-guide/quarkus-agent-mcp.adoc[AI-assisted development]
** xref:user-guide/security-model.adoc[Security model]
** xref:user-guide/examples.adoc[Examples]
** xref:user-guide/kubernetes.adoc[Kubernetes]
diff --git a/docs/modules/ROOT/pages/user-guide/quarkus-agent-mcp.adoc
b/docs/modules/ROOT/pages/user-guide/quarkus-agent-mcp.adoc
new file mode 100644
index 0000000000..e2dfbaa622
--- /dev/null
+++ b/docs/modules/ROOT/pages/user-guide/quarkus-agent-mcp.adoc
@@ -0,0 +1,69 @@
+= AI-assisted Development
+
+Quarkus ships a https://quarkus.io/blog/introducing-agent-mcp[Quarkus Agent
MCP server]
+that exposes extension skills, documentation search and Dev UI tooling to AI
coding assistants via the
+https://modelcontextprotocol.io[Model Context Protocol] (MCP).
+
+Camel Quarkus plugs into it with a built-in skill. One MCP server, nothing
else to install.
+
+== Built-in Camel Quarkus skills
+
+Camel Quarkus extensions can ship a skill inside their deployment JAR
+(`META-INF/quarkus-skill.md`). The `quarkus_skills` MCP tool discovers each
skill
+automatically in any project depending on that extension. No additional setup
is required.
+
+Currently, `camel-quarkus-core` ships the main Camel Quarkus skill and
`camel-quarkus-bean`
+ships guidance for calling CDI beans from routes.
+
+The core skill teaches the AI agent to:
+
+* Prefer the type-safe Endpoint DSL (`EndpointRouteBuilder`) for defining
routes, so that
+ endpoint options are checked at compile time
+* Look up endpoint options in the Camel catalog metadata embedded in each
component JAR,
+ or in the versioned component documentation, instead of guessing URI
parameters
+* Validate routes with the
+
https://camel.apache.org/manual/camel-report-maven-plugin.html[`camel-report-maven-plugin:validate`]
+ goal before delivering them
+* Follow CDI injection patterns for `CamelContext`, `ProducerTemplate`, and
named beans
+* Prefer `@CamelQuarkusTest` for JVM-mode tests (plain `@QuarkusTest` when no
Camel-specific
+ features are needed), never extend `CamelTestSupport`, and avoid `@Inject` in
+ `@QuarkusIntegrationTest` native tests
+* Apply `@RegisterForReflection` on classes accessed via reflection in native
mode
+
+== Dev MCP tools
+
+When the application runs in dev mode, the `camel-quarkus-core` extension also
exposes
+https://quarkus.io/guides/dev-mcp[Dev MCP] tools, proxied by the Quarkus Agent
MCP server:
+
+* `camelCatalogDoc` — Camel catalog documentation of a component (endpoint URI
syntax and
+ options), from the exact Camel version of the application, with optional
keyword filtering
+* `camelValidateEndpointUri` — validates an endpoint URI against the
application's catalog:
+ unknown options (with suggestions), invalid values, missing required options
+
+The built-in skill instructs agents to prefer these tools over static lookups
whenever
+dev mode is running.
+
+No setup is needed when the tools are accessed through the Quarkus Agent MCP
server.
+To connect an MCP client directly instead, enable Dev MCP in the Dev UI
settings dialog
+(Dev MCP tab): the tools are then served at `http://localhost:8080/q/dev-mcp`,
and the
+tab lists the connected clients.
+
+== Setting up the Quarkus Agent MCP server
+
+Follow the https://quarkus.io/blog/introducing-agent-mcp[Quarkus Agent MCP]
guide to install
+and configure the server for your IDE or AI coding assistant.
+
+Once active, the assistant loads the Camel Quarkus skill via `quarkus_skills`
before
+generating code, and can manage the application lifecycle, run tests, and
search the
+Quarkus documentation through the same server.
+
+== Optional: Camel JBang MCP server
+
+Apache Camel also provides a standalone
+https://camel.apache.org/manual/camel-jbang-mcp.html[Camel JBang MCP server]
+exposing the full Camel catalog as MCP tools, plus authoring and runtime
tooling built
+around https://camel.apache.org/manual/camel-jbang.html[Camel JBang].
+
+It is not required for Camel Quarkus development, and its runtime tools (such
as `camel_run`)
+target standalone Camel JBang integrations rather than Quarkus applications.
+Its catalog tools can still be a useful complement if you work with both
runtimes.
diff --git
a/extensions-core/core/deployment/src/main/resources/META-INF/quarkus-skill.md
b/extensions-core/core/deployment/src/main/resources/META-INF/quarkus-skill.md
new file mode 100644
index 0000000000..842f0c6ac5
--- /dev/null
+++
b/extensions-core/core/deployment/src/main/resources/META-INF/quarkus-skill.md
@@ -0,0 +1,89 @@
+<!--
+
+ 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.
+
+-->
+
+### Defining routes
+
+Prefer the type-safe **Endpoint DSL**: extend
`org.apache.camel.builder.endpoint.EndpointRouteBuilder` (included in
`camel-quarkus-core`, no extra dependency) and override `configure()`.
Components and their options are fluent methods, so a wrong option is a compile
error instead of a runtime failure. No annotation needed; add
`@ApplicationScoped` only when the builder itself injects beans or config.
+
+```java
+public class MyRoute extends EndpointRouteBuilder {
+ @Override
+ public void configure() {
+ from(timer("tick").period(5000))
+ .log("Hello ${body}");
+ }
+}
+```
+
+Beware: builder methods for **all** components compile via
`camel-quarkus-core`, even when the component's extension is missing from the
`pom.xml`. The route then fails at runtime. Always add the extension that
provides each component used; when its artifact name is not obvious from the
scheme (e.g. `smtp` is in `camel-quarkus-mail`), find it in the Quarkus
platform catalog.
+
+Plain `RouteBuilder` with string URIs (`from("timer:tick?period=5000")`) also
works.
+
+YAML DSL needs `camel-quarkus-yaml-dsl`. Route files are auto-discovered from
the default `camel.main.routes-include-pattern`
(`classpath:camel/*,classpath:camel-template/*,classpath:camel-rest/*`), so a
file under `src/main/resources/camel/` loads with no configuration. Setting
that property in `application.properties` **replaces** the defaults, so keep
the default patterns if you add your own, or other route files stop loading.
+
+### Dependencies and configuration
+
+- Import the `io.quarkus.platform:quarkus-camel-bom` in
`<dependencyManagement>` (next to `quarkus-bom`) and declare `camel-quarkus-*`
dependencies **without versions**.
+- Configure components in `application.properties` with
`camel.component.<name>.<option> = value` instead of Java configuration code.
+- In dev and test mode, Quarkus **Dev Services** auto-start backing services
(Kafka, databases, Infinispan, ...) and inject their connection config. Do not
hardcode that detail (Kafka `brokers`, a JDBC URL, ...): setting it yourself
disables the Dev Services wiring. Leave it unset in dev/test and configure it
only for production (e.g. with a `%prod.` profile).
+- Components that talk to a backend (kafka, sql, jms, aws, ...) need that
backend actually running. In dev/test, Dev Services starts it — but only when a
container runtime (Podman or Docker) is reachable, which a sandbox often lacks.
A valid URI is not a working route: when a runtime is up, smoke-test end to end
(send a message, confirm it is consumed) before calling it done. When none is
reachable, Dev Services cannot start the backend — report that as an
environment limitation; do not [...]
+- Before using an extension, read its reference page:
`https://camel.apache.org/camel-quarkus/latest/reference/extensions/<name>.html`.
It documents the extension's usage patterns, configuration, and limitations
(such as JVM-only support or native mode constraints).
+
+### Endpoint options: never guess
+
+Do not invent URI parameters. Look them up, in this order:
+
+1. **Preferred**: the Dev MCP tools `camelCatalogDoc` (component
documentation, with an `optionsFilter` keyword) and `camelValidateEndpointUri`
(validates a URI: unknown options with suggestions, invalid values, missing
required options). They use the exact Camel version and classpath of the
application. They require dev mode: start it first if it is not running.
+2. When dev mode is not an option, the versioned docs:
`https://camel.apache.org/components/<camel-major.minor>.x/<name>-component.html`
+3. Last resort, offline: the component JAR embeds its full catalog in
`META-INF/org/apache/camel/component/<name>/<name>.json` (in the local Maven
repository once the `camel-quarkus-<name>` dependency is added).
+
+After writing or changing routes with string URIs or YAML, validate endpoint
URIs and simple expressions (no compile or run needed; Endpoint DSL options are
already checked by the compiler):
+
+```shell
+mvn org.apache.camel:camel-report-maven-plugin:<camel-version>:validate
+```
+
+Use the project's Camel version (see the `quarkus-camel-bom` import). It also
flags duplicate route ids, `direct:`/`seda:` endpoints with no consumer, and
bad Camel options in `application.properties`.
+
+### CDI patterns
+
+```java
+@Inject CamelContext context;
+@Inject ProducerTemplate producerTemplate;
+```
+
+Expose processors to routes by name with `@Named("myProcessor")` +
`@ApplicationScoped`, then `.process("myProcessor")`. Use
`@ActivateRequestContext` on methods called from routes that touch
request-scoped beans. Never use Spring annotations (`@Component`,
`@Autowired`): CDI only.
+
+### Testing
+
+Before writing any Camel Quarkus test, read the testing guide:
+https://camel.apache.org/camel-quarkus/latest/user-guide/testing.html
+
+The mistakes to avoid:
+
+- JVM tests: prefer `@CamelQuarkusTest` (from the `camel-quarkus-junit` test
dependency). It configures Camel test features through annotation attributes
instead of a base class, and implies `@QuarkusTest`. JVM-only. Plain
`@QuarkusTest` on its own is fine when you need no Camel-specific test
features. The `CamelQuarkusTestSupport` base class is still supported; **never
extend `CamelTestSupport`** (not suited to Quarkus).
+- Native tests are `@QuarkusIntegrationTest` extending the JVM test class. The
app runs as a separate process, so **`@Inject` does not work there**: assert
through REST endpoints (RestAssured) or external effects instead.
+- With `@CamelQuarkusTest`, declare test routes with the `routes` attribute
and inject `ProducerTemplate` / `MockEndpoint` (`@Inject` / `@EndpointInject`).
Advise application routes with `advices` / `replaceRouteFromWith`, or per test
method through an injected `CamelTestHelper`; `mockEndpoints` /
`mockEndpointsAndSkip` need isolated mode (`@TestProfile`). With the
`CamelQuarkusTestSupport` base class instead, override `createRouteBuilder()`
for test routes and use its `adviceRoute("rout [...]
+
+### Native mode pitfalls
+
+- Classes reached via reflection (custom type converters, processors
referenced by class name) need `@RegisterForReflection`.
+- Exception classes used in `onException(MyException.class)` must also be
registered with `@RegisterForReflection`.
+- Resources read with `getResource()` / `getResourceAsStream()` must be listed
in `quarkus.native.resources.includes` in `application.properties`.
+- Dynamic class loading does not work in native images.
diff --git
a/extensions-core/core/deployment/src/test/java/org/apache/camel/quarkus/core/deployment/devui/CamelQuarkusCoreDevUITest.java
b/extensions-core/core/deployment/src/test/java/org/apache/camel/quarkus/core/deployment/devui/CamelQuarkusCoreDevUITest.java
index 6802a498f7..639faf502e 100644
---
a/extensions-core/core/deployment/src/test/java/org/apache/camel/quarkus/core/deployment/devui/CamelQuarkusCoreDevUITest.java
+++
b/extensions-core/core/deployment/src/test/java/org/apache/camel/quarkus/core/deployment/devui/CamelQuarkusCoreDevUITest.java
@@ -26,6 +26,8 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
public class CamelQuarkusCoreDevUITest extends DevUIJsonRPCTest {
@RegisterExtension
@@ -61,6 +63,79 @@ public class CamelQuarkusCoreDevUITest extends
DevUIJsonRPCTest {
assertEquals(withGoodOption, withBadOption);
}
+ @Test
+ void camelCatalogDocReturnsComponentSchema() throws Exception {
+ String result = super.executeJsonRPCMethod(String.class,
"camelCatalogDoc",
+ Map.of("name", "timer", "optionsFilter", ""));
+ JsonObject json = (JsonObject) Json.decodeValue(result);
+ assertEquals("timer",
json.getJsonObject("component").getString("scheme"));
+ assertTrue(json.getJsonObject("properties").containsKey("period"));
+ }
+
+ @Test
+ void camelCatalogDocFiltersOptions() throws Exception {
+ String result = super.executeJsonRPCMethod(String.class,
"camelCatalogDoc",
+ Map.of("name", "timer", "optionsFilter", "period"));
+ JsonObject json = (JsonObject) Json.decodeValue(result);
+ JsonObject properties = json.getJsonObject("properties");
+ assertTrue(properties.containsKey("period"));
+ assertFalse(properties.containsKey("timerName"));
+ }
+
+ @Test
+ void camelCatalogDocUnknownComponentReturnsError() throws Exception {
+ String result = super.executeJsonRPCMethod(String.class,
"camelCatalogDoc",
+ Map.of("name", "kafka", "optionsFilter", ""));
+ JsonObject json = (JsonObject) Json.decodeValue(result);
+ assertTrue(json.getString("error").contains("kafka"));
+ assertTrue(json.getString("error").contains("classpath"));
+ }
+
+ @Test
+ void camelValidateEndpointUriAcceptsValidUri() throws Exception {
+ String result = super.executeJsonRPCMethod(String.class,
"camelValidateEndpointUri",
+ Map.of("uri", "timer:tick?period=5000"));
+ JsonObject json = (JsonObject) Json.decodeValue(result);
+ assertTrue(json.getBoolean("valid"));
+ }
+
+ @Test
+ void camelValidateEndpointUriRejectsUnknownOption() throws Exception {
+ String result = super.executeJsonRPCMethod(String.class,
"camelValidateEndpointUri",
+ Map.of("uri", "timer:tick?bogusOption=true"));
+ JsonObject json = (JsonObject) Json.decodeValue(result);
+ assertFalse(json.getBoolean("valid"));
+ assertTrue(json.getString("errors").contains("bogusOption"));
+ }
+
+ @Test
+ void camelValidateEndpointUriRejectsMalformedUri() throws Exception {
+ String result = super.executeJsonRPCMethod(String.class,
"camelValidateEndpointUri",
+ Map.of("uri", ""));
+ JsonObject json = (JsonObject) Json.decodeValue(result);
+ assertFalse(json.getBoolean("valid"));
+ assertTrue(json.getString("errors").contains("Malformed"));
+ }
+
+ @Test
+ void camelValidateEndpointUriRejectsSchemelessUri() throws Exception {
+ String result = super.executeJsonRPCMethod(String.class,
"camelValidateEndpointUri",
+ Map.of("uri", "foobar"));
+ JsonObject json = (JsonObject) Json.decodeValue(result);
+ assertFalse(json.getBoolean("valid"));
+ assertTrue(json.getString("errors").contains("Malformed"));
+ }
+
+ @Test
+ void camelValidateEndpointUriRejectsUnknownComponent() throws Exception {
+ String result = super.executeJsonRPCMethod(String.class,
"camelValidateEndpointUri",
+ Map.of("uri", "kafka:orders?brokers=localhost:9092"));
+ JsonObject json = (JsonObject) Json.decodeValue(result);
+ assertFalse(json.getBoolean("valid"));
+ assertTrue(json.getString("errors").contains("kafka"));
+ assertTrue(json.getString("errors").contains("classpath"));
+ }
+
@Test
void contextConsoleStripsAllOptions() throws Exception {
// context has no allowed options, so malicious keys should be
stripped and the console should still respond
diff --git
a/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/devui/CamelCoreDevUIService.java
b/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/devui/CamelCoreDevUIService.java
index b6045aeaed..339903f052 100644
---
a/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/devui/CamelCoreDevUIService.java
+++
b/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/devui/CamelCoreDevUIService.java
@@ -16,9 +16,12 @@
*/
package org.apache.camel.quarkus.devui;
+import java.net.URISyntaxException;
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -26,14 +29,22 @@ import java.util.concurrent.ConcurrentHashMap;
import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
+import io.quarkus.runtime.annotations.DevMCPEnableByDefault;
+import io.quarkus.runtime.annotations.JsonRpcDescription;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.operators.multi.processors.BroadcastProcessor;
import io.smallrye.mutiny.subscription.Cancellable;
import jakarta.enterprise.context.ApplicationScoped;
import org.apache.camel.CamelContext;
+import org.apache.camel.catalog.EndpointValidationResult;
+import org.apache.camel.catalog.RuntimeCamelCatalog;
+import org.apache.camel.catalog.impl.AbstractCamelCatalog;
import org.apache.camel.console.DevConsole;
import org.apache.camel.console.DevConsoleRegistry;
+import org.apache.camel.util.StringHelper;
+import org.apache.camel.util.json.DeserializationException;
import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.logging.Logger;
@@ -141,6 +152,240 @@ public class CamelCoreDevUIService {
}) != null;
}
+ /**
+ * Gets the Camel catalog documentation of a component available in the
application.
+ *
+ * @param name The component scheme
+ * @param optionsFilter Keyword to keep only the options whose name or
description matches; an empty
+ * string returns all options
+ * @return JSON String representation of the component
catalog documentation
+ */
+ @JsonRpcDescription("Camel catalog documentation of a component: endpoint
URI syntax and all options"
+ + " (name, type, default value, description), from the exact Camel
version of this application."
+ + " Use it before writing a route with an unfamiliar component
instead of guessing URI options.")
+ @DevMCPEnableByDefault
+ public String camelCatalogDoc(
+ @JsonRpcDescription("Component scheme, e.g. timer, kafka, sql")
String name,
+ @JsonRpcDescription("Keyword to keep only the options whose name
or description matches;"
+ + " pass an empty string to return all options") String
optionsFilter) {
+ RuntimeCamelCatalog catalog =
getContextPlugin(RuntimeCamelCatalog.class);
+ if (catalog == null) {
+ return "{}";
+ }
+ if (name == null || name.isBlank()) {
+ JsonObject error = new JsonObject();
+ error.put("error", "Component scheme must not be blank");
+ return error.toJson();
+ }
+ String json = catalog.componentJSonSchema(name);
+ if (json == null) {
+ JsonObject error = new JsonObject();
+ error.put("error", missingComponentMessage(name));
+ return error.toJson();
+ }
+ if (optionsFilter == null || optionsFilter.isBlank()) {
+ return json;
+ }
+ return filterComponentOptions(json, optionsFilter);
+ }
+
+ /**
+ * Validates a Camel endpoint URI against the catalog of the application.
+ *
+ * @param uri The full endpoint URI
+ * @return JSON String representation of the validation result
+ */
+ @JsonRpcDescription("Validates a Camel endpoint URI against the catalog of
this application:"
+ + " unknown options (with suggestions), invalid enum or boolean
values, missing required options."
+ + " Call it after writing or changing any endpoint URI.")
+ @DevMCPEnableByDefault
+ public String camelValidateEndpointUri(
+ @JsonRpcDescription("Full endpoint URI, e.g.
kafka:orders?brokers=localhost:9092") String uri) {
+ RuntimeCamelCatalog catalog =
getContextPlugin(RuntimeCamelCatalog.class);
+ if (catalog == null) {
+ return "{}";
+ }
+ JsonObject result = new JsonObject();
+ if (uri == null || uri.isBlank()) {
+ result.put("valid", false);
+ result.put("errors", "Malformed endpoint URI, expected
scheme:path[?options]");
+ return result.toJson();
+ }
+ EndpointValidationResult validation;
+ try {
+ validation = validateEndpointUri(catalog, uri);
+ } catch (RuntimeException e) {
+ result.put("valid", false);
+ result.put("errors", "Could not validate endpoint URI: " +
e.getMessage());
+ return result.toJson();
+ }
+ if (validation.getUnknownComponent() != null) {
+ result.put("valid", false);
+ String scheme = StringHelper.before(uri, ":");
+ if (scheme == null || scheme.isBlank()) {
+ // No scheme at all: the catalog reports the whole URI as the
unknown component
+ result.put("errors", "Malformed endpoint URI, expected
scheme:path[?options]");
+ } else {
+ result.put("errors",
missingComponentMessage(validation.getUnknownComponent()));
+ }
+ return result.toJson();
+ }
+ result.put("valid", validation.isSuccess());
+ if (!validation.isSuccess()) {
+ result.put("errors", validation.summaryErrorMessage(false));
+ }
+ return result.toJson();
+ }
+
+ private static EndpointValidationResult
validateEndpointUri(RuntimeCamelCatalog catalog, String uri) {
+ if (catalog instanceof AbstractCamelCatalog fullCatalog) {
+ // validateEndpointProperties normalizes the URI and honours the
component's lenientProperties
+ // flag, so lenient components (e.g. http) don't report their
dynamic query parameters as unknown.
+ return fullCatalog.validateEndpointProperties(uri);
+ }
+ // Fallback for a non-standard RuntimeCamelCatalog implementation:
option-level validation only.
+ EndpointValidationResult fallback = new EndpointValidationResult(uri);
+ String scheme = StringHelper.before(uri, ":");
+ if (scheme == null || scheme.isBlank()) {
+ fallback.addSyntaxError("Malformed endpoint URI, expected
scheme:path[?options]");
+ return fallback;
+ }
+ try {
+ return catalog.validateProperties(scheme,
catalog.endpointProperties(uri));
+ } catch (URISyntaxException e) {
+ fallback.addSyntaxError(e.getMessage());
+ return fallback;
+ }
+ }
+
+ private static String missingComponentMessage(String scheme) {
+ return "Component '" + scheme + "' is not on the application
classpath."
+ + " Find the Camel Quarkus extension that provides it in the
Quarkus platform catalog"
+ + " and add it to the project.";
+ }
+
+ private static String filterComponentOptions(String json, String filter) {
+ try {
+ JsonObject schema = (JsonObject) Jsoner.deserialize(json);
+ String keyword = filter.toLowerCase(Locale.ROOT);
+ for (String section : List.of("componentProperties", "properties",
"headers")) {
+ if (schema.get(section) instanceof JsonObject options) {
+ JsonObject filtered = new JsonObject();
+ for (Map.Entry<String, Object> option :
options.entrySet()) {
+ if (optionMatches(option, keyword)) {
+ filtered.put(option.getKey(), option.getValue());
+ }
+ }
+ schema.put(section, filtered);
+ }
+ }
+ if (schema.get("apiProperties") instanceof JsonObject
apiProperties) {
+ schema.put("apiProperties", filterApiProperties(apiProperties,
keyword));
+ }
+ if (schema.get("apis") instanceof JsonObject apis) {
+ schema.put("apis", filterApis(apis, keyword));
+ }
+ return schema.toJson();
+ } catch (DeserializationException e) {
+ return json;
+ }
+ }
+
+ /**
+ * Filters the deeply nested options of API-based components (box, twilio,
google-drive, ...). The real endpoint
+ * options live at {@code
apiProperties[api].methods[method].properties[option]}, so descend to that
level, keep the
+ * options matching the keyword, and prune methods and APIs left with no
matching option.
+ */
+ private static JsonObject filterApiProperties(JsonObject apiProperties,
String keyword) {
+ JsonObject filteredApis = new JsonObject();
+ for (Map.Entry<String, Object> api : apiProperties.entrySet()) {
+ if (!(api.getValue() instanceof JsonObject apiBody) ||
!(apiBody.get("methods") instanceof JsonObject methods)) {
+ continue;
+ }
+ JsonObject filteredMethods = new JsonObject();
+ for (Map.Entry<String, Object> method : methods.entrySet()) {
+ if (!(method.getValue() instanceof JsonObject methodBody)
+ || !(methodBody.get("properties") instanceof
JsonObject properties)) {
+ continue;
+ }
+ JsonObject filteredProperties = new JsonObject();
+ for (Map.Entry<String, Object> option : properties.entrySet())
{
+ if (optionMatches(option, keyword)) {
+ filteredProperties.put(option.getKey(),
option.getValue());
+ }
+ }
+ if (!filteredProperties.isEmpty()) {
+ methodBody.put("properties", filteredProperties);
+ filteredMethods.put(method.getKey(), methodBody);
+ }
+ }
+ if (!filteredMethods.isEmpty()) {
+ apiBody.put("methods", filteredMethods);
+ filteredApis.put(api.getKey(), apiBody);
+ }
+ }
+ return filteredApis;
+ }
+
+ /**
+ * Filters the API metadata section of API-based components, keeping an
API whose name, description or one of its
+ * method names matches the keyword.
+ */
+ private static JsonObject filterApis(JsonObject apis, String keyword) {
+ JsonObject filtered = new JsonObject();
+ for (Map.Entry<String, Object> api : apis.entrySet()) {
+ if (apiMatches(api, keyword)) {
+ filtered.put(api.getKey(), api.getValue());
+ }
+ }
+ return filtered;
+ }
+
+ private static boolean apiMatches(Map.Entry<String, Object> api, String
keyword) {
+ if (api.getKey().toLowerCase(Locale.ROOT).contains(keyword)) {
+ return true;
+ }
+ if (api.getValue() instanceof JsonObject body) {
+ if (body.get("description") instanceof String description
+ && description.toLowerCase(Locale.ROOT).contains(keyword))
{
+ return true;
+ }
+ if (body.get("methods") instanceof JsonObject methods) {
+ for (String methodName : methods.keySet()) {
+ if (methodName.toLowerCase(Locale.ROOT).contains(keyword))
{
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private static boolean optionMatches(Map.Entry<String, Object> option,
String keyword) {
+ if (option.getKey().toLowerCase(Locale.ROOT).contains(keyword)) {
+ return true;
+ }
+ if (option.getValue() instanceof Map<?, ?> attributes) {
+ return attributes.get("description") instanceof String description
+ && description.toLowerCase(Locale.ROOT).contains(keyword);
+ }
+ return false;
+ }
+
+ private static <T> T getContextPlugin(Class<T> type) {
+ ArcContainer container = Arc.container();
+ if (container == null) {
+ // The app is likely performing a dev mode hot reload, thus the
container may be temporarily unavailable
+ return null;
+ }
+
+ return container
+ .select(CamelContext.class)
+ .get()
+ .getCamelContextExtension()
+ .getContextPlugin(type);
+ }
+
private static boolean isConsoleAllowed(String id) {
return ALLOWED_CONSOLE_IDS.contains(id);
}
@@ -178,20 +423,6 @@ public class CamelCoreDevUIService {
return PROCESSORS.computeIfAbsent(id, consoleId -> new
ConsoleSubscription(consoleId, options));
}
- static DevConsoleRegistry getDevConsoleRegistry() {
- ArcContainer container = Arc.container();
- if (container == null) {
- // The app is likely performing a dev mode hot reload, thus the
container may be temporarily unavailable
- return null;
- }
-
- return container
- .select(CamelContext.class)
- .get()
- .getCamelContextExtension()
- .getContextPlugin(DevConsoleRegistry.class);
- }
-
/**
* Enables any Camel console to have its data streamed and updated within
a specified period.
*/
@@ -226,7 +457,7 @@ public class CamelCoreDevUIService {
String callDevConsole() {
String result = "{}";
- DevConsoleRegistry devConsoleRegistry = getDevConsoleRegistry();
+ DevConsoleRegistry devConsoleRegistry =
getContextPlugin(DevConsoleRegistry.class);
if (devConsoleRegistry != null) {
Optional<DevConsole> devConsoleOptional =
devConsoleRegistry.getConsole(id);
if (devConsoleOptional.isPresent()) {
diff --git
a/extensions/bean/deployment/src/main/resources/META-INF/quarkus-skill.md
b/extensions/bean/deployment/src/main/resources/META-INF/quarkus-skill.md
new file mode 100644
index 0000000000..a42d32a481
--- /dev/null
+++ b/extensions/bean/deployment/src/main/resources/META-INF/quarkus-skill.md
@@ -0,0 +1,45 @@
+<!--
+
+ 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.
+
+-->
+
+### Referring to beans by name in routes
+
+A bean called by name from a route needs three annotations: `@Named("myBean")`
(or `io.smallrye.common.annotation.Identifier`), a CDI scope such as
`@ApplicationScoped`, and `@RegisterForReflection` (**required for native
mode**):
+
+```java
+@ApplicationScoped
+@Named("myBean")
+@RegisterForReflection
+public class MyBean {
+ public String hello(String name) {
+ return "Hello " + name;
+ }
+}
+```
+
+Use it in a route with the `.bean("myBean", "hello")` EIP (works in every
DSL), or as an endpoint: `.to(bean("myBean").method("hello"))` with the
Endpoint DSL, `.to("bean:myBean?method=hello")` with string URIs.
+
+### POJO consuming and producing
+
+- `@Consume("direct:in")` goes on the bean **method** that handles the
messages: it auto-creates a route delivering each message to that method.
+- `@Produce("direct:out")` goes on a **field** (`ProducerTemplate`,
`FluentProducerTemplate`, or a custom interface) to send messages to the
endpoint.
+- The field-only rule applies to `@Produce` and `@EndpointInject` (not
supported on setter methods). It does **not** apply to `@Consume`, which
annotates the method that handles the messages — that method is what the
generated route calls.
+
+### Pitfalls
+
+- A bean method that touches request-scoped state (such as Hibernate Panache
entities) throws `ContextNotActiveException` when invoked from a route:
annotate the method with `@ActivateRequestContext`.