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`.

Reply via email to