This is an automated email from the ASF dual-hosted git repository.

acosentino pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 4beb77dfe0b9 CAMEL-23056 - Camel JBang MCP - Add contract-first 
OpenAPI REST DSL s… (#21589)
4beb77dfe0b9 is described below

commit 4beb77dfe0b9b0f4e17129ef51a500743178fd53
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Feb 24 14:53:45 2026 +0100

    CAMEL-23056 - Camel JBang MCP - Add contract-first OpenAPI REST DSL s… 
(#21589)
    
    * CAMEL-23056 - Camel JBang MCP - Add contract-first OpenAPI REST DSL 
support tools
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-23056 - Camel JBang MCP - Add contract-first OpenAPI REST DSL 
support tools
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    ---------
    
    Signed-off-by: Andrea Cosentino <[email protected]>
---
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |  80 ++-
 dsl/camel-jbang/camel-jbang-mcp/pom.xml            |  23 +
 .../dsl/jbang/core/commands/mcp/OpenApiTools.java  | 551 +++++++++++++++++++++
 .../jbang/core/commands/mcp/OpenApiToolsTest.java  | 403 +++++++++++++++
 4 files changed, 1056 insertions(+), 1 deletion(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index 32438585bf9c..0777b7c6373e 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -24,7 +24,7 @@ By default, the HTTP server is disabled. To enable it, set 
`quarkus.http.host-en
 
 == Available Tools
 
-The server exposes 16 tools organized into six functional areas.
+The server exposes 19 tools organized into seven functional areas.
 
 === Catalog Exploration
 
@@ -118,6 +118,33 @@ The server exposes 16 tools organized into six functional 
areas.
 | Assists with route DSL format transformation between YAML and XML.
 |===
 
+=== OpenAPI Contract-First
+
+Since Camel 4.6, the recommended approach for building REST APIs from OpenAPI 
specifications is **contract-first**:
+referencing the OpenAPI spec directly at runtime via `rest:openApi` rather 
than generating REST DSL code. These tools
+help validate, scaffold, and provide mock guidance for that workflow.
+
+[cols="1,3",options="header"]
+|===
+| Tool | Description
+
+| `camel_openapi_validate`
+| Validates an OpenAPI specification for compatibility with Camel's 
contract-first REST support. Checks for missing
+  `operationId` fields, unsupported security schemes, OpenAPI 3.1 limitations, 
webhooks usage, and empty paths.
+  Returns errors, warnings, and info-level diagnostics.
+
+| `camel_openapi_scaffold`
+| Generates a Camel YAML scaffold for contract-first OpenAPI integration. 
Produces a `rest:openApi` configuration
+  block referencing the spec file and a `direct:<operationId>` route stub for 
each operation, with `Content-Type`
+  and `CamelHttpResponseCode` headers pre-configured from the spec. Supports 
configuring the `missingOperation`
+  mode (`fail`, `ignore`, or `mock`).
+
+| `camel_openapi_mock_guidance`
+| Provides guidance on configuring Camel's `missingOperation` modes (`fail`, 
`ignore`, `mock`). For `mock` mode,
+  returns the `camel-mock/` directory structure, mock file paths derived from 
the API paths, and example content
+  from the spec. Explains the behavior of each mode.
+|===
+
 === Version Management
 
 [cols="1,3",options="header"]
@@ -325,3 +352,54 @@ What are the latest LTS versions of Camel for Spring Boot?
 
 The assistant calls `camel_version_list` with `runtime=spring-boot` and 
`lts=true` and returns version
 information including release dates, end-of-life dates, and JDK requirements.
+
+=== Validating an OpenAPI Spec for Camel
+
+----
+I have this OpenAPI spec for my Pet Store API. Can you validate it for use 
with Camel's contract-first REST support?
+----
+
+Paste the OpenAPI spec (JSON or YAML) and the assistant calls 
`camel_openapi_validate`. It reports any compatibility
+issues such as missing `operationId` fields, unsupported security schemes 
(OAuth2, mutual TLS), OpenAPI 3.1
+limitations, and webhooks usage. A valid spec with no issues returns `valid: 
true` with an operation count.
+
+=== Scaffolding a Contract-First REST API
+
+----
+Generate a Camel YAML scaffold for this OpenAPI spec. The spec file will be 
called petstore.yaml
+and I want missing operations to use mock mode.
+----
+
+The assistant calls `camel_openapi_scaffold` with `specFilename=petstore.yaml` 
and `missingOperation=mock`.
+It returns a ready-to-use YAML file containing:
+
+* A `rest:openApi` configuration block referencing the spec file with 
`missingOperation: mock`
+* A `direct:<operationId>` route stub for each operation with `Content-Type` 
and response code headers
+
+=== Getting Mock Guidance
+
+----
+I want to use Camel's mock mode for my OpenAPI REST API during development. 
Show me the directory
+structure and mock files I need to create.
+----
+
+The assistant calls `camel_openapi_mock_guidance` with `mode=mock`. It returns:
+
+* An explanation of how mock mode works
+* The YAML configuration snippet with `missingOperation: mock`
+* The `camel-mock/` directory structure with mock file paths derived from the 
API paths
+* Example content for mock files based on examples defined in the spec
+
+=== Combined Contract-First Workflow
+
+For a complete prototyping workflow, you can combine all three tools:
+
+----
+I'm building a new REST API with Camel using contract-first. Here's my OpenAPI 
spec.
+Please validate it for compatibility issues, then generate the Camel YAML 
scaffold
+with mock mode so I can prototype quickly.
+----
+
+The assistant first calls `camel_openapi_validate` to check for issues, then 
calls `camel_openapi_scaffold`
+to generate the route scaffold. This gives you a validated spec and a complete 
starting point where you can
+implement routes one at a time while Camel auto-mocks the rest.
diff --git a/dsl/camel-jbang/camel-jbang-mcp/pom.xml 
b/dsl/camel-jbang/camel-jbang-mcp/pom.xml
index b3e4035b73d5..f5f8f0d427f5 100644
--- a/dsl/camel-jbang/camel-jbang-mcp/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-mcp/pom.xml
@@ -106,6 +106,29 @@
             <artifactId>camel-yaml-dsl</artifactId>
         </dependency>
 
+        <!-- Swagger/OpenAPI parser for contract-first OpenAPI tools -->
+        <dependency>
+            <groupId>io.swagger.core.v3</groupId>
+            <artifactId>swagger-core-jakarta</artifactId>
+            <version>${swagger-openapi3-version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.swagger.core.v3</groupId>
+            <artifactId>swagger-models-jakarta</artifactId>
+            <version>${swagger-openapi3-version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.swagger.parser.v3</groupId>
+            <artifactId>swagger-parser-v3</artifactId>
+            <version>${swagger-openapi3-java-parser-version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>io.swagger.core.v3</groupId>
+                    <artifactId>*</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
         <!-- Apache Commons Text for fuzzy string matching (Levenshtein 
distance) -->
         <dependency>
             <groupId>org.apache.commons</groupId>
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiTools.java
new file mode 100644
index 000000000000..bb1f9b09bc09
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiTools.java
@@ -0,0 +1,551 @@
+/*
+ * 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 java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+import io.quarkiverse.mcp.server.Tool;
+import io.quarkiverse.mcp.server.ToolArg;
+import io.quarkiverse.mcp.server.ToolCallException;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.PathItem;
+import io.swagger.v3.oas.models.media.MediaType;
+import io.swagger.v3.oas.models.responses.ApiResponse;
+import io.swagger.v3.oas.models.responses.ApiResponses;
+import io.swagger.v3.oas.models.security.SecurityScheme;
+import io.swagger.v3.parser.OpenAPIV3Parser;
+import io.swagger.v3.parser.core.models.SwaggerParseResult;
+
+/**
+ * MCP Tools for contract-first OpenAPI support in Apache Camel.
+ *
+ * Since Camel 4.6, the recommended approach is contract-first: referencing 
the OpenAPI spec directly at runtime via
+ * rest:openApi. These tools help validate, scaffold, and provide mock 
guidance for that workflow.
+ */
+@ApplicationScoped
+public class OpenApiTools {
+
+    private static final Set<String> VALID_MISSING_OPERATION_MODES = 
Set.of("fail", "ignore", "mock");
+
+    @Tool(description = "Validate an OpenAPI specification for use with 
Camel's contract-first REST support. "
+                        + "Checks for compatibility issues like missing 
operationIds, unsupported security schemes, "
+                        + "and OpenAPI 3.1 features that Camel does not fully 
support.")
+    public ValidateResult camel_openapi_validate(
+            @ToolArg(description = "OpenAPI 3.x specification content (JSON or 
YAML string)") String spec) {
+
+        OpenAPI openAPI = parseSpec(spec);
+
+        List<DiagnosticMessage> errors = new ArrayList<>();
+        List<DiagnosticMessage> warnings = new ArrayList<>();
+        List<DiagnosticMessage> info = new ArrayList<>();
+
+        // Check OpenAPI version for 3.1 limitations
+        if (openAPI.getOpenapi() != null && 
openAPI.getOpenapi().startsWith("3.1")) {
+            warnings.add(new DiagnosticMessage(
+                    "OPENAPI_31",
+                    "OpenAPI 3.1 detected. Camel supports 3.0.x fully; 3.1 
features like "
+                                  + "webhooks and advanced JSON Schema may not 
be supported.",
+                    null));
+        }
+
+        // Check for paths
+        if (openAPI.getPaths() == null || openAPI.getPaths().isEmpty()) {
+            errors.add(new DiagnosticMessage(
+                    "NO_PATHS",
+                    "No paths defined in the specification. Camel REST 
requires at least one path with operations.",
+                    null));
+        } else {
+            // Check each operation
+            for (Map.Entry<String, PathItem> pathEntry : 
openAPI.getPaths().entrySet()) {
+                String path = pathEntry.getKey();
+                PathItem pathItem = pathEntry.getValue();
+
+                if (pathItem.readOperationsMap() == null || 
pathItem.readOperationsMap().isEmpty()) {
+                    warnings.add(new DiagnosticMessage(
+                            "EMPTY_PATH_ITEM",
+                            "Path '" + path + "' has no operations defined.",
+                            path));
+                    continue;
+                }
+
+                for (Map.Entry<PathItem.HttpMethod, Operation> opEntry : 
pathItem.readOperationsMap().entrySet()) {
+                    Operation op = opEntry.getValue();
+                    String method = opEntry.getKey().name();
+                    if (op.getOperationId() == null || 
op.getOperationId().isBlank()) {
+                        String generated = "GENOPID_" + method + 
path.replace("/", "_").replace("{", "").replace("}", "");
+                        warnings.add(new DiagnosticMessage(
+                                "MISSING_OPERATION_ID",
+                                "Operation " + method + " " + path + " has no 
operationId. "
+                                                        + "Camel will 
auto-generate: " + generated,
+                                path));
+                    }
+                }
+            }
+        }
+
+        // Check webhooks
+        if (openAPI.getWebhooks() != null && !openAPI.getWebhooks().isEmpty()) 
{
+            warnings.add(new DiagnosticMessage(
+                    "WEBHOOKS_PRESENT",
+                    "Webhooks are defined in the spec but are not supported by 
Camel's REST OpenAPI integration.",
+                    null));
+        }
+
+        // Check security schemes
+        if (openAPI.getComponents() != null && 
openAPI.getComponents().getSecuritySchemes() != null) {
+            for (Map.Entry<String, SecurityScheme> schemeEntry : 
openAPI.getComponents().getSecuritySchemes().entrySet()) {
+                String name = schemeEntry.getKey();
+                SecurityScheme scheme = schemeEntry.getValue();
+                checkSecurityScheme(name, scheme, warnings, info);
+            }
+        }
+
+        int operationCount = countOperations(openAPI);
+        boolean valid = errors.isEmpty();
+
+        return new ValidateResult(valid, errors, warnings, info, 
operationCount);
+    }
+
+    @Tool(description = "Generate Camel YAML scaffold for contract-first 
OpenAPI integration. "
+                        + "Produces a rest:openApi configuration block and 
route stubs for each operation "
+                        + "defined in the spec. This is the recommended 
approach since Camel 4.6.")
+    public ScaffoldResult camel_openapi_scaffold(
+            @ToolArg(description = "OpenAPI 3.x specification content (JSON or 
YAML string)") String spec,
+            @ToolArg(description = "Filename of the OpenAPI spec file as it 
will be referenced at runtime "
+                                   + "(default: 'openapi.json')") String 
specFilename,
+            @ToolArg(description = "Behavior when a route is missing for an 
operationId: "
+                                   + "'fail' (default, throw error), 'ignore' 
(skip silently), "
+                                   + "or 'mock' (return mock responses)") 
String missingOperation) {
+
+        OpenAPI openAPI = parseSpec(spec);
+
+        String filename = (specFilename == null || specFilename.isBlank()) ? 
"openapi.json" : specFilename.strip();
+        String mode
+                = (missingOperation == null || missingOperation.isBlank()) ? 
"fail" : missingOperation.strip().toLowerCase();
+
+        if (!VALID_MISSING_OPERATION_MODES.contains(mode)) {
+            throw new ToolCallException(
+                    "'missingOperation' must be 'fail', 'ignore', or 'mock', 
got: " + missingOperation, null);
+        }
+
+        String apiTitle = openAPI.getInfo() != null ? 
openAPI.getInfo().getTitle() : null;
+        List<OperationStub> stubs = collectOperationStubs(openAPI);
+
+        StringBuilder yaml = new StringBuilder();
+
+        // rest:openApi block
+        yaml.append("- rest:\n");
+        yaml.append("    openApi:\n");
+        yaml.append("      specification: ").append(filename).append("\n");
+        if (!"fail".equals(mode)) {
+            yaml.append("      missingOperation: ").append(mode).append("\n");
+        }
+
+        // Route stubs
+        for (OperationStub stub : stubs) {
+            yaml.append("- route:\n");
+            yaml.append("    id: ").append(stub.operationId()).append("\n");
+            yaml.append("    from:\n");
+            yaml.append("      uri: 
direct:").append(stub.operationId()).append("\n");
+            yaml.append("      steps:\n");
+
+            // Set Content-Type header if we know it
+            if (stub.contentType() != null) {
+                yaml.append("        - setHeader:\n");
+                yaml.append("            name: Content-Type\n");
+                yaml.append("            constant: 
").append(stub.contentType()).append("\n");
+            }
+
+            // Set response code if we know it
+            if (stub.responseCode() != null) {
+                yaml.append("        - setHeader:\n");
+                yaml.append("            name: CamelHttpResponseCode\n");
+                yaml.append("            constant: 
").append(stub.responseCode()).append("\n");
+            }
+
+            yaml.append("        - setBody:\n");
+            yaml.append("            constant: \"TODO: implement 
").append(stub.operationId()).append("\"\n");
+        }
+
+        return new ScaffoldResult(yaml.toString(), stubs.size(), filename, 
mode, apiTitle);
+    }
+
+    @Tool(description = "Get guidance on configuring Camel's contract-first 
REST missingOperation modes "
+                        + "(fail, ignore, mock). For 'mock' mode, provides 
directory structure, mock file paths, "
+                        + "and example content derived from the OpenAPI spec.")
+    public MockGuidanceResult camel_openapi_mock_guidance(
+            @ToolArg(description = "OpenAPI 3.x specification content (JSON or 
YAML string)") String spec,
+            @ToolArg(description = "The missingOperation mode to get guidance 
for: "
+                                   + "'mock' (default), 'fail', or 'ignore'") 
String mode) {
+
+        OpenAPI openAPI = parseSpec(spec);
+
+        String effectiveMode = (mode == null || mode.isBlank()) ? "mock" : 
mode.strip().toLowerCase();
+        if (!VALID_MISSING_OPERATION_MODES.contains(effectiveMode)) {
+            throw new ToolCallException(
+                    "'mode' must be 'fail', 'ignore', or 'mock', got: " + 
mode, null);
+        }
+
+        String modeExplanation = getModeExplanation(effectiveMode);
+
+        // Configuration YAML snippet
+        StringBuilder configYaml = new StringBuilder();
+        configYaml.append("- rest:\n");
+        configYaml.append("    openApi:\n");
+        configYaml.append("      specification: openapi.json\n");
+        configYaml.append("      missingOperation: 
").append(effectiveMode).append("\n");
+
+        List<MockFileInfo> mockFiles = new ArrayList<>();
+        String directoryStructure = null;
+
+        if ("mock".equals(effectiveMode)) {
+            // Build mock file info from spec
+            Set<String> directories = new TreeSet<>();
+            directories.add("camel-mock/");
+
+            if (openAPI.getPaths() != null) {
+                for (Map.Entry<String, PathItem> pathEntry : 
openAPI.getPaths().entrySet()) {
+                    String path = pathEntry.getKey();
+                    PathItem pathItem = pathEntry.getValue();
+
+                    if (pathItem.readOperationsMap() == null) {
+                        continue;
+                    }
+
+                    for (Map.Entry<PathItem.HttpMethod, Operation> opEntry : 
pathItem.readOperationsMap().entrySet()) {
+                        String method = opEntry.getKey().name();
+                        Operation op = opEntry.getValue();
+
+                        // Determine response content type and example
+                        String contentType = null;
+                        String exampleContent = null;
+                        String responseCode = null;
+
+                        if (op.getResponses() != null) {
+                            Map.Entry<String, ApiResponse> successResponse = 
findFirstSuccessResponse(op.getResponses());
+                            if (successResponse != null) {
+                                responseCode = successResponse.getKey();
+                                ApiResponse resp = successResponse.getValue();
+                                if (resp.getContent() != null && 
!resp.getContent().isEmpty()) {
+                                    Map.Entry<String, MediaType> firstContent
+                                            = 
resp.getContent().entrySet().iterator().next();
+                                    contentType = firstContent.getKey();
+                                    MediaType mediaType = 
firstContent.getValue();
+                                    if (mediaType.getExample() != null) {
+                                        exampleContent = 
mediaType.getExample().toString();
+                                    }
+                                }
+                            }
+                        }
+
+                        // Determine file extension from content type
+                        String ext = getFileExtension(contentType);
+
+                        // Build mock file path: camel-mock/<path>.<ext>
+                        String cleanPath = path.startsWith("/") ? 
path.substring(1) : path;
+                        // Replace path parameters with placeholder directory 
names
+                        cleanPath = cleanPath.replace("{", "_").replace("}", 
"_");
+                        String filePath = "camel-mock/" + cleanPath + "." + 
method.toLowerCase() + ext;
+
+                        // Track parent directories
+                        String[] parts = filePath.split("/");
+                        StringBuilder dirBuilder = new StringBuilder();
+                        for (int i = 0; i < parts.length - 1; i++) {
+                            dirBuilder.append(parts[i]).append("/");
+                            directories.add(dirBuilder.toString());
+                        }
+
+                        String operationId = op.getOperationId() != null ? 
op.getOperationId() : method + " " + path;
+
+                        String note = null;
+                        if ("GET".equals(method) && exampleContent == null) {
+                            note = "GET without a mock file returns HTTP 204 
(No Content)";
+                        } else if ("POST".equals(method) || 
"PUT".equals(method) || "DELETE".equals(method)) {
+                            if (exampleContent == null) {
+                                note = method + " without a mock file echoes 
the request body back";
+                            }
+                        }
+
+                        mockFiles.add(new MockFileInfo(filePath, operationId, 
contentType, exampleContent, note));
+                    }
+                }
+            }
+
+            // Build directory structure string
+            StringBuilder dirStructure = new StringBuilder();
+            for (String dir : directories) {
+                int depth = dir.split("/").length - 1;
+                dirStructure.append("  
".repeat(depth)).append(dir.substring(dir.lastIndexOf('/') == dir.length() - 1
+                        ? dir.substring(0, dir.length() - 1).lastIndexOf('/') 
+ 1
+                        : dir.lastIndexOf('/') + 1));
+                dirStructure.append("\n");
+            }
+            directoryStructure = dirStructure.toString().stripTrailing();
+        }
+
+        return new MockGuidanceResult(
+                effectiveMode, modeExplanation, configYaml.toString(),
+                directoryStructure, mockFiles.isEmpty() ? null : mockFiles);
+    }
+
+    // -- Shared helpers --
+
+    OpenAPI parseSpec(String spec) {
+        if (spec == null || spec.isBlank()) {
+            throw new ToolCallException("'spec' parameter is required and must 
not be blank", null);
+        }
+
+        SwaggerParseResult parseResult = new 
OpenAPIV3Parser().readContents(spec);
+        OpenAPI openAPI = parseResult.getOpenAPI();
+        if (openAPI == null) {
+            String errors = parseResult.getMessages() != null
+                    ? String.join("; ", parseResult.getMessages())
+                    : "Unknown parse error";
+            throw new ToolCallException("Failed to parse OpenAPI spec: " + 
errors, null);
+        }
+        return openAPI;
+    }
+
+    private int countOperations(OpenAPI openAPI) {
+        if (openAPI.getPaths() == null) {
+            return 0;
+        }
+        int count = 0;
+        for (PathItem item : openAPI.getPaths().values()) {
+            if (item.readOperationsMap() != null) {
+                count += item.readOperationsMap().size();
+            }
+        }
+        return count;
+    }
+
+    private List<OperationStub> collectOperationStubs(OpenAPI openAPI) {
+        List<OperationStub> stubs = new ArrayList<>();
+        if (openAPI.getPaths() == null) {
+            return stubs;
+        }
+
+        for (Map.Entry<String, PathItem> pathEntry : 
openAPI.getPaths().entrySet()) {
+            String path = pathEntry.getKey();
+            PathItem pathItem = pathEntry.getValue();
+
+            if (pathItem.readOperationsMap() == null) {
+                continue;
+            }
+
+            for (Map.Entry<PathItem.HttpMethod, Operation> opEntry : 
pathItem.readOperationsMap().entrySet()) {
+                String method = opEntry.getKey().name().toLowerCase();
+                Operation op = opEntry.getValue();
+
+                String operationId = op.getOperationId();
+                if (operationId == null || operationId.isBlank()) {
+                    operationId = "GENOPID_" + method.toUpperCase()
+                                  + path.replace("/", "_").replace("{", 
"").replace("}", "");
+                }
+
+                String responseCode = null;
+                String contentType = null;
+                String consumesType = null;
+
+                // Find response info
+                if (op.getResponses() != null) {
+                    Map.Entry<String, ApiResponse> successResponse = 
findFirstSuccessResponse(op.getResponses());
+                    if (successResponse != null) {
+                        responseCode = successResponse.getKey();
+                        ApiResponse resp = successResponse.getValue();
+                        if (resp.getContent() != null && 
!resp.getContent().isEmpty()) {
+                            contentType = 
resp.getContent().keySet().iterator().next();
+                        }
+                    }
+                }
+
+                // Find request body content type
+                if (op.getRequestBody() != null && 
op.getRequestBody().getContent() != null
+                        && !op.getRequestBody().getContent().isEmpty()) {
+                    consumesType = 
op.getRequestBody().getContent().keySet().iterator().next();
+                }
+
+                String summary = op.getSummary();
+
+                stubs.add(new OperationStub(operationId, method, path, 
responseCode, contentType, consumesType, summary));
+            }
+        }
+        return stubs;
+    }
+
+    private Map.Entry<String, ApiResponse> 
findFirstSuccessResponse(ApiResponses responses) {
+        // Try 2xx codes in order
+        for (Map.Entry<String, ApiResponse> entry : responses.entrySet()) {
+            String code = entry.getKey();
+            if (code.startsWith("2")) {
+                return entry;
+            }
+        }
+        // Fall back to "default"
+        if (responses.getDefault() != null) {
+            return Map.entry("200", responses.getDefault());
+        }
+        return null;
+    }
+
+    private void checkSecurityScheme(
+            String name, SecurityScheme scheme,
+            List<DiagnosticMessage> warnings, List<DiagnosticMessage> info) {
+
+        if (scheme.getType() == null) {
+            return;
+        }
+
+        switch (scheme.getType()) {
+            case APIKEY:
+                if (scheme.getIn() == SecurityScheme.In.QUERY) {
+                    info.add(new DiagnosticMessage(
+                            "SECURITY_APIKEY_QUERY",
+                            "Security scheme '" + name + "' (apiKey in query) 
is supported by Camel.",
+                            null));
+                } else {
+                    warnings.add(new DiagnosticMessage(
+                            "SECURITY_APIKEY_NOT_QUERY",
+                            "Security scheme '" + name + "' (apiKey in " + 
scheme.getIn()
+                                                         + ") is defined but 
not enforced by Camel's REST OpenAPI integration. "
+                                                         + "You must handle 
authentication in your route logic.",
+                            null));
+                }
+                break;
+            case HTTP:
+                warnings.add(new DiagnosticMessage(
+                        "SECURITY_HTTP",
+                        "Security scheme '" + name + "' (HTTP " + 
scheme.getScheme()
+                                         + ") is defined but not enforced by 
Camel's REST OpenAPI integration. "
+                                         + "You must handle authentication in 
your route logic.",
+                        null));
+                break;
+            case OAUTH2:
+                warnings.add(new DiagnosticMessage(
+                        "SECURITY_OAUTH2",
+                        "Security scheme '" + name
+                                           + "' (OAuth2) is defined but not 
enforced by Camel's REST OpenAPI integration. "
+                                           + "You must handle authentication 
in your route logic.",
+                        null));
+                break;
+            case OPENIDCONNECT:
+                warnings.add(new DiagnosticMessage(
+                        "SECURITY_OPENIDCONNECT",
+                        "Security scheme '" + name
+                                                  + "' (OpenID Connect) is 
defined but not enforced by Camel's REST OpenAPI integration. "
+                                                  + "You must handle 
authentication in your route logic.",
+                        null));
+                break;
+            case MUTUALTLS:
+                warnings.add(new DiagnosticMessage(
+                        "SECURITY_MUTUALTLS",
+                        "Security scheme '" + name
+                                              + "' (Mutual TLS) is defined but 
not enforced by Camel's REST OpenAPI integration. "
+                                              + "You must handle 
authentication in your route logic.",
+                        null));
+                break;
+            default:
+                break;
+        }
+    }
+
+    private String getModeExplanation(String mode) {
+        return switch (mode) {
+            case "fail" -> "In 'fail' mode (the default), Camel throws an 
exception at startup if any operationId "
+                           + "in the OpenAPI spec does not have a 
corresponding direct:<operationId> route. "
+                           + "This ensures all API operations are explicitly 
implemented.";
+            case "ignore" -> "In 'ignore' mode, Camel silently skips 
operations that do not have a corresponding "
+                             + "direct:<operationId> route. Requests to those 
endpoints return HTTP 404. "
+                             + "Useful during incremental development.";
+            case "mock" -> "In 'mock' mode, Camel auto-generates mock 
responses for operations without a "
+                           + "direct:<operationId> route. For GET requests, it 
looks for mock data files under "
+                           + "camel-mock/ directory. For POST/PUT/DELETE, it 
echoes the request body. "
+                           + "GET without a mock file returns HTTP 204. Useful 
for prototyping and testing.";
+            default -> "";
+        };
+    }
+
+    private String getFileExtension(String contentType) {
+        if (contentType == null) {
+            return ".json";
+        }
+        if (contentType.contains("json")) {
+            return ".json";
+        }
+        if (contentType.contains("xml")) {
+            return ".xml";
+        }
+        if (contentType.contains("text")) {
+            return ".txt";
+        }
+        return ".json";
+    }
+
+    // -- Result records --
+
+    public record DiagnosticMessage(String code, String message, String path) {
+    }
+
+    public record ValidateResult(
+            boolean valid,
+            List<DiagnosticMessage> errors,
+            List<DiagnosticMessage> warnings,
+            List<DiagnosticMessage> info,
+            int operationCount) {
+    }
+
+    public record OperationStub(
+            String operationId,
+            String method,
+            String path,
+            String responseCode,
+            String contentType,
+            String consumesType,
+            String summary) {
+    }
+
+    public record ScaffoldResult(
+            String yaml,
+            int operationCount,
+            String specFilename,
+            String missingOperation,
+            String apiTitle) {
+    }
+
+    public record MockFileInfo(
+            String filePath,
+            String operation,
+            String contentType,
+            String exampleContent,
+            String note) {
+    }
+
+    public record MockGuidanceResult(
+            String mode,
+            String modeExplanation,
+            String configurationYaml,
+            String directoryStructure,
+            List<MockFileInfo> mockFiles) {
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiToolsTest.java
new file mode 100644
index 000000000000..7fcb93850c67
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiToolsTest.java
@@ -0,0 +1,403 @@
+/*
+ * 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 io.quarkiverse.mcp.server.ToolCallException;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class OpenApiToolsTest {
+
+    private final OpenApiTools tools = new OpenApiTools();
+
+    private static final String MINIMAL_SPEC = """
+            {
+              "openapi": "3.0.3",
+              "info": {
+                "title": "Pet Store",
+                "version": "1.0.0"
+              },
+              "paths": {
+                "/pets": {
+                  "get": {
+                    "operationId": "listPets",
+                    "summary": "List all pets",
+                    "responses": {
+                      "200": {
+                        "description": "A list of pets",
+                        "content": {
+                          "application/json": {
+                            "schema": { "type": "array", "items": { "type": 
"string" } },
+                            "example": ["dog", "cat"]
+                          }
+                        }
+                      }
+                    }
+                  },
+                  "post": {
+                    "operationId": "createPet",
+                    "summary": "Create a pet",
+                    "requestBody": {
+                      "content": {
+                        "application/json": {
+                          "schema": { "type": "object" }
+                        }
+                      }
+                    },
+                    "responses": {
+                      "201": { "description": "Pet created" }
+                    }
+                  }
+                }
+              }
+            }
+            """;
+
+    private static final String SPEC_NO_OPERATION_IDS = """
+            {
+              "openapi": "3.0.3",
+              "info": { "title": "Test", "version": "1.0.0" },
+              "paths": {
+                "/items": {
+                  "get": {
+                    "responses": { "200": { "description": "OK" } }
+                  }
+                }
+              }
+            }
+            """;
+
+    private static final String SPEC_WITH_SECURITY = """
+            {
+              "openapi": "3.0.3",
+              "info": { "title": "Secure API", "version": "1.0.0" },
+              "paths": {
+                "/data": {
+                  "get": {
+                    "operationId": "getData",
+                    "responses": { "200": { "description": "OK" } }
+                  }
+                }
+              },
+              "components": {
+                "securitySchemes": {
+                  "apiKeyQuery": {
+                    "type": "apiKey",
+                    "in": "query",
+                    "name": "api_key"
+                  },
+                  "apiKeyHeader": {
+                    "type": "apiKey",
+                    "in": "header",
+                    "name": "X-API-Key"
+                  },
+                  "bearerAuth": {
+                    "type": "http",
+                    "scheme": "bearer"
+                  },
+                  "oauth": {
+                    "type": "oauth2",
+                    "flows": {}
+                  }
+                }
+              }
+            }
+            """;
+
+    private static final String SPEC_NO_PATHS = """
+            {
+              "openapi": "3.0.3",
+              "info": { "title": "Empty", "version": "1.0.0" },
+              "paths": {}
+            }
+            """;
+
+    private static final String SPEC_31_WITH_WEBHOOKS = """
+            {
+              "openapi": "3.1.0",
+              "info": { "title": "Webhook API", "version": "1.0.0" },
+              "paths": {
+                "/hook": {
+                  "post": {
+                    "operationId": "receiveHook",
+                    "responses": { "200": { "description": "OK" } }
+                  }
+                }
+              },
+              "webhooks": {
+                "petEvent": {
+                  "post": {
+                    "operationId": "petWebhook",
+                    "responses": { "200": { "description": "OK" } }
+                  }
+                }
+              }
+            }
+            """;
+
+    private static final String SPEC_MULTI_RESPONSE = """
+            {
+              "openapi": "3.0.3",
+              "info": { "title": "Multi", "version": "1.0.0" },
+              "paths": {
+                "/items": {
+                  "get": {
+                    "operationId": "getItems",
+                    "responses": {
+                      "200": {
+                        "description": "Items list",
+                        "content": {
+                          "application/xml": {
+                            "schema": { "type": "string" }
+                          }
+                        }
+                      }
+                    }
+                  },
+                  "delete": {
+                    "operationId": "deleteItem",
+                    "responses": {
+                      "204": { "description": "Deleted" }
+                    }
+                  }
+                }
+              }
+            }
+            """;
+
+    // ---- Validate tests ----
+
+    @Test
+    void validateValidSpec() {
+        OpenApiTools.ValidateResult result = 
tools.camel_openapi_validate(MINIMAL_SPEC);
+
+        assertThat(result.valid()).isTrue();
+        assertThat(result.errors()).isEmpty();
+        assertThat(result.operationCount()).isEqualTo(2);
+    }
+
+    @Test
+    void validateNullSpecThrows() {
+        assertThatThrownBy(() -> tools.camel_openapi_validate(null))
+                .isInstanceOf(ToolCallException.class)
+                .hasMessageContaining("spec");
+    }
+
+    @Test
+    void validateBlankSpecThrows() {
+        assertThatThrownBy(() -> tools.camel_openapi_validate("   "))
+                .isInstanceOf(ToolCallException.class)
+                .hasMessageContaining("spec");
+    }
+
+    @Test
+    void validateInvalidSpecThrows() {
+        assertThatThrownBy(() -> tools.camel_openapi_validate("{ invalid json 
!!!"))
+                .isInstanceOf(ToolCallException.class)
+                .hasMessageContaining("Failed to parse OpenAPI spec");
+    }
+
+    @Test
+    void validateMissingOperationIdWarns() {
+        OpenApiTools.ValidateResult result = 
tools.camel_openapi_validate(SPEC_NO_OPERATION_IDS);
+
+        assertThat(result.valid()).isTrue();
+        assertThat(result.warnings()).anySatisfy(w -> {
+            assertThat(w.code()).isEqualTo("MISSING_OPERATION_ID");
+            assertThat(w.message()).contains("GENOPID_");
+        });
+    }
+
+    @Test
+    void validateSecuritySchemeWarnings() {
+        OpenApiTools.ValidateResult result = 
tools.camel_openapi_validate(SPEC_WITH_SECURITY);
+
+        // apiKey in query should be info
+        assertThat(result.info()).anySatisfy(i -> 
assertThat(i.code()).isEqualTo("SECURITY_APIKEY_QUERY"));
+        // apiKey in header should be warning
+        assertThat(result.warnings()).anySatisfy(w -> 
assertThat(w.code()).isEqualTo("SECURITY_APIKEY_NOT_QUERY"));
+        // HTTP bearer should be warning
+        assertThat(result.warnings()).anySatisfy(w -> 
assertThat(w.code()).isEqualTo("SECURITY_HTTP"));
+        // OAuth2 should be warning
+        assertThat(result.warnings()).anySatisfy(w -> 
assertThat(w.code()).isEqualTo("SECURITY_OAUTH2"));
+    }
+
+    @Test
+    void validateNoPathsError() {
+        OpenApiTools.ValidateResult result = 
tools.camel_openapi_validate(SPEC_NO_PATHS);
+
+        assertThat(result.valid()).isFalse();
+        assertThat(result.errors()).anySatisfy(e -> 
assertThat(e.code()).isEqualTo("NO_PATHS"));
+    }
+
+    @Test
+    void validateWebhooksWarning() {
+        OpenApiTools.ValidateResult result = 
tools.camel_openapi_validate(SPEC_31_WITH_WEBHOOKS);
+
+        assertThat(result.warnings()).anySatisfy(w -> 
assertThat(w.code()).isEqualTo("WEBHOOKS_PRESENT"));
+        assertThat(result.warnings()).anySatisfy(w -> 
assertThat(w.code()).isEqualTo("OPENAPI_31"));
+    }
+
+    // ---- Scaffold tests ----
+
+    @Test
+    void scaffoldGeneratesCorrectYaml() {
+        OpenApiTools.ScaffoldResult result = 
tools.camel_openapi_scaffold(MINIMAL_SPEC, null, null);
+
+        assertThat(result.yaml()).contains("rest:");
+        assertThat(result.yaml()).contains("openApi:");
+        assertThat(result.yaml()).contains("specification: openapi.json");
+        assertThat(result.operationCount()).isEqualTo(2);
+        assertThat(result.apiTitle()).isEqualTo("Pet Store");
+    }
+
+    @Test
+    void scaffoldNullSpecThrows() {
+        assertThatThrownBy(() -> tools.camel_openapi_scaffold(null, null, 
null))
+                .isInstanceOf(ToolCallException.class)
+                .hasMessageContaining("spec");
+    }
+
+    @Test
+    void scaffoldContainsDirectRoutes() {
+        OpenApiTools.ScaffoldResult result = 
tools.camel_openapi_scaffold(MINIMAL_SPEC, null, null);
+
+        assertThat(result.yaml()).contains("direct:listPets");
+        assertThat(result.yaml()).contains("direct:createPet");
+    }
+
+    @Test
+    void scaffoldResponseCodesFromSpec() {
+        OpenApiTools.ScaffoldResult result = 
tools.camel_openapi_scaffold(MINIMAL_SPEC, null, null);
+
+        assertThat(result.yaml()).contains("constant: 200");
+        assertThat(result.yaml()).contains("constant: 201");
+    }
+
+    @Test
+    void scaffoldContentTypeHeaders() {
+        OpenApiTools.ScaffoldResult result = 
tools.camel_openapi_scaffold(MINIMAL_SPEC, null, null);
+
+        assertThat(result.yaml()).contains("constant: application/json");
+    }
+
+    @Test
+    void scaffoldCustomFilename() {
+        OpenApiTools.ScaffoldResult result = 
tools.camel_openapi_scaffold(MINIMAL_SPEC, "petstore.yaml", null);
+
+        assertThat(result.yaml()).contains("specification: petstore.yaml");
+        assertThat(result.specFilename()).isEqualTo("petstore.yaml");
+    }
+
+    @Test
+    void scaffoldMissingOperationModeApplied() {
+        OpenApiTools.ScaffoldResult result = 
tools.camel_openapi_scaffold(MINIMAL_SPEC, null, "mock");
+
+        assertThat(result.yaml()).contains("missingOperation: mock");
+        assertThat(result.missingOperation()).isEqualTo("mock");
+    }
+
+    @Test
+    void scaffoldInvalidModeThrows() {
+        assertThatThrownBy(() -> tools.camel_openapi_scaffold(MINIMAL_SPEC, 
null, "invalid"))
+                .isInstanceOf(ToolCallException.class)
+                .hasMessageContaining("missingOperation");
+    }
+
+    // ---- Mock guidance tests ----
+
+    @Test
+    void mockGuidanceDefaultModeIsMock() {
+        OpenApiTools.MockGuidanceResult result = 
tools.camel_openapi_mock_guidance(MINIMAL_SPEC, null);
+
+        assertThat(result.mode()).isEqualTo("mock");
+        assertThat(result.modeExplanation()).contains("mock");
+    }
+
+    @Test
+    void mockGuidanceGeneratesMockFilePaths() {
+        OpenApiTools.MockGuidanceResult result = 
tools.camel_openapi_mock_guidance(MINIMAL_SPEC, "mock");
+
+        assertThat(result.mockFiles()).isNotNull();
+        assertThat(result.mockFiles()).anySatisfy(f -> 
assertThat(f.filePath()).contains("camel-mock/"));
+    }
+
+    @Test
+    void mockGuidanceFailModeExplanation() {
+        OpenApiTools.MockGuidanceResult result = 
tools.camel_openapi_mock_guidance(MINIMAL_SPEC, "fail");
+
+        assertThat(result.mode()).isEqualTo("fail");
+        assertThat(result.modeExplanation()).contains("fail");
+        assertThat(result.modeExplanation()).contains("exception");
+        // No mock files for fail mode
+        assertThat(result.mockFiles()).isNull();
+        assertThat(result.directoryStructure()).isNull();
+    }
+
+    @Test
+    void mockGuidanceIgnoreModeExplanation() {
+        OpenApiTools.MockGuidanceResult result = 
tools.camel_openapi_mock_guidance(MINIMAL_SPEC, "ignore");
+
+        assertThat(result.mode()).isEqualTo("ignore");
+        assertThat(result.modeExplanation()).contains("ignore");
+        assertThat(result.modeExplanation()).contains("404");
+    }
+
+    @Test
+    void mockGuidanceConfigYamlCorrect() {
+        OpenApiTools.MockGuidanceResult result = 
tools.camel_openapi_mock_guidance(MINIMAL_SPEC, "mock");
+
+        assertThat(result.configurationYaml()).contains("missingOperation: 
mock");
+        assertThat(result.configurationYaml()).contains("specification: 
openapi.json");
+    }
+
+    @Test
+    void mockGuidanceNullSpecThrows() {
+        assertThatThrownBy(() -> tools.camel_openapi_mock_guidance(null, 
"mock"))
+                .isInstanceOf(ToolCallException.class)
+                .hasMessageContaining("spec");
+    }
+
+    @Test
+    void mockGuidanceExampleContentPopulated() {
+        OpenApiTools.MockGuidanceResult result = 
tools.camel_openapi_mock_guidance(MINIMAL_SPEC, "mock");
+
+        // The MINIMAL_SPEC has example data on the GET /pets response
+        assertThat(result.mockFiles()).isNotNull();
+        assertThat(result.mockFiles()).anySatisfy(f -> {
+            assertThat(f.operation()).isEqualTo("listPets");
+            assertThat(f.exampleContent()).isNotNull();
+        });
+    }
+
+    @Test
+    void mockGuidanceInvalidModeThrows() {
+        assertThatThrownBy(() -> 
tools.camel_openapi_mock_guidance(MINIMAL_SPEC, "invalid"))
+                .isInstanceOf(ToolCallException.class)
+                .hasMessageContaining("mode");
+    }
+
+    @Test
+    void mockGuidanceDirectoryStructurePresent() {
+        OpenApiTools.MockGuidanceResult result = 
tools.camel_openapi_mock_guidance(MINIMAL_SPEC, "mock");
+
+        assertThat(result.directoryStructure()).isNotNull();
+        assertThat(result.directoryStructure()).contains("camel-mock");
+    }
+}

Reply via email to