This is an automated email from the ASF dual-hosted git repository. acosentino pushed a commit to branch 23126 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 28fe2c786ce7c6c7176354cf5c1901ecd8698674 Author: Andrea Cosentino <[email protected]> AuthorDate: Wed Mar 4 14:32:55 2026 +0100 CAMEL-23126 - Camel-jbang-mcp: Add MCP Prompts for structured multi-step workflows to camel-jbang-mcp Signed-off-by: Andrea Cosentino <[email protected]> --- .../org/apache/camel/catalog/components/seda.json | 2 +- .../org/apache/camel/catalog/components/stub.json | 2 +- .../jbang/core/commands/mcp/PromptDefinitions.java | 224 +++++++++++++++++++++ .../core/commands/mcp/PromptDefinitionsTest.java | 208 +++++++++++++++++++ 4 files changed, 434 insertions(+), 2 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/seda.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/seda.json index fcafafdfaae0..d2ef0a7fcc49 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/seda.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/seda.json @@ -38,7 +38,7 @@ "properties": { "name": { "index": 0, "kind": "path", "displayName": "Name", "group": "common", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Name of queue" }, "size": { "index": 1, "kind": "parameter", "displayName": "Size", "group": "common", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 1000, "description": "The maximum capacity of the SEDA queue (i.e., the number of messages it can hold). Will by default use the queueSize set on the SEDA component." }, - "concurrentConsumers": { "index": 2, "kind": "parameter", "displayName": "Concurrent Consumers", "group": "consumer", "label": "consumer", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 1, "description": "Number of concurrent threads processing exchanges." }, + "concurrentConsumers": { "index": 2, "kind": "parameter", "displayName": "Concurrent Consumers", "group": "consumer", "label": "consumer", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 1, "description": "Number of concurrent threads processing exchanges. When virtualThreadPerTask is enabled, this becomes a concurrency limit (0 = unlimited) and defaults to 0 instead of 1." }, "bridgeErrorHandler": { "index": 3, "kind": "parameter", "displayName": "Bridge Error Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Allows for bridging the consumer to the Camel routing Error Handler, which mean any exceptions (if possible) occurred while the Camel consumer is trying to pickup incoming [...] "exceptionHandler": { "index": 4, "kind": "parameter", "displayName": "Exception Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", "deprecated": false, "autowired": false, "secret": false, "description": "To let the consumer use a custom ExceptionHandler. Notice if the option bridgeErrorHandler is enabled then this option is not in use. By def [...] "exchangePattern": { "index": 5, "kind": "parameter", "displayName": "Exchange Pattern", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], "deprecated": false, "autowired": false, "secret": false, "description": "Sets the exchange pattern when the consumer creates an exchange." }, diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/stub.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/stub.json index 6d33364a3a0c..286a319f9264 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/stub.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/stub.json @@ -40,7 +40,7 @@ "properties": { "name": { "index": 0, "kind": "path", "displayName": "Name", "group": "common", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Name of queue" }, "size": { "index": 1, "kind": "parameter", "displayName": "Size", "group": "common", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 1000, "description": "The maximum capacity of the SEDA queue (i.e., the number of messages it can hold). Will by default use the queueSize set on the SEDA component." }, - "concurrentConsumers": { "index": 2, "kind": "parameter", "displayName": "Concurrent Consumers", "group": "consumer", "label": "consumer", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 1, "description": "Number of concurrent threads processing exchanges." }, + "concurrentConsumers": { "index": 2, "kind": "parameter", "displayName": "Concurrent Consumers", "group": "consumer", "label": "consumer", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 1, "description": "Number of concurrent threads processing exchanges. When virtualThreadPerTask is enabled, this becomes a concurrency limit (0 = unlimited) and defaults to 0 instead of 1." }, "bridgeErrorHandler": { "index": 3, "kind": "parameter", "displayName": "Bridge Error Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Allows for bridging the consumer to the Camel routing Error Handler, which mean any exceptions (if possible) occurred while the Camel consumer is trying to pickup incoming [...] "exceptionHandler": { "index": 4, "kind": "parameter", "displayName": "Exception Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", "deprecated": false, "autowired": false, "secret": false, "description": "To let the consumer use a custom ExceptionHandler. Notice if the option bridgeErrorHandler is enabled then this option is not in use. By def [...] "exchangePattern": { "index": 5, "kind": "parameter", "displayName": "Exchange Pattern", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], "deprecated": false, "autowired": false, "secret": false, "description": "Sets the exchange pattern when the consumer creates an exchange." }, diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitions.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitions.java new file mode 100644 index 000000000000..9bd4c345724e --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitions.java @@ -0,0 +1,224 @@ +/* + * 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 jakarta.enterprise.context.ApplicationScoped; + +import io.quarkiverse.mcp.server.Prompt; +import io.quarkiverse.mcp.server.PromptArg; +import io.quarkiverse.mcp.server.PromptMessage; + +/** + * MCP Prompt definitions that provide structured multi-step workflows for LLMs. + * <p> + * Prompts guide the LLM through orchestrating multiple existing tools in the correct sequence, rather than requiring it + * to discover the workflow on its own. + */ +@ApplicationScoped +public class PromptDefinitions { + + /** + * Guided workflow for building a Camel integration from requirements. + */ + @Prompt(name = "camel_build_integration", + description = "Guided workflow to build a Camel integration: " + + "analyze requirements, discover components and EIPs, " + + "generate a YAML route, validate it, and run a security check.") + public List<PromptMessage> buildIntegration( + @PromptArg(name = "requirements", + description = "Natural-language description of what the integration should do") String requirements, + @PromptArg(name = "runtime", description = "Target runtime: main, spring-boot, or quarkus (default: main)", + required = false) String runtime) { + + String resolvedRuntime = runtime != null && !runtime.isBlank() ? runtime : "main"; + + String instructions = """ + You are building a Camel integration for the "%s" runtime. + + ## Requirements + %s + + ## Workflow + + Follow these steps in order: + + ### Step 1: Identify components + Analyze the requirements above and identify the Camel components needed. + Call `camel_catalog_components` with a relevant filter and runtime="%s" to find matching components. + + ### Step 2: Identify EIPs + Determine which Enterprise Integration Patterns are needed (e.g., split, aggregate, filter, choice). + Call `camel_catalog_eips` with a relevant filter to find matching patterns. + + ### Step 3: Get component details + For each component you selected, call `camel_catalog_component_doc` with the component name \ + and runtime="%s" to get its endpoint options, required parameters, and URI syntax. + + ### Step 4: Build the route + Using the gathered information, write a complete YAML route definition. \ + Use correct component URI syntax and required options from the documentation. + + ### Step 5: Validate + Call `camel_validate_yaml_dsl` with the generated YAML route to check for syntax errors. + If validation fails, fix the issues and re-validate. + + ### Step 6: Security review + Call `camel_route_harden_context` with the generated route and format="yaml" \ + to identify security concerns. Address any critical or high-severity findings. + + ### Step 7: Present result + Present the final YAML route along with: + - A brief explanation of each component and EIP used + - Any security recommendations from Step 6 + - Instructions for running the route (e.g., with camel-jbang) + """.formatted(resolvedRuntime, requirements, resolvedRuntime, resolvedRuntime); + + return List.of(PromptMessage.withUserRole(instructions)); + } + + /** + * Guided workflow for migrating a Camel project to a new version. + */ + @Prompt(name = "camel_migrate_project", + description = "Guided workflow to migrate a Camel project: " + + "analyze the pom.xml, check compatibility, " + + "get OpenRewrite recipes, search migration guides, " + + "and produce a migration summary.") + public List<PromptMessage> migrateProject( + @PromptArg(name = "pomContent", description = "The project's pom.xml file content") String pomContent, + @PromptArg(name = "targetVersion", description = "Target Camel version to migrate to (e.g., 4.18.0)", + required = false) String targetVersion) { + + String versionNote = targetVersion != null && !targetVersion.isBlank() + ? "Target version: " + targetVersion + : "Target version: latest stable (determine from camel_version_list)"; + + String instructions = """ + You are migrating a Camel project to a newer version. + + ## %s + + ## Project pom.xml + ```xml + %s + ``` + + ## Workflow + + Follow these steps in order: + + ### Step 1: Analyze the project + Call `camel_migration_analyze` with the pom.xml content above. + This detects the current runtime, Camel version, Java version, and component dependencies. + + ### Step 2: Determine target version + If no target version was specified, call `camel_version_list` with the detected runtime \ + to find the latest stable version. For LTS releases, filter with lts=true. + + ### Step 3: Check compatibility + Based on the detected runtime from Step 1: + - For **wildfly** or **karaf** runtimes: call `camel_migration_wildfly_karaf` with the pom.xml \ + content, target runtime, and target version. + - For **main**, **spring-boot**, or **quarkus** runtimes: call `camel_migration_compatibility` \ + with the detected components, current version, target version, runtime, and Java version. + + Review any blockers (e.g., Java version too old) and warnings. + + ### Step 4: Get migration recipes + Call `camel_migration_recipes` with the runtime, current version, target version, \ + Java version, and dryRun=true to get the OpenRewrite Maven commands. + + ### Step 5: Search for breaking changes + For each component detected in Step 1, call `camel_migration_guide_search` \ + with the component name to find relevant breaking changes and rename mappings. + + ### Step 6: Produce migration summary + Present a structured summary: + - **Current state**: runtime, Camel version, Java version, component count + - **Target state**: target version, required Java version + - **Blockers**: issues that must be resolved before migration + - **Breaking changes**: component renames, API changes found in guides + - **Migration commands**: the OpenRewrite commands from Step 4 + - **Manual steps**: any changes that OpenRewrite cannot automate + """.formatted(versionNote, pomContent); + + return List.of(PromptMessage.withUserRole(instructions)); + } + + /** + * Guided workflow for a security review of a Camel route. + */ + @Prompt(name = "camel_security_review", + description = "Guided workflow to perform a security audit of a Camel route: " + + "analyze security-sensitive components, check for vulnerabilities, " + + "and produce an actionable audit checklist.") + public List<PromptMessage> securityReview( + @PromptArg(name = "route", description = "The Camel route content to review") String route, + @PromptArg(name = "format", description = "Route format: yaml, xml, or java (default: yaml)", + required = false) String format) { + + String resolvedFormat = format != null && !format.isBlank() ? format : "yaml"; + + String instructions = """ + You are performing a security audit of a Camel route. + + ## Route (format: %s) + ``` + %s + ``` + + ## Workflow + + Follow these steps in order: + + ### Step 1: Analyze security + Call `camel_route_harden_context` with the route above and format="%s". + This returns security-sensitive components, vulnerabilities, and risk levels. + + ### Step 2: Understand route structure + Call `camel_route_context` with the route and format="%s". + This returns the components and EIPs used, helping you understand the full data flow. + + ### Step 3: Produce audit checklist + Using the results from Steps 1 and 2, produce a structured security audit report: + + **Critical Issues** (must fix before production): + - List all critical-severity concerns from the security analysis + - For each: describe the issue, the affected component, and the specific fix + + **Warnings** (should fix): + - List all high and medium-severity concerns + - For each: describe the risk and the recommended mitigation + + **Positive Findings** (already secured): + - List all positive security findings (TLS enabled, property placeholders used, etc.) + + **Recommendations**: + - Provide actionable, prioritized recommendations based on the specific components used + - Reference the relevant security best practices for each component + - Include specific configuration examples where applicable + + **Compliance Notes**: + - Note any components that handle PII or sensitive data + - Flag any components that communicate over the network without encryption + """.formatted(resolvedFormat, route, resolvedFormat, resolvedFormat); + + return List.of(PromptMessage.withUserRole(instructions)); + } +} diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitionsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitionsTest.java new file mode 100644 index 000000000000..64fd0da9eea2 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitionsTest.java @@ -0,0 +1,208 @@ +/* + * 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 io.quarkiverse.mcp.server.PromptMessage; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class PromptDefinitionsTest { + + private final PromptDefinitions prompts = new PromptDefinitions(); + + // ---- camel_build_integration ---- + + @Test + void buildIntegrationReturnsNonEmptyMessages() { + List<PromptMessage> result = prompts.buildIntegration("Read from Kafka and write to S3", "spring-boot"); + + assertThat(result).isNotEmpty(); + } + + @Test + void buildIntegrationContainsRequirements() { + List<PromptMessage> result = prompts.buildIntegration("Read from Kafka and write to S3", null); + + String text = extractText(result); + assertThat(text).contains("Read from Kafka and write to S3"); + } + + @Test + void buildIntegrationReferencesTools() { + List<PromptMessage> result = prompts.buildIntegration("poll an FTP server", null); + + String text = extractText(result); + assertThat(text).contains("camel_catalog_components"); + assertThat(text).contains("camel_catalog_eips"); + assertThat(text).contains("camel_catalog_component_doc"); + assertThat(text).contains("camel_validate_yaml_dsl"); + assertThat(text).contains("camel_route_harden_context"); + } + + @Test + void buildIntegrationDefaultsRuntimeToMain() { + List<PromptMessage> result = prompts.buildIntegration("timer to log", null); + + String text = extractText(result); + assertThat(text).contains("\"main\" runtime"); + } + + @Test + void buildIntegrationUsesSpecifiedRuntime() { + List<PromptMessage> result = prompts.buildIntegration("timer to log", "quarkus"); + + String text = extractText(result); + assertThat(text).contains("\"quarkus\" runtime"); + } + + @Test + void buildIntegrationBlankRuntimeDefaultsToMain() { + List<PromptMessage> result = prompts.buildIntegration("timer to log", " "); + + String text = extractText(result); + assertThat(text).contains("\"main\" runtime"); + } + + // ---- camel_migrate_project ---- + + @Test + void migrateProjectReturnsNonEmptyMessages() { + List<PromptMessage> result = prompts.migrateProject("<project/>", "4.18.0"); + + assertThat(result).isNotEmpty(); + } + + @Test + void migrateProjectContainsPomContent() { + String pom = "<project><version>3.20.0</version></project>"; + List<PromptMessage> result = prompts.migrateProject(pom, null); + + String text = extractText(result); + assertThat(text).contains(pom); + } + + @Test + void migrateProjectReferencesTools() { + List<PromptMessage> result = prompts.migrateProject("<project/>", "4.18.0"); + + String text = extractText(result); + assertThat(text).contains("camel_migration_analyze"); + assertThat(text).contains("camel_migration_compatibility"); + assertThat(text).contains("camel_migration_wildfly_karaf"); + assertThat(text).contains("camel_migration_recipes"); + assertThat(text).contains("camel_migration_guide_search"); + } + + @Test + void migrateProjectIncludesTargetVersion() { + List<PromptMessage> result = prompts.migrateProject("<project/>", "4.18.0"); + + String text = extractText(result); + assertThat(text).contains("Target version: 4.18.0"); + } + + @Test + void migrateProjectNullVersionSuggestsLatest() { + List<PromptMessage> result = prompts.migrateProject("<project/>", null); + + String text = extractText(result); + assertThat(text).contains("camel_version_list"); + } + + @Test + void migrateProjectBlankVersionSuggestsLatest() { + List<PromptMessage> result = prompts.migrateProject("<project/>", " "); + + String text = extractText(result); + assertThat(text).contains("camel_version_list"); + } + + // ---- camel_security_review ---- + + @Test + void securityReviewReturnsNonEmptyMessages() { + List<PromptMessage> result = prompts.securityReview("from: timer:tick", "yaml"); + + assertThat(result).isNotEmpty(); + } + + @Test + void securityReviewContainsRoute() { + String route = "from:\n uri: kafka:topic\n steps:\n - to: log:out"; + List<PromptMessage> result = prompts.securityReview(route, null); + + String text = extractText(result); + assertThat(text).contains(route); + } + + @Test + void securityReviewReferencesTools() { + List<PromptMessage> result = prompts.securityReview("from: timer:tick", null); + + String text = extractText(result); + assertThat(text).contains("camel_route_harden_context"); + assertThat(text).contains("camel_route_context"); + } + + @Test + void securityReviewDefaultsFormatToYaml() { + List<PromptMessage> result = prompts.securityReview("from: timer:tick", null); + + String text = extractText(result); + assertThat(text).contains("format: yaml"); + } + + @Test + void securityReviewUsesSpecifiedFormat() { + List<PromptMessage> result = prompts.securityReview("<route/>", "xml"); + + String text = extractText(result); + assertThat(text).contains("format: xml"); + } + + @Test + void securityReviewBlankFormatDefaultsToYaml() { + List<PromptMessage> result = prompts.securityReview("from: timer:tick", " "); + + String text = extractText(result); + assertThat(text).contains("format: yaml"); + } + + @Test + void securityReviewContainsAuditSections() { + List<PromptMessage> result = prompts.securityReview("from: timer:tick", null); + + String text = extractText(result); + assertThat(text).contains("Critical Issues"); + assertThat(text).contains("Warnings"); + assertThat(text).contains("Positive Findings"); + assertThat(text).contains("Recommendations"); + } + + // ---- helper ---- + + private String extractText(List<PromptMessage> messages) { + StringBuilder sb = new StringBuilder(); + for (PromptMessage msg : messages) { + sb.append(msg.content().asText().text()); + } + return sb.toString(); + } +}
