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 564b87d12f Fixes #9300. Add ai-resource extension and serve 
ai-resource routes as MCP resources
564b87d12f is described below

commit 564b87d12f0cbf6917a56504dd875a8dfc23df1d
Author: Jiří Ondrušek <[email protected]>
AuthorDate: Sat Oct 24 10:19:18 2026 +0200

    Fixes #9300. Add ai-resource extension and serve ai-resource routes as MCP 
resources
    
    * Fixes #9300. Expose ai-resource routes as MCP resources
    
    - Add the camel-quarkus-ai-resource extension (preview)
    - Publish tagged ai-resource routes via the quarkus-mcp-server
      ResourceManager, with the new mcp-server resource-timeout option
    - Refuse clashing names/uris; remove only resources the engine added
    - Inject the tool and resource managers so listChanged is advertised
    - Add ITs, including route-backed resources
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    
    * Fixes #9300. Address review round 1
    
    Squash of the 8 commits answering the first review of #9368:
    
    - Remove MCP resources from the default server only
    - Refuse MCP tools clashing with application tools like resources
    - Make the MCP server timeouts runtime configuration
    - Widen the timing margins of the MCP server timeout tests
    - Read a binary MCP resource longer than a MIME Base64 line
    - Test the MCP tool clash refusal at application startup
    - Test that MCP tools are removed from the default server only
    - Mention the MCP tool clashes in the configuration docs
    
    Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
    
    ---------
    
    Co-authored-by: jiriOndrusek-agent <[email protected]>
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 catalog/pom.xml                                    |  13 +
 .../ROOT/examples/components/ai-resource.yml       |  13 +
 docs/modules/ROOT/nav.adoc                         |   1 +
 .../pages/reference/extensions/ai-resource.adoc    |  50 ++++
 .../pages/reference/extensions/mcp-server.adoc     |  55 ++++-
 extensions/ai-resource/deployment/pom.xml          |  63 +++++
 .../resource/deployment/AiResourceProcessor.java   |  21 +-
 extensions/ai-resource/pom.xml                     |  39 +++
 extensions/ai-resource/runtime/pom.xml             | 102 ++++++++
 .../ai-resource/runtime/src/main/doc/usage.adoc    |   1 +
 .../main/resources/META-INF/quarkus-extension.yaml |  36 +++
 .../mcp/server/deployment/McpServerProcessor.java  |  10 +-
 .../mcp-server/runtime/src/main/doc/intro.adoc     |   7 +-
 .../mcp-server/runtime/src/main/doc/usage.adoc     |  30 ++-
 .../component/mcp/server/CamelMcpServerConfig.java |  17 +-
 .../mcp/server/CamelMcpServerRecorder.java         |  16 +-
 ...onfig.java => CamelMcpServerRunTimeConfig.java} |  30 +--
 .../mcp/server/QuarkusMcpServerEngine.java         |  96 +++++++-
 .../mcp/server/QuarkusMcpServerEngineFactory.java  |  28 ++-
 extensions/pom.xml                                 |   1 +
 .../{mcp-server => ai-resource}/pom.xml            |  60 +++--
 .../ai/resource/it/AiResourceResource.java         | 121 ++++++++++
 .../component/ai/resource/it/AiResourceRoutes.java |  57 +++++
 .../quarkus/component/ai/resource/it/Order.java}   |  18 +-
 .../component/ai/resource/it/AiResourceIT.java}    |  17 +-
 .../component/ai/resource/it/AiResourceTest.java   | 147 ++++++++++++
 integration-tests/mcp-server/pom.xml               |  34 +++
 .../component/mcp/server/it/McpServerResource.java |  14 ++
 .../component/mcp/server/it/McpServerRoutes.java   |  83 ++++++-
 .../it/{QuarkusDefinedTools.java => Order.java}    |  16 +-
 .../mcp/server/it/QuarkusDefinedResources.java     |  55 +++++
 .../mcp/server/it/QuarkusDefinedTools.java         |  23 +-
 .../src/main/resources/application.properties      |   4 +
 .../component/mcp/server/it/McpServerTest.java     | 265 ++++++++++++++++++++-
 integration-tests/pom.xml                          |   1 +
 poms/bom/pom.xml                                   |  10 +
 poms/bom/src/main/generated/flattened-full-pom.xml |  10 +
 .../src/main/generated/flattened-reduced-pom.xml   |  10 +
 .../generated/flattened-reduced-verbose-pom.xml    |  10 +
 tooling/scripts/test-categories.yaml               |   1 +
 40 files changed, 1415 insertions(+), 170 deletions(-)

diff --git a/catalog/pom.xml b/catalog/pom.xml
index eedcb6d9d7..594af814bd 100644
--- a/catalog/pom.xml
+++ b/catalog/pom.xml
@@ -123,6 +123,19 @@
                 </exclusion>
             </exclusions>
         </dependency>
+        <dependency>
+            <groupId>org.apache.camel.quarkus</groupId>
+            <artifactId>camel-quarkus-ai-resource</artifactId>
+            <version>${project.version}</version>
+            <type>pom</type>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>*</groupId>
+                    <artifactId>*</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
         <dependency>
             <groupId>org.apache.camel.quarkus</groupId>
             <artifactId>camel-quarkus-ai-tool</artifactId>
diff --git a/docs/modules/ROOT/examples/components/ai-resource.yml 
b/docs/modules/ROOT/examples/components/ai-resource.yml
new file mode 100644
index 0000000000..06baae00a1
--- /dev/null
+++ b/docs/modules/ROOT/examples/components/ai-resource.yml
@@ -0,0 +1,13 @@
+# Do not edit directly!
+# This file was generated by 
camel-quarkus-maven-plugin:update-extension-doc-page
+cqArtifactId: camel-quarkus-ai-resource
+cqArtifactIdBase: ai-resource
+cqNativeSupported: true
+cqStatus: Stable
+cqDeprecated: false
+cqJvmSince: 4.0.0
+cqNativeSince: 4.0.0
+cqCamelPartName: ai-resource
+cqCamelPartTitle: AI Resource
+cqCamelPartDescription: Framework-agnostic consumer endpoint that registers a 
Camel route as a read-only AI resource in the shared AiResourceRegistry.
+cqExtensionPageTitle: AI Resource
diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc
index 6cfbb1bae3..0208871b2d 100644
--- a/docs/modules/ROOT/nav.adoc
+++ b/docs/modules/ROOT/nav.adoc
@@ -37,6 +37,7 @@
 ** xref:reference/index.adoc[Extensions]
 // extensions: START
 *** xref:reference/extensions/a2a.adoc[A2A]
+*** xref:reference/extensions/ai-resource.adoc[AI Resource]
 *** xref:reference/extensions/ai-tool.adoc[AI Tool]
 *** xref:reference/extensions/amqp.adoc[AMQP]
 *** xref:reference/extensions/as2.adoc[AS2]
diff --git a/docs/modules/ROOT/pages/reference/extensions/ai-resource.adoc 
b/docs/modules/ROOT/pages/reference/extensions/ai-resource.adoc
new file mode 100644
index 0000000000..ac3a748efd
--- /dev/null
+++ b/docs/modules/ROOT/pages/reference/extensions/ai-resource.adoc
@@ -0,0 +1,50 @@
+// Do not edit directly!
+// This file was generated by 
camel-quarkus-maven-plugin:update-extension-doc-page
+[id="extensions-ai-resource"]
+= AI Resource
+:linkattrs:
+:cq-artifact-id: camel-quarkus-ai-resource
+:cq-native-supported: true
+:cq-status: Preview
+:cq-status-deprecation: Preview
+:cq-description: Framework-agnostic consumer endpoint that registers a Camel 
route as a read-only AI resource in the shared AiResourceRegistry.
+:cq-deprecated: false
+:cq-jvm-since: 4.0.0
+:cq-native-since: 4.0.0
+
+ifeval::[{doc-show-badges} == true]
+[.badges]
+[.badge-key]##JVM since##[.badge-supported]##4.0.0## [.badge-key]##Native 
since##[.badge-supported]##4.0.0##
+endif::[]
+
+Framework-agnostic consumer endpoint that registers a Camel route as a 
read-only AI resource in the shared AiResourceRegistry.
+
+[id="extensions-ai-resource-whats-inside"]
+== What's inside
+
+* xref:{cq-camel-components}::ai-resource-component.adoc[AI Resource 
component], URI syntax: `ai-resource:resourceName`
+
+Please refer to the above link for usage and configuration details.
+
+[id="extensions-ai-resource-maven-coordinates"]
+== Maven coordinates
+
+https://{link-quarkus-code-generator}/?extension-search=camel-quarkus-ai-resource[Create
 a new project with this extension on {link-quarkus-code-generator}, 
window="_blank"]
+
+Or add the coordinates to your existing project:
+
+[source,xml]
+----
+<dependency>
+    <groupId>org.apache.camel.quarkus</groupId>
+    <artifactId>camel-quarkus-ai-resource</artifactId>
+</dependency>
+----
+ifeval::[{doc-show-user-guide-link} == true]
+Check the xref:user-guide/index.adoc[User guide] for more information about 
writing Camel Quarkus applications.
+endif::[]
+
+[id="extensions-ai-resource-usage"]
+== Usage
+The `ai-resource` component registers Camel routes as read-only resources in a 
shared `AiResourceRegistry`. To serve the tagged routes as MCP resources, add 
the `camel-quarkus-mcp-server` extension, see 
xref:reference/extensions/mcp-server.adoc#extensions-mcp-server-usage-resources[MCP
 Server resources].
+
diff --git a/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc 
b/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc
index 6790c70ad1..ecc0a279e7 100644
--- a/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc
+++ b/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc
@@ -18,11 +18,12 @@ ifeval::[{doc-show-badges} == true]
 endif::[]
 
 The MCP Server extension exposes Camel routes registered via the `ai-tool`
-component as tools of a Model Context Protocol (MCP) server, served through
+component as tools, and `ai-resource` routes as resources, of a Model Context
+Protocol (MCP) server, served through
 the quarkiverse 
https://docs.quarkiverse.io/quarkus-mcp-server/dev/index.html[quarkus-mcp-server]
 extension. No route is needed for the server itself: add the extension, tag
-the `ai-tool` routes to expose, and any MCP client (another Camel
-application, an IDE, a coding agent) can discover and call them.
+the `ai-tool` and `ai-resource` routes to expose, and any MCP client (another
+Camel application, an IDE, a coding agent) can discover and use them.
 
 Tool semantics — tag-based opt-in (the untagged default pool is never
 exposed), flat-namespace collision refusal, per-call timeout and error
@@ -107,19 +108,38 @@ in the AI Tool component documentation.
 In native mode, an `outputSchema` loaded from `classpath:` must be
 
xref:user-guide/native-mode.adoc#embedding-resource-in-native-executable[embedded
 in the native executable].
 
+[id="extensions-mcp-server-usage-resources"]
+=== Resources
+
+Add the `camel-quarkus-ai-resource` extension to define
+xref:{cq-camel-components}::ai-resource-component.adoc[`ai-resource`] routes:
+the tagged ones are served as MCP resources, selected by the same
+`quarkus.camel.mcp-server.tags`. Like tools, they follow the route lifecycle
+and clients are notified via `notifications/resources/list_changed`. A failed
+read returns a JSON-RPC error with the generic message `Resource read failed`;
+a read exceeding `quarkus.camel.mcp-server.resource-timeout` returns
+`Resource read timed out`. Resource names must be unique, as
+quarkus-mcp-server requires: a resource whose name is already taken, or
+whose uri is already used by a resource the application registers with
+quarkus-mcp-server, is refused with an ERROR log, while its route keeps
+running.
+
 
[id="extensions-mcp-server-usage-combining-the-camel-and-quarkus-mcp-server-configuration"]
 === Combining the Camel and quarkus-mcp-server configuration
 
 The configuration is split by ownership: `quarkus.camel.mcp-server.*`
-decides *which tools exist and how they execute* (identical semantics on
-every Camel runtime), while `quarkus.mcp.server.*` decides *how the server
-is exposed*. Both are set side by side in `application.properties`:
+decides *which tools and resources exist and how they execute* (identical
+semantics on every Camel runtime, except for the tool and resource clashes
+refused by quarkus-mcp-server), while `quarkus.mcp.server.*` decides
+*how the server is exposed*. Both are set side by side in
+`application.properties`:
 
 [source,properties]
 ----
-# Camel-owned: tool selection and execution
+# Camel-owned: tool and resource selection and execution
 quarkus.camel.mcp-server.tags = crm,notify
 quarkus.camel.mcp-server.tool-timeout = 10000
+quarkus.camel.mcp-server.resource-timeout = 5000
 
 # quarkus-mcp-server-owned: serving and server identity
 quarkus.mcp.server.server-info.name = my-integration-app
@@ -248,7 +268,9 @@ public class CalculatorTools {
 
 is exposed alongside the `ai-tool` routes. Annotated tools are registered at
 build time; Camel tools are added and removed dynamically with the route
-lifecycle. Choose distinct tool names — MCP has a flat tool namespace.
+lifecycle. Choose distinct tool names — MCP has a flat tool namespace: a
+Camel tool whose name is already taken by a tool of the application is
+refused with an ERROR log, while its route keeps running.
 
 [id="extensions-mcp-server-usage-dynamic-tools"]
 === Dynamic tools
@@ -290,23 +312,32 @@ Boot it is the `camel-mcp-server-starter`.
 
 a|icon:lock[title=Fixed at build time] 
[[quarkus-camel-mcp-server-enabled]]`link:#quarkus-camel-mcp-server-enabled[quarkus.camel.mcp-server.enabled]`
 
-Whether to expose ai-tool routes as MCP tools through the quarkus-mcp-server 
extension.
+Whether to expose ai-tool routes as MCP tools and ai-resource routes as MCP 
resources through the
+quarkus-mcp-server extension.
 | `boolean`
 | `true`
 
 a|icon:lock[title=Fixed at build time] 
[[quarkus-camel-mcp-server-tags]]`link:#quarkus-camel-mcp-server-tags[quarkus.camel.mcp-server.tags]`
 
-Comma-separated list of ai-tool tags to expose as MCP tools. Only tools 
registered under one of these tags are
-exposed; the untagged default pool is never exposed. When not set, no tools 
are exposed.
+Comma-separated list of tags selecting the ai-tool routes to expose as MCP 
tools and the ai-resource routes to
+expose as MCP resources. Only routes registered under one of these tags are 
exposed; the untagged default pools
+are never exposed. When not set, nothing is exposed.
 | `string`
 | 
 
-a|icon:lock[title=Fixed at build time] 
[[quarkus-camel-mcp-server-tool-timeout]]`link:#quarkus-camel-mcp-server-tool-timeout[quarkus.camel.mcp-server.tool-timeout]`
+a| 
[[quarkus-camel-mcp-server-tool-timeout]]`link:#quarkus-camel-mcp-server-tool-timeout[quarkus.camel.mcp-server.tool-timeout]`
 
 Per-call tool execution timeout in milliseconds. A call exceeding the timeout 
returns an error result to the
 MCP client; the underlying route keeps running until it completes on its own.
 | `long`
 | `20000`
+
+a| 
[[quarkus-camel-mcp-server-resource-timeout]]`link:#quarkus-camel-mcp-server-resource-timeout[quarkus.camel.mcp-server.resource-timeout]`
+
+Per-read resource execution timeout in milliseconds. A read exceeding the 
timeout returns an error to the MCP
+client; the underlying route keeps running until it completes on its own.
+| `long`
+| `20000`
 |===
 
 [.configuration-legend]
diff --git a/extensions/ai-resource/deployment/pom.xml 
b/extensions/ai-resource/deployment/pom.xml
new file mode 100644
index 0000000000..f45d78d1ac
--- /dev/null
+++ b/extensions/ai-resource/deployment/pom.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.camel.quarkus</groupId>
+        <artifactId>camel-quarkus-ai-resource-parent</artifactId>
+        <version>4.0.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>camel-quarkus-ai-resource-deployment</artifactId>
+    <name>Camel Quarkus :: AI Resource :: Deployment</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.camel.quarkus</groupId>
+            <artifactId>camel-quarkus-core-deployment</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel.quarkus</groupId>
+            <artifactId>camel-quarkus-ai-resource</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <annotationProcessorPaths>
+                        <path>
+                            <groupId>io.quarkus</groupId>
+                            
<artifactId>quarkus-extension-processor</artifactId>
+                            <version>${quarkus.version}</version>
+                        </path>
+                    </annotationProcessorPaths>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git 
a/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java
 
b/extensions/ai-resource/deployment/src/main/java/org/apache/camel/quarkus/component/ai/resource/deployment/AiResourceProcessor.java
similarity index 60%
copy from 
integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java
copy to 
extensions/ai-resource/deployment/src/main/java/org/apache/camel/quarkus/component/ai/resource/deployment/AiResourceProcessor.java
index 88a6cf149f..a55173832f 100644
--- 
a/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java
+++ 
b/extensions/ai-resource/deployment/src/main/java/org/apache/camel/quarkus/component/ai/resource/deployment/AiResourceProcessor.java
@@ -14,20 +14,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.camel.quarkus.component.mcp.server.it;
+package org.apache.camel.quarkus.component.ai.resource.deployment;
 
-import io.quarkiverse.mcp.server.Tool;
-import io.quarkiverse.mcp.server.ToolArg;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.builditem.FeatureBuildItem;
 
-/**
- * A plain quarkus-mcp-server annotated tool living on the same MCP server as 
the Camel ai-tool routes.
- */
-public class QuarkusDefinedTools {
+class AiResourceProcessor {
+
+    private static final String FEATURE = "camel-ai-resource";
 
-    @Tool(name = "add_numbers", description = "Add two numbers")
-    String add(
-            @ToolArg(description = "First addend") long a,
-            @ToolArg(description = "Second addend") long b) {
-        return String.valueOf(a + b);
+    @BuildStep
+    FeatureBuildItem feature() {
+        return new FeatureBuildItem(FEATURE);
     }
 }
diff --git a/extensions/ai-resource/pom.xml b/extensions/ai-resource/pom.xml
new file mode 100644
index 0000000000..4265cb4afd
--- /dev/null
+++ b/extensions/ai-resource/pom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.camel.quarkus</groupId>
+        <artifactId>camel-quarkus-extensions</artifactId>
+        <version>4.0.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>camel-quarkus-ai-resource-parent</artifactId>
+    <name>Camel Quarkus :: AI Resource</name>
+    <packaging>pom</packaging>
+
+    <modules>
+        <module>deployment</module>
+        <module>runtime</module>
+    </modules>
+</project>
diff --git a/extensions/ai-resource/runtime/pom.xml 
b/extensions/ai-resource/runtime/pom.xml
new file mode 100644
index 0000000000..4a606a1169
--- /dev/null
+++ b/extensions/ai-resource/runtime/pom.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.camel.quarkus</groupId>
+        <artifactId>camel-quarkus-ai-resource-parent</artifactId>
+        <version>4.0.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>camel-quarkus-ai-resource</artifactId>
+    <name>Camel Quarkus :: AI Resource :: Runtime</name>
+    <description>Framework-agnostic consumer endpoint that registers a Camel 
route as a read-only AI resource in the shared AiResourceRegistry.</description>
+
+    <properties>
+        <quarkus.metadata.status>preview</quarkus.metadata.status>
+        <camel.quarkus.jvmSince>4.0.0</camel.quarkus.jvmSince>
+        <camel.quarkus.nativeSince>4.0.0</camel.quarkus.nativeSince>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.camel.quarkus</groupId>
+            <artifactId>camel-quarkus-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-ai-resource</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>io.quarkus</groupId>
+                <artifactId>quarkus-extension-maven-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <annotationProcessorPaths>
+                        <path>
+                            <groupId>io.quarkus</groupId>
+                            
<artifactId>quarkus-extension-processor</artifactId>
+                            <version>${quarkus.version}</version>
+                        </path>
+                    </annotationProcessorPaths>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+
+    <profiles>
+        <profile>
+            <id>full</id>
+            <activation>
+                <property>
+                    <name>!quickly</name>
+                </property>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.camel.quarkus</groupId>
+                        <artifactId>camel-quarkus-maven-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>update-extension-doc-page</id>
+                                <goals>
+                                    <goal>update-extension-doc-page</goal>
+                                </goals>
+                                <phase>process-classes</phase>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+</project>
diff --git a/extensions/ai-resource/runtime/src/main/doc/usage.adoc 
b/extensions/ai-resource/runtime/src/main/doc/usage.adoc
new file mode 100644
index 0000000000..84f3753331
--- /dev/null
+++ b/extensions/ai-resource/runtime/src/main/doc/usage.adoc
@@ -0,0 +1 @@
+The `ai-resource` component registers Camel routes as read-only resources in a 
shared `AiResourceRegistry`. To serve the tagged routes as MCP resources, add 
the `camel-quarkus-mcp-server` extension, see 
xref:reference/extensions/mcp-server.adoc#extensions-mcp-server-usage-resources[MCP
 Server resources].
diff --git 
a/extensions/ai-resource/runtime/src/main/resources/META-INF/quarkus-extension.yaml
 
b/extensions/ai-resource/runtime/src/main/resources/META-INF/quarkus-extension.yaml
new file mode 100644
index 0000000000..601113e01f
--- /dev/null
+++ 
b/extensions/ai-resource/runtime/src/main/resources/META-INF/quarkus-extension.yaml
@@ -0,0 +1,36 @@
+#
+# 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.
+#
+
+# This is a generated file. Do not edit directly!
+# To re-generate, run the following command from the top level directory:
+#
+#   mvn -N cq:update-quarkus-metadata
+#
+---
+name: "Camel AI Resource"
+description: "Framework-agnostic consumer endpoint that registers a Camel 
route as a read-only AI resource in the shared AiResourceRegistry"
+metadata:
+  icon-url: 
"https://raw.githubusercontent.com/apache/camel-website/main/antora-ui-camel/src/img/logo-d.svg";
+  sponsor: "Apache Software Foundation"
+  guide: 
"https://camel.apache.org/camel-quarkus/latest/reference/extensions/ai-resource.html";
+  categories:
+  - "integration"
+  status: "preview"
+  integrates:
+    - name: "Camel"
+      artifact: "org.apache.camel:camel-base"
+      version: "${camel.version}"
\ No newline at end of file
diff --git 
a/extensions/mcp-server/deployment/src/main/java/org/apache/camel/quarkus/component/mcp/server/deployment/McpServerProcessor.java
 
b/extensions/mcp-server/deployment/src/main/java/org/apache/camel/quarkus/component/mcp/server/deployment/McpServerProcessor.java
index f43febbb35..e30e0aad63 100644
--- 
a/extensions/mcp-server/deployment/src/main/java/org/apache/camel/quarkus/component/mcp/server/deployment/McpServerProcessor.java
+++ 
b/extensions/mcp-server/deployment/src/main/java/org/apache/camel/quarkus/component/mcp/server/deployment/McpServerProcessor.java
@@ -18,12 +18,14 @@ package 
org.apache.camel.quarkus.component.mcp.server.deployment;
 
 import java.util.function.BooleanSupplier;
 
+import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
 import io.quarkus.deployment.annotations.BuildStep;
 import io.quarkus.deployment.annotations.ExecutionTime;
 import io.quarkus.deployment.annotations.Record;
 import io.quarkus.deployment.builditem.FeatureBuildItem;
 import org.apache.camel.quarkus.component.mcp.server.CamelMcpServerConfig;
 import org.apache.camel.quarkus.component.mcp.server.CamelMcpServerRecorder;
+import 
org.apache.camel.quarkus.component.mcp.server.QuarkusMcpServerEngineFactory;
 import 
org.apache.camel.quarkus.core.deployment.spi.RuntimeCamelContextCustomizerBuildItem;
 
 class McpServerProcessor {
@@ -35,11 +37,15 @@ class McpServerProcessor {
         return new FeatureBuildItem(FEATURE);
     }
 
+    @BuildStep(onlyIf = McpServerEnabled.class)
+    AdditionalBeanBuildItem mcpServerEngineFactory() {
+        return 
AdditionalBeanBuildItem.unremovableOf(QuarkusMcpServerEngineFactory.class);
+    }
+
     @BuildStep(onlyIf = McpServerEnabled.class)
     @Record(ExecutionTime.RUNTIME_INIT)
     RuntimeCamelContextCustomizerBuildItem 
mcpServerBridge(CamelMcpServerRecorder recorder, CamelMcpServerConfig config) {
-        return new RuntimeCamelContextCustomizerBuildItem(
-                recorder.createContextCustomizer(config.tags().orElse(null), 
config.toolTimeout()));
+        return new 
RuntimeCamelContextCustomizerBuildItem(recorder.createContextCustomizer(config.tags().orElse(null)));
     }
 
     static final class McpServerEnabled implements BooleanSupplier {
diff --git a/extensions/mcp-server/runtime/src/main/doc/intro.adoc 
b/extensions/mcp-server/runtime/src/main/doc/intro.adoc
index 7c60ee6f2d..f6faac8d46 100644
--- a/extensions/mcp-server/runtime/src/main/doc/intro.adoc
+++ b/extensions/mcp-server/runtime/src/main/doc/intro.adoc
@@ -1,9 +1,10 @@
 The MCP Server extension exposes Camel routes registered via the `ai-tool`
-component as tools of a Model Context Protocol (MCP) server, served through
+component as tools, and `ai-resource` routes as resources, of a Model Context
+Protocol (MCP) server, served through
 the quarkiverse 
https://docs.quarkiverse.io/quarkus-mcp-server/dev/index.html[quarkus-mcp-server]
 extension. No route is needed for the server itself: add the extension, tag
-the `ai-tool` routes to expose, and any MCP client (another Camel
-application, an IDE, a coding agent) can discover and call them.
+the `ai-tool` and `ai-resource` routes to expose, and any MCP client (another
+Camel application, an IDE, a coding agent) can discover and use them.
 
 Tool semantics — tag-based opt-in (the untagged default pool is never
 exposed), flat-namespace collision refusal, per-call timeout and error
diff --git a/extensions/mcp-server/runtime/src/main/doc/usage.adoc 
b/extensions/mcp-server/runtime/src/main/doc/usage.adoc
index 64b4c5b902..5473295c53 100644
--- a/extensions/mcp-server/runtime/src/main/doc/usage.adoc
+++ b/extensions/mcp-server/runtime/src/main/doc/usage.adoc
@@ -45,18 +45,36 @@ in the AI Tool component documentation.
 In native mode, an `outputSchema` loaded from `classpath:` must be
 
xref:user-guide/native-mode.adoc#embedding-resource-in-native-executable[embedded
 in the native executable].
 
+=== Resources
+
+Add the `camel-quarkus-ai-resource` extension to define
+xref:{cq-camel-components}::ai-resource-component.adoc[`ai-resource`] routes:
+the tagged ones are served as MCP resources, selected by the same
+`quarkus.camel.mcp-server.tags`. Like tools, they follow the route lifecycle
+and clients are notified via `notifications/resources/list_changed`. A failed
+read returns a JSON-RPC error with the generic message `Resource read failed`;
+a read exceeding `quarkus.camel.mcp-server.resource-timeout` returns
+`Resource read timed out`. Resource names must be unique, as
+quarkus-mcp-server requires: a resource whose name is already taken, or
+whose uri is already used by a resource the application registers with
+quarkus-mcp-server, is refused with an ERROR log, while its route keeps
+running.
+
 === Combining the Camel and quarkus-mcp-server configuration
 
 The configuration is split by ownership: `quarkus.camel.mcp-server.*`
-decides *which tools exist and how they execute* (identical semantics on
-every Camel runtime), while `quarkus.mcp.server.*` decides *how the server
-is exposed*. Both are set side by side in `application.properties`:
+decides *which tools and resources exist and how they execute* (identical
+semantics on every Camel runtime, except for the tool and resource clashes
+refused by quarkus-mcp-server), while `quarkus.mcp.server.*` decides
+*how the server is exposed*. Both are set side by side in
+`application.properties`:
 
 [source,properties]
 ----
-# Camel-owned: tool selection and execution
+# Camel-owned: tool and resource selection and execution
 quarkus.camel.mcp-server.tags = crm,notify
 quarkus.camel.mcp-server.tool-timeout = 10000
+quarkus.camel.mcp-server.resource-timeout = 5000
 
 # quarkus-mcp-server-owned: serving and server identity
 quarkus.mcp.server.server-info.name = my-integration-app
@@ -182,7 +200,9 @@ public class CalculatorTools {
 
 is exposed alongside the `ai-tool` routes. Annotated tools are registered at
 build time; Camel tools are added and removed dynamically with the route
-lifecycle. Choose distinct tool names — MCP has a flat tool namespace.
+lifecycle. Choose distinct tool names — MCP has a flat tool namespace: a
+Camel tool whose name is already taken by a tool of the application is
+refused with an ERROR log, while its route keeps running.
 
 === Dynamic tools
 
diff --git 
a/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerConfig.java
 
b/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerConfig.java
index 5975fc5d86..45b50b1568 100644
--- 
a/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerConfig.java
+++ 
b/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerConfig.java
@@ -32,7 +32,8 @@ import io.smallrye.config.WithDefault;
 public interface CamelMcpServerConfig {
 
     /**
-     * Whether to expose ai-tool routes as MCP tools through the 
quarkus-mcp-server extension.
+     * Whether to expose ai-tool routes as MCP tools and ai-resource routes as 
MCP resources through the
+     * quarkus-mcp-server extension.
      *
      * @asciidoclet
      */
@@ -40,19 +41,11 @@ public interface CamelMcpServerConfig {
     boolean enabled();
 
     /**
-     * Comma-separated list of ai-tool tags to expose as MCP tools. Only tools 
registered under one of these tags are
-     * exposed; the untagged default pool is never exposed. When not set, no 
tools are exposed.
+     * Comma-separated list of tags selecting the ai-tool routes to expose as 
MCP tools and the ai-resource routes to
+     * expose as MCP resources. Only routes registered under one of these tags 
are exposed; the untagged default pools
+     * are never exposed. When not set, nothing is exposed.
      *
      * @asciidoclet
      */
     Optional<String> tags();
-
-    /**
-     * Per-call tool execution timeout in milliseconds. A call exceeding the 
timeout returns an error result to the
-     * MCP client; the underlying route keeps running until it completes on 
its own.
-     *
-     * @asciidoclet
-     */
-    @WithDefault("20000")
-    long toolTimeout();
 }
diff --git 
a/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerRecorder.java
 
b/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerRecorder.java
index f7d5ef2157..238d193c0b 100644
--- 
a/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerRecorder.java
+++ 
b/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerRecorder.java
@@ -16,7 +16,6 @@
  */
 package org.apache.camel.quarkus.component.mcp.server;
 
-import io.quarkiverse.mcp.server.ToolManager;
 import io.quarkus.arc.Arc;
 import io.quarkus.runtime.RuntimeValue;
 import io.quarkus.runtime.annotations.Recorder;
@@ -30,16 +29,23 @@ import org.apache.camel.spi.CamelContextCustomizer;
 @Recorder
 public class CamelMcpServerRecorder {
 
-    public RuntimeValue<CamelContextCustomizer> createContextCustomizer(String 
tags, long toolTimeout) {
+    private final RuntimeValue<CamelMcpServerRunTimeConfig> runtimeConfig;
+
+    public CamelMcpServerRecorder(RuntimeValue<CamelMcpServerRunTimeConfig> 
runtimeConfig) {
+        this.runtimeConfig = runtimeConfig;
+    }
+
+    public RuntimeValue<CamelContextCustomizer> createContextCustomizer(String 
tags) {
         return new RuntimeValue<>(new CamelContextCustomizer() {
             @Override
             public void configure(CamelContext camelContext) {
-                ToolManager toolManager = 
Arc.container().instance(ToolManager.class).get();
-                QuarkusMcpServerEngine engine = new 
QuarkusMcpServerEngine(toolManager);
+                QuarkusMcpServerEngine engine = 
Arc.container().instance(QuarkusMcpServerEngineFactory.class).get()
+                        .createEngine();
 
                 McpServerConfiguration configuration = new 
McpServerConfiguration();
                 configuration.setTags(tags);
-                configuration.setToolTimeout(toolTimeout);
+                
configuration.setToolTimeout(runtimeConfig.getValue().toolTimeout());
+                
configuration.setResourceTimeout(runtimeConfig.getValue().resourceTimeout());
                 McpServerBridge bridge = new McpServerBridge(configuration);
                 try {
                     // the bridge resolves the engine registry-first
diff --git 
a/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerConfig.java
 
b/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerRunTimeConfig.java
similarity index 64%
copy from 
extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerConfig.java
copy to 
extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerRunTimeConfig.java
index 5975fc5d86..ec0ad50638 100644
--- 
a/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerConfig.java
+++ 
b/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/CamelMcpServerRunTimeConfig.java
@@ -16,43 +16,33 @@
  */
 package org.apache.camel.quarkus.component.mcp.server;
 
-import java.util.Optional;
-
 import io.quarkus.runtime.annotations.ConfigPhase;
 import io.quarkus.runtime.annotations.ConfigRoot;
 import io.smallrye.config.ConfigMapping;
 import io.smallrye.config.WithDefault;
 
 /**
- * Bridge-owned configuration of the Camel MCP server. Serving concerns 
(endpoint path, transports, authentication)
- * are owned by the quarkiverse quarkus-mcp-server extension and configured 
via {@code quarkus.mcp.server.*}.
+ * Bridge-owned runtime configuration of the Camel MCP server, see {@link 
CamelMcpServerConfig}.
  */
-@ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED)
+@ConfigRoot(phase = ConfigPhase.RUN_TIME)
 @ConfigMapping(prefix = "quarkus.camel.mcp-server")
-public interface CamelMcpServerConfig {
+public interface CamelMcpServerRunTimeConfig {
 
     /**
-     * Whether to expose ai-tool routes as MCP tools through the 
quarkus-mcp-server extension.
-     *
-     * @asciidoclet
-     */
-    @WithDefault("true")
-    boolean enabled();
-
-    /**
-     * Comma-separated list of ai-tool tags to expose as MCP tools. Only tools 
registered under one of these tags are
-     * exposed; the untagged default pool is never exposed. When not set, no 
tools are exposed.
+     * Per-call tool execution timeout in milliseconds. A call exceeding the 
timeout returns an error result to the
+     * MCP client; the underlying route keeps running until it completes on 
its own.
      *
      * @asciidoclet
      */
-    Optional<String> tags();
+    @WithDefault("20000")
+    long toolTimeout();
 
     /**
-     * Per-call tool execution timeout in milliseconds. A call exceeding the 
timeout returns an error result to the
-     * MCP client; the underlying route keeps running until it completes on 
its own.
+     * Per-read resource execution timeout in milliseconds. A read exceeding 
the timeout returns an error to the MCP
+     * client; the underlying route keeps running until it completes on its 
own.
      *
      * @asciidoclet
      */
     @WithDefault("20000")
-    long toolTimeout();
+    long resourceTimeout();
 }
diff --git 
a/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/QuarkusMcpServerEngine.java
 
b/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/QuarkusMcpServerEngine.java
index 9b09642325..e6d02a540c 100644
--- 
a/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/QuarkusMcpServerEngine.java
+++ 
b/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/QuarkusMcpServerEngine.java
@@ -17,18 +17,31 @@
 package org.apache.camel.quarkus.component.mcp.server;
 
 import java.lang.reflect.Type;
+import java.util.Base64;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
+import io.quarkiverse.mcp.server.BlobResourceContents;
+import io.quarkiverse.mcp.server.JsonRpcErrorCodes;
+import io.quarkiverse.mcp.server.McpException;
+import io.quarkiverse.mcp.server.McpServer;
+import io.quarkiverse.mcp.server.ResourceContents;
+import io.quarkiverse.mcp.server.ResourceManager;
+import io.quarkiverse.mcp.server.ResourceResponse;
 import io.quarkiverse.mcp.server.TextContent;
+import io.quarkiverse.mcp.server.TextResourceContents;
 import io.quarkiverse.mcp.server.ToolManager;
 import io.quarkiverse.mcp.server.ToolResponse;
 import io.vertx.core.json.JsonObject;
 import org.apache.camel.CamelContext;
 import org.apache.camel.component.ai.tool.AiToolAnnotations;
 import org.apache.camel.component.ai.tool.AiToolParameterHelper.ParameterDef;
+import org.apache.camel.component.mcp.server.McpResourceReadResult;
 import org.apache.camel.component.mcp.server.McpServerEngine;
 import org.apache.camel.component.mcp.server.McpServerInfo;
+import org.apache.camel.component.mcp.server.McpServerResource;
 import org.apache.camel.component.mcp.server.McpServerTool;
 import org.apache.camel.component.mcp.server.McpToolCallResult;
 import org.apache.camel.support.service.ServiceSupport;
@@ -36,19 +49,25 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * {@link McpServerEngine} publishing tools into the quarkiverse 
quarkus-mcp-server through its programmatic
- * {@link ToolManager} API. Serving concerns (endpoint path, transports, 
authentication) are owned by
- * quarkus-mcp-server and configured via {@code quarkus.mcp.server.*}.
+ * {@link McpServerEngine} publishing tools and resources into the quarkiverse 
quarkus-mcp-server through its
+ * programmatic {@link ToolManager} and {@link ResourceManager} APIs. Serving 
concerns (endpoint path, transports,
+ * authentication) are owned by quarkus-mcp-server and configured via {@code 
quarkus.mcp.server.*}.
  */
 public class QuarkusMcpServerEngine extends ServiceSupport implements 
McpServerEngine {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(QuarkusMcpServerEngine.class);
 
     private final ToolManager toolManager;
+    private final ResourceManager resourceManager;
+    // names of the tools registered by this engine; a refused tool's name 
belongs to an application tool
+    private final Set<String> registeredToolNames = 
ConcurrentHashMap.newKeySet();
+    // uris of the resources registered by this engine; a refused resource's 
uri may belong to an application resource
+    private final Set<String> registeredResourceUris = 
ConcurrentHashMap.newKeySet();
     private CamelContext camelContext;
 
-    public QuarkusMcpServerEngine(ToolManager toolManager) {
+    public QuarkusMcpServerEngine(ToolManager toolManager, ResourceManager 
resourceManager) {
         this.toolManager = toolManager;
+        this.resourceManager = resourceManager;
     }
 
     @Override
@@ -93,20 +112,85 @@ public class QuarkusMcpServerEngine extends ServiceSupport 
implements McpServerE
                     ? ToolResponse.error(result.text())
                     : new ToolResponse(false, List.of(new 
TextContent(result.text())), result.structuredContent(), null);
         });
-        definition.register();
+        try {
+            definition.register();
+        } catch (IllegalArgumentException e) {
+            // the name is taken by an application tool: refuse the tool, like 
the bridge does on a name clash
+            LOG.error("Refusing to expose MCP tool '{}': {}. MCP has a flat 
tool namespace.", tool.name(), e.getMessage());
+            return;
+        }
+        registeredToolNames.add(tool.name());
         LOG.debug("MCP tool added: {}", tool.name());
     }
 
     @Override
     public void toolRemoved(String toolName) {
+        if (!registeredToolNames.remove(toolName)) {
+            // refused in toolAdded: removing by name would delete the 
application tool with the same name
+            return;
+        }
         try {
-            toolManager.removeTool(toolName);
+            toolManager.removeTool(toolName, McpServer.DEFAULT);
             LOG.debug("MCP tool removed: {}", toolName);
         } catch (Exception e) {
             LOG.debug("Failed to remove MCP tool {}: {}", toolName, 
e.getMessage());
         }
     }
 
+    @Override
+    public void resourceAdded(McpServerResource resource) {
+        ResourceManager.ResourceDefinition definition = 
resourceManager.newResource(resource.name())
+                .setUri(resource.uri())
+                .setMimeType(resource.mimeType())
+                .setDescription(resource.description());
+        if (resource.title() != null && !resource.title().isBlank()) {
+            definition.setTitle(resource.title());
+        }
+        definition.setHandler(arguments -> {
+            McpResourceReadResult result = resource.handler().read();
+            if (result.isError()) {
+                // resources/read has no in-band error flag; the message is 
pre-sanitized by the bridge
+                throw new McpException(result.errorMessage(), 
JsonRpcErrorCodes.INTERNAL_ERROR);
+            }
+            // plain Base64 as in the Vert.x engine: 
BlobResourceContents.create(String, byte[]) adds MIME line breaks
+            ResourceContents contents = result.blob() != null
+                    ? new BlobResourceContents(resource.uri(), 
Base64.getEncoder().encodeToString(result.blob()),
+                            resource.mimeType())
+                    : new TextResourceContents(resource.uri(), result.text(), 
resource.mimeType());
+            return new ResourceResponse(List.of(contents));
+        });
+        try {
+            definition.register();
+        } catch (IllegalArgumentException e) {
+            // quarkus-mcp-server also requires unique names: refuse the 
resource, like the bridge does on a uri clash
+            LOG.error("Refusing to expose MCP resource '{}': {}. Resource 
names and uris must be unique.",
+                    resource.uri(), e.getMessage());
+            return;
+        }
+        registeredResourceUris.add(resource.uri());
+        LOG.debug("MCP resource added: {}", resource.uri());
+    }
+
+    @Override
+    public void resourceRemoved(String resourceUri) {
+        if (!registeredResourceUris.remove(resourceUri)) {
+            // refused in resourceAdded: removing by uri would delete another 
resource with the same uri
+            return;
+        }
+        try {
+            // only from the default server, where resourceAdded registered it
+            resourceManager.removeResource(resourceUri, McpServer.DEFAULT);
+            LOG.debug("MCP resource removed: {}", resourceUri);
+        } catch (Exception e) {
+            LOG.debug("Failed to remove MCP resource {}: {}", resourceUri, 
e.getMessage());
+        }
+    }
+
+    @Override
+    public boolean supportsResources() {
+        return true;
+    }
+
     /**
      * Maps the ai-tool annotation hints to the quarkiverse tool definition: 
{@code title} to the tool's top-level
      * title and the boolean hints to {@link ToolManager.ToolAnnotations}. The 
quarkiverse annotations record uses
diff --git 
a/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java
 
b/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/QuarkusMcpServerEngineFactory.java
similarity index 50%
copy from 
integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java
copy to 
extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/QuarkusMcpServerEngineFactory.java
index 88a6cf149f..ccc3d76a05 100644
--- 
a/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java
+++ 
b/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/QuarkusMcpServerEngineFactory.java
@@ -14,20 +14,28 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.camel.quarkus.component.mcp.server.it;
+package org.apache.camel.quarkus.component.mcp.server;
 
-import io.quarkiverse.mcp.server.Tool;
-import io.quarkiverse.mcp.server.ToolArg;
+import io.quarkiverse.mcp.server.ResourceManager;
+import io.quarkiverse.mcp.server.ToolManager;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
 
 /**
- * A plain quarkus-mcp-server annotated tool living on the same MCP server as 
the Camel ai-tool routes.
+ * Creates the {@link QuarkusMcpServerEngine}. The managers are injected 
rather than looked up programmatically:
+ * quarkus-mcp-server advertises the {@code listChanged} capability of tools 
and resources only when their manager has
+ * an injection point, and the Camel tools and resources change with the route 
lifecycle.
  */
-public class QuarkusDefinedTools {
+@Singleton
+public class QuarkusMcpServerEngineFactory {
 
-    @Tool(name = "add_numbers", description = "Add two numbers")
-    String add(
-            @ToolArg(description = "First addend") long a,
-            @ToolArg(description = "Second addend") long b) {
-        return String.valueOf(a + b);
+    @Inject
+    ToolManager toolManager;
+
+    @Inject
+    ResourceManager resourceManager;
+
+    QuarkusMcpServerEngine createEngine() {
+        return new QuarkusMcpServerEngine(toolManager, resourceManager);
     }
 }
diff --git a/extensions/pom.xml b/extensions/pom.xml
index 1b0d97dd64..381984e59e 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -37,6 +37,7 @@
         <module>a2a</module>
         <module>activemq</module>
         <module>activemq6</module>
+        <module>ai-resource</module>
         <module>ai-tool</module>
         <module>amqp</module>
         <module>arangodb</module>
diff --git a/integration-tests/mcp-server/pom.xml 
b/integration-tests/ai-resource/pom.xml
similarity index 73%
copy from integration-tests/mcp-server/pom.xml
copy to integration-tests/ai-resource/pom.xml
index fead80ad89..2f6ec263a6 100644
--- a/integration-tests/mcp-server/pom.xml
+++ b/integration-tests/ai-resource/pom.xml
@@ -17,7 +17,9 @@
     limitations under the License.
 
 -->
-<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+<project xmlns="http://maven.apache.org/POM/4.0.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.apache.camel.quarkus</groupId>
@@ -26,24 +28,22 @@
         <relativePath>../../poms/build-parent-it/pom.xml</relativePath>
     </parent>
 
-    <artifactId>camel-quarkus-integration-test-mcp-server</artifactId>
-    <name>Camel Quarkus :: Integration Tests :: MCP Server</name>
-    <description>Integration tests for Camel Quarkus MCP Server 
extension</description>
-
-    <properties>
-        <!-- TODO: Re-enable when quarkus-mcp-server is compatible with 
Quarkus 4 - https://github.com/apache/camel-quarkus/issues/9362 -->
-        <skipTests>true</skipTests>
-        <quarkus.build.skip>true</quarkus.build.skip>
-    </properties>
+    <artifactId>camel-quarkus-integration-test-ai-resource</artifactId>
+    <name>Camel Quarkus :: Integration Tests :: AI Resource</name>
+    <description>Integration tests for Camel Quarkus AI Resource 
extension</description>
 
     <dependencies>
         <dependency>
             <groupId>org.apache.camel.quarkus</groupId>
-            <artifactId>camel-quarkus-mcp-server</artifactId>
+            <artifactId>camel-quarkus-ai-resource</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel.quarkus</groupId>
+            <artifactId>camel-quarkus-file</artifactId>
         </dependency>
         <dependency>
             <groupId>org.apache.camel.quarkus</groupId>
-            <artifactId>camel-quarkus-ai-tool</artifactId>
+            <artifactId>camel-quarkus-jackson</artifactId>
         </dependency>
         <dependency>
             <groupId>io.quarkus</groupId>
@@ -51,21 +51,6 @@
         </dependency>
 
         <!-- test dependencies -->
-        <dependency>
-            <groupId>io.quarkiverse.mcp</groupId>
-            <artifactId>quarkus-mcp-server-test</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.awaitility</groupId>
-            <artifactId>awaitility</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.assertj</groupId>
-            <artifactId>assertj-core</artifactId>
-            <scope>test</scope>
-        </dependency>
         <dependency>
             <groupId>io.quarkus</groupId>
             <artifactId>quarkus-junit</artifactId>
@@ -100,9 +85,6 @@
                                     <goal>integration-test</goal>
                                     <goal>verify</goal>
                                 </goals>
-                                <configuration>
-                                    
<rerunFailingTestsCount>${rerun.failing.test.count}</rerunFailingTestsCount>
-                                </configuration>
                             </execution>
                         </executions>
                     </plugin>
@@ -120,7 +102,7 @@
                 <!-- The following dependencies guarantee that this module is 
built after them. You can update them by running `mvn process-resources 
-Pformat -N` from the source tree root directory -->
                 <dependency>
                     <groupId>org.apache.camel.quarkus</groupId>
-                    <artifactId>camel-quarkus-ai-tool-deployment</artifactId>
+                    
<artifactId>camel-quarkus-ai-resource-deployment</artifactId>
                     <version>${project.version}</version>
                     <type>pom</type>
                     <scope>test</scope>
@@ -133,7 +115,20 @@
                 </dependency>
                 <dependency>
                     <groupId>org.apache.camel.quarkus</groupId>
-                    
<artifactId>camel-quarkus-mcp-server-deployment</artifactId>
+                    <artifactId>camel-quarkus-file-deployment</artifactId>
+                    <version>${project.version}</version>
+                    <type>pom</type>
+                    <scope>test</scope>
+                    <exclusions>
+                        <exclusion>
+                            <groupId>*</groupId>
+                            <artifactId>*</artifactId>
+                        </exclusion>
+                    </exclusions>
+                </dependency>
+                <dependency>
+                    <groupId>org.apache.camel.quarkus</groupId>
+                    <artifactId>camel-quarkus-jackson-deployment</artifactId>
                     <version>${project.version}</version>
                     <type>pom</type>
                     <scope>test</scope>
@@ -147,4 +142,5 @@
             </dependencies>
         </profile>
     </profiles>
+
 </project>
diff --git 
a/integration-tests/ai-resource/src/main/java/org/apache/camel/quarkus/component/ai/resource/it/AiResourceResource.java
 
b/integration-tests/ai-resource/src/main/java/org/apache/camel/quarkus/component/ai/resource/it/AiResourceResource.java
new file mode 100644
index 0000000000..4f10c53b4d
--- /dev/null
+++ 
b/integration-tests/ai-resource/src/main/java/org/apache/camel/quarkus/component/ai/resource/it/AiResourceResource.java
@@ -0,0 +1,121 @@
+/*
+ * 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.
+ */
+package org.apache.camel.quarkus.component.ai.resource.it;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.component.ai.resource.AiResourceExecutor;
+import org.apache.camel.component.ai.resource.AiResourceRegistry;
+import org.apache.camel.component.ai.resource.AiResourceResult;
+import org.apache.camel.component.ai.resource.AiResourceSpec;
+import org.apache.camel.support.DefaultExchange;
+
+@Path("/ai-resource")
+@ApplicationScoped
+public class AiResourceResource {
+
+    @Inject
+    CamelContext camelContext;
+
+    @Path("/resources/tag/{tag}")
+    @GET
+    @Produces(MediaType.TEXT_PLAIN)
+    public String getResourcesByTag(@PathParam("tag") String tag) {
+        AiResourceRegistry registry = 
AiResourceRegistry.getOrCreate(camelContext);
+        Set<AiResourceSpec> resources = registry.getResourcesByTag(tag);
+        return resources.stream()
+                .map(AiResourceSpec::getName)
+                .sorted()
+                .collect(Collectors.joining(","));
+    }
+
+    @Path("/resources/all")
+    @GET
+    @Produces(MediaType.TEXT_PLAIN)
+    public String getAllResources() {
+        AiResourceRegistry registry = 
AiResourceRegistry.getOrCreate(camelContext);
+        Set<AiResourceSpec> resources = registry.getAllResources();
+        return resources.stream()
+                .map(AiResourceSpec::getName)
+                .sorted()
+                .collect(Collectors.joining(","));
+    }
+
+    @Path("/resources/{resourceName}/description")
+    @GET
+    @Produces(MediaType.TEXT_PLAIN)
+    public String getResourceDescription(@PathParam("resourceName") String 
resourceName) {
+        AiResourceSpec spec = findResource(resourceName);
+        if (spec == null) {
+            return "NOT_FOUND";
+        }
+        return spec.getDescription();
+    }
+
+    @Path("/read/{resourceName}")
+    @GET
+    public Response readResource(@PathParam("resourceName") String 
resourceName) {
+        AiResourceSpec spec = findResource(resourceName);
+        if (spec == null) {
+            return Response.status(404).entity("Resource not found: " + 
resourceName).build();
+        }
+
+        Exchange exchange = new DefaultExchange(camelContext);
+        AiResourceResult result = AiResourceExecutor.execute(spec, exchange);
+
+        if (result instanceof AiResourceResult.Text t) {
+            return Response.ok(t.value(), spec.getMimeType()).build();
+        } else if (result instanceof AiResourceResult.Binary b) {
+            return Response.ok(b.value(), spec.getMimeType()).build();
+        } else if (result instanceof AiResourceResult.ExecutionError e) {
+            return Response.status(500).entity(e.message()).build();
+        }
+        return Response.status(500).entity("Unknown result type").build();
+    }
+
+    @Path("/notes")
+    @PUT
+    @Consumes(MediaType.TEXT_PLAIN)
+    public void writeNotes(String content) throws IOException {
+        Files.createDirectories(Paths.get(AiResourceRoutes.NOTES_DIR));
+        Files.writeString(Paths.get(AiResourceRoutes.NOTES_DIR, "notes.txt"), 
content);
+    }
+
+    private AiResourceSpec findResource(String resourceName) {
+        AiResourceRegistry registry = 
AiResourceRegistry.getOrCreate(camelContext);
+        return registry.getAllResources().stream()
+                .filter(s -> s.getName().equals(resourceName))
+                .findFirst()
+                .orElse(null);
+    }
+}
diff --git 
a/integration-tests/ai-resource/src/main/java/org/apache/camel/quarkus/component/ai/resource/it/AiResourceRoutes.java
 
b/integration-tests/ai-resource/src/main/java/org/apache/camel/quarkus/component/ai/resource/it/AiResourceRoutes.java
new file mode 100644
index 0000000000..514940b4ab
--- /dev/null
+++ 
b/integration-tests/ai-resource/src/main/java/org/apache/camel/quarkus/component/ai/resource/it/AiResourceRoutes.java
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+package org.apache.camel.quarkus.component.ai.resource.it;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.model.dataformat.JsonLibrary;
+
+public class AiResourceRoutes extends RouteBuilder {
+    public static final String NOTES_DIR = "target/test-files";
+
+    @Override
+    public void configure() {
+        
from("ai-resource:appConfig?resourceUri=camel:///config/app.json&tags=config"
+                + "&description=Current application configuration"
+                + "&mimeType=application/json")
+                .setBody(constant("{\"env\":\"test\"}"));
+
+        
from("ai-resource:latestReport?resourceUri=camel:///reports/latest.pdf&tags=reports"
+                + "&description=Latest sales report"
+                + "&mimeType=application/pdf")
+                .setBody(constant(new byte[] { 0x25, 0x50, 0x44, 0x46 }));
+
+        from("ai-resource:readme?resourceUri=camel:///readme.txt"
+                + "&description=Project readme")
+                .setBody(constant("Read me"));
+
+        from("ai-resource:failingResource?resourceUri=camel:///fail&tags=test"
+                + "&description=A resource that always fails")
+                .throwException(new IllegalStateException("Intentional 
failure"));
+
+        // the file is polled on every read; idempotent=false keeps it 
readable more than once
+        from("ai-resource:notes?resourceUri=camel:///notes.txt&tags=files"
+                + "&description=Notes read from a file"
+                + "&mimeType=text/plain")
+                .pollEnrich("file:" + NOTES_DIR + 
"?fileName=notes.txt&noop=true&idempotent=false", 1000);
+
+        
from("ai-resource:latestOrder?resourceUri=camel:///orders/latest.json&tags=orders"
+                + "&description=Latest order"
+                + "&mimeType=application/json")
+                .setBody(constant(new Order("A-1", 2)))
+                .marshal().json(JsonLibrary.Jackson);
+    }
+}
diff --git 
a/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java
 
b/integration-tests/ai-resource/src/main/java/org/apache/camel/quarkus/component/ai/resource/it/Order.java
similarity index 60%
copy from 
integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java
copy to 
integration-tests/ai-resource/src/main/java/org/apache/camel/quarkus/component/ai/resource/it/Order.java
index 88a6cf149f..e536419756 100644
--- 
a/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java
+++ 
b/integration-tests/ai-resource/src/main/java/org/apache/camel/quarkus/component/ai/resource/it/Order.java
@@ -14,20 +14,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.camel.quarkus.component.mcp.server.it;
+package org.apache.camel.quarkus.component.ai.resource.it;
 
-import io.quarkiverse.mcp.server.Tool;
-import io.quarkiverse.mcp.server.ToolArg;
+import io.quarkus.runtime.annotations.RegisterForReflection;
 
-/**
- * A plain quarkus-mcp-server annotated tool living on the same MCP server as 
the Camel ai-tool routes.
- */
-public class QuarkusDefinedTools {
-
-    @Tool(name = "add_numbers", description = "Add two numbers")
-    String add(
-            @ToolArg(description = "First addend") long a,
-            @ToolArg(description = "Second addend") long b) {
-        return String.valueOf(a + b);
-    }
+@RegisterForReflection
+public record Order(String id, int quantity) {
 }
diff --git 
a/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java
 
b/integration-tests/ai-resource/src/test/java/org/apache/camel/quarkus/component/ai/resource/it/AiResourceIT.java
similarity index 60%
copy from 
integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java
copy to 
integration-tests/ai-resource/src/test/java/org/apache/camel/quarkus/component/ai/resource/it/AiResourceIT.java
index 88a6cf149f..28e9a7034f 100644
--- 
a/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java
+++ 
b/integration-tests/ai-resource/src/test/java/org/apache/camel/quarkus/component/ai/resource/it/AiResourceIT.java
@@ -14,20 +14,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.camel.quarkus.component.mcp.server.it;
+package org.apache.camel.quarkus.component.ai.resource.it;
 
-import io.quarkiverse.mcp.server.Tool;
-import io.quarkiverse.mcp.server.ToolArg;
+import io.quarkus.test.junit.QuarkusIntegrationTest;
 
-/**
- * A plain quarkus-mcp-server annotated tool living on the same MCP server as 
the Camel ai-tool routes.
- */
-public class QuarkusDefinedTools {
+@QuarkusIntegrationTest
+class AiResourceIT extends AiResourceTest {
 
-    @Tool(name = "add_numbers", description = "Add two numbers")
-    String add(
-            @ToolArg(description = "First addend") long a,
-            @ToolArg(description = "Second addend") long b) {
-        return String.valueOf(a + b);
-    }
 }
diff --git 
a/integration-tests/ai-resource/src/test/java/org/apache/camel/quarkus/component/ai/resource/it/AiResourceTest.java
 
b/integration-tests/ai-resource/src/test/java/org/apache/camel/quarkus/component/ai/resource/it/AiResourceTest.java
new file mode 100644
index 0000000000..feb9d0e622
--- /dev/null
+++ 
b/integration-tests/ai-resource/src/test/java/org/apache/camel/quarkus/component/ai/resource/it/AiResourceTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.
+ */
+package org.apache.camel.quarkus.component.ai.resource.it;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@QuarkusTest
+class AiResourceTest {
+
+    @Test
+    void resourceRegistrationByTag() {
+        RestAssured.get("/ai-resource/resources/tag/config")
+                .then()
+                .statusCode(200)
+                .body(containsString("appConfig"));
+
+        RestAssured.get("/ai-resource/resources/tag/reports")
+                .then()
+                .statusCode(200)
+                .body(containsString("latestReport"));
+    }
+
+    @Test
+    void taglessResourceInDefaultPool() {
+        RestAssured.get("/ai-resource/resources/all")
+                .then()
+                .statusCode(200)
+                .body(containsString("readme"));
+    }
+
+    @Test
+    void defaultPoolMergedWithTaggedResources() {
+        RestAssured.get("/ai-resource/resources/tag/config")
+                .then()
+                .statusCode(200)
+                .body(containsString("readme"));
+    }
+
+    @Test
+    void allResourcesReturnsEveryRegisteredResource() {
+        String resources = RestAssured.get("/ai-resource/resources/all")
+                .then()
+                .statusCode(200)
+                .extract().asString();
+
+        assertTrue(resources.contains("appConfig"), "Expected appConfig in: " 
+ resources);
+        assertTrue(resources.contains("latestReport"), "Expected latestReport 
in: " + resources);
+        assertTrue(resources.contains("readme"), "Expected readme in: " + 
resources);
+        assertTrue(resources.contains("failingResource"), "Expected 
failingResource in: " + resources);
+    }
+
+    @Test
+    void resourceDescription() {
+        RestAssured.get("/ai-resource/resources/appConfig/description")
+                .then()
+                .statusCode(200)
+                .body(is("Current application configuration"));
+    }
+
+    @Test
+    void readTextResource() {
+        RestAssured.get("/ai-resource/read/appConfig")
+                .then()
+                .statusCode(200)
+                .body(is("{\"env\":\"test\"}"));
+    }
+
+    @Test
+    void readBinaryResource() {
+        byte[] content = RestAssured.get("/ai-resource/read/latestReport")
+                .then()
+                .statusCode(200)
+                .extract().asByteArray();
+
+        assertArrayEquals(new byte[] { 0x25, 0x50, 0x44, 0x46 }, content);
+    }
+
+    @Test
+    void readFileResourceOnEveryRead() {
+        writeNotes("first version");
+        RestAssured.get("/ai-resource/read/notes")
+                .then()
+                .statusCode(200)
+                .body(is("first version"));
+
+        // the route reads the file again, nothing is cached
+        writeNotes("second version");
+        RestAssured.get("/ai-resource/read/notes")
+                .then()
+                .statusCode(200)
+                .body(is("second version"));
+    }
+
+    @Test
+    void readJsonResourceMarshalledByJackson() {
+        RestAssured.get("/ai-resource/read/latestOrder")
+                .then()
+                .statusCode(200)
+                .contentType(ContentType.JSON)
+                .body("id", is("A-1"), "quantity", is(2));
+    }
+
+    @Test
+    void readFailingResource() {
+        RestAssured.get("/ai-resource/read/failingResource")
+                .then()
+                .statusCode(500);
+    }
+
+    @Test
+    void readNonExistentResource() {
+        RestAssured.get("/ai-resource/read/nonExistent")
+                .then()
+                .statusCode(404);
+    }
+
+    private static void writeNotes(String content) {
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body(content)
+                .put("/ai-resource/notes")
+                .then()
+                .statusCode(204);
+    }
+}
diff --git a/integration-tests/mcp-server/pom.xml 
b/integration-tests/mcp-server/pom.xml
index fead80ad89..5c8f4463a0 100644
--- a/integration-tests/mcp-server/pom.xml
+++ b/integration-tests/mcp-server/pom.xml
@@ -41,10 +41,18 @@
             <groupId>org.apache.camel.quarkus</groupId>
             <artifactId>camel-quarkus-mcp-server</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.camel.quarkus</groupId>
+            <artifactId>camel-quarkus-ai-resource</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.apache.camel.quarkus</groupId>
             <artifactId>camel-quarkus-ai-tool</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.camel.quarkus</groupId>
+            <artifactId>camel-quarkus-jackson</artifactId>
+        </dependency>
         <dependency>
             <groupId>io.quarkus</groupId>
             <artifactId>quarkus-resteasy</artifactId>
@@ -118,6 +126,19 @@
             </activation>
             <dependencies>
                 <!-- The following dependencies guarantee that this module is 
built after them. You can update them by running `mvn process-resources 
-Pformat -N` from the source tree root directory -->
+                <dependency>
+                    <groupId>org.apache.camel.quarkus</groupId>
+                    
<artifactId>camel-quarkus-ai-resource-deployment</artifactId>
+                    <version>${project.version}</version>
+                    <type>pom</type>
+                    <scope>test</scope>
+                    <exclusions>
+                        <exclusion>
+                            <groupId>*</groupId>
+                            <artifactId>*</artifactId>
+                        </exclusion>
+                    </exclusions>
+                </dependency>
                 <dependency>
                     <groupId>org.apache.camel.quarkus</groupId>
                     <artifactId>camel-quarkus-ai-tool-deployment</artifactId>
@@ -131,6 +152,19 @@
                         </exclusion>
                     </exclusions>
                 </dependency>
+                <dependency>
+                    <groupId>org.apache.camel.quarkus</groupId>
+                    <artifactId>camel-quarkus-jackson-deployment</artifactId>
+                    <version>${project.version}</version>
+                    <type>pom</type>
+                    <scope>test</scope>
+                    <exclusions>
+                        <exclusion>
+                            <groupId>*</groupId>
+                            <artifactId>*</artifactId>
+                        </exclusion>
+                    </exclusions>
+                </dependency>
                 <dependency>
                     <groupId>org.apache.camel.quarkus</groupId>
                     
<artifactId>camel-quarkus-mcp-server-deployment</artifactId>
diff --git 
a/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerResource.java
 
b/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerResource.java
index 1012e173fa..fb724ec09a 100644
--- 
a/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerResource.java
+++ 
b/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerResource.java
@@ -16,9 +16,15 @@
  */
 package org.apache.camel.quarkus.component.mcp.server.it;
 
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
 import jakarta.inject.Inject;
+import jakarta.ws.rs.Consumes;
 import jakarta.ws.rs.GET;
 import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
 import jakarta.ws.rs.Path;
 import jakarta.ws.rs.PathParam;
 import jakarta.ws.rs.Produces;
@@ -49,4 +55,12 @@ public class McpServerResource {
     public String routeStatus(@PathParam("routeId") String routeId) {
         return 
camelContext.getRouteController().getRouteStatus(routeId).name();
     }
+
+    @Path("/notes")
+    @PUT
+    @Consumes(MediaType.TEXT_PLAIN)
+    public void writeNotes(String content) throws IOException {
+        Files.createDirectories(Paths.get(McpServerRoutes.NOTES_DIR));
+        Files.writeString(Paths.get(McpServerRoutes.NOTES_DIR, "notes.txt"), 
content);
+    }
 }
diff --git 
a/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerRoutes.java
 
b/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerRoutes.java
index b3aa7354e7..b8c1ab69ee 100644
--- 
a/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerRoutes.java
+++ 
b/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerRoutes.java
@@ -16,15 +16,24 @@
  */
 package org.apache.camel.quarkus.component.mcp.server.it;
 
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.util.List;
 import java.util.Map;
 
 import jakarta.enterprise.context.ApplicationScoped;
 import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.model.dataformat.JsonLibrary;
 
 @ApplicationScoped
 public class McpServerRoutes extends RouteBuilder {
 
+    public static final String NOTES_DIR = "target/test-files";
+    // over 57 bytes, so that a MIME Base64 encoder would break the line
+    public static final byte[] REPORT = "%PDF-1.7 latest report, long enough 
to span more than one MIME Base64 line"
+            .getBytes(StandardCharsets.US_ASCII);
+
     @Override
     public void configure() {
         from("ai-tool:say_hello?tags=conformance&description=Say hello"
@@ -37,8 +46,9 @@ public class McpServerRoutes extends RouteBuilder {
                     throw new IllegalStateException("secret internal detail");
                 });
 
+        // between tool-timeout and resource-timeout, like delayed_resource
         from("ai-tool:slow_tool?tags=conformance&description=Exceeds the tool 
timeout")
-                .delay(6000)
+                .delay(4000)
                 .setBody(constant("done"));
 
         from("ai-tool:hidden_tool?description=Untagged tool, must not be 
exposed")
@@ -69,5 +79,76 @@ public class McpServerRoutes extends RouteBuilder {
                 + 
"&outputParameter.temperature=number&outputParameter.temperature.required=true"
                 + 
"&outputParameter.unit=string&outputParameter.unit.enum=celsius,fahrenheit")
                 
.setBody(constant("{\"temperature\":21.5,\"unit\":\"celsius\"}"));
+
+        // the name is taken by the tool registered in QuarkusDefinedTools, so 
it is refused once started
+        from("ai-tool:user_tool?tags=conformance"
+                + "&description=Name already taken by an application tool, 
must be refused")
+                .routeId("clashing-tool-route")
+                .autoStartup(false)
+                .setBody(constant("camel"));
+
+        // the name is taken by the annotated tool in QuarkusDefinedTools; 
started with the application, so it is
+        // refused while the MCP bridge starts, which must not fail the 
application startup
+        from("ai-tool:add_numbers?tags=conformance"
+                + "&description=Name already taken by an annotated tool, must 
be refused")
+                .setBody(constant("camel"));
+
+        
from("ai-resource:app_config?resourceUri=camel:///config/app.json&tags=conformance"
+                + "&description=Application 
configuration&mimeType=application/json&title=App config")
+                .routeId("app-config-route")
+                .setBody(constant("{\"env\":\"test\"}"));
+
+        
from("ai-resource:latest_report?resourceUri=camel:///reports/latest.pdf&tags=conformance"
+                + "&description=Latest report&mimeType=application/pdf")
+                .setBody(constant(REPORT));
+
+        // an InputStream body, as returned by producers such as aws2-s3 
getObject; the file is opened on every read
+        
from("ai-resource:notes?resourceUri=camel:///notes.txt&tags=conformance"
+                + "&description=Notes read from a file&mimeType=text/plain")
+                .process(e -> 
e.getMessage().setBody(Files.newInputStream(Paths.get(NOTES_DIR, 
"notes.txt"))));
+
+        
from("ai-resource:latest_order?resourceUri=camel:///orders/latest.json&tags=conformance"
+                + "&description=Latest order&mimeType=application/json")
+                .setBody(constant(new Order("A-1", 2)))
+                .marshal().json(JsonLibrary.Jackson);
+
+        
from("ai-resource:fail_resource?resourceUri=camel:///fail&tags=conformance&description=Always
 fails")
+                .process(e -> {
+                    throw new IllegalStateException("secret internal detail");
+                });
+
+        
from("ai-resource:slow_resource?resourceUri=camel:///slow&tags=conformance"
+                + "&description=Exceeds the resource timeout")
+                .delay(8000)
+                .setBody(constant("done"));
+
+        // between tool-timeout and resource-timeout, like slow_tool
+        
from("ai-resource:delayed_resource?resourceUri=camel:///delayed&tags=conformance"
+                + "&description=Exceeds the tool timeout, not the resource 
timeout")
+                .delay(3000)
+                .setBody(constant("delayed"));
+
+        from("ai-resource:hidden_resource?resourceUri=camel:///hidden"
+                + "&description=Untagged resource, must not be exposed")
+                .setBody(constant("hidden"));
+
+        
from("ai-resource:other_resource?resourceUri=camel:///other&tags=untrusted"
+                + "&description=Not a selected tag, must not be exposed")
+                .setBody(constant("other"));
+
+        
from("ai-resource:same_name?resourceUri=camel:///same/first&tags=conformance"
+                + "&description=First resource named same_name")
+                .setBody(constant("first"));
+
+        
from("ai-resource:same_name?resourceUri=camel:///same/second&tags=conformance"
+                + "&description=Name already taken, must be refused")
+                .setBody(constant("second"));
+
+        // the uri is taken by the resource registered in 
QuarkusDefinedResources, so it is refused once started
+        
from("ai-resource:clashing_resource?resourceUri=camel:///user/clash&tags=conformance"
+                + "&description=Uri already taken by an application resource, 
must be refused")
+                .routeId("clashing-resource-route")
+                .autoStartup(false)
+                .setBody(constant("camel"));
     }
 }
diff --git 
a/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java
 
b/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/Order.java
similarity index 64%
copy from 
integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java
copy to 
integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/Order.java
index 88a6cf149f..8a45082ba7 100644
--- 
a/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java
+++ 
b/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/Order.java
@@ -16,18 +16,8 @@
  */
 package org.apache.camel.quarkus.component.mcp.server.it;
 
-import io.quarkiverse.mcp.server.Tool;
-import io.quarkiverse.mcp.server.ToolArg;
+import io.quarkus.runtime.annotations.RegisterForReflection;
 
-/**
- * A plain quarkus-mcp-server annotated tool living on the same MCP server as 
the Camel ai-tool routes.
- */
-public class QuarkusDefinedTools {
-
-    @Tool(name = "add_numbers", description = "Add two numbers")
-    String add(
-            @ToolArg(description = "First addend") long a,
-            @ToolArg(description = "Second addend") long b) {
-        return String.valueOf(a + b);
-    }
+@RegisterForReflection
+public record Order(String id, int quantity) {
 }
diff --git 
a/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedResources.java
 
b/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedResources.java
new file mode 100644
index 0000000000..6353dc7c16
--- /dev/null
+++ 
b/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedResources.java
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+package org.apache.camel.quarkus.component.mcp.server.it;
+
+import java.util.List;
+
+import io.quarkiverse.mcp.server.ResourceManager;
+import io.quarkiverse.mcp.server.ResourceResponse;
+import io.quarkiverse.mcp.server.TextResourceContents;
+import io.quarkus.arc.Arc;
+import io.quarkus.runtime.StartupEvent;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.event.Observes;
+
+/**
+ * Resources registered by the application itself through the 
quarkus-mcp-server {@link ResourceManager}: one at the
+ * same uri as the clashing-resource-route ai-resource route, and one at the 
uri of the app-config-route ai-resource
+ * route, on another MCP server.
+ */
+@ApplicationScoped
+public class QuarkusDefinedResources {
+
+    void registerResources(@Observes StartupEvent event) {
+        // looked up, not injected: any ResourceManager injection point makes 
quarkus-mcp-server advertise
+        // resources listChanged, which would hide a missing injection in 
QuarkusMcpServerEngineFactory
+        ResourceManager resourceManager = 
Arc.container().instance(ResourceManager.class).get();
+        resourceManager.newResource("user_resource")
+                .setUri("camel:///user/clash")
+                .setDescription("Resource registered by the application")
+                .setHandler(arguments -> new ResourceResponse(
+                        
List.of(TextResourceContents.create(arguments.requestUri().value(), "user"))))
+                .register();
+        resourceManager.newResource("other_config")
+                .setUri("camel:///config/app.json")
+                .setServerName("other")
+                .setDescription("Resource registered by the application on 
another server")
+                .setHandler(arguments -> new ResourceResponse(
+                        
List.of(TextResourceContents.create(arguments.requestUri().value(), "other"))))
+                .register();
+    }
+}
diff --git 
a/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java
 
b/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java
index 88a6cf149f..d863773a87 100644
--- 
a/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java
+++ 
b/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/QuarkusDefinedTools.java
@@ -18,9 +18,16 @@ package org.apache.camel.quarkus.component.mcp.server.it;
 
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
+import io.quarkiverse.mcp.server.ToolManager;
+import io.quarkiverse.mcp.server.ToolResponse;
+import io.quarkus.arc.Arc;
+import io.quarkus.runtime.StartupEvent;
+import jakarta.enterprise.event.Observes;
 
 /**
- * A plain quarkus-mcp-server annotated tool living on the same MCP server as 
the Camel ai-tool routes.
+ * A plain quarkus-mcp-server annotated tool living on the same MCP server as 
the Camel ai-tool routes, and tools
+ * registered through the quarkus-mcp-server {@link ToolManager}: one under 
the same name as the clashing-tool-route
+ * ai-tool route, and one under the name of the say-hello-route ai-tool route, 
on another MCP server.
  */
 public class QuarkusDefinedTools {
 
@@ -30,4 +37,18 @@ public class QuarkusDefinedTools {
             @ToolArg(description = "Second addend") long b) {
         return String.valueOf(a + b);
     }
+
+    void registerTools(@Observes StartupEvent event) {
+        // looked up, not injected, as in QuarkusDefinedResources
+        ToolManager toolManager = 
Arc.container().instance(ToolManager.class).get();
+        toolManager.newTool("user_tool")
+                .setDescription("Tool registered by the application")
+                .setHandler(arguments -> ToolResponse.success("user"))
+                .register();
+        toolManager.newTool("say_hello")
+                .setServerName("other")
+                .setDescription("Tool registered by the application on another 
server")
+                .setHandler(arguments -> ToolResponse.success("other"))
+                .register();
+    }
 }
diff --git 
a/integration-tests/mcp-server/src/main/resources/application.properties 
b/integration-tests/mcp-server/src/main/resources/application.properties
index 11d2cf0016..1b1a7a1526 100644
--- a/integration-tests/mcp-server/src/main/resources/application.properties
+++ b/integration-tests/mcp-server/src/main/resources/application.properties
@@ -16,5 +16,9 @@
 ## ---------------------------------------------------------------------------
 quarkus.camel.mcp-server.tags=conformance
 quarkus.camel.mcp-server.tool-timeout=2000
+# differs from tool-timeout, so that mixing up the two options fails a test
+quarkus.camel.mcp-server.resource-timeout=6000
+# a second MCP server, serving an application tool and resource under the name 
and uri of Camel ones
+quarkus.mcp.server.other.http.root-path=/other-mcp
 # the argSchema and outputSchema resources must be reachable in native mode
 
quarkus.native.resources.includes=schema/create-order.json,schema/order-status.json
diff --git 
a/integration-tests/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java
 
b/integration-tests/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java
index 67fc11dc10..54eda40360 100644
--- 
a/integration-tests/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java
+++ 
b/integration-tests/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java
@@ -17,19 +17,27 @@
 package org.apache.camel.quarkus.component.mcp.server.it;
 
 import java.net.URI;
+import java.util.Base64;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
+import io.quarkiverse.mcp.server.BlobResourceContents;
+import io.quarkiverse.mcp.server.JsonRpcErrorCodes;
 import io.quarkiverse.mcp.server.TextContent;
+import io.quarkiverse.mcp.server.TextResourceContents;
 import io.quarkiverse.mcp.server.ToolResponse;
 import io.quarkiverse.mcp.server.test.McpAssured;
 import io.quarkiverse.mcp.server.test.McpAssured.McpStreamableTestClient;
+import io.quarkiverse.mcp.server.test.McpAssured.ResourceInfo;
+import io.quarkiverse.mcp.server.test.McpAssured.ResourcesPage;
+import io.quarkiverse.mcp.server.test.McpAssured.ServerCapability;
 import io.quarkiverse.mcp.server.test.McpAssured.ToolInfo;
 import io.quarkiverse.mcp.server.test.McpAssured.ToolsPage;
 import io.quarkus.test.junit.QuarkusTest;
 import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
 import io.vertx.core.json.JsonArray;
 import io.vertx.core.json.JsonObject;
 import org.eclipse.microprofile.config.ConfigProvider;
@@ -50,6 +58,7 @@ import static org.awaitility.Awaitility.await;
 class McpServerTest {
 
     private McpStreamableTestClient client;
+    private McpStreamableTestClient otherClient;
 
     private McpStreamableTestClient client() {
         if (client == null) {
@@ -62,12 +71,29 @@ class McpServerTest {
         return client;
     }
 
+    // a client of the other MCP server configured in application.properties
+    private McpStreamableTestClient otherClient() {
+        if (otherClient == null) {
+            otherClient = McpAssured.newStreamableClient()
+                    .setBaseUri(URI.create("http://localhost:%d".formatted(
+                            
ConfigProvider.getConfig().getValue("quarkus.http.test-port", Integer.class))))
+                    .setMcpPath("/other-mcp")
+                    .build()
+                    .connect();
+        }
+        return otherClient;
+    }
+
     @AfterEach
     void closeClient() {
         if (client != null) {
             client.disconnect();
             client = null;
         }
+        if (otherClient != null) {
+            otherClient.disconnect();
+            otherClient = null;
+        }
     }
 
     @Test
@@ -169,7 +195,7 @@ class McpServerTest {
 
     @Test
     void testQuarkusAnnotatedToolsCoexistWithCamelTools() {
-        // both tool sources are served by the same MCP server
+        // both tool sources are served by the same MCP server; the 
add_numbers ai-tool route is refused
         client().when()
                 .toolsList(page -> 
assertThat(toolNames(page)).contains("add_numbers", "say_hello"))
                 .toolsCall("add_numbers", Map.of("a", 17, "b", 25), response 
-> {
@@ -212,37 +238,254 @@ class McpServerTest {
                 .thenAssertResults();
     }
 
+    @Test
+    void testStoppingRefusedToolKeepsApplicationTool() {
+        // the route is refused, its name is taken by a tool the application 
registered itself
+        controlRoute("clashing-tool-route", "start");
+        assertThat(routeStatus("clashing-tool-route")).isEqualTo("Started");
+        controlRoute("clashing-tool-route", "stop");
+        assertThat(routeStatus("clashing-tool-route")).isEqualTo("Stopped");
+
+        client().when()
+                .toolsCall("user_tool", response -> 
assertThat(textOf(response)).isEqualTo("user"))
+                .thenAssertResults();
+    }
+
+    @Test
+    void testStoppingToolRouteKeepsToolOfOtherServer() {
+        // the application registered a tool with the same name on another MCP 
server
+        controlRoute("say-hello-route", "stop");
+        try {
+            otherClient().when()
+                    .toolsCall("say_hello", response -> 
assertThat(textOf(response)).isEqualTo("other"))
+                    .thenAssertResults();
+        } finally {
+            controlRoute("say-hello-route", "start");
+        }
+    }
+
     @Test
     void testToolsListReflectsRouteStopAndStart() {
         client().when()
                 .toolsList(page -> 
assertThat(toolNames(page)).contains("say_hello"))
                 .thenAssertResults();
 
-        controlRoute("stop");
-        assertThat(routeStatus()).isEqualTo("Stopped");
+        controlRoute("say-hello-route", "stop");
+        assertThat(routeStatus("say-hello-route")).isEqualTo("Stopped");
         await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> 
client().when()
                 .toolsList(page -> 
assertThat(toolNames(page)).doesNotContain("say_hello"))
                 .thenAssertResults());
 
-        controlRoute("start");
-        assertThat(routeStatus()).isEqualTo("Started");
+        controlRoute("say-hello-route", "start");
+        assertThat(routeStatus("say-hello-route")).isEqualTo("Started");
         await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> 
client().when()
                 .toolsList(page -> 
assertThat(toolNames(page)).contains("say_hello"))
                 .thenAssertResults());
     }
 
-    private static void controlRoute(String action) {
+    // The resource scenarios below mirror 
McpServerResourceConformanceTestSupport from camel-mcp-server-api
+
+    @Test
+    void testServerAdvertisesResourcesCapability() {
+        
assertThat(client().initResult().capabilities()).extracting(ServerCapability::name).contains("resources");
+    }
+
+    @Test
+    void testServerAdvertisesListChangedForToolsAndResources() {
+        // both lists follow the route lifecycle
+        assertThat(client().initResult().capabilities())
+                .filteredOn(capability -> List.of("tools", 
"resources").contains(capability.name()))
+                .hasSize(2)
+                .allSatisfy(capability -> 
assertThat(capability.properties()).containsEntry("listChanged", true));
+    }
+
+    @Test
+    void testListResourcesExposesOnlySelectedTags() {
+        client().when()
+                .resourcesList(page -> {
+                    assertThat(resourceUris(page))
+                            .contains("camel:///config/app.json", 
"camel:///reports/latest.pdf", "camel:///fail",
+                                    "camel:///slow")
+                            .doesNotContain("camel:///hidden", 
"camel:///other");
+
+                    ResourceInfo config = 
page.findByUri("camel:///config/app.json");
+                    assertThat(config.name()).isEqualTo("app_config");
+                    assertThat(config.title()).isEqualTo("App config");
+                    assertThat(config.description()).isEqualTo("Application 
configuration");
+                    
assertThat(config.mimeType()).isEqualTo("application/json");
+                })
+                .thenAssertResults();
+    }
+
+    @Test
+    void testReadTextResource() {
+        client().when()
+                .resourcesRead("camel:///config/app.json", response -> {
+                    assertThat(response.contents()).hasSize(1);
+                    TextResourceContents contents = 
response.contents().get(0).asText();
+                    
assertThat(contents.uri()).isEqualTo("camel:///config/app.json");
+                    
assertThat(contents.mimeType()).isEqualTo("application/json");
+                    
assertThat(contents.text()).isEqualTo("{\"env\":\"test\"}");
+                })
+                .thenAssertResults();
+    }
+
+    @Test
+    void testReadBinaryResource() {
+        client().when()
+                .resourcesRead("camel:///reports/latest.pdf", response -> {
+                    assertThat(response.contents()).hasSize(1);
+                    BlobResourceContents contents = 
response.contents().get(0).asBlob();
+                    
assertThat(contents.mimeType()).isEqualTo("application/pdf");
+                    // the basic decoder rejects the line breaks of a MIME 
encoder
+                    
assertThat(Base64.getDecoder().decode(contents.blob())).isEqualTo(McpServerRoutes.REPORT);
+                })
+                .thenAssertResults();
+    }
+
+    @Test
+    void testReadFileResourceOnEveryRead() {
+        writeNotes("first version");
+        client().when()
+                .resourcesRead("camel:///notes.txt", response -> {
+                    TextResourceContents contents = 
response.contents().get(0).asText();
+                    assertThat(contents.mimeType()).isEqualTo("text/plain");
+                    assertThat(contents.text()).isEqualTo("first version");
+                })
+                .thenAssertResults();
+
+        // the route reads the file again, nothing is cached
+        writeNotes("second version");
+        client().when()
+                .resourcesRead("camel:///notes.txt",
+                        response -> 
assertThat(response.contents().get(0).asText().text()).isEqualTo("second 
version"))
+                .thenAssertResults();
+    }
+
+    @Test
+    void testReadJsonResourceMarshalledByJackson() {
+        client().when()
+                .resourcesRead("camel:///orders/latest.json", response -> {
+                    TextResourceContents contents = 
response.contents().get(0).asText();
+                    
assertThat(contents.mimeType()).isEqualTo("application/json");
+                    JsonObject order = new JsonObject(contents.text());
+                    assertThat(order.getString("id")).isEqualTo("A-1");
+                    assertThat(order.getInteger("quantity")).isEqualTo(2);
+                })
+                .thenAssertResults();
+    }
+
+    @Test
+    void testReadResourceErrorIsSanitized() {
+        client().when()
+                .resourcesRead("camel:///fail")
+                .withErrorAssert(error -> {
+                    
assertThat(error.code()).isEqualTo(JsonRpcErrorCodes.INTERNAL_ERROR);
+                    assertThat(error.message())
+                            .doesNotContain("secret internal detail")
+                            .isEqualTo("Resource read failed");
+                })
+                .send()
+                .thenAssertResults();
+    }
+
+    @Test
+    void testReadResourceTimeout() {
+        client().when()
+                .resourcesRead("camel:///slow")
+                .withErrorAssert(error -> 
assertThat(error.message()).contains("timed out"))
+                .send()
+                .thenAssertResults();
+    }
+
+    @Test
+    void testReadResourceWithinResourceTimeout() {
+        // exceeds tool-timeout, so it times out if the two options are mixed 
up
+        client().when()
+                .resourcesRead("camel:///delayed",
+                        response -> 
assertThat(response.contents().get(0).asText().text()).isEqualTo("delayed"))
+                .thenAssertResults();
+    }
+
+    @Test
+    void testResourceWithDuplicateNameIsRefused() {
+        // quarkus-mcp-server requires unique resource names; the application 
must still start
+        client().when()
+                .resourcesList(page -> assertThat(resourceUris(page))
+                        .contains("camel:///same/first")
+                        .doesNotContain("camel:///same/second"))
+                .thenAssertResults();
+    }
+
+    @Test
+    void testStoppingRefusedResourceKeepsApplicationResource() {
+        // the route is refused, its uri is taken by a resource the 
application registered itself
+        controlRoute("clashing-resource-route", "start");
+        
assertThat(routeStatus("clashing-resource-route")).isEqualTo("Started");
+        controlRoute("clashing-resource-route", "stop");
+        
assertThat(routeStatus("clashing-resource-route")).isEqualTo("Stopped");
+
+        client().when()
+                .resourcesList(page -> 
assertThat(resourceUris(page)).contains("camel:///user/clash"))
+                .resourcesRead("camel:///user/clash",
+                        response -> 
assertThat(response.contents().get(0).asText().text()).isEqualTo("user"))
+                .thenAssertResults();
+    }
+
+    @Test
+    void testStoppingResourceRouteKeepsResourceOfOtherServer() {
+        // the application registered a resource at the same uri on another 
MCP server
+        controlRoute("app-config-route", "stop");
+        try {
+            otherClient().when()
+                    .resourcesRead("camel:///config/app.json",
+                            response -> 
assertThat(response.contents().get(0).asText().text()).isEqualTo("other"))
+                    .thenAssertResults();
+        } finally {
+            controlRoute("app-config-route", "start");
+        }
+    }
+
+    @Test
+    void testResourcesListReflectsRouteStopAndStart() {
+        client().when()
+                .resourcesList(page -> 
assertThat(resourceUris(page)).contains("camel:///config/app.json"))
+                .thenAssertResults();
+
+        controlRoute("app-config-route", "stop");
+        assertThat(routeStatus("app-config-route")).isEqualTo("Stopped");
+        await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> 
client().when()
+                .resourcesList(page -> 
assertThat(resourceUris(page)).doesNotContain("camel:///config/app.json"))
+                .thenAssertResults());
+
+        controlRoute("app-config-route", "start");
+        assertThat(routeStatus("app-config-route")).isEqualTo("Started");
+        await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> 
client().when()
+                .resourcesList(page -> 
assertThat(resourceUris(page)).contains("camel:///config/app.json"))
+                .thenAssertResults());
+    }
+
+    private static void controlRoute(String routeId, String action) {
         RestAssured.given()
                 .when()
-                .post("/mcp-server/route/say-hello-route/" + action)
+                .post("/mcp-server/route/" + routeId + "/" + action)
                 .then()
                 .statusCode(204);
     }
 
-    private static String routeStatus() {
+    private static void writeNotes(String content) {
+        RestAssured.given()
+                .contentType(ContentType.TEXT)
+                .body(content)
+                .put("/mcp-server/notes")
+                .then()
+                .statusCode(204);
+    }
+
+    private static String routeStatus(String routeId) {
         return RestAssured.given()
                 .when()
-                .get("/mcp-server/route/say-hello-route/status")
+                .get("/mcp-server/route/" + routeId + "/status")
                 .then()
                 .statusCode(200)
                 .extract()
@@ -254,6 +497,10 @@ class McpServerTest {
         return page.tools().stream().map(ToolInfo::name).toList();
     }
 
+    private static List<String> resourceUris(ResourcesPage page) {
+        return page.resources().stream().map(ResourceInfo::uri).toList();
+    }
+
     private static ToolInfo toolByName(ToolsPage page, String name) {
         return page.tools().stream()
                 .filter(t -> name.equals(t.name()))
diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml
index 68cd138d83..f137b7be6b 100644
--- a/integration-tests/pom.xml
+++ b/integration-tests/pom.xml
@@ -51,6 +51,7 @@
         <module>a2a</module>
         <module>activemq</module>
         <module>activemq6</module>
+        <module>ai-resource</module>
         <module>ai-tool</module>
         <module>ai-tool-langchain4j</module>
         <module>amqp</module>
diff --git a/poms/bom/pom.xml b/poms/bom/pom.xml
index 781addce2a..9ff58d566f 100644
--- a/poms/bom/pom.xml
+++ b/poms/bom/pom.xml
@@ -3528,6 +3528,16 @@
                 <artifactId>camel-quarkus-activemq6-deployment</artifactId>
                 <version>${camel-quarkus.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.apache.camel.quarkus</groupId>
+                <artifactId>camel-quarkus-ai-resource</artifactId>
+                <version>${camel-quarkus.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.camel.quarkus</groupId>
+                <artifactId>camel-quarkus-ai-resource-deployment</artifactId>
+                <version>${camel-quarkus.version}</version>
+            </dependency>
             <dependency>
                 <groupId>org.apache.camel.quarkus</groupId>
                 <artifactId>camel-quarkus-ai-tool</artifactId>
diff --git a/poms/bom/src/main/generated/flattened-full-pom.xml 
b/poms/bom/src/main/generated/flattened-full-pom.xml
index 107ac1e108..37c22d1909 100644
--- a/poms/bom/src/main/generated/flattened-full-pom.xml
+++ b/poms/bom/src/main/generated/flattened-full-pom.xml
@@ -3433,6 +3433,16 @@
         <artifactId>camel-quarkus-activemq6-deployment</artifactId>
         <version>4.0.0-SNAPSHOT</version>
       </dependency>
+      <dependency>
+        <groupId>org.apache.camel.quarkus</groupId>
+        <artifactId>camel-quarkus-ai-resource</artifactId>
+        <version>4.0.0-SNAPSHOT</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.camel.quarkus</groupId>
+        <artifactId>camel-quarkus-ai-resource-deployment</artifactId>
+        <version>4.0.0-SNAPSHOT</version>
+      </dependency>
       <dependency>
         <groupId>org.apache.camel.quarkus</groupId>
         <artifactId>camel-quarkus-ai-tool</artifactId>
diff --git a/poms/bom/src/main/generated/flattened-reduced-pom.xml 
b/poms/bom/src/main/generated/flattened-reduced-pom.xml
index 44d20d48ba..758f986c42 100644
--- a/poms/bom/src/main/generated/flattened-reduced-pom.xml
+++ b/poms/bom/src/main/generated/flattened-reduced-pom.xml
@@ -4828,6 +4828,16 @@
         <artifactId>camel-quarkus-activemq6-deployment</artifactId>
         <version>4.0.0-SNAPSHOT</version>
       </dependency>
+      <dependency>
+        <groupId>org.apache.camel.quarkus</groupId>
+        <artifactId>camel-quarkus-ai-resource</artifactId>
+        <version>4.0.0-SNAPSHOT</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.camel.quarkus</groupId>
+        <artifactId>camel-quarkus-ai-resource-deployment</artifactId>
+        <version>4.0.0-SNAPSHOT</version>
+      </dependency>
       <dependency>
         <groupId>org.apache.camel.quarkus</groupId>
         <artifactId>camel-quarkus-ai-tool</artifactId>
diff --git a/poms/bom/src/main/generated/flattened-reduced-verbose-pom.xml 
b/poms/bom/src/main/generated/flattened-reduced-verbose-pom.xml
index bed047fa93..989532e4fe 100644
--- a/poms/bom/src/main/generated/flattened-reduced-verbose-pom.xml
+++ b/poms/bom/src/main/generated/flattened-reduced-verbose-pom.xml
@@ -4828,6 +4828,16 @@
         <artifactId>camel-quarkus-activemq6-deployment</artifactId>
         <version>4.0.0-SNAPSHOT</version>
       </dependency>
+      <dependency>
+        <groupId>org.apache.camel.quarkus</groupId>
+        <artifactId>camel-quarkus-ai-resource</artifactId>
+        <version>4.0.0-SNAPSHOT</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.camel.quarkus</groupId>
+        <artifactId>camel-quarkus-ai-resource-deployment</artifactId>
+        <version>4.0.0-SNAPSHOT</version>
+      </dependency>
       <dependency>
         <groupId>org.apache.camel.quarkus</groupId>
         <artifactId>camel-quarkus-ai-tool</artifactId>
diff --git a/tooling/scripts/test-categories.yaml 
b/tooling/scripts/test-categories.yaml
index 9b5d9f8f07..f993067c61 100644
--- a/tooling/scripts/test-categories.yaml
+++ b/tooling/scripts/test-categories.yaml
@@ -232,6 +232,7 @@ group-11:
   - xml-grouped
   - xslt-saxon
 group-12:
+  - ai-resource
   - ai-tool
   - aws2-grouped
   - github2

Reply via email to