You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ac...@apache.org on 2026/01/22 08:39:57 UTC
(camel) branch 22886 created (now 1c5e4e2b505b)
This is an automated email from the ASF dual-hosted git repository.
acosentino pushed a change to branch 22886
in repository https://gitbox.apache.org/repos/asf/camel.git
at 1c5e4e2b505b CAMEL-22886 - Camel-Jbang: Add an MCP module
This branch includes the following new commits:
new 1c5e4e2b505b CAMEL-22886 - Camel-Jbang: Add an MCP module
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(camel) 01/01: CAMEL-22886 - Camel-Jbang: Add an MCP module
Posted by ac...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
acosentino pushed a commit to branch 22886
in repository https://gitbox.apache.org/repos/asf/camel.git
commit 1c5e4e2b505bfca821be635061a0baa0fef7ba39
Author: Andrea Cosentino <an...@gmail.com>
AuthorDate: Thu Jan 22 09:24:05 2026 +0100
CAMEL-22886 - Camel-Jbang: Add an MCP module
Signed-off-by: Andrea Cosentino <an...@gmail.com>
---
dsl/camel-jbang/camel-jbang-mcp/pom.xml | 143 ++++++++
.../camel-jbang-plugin/camel-jbang-plugin-mcp | 2 +
.../dsl/jbang/core/commands/mcp/CatalogTools.java | 363 +++++++++++++++++++++
.../dsl/jbang/core/commands/mcp/ExplainTools.java | 212 ++++++++++++
.../jbang/core/commands/mcp/TransformTools.java | 188 +++++++++++
.../src/main/resources/application.properties | 41 +++
dsl/camel-jbang/pom.xml | 1 +
7 files changed, 950 insertions(+)
diff --git a/dsl/camel-jbang/camel-jbang-mcp/pom.xml b/dsl/camel-jbang/camel-jbang-mcp/pom.xml
new file mode 100644
index 000000000000..a66bfdbb5721
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-mcp/pom.xml
@@ -0,0 +1,143 @@
+<?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</groupId>
+ <artifactId>camel-jbang-parent</artifactId>
+ <version>4.18.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>camel-jbang-mcp</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Camel :: JBang :: MCP</name>
+ <description>Camel JBang MCP (Model Context Protocol) Server using Quarkus</description>
+
+ <properties>
+ <firstVersion>4.18.0</firstVersion>
+ <label>jbang,mcp,ai</label>
+ <supportLevel>Preview</supportLevel>
+ <camel-prepare-component>false</camel-prepare-component>
+ <quarkus-mcp-server-version>1.2.0</quarkus-mcp-server-version>
+ <quarkus.platform.version>3.30.6</quarkus.platform.version>
+ <!-- Build uber-jar for easy JBang execution -->
+ <quarkus.package.jar.type>uber-jar</quarkus.package.jar.type>
+ </properties>
+
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>io.quarkus.platform</groupId>
+ <artifactId>quarkus-bom</artifactId>
+ <version>${quarkus.platform.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
+ <dependencies>
+ <!-- Quarkus MCP Server with STDIO transport -->
+ <dependency>
+ <groupId>io.quarkiverse.mcp</groupId>
+ <artifactId>quarkus-mcp-server-stdio</artifactId>
+ <version>${quarkus-mcp-server-version}</version>
+ </dependency>
+
+ <!-- Quarkus core dependencies -->
+ <dependency>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-arc</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-jackson</artifactId>
+ </dependency>
+
+ <!-- Quarkus LangChain4j for LLM integration -->
+ <dependency>
+ <groupId>io.quarkiverse.langchain4j</groupId>
+ <artifactId>quarkus-langchain4j-ollama</artifactId>
+ <version>1.6.0.CR1</version>
+ </dependency>
+
+ <!-- camel catalog for component/eip metadata -->
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-catalog</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-route-parser</artifactId>
+ </dependency>
+
+ <!-- test dependencies -->
+ <dependency>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-junit5</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>io.quarkus.platform</groupId>
+ <artifactId>quarkus-maven-plugin</artifactId>
+ <version>${quarkus.platform.version}</version>
+ <extensions>true</extensions>
+ <executions>
+ <execution>
+ <goals>
+ <goal>build</goal>
+ <goal>generate-code</goal>
+ <goal>generate-code-tests</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <parameters>true</parameters>
+ </configuration>
+ </plugin>
+ <plugin>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <systemPropertyVariables>
+ <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
+ </systemPropertyVariables>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/generated/resources/META-INF/services/org/apache/camel/camel-jbang-plugin/camel-jbang-plugin-mcp b/dsl/camel-jbang/camel-jbang-mcp/src/generated/resources/META-INF/services/org/apache/camel/camel-jbang-plugin/camel-jbang-plugin-mcp
new file mode 100644
index 000000000000..d57b288500ea
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-mcp/src/generated/resources/META-INF/services/org/apache/camel/camel-jbang-plugin/camel-jbang-plugin-mcp
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.dsl.jbang.core.commands.mcp.McpPlugin
diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogTools.java
new file mode 100644
index 000000000000..dcd315d620a0
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogTools.java
@@ -0,0 +1,363 @@
+/*
+ * 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.dsl.jbang.core.commands.mcp;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.quarkiverse.mcp.server.Tool;
+import io.quarkiverse.mcp.server.ToolArg;
+import io.quarkiverse.mcp.server.ToolCallException;
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.tooling.model.ComponentModel;
+import org.apache.camel.tooling.model.DataFormatModel;
+import org.apache.camel.tooling.model.EipModel;
+import org.apache.camel.tooling.model.LanguageModel;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * MCP Tools for querying the Camel Catalog using Quarkus MCP Server.
+ */
+@ApplicationScoped
+public class CatalogTools {
+
+ private final CamelCatalog catalog;
+
+ @Inject
+ ObjectMapper mapper;
+
+ public CatalogTools() {
+ this.catalog = new DefaultCamelCatalog();
+ }
+
+ /**
+ * Tool to list available Camel components.
+ */
+ @Tool(description = "List available Camel components from the catalog. " +
+ "Returns component name, description, and labels. " +
+ "Use filter to search by name, label to filter by category.")
+ public String camel_catalog_components(
+ @ToolArg(description = "Filter components by name (case-insensitive substring match)") String filter,
+ @ToolArg(description = "Filter by category label (e.g., cloud, messaging, database, file)") String label,
+ @ToolArg(description = "Maximum number of results to return (default: 50)") Integer limit) {
+
+ int maxResults = limit != null ? limit : 50;
+
+ try {
+ List<Map<String, Object>> components = catalog.findComponentNames().stream()
+ .map(catalog::componentModel)
+ .filter(m -> m != null)
+ .filter(m -> matchesFilter(m.getScheme(), m.getTitle(), m.getDescription(), filter))
+ .filter(m -> matchesLabel(m.getLabel(), label))
+ .limit(maxResults)
+ .map(this::componentToMap)
+ .collect(Collectors.toList());
+
+ JsonObject resultJson = new JsonObject();
+ resultJson.put("count", components.size());
+ resultJson.put("camelVersion", catalog.getCatalogVersion());
+ JsonArray arr = new JsonArray();
+ components.forEach(c -> {
+ JsonObject jo = new JsonObject();
+ jo.putAll(c);
+ arr.add(jo);
+ });
+ resultJson.put("components", arr);
+
+ return resultJson.toJson();
+ } catch (Exception e) {
+ throw new ToolCallException("Failed to list components: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Tool to get detailed documentation for a specific component.
+ */
+ @Tool(description = "Get detailed documentation for a Camel component including all options, " +
+ "endpoint parameters, and usage examples.")
+ public String camel_catalog_component_doc(
+ @ToolArg(description = "Component name (e.g., kafka, http, file, timer)") String component) {
+
+ if (component == null || component.isBlank()) {
+ throw new ToolCallException("Component name is required", null);
+ }
+
+ ComponentModel model = catalog.componentModel(component);
+ if (model == null) {
+ throw new ToolCallException("Component not found: " + component, null);
+ }
+
+ JsonObject resultJson = componentToDetailedJson(model);
+ return resultJson.toJson();
+ }
+
+ /**
+ * Tool to list data formats.
+ */
+ @Tool(description = "List available Camel data formats for marshalling/unmarshalling " +
+ "(e.g., json, xml, csv, avro, protobuf).")
+ public String camel_catalog_dataformats(
+ @ToolArg(description = "Filter by name") String filter,
+ @ToolArg(description = "Maximum results (default: 50)") Integer limit) {
+
+ int maxResults = limit != null ? limit : 50;
+
+ try {
+ List<Map<String, Object>> dataFormats = catalog.findDataFormatNames().stream()
+ .map(catalog::dataFormatModel)
+ .filter(m -> m != null)
+ .filter(m -> matchesFilter(m.getName(), m.getTitle(), m.getDescription(), filter))
+ .limit(maxResults)
+ .map(this::dataFormatToMap)
+ .collect(Collectors.toList());
+
+ JsonObject resultJson = new JsonObject();
+ resultJson.put("count", dataFormats.size());
+ JsonArray arr = new JsonArray();
+ dataFormats.forEach(df -> {
+ JsonObject jo = new JsonObject();
+ jo.putAll(df);
+ arr.add(jo);
+ });
+ resultJson.put("dataFormats", arr);
+
+ return resultJson.toJson();
+ } catch (Exception e) {
+ throw new ToolCallException("Failed to list data formats: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Tool to list expression languages.
+ */
+ @Tool(description = "List available Camel expression languages " +
+ "(e.g., simple, jsonpath, xpath, groovy, jq).")
+ public String camel_catalog_languages(
+ @ToolArg(description = "Filter by name") String filter) {
+
+ try {
+ List<Map<String, Object>> languages = catalog.findLanguageNames().stream()
+ .map(catalog::languageModel)
+ .filter(m -> m != null)
+ .filter(m -> matchesFilter(m.getName(), m.getTitle(), m.getDescription(), filter))
+ .map(this::languageToMap)
+ .collect(Collectors.toList());
+
+ JsonObject resultJson = new JsonObject();
+ resultJson.put("count", languages.size());
+ JsonArray arr = new JsonArray();
+ languages.forEach(l -> {
+ JsonObject jo = new JsonObject();
+ jo.putAll(l);
+ arr.add(jo);
+ });
+ resultJson.put("languages", arr);
+
+ return resultJson.toJson();
+ } catch (Exception e) {
+ throw new ToolCallException("Failed to list languages: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Tool to list EIPs (Enterprise Integration Patterns).
+ */
+ @Tool(description = "List Camel Enterprise Integration Patterns (EIPs) like split, aggregate, " +
+ "filter, choice, multicast, circuit-breaker, etc.")
+ public String camel_catalog_eips(
+ @ToolArg(description = "Filter by name") String filter,
+ @ToolArg(description = "Filter by category (e.g., routing, transformation, error handling)") String label) {
+
+ try {
+ List<Map<String, Object>> eips = catalog.findModelNames().stream()
+ .map(catalog::eipModel)
+ .filter(m -> m != null)
+ .filter(m -> matchesFilter(m.getName(), m.getTitle(), m.getDescription(), filter))
+ .filter(m -> matchesLabel(m.getLabel(), label))
+ .map(this::eipToMap)
+ .collect(Collectors.toList());
+
+ JsonObject resultJson = new JsonObject();
+ resultJson.put("count", eips.size());
+ JsonArray arr = new JsonArray();
+ eips.forEach(e -> {
+ JsonObject jo = new JsonObject();
+ jo.putAll(e);
+ arr.add(jo);
+ });
+ resultJson.put("eips", arr);
+
+ return resultJson.toJson();
+ } catch (Exception e) {
+ throw new ToolCallException("Failed to list EIPs: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Tool to get detailed documentation for a specific EIP.
+ */
+ @Tool(description = "Get detailed documentation for a Camel EIP (Enterprise Integration Pattern).")
+ public String camel_catalog_eip_doc(
+ @ToolArg(description = "EIP name (e.g., split, aggregate, choice, filter)") String eip) {
+
+ if (eip == null || eip.isBlank()) {
+ throw new ToolCallException("EIP name is required", null);
+ }
+
+ EipModel model = catalog.eipModel(eip);
+ if (model == null) {
+ throw new ToolCallException("EIP not found: " + eip, null);
+ }
+
+ JsonObject resultJson = eipToDetailedJson(model);
+ return resultJson.toJson();
+ }
+
+ // Helper methods
+
+ private boolean matchesFilter(String name, String title, String description, String filter) {
+ if (filter == null || filter.isBlank()) {
+ return true;
+ }
+ String lowerFilter = filter.toLowerCase();
+ return (name != null && name.toLowerCase().contains(lowerFilter))
+ || (title != null && title.toLowerCase().contains(lowerFilter))
+ || (description != null && description.toLowerCase().contains(lowerFilter));
+ }
+
+ private boolean matchesLabel(String labels, String labelFilter) {
+ if (labelFilter == null || labelFilter.isBlank()) {
+ return true;
+ }
+ if (labels == null) {
+ return false;
+ }
+ return labels.toLowerCase().contains(labelFilter.toLowerCase());
+ }
+
+ private Map<String, Object> componentToMap(ComponentModel model) {
+ return Map.of(
+ "name", model.getScheme(),
+ "title", model.getTitle() != null ? model.getTitle() : "",
+ "description", model.getDescription() != null ? model.getDescription() : "",
+ "label", model.getLabel() != null ? model.getLabel() : "",
+ "deprecated", model.isDeprecated(),
+ "supportLevel", model.getSupportLevel() != null ? model.getSupportLevel().name() : "");
+ }
+
+ private JsonObject componentToDetailedJson(ComponentModel model) {
+ JsonObject jo = new JsonObject();
+ jo.put("name", model.getScheme());
+ jo.put("title", model.getTitle());
+ jo.put("description", model.getDescription());
+ jo.put("label", model.getLabel());
+ jo.put("deprecated", model.isDeprecated());
+ jo.put("supportLevel", model.getSupportLevel() != null ? model.getSupportLevel().name() : null);
+ jo.put("groupId", model.getGroupId());
+ jo.put("artifactId", model.getArtifactId());
+ jo.put("version", model.getVersion());
+ jo.put("syntax", model.getSyntax());
+ jo.put("async", model.isAsync());
+ jo.put("consumerOnly", model.isConsumerOnly());
+ jo.put("producerOnly", model.isProducerOnly());
+
+ JsonArray options = new JsonArray();
+ if (model.getComponentOptions() != null) {
+ model.getComponentOptions().forEach(opt -> {
+ JsonObject optJson = new JsonObject();
+ optJson.put("name", opt.getName());
+ optJson.put("description", opt.getDescription());
+ optJson.put("type", opt.getType());
+ optJson.put("required", opt.isRequired());
+ optJson.put("defaultValue", opt.getDefaultValue());
+ options.add(optJson);
+ });
+ }
+ jo.put("componentOptions", options);
+
+ JsonArray endpointOptions = new JsonArray();
+ if (model.getEndpointOptions() != null) {
+ model.getEndpointOptions().forEach(opt -> {
+ JsonObject optJson = new JsonObject();
+ optJson.put("name", opt.getName());
+ optJson.put("description", opt.getDescription());
+ optJson.put("type", opt.getType());
+ optJson.put("required", opt.isRequired());
+ optJson.put("defaultValue", opt.getDefaultValue());
+ optJson.put("group", opt.getGroup());
+ endpointOptions.add(optJson);
+ });
+ }
+ jo.put("endpointOptions", endpointOptions);
+
+ return jo;
+ }
+
+ private Map<String, Object> dataFormatToMap(DataFormatModel model) {
+ return Map.of(
+ "name", model.getName(),
+ "title", model.getTitle() != null ? model.getTitle() : "",
+ "description", model.getDescription() != null ? model.getDescription() : "",
+ "deprecated", model.isDeprecated());
+ }
+
+ private Map<String, Object> languageToMap(LanguageModel model) {
+ return Map.of(
+ "name", model.getName(),
+ "title", model.getTitle() != null ? model.getTitle() : "",
+ "description", model.getDescription() != null ? model.getDescription() : "");
+ }
+
+ private Map<String, Object> eipToMap(EipModel model) {
+ return Map.of(
+ "name", model.getName(),
+ "title", model.getTitle() != null ? model.getTitle() : "",
+ "description", model.getDescription() != null ? model.getDescription() : "",
+ "label", model.getLabel() != null ? model.getLabel() : "");
+ }
+
+ private JsonObject eipToDetailedJson(EipModel model) {
+ JsonObject jo = new JsonObject();
+ jo.put("name", model.getName());
+ jo.put("title", model.getTitle());
+ jo.put("description", model.getDescription());
+ jo.put("label", model.getLabel());
+
+ JsonArray options = new JsonArray();
+ if (model.getOptions() != null) {
+ model.getOptions().forEach(opt -> {
+ JsonObject optJson = new JsonObject();
+ optJson.put("name", opt.getName());
+ optJson.put("description", opt.getDescription());
+ optJson.put("type", opt.getType());
+ optJson.put("required", opt.isRequired());
+ optJson.put("defaultValue", opt.getDefaultValue());
+ options.add(optJson);
+ });
+ }
+ jo.put("options", options);
+
+ return jo;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExplainTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExplainTools.java
new file mode 100644
index 000000000000..0ea1ffd5364d
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExplainTools.java
@@ -0,0 +1,212 @@
+/*
+ * 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.dsl.jbang.core.commands.mcp;
+
+import java.util.Arrays;
+import java.util.List;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+import dev.langchain4j.model.chat.ChatModel;
+import io.quarkiverse.mcp.server.Tool;
+import io.quarkiverse.mcp.server.ToolArg;
+import io.quarkiverse.mcp.server.ToolCallException;
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.tooling.model.ComponentModel;
+import org.apache.camel.tooling.model.EipModel;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * MCP Tool for explaining Camel routes using AI/LLM services.
+ * <p>
+ * The LLM is configured at the application level via application.properties. Supported configurations include:
+ * <ul>
+ * <li>Ollama: quarkus.langchain4j.ollama.*</li>
+ * <li>OpenAI: quarkus.langchain4j.openai.*</li>
+ * </ul>
+ */
+@ApplicationScoped
+public class ExplainTools {
+
+ private static final List<String> COMMON_COMPONENTS = Arrays.asList(
+ "kafka", "http", "https", "file", "timer", "direct", "seda",
+ "log", "mock", "rest", "rest-api", "sql", "jms", "activemq",
+ "aws2-s3", "aws2-sqs", "aws2-sns", "aws2-kinesis",
+ "azure-storage-blob", "azure-storage-queue",
+ "google-pubsub", "google-storage",
+ "mongodb", "couchdb", "cassandraql",
+ "elasticsearch", "opensearch",
+ "ftp", "sftp", "ftps",
+ "smtp", "imap", "pop3",
+ "websocket", "netty-http", "vertx-http",
+ "bean", "process", "script");
+
+ private static final List<String> COMMON_EIPS = Arrays.asList(
+ "split", "aggregate", "filter", "choice", "when", "otherwise",
+ "multicast", "recipientList", "routingSlip", "dynamicRouter",
+ "circuitBreaker", "throttle", "delay", "resequence",
+ "idempotentConsumer", "loadBalance", "saga",
+ "onException", "doTry", "doCatch", "doFinally",
+ "transform", "setBody", "setHeader", "removeHeader",
+ "marshal", "unmarshal", "convertBodyTo",
+ "enrich", "pollEnrich", "wireTap", "pipeline");
+
+ private final CamelCatalog catalog;
+
+ @Inject
+ ChatModel chatModel;
+
+ public ExplainTools() {
+ this.catalog = new DefaultCamelCatalog();
+ }
+
+ /**
+ * Tool to explain a Camel route using AI/LLM.
+ */
+ @Tool(description = "Explain what a Camel route does using AI/LLM. " +
+ "Returns a human-readable explanation of the route's purpose and flow. " +
+ "The LLM is configured at the server level via application.properties.")
+ public String camel_explain_route(
+ @ToolArg(description = "The Camel route content (YAML, XML, or Java DSL)") String route,
+ @ToolArg(description = "Route format: yaml, xml, or java (default: yaml)") String format,
+ @ToolArg(description = "Include Camel Catalog context in prompt (default: true)") Boolean catalogContext,
+ @ToolArg(description = "Include verbose technical details (default: false)") Boolean verbose) {
+
+ if (route == null || route.isBlank()) {
+ throw new ToolCallException("Route content is required", null);
+ }
+
+ if (chatModel == null) {
+ throw new ToolCallException(
+ "No LLM configured. Please configure an LLM provider in application.properties " +
+ "(e.g., quarkus.langchain4j.ollama.* or quarkus.langchain4j.openai.*)",
+ null);
+ }
+
+ // Set defaults
+ String resolvedFormat = format != null && !format.isBlank() ? format.toLowerCase() : "yaml";
+ boolean useCatalogContext = catalogContext == null || catalogContext;
+ boolean useVerbose = verbose != null && verbose;
+
+ // Build the prompt
+ String systemPrompt = buildSystemPrompt(useVerbose);
+ String userPrompt = buildUserPrompt(route, resolvedFormat, useCatalogContext);
+ String fullPrompt = systemPrompt + "\n\n" + userPrompt;
+
+ // Call the injected LLM
+ String explanation;
+ try {
+ explanation = chatModel.chat(fullPrompt);
+ } catch (Exception e) {
+ throw new ToolCallException("Error calling LLM: " + e.getMessage(), e);
+ }
+
+ if (explanation == null || explanation.isBlank()) {
+ throw new ToolCallException("Failed to get explanation from LLM", null);
+ }
+
+ // Build result
+ JsonObject result = new JsonObject();
+ result.put("explanation", explanation);
+ result.put("format", resolvedFormat);
+
+ return result.toJson();
+ }
+
+ private String buildSystemPrompt(boolean verbose) {
+ StringBuilder prompt = new StringBuilder();
+ prompt.append("You are an Apache Camel integration expert. ");
+ prompt.append("Your task is to explain Camel routes in plain English.\n\n");
+ prompt.append("Guidelines:\n");
+ prompt.append("- Start with a one-sentence summary\n");
+ prompt.append("- Describe step by step what the route does\n");
+ prompt.append("- Explain each component and EIP used\n");
+ prompt.append("- Mention error handling if present\n");
+ prompt.append("- Use bullet points for clarity\n");
+ prompt.append("- Format output as Markdown\n");
+
+ if (verbose) {
+ prompt.append("- Include technical details about options and expressions\n");
+ }
+
+ return prompt.toString();
+ }
+
+ private String buildUserPrompt(String routeContent, String fileExtension, boolean includeCatalogContext) {
+ StringBuilder prompt = new StringBuilder();
+
+ if (includeCatalogContext) {
+ String catalogInfo = buildCatalogContext(routeContent);
+ if (!catalogInfo.isEmpty()) {
+ prompt.append("Reference information:\n").append(catalogInfo).append("\n");
+ }
+ }
+
+ prompt.append("Format: ").append(fileExtension.toUpperCase()).append("\n\n");
+ prompt.append("Route definition:\n```").append(fileExtension).append("\n");
+ prompt.append(routeContent).append("\n```\n\n");
+ prompt.append("Please explain this Camel route:");
+
+ return prompt.toString();
+ }
+
+ private String buildCatalogContext(String routeContent) {
+ StringBuilder context = new StringBuilder();
+ String lowerContent = routeContent.toLowerCase();
+
+ // Add component descriptions
+ for (String comp : COMMON_COMPONENTS) {
+ if (containsComponent(lowerContent, comp)) {
+ ComponentModel model = catalog.componentModel(comp);
+ if (model != null && model.getDescription() != null) {
+ context.append("- ").append(comp).append(": ").append(model.getDescription()).append("\n");
+ }
+ }
+ }
+
+ // Add EIP descriptions
+ for (String eip : COMMON_EIPS) {
+ if (lowerContent.contains(eip.toLowerCase()) || lowerContent.contains(camelCaseToDash(eip))) {
+ EipModel model = catalog.eipModel(eip);
+ if (model != null && model.getDescription() != null) {
+ context.append("- ").append(eip).append(" (EIP): ").append(model.getDescription()).append("\n");
+ }
+ }
+ }
+
+ return context.toString();
+ }
+
+ private boolean containsComponent(String content, String comp) {
+ return content.contains(comp + ":")
+ || content.contains("\"" + comp + "\"")
+ || content.contains("'" + comp + "'");
+ }
+
+ private String camelCaseToDash(String text) {
+ StringBuilder sb = new StringBuilder();
+ for (char c : text.toCharArray()) {
+ if (Character.isUpperCase(c) && sb.length() > 0) {
+ sb.append('-');
+ }
+ sb.append(Character.toLowerCase(c));
+ }
+ return sb.toString();
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java
new file mode 100644
index 000000000000..d80ba6ea10d6
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java
@@ -0,0 +1,188 @@
+/*
+ * 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.dsl.jbang.core.commands.mcp;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+import io.quarkiverse.mcp.server.Tool;
+import io.quarkiverse.mcp.server.ToolArg;
+import io.quarkiverse.mcp.server.ToolCallException;
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.catalog.EndpointValidationResult;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * MCP Tools for validating and transforming Camel routes using Quarkus MCP Server.
+ */
+@ApplicationScoped
+public class TransformTools {
+
+ private final CamelCatalog catalog;
+
+ public TransformTools() {
+ this.catalog = new DefaultCamelCatalog();
+ }
+
+ /**
+ * Tool to validate a Camel route or endpoint URI.
+ */
+ @Tool(description = "Validate a Camel endpoint URI or route definition. " +
+ "Checks syntax, required options, and valid parameter names.")
+ public String camel_validate_route(
+ @ToolArg(description = "Camel endpoint URI to validate (e.g., 'kafka:myTopic?brokers=localhost:9092')") String uri,
+ @ToolArg(description = "YAML route definition to validate") String route) {
+
+ if (uri == null && route == null) {
+ throw new ToolCallException("Either 'uri' or 'route' is required", null);
+ }
+
+ JsonObject resultJson = new JsonObject();
+
+ if (uri != null) {
+ EndpointValidationResult validation = catalog.validateEndpointProperties(uri);
+ resultJson.put("uri", uri);
+ resultJson.put("valid", validation.isSuccess());
+
+ if (!validation.isSuccess()) {
+ JsonObject errors = new JsonObject();
+ if (validation.getUnknown() != null && !validation.getUnknown().isEmpty()) {
+ errors.put("unknownOptions", String.join(", ", validation.getUnknown()));
+ }
+ if (validation.getRequired() != null && !validation.getRequired().isEmpty()) {
+ errors.put("missingRequired", String.join(", ", validation.getRequired()));
+ }
+ if (validation.getInvalidEnum() != null && !validation.getInvalidEnum().isEmpty()) {
+ errors.put("invalidEnumValues", validation.getInvalidEnum().toString());
+ }
+ if (validation.getInvalidInteger() != null && !validation.getInvalidInteger().isEmpty()) {
+ errors.put("invalidIntegers", validation.getInvalidInteger().toString());
+ }
+ if (validation.getInvalidBoolean() != null && !validation.getInvalidBoolean().isEmpty()) {
+ errors.put("invalidBooleans", validation.getInvalidBoolean().toString());
+ }
+ if (validation.getSyntaxError() != null) {
+ errors.put("syntaxError", validation.getSyntaxError());
+ }
+ resultJson.put("errors", errors);
+
+ if (validation.getUnknown() != null && validation.getUnknownSuggestions() != null) {
+ JsonObject suggestions = new JsonObject();
+ for (String unknown : validation.getUnknown()) {
+ String[] suggestionArr = validation.getUnknownSuggestions().get(unknown);
+ if (suggestionArr != null && suggestionArr.length > 0) {
+ suggestions.put(unknown, String.join(", ", suggestionArr));
+ }
+ }
+ if (!suggestions.isEmpty()) {
+ resultJson.put("suggestions", suggestions);
+ }
+ }
+ }
+ }
+
+ if (route != null) {
+ resultJson.put("routeProvided", true);
+ resultJson.put("valid", true);
+ resultJson.put("note",
+ "Full route validation requires loading the route into a CamelContext. " +
+ "Use 'camel run --validate' for complete validation.");
+
+ List<String> uris = extractUrisFromRoute(route);
+ if (!uris.isEmpty()) {
+ JsonObject uriValidations = new JsonObject();
+ boolean allValid = true;
+ for (String extractedUri : uris) {
+ EndpointValidationResult validation = catalog.validateEndpointProperties(extractedUri);
+ uriValidations.put(extractedUri, validation.isSuccess());
+ if (!validation.isSuccess()) {
+ allValid = false;
+ }
+ }
+ resultJson.put("uriValidations", uriValidations);
+ resultJson.put("valid", allValid);
+ }
+ }
+
+ return resultJson.toJson();
+ }
+
+ /**
+ * Tool to transform routes between DSL formats.
+ */
+ @Tool(description = "Transform a Camel route between different DSL formats (YAML, XML). " +
+ "Note: Java to YAML/XML transformation has limitations.")
+ public String camel_transform_route(
+ @ToolArg(description = "Route definition to transform") String route,
+ @ToolArg(description = "Source format (yaml, xml, java)") String fromFormat,
+ @ToolArg(description = "Target format (yaml, xml)") String toFormat) {
+
+ if (route == null || fromFormat == null || toFormat == null) {
+ throw new ToolCallException("route, fromFormat, and toFormat are required", null);
+ }
+
+ JsonObject resultJson = new JsonObject();
+ resultJson.put("fromFormat", fromFormat);
+ resultJson.put("toFormat", toFormat);
+ resultJson.put("note",
+ "Route transformation requires the full Camel route parser. " +
+ "Use 'camel transform route' CLI command for complete transformation.");
+ resultJson.put("supported", true);
+ resultJson.put("recommendation",
+ "Use 'camel transform route --format=" + toFormat + " <file>' for DSL transformation");
+
+ return resultJson.toJson();
+ }
+
+ /**
+ * Extract endpoint URIs from a YAML route definition.
+ */
+ private List<String> extractUrisFromRoute(String route) {
+ List<String> uris = new ArrayList<>();
+
+ String[] lines = route.split("\n");
+ for (String line : lines) {
+ line = line.trim();
+ if (line.contains(":") && !line.startsWith("#")) {
+ int colonPos = line.indexOf(":");
+ if (colonPos > 0 && colonPos < line.length() - 1) {
+ String key = line.substring(0, colonPos).trim();
+ String value = line.substring(colonPos + 1).trim();
+
+ if (value.startsWith("\"") && value.endsWith("\"")) {
+ value = value.substring(1, value.length() - 1);
+ } else if (value.startsWith("'") && value.endsWith("'")) {
+ value = value.substring(1, value.length() - 1);
+ }
+
+ if ((key.equals("uri") || key.equals("from") || key.equals("to"))
+ && value.contains(":") && !value.startsWith("$")) {
+ String scheme = value.split(":")[0];
+ if (catalog.findComponentNames().contains(scheme)) {
+ uris.add(value);
+ }
+ }
+ }
+ }
+ }
+
+ return uris;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/resources/application.properties b/dsl/camel-jbang/camel-jbang-mcp/src/main/resources/application.properties
new file mode 100644
index 000000000000..170fb968b2aa
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/resources/application.properties
@@ -0,0 +1,41 @@
+## ---------------------------------------------------------------------------
+## 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.
+## ---------------------------------------------------------------------------
+# Camel MCP Server Configuration
+# This file configures the Quarkus MCP Server for Apache Camel
+
+# Server name and version for MCP protocol
+quarkus.application.name=camel-mcp-server
+quarkus.application.version=${camel.version:4.18.0-SNAPSHOT}
+
+# Disable banner for cleaner stdio output
+quarkus.banner.enabled=false
+
+# Logging configuration - stderr only to avoid interfering with stdio transport
+quarkus.log.console.stderr=true
+quarkus.log.level=WARN
+quarkus.log.category."org.apache.camel".level=INFO
+quarkus.log.category."io.quarkiverse.mcp".level=INFO
+
+# LLM Configuration for Camel route explanation
+# Configure Ollama (default local LLM)
+quarkus.langchain4j.ollama.base-url=${OLLAMA_BASE_URL:http://localhost:11434}
+quarkus.langchain4j.ollama.chat-model.model-id=${OLLAMA_MODEL:llama3.2}
+quarkus.langchain4j.ollama.timeout=120s
+
+# To use OpenAI instead, set these environment variables:
+# QUARKUS_LANGCHAIN4J_OPENAI_API_KEY=your-api-key
+# QUARKUS_LANGCHAIN4J_OPENAI_CHAT_MODEL_MODEL_NAME=gpt-4o-mini
diff --git a/dsl/camel-jbang/pom.xml b/dsl/camel-jbang/pom.xml
index 539a068e2295..20b03bf31eef 100644
--- a/dsl/camel-jbang/pom.xml
+++ b/dsl/camel-jbang/pom.xml
@@ -37,6 +37,7 @@
<module>camel-jbang-console</module>
<module>camel-jbang-core</module>
<module>camel-jbang-main</module>
+ <module>camel-jbang-mcp</module>
<module>camel-jbang-plugin-generate</module>
<module>camel-jbang-plugin-edit</module>
<module>camel-jbang-plugin-kubernetes</module>