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/03/04 14:37:16 UTC
(camel) 01/01: CAMEL-23126 - Camel-jbang-mcp: Add MCP Prompts for structured multi-step workflows to camel-jbang-mcp
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 d6ef3ef71620ed376a73352045a0187ddd3849e2
Author: Andrea Cosentino <an...@gmail.com>
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 <an...@gmail.com>
---
.../org/apache/camel/catalog/components/seda.json | 2 +-
.../org/apache/camel/catalog/components/stub.json | 2 +-
.../endpoint/dsl/SedaEndpointBuilderFactory.java | 12 +-
.../endpoint/dsl/StubEndpointBuilderFactory.java | 12 +-
.../jbang/core/commands/mcp/PromptDefinitions.java | 224 +++++++++++++++++++++
.../core/commands/mcp/PromptDefinitionsTest.java | 208 +++++++++++++++++++
6 files changed, 446 insertions(+), 14 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-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SedaEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SedaEndpointBuilderFactory.java
index 4a8dabc31c6c..6c1890a0b328 100644
--- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SedaEndpointBuilderFactory.java
+++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SedaEndpointBuilderFactory.java
@@ -81,12 +81,12 @@ public interface SedaEndpointBuilderFactory {
* Number of concurrent threads processing exchanges. When
* virtualThreadPerTask is enabled, this becomes a concurrency limit (0
* = unlimited) and defaults to 0 instead of 1.
- *
+ *
* The option is a: <code>int</code> type.
- *
+ *
* Default: 1
* Group: consumer
- *
+ *
* @param concurrentConsumers the value to set
* @return the dsl builder
*/
@@ -98,12 +98,12 @@ public interface SedaEndpointBuilderFactory {
* Number of concurrent threads processing exchanges. When
* virtualThreadPerTask is enabled, this becomes a concurrency limit (0
* = unlimited) and defaults to 0 instead of 1.
- *
+ *
* The option will be converted to a <code>int</code> type.
- *
+ *
* Default: 1
* Group: consumer
- *
+ *
* @param concurrentConsumers the value to set
* @return the dsl builder
*/
diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/StubEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/StubEndpointBuilderFactory.java
index 5be38587183d..6fd4b9f479c1 100644
--- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/StubEndpointBuilderFactory.java
+++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/StubEndpointBuilderFactory.java
@@ -81,12 +81,12 @@ public interface StubEndpointBuilderFactory {
* Number of concurrent threads processing exchanges. When
* virtualThreadPerTask is enabled, this becomes a concurrency limit (0
* = unlimited) and defaults to 0 instead of 1.
- *
+ *
* The option is a: <code>int</code> type.
- *
+ *
* Default: 1
* Group: consumer
- *
+ *
* @param concurrentConsumers the value to set
* @return the dsl builder
*/
@@ -98,12 +98,12 @@ public interface StubEndpointBuilderFactory {
* Number of concurrent threads processing exchanges. When
* virtualThreadPerTask is enabled, this becomes a concurrency limit (0
* = unlimited) and defaults to 0 instead of 1.
- *
+ *
* The option will be converted to a <code>int</code> type.
- *
+ *
* Default: 1
* Group: consumer
- *
+ *
* @param concurrentConsumers the value to set
* @return the dsl builder
*/
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();
+ }
+}