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",