This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-jbang-examples.git


The following commit(s) were added to refs/heads/main by this push:
     new 05d730e  CAMEL-24808: rung 8 of the ladder: AI (#85)
05d730e is described below

commit 05d730eced9eb6ad3abe48cee17f4761bc61d23f
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Sep 18 20:50:08 2026 +0200

    CAMEL-24808: rung 8 of the ladder: AI (#85)
    
    Under ai/: mcp-server (new) exposes two routes as MCP tools, stock_level 
and order_status, with nothing but properties to switch the server on; its test 
opens an MCP session over HTTP and calls both tools. langchain4j-chat replaces 
genai-observability, rewritten in YAML: the chat model is a bean built through 
its LangChain4j builder from properties, and a local Ollama model started with 
camel infra writes the shipping notification for each order; the observability 
part becomes a try-c [...]
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
---
 .github/workflows/build.yml                        |   2 +
 ai/docling-langchain4j-rag/README.md               |  53 ++---------
 ai/docling-langchain4j-rag/application.properties  |   2 +-
 ai/docling-langchain4j-rag/compose.yaml            |  45 ---------
 ai/docling-langchain4j-rag/metadata.json           |   3 +-
 .../GenAiObservabilityRoute.java                   |  52 ----------
 ai/genai-observability/README.md                   |  91 ------------------
 ai/genai-observability/application.properties      |   7 --
 ai/genai-observability/metadata.json               |  24 -----
 ai/langchain4j-chat/README.md                      |  86 +++++++++++++++++
 .../application.properties                         |  23 +----
 ai/langchain4j-chat/langchain4j-chat.camel.yaml    |  46 +++++++++
 ai/langchain4j-chat/metadata.json                  |  36 +++++++
 ai/langchain4j-chat/orders/order-1001.json         |   2 +
 ai/langchain4j-chat/orders/order-1002.json         |   2 +
 ai/langchain4j-chat/orders/order-1003.json         |   2 +
 .../test/langchain4j-chat.citrus.it.yaml           |  51 ++++++++++
 ai/mcp-server/README.md                            | 105 +++++++++++++++++++++
 .../application.properties                         |  25 +----
 ai/mcp-server/mcp-server.camel.yaml                |  82 ++++++++++++++++
 ai/mcp-server/metadata.json                        |  33 +++++++
 ai/mcp-server/orders/order-1001.json               |   2 +
 ai/mcp-server/orders/order-1002.json               |   2 +
 ai/mcp-server/orders/order-1003.json               |   2 +
 ai/mcp-server/stock.json                           |   5 +
 ai/mcp-server/test/mcp-server.citrus.it.yaml       |  77 +++++++++++++++
 camel-jbang-example-catalog.json                   |  82 +++++++++++++---
 27 files changed, 622 insertions(+), 320 deletions(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index edf83d2..3eed449 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -76,6 +76,8 @@ jobs:
             path: connect/http-client
           - name: File processing
             path: connect/file-processing
+          - name: MCP server
+            path: ai/mcp-server
           - name: OpenAPI server
             path: contracts/openapi-server
           - name: OpenAPI client
diff --git a/ai/docling-langchain4j-rag/README.md 
b/ai/docling-langchain4j-rag/README.md
index 7d90f85..28b1915 100644
--- a/ai/docling-langchain4j-rag/README.md
+++ b/ai/docling-langchain4j-rag/README.md
@@ -50,7 +50,6 @@ Documents -> Docling (Convert) -> Markdown -> LangChain4j -> 
Ollama (LLM) -> Ana
 docling-langchain4j-rag/
 ├── docling-langchain4j-rag.yaml   # Main YAML configuration
 ├── application.properties          # Configuration settings
-├── compose.yaml                    # Docker Compose for services
 ├── sample.md                       # Sample document (copy to documents/ for 
testing)
 ├── README.md                       # This file
 ├── documents/                      # Input directory (files auto-deleted 
after processing)
@@ -61,51 +60,15 @@ docling-langchain4j-rag/
 
 ### Step 1: Start Required Services
 
-You have two options for running the required services:
-
-#### Option A: Using Docker Compose (Recommended)
-
-Start both Docling and Ollama services:
-
-```sh
-$ docker compose up -d
-```
-
-Pull the Ollama model (first time only):
-
-```sh
-$ docker exec -it ollama ollama pull orca-mini
-```
-
-Verify services are running:
+The Camel CLI starts both services in containers (Docker or Podman must be 
running):
 
 ```sh
-$ curl http://localhost:5001/   # Docling
-$ curl http://localhost:11434/  # Ollama
+$ camel infra run docling ollama
 ```
 
-#### Option B: Using Camel Infra Commands (If Available)
-
-```sh
-# Start Docling (if camel infra supports it)
-$ jbang -Dcamel.jbang.version=4.16.0 camel@apache/camel infra run docling
-
-# Start Ollama (if camel infra supports it)
-$ jbang -Dcamel.jbang.version=4.16.0 camel@apache/camel infra run ollama
-```
-
-#### Option C: Manual Docker Commands
-
-```sh
-# Start Docling-Serve
-$ docker run -d -p 5001:5001 --name docling-serve 
ghcr.io/docling-project/docling-serve:latest
-
-# Start Ollama
-$ docker run -d -p 11434:11434 --name ollama ollama/ollama:latest
-
-# Pull Ollama model
-$ docker exec -it ollama ollama pull orca-mini
-```
+Docling serves on http://localhost:5001 and Ollama on http://localhost:11434, 
where the container pulls the
+`granite4:3b` model on first start; both match `application.properties`. Stop 
them later with
+`camel infra stop docling` and `camel infra stop ollama`.
 
 ### Step 2: Create Required Directories
 
@@ -120,8 +83,7 @@ $ mkdir -p documents output
 ### Step 3: Run the Camel Application
 
 ```sh
-$ jbang -Dcamel.jbang.version=4.16.0 camel@apache/camel run \
-  --fresh \
+$ camel run *
   --dep=camel:docling \
   --dep=camel:langchain4j-chat \
   --dep=camel:platform-http \
@@ -549,7 +511,8 @@ Stop all services:
 
 ```sh
 # Docker Compose
-$ docker compose down
+$ camel infra stop docling
+$ camel infra stop ollama
 
 # Or manual cleanup
 $ docker stop docling-serve ollama
diff --git a/ai/docling-langchain4j-rag/application.properties 
b/ai/docling-langchain4j-rag/application.properties
index 1c416dd..601a6c1 100644
--- a/ai/docling-langchain4j-rag/application.properties
+++ b/ai/docling-langchain4j-rag/application.properties
@@ -29,7 +29,7 @@ docling.serve.url=http://localhost:5001
 
 # Ollama Configuration
 ollama.base.url=http://localhost:11434
-ollama.model.name=orca-mini
+ollama.model.name=granite4:3b
 
 # Batch Processing Configuration
 # Set to -1 to disable batch processing
diff --git a/ai/docling-langchain4j-rag/compose.yaml 
b/ai/docling-langchain4j-rag/compose.yaml
deleted file mode 100644
index 56f4d91..0000000
--- a/ai/docling-langchain4j-rag/compose.yaml
+++ /dev/null
@@ -1,45 +0,0 @@
-# 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.
-
-services:
-  docling-serve:
-    image: ghcr.io/docling-project/docling-serve:latest
-    container_name: docling-serve
-    ports:
-      - "5001:5001"
-    restart: unless-stopped
-    healthcheck:
-      test: ["CMD", "curl", "-f", "http://localhost:5001/";]
-      interval: 30s
-      timeout: 10s
-      retries: 3
-
-  ollama:
-    image: ollama/ollama:latest
-    container_name: ollama-rag
-    ports:
-      - "11435:11434"
-    volumes:
-      - ollama_data:/root/.ollama
-    restart: unless-stopped
-    healthcheck:
-      test: ["CMD", "curl", "-f", "http://localhost:11434/";]
-      interval: 30s
-      timeout: 10s
-      retries: 3
-
-volumes:
-  ollama_data:
-    driver: local
diff --git a/ai/docling-langchain4j-rag/metadata.json 
b/ai/docling-langchain4j-rag/metadata.json
index b3622ce..90d42a3 100644
--- a/ai/docling-langchain4j-rag/metadata.json
+++ b/ai/docling-langchain4j-rag/metadata.json
@@ -31,5 +31,6 @@
     "infraServices": [
         "docling",
         "ollama"
-    ]
+    ],
+    "requiresDocker": true
 }
diff --git a/ai/genai-observability/GenAiObservabilityRoute.java 
b/ai/genai-observability/GenAiObservabilityRoute.java
deleted file mode 100644
index 9b2eaa8..0000000
--- a/ai/genai-observability/GenAiObservabilityRoute.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * 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.
- */
-
-import dev.langchain4j.model.chat.ChatModel;
-import dev.langchain4j.model.ollama.OllamaChatModel;
-
-import org.apache.camel.builder.RouteBuilder;
-
-import static java.time.Duration.ofSeconds;
-
-/**
- * Minimal GenAI route for observing LLM calls with Camel 4.23+.
- * <p>
- * Requires Ollama running locally (see README.md).
- */
-public class GenAiObservabilityRoute extends RouteBuilder {
-
-    @Override
-    public void configure() throws Exception {
-        String baseUrl = 
getContext().resolvePropertyPlaceholders("{{ollama.baseUrl:http://localhost:11434}}";);
-        String modelName = 
getContext().resolvePropertyPlaceholders("{{ollama.model:llama3.2}}");
-        ChatModel chatModel = OllamaChatModel.builder()
-                .baseUrl(baseUrl)
-                .modelName(modelName)
-                .temperature(0.2)
-                .timeout(ofSeconds(120))
-                .build();
-        getContext().getRegistry().bind("chatModel", chatModel);
-
-        from("timer:genai?period={{genai.period:15000}}")
-                .routeId("genai-chat")
-                .setBody(constant("In one sentence, what is Apache Camel 
integration?"))
-                .to("langchain4j-chat:demo?chatModel=#chatModel")
-                .log("LLM reply: ${body}")
-                .log("Request model: 
${header.CamelLangChain4jChatRequestModel}")
-                .log("Response model: 
${header.CamelLangChain4jChatResponseModel}");
-    }
-}
diff --git a/ai/genai-observability/README.md b/ai/genai-observability/README.md
deleted file mode 100644
index a8f5c3c..0000000
--- a/ai/genai-observability/README.md
+++ /dev/null
@@ -1,91 +0,0 @@
-## GenAI Observability (JBang / CLI / TUI)
-
-This example demonstrates **GenAI observability** in Apache Camel 4.23+: 
OpenTelemetry
-`gen_ai.*` span attributes and Micrometer metrics for `langchain4j-chat` LLM 
calls.
-
-A timer route sends a prompt to Ollama via LangChain4j every 15 seconds. With
-`--observe`, Camel exposes health/metrics/tracing and the TUI can show GenAI 
spans
-and token usage.
-
-Blog post: see `docs/blog-drafts/genai-observability-01-jbang-cli-tui.adoc` in 
the
-[Apache Camel](https://github.com/apache/camel) source tree.
-
-### Prerequisites
-
-- [Camel 
JBang](https://camel.apache.org/manual/camel-jbang-jdk-installation.html) 
(Camel 4.23+)
-- [Ollama](https://ollama.com/) running locally
-
-```sh
-ollama pull llama3.2
-ollama serve
-```
-
-### How to run
-
-From this directory:
-
-```sh
-camel run GenAiObservabilityRoute.java application.properties --observe \
-  --dependency=camel-langchain4j-chat \
-  --dependency=camel-ai-observability \
-  --dependency=langchain4j-ollama
-```
-
-Or run directly from GitHub:
-
-```sh
-camel run 
https://github.com/apache/camel-jbang-examples/tree/main/ai/genai-observability 
\
-  --observe \
-  --dependency=camel-langchain4j-chat \
-  --dependency=camel-ai-observability \
-  --dependency=langchain4j-ollama
-```
-
-### Verify GenAI observability
-
-**Metrics** (Prometheus format):
-
-```sh
-curl -s http://127.0.0.1:9876/observe/metrics | grep gen_ai
-```
-
-Look for `gen_ai_client_operation` and `gen_ai_client_token_usage`.
-
-**TUI — Spans tab**
-
-In another terminal:
-
-```sh
-camel tui
-```
-
-Select the running integration, open the **Spans** tab, and trigger a timer 
tick.
-Each LLM call creates a child span with `gen_ai.operation.name=chat`,
-`gen_ai.request.model`, and token usage attributes.
-
-**TUI — AI usage (Ctrl+U)**
-
-With the AI panel open, press **Ctrl+U** to see combined token usage from
-`camel ask` and route-level GenAI spans (Camel 4.23 Phase 2).
-
-**Ask about your integration**
-
-```sh
-camel ask "Which routes call an LLM and what model do they use?"
-```
-
-### Disable GenAI observability
-
-```properties
-camel.aiObservability.enabled=false
-```
-
-### Help and contributions
-
-If you hit any problem using Camel or have some feedback, then please
-[let us know](https://camel.apache.org/community/support/).
-
-We also love contributors, so
-[get involved](https://camel.apache.org/community/contributing/) :-)
-
-The Camel riders!
diff --git a/ai/genai-observability/application.properties 
b/ai/genai-observability/application.properties
deleted file mode 100644
index 1aa678e..0000000
--- a/ai/genai-observability/application.properties
+++ /dev/null
@@ -1,7 +0,0 @@
-# Ollama connection (start Ollama and pull the model first: ollama pull 
llama3.2)
-ollama.baseUrl=http://localhost:11434
-ollama.model=llama3.2
-genai.period=15000
-
-# GenAI observability (Camel 4.23+) — enabled by default when observability 
backends are present
-camel.ai.observability.enabled=true
diff --git a/ai/genai-observability/metadata.json 
b/ai/genai-observability/metadata.json
deleted file mode 100644
index 26fe9dd..0000000
--- a/ai/genai-observability/metadata.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
-    "title": "GenAI Observability",
-    "description": "A timer sends a prompt to a local Ollama model through 
langchain4j-chat every fifteen seconds and logs the reply and the model name; 
with --observe, OpenTelemetry gen_ai spans and Micrometer metrics show token 
usage per call.",
-    "level": "ai",
-    "teaches": {
-        "components": [
-            "timer",
-            "langchain4j-chat",
-            "log"
-        ]
-    },
-    "tags": [
-        "ai",
-        "observability",
-        "langchain4j",
-        "opentelemetry",
-        "genai",
-        "tui"
-    ],
-    "bundled": false,
-    "infraServices": [
-        "ollama"
-    ]
-}
diff --git a/ai/langchain4j-chat/README.md b/ai/langchain4j-chat/README.md
new file mode 100644
index 0000000..3a6a23f
--- /dev/null
+++ b/ai/langchain4j-chat/README.md
@@ -0,0 +1,86 @@
+# LangChain4j chat
+
+A local model writes the shipping notification for each order. The chat model 
is a bean built from
+`application.properties`, the route turns the order into a prompt, and the 
reply comes back as the body with the
+token counts in headers.
+
+## What you will see
+
+```text
+INFO ... langchain4j-chat.camel.yaml:45      : Notification for ORD-1002 (107 
in, 35 out): Dear Customer C-207,
+Your order ORD-1002 containing 3 CAMEL-MUGs has been shipped today. We hope 
you enjoy your new items!
+```
+
+The wording is the model's; `granite4:3b` writes plainly, a larger model 
writes better.
+
+## Install Camel CLI
+
+<!-- see installation instructions in ../../install.adoc -->
+
+## Run it
+
+The example needs a running Ollama with a model, which the Camel CLI starts 
for you in a container (Docker or
+Podman must be running); the container pulls the `granite4:3b` model on first 
start, which takes a while. In one
+terminal:
+
+```shell
+camel infra run ollama
+```
+
+In another terminal:
+
+```shell
+camel run *
+```
+
+An Ollama installed on the machine works too: set `ollama.model` in 
`application.properties` to a model you have
+pulled, or override it for one run with `OLLAMA_MODEL=llama3.2 camel run *`. A 
3B model on a CPU takes tens of
+seconds per reply; a larger model or a GPU is faster. Stop the container with 
`camel infra stop ollama`.
+
+## How it works
+
+- The `beans` block builds the chat model: LangChain4j models come from 
builders, so the bean names the
+  `builderClass`, the `builderMethod` and the builder's properties, with the 
URL and model name read from
+  `application.properties`. The CLI downloads `langchain4j-ollama` from the 
class name.
+- `langchain4j-chat` with `chatModel: "#chatModel"` sends the body as a single 
user message and replaces the
+  body with the reply; `CamelLangChain4jChatInputTokenCount` and 
`...OutputTokenCount` say what it cost.
+- The prompt is built with the Simple language from the order fields; 
`${body[lines]}` prints the list of lines.
+- The order id is kept in an exchange property because the reply replaces the 
body.
+
+## Build it step by step
+
+1. A timer route that sends a fixed question to `langchain4j-chat` and logs 
the reply; start Ollama and see
+   the first answer arrive.
+2. Read the orders and build the prompt from the order; log the token headers.
+3. Lower `temperature` and compare the replies across runs.
+
+## Try changing
+
+- Run with `camel run * --observe` and open the Spans tab in `camel tui`: 
every call to the model is a span with
+  `gen_ai.operation.name`, the model and the token usage.
+- Switch to `CHAT_SINGLE_MESSAGE_WITH_PROMPT` with a 
`CamelLangChain4jChatPromptTemplate` header and pass the
+  order fields as variables instead of building the prompt yourself.
+- Point `ollama.base.url` at an OpenAI-compatible server by swapping the bean 
for `OpenAiChatModel` from
+  `langchain4j-open-ai`.
+
+## Integration testing
+
+The example comes with a test in the [Citrus](https://citrusframework.org/) 
YAML DSL,
+`test/langchain4j-chat.citrus.it.yaml`, which the Camel CLI runs:
+
+```shell
+camel test run test/langchain4j-chat.citrus.it.yaml
+```
+
+The test starts Ollama on a side port, runs the route and verifies that a 
notification is logged for the
+first order. It pulls a 2.5 GB model and runs it on the CPU, so it is a local 
check and not part of the CI build.
+
+## Help and contributions
+
+If you hit any problem using Camel or have some feedback, then please
+[let us know](https://camel.apache.org/community/support/).
+
+We also love contributors, so
+[get involved](https://camel.apache.org/community/contributing/) :-)
+
+The Camel riders!
diff --git a/ai/docling-langchain4j-rag/application.properties 
b/ai/langchain4j-chat/application.properties
similarity index 59%
copy from ai/docling-langchain4j-rag/application.properties
copy to ai/langchain4j-chat/application.properties
index 1c416dd..782689c 100644
--- a/ai/docling-langchain4j-rag/application.properties
+++ b/ai/langchain4j-chat/application.properties
@@ -15,25 +15,6 @@
 # specific language governing permissions and limitations
 # under the License.
 
-# Application Configuration
-camel.main.name = DoclingLangChain4jRAG
-
-# Directory Configuration
-documents.directory=documents
-output.directory=output
-
-# Docling-Serve Configuration
-# Can be run via: docker run -p 5001:5001 
ghcr.io/docling-project/docling-serve:latest
-# Or via: camel infra run docling (if available)
-docling.serve.url=http://localhost:5001
-
-# Ollama Configuration
+# the model camel infra run ollama pulls, on the port it prints
 ollama.base.url=http://localhost:11434
-ollama.model.name=orca-mini
-
-# Batch Processing Configuration
-# Set to -1 to disable batch processing
-batch.delay=10000
-
-# HTTP Server Configuration
-camel.server.port=8080
+ollama.model=granite4:3b
diff --git a/ai/langchain4j-chat/langchain4j-chat.camel.yaml 
b/ai/langchain4j-chat/langchain4j-chat.camel.yaml
new file mode 100644
index 0000000..04880bd
--- /dev/null
+++ b/ai/langchain4j-chat/langchain4j-chat.camel.yaml
@@ -0,0 +1,46 @@
+# A local model writes the shipping notification for each order. The chat
+# model is a bean built from application.properties; the route builds the
+# prompt from the order and logs the reply. Start the model with:
+# camel infra run ollama
+- beans:
+    - name: chatModel
+      type: dev.langchain4j.model.ollama.OllamaChatModel
+      builderClass: 
dev.langchain4j.model.ollama.OllamaChatModel$OllamaChatModelBuilder
+      builderMethod: build
+      properties:
+        baseUrl: "{{ollama.base.url}}"
+        modelName: "{{ollama.model}}"
+        temperature: 0.2
+
+- route:
+    id: shipping-notifications
+    from:
+      uri: file
+      parameters:
+        directoryName: orders
+        noop: true
+        sortBy: "file:name"
+      steps:
+        - unmarshal:
+            json:
+              library: Jackson
+        - setProperty:
+            name: orderId
+            expression:
+              simple:
+                expression: "${body[orderId]}"
+        - setBody:
+            expression:
+              simple:
+                expression: >-
+                  You write short, friendly emails for a web shop that sells 
Apache Camel merchandise.
+                  Write the shipping notification for order ${body[orderId]}, 
shipped today to customer ${body[customer]} in ${body[country]}.
+                  The order has ${body[lines].size()} line(s): ${body[lines]}.
+                  Two sentences, no subject line, no placeholders, plain text.
+        - to:
+            uri: langchain4j-chat
+            parameters:
+              chatId: notifications
+              chatModel: "#chatModel"
+        - log:
+            message: "Notification for ${exchangeProperty.orderId} 
(${header.CamelLangChain4jChatInputTokenCount} in, 
${header.CamelLangChain4jChatOutputTokenCount} out): ${body}"
diff --git a/ai/langchain4j-chat/metadata.json 
b/ai/langchain4j-chat/metadata.json
new file mode 100644
index 0000000..cb1d873
--- /dev/null
+++ b/ai/langchain4j-chat/metadata.json
@@ -0,0 +1,36 @@
+{
+    "title": "LangChain4j chat",
+    "description": "A local Ollama model started with camel infra writes the 
shipping notification for each of the three orders; the chat model is a bean 
built from properties, the prompt comes from the order, and the log shows the 
reply with its token counts.",
+    "level": "ai",
+    "teaches": {
+        "components": [
+            "langchain4j-chat",
+            "file",
+            "log"
+        ],
+        "eips": [
+            "setBody",
+            "setProperty",
+            "unmarshal"
+        ],
+        "languages": [
+            "simple"
+        ],
+        "dataformats": [
+            "json"
+        ]
+    },
+    "tags": [
+        "shop",
+        "ai",
+        "llm",
+        "ollama",
+        "langchain4j"
+    ],
+    "infraServices": [
+        "ollama"
+    ],
+    "requiresDocker": true,
+    "bundled": false,
+    "ciSkip": true
+}
diff --git a/ai/langchain4j-chat/orders/order-1001.json 
b/ai/langchain4j-chat/orders/order-1001.json
new file mode 100644
index 0000000..3eb5f03
--- /dev/null
+++ b/ai/langchain4j-chat/orders/order-1001.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1001", "customer": "C-482", "country": "DK", "status": "paid",
+ "lines": [{"sku": "CAMEL-TSHIRT", "qty": 2, "price": 19.95}, {"sku": 
"CAMEL-MUG", "qty": 1, "price": 9.50}]}
diff --git a/ai/langchain4j-chat/orders/order-1002.json 
b/ai/langchain4j-chat/orders/order-1002.json
new file mode 100644
index 0000000..bb7aa0e
--- /dev/null
+++ b/ai/langchain4j-chat/orders/order-1002.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1002", "customer": "C-207", "country": "DE", "status": "paid",
+ "lines": [{"sku": "CAMEL-MUG", "qty": 3, "price": 9.50}]}
diff --git a/ai/langchain4j-chat/orders/order-1003.json 
b/ai/langchain4j-chat/orders/order-1003.json
new file mode 100644
index 0000000..9edab76
--- /dev/null
+++ b/ai/langchain4j-chat/orders/order-1003.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1003", "customer": "C-134", "country": "US", "status": 
"pending",
+ "lines": [{"sku": "CAMEL-TSHIRT", "qty": 1, "price": 19.95}, {"sku": 
"CAMEL-CAP", "qty": 1, "price": 14.00}, {"sku": "CAMEL-MUG", "qty": 2, "price": 
9.50}]}
diff --git a/ai/langchain4j-chat/test/langchain4j-chat.citrus.it.yaml 
b/ai/langchain4j-chat/test/langchain4j-chat.citrus.it.yaml
new file mode 100644
index 0000000..4bcfa09
--- /dev/null
+++ b/ai/langchain4j-chat/test/langchain4j-chat.citrus.it.yaml
@@ -0,0 +1,51 @@
+name: langchain4j-chat-test
+description: A local model writes the shipping notification for an order
+actions:
+  - camel:
+      infra:
+        run:
+          service: ollama
+          # a side port, so an Ollama already installed on the machine does 
not get in the way
+          port: 11435
+  # the container pulls the model after it starts; wait until Ollama lists it
+  - repeatOnError:
+      until: i >= 60
+      index: i
+      autoSleep: 5000
+      actions:
+        - http:
+            client: "http://localhost:11435";
+            sendRequest:
+              GET:
+                path: "/api/tags"
+        - http:
+            client: "http://localhost:11435";
+            receiveResponse:
+              response:
+                status: "200"
+              validate:
+                - jsonPath:
+                    - expression: "$.models[?(@.name=='granite4:3b')].name"
+                      value: "granite4:3b"
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "langchain4j-chat"
+            file: "../langchain4j-chat.camel.yaml"
+            # the properties file would win over an inline property, so both 
are given here
+            systemProperties:
+              properties:
+                - name: ollama.base.url
+                  value: http://localhost:11435
+                - name: ollama.model
+                  value: granite4:3b
+          resources:
+            - "orders/order-1001.json"
+            - "orders/order-1002.json"
+            - "orders/order-1003.json"
+  - camel:
+      jbang:
+        verify:
+          integration: "langchain4j-chat"
+          logMessage: "Notification for ORD-1001"
diff --git a/ai/mcp-server/README.md b/ai/mcp-server/README.md
new file mode 100644
index 0000000..9353f2d
--- /dev/null
+++ b/ai/mcp-server/README.md
@@ -0,0 +1,105 @@
+# MCP server
+
+Two routes exposed as MCP tools, so an AI agent can ask the shop about stock 
and orders. There is no server route:
+`application.properties` switches the MCP server on and picks the tools by 
tag, and every `ai-tool` route with
+that tag becomes a tool any MCP client can discover and call, a coding agent 
included.
+
+## What you will see
+
+```text
+$ camel run *
+...
+INFO ... VertxMcpServerEngine : MCP server 'webshop' serving tools on path /mcp
+INFO ... Started stock-level (ai-tool://stock_level)
+INFO ... Started order-status (ai-tool://order_status)
+
+when a client calls the tools:
+
+INFO ... mcp-server.camel.yaml:38 : Tool stock_level(CAMEL-MUG): 42 CAMEL-MUG 
in stock
+INFO ... mcp-server.camel.yaml:81 : Tool order_status(ORD-1003): Order 
ORD-1003 for customer C-134 in US is pending with 3 line(s)
+```
+
+## Install Camel CLI
+
+<!-- see installation instructions in ../../install.adoc -->
+
+## Run it
+
+```shell
+camel run *
+```
+
+The MCP endpoint is http://localhost:8080/mcp over streamable HTTP. Point an 
MCP client at it; for Claude Code,
+Cursor or another agent that reads an `mcp.json`:
+
+```json
+{
+  "mcpServers": {
+    "webshop": {"type": "http", "url": "http://localhost:8080/mcp"}
+  }
+}
+```
+
+Then ask the agent how many mugs are in stock, or what the status of order 
ORD-1003 is, and watch the log.
+Without an agent, `curl` speaks the protocol too: an `initialize` request 
returns an `Mcp-Session-Id` header,
+and the following requests carry it:
+
+```shell
+curl -s -i -X POST localhost:8080/mcp -H 'Content-Type: application/json' -H 
'Accept: application/json, text/event-stream' \
+  -d 
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
+
+curl -s -X POST localhost:8080/mcp -H 'Content-Type: application/json' -H 
'Accept: application/json, text/event-stream' -H 'Mcp-Session-Id: <the id>' \
+  -d 
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"stock_level","arguments":{"sku":"CAMEL-MUG"}}}'
+```
+
+The MCP server is a preview feature since Camel 4.22.
+
+## How it works
+
+- `ai-tool:stock_level` is a consumer endpoint that registers the route as a 
tool: the name the model sees, a
+  `description` it reads to decide when to call it, `parameter.sku` with its 
type, description and `required`,
+  and `readOnlyHint` as an advisory hint. When a client calls the tool, the 
arguments arrive as headers and the
+  route's final body is the tool result.
+- `tags: shop` groups the tools; `camel.server.mcp-tags=shop` in 
`application.properties` exposes that group.
+  `camel.server.mcp-enabled=true` starts the MCP server on the CLI's HTTP 
server and `camel.server.mcp-server-name`
+  is what the client shows; Camel 4.23 adds a title, description and 
instructions next to it.
+- `order_status` reads the order file with `pollEnrich` and a file name built 
from the id, `order-1001.json`
+  for `ORD-1001`, and answers a sentence; a missing file leaves the body null, 
which becomes the unknown answer.
+- Nothing here is specific to MCP: the same `ai-tool` routes are the tools of 
a Camel agent built with
+  `langchain4j-agent` or `openai`, selected by the same tags.
+
+## Build it step by step
+
+1. One `ai-tool` route that answers a fixed text, with the four 
`camel.server.mcp-*` properties; run it and see
+   the server line in the log, then `initialize` and `tools/list` with `curl`.
+2. Add a parameter and use `${header.sku}` in the answer; call it with 
`tools/call`.
+3. Answer from `stock.json` with `jsonpath` and add the unknown case.
+4. Add the second tool and connect a real agent.
+
+## Try changing
+
+- Add an `ai-resource` route that serves `stock.json` as an MCP resource and 
read it from the client.
+- Add `camel.server.mcp-transport=stdio` and run with `camel run * 
--mcp-stdio` so an IDE launches the
+  example as a subprocess.
+- Give a tool `returnDirect: true` and see the difference when a Camel agent 
calls it.
+
+## Integration testing
+
+The example comes with a test in the [Citrus](https://citrusframework.org/) 
YAML DSL,
+`test/mcp-server.citrus.it.yaml`, which the Camel CLI runs:
+
+```shell
+camel test run test/mcp-server.citrus.it.yaml
+```
+
+The test starts the example, opens an MCP session over HTTP and calls both 
tools, checking the answers.
+
+## Help and contributions
+
+If you hit any problem using Camel or have some feedback, then please
+[let us know](https://camel.apache.org/community/support/).
+
+We also love contributors, so
+[get involved](https://camel.apache.org/community/contributing/) :-)
+
+The Camel riders!
diff --git a/ai/docling-langchain4j-rag/application.properties 
b/ai/mcp-server/application.properties
similarity index 58%
copy from ai/docling-langchain4j-rag/application.properties
copy to ai/mcp-server/application.properties
index 1c416dd..13ca646 100644
--- a/ai/docling-langchain4j-rag/application.properties
+++ b/ai/mcp-server/application.properties
@@ -15,25 +15,8 @@
 # specific language governing permissions and limitations
 # under the License.
 
-# Application Configuration
-camel.main.name = DoclingLangChain4jRAG
-
-# Directory Configuration
-documents.directory=documents
-output.directory=output
-
-# Docling-Serve Configuration
-# Can be run via: docker run -p 5001:5001 
ghcr.io/docling-project/docling-serve:latest
-# Or via: camel infra run docling (if available)
-docling.serve.url=http://localhost:5001
-
-# Ollama Configuration
-ollama.base.url=http://localhost:11434
-ollama.model.name=orca-mini
-
-# Batch Processing Configuration
-# Set to -1 to disable batch processing
-batch.delay=10000
-
-# HTTP Server Configuration
+camel.server.enabled=true
 camel.server.port=8080
+camel.server.mcp-enabled=true
+camel.server.mcp-tags=shop
+camel.server.mcp-server-name=webshop
diff --git a/ai/mcp-server/mcp-server.camel.yaml 
b/ai/mcp-server/mcp-server.camel.yaml
new file mode 100644
index 0000000..02834fb
--- /dev/null
+++ b/ai/mcp-server/mcp-server.camel.yaml
@@ -0,0 +1,82 @@
+# Two routes exposed as MCP tools, so an AI agent can ask the shop about
+# stock and orders. No server route is needed: application.properties
+# switches the MCP server on and picks the tools by tag.
+- route:
+    id: stock-level
+    from:
+      uri: ai-tool:stock_level
+      parameters:
+        tags: shop
+        description: "Stock level of one product in the warehouse, by SKU"
+        parameter.sku: string
+        parameter.sku.description: "The product SKU, for example CAMEL-MUG"
+        parameter.sku.required: "true"
+        readOnlyHint: "true"
+      steps:
+        - setBody:
+            expression:
+              constant:
+                expression: resource:file:stock.json
+        - setBody:
+            expression:
+              jsonpath:
+                expression: "$[?(@.sku == '${header.sku}')]"
+                resultType: java.util.List
+        - choice:
+            when:
+              - expression:
+                  simple:
+                    expression: "${body.size()} == 0"
+                steps:
+                  - setBody:
+                      expression:
+                        simple:
+                          expression: "Unknown SKU ${header.sku}"
+            otherwise:
+              steps:
+                - setBody:
+                    expression:
+                      simple:
+                        expression: "${body[0][qty]} ${header.sku} in stock"
+        - log:
+            message: "Tool stock_level(${header.sku}): ${body}"
+
+- route:
+    id: order-status
+    from:
+      uri: ai-tool:order_status
+      parameters:
+        tags: shop
+        description: "Status and lines of an order, by order id"
+        parameter.orderId: string
+        parameter.orderId.description: "The order id, for example ORD-1001"
+        parameter.orderId.required: "true"
+        readOnlyHint: "true"
+      steps:
+        # order ORD-1001 is the file orders/order-1001.json
+        - pollEnrich:
+            expression:
+              simple:
+                expression: 
"file:orders?fileName=order-${header.orderId.substring(4)}.json&noop=true&idempotent=false"
+            timeout: 1000
+        - choice:
+            when:
+              - expression:
+                  simple:
+                    expression: "${body} == null"
+                steps:
+                  - setBody:
+                      expression:
+                        simple:
+                          expression: "Unknown order ${header.orderId}"
+            otherwise:
+              steps:
+                - unmarshal:
+                    json:
+                      library: Jackson
+                - setBody:
+                    expression:
+                      simple:
+                        expression: "Order ${body[orderId]} for customer 
${body[customer]} in ${body[country]} is ${body[status]} with 
${body[lines].size()} line(s)"
+        - log:
+            message: "Tool order_status(${header.orderId}): ${body}"
diff --git a/ai/mcp-server/metadata.json b/ai/mcp-server/metadata.json
new file mode 100644
index 0000000..7539354
--- /dev/null
+++ b/ai/mcp-server/metadata.json
@@ -0,0 +1,33 @@
+{
+    "title": "MCP server",
+    "description": "Two routes are exposed as MCP tools, stock_level by SKU 
and order_status by order id, on http://localhost:8080/mcp with nothing but 
properties to switch the server on; any MCP client, a coding agent included, 
can list and call them, and the log shows each call.",
+    "level": "ai",
+    "teaches": {
+        "components": [
+            "ai-tool",
+            "file",
+            "log"
+        ],
+        "eips": [
+            "pollEnrich",
+            "choice",
+            "unmarshal"
+        ],
+        "languages": [
+            "simple",
+            "jsonpath",
+            "constant"
+        ],
+        "dataformats": [
+            "json"
+        ]
+    },
+    "tags": [
+        "shop",
+        "ai",
+        "mcp",
+        "agent",
+        "tools"
+    ],
+    "bundled": false
+}
diff --git a/ai/mcp-server/orders/order-1001.json 
b/ai/mcp-server/orders/order-1001.json
new file mode 100644
index 0000000..3eb5f03
--- /dev/null
+++ b/ai/mcp-server/orders/order-1001.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1001", "customer": "C-482", "country": "DK", "status": "paid",
+ "lines": [{"sku": "CAMEL-TSHIRT", "qty": 2, "price": 19.95}, {"sku": 
"CAMEL-MUG", "qty": 1, "price": 9.50}]}
diff --git a/ai/mcp-server/orders/order-1002.json 
b/ai/mcp-server/orders/order-1002.json
new file mode 100644
index 0000000..bb7aa0e
--- /dev/null
+++ b/ai/mcp-server/orders/order-1002.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1002", "customer": "C-207", "country": "DE", "status": "paid",
+ "lines": [{"sku": "CAMEL-MUG", "qty": 3, "price": 9.50}]}
diff --git a/ai/mcp-server/orders/order-1003.json 
b/ai/mcp-server/orders/order-1003.json
new file mode 100644
index 0000000..9edab76
--- /dev/null
+++ b/ai/mcp-server/orders/order-1003.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1003", "customer": "C-134", "country": "US", "status": 
"pending",
+ "lines": [{"sku": "CAMEL-TSHIRT", "qty": 1, "price": 19.95}, {"sku": 
"CAMEL-CAP", "qty": 1, "price": 14.00}, {"sku": "CAMEL-MUG", "qty": 2, "price": 
9.50}]}
diff --git a/ai/mcp-server/stock.json b/ai/mcp-server/stock.json
new file mode 100644
index 0000000..efab5de
--- /dev/null
+++ b/ai/mcp-server/stock.json
@@ -0,0 +1,5 @@
+[
+  {"sku": "CAMEL-TSHIRT", "qty": 120},
+  {"sku": "CAMEL-MUG", "qty": 42},
+  {"sku": "CAMEL-CAP", "qty": 0}
+]
diff --git a/ai/mcp-server/test/mcp-server.citrus.it.yaml 
b/ai/mcp-server/test/mcp-server.citrus.it.yaml
new file mode 100644
index 0000000..ede3ee4
--- /dev/null
+++ b/ai/mcp-server/test/mcp-server.citrus.it.yaml
@@ -0,0 +1,77 @@
+name: mcp-server-test
+description: The shop tools answer over MCP
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "mcp-server"
+            file: "../mcp-server.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "stock.json"
+            - "orders/order-1001.json"
+            - "orders/order-1002.json"
+            - "orders/order-1003.json"
+  - camel:
+      jbang:
+        verify:
+          integration: "mcp-server"
+          logMessage: "MCP server 'webshop' serving tools"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        POST:
+          path: "/mcp"
+          contentType: "application/json"
+          accept: "application/json, text/event-stream"
+          body:
+            data: 
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"citrus","version":"1"}}}'
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "200"
+        extract:
+          header:
+            - name: "Mcp-Session-Id"
+              variable: "sessionId"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        POST:
+          path: "/mcp"
+          contentType: "application/json"
+          accept: "application/json, text/event-stream"
+          headers:
+            - name: "Mcp-Session-Id"
+              value: "${sessionId}"
+          body:
+            data: 
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"stock_level","arguments":{"sku":"CAMEL-MUG"}}}'
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "200"
+          body:
+            data: "@contains('42 CAMEL-MUG in stock')@"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        POST:
+          path: "/mcp"
+          contentType: "application/json"
+          accept: "application/json, text/event-stream"
+          headers:
+            - name: "Mcp-Session-Id"
+              value: "${sessionId}"
+          body:
+            data: 
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"order_status","arguments":{"orderId":"ORD-1003"}}}'
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "200"
+          body:
+            data: "@contains('Order ORD-1003 for customer C-134 in US is 
pending with 3 line(s)')@"
diff --git a/camel-jbang-example-catalog.json b/camel-jbang-example-catalog.json
index 0295486..0967763 100644
--- a/camel-jbang-example-catalog.json
+++ b/camel-jbang-example-catalog.json
@@ -44,37 +44,95 @@
         ]
     },
     {
-        "name": "ai/genai-observability",
-        "title": "GenAI Observability",
-        "description": "A timer sends a prompt to a local Ollama model through 
langchain4j-chat every fifteen seconds and logs the reply and the model name; 
with --observe, OpenTelemetry gen_ai spans and Micrometer metrics show token 
usage per call.",
+        "name": "ai/langchain4j-chat",
+        "title": "LangChain4j chat",
+        "description": "A local Ollama model started with camel infra writes 
the shipping notification for each of the three orders; the chat model is a 
bean built from properties, the prompt comes from the order, and the log shows 
the reply with its token counts.",
         "level": "ai",
         "teaches": {
             "components": [
-                "timer",
                 "langchain4j-chat",
+                "file",
                 "log"
+            ],
+            "eips": [
+                "setBody",
+                "setProperty",
+                "unmarshal"
+            ],
+            "languages": [
+                "simple"
+            ],
+            "dataformats": [
+                "json"
             ]
         },
         "tags": [
+            "shop",
             "ai",
-            "observability",
-            "langchain4j",
-            "opentelemetry",
-            "genai",
-            "tui"
+            "llm",
+            "ollama",
+            "langchain4j"
         ],
         "bundled": false,
         "requiresDocker": true,
-        "hasCitrusTests": false,
+        "hasCitrusTests": true,
         "files": [
-            "GenAiObservabilityRoute.java",
             "README.md",
-            "application.properties"
+            "application.properties",
+            "langchain4j-chat.camel.yaml",
+            "orders/order-1001.json",
+            "orders/order-1002.json",
+            "orders/order-1003.json"
         ],
         "infraServices": [
             "ollama"
         ]
     },
+    {
+        "name": "ai/mcp-server",
+        "title": "MCP server",
+        "description": "Two routes are exposed as MCP tools, stock_level by 
SKU and order_status by order id, on http://localhost:8080/mcp with nothing but 
properties to switch the server on; any MCP client, a coding agent included, 
can list and call them, and the log shows each call.",
+        "level": "ai",
+        "teaches": {
+            "components": [
+                "ai-tool",
+                "file",
+                "log"
+            ],
+            "eips": [
+                "pollEnrich",
+                "choice",
+                "unmarshal"
+            ],
+            "languages": [
+                "simple",
+                "jsonpath",
+                "constant"
+            ],
+            "dataformats": [
+                "json"
+            ]
+        },
+        "tags": [
+            "shop",
+            "ai",
+            "mcp",
+            "agent",
+            "tools"
+        ],
+        "bundled": false,
+        "requiresDocker": false,
+        "hasCitrusTests": true,
+        "files": [
+            "README.md",
+            "application.properties",
+            "mcp-server.camel.yaml",
+            "orders/order-1001.json",
+            "orders/order-1002.json",
+            "orders/order-1003.json",
+            "stock.json"
+        ]
+    },
     {
         "name": "ai/openai-pii-redaction",
         "title": "OpenAI PII Redaction",

Reply via email to