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:58 UTC
(camel) 01/01: CAMEL-22886 - Camel-Jbang: Add an MCP module
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>