toolListCache = new ConcurrentHashMap<>();
+ private McpServerConfig serverConfig;
+
+ /**
+ * Caches a tool definition, associating it with a unique key (e.g., the operation ID).
+ * If a tool with the same key already exists, it will be overwritten.
+ *
+ * @param key The unique identifier for the tool.
+ * @param tool The {@link McpSchema.Tool} object to cache.
+ */
+ public void putTool(String key, McpSchema.Tool tool) {
+ toolListCache.put(key, tool);
+ }
+
+ /**
+ * Retrieves a tool from the cache by its key.
+ *
+ * @param key The unique identifier of the tool to retrieve.
+ * @return The cached {@link McpSchema.Tool} object, or {@code null} if no tool is found for the given key.
+ */
+ public McpSchema.Tool getTool(String key) {
+ return toolListCache.get(key);
+ }
+
+ /**
+ * Caches the server's global configuration object.
+ * This will overwrite any previously stored configuration.
+ *
+ * @param config The {@link McpServerConfig} object to cache.
+ */
+ public void putServerConfig(McpServerConfig config) {
+ serverConfig = config;
+ }
+
+ /**
+ * Retrieves the cached server configuration object.
+ *
+ * @return The cached {@link McpServerConfig} object.
+ */
+ public McpServerConfig getServerConfig() {
+ return serverConfig;
+ }
+}
diff --git a/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/config/OpenApiMcpServerConfiguration.java b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/config/OpenApiMcpServerConfiguration.java
new file mode 100644
index 0000000..2dab3bb
--- /dev/null
+++ b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/config/OpenApiMcpServerConfiguration.java
@@ -0,0 +1,122 @@
+/*
+ * --------------------------------------------------------------------------
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ * Licensed under the Universal Permissive License v1.0 as shown at http://oss.oracle.com/licenses/upl.
+ * --------------------------------------------------------------------------
+ */
+package com.oracle.mcp.openapi.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.oracle.mcp.openapi.cache.McpServerCacheService;
+import com.oracle.mcp.openapi.fetcher.OpenApiSchemaFetcher;
+import com.oracle.mcp.openapi.rest.RestApiAuthHandler;
+import com.oracle.mcp.openapi.rest.RestApiExecutionService;
+import com.oracle.mcp.openapi.tool.OpenApiMcpToolExecutor;
+import com.oracle.mcp.openapi.tool.OpenApiMcpToolInitializer;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Spring configuration class responsible for defining and wiring all the necessary beans
+ * for the OpenAPI MCP server application.
+ *
+ * This class centralizes the creation of services, mappers, and other components,
+ * managing their dependencies through Spring's dependency injection framework.
+ *
+ * @author Joby Wilson Mathews (joby.mathews@oracle.com)
+ */
+@Configuration
+public class OpenApiMcpServerConfiguration {
+
+ /**
+ * Creates a singleton {@link ObjectMapper} bean for handling JSON serialization and deserialization.
+ *
+ * @return A new {@code ObjectMapper} instance.
+ */
+ @Bean("jsonMapper")
+ public ObjectMapper jsonMapper() {
+ return new ObjectMapper();
+ }
+
+ /**
+ * Creates a singleton {@link ObjectMapper} bean specifically configured for handling YAML.
+ *
+ * @return A new {@code ObjectMapper} instance configured with a {@link YAMLFactory}.
+ */
+ @Bean("yamlMapper")
+ public ObjectMapper yamlMapper() {
+ return new ObjectMapper(new YAMLFactory());
+ }
+
+ /**
+ * Creates a singleton {@link McpServerCacheService} bean to act as an in-memory
+ * cache for the server configuration and parsed tools.
+ *
+ * @return A new {@code McpServerCacheService} instance.
+ */
+ @Bean
+ public McpServerCacheService mcpServerCacheService() {
+ return new McpServerCacheService();
+ }
+
+ /**
+ * Creates a singleton {@link RestApiExecutionService} bean responsible for executing
+ * HTTP requests against the target OpenAPI.
+ *
+ * @param mcpServerCacheService The cache service, used to retrieve server and auth configurations.
+ * @return A new {@code RestApiExecutionService} instance.
+ */
+ @Bean
+ public RestApiExecutionService restApiExecutionService(McpServerCacheService mcpServerCacheService) {
+ return new RestApiExecutionService(mcpServerCacheService);
+ }
+
+ /**
+ * Creates a singleton {@link OpenApiMcpToolInitializer} bean that parses an OpenAPI
+ * specification and converts its operations into MCP tools.
+ *
+ * @param mcpServerCacheService The cache service where the converted tools will be stored.
+ * @return A new {@code OpenApiMcpToolInitializer} instance.
+ */
+ @Bean
+ public OpenApiMcpToolInitializer openApiToMcpToolConverter(McpServerCacheService mcpServerCacheService) {
+ return new OpenApiMcpToolInitializer(mcpServerCacheService);
+ }
+
+ /**
+ * Creates a singleton {@link OpenApiSchemaFetcher} bean for retrieving the
+ * OpenAPI specification from a URL or local path.
+ *
+ * @param jsonMapper The mapper for parsing JSON-formatted specifications.
+ * @param yamlMapper The mapper for parsing YAML-formatted specifications.
+ * @return A new {@code OpenApiSchemaFetcher} instance.
+ */
+ @Bean
+ public OpenApiSchemaFetcher openApiDefinitionFetcher(@Qualifier("jsonMapper") ObjectMapper jsonMapper,
+ @Qualifier("yamlMapper") ObjectMapper yamlMapper,
+ RestApiAuthHandler restApiAuthHandler) {
+ return new OpenApiSchemaFetcher(jsonMapper, yamlMapper, restApiAuthHandler);
+ }
+
+ @Bean
+ public RestApiAuthHandler restApiAuthHandler(){
+ return new RestApiAuthHandler();
+ }
+
+ /**
+ * Creates a singleton {@link OpenApiMcpToolExecutor} bean that handles the
+ * execution of a specific MCP tool call by translating it into an HTTP request.
+ *
+ * @param mcpServerCacheService The cache service to look up tool definitions and server config.
+ * @param restApiExecutionService The service to execute the final HTTP request.
+ * @param jsonMapper The mapper to serialize the request body arguments to JSON.
+ * @return A new {@code OpenApiMcpToolExecutor} instance.
+ */
+ @Bean
+ public OpenApiMcpToolExecutor openApiMcpToolExecutor(McpServerCacheService mcpServerCacheService, RestApiExecutionService restApiExecutionService, @Qualifier("jsonMapper") ObjectMapper jsonMapper,RestApiAuthHandler restApiAuthHandler) {
+ return new OpenApiMcpToolExecutor(mcpServerCacheService, restApiExecutionService, jsonMapper,restApiAuthHandler);
+ }
+}
\ No newline at end of file
diff --git a/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/constants/CommonConstant.java b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/constants/CommonConstant.java
new file mode 100644
index 0000000..2be8db3
--- /dev/null
+++ b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/constants/CommonConstant.java
@@ -0,0 +1,42 @@
+package com.oracle.mcp.openapi.constants;
+
+public interface CommonConstant {
+
+ String OBJECT = "object";
+
+ String ADDITIONAL_PROPERTIES ="additionalProperties";
+
+ String OPEN_API = "openapi";
+ String SWAGGER = "swagger";
+
+ // Meta keys
+ String HTTP_METHOD = "httpMethod";
+ String PATH = "path";
+ String TAGS = "tags";
+ String SECURITY = "security";
+ String PATH_PARAMS = "pathParams";
+ String QUERY_PARAMS = "queryParams";
+ String HEADER_PARAMS = "headerParams";
+ String COOKIE_PARAMS = "cookieParams";
+
+ // Schema keys
+ String TYPE = "type";
+ String DESCRIPTION = "description";
+ String FORMAT = "format";
+ String ENUM = "enum";
+ String PROPERTIES = "properties";
+ String REQUIRED = "required";
+ String ITEMS = "items";
+ String NAME = "name";
+ String ARRAY = "array" ;
+ String QUERY = "query";
+
+
+
+ //String constants
+ String UNDER_SCORE = "_";
+
+ //REST constants
+ String APPLICATION_JSON = "application/json";
+
+}
diff --git a/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/constants/ErrorMessage.java b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/constants/ErrorMessage.java
new file mode 100644
index 0000000..0c4e193
--- /dev/null
+++ b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/constants/ErrorMessage.java
@@ -0,0 +1,31 @@
+/*
+ * --------------------------------------------------------------------------
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ * Licensed under the Universal Permissive License v1.0 as shown at http://oss.oracle.com/licenses/upl.
+ * --------------------------------------------------------------------------
+ */
+package com.oracle.mcp.openapi.constants;
+
+
+/**
+ * Defines a collection of constant error message strings used throughout the application.
+ *
+ * This interface centralizes user-facing error messages to ensure consistency and
+ * ease of maintenance. By keeping them in one place, we can easily update or
+ * translate them without searching through the entire codebase.
+ *
+ * @author Joby Wilson Mathews (joby.mathews@oracle.com)
+ */
+public interface ErrorMessage {
+
+ String MISSING_API_SPEC = "API specification not provided. Please pass --api-spec or set the API_SPEC environment variable.";
+
+ String MISSING_API_BASE_URL = "API base url not provided. Please pass --api-base-url or set the API_BASE_URL environment variable.";
+
+ String MISSING_PATH_IN_SPEC = "'paths' object not found in the specification.";
+
+ String INVALID_SPEC_DEFINITION = "Unsupported API definition: missing 'openapi' or 'swagger' field";
+
+ String INVALID_SWAGGER_SPEC = "Invalid Swagger specification provided.";
+
+}
diff --git a/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/enums/OpenApiSchemaAuthType.java b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/enums/OpenApiSchemaAuthType.java
new file mode 100644
index 0000000..f322eff
--- /dev/null
+++ b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/enums/OpenApiSchemaAuthType.java
@@ -0,0 +1,61 @@
+/*
+ * --------------------------------------------------------------------------
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ * Licensed under the Universal Permissive License v1.0 as shown at http://oss.oracle.com/licenses/upl.
+ * --------------------------------------------------------------------------
+ */
+package com.oracle.mcp.openapi.enums;
+
+import com.oracle.mcp.openapi.model.McpServerConfig;
+
+/**
+ * Represents the supported authentication types for the OpenAPI MCP server.
+ * This enum provides a type-safe way to handle different authentication schemes.
+ *
+ * @author Joby Wilson Mathews (joby.mathews@oracle.com)
+ */
+public enum OpenApiSchemaAuthType {
+ /**
+ * Basic Authentication using a username and password.
+ */
+ BASIC,
+ /**
+ * Bearer Token Authentication using an opaque token.
+ */
+ BEARER,
+ /**
+ * API Key Authentication, where the key can be sent in a header or query parameter.
+ */
+ API_KEY,
+ /**
+ * Custom Authentication using a user-defined set of headers.
+ */
+ CUSTOM,
+ /**
+ * No authentication is required.
+ */
+ NONE;
+
+ /**
+ * Safely determines the {@code OpenApiSchemaAuthType} from the server configuration.
+ *
+ * This method parses the raw authentication type string provided in the
+ * {@link McpServerConfig}. It handles null, empty, or invalid strings by defaulting
+ * to {@link #NONE}, preventing runtime exceptions.
+ *
+ * @param request The server configuration object containing the raw auth type string.
+ * @return The corresponding {@code OpenApiSchemaAuthType} enum constant. Returns {@link #NONE}
+ * if the provided auth type is null, empty, or does not match any known type.
+ */
+ public static OpenApiSchemaAuthType getType(McpServerConfig request) {
+ String authType = request.getRawAuthType();
+ if (authType == null || authType.isEmpty()) {
+ return NONE;
+ }
+ try {
+ return OpenApiSchemaAuthType.valueOf(authType.toUpperCase());
+ } catch (IllegalArgumentException ex) {
+ return NONE;
+ }
+ }
+}
diff --git a/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/enums/OpenApiSchemaSourceType.java b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/enums/OpenApiSchemaSourceType.java
new file mode 100644
index 0000000..e6f9e6c
--- /dev/null
+++ b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/enums/OpenApiSchemaSourceType.java
@@ -0,0 +1,57 @@
+/*
+ * --------------------------------------------------------------------------
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ * Licensed under the Universal Permissive License v1.0 as shown at http://oss.oracle.com/licenses/upl.
+ * --------------------------------------------------------------------------
+ */
+package com.oracle.mcp.openapi.enums;
+
+import com.oracle.mcp.openapi.model.McpServerConfig;
+
+/**
+ * Represents the source type of OpenAPI specification.
+ *
+ * This enum distinguishes between specifications loaded from a remote URL and
+ * those read from a local file system path.
+ *
+ * @author Joby Wilson Mathews (joby.mathews@oracle.com)
+ */
+public enum OpenApiSchemaSourceType {
+ /**
+ * The specification is located at a remote URL
+ */
+ URL,
+ /**
+ * The specification is located at a path on the local file system.
+ */
+ FILE;
+
+ /**
+ * Determines the source type of the OpenAPI specification from the server configuration.
+ *
+ * This method inspects the specification location string provided in the
+ * {@link McpServerConfig}. It returns {@link #URL} if the string starts
+ * with "http://" or "https://", and {@link #FILE} otherwise.
+ *
+ * @param request The server configuration containing the API specification location.
+ * @return The determined {@code OpenApiSchemaSourceType} (either {@link #URL} or {@link #FILE}).
+ * @throws IllegalArgumentException if the API specification location is null or empty in the configuration.
+ */
+ public static OpenApiSchemaSourceType getType(McpServerConfig request) {
+ // Backward compatibility: prefer specUrl if set
+ String specUrl = request.getApiSpec();
+ if (specUrl != null && !specUrl.trim().isEmpty()) {
+ return URL;
+ }
+
+ String specLocation = request.getApiSpec();
+ if (specLocation == null || specLocation.trim().isEmpty()) {
+ throw new IllegalArgumentException("No specUrl or specLocation defined in McpServerConfig.");
+ }
+
+ if (specLocation.startsWith("http://") || specLocation.startsWith("https://")) {
+ return URL;
+ }
+ return FILE;
+ }
+}
diff --git a/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/enums/OpenApiSchemaType.java b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/enums/OpenApiSchemaType.java
new file mode 100644
index 0000000..5266165
--- /dev/null
+++ b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/enums/OpenApiSchemaType.java
@@ -0,0 +1,78 @@
+/*
+ * --------------------------------------------------------------------------
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ * Licensed under the Universal Permissive License v1.0 as shown at http://oss.oracle.com/licenses/upl.
+ * --------------------------------------------------------------------------
+ */
+package com.oracle.mcp.openapi.enums;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+
+/**
+ * Represents the format of an OpenAPI specification file.
+ *
+ * This enum is used to identify whether a given specification is written in JSON or YAML,
+ * or if its format cannot be determined.
+ *
+ * @author Joby Wilson Mathews (joby.mathews@oracle.com)
+ */
+public enum OpenApiSchemaType {
+ /**
+ * The specification is in JSON format.
+ */
+ JSON,
+ /**
+ * The specification is in YAML format.
+ */
+ YAML,
+ /**
+ * The format of the specification could not be determined.
+ */
+ UNKNOWN;
+
+ /**
+ * A reusable, thread-safe mapper instance for parsing JSON content.
+ */
+ private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
+ /**
+ * A reusable, thread-safe mapper instance for parsing YAML content.
+ */
+ private static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory());
+
+ /**
+ * Determines the schema file type from its string content by attempting to parse it.
+ *
+ * This method first tries to parse the input string as JSON. If that fails, it
+ * then attempts to parse it as YAML. If both attempts fail, or if the input is
+ * null or empty, it returns {@link #UNKNOWN}.
+ *
+ * @param dataString The string content of the OpenAPI specification.
+ * @return The determined {@code OpenApiSchemaType} ({@link #JSON}, {@link #YAML}, or {@link #UNKNOWN}).
+ */
+ public static OpenApiSchemaType getType(String dataString) {
+ if (dataString == null || dataString.trim().isEmpty()) {
+ return UNKNOWN;
+ }
+
+ // First, try to parse as JSON
+ try {
+ JSON_MAPPER.readTree(dataString);
+ return JSON;
+ } catch (JsonProcessingException e) {
+ // It's not JSON, so we proceed to check for YAML.
+ }
+
+ // If JSON parsing failed, try YAML
+ try {
+ YAML_MAPPER.readTree(dataString);
+ return YAML;
+ } catch (JsonProcessingException e) {
+ // It's not YAML either.
+ }
+
+ // If both attempts fail, return UNKNOWN
+ return UNKNOWN;
+ }
+}
\ No newline at end of file
diff --git a/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/exception/McpServerToolExecutionException.java b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/exception/McpServerToolExecutionException.java
new file mode 100644
index 0000000..b6a6e80
--- /dev/null
+++ b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/exception/McpServerToolExecutionException.java
@@ -0,0 +1,41 @@
+/*
+ * --------------------------------------------------------------------------
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ * Licensed under the Universal Permissive License v1.0 as shown at http://oss.oracle.com/licenses/upl.
+ * --------------------------------------------------------------------------
+ */
+package com.oracle.mcp.openapi.exception;
+
+/**
+ * A custom exception thrown when an error occurs during the execution of an OpenAPI-based MCP tool.
+ *
+ * This exception is used to wrap underlying issues such as network errors, I/O problems,
+ * or any other runtime failure encountered while invoking an external API endpoint.
+ *
+ * @author Joby Wilson Mathews (joby.mathews@oracle.com)
+ */
+public class McpServerToolExecutionException extends Exception {
+
+ /**
+ * Constructs a new {@code McpServerToolExecutionException} with the specified detail message.
+ *
+ * @param message The detail message (which is saved for later retrieval by the {@link #getMessage()} method).
+ */
+ public McpServerToolExecutionException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new {@code McpServerToolExecutionException} with the specified detail message and cause.
+ *
+ * Note that the detail message associated with {@code cause} is not automatically
+ * incorporated in this exception's detail message.
+ *
+ * @param message The detail message (which is saved for later retrieval by the {@link #getMessage()} method).
+ * @param cause The cause (which is saved for later retrieval by the {@link #getCause()} method).
+ * A {@code null} value is permitted, and indicates that the cause is nonexistent or unknown.
+ */
+ public McpServerToolExecutionException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/exception/McpServerToolInitializeException.java b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/exception/McpServerToolInitializeException.java
new file mode 100644
index 0000000..849f4e2
--- /dev/null
+++ b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/exception/McpServerToolInitializeException.java
@@ -0,0 +1,42 @@
+/*
+ * --------------------------------------------------------------------------
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ * Licensed under the Universal Permissive License v1.0 as shown at http://oss.oracle.com/licenses/upl.
+ * --------------------------------------------------------------------------
+ */
+package com.oracle.mcp.openapi.exception;
+
+/**
+ * A custom exception thrown when an error occurs during the initialization phase of the MCP server.
+ *
+ * This exception is used to indicate failures related to initial setup, such as parsing
+ * command-line arguments, reading the OpenAPI specification, or configuring the server
+ * before it is ready to execute tools.
+ *
+ * @author Joby Wilson Mathews (joby.mathews@oracle.com)
+ */
+public class McpServerToolInitializeException extends Exception {
+
+ /**
+ * Constructs a new {@code McpServerToolInitializeException} with the specified detail message.
+ *
+ * @param message The detail message (which is saved for later retrieval by the {@link #getMessage()} method).
+ */
+ public McpServerToolInitializeException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new {@code McpServerToolInitializeException} with the specified detail message and cause.
+ *
+ * Note that the detail message associated with {@code cause} is not automatically
+ * incorporated in this exception's detail message.
+ *
+ * @param message The detail message (which is saved for later retrieval by the {@link #getMessage()} method).
+ * @param cause The cause (which is saved for later retrieval by the {@link #getCause()} method).
+ * A {@code null} value is permitted, and indicates that the cause is nonexistent or unknown.
+ */
+ public McpServerToolInitializeException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/fetcher/OpenApiSchemaFetcher.java b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/fetcher/OpenApiSchemaFetcher.java
new file mode 100644
index 0000000..1cbb806
--- /dev/null
+++ b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/fetcher/OpenApiSchemaFetcher.java
@@ -0,0 +1,155 @@
+/*
+ * --------------------------------------------------------------------------
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ * Licensed under the Universal Permissive License v1.0 as shown at http://oss.oracle.com/licenses/upl.
+ * --------------------------------------------------------------------------
+ */
+package com.oracle.mcp.openapi.fetcher;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.oracle.mcp.openapi.enums.OpenApiSchemaAuthType;
+import com.oracle.mcp.openapi.model.McpServerConfig;
+import com.oracle.mcp.openapi.enums.OpenApiSchemaSourceType;
+import com.oracle.mcp.openapi.enums.OpenApiSchemaType;
+import com.oracle.mcp.openapi.rest.RestApiAuthHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
+
+/**
+ * Utility class responsible for fetching and parsing OpenAPI schema definitions.
+ *
+ * A schema can be retrieved either from:
+ *
+ * - A remote URL (with optional Basic Authentication)
+ * - A local file system path
+ *
+ *
+ * Once retrieved, the schema content is parsed into a Jackson {@link JsonNode}
+ * using either a JSON or YAML {@link ObjectMapper}, depending on the detected format.
+ *
+ * @author Joby Wilson Mathews (joby.mathews@oracle.com)
+ */
+public class OpenApiSchemaFetcher {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(OpenApiSchemaFetcher.class);
+
+ private final ObjectMapper jsonMapper;
+ private final ObjectMapper yamlMapper;
+ private final RestApiAuthHandler restApiAuthHandler;
+
+ /**
+ * Creates a new {@code OpenApiSchemaFetcher}.
+ *
+ * @param jsonMapper {@link ObjectMapper} configured for JSON parsing.
+ * @param yamlMapper {@link ObjectMapper} configured for YAML parsing.
+ */
+ public OpenApiSchemaFetcher(ObjectMapper jsonMapper, ObjectMapper yamlMapper, RestApiAuthHandler restApiAuthHandler) {
+ this.jsonMapper = jsonMapper;
+ this.yamlMapper = yamlMapper;
+ this.restApiAuthHandler = restApiAuthHandler;
+ }
+
+ /**
+ * Fetches an OpenAPI schema from the configured source.
+ *
+ * The schema may be downloaded from a remote URL or read from a file,
+ * depending on the {@link OpenApiSchemaSourceType}.
+ *
+ * @param mcpServerConfig configuration containing schema location and authentication details.
+ * @return the parsed OpenAPI schema as a {@link JsonNode}.
+ * @throws Exception if the schema cannot be retrieved or parsed.
+ */
+ public JsonNode fetch(McpServerConfig mcpServerConfig) throws Exception {
+ OpenApiSchemaSourceType type = OpenApiSchemaSourceType.getType(mcpServerConfig);
+ String content;
+ if (type == OpenApiSchemaSourceType.URL) {
+ content = downloadContent(mcpServerConfig);
+ } else {
+ content = loadFromFile(mcpServerConfig);
+ }
+ return parseContent(content);
+ }
+
+ /**
+ * Reads the OpenAPI specification from a local file.
+ *
+ * @param mcpServerConfig configuration specifying the file path.
+ * @return schema content as a UTF-8 string.
+ * @throws IOException if the file cannot be read.
+ */
+ private String loadFromFile(McpServerConfig mcpServerConfig) throws IOException {
+ Path path = Paths.get(mcpServerConfig.getApiSpec());
+ return Files.readString(path, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Downloads the OpenAPI specification from a remote URL.
+ *
+ * If Basic Authentication is configured, the request will include the
+ * {@code Authorization} header.
+ *
+ * @param mcpServerConfig configuration specifying the URL and authentication details.
+ * @return schema content as a UTF-8 string.
+ * @throws Exception if the download fails.
+ */
+ private String downloadContent(McpServerConfig mcpServerConfig) throws Exception {
+ URL url = new URL(mcpServerConfig.getApiSpec());
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setConnectTimeout(10_000);
+ conn.setReadTimeout(10_000);
+ conn.setRequestMethod("GET");
+
+ applyAuth(conn, mcpServerConfig);
+
+ try (InputStream in = conn.getInputStream()) {
+ byte[] bytes = in.readAllBytes();
+ return new String(bytes, StandardCharsets.UTF_8);
+ }
+ }
+
+ /**
+ * Applies authentication headers to the connection if required.
+ *
+ * Currently supports only {@link OpenApiSchemaAuthType#BASIC}.
+ * Passwords and sensitive data are securely cleared from memory
+ * after use.
+ *
+ * @param conn the {@link HttpURLConnection} to update.
+ * @param mcpServerConfig configuration containing authentication details.
+ */
+ private void applyAuth(HttpURLConnection conn, McpServerConfig mcpServerConfig) {
+ Map headers = restApiAuthHandler.extractAuthHeaders(mcpServerConfig);
+ for (Map.Entry entry : headers.entrySet()) {
+ conn.setRequestProperty(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * Parses schema content into a {@link JsonNode}.
+ *
+ * Automatically detects whether the input is YAML or JSON.
+ *
+ * @param content schema content as a string.
+ * @return parsed {@link JsonNode}.
+ * @throws Exception if parsing fails.
+ */
+ private JsonNode parseContent(String content) throws Exception {
+ OpenApiSchemaType type = OpenApiSchemaType.getType(content);
+ if (type == OpenApiSchemaType.YAML) {
+ return yamlMapper.readTree(content);
+ } else {
+ return jsonMapper.readTree(content);
+ }
+ }
+}
diff --git a/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/mapper/McpToolMapper.java b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/mapper/McpToolMapper.java
new file mode 100644
index 0000000..9e8ae05
--- /dev/null
+++ b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/mapper/McpToolMapper.java
@@ -0,0 +1,117 @@
+/*
+ * --------------------------------------------------------------------------
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ * Licensed under the Universal Permissive License v1.0 as shown at http://oss.oracle.com/licenses/upl.
+ * --------------------------------------------------------------------------
+ */
+package com.oracle.mcp.openapi.mapper;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.oracle.mcp.openapi.exception.McpServerToolInitializeException;
+import com.oracle.mcp.openapi.model.override.ToolOverridesConfig;
+import com.oracle.mcp.openapi.util.McpServerUtil;
+import io.modelcontextprotocol.spec.McpSchema;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Mapper interface for converting OpenAPI specifications into
+ * {@link McpSchema.Tool} representations.
+ *
+ * Implementations of this interface are responsible for reading a parsed
+ * API specification (as a Jackson {@link JsonNode}) and mapping it into
+ * a list of tool definitions that conform to the MCP schema.
+ *
+ * @author Joby Wilson Mathews (joby.mathews@oracle.com)
+ */
+public interface McpToolMapper {
+
+ /**
+ * Converts an OpenAPI specification into a list of MCP tools.
+ *
+ * @param toolOverridesConfig the tool overrides configuration.
+ * @param apiSpec the OpenAPI specification represented as a {@link JsonNode};
+ * must not be {@code null}.
+ * @return a list of {@link McpSchema.Tool} objects derived from the given API specification;
+ * never {@code null}, but may be empty if no tools are found.
+ */
+ List convert(JsonNode apiSpec,ToolOverridesConfig toolOverridesConfig) throws McpServerToolInitializeException;
+
+ default String generateToolName(
+ String method,
+ String path,
+ String operationId,
+ Set existingNames) {
+
+ String baseName;
+
+ if (operationId != null && !operationId.isEmpty()) {
+ baseName = operationId;
+ } else {
+ StringBuilder name = new StringBuilder(method.toLowerCase());
+ for (String segment : path.split("/")) {
+ if (segment.isEmpty()){
+ continue;
+ }
+
+ if (segment.startsWith("{") && segment.endsWith("}")) {
+ String varName = segment.substring(1, segment.length() - 1);
+ name.append("By").append(McpServerUtil.capitalize(varName));
+ } else {
+ name.append(McpServerUtil.capitalize(segment));
+ }
+ }
+ baseName = name.toString();
+ }
+
+ String uniqueName = baseName;
+
+ // Resolve clash: add HTTP method if already taken
+ if (existingNames.contains(uniqueName)) {
+ // Append method suffix if not already included
+ String methodSuffix = McpServerUtil.capitalize(method.toLowerCase());
+ if (!uniqueName.endsWith(methodSuffix)) {
+ uniqueName = baseName + methodSuffix;
+ }
+ // In the rare case it's STILL a clash, append last path segment
+ if (existingNames.contains(uniqueName)) {
+ String lastSegment = getLastPathSegment(path);
+ uniqueName = baseName + McpServerUtil.capitalize(method.toLowerCase()) + McpServerUtil.capitalize(lastSegment);
+ }
+ }
+
+ existingNames.add(uniqueName);
+ return uniqueName;
+ }
+
+
+ default String getLastPathSegment(String path) {
+ String[] segments = path.split("/");
+ for (int i = segments.length - 1; i >= 0; i--) {
+ if (!segments[i].isEmpty() && !segments[i].startsWith("{")) {
+ return segments[i];
+ }
+ }
+ return "Endpoint";
+ }
+
+ default boolean skipTool(String toolName, ToolOverridesConfig config) {
+ if (config == null) {
+ return false;
+ }
+
+ Set includeOnly = config.getIncludeOnly() == null
+ ? Collections.emptySet()
+ : config.getIncludeOnly();
+
+ Set exclude = config.getExclude() == null
+ ? Collections.emptySet()
+ : config.getExclude();
+
+ // Apply filtering: Exclude wins
+ return (!includeOnly.isEmpty() && !includeOnly.contains(toolName))
+ || exclude.contains(toolName);
+ }
+}
diff --git a/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/mapper/impl/OpenApiToMcpToolMapper.java b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/mapper/impl/OpenApiToMcpToolMapper.java
new file mode 100644
index 0000000..99f7811
--- /dev/null
+++ b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/mapper/impl/OpenApiToMcpToolMapper.java
@@ -0,0 +1,484 @@
+/*
+ * --------------------------------------------------------------------------
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ * Licensed under the Universal Permissive License v1.0 as shown at http://oss.oracle.com/licenses/upl.
+ * --------------------------------------------------------------------------
+ */
+package com.oracle.mcp.openapi.mapper.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.oracle.mcp.openapi.cache.McpServerCacheService;
+import com.oracle.mcp.openapi.constants.CommonConstant;
+import com.oracle.mcp.openapi.constants.ErrorMessage;
+import com.oracle.mcp.openapi.exception.McpServerToolInitializeException;
+import com.oracle.mcp.openapi.mapper.McpToolMapper;
+import com.oracle.mcp.openapi.model.override.ToolOverride;
+import com.oracle.mcp.openapi.model.override.ToolOverridesConfig;
+import com.oracle.mcp.openapi.util.McpServerUtil;
+import io.modelcontextprotocol.spec.McpSchema;
+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.Schema;
+import io.swagger.v3.oas.models.parameters.Parameter;
+import io.swagger.v3.parser.OpenAPIV3Parser;
+import io.swagger.v3.parser.core.models.ParseOptions;
+import io.swagger.v3.parser.core.models.SwaggerParseResult;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Set;
+import java.util.Map;
+
+
+/**
+ * Implementation of {@link McpToolMapper} that converts an OpenAPI specification
+ * into a list of {@link McpSchema.Tool} objects.
+ *
+ * This class parses the OpenAPI JSON, extracts operations, parameters,
+ * request/response schemas, and builds tool definitions compatible
+ * with the MCP schema. Converted tools are also cached using
+ * {@link McpServerCacheService}.
+ *
+ * @author Joby Wilson Mathews (joby.mathews@oracle.com)
+ */
+public class OpenApiToMcpToolMapper implements McpToolMapper {
+
+ private final McpServerCacheService mcpServerCacheService;
+ private static final Logger LOGGER = LoggerFactory.getLogger(OpenApiToMcpToolMapper.class);
+
+ public OpenApiToMcpToolMapper(McpServerCacheService mcpServerCacheService) {
+ this.mcpServerCacheService = mcpServerCacheService;
+ }
+
+ @Override
+ public List convert(JsonNode openApiJson, ToolOverridesConfig toolOverridesConfig) throws McpServerToolInitializeException {
+ LOGGER.debug("Parsing OpenAPI schema to OpenAPI object.");
+ OpenAPI openAPI = parseOpenApi(openApiJson);
+ LOGGER.debug("Successfully parsed OpenAPI schema");
+ return convert(openAPI,toolOverridesConfig);
+
+ }
+
+ public List convert(OpenAPI openAPI, ToolOverridesConfig toolOverridesConfig) throws McpServerToolInitializeException {
+ if(openAPI==null){
+ LOGGER.error("No schema found.");
+ return Collections.emptyList();
+ }
+ if (openAPI.getPaths() == null || openAPI.getPaths().isEmpty()) {
+ LOGGER.error("No paths defined in schema.");
+ throw new McpServerToolInitializeException(ErrorMessage.MISSING_PATH_IN_SPEC);
+ }
+
+ List mcpTools = processPaths(openAPI, toolOverridesConfig);
+ LOGGER.debug("Conversion complete. Total tools created: {}", mcpTools.size());
+ updateToolsToCache(mcpTools);
+ return mcpTools;
+ }
+
+ private List processPaths(OpenAPI openAPI, ToolOverridesConfig toolOverridesConfig) {
+ List mcpTools = new ArrayList<>();
+ Set toolNames = new HashSet<>();
+
+ for (Map.Entry pathEntry : openAPI.getPaths().entrySet()) {
+ String path = pathEntry.getKey();
+ LOGGER.debug("Parsing Path: {}", path);
+ PathItem pathItem = pathEntry.getValue();
+ if (pathItem == null) continue;
+
+ processOperationsForPath(openAPI, path, pathItem, mcpTools, toolNames, toolOverridesConfig);
+ }
+ return mcpTools;
+ }
+
+ private void processOperationsForPath(OpenAPI openAPI, String path, PathItem pathItem,
+ List mcpTools, Set toolNames,
+ ToolOverridesConfig toolOverridesConfig) {
+ for (Map.Entry methodEntry : pathItem.readOperationsMap().entrySet()) {
+ PathItem.HttpMethod method = methodEntry.getKey();
+ Operation operation = methodEntry.getValue();
+ if (operation == null){
+ continue;
+ }
+
+ McpSchema.Tool tool = buildToolFromOperation(openAPI, path, method, operation, toolNames, toolOverridesConfig);
+ if (tool != null){
+ mcpTools.add(tool);
+ }
+ }
+ }
+
+ private McpSchema.Tool buildToolFromOperation(OpenAPI openAPI, String path, PathItem.HttpMethod method,
+ Operation operation, Set toolNames,
+ ToolOverridesConfig toolOverridesConfig) {
+
+ String toolName = generateToolName(method.name(), path, operation.getOperationId(), toolNames);
+
+ if (skipTool(toolName, toolOverridesConfig)) {
+ LOGGER.debug("Skipping tool: {} as it is in tool override file", toolName);
+ return null;
+ }
+
+ LOGGER.debug("--- Parsing Operation: {} {} (ID: {}) ---", method.name().toUpperCase(), path, toolName);
+
+ ToolOverride toolOverride = toolOverridesConfig.getTools().getOrDefault(toolName, ToolOverride.EMPTY_TOOL_OVERRIDE);
+ String toolTitle = getToolTitle(operation, toolOverride, toolName);
+ String toolDescription = getToolDescription(operation, toolOverride);
+ Map componentsSchemas = openAPI.getComponents() != null ? openAPI.getComponents().getSchemas() : new HashMap<>();
+
+ // Input Schema
+ McpSchema.JsonSchema inputSchema = getInputSchema(operation, componentsSchemas);
+
+ // Output Schema
+ Map outputSchema = getOutputSchema();
+
+ // Params
+ Map> pathParams = new HashMap<>();
+ Map> queryParams = new HashMap<>();
+ Map> headerParams = new HashMap<>();
+ Map> cookieParams = new HashMap<>();
+ populatePathQueryHeaderCookieParams(operation, pathParams, queryParams, headerParams, cookieParams);
+
+ // Meta
+ Map meta = buildMeta(method, path, operation, pathParams, queryParams, headerParams, cookieParams);
+
+ return McpSchema.Tool.builder()
+ .title(toolTitle)
+ .name(toolName)
+ .description(toolDescription)
+ .inputSchema(inputSchema)
+ .outputSchema(outputSchema)
+ .meta(meta)
+ .build();
+ }
+
+ public void populatePathQueryHeaderCookieParams(Operation operation,
+ Map> pathParams,
+ Map> queryParams,
+ Map> headerParams,
+ Map> cookieParams) {
+ if (operation.getParameters() != null) {
+ for (Parameter param : operation.getParameters()) {
+ Map propSchema = createPropertySchema(
+ param.getSchema(),
+ param.getDescription(),
+ param.getSchema() != null ? param.getSchema().getEnum() : null
+ );
+
+ String name = param.getName();
+
+ switch (param.getIn()) {
+ case "path" -> pathParams.put(name, propSchema);
+ case "query" -> queryParams.put(name, propSchema);
+ case "header" -> headerParams.put(name, propSchema);
+ case "cookie" -> cookieParams.put(name, propSchema);
+ default -> LOGGER.warn("Unknown parameter location: {}", param.getIn());
+ }
+ }
+ }
+ }
+
+ private Map createPropertySchema(Schema> schema, String description, List> enums) {
+ Map propSchema = new LinkedHashMap<>();
+ propSchema.put("type", mapOpenApiType(schema != null ? schema.getType() : null));
+ if (description != null){
+ propSchema.put("description", description);
+ }
+ if (enums != null){
+ propSchema.put("enum", enums);
+ }
+ return propSchema;
+ }
+
+ private String getToolTitle(Operation operation, ToolOverride toolOverride, String toolName) {
+ String overrideTitle = toolOverride.getTitle();
+ if (McpServerUtil.isNotBlank(overrideTitle)){
+ return overrideTitle;
+ }
+
+ return (operation.getSummary() != null && !operation.getSummary().isEmpty())
+ ? operation.getSummary()
+ : toolName;
+ }
+
+ private String getToolDescription(Operation operation, ToolOverride toolOverride) {
+ String overrideDescription = toolOverride.getDescription();
+ if (McpServerUtil.isNotBlank(overrideDescription)){
+ return overrideDescription;
+ }
+ if (McpServerUtil.isNotBlank(operation.getSummary())){
+ return operation.getSummary();
+ }
+ if (McpServerUtil.isNotBlank(operation.getDescription())){
+ return operation.getDescription();
+ }
+ return "";
+ }
+
+ private Map getOutputSchema() {
+ Map outputSchema = new HashMap<>();
+ outputSchema.put("type","object");
+ outputSchema.put(CommonConstant.ADDITIONAL_PROPERTIES, true);
+ return outputSchema;
+ }
+
+ private Map buildMeta(PathItem.HttpMethod method, String path, Operation operation,
+ Map> pathParams,
+ Map> queryParams,
+ Map> headerParams,
+ Map> cookieParams) {
+ Map meta = new LinkedHashMap<>();
+ meta.put(CommonConstant.HTTP_METHOD, method.name());
+ meta.put(CommonConstant.PATH, path);
+
+ if (operation.getTags() != null){
+ meta.put(CommonConstant.TAGS, operation.getTags());
+ }
+ if (operation.getSecurity() != null){
+ meta.put(CommonConstant.SECURITY, operation.getSecurity());
+ }
+
+ if (!pathParams.isEmpty()){
+ meta.put(CommonConstant.PATH_PARAMS, pathParams);
+ }
+ if (!queryParams.isEmpty()){
+ meta.put(CommonConstant.QUERY_PARAMS, queryParams);
+ }
+ if (!headerParams.isEmpty()){
+ meta.put(CommonConstant.HEADER_PARAMS, headerParams);
+ }
+ if (!cookieParams.isEmpty()){
+ meta.put(CommonConstant.COOKIE_PARAMS, cookieParams);
+ }
+
+ return meta;
+ }
+
+ private void updateToolsToCache(List tools) {
+ for (McpSchema.Tool tool : tools) {
+ mcpServerCacheService.putTool(tool.name(), tool);
+ }
+ }
+
+ private OpenAPI parseOpenApi(JsonNode jsonNode) {
+ String jsonString = jsonNode.toString();
+ ParseOptions options = new ParseOptions();
+ options.setResolve(true);
+ options.setResolveFully(true);
+
+ SwaggerParseResult result = new OpenAPIV3Parser().readContents(jsonString, null, options);
+ List messages = result.getMessages();
+ if (messages != null && !messages.isEmpty()) {
+ LOGGER.info("OpenAPI validation errors: {}", messages);
+ }
+ return result.getOpenAPI();
+ }
+
+ private McpSchema.JsonSchema getInputSchema(Operation operation, Map componentsSchemas) {
+ Map properties = new LinkedHashMap<>();
+ List required = new ArrayList<>();
+ Set visitedRefs = new HashSet<>();
+
+ handleParameters(operation.getParameters(), properties, required, componentsSchemas, visitedRefs);
+
+ if (operation.getRequestBody() != null && operation.getRequestBody().getContent() != null) {
+ Schema> bodySchema = operation.getRequestBody().getContent().get("application/json") != null
+ ? operation.getRequestBody().getContent().get("application/json").getSchema()
+ : null;
+
+ if (bodySchema != null) {
+ bodySchema = resolveRef(bodySchema, componentsSchemas, visitedRefs);
+ Map bodyProps = buildSchemaRecursively(bodySchema, componentsSchemas, visitedRefs);
+
+ // Flatten object-only allOf, keep combinators nested
+ if ("object".equals(bodyProps.get("type"))) {
+ Map topProps = new LinkedHashMap<>();
+ mergeAllOfProperties(bodyProps, topProps);
+ // merge top-level properties into final properties
+ properties.putAll(topProps);
+ // merge required
+ if (bodyProps.get("required") instanceof List> reqList) {
+ reqList.forEach(r -> required.add((String) r));
+ }
+ } else {
+ // Non-object root schema, wrap under "body"
+ properties.put("body", bodyProps);
+ if (Boolean.TRUE.equals(operation.getRequestBody().getRequired())) {
+ required.add("body");
+ }
+ }
+ }
+ }
+ return new McpSchema.JsonSchema(
+ "object",
+ properties.isEmpty() ? null : properties,
+ required.isEmpty() ? null : required,
+ false,
+ null,
+ null
+ );
+ }
+
+
+ private void handleParameters(List parameters,
+ Map properties,
+ List required,
+ Map componentsSchemas,
+ Set visitedRefs) {
+ if (parameters != null) {
+ for (Parameter param : parameters) {
+ Schema> schema = resolveRef(param.getSchema(), componentsSchemas, visitedRefs);
+ Map propSchema = buildSchemaRecursively(schema, componentsSchemas, visitedRefs);
+
+ String name = param.getName();
+ if (name != null && !name.isEmpty()) {
+ properties.put(name, propSchema);
+
+ if (Boolean.TRUE.equals(param.getRequired())) {
+ required.add(name);
+ }
+ } else {
+ // fallback: generate a safe name if missing
+ String safeName = "param_" + properties.size();
+ properties.put(safeName, propSchema);
+ if (Boolean.TRUE.equals(param.getRequired())) {
+ required.add(safeName);
+ }
+ }
+
+ // Log a warning if the location is unknown, but still include it
+ if (!Set.of("path", "query", "header", "cookie").contains(param.getIn())) {
+ LOGGER.warn("Unknown parameter location '{}', still adding '{}' to tool schema",
+ param.getIn(), name);
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void mergeAllOfProperties(Map schema, Map target) {
+ if (schema.containsKey("allOf")) {
+ List
+ *
+ * @param jsonNode the JSON representation of the API definition
+ * @return a list of mapped {@link McpSchema.Tool} objects
+ * @throws IllegalArgumentException if {@code jsonNode} is {@code null}
+ * @throws UnsupportedApiDefinitionException if the specification type is unsupported
+ */
+ private List parseApi(McpServerConfig serverConfig,JsonNode jsonNode) throws McpServerToolInitializeException {
+ if (jsonNode == null) {
+ throw new IllegalArgumentException("jsonNode cannot be null");
+ }
+ ToolOverridesConfig toolOverridesJson = null;
+ try {
+ toolOverridesJson = serverConfig.getToolOverridesConfig();
+ } catch (JsonProcessingException e) {
+ LOGGER.warn("Failed to parse tool overrides JSON: {}", e.getMessage());
+ }
+ // Detect version
+ if (jsonNode.has(CommonConstant.OPEN_API)) {
+ return new OpenApiToMcpToolMapper(mcpServerCacheService).convert(jsonNode,toolOverridesJson);
+ } else if (jsonNode.has(CommonConstant.SWAGGER)) {
+ return new SwaggerToMcpToolMapper(mcpServerCacheService).convert(jsonNode,toolOverridesJson);
+ } else {
+ throw new McpServerToolInitializeException(ErrorMessage.INVALID_SPEC_DEFINITION);
+ }
+
+ }
+}
diff --git a/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/util/McpServerUtil.java b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/util/McpServerUtil.java
new file mode 100644
index 0000000..9956d19
--- /dev/null
+++ b/src/openapi-mcp-server/src/main/java/com/oracle/mcp/openapi/util/McpServerUtil.java
@@ -0,0 +1,60 @@
+/*
+ * --------------------------------------------------------------------------
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ * Licensed under the Universal Permissive License v1.0 as shown at http://oss.oracle.com/licenses/upl.
+ * --------------------------------------------------------------------------
+ */
+package com.oracle.mcp.openapi.util;
+
+/**
+ * Utility class for MCP Server operations.
+ * @author Joby Wilson Mathews (joby.mathews@oracle.com)
+ */
+public final class McpServerUtil {
+
+ private static final String CAMEL_CASE_REGEX = "[^a-zA-Z0-9]+";
+
+ /**
+ * Converts an input string into camelCase format.
+ */
+ public static String toCamelCase(String input) {
+ if (input == null || input.isEmpty()){
+ return "";
+ }
+ String[] parts = input.split(CAMEL_CASE_REGEX);
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < parts.length; i++) {
+ String word = parts[i].toLowerCase();
+ if (word.isEmpty()) continue;
+ if (i == 0) {
+ sb.append(word);
+ } else {
+ sb.append(Character.toUpperCase(word.charAt(0))).append(word.substring(1));
+ }
+ }
+ return sb.toString();
+ }
+ /**
+ * Capitalizes the first letter of the given string.
+ * If the string is null or empty, it is returned as is.
+ *
+ * @param str The input string to capitalize.
+ * @return The capitalized string, or the original string if it is null or empty.
+ */
+ public static String capitalize(String str) {
+ if (str == null || str.isEmpty()){
+ return str;
+ }
+ return str.substring(0, 1).toUpperCase() + str.substring(1);
+ }
+ /**
+ * Checks if a string is not null and not empty.
+ *
+ * @param str The string to check.
+ * @return true if the string is not null and not empty, false otherwise.
+ */
+ public static boolean isNotBlank(String str) {
+ return str != null && !str.isEmpty();
+ }
+
+}
diff --git a/src/openapi-mcp-server/src/main/resources/application.properties b/src/openapi-mcp-server/src/main/resources/application.properties
new file mode 100644
index 0000000..33406d2
--- /dev/null
+++ b/src/openapi-mcp-server/src/main/resources/application.properties
@@ -0,0 +1 @@
+spring.main.banner-mode=off
diff --git a/src/openapi-mcp-server/src/main/resources/logback.xml b/src/openapi-mcp-server/src/main/resources/logback.xml
new file mode 100644
index 0000000..478055e
--- /dev/null
+++ b/src/openapi-mcp-server/src/main/resources/logback.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+ System.out
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+ ${LOG_PATH:-${java.io.tmpdir}}/app.log
+
+ ${LOG_PATH}/app-%d{yyyy-MM-dd}.%i.log
+ 10MB
+ 30
+ 1GB
+
+
+ %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/openapi-mcp-server/src/test/java/com/oracle/mcp/openapi/OpenApiMcpServerTest.java b/src/openapi-mcp-server/src/test/java/com/oracle/mcp/openapi/OpenApiMcpServerTest.java
new file mode 100644
index 0000000..93ab56d
--- /dev/null
+++ b/src/openapi-mcp-server/src/test/java/com/oracle/mcp/openapi/OpenApiMcpServerTest.java
@@ -0,0 +1,180 @@
+/*
+ * --------------------------------------------------------------------------
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ * Licensed under the Universal Permissive License v1.0 as shown at http://oss.oracle.com/licenses/upl.
+ * --------------------------------------------------------------------------
+ */
+package com.oracle.mcp.openapi;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import com.oracle.mcp.openapi.cache.McpServerCacheService;
+import com.oracle.mcp.openapi.model.McpServerConfig;
+import com.oracle.mcp.openapi.rest.RestApiAuthHandler;
+import com.oracle.mcp.openapi.rest.RestApiExecutionService;
+import com.oracle.mcp.openapi.tool.OpenApiMcpToolExecutor;
+import io.modelcontextprotocol.server.McpAsyncServer;
+import io.modelcontextprotocol.server.McpSyncServer;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.ApplicationContext;
+
+import java.lang.reflect.Field;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Integration tests for the OpenAPI MCP server using a mocked OpenAPI specification.
+ *
+ * This test class uses WireMock to simulate an OpenAPI server with various authentication methods.
+ * It verifies that the MCP server correctly initializes tools and executes them with different auth types.
+ *
+ * @author Joby Wilson Mathews (joby.mathews@oracle.com)
+ */
+@SpringBootTest(
+ args = {
+ "--api-spec", "http://localhost:8080/rest/v1/metadata-catalog/companies",
+ "--api-base-url", "http://localhost:8080"
+ }
+)
+class OpenApiMcpServerTest {
+
+ @Autowired private McpSyncServer mcpSyncServer;
+ @Autowired private RestApiExecutionService restApiExecutionService;
+ @Autowired private McpServerCacheService mcpServerCacheService;
+ @Autowired private RestApiAuthHandler restApiAuthHandler;
+ @Autowired private ApplicationContext context;
+
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ private static String expectedTools;
+ private static String companiesResponse;
+ private static String companyResponse;
+
+ @BeforeAll
+ static void setup() throws Exception {
+ // Start WireMock once for all tests
+ WireMockServer wireMockServer = new WireMockServer(
+ WireMockConfiguration.options()
+ .port(8080)
+ .usingFilesUnderDirectory("src/test/resources")
+ );
+ wireMockServer.start();
+
+ // Load test resources
+ expectedTools = readFile("src/test/resources/tools/listTool.json");
+ companiesResponse = readFile("src/test/resources/__files/companies-response.json");
+ companyResponse = readFile("src/test/resources/__files/company-1-response.json");
+ }
+
+ private static String readFile(String path) throws Exception {
+ return Files.readString(Paths.get(path));
+ }
+
+ private OpenApiMcpToolExecutor newExecutor(McpServerCacheService cache) {
+ return new OpenApiMcpToolExecutor(cache, restApiExecutionService, objectMapper, restApiAuthHandler);
+ }
+
+ private String executeTool(McpServerCacheService cache, String toolName, String input) throws Exception {
+ OpenApiMcpToolExecutor executor = newExecutor(cache);
+ McpSchema.CallToolRequest request = new McpSchema.CallToolRequest(toolName, input);
+ McpSchema.CallToolResult result = executor.execute(request);
+ Object resultObj = result.structuredContent().get("response");
+ JsonNode jsonNode = objectMapper.readTree((String)resultObj);
+ return objectMapper.writeValueAsString(jsonNode);
+ }
+
+ @Test
+ void testListTools() throws Exception {
+ McpAsyncServer asyncServer = mcpSyncServer.getAsyncServer();
+
+ Field toolsField = asyncServer.getClass().getDeclaredField("tools");
+ toolsField.setAccessible(true);
+ CopyOnWriteArrayList> tools = (CopyOnWriteArrayList>) toolsField.get(asyncServer);
+
+ assertEquals(4, tools.size());
+ assertEquals(expectedTools, objectMapper.writeValueAsString(tools));
+ }
+
+ @Test
+ void testExecuteGetAllTools_BasicAuth() throws Exception {
+ McpServerCacheService cacheService = mockConfig(
+ McpServerConfig.builder()
+ .apiBaseUrl("http://localhost:8080")
+ .authType("BASIC")
+ .authUsername("test-user")
+ .authPassword("test-password".toCharArray())
+ .build(),
+ "getCompanies"
+ );
+
+ String response = executeTool(cacheService, "getCompanies", "{}");
+ assertEquals(companiesResponse, response);
+ }
+
+ @Test
+ void testExecuteGetOneCompany_BearerAuth() throws Exception {
+ McpServerCacheService cacheService = mockConfig(
+ McpServerConfig.builder()
+ .apiBaseUrl("http://localhost:8080")
+ .authType("BEARER")
+ .authToken("test-token".toCharArray())
+ .build(),
+ "getCompanyById"
+ );
+
+ String response = executeTool(cacheService, "getCompanyById", "{\"companyId\":1}");
+ assertEquals(companyResponse, response);
+ }
+
+ @Test
+ void testExecuteCreateCompany_ApiKeyAuth() throws Exception {
+ McpServerCacheService cacheService = mockConfig(
+ McpServerConfig.builder()
+ .apiBaseUrl("http://localhost:8080")
+ .authType("API_KEY")
+ .authApiKeyIn("HEADER")
+ .authApiKeyName("X-API-KEY")
+ .authApiKey("test-api-key".toCharArray())
+ .build(),
+ "createCompany"
+ );
+
+ String response = executeTool(cacheService, "createCompany",
+ "{ \"name\": \"Test Company\", \"address\": \"123 Main St\" }");
+ assertEquals(companyResponse, response);
+ }
+
+ @Test
+ void testExecuteUpdateCompany_CustomAuth() throws Exception {
+ McpServerCacheService cacheService = mockConfig(
+ McpServerConfig.builder()
+ .apiBaseUrl("http://localhost:8080")
+ .authType("CUSTOM")
+ .authCustomHeaders(Map.of("CUSTOM-HEADER", "test-custom-key"))
+ .build(),
+ "updateCompany"
+ );
+
+ String response = executeTool(cacheService, "updateCompany",
+ "{ \"companyId\": 1, \"name\": \"Acme Corp - Updated\", \"industry\": \"Technology\" }");
+ assertEquals(companyResponse, response);
+ }
+
+ private McpServerCacheService mockConfig(McpServerConfig config, String toolName) {
+ McpServerCacheService mockCache = Mockito.mock(McpServerCacheService.class);
+ Mockito.when(mockCache.getServerConfig()).thenReturn(config);
+ Mockito.when(mockCache.getTool(toolName)).thenReturn(this.mcpServerCacheService.getTool(toolName));
+ return mockCache;
+ }
+}
diff --git a/src/openapi-mcp-server/src/test/java/com/oracle/mcp/openapi/mapper/McpToolMapperTest.java b/src/openapi-mcp-server/src/test/java/com/oracle/mcp/openapi/mapper/McpToolMapperTest.java
new file mode 100644
index 0000000..54c3191
--- /dev/null
+++ b/src/openapi-mcp-server/src/test/java/com/oracle/mcp/openapi/mapper/McpToolMapperTest.java
@@ -0,0 +1,83 @@
+/*
+ * --------------------------------------------------------------------------
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ * Licensed under the Universal Permissive License v1.0 as shown at http://oss.oracle.com/licenses/upl.
+ * --------------------------------------------------------------------------
+ */
+package com.oracle.mcp.openapi.mapper;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Unit tests for {@link McpToolMapper}.
+ *
+ * @author Joby Wilson Mathews (joby.mathews@oracle.com)
+ */
+public class McpToolMapperTest {
+
+ private Set existingNames;
+
+ private static McpToolMapper toolNameTestMapper;
+
+ @BeforeAll
+ static void init() {
+ toolNameTestMapper = (apiSpec, toolOverridesConfig) -> List.of();
+ }
+
+ @BeforeEach
+ void setUp() {
+ existingNames = new HashSet<>();
+ }
+
+ @Test
+ void shouldUseOperationIdWhenPresent() {
+ String result = toolNameTestMapper.generateToolName("get", "/users", "listUsers", existingNames);
+ assertEquals("listUsers", result);
+ }
+
+ @Test
+ void shouldFallbackToMethodAndPathWhenNoOperationId() {
+ String result = toolNameTestMapper.generateToolName("get", "/users", null, existingNames);
+ assertEquals("getUsers", result);
+ }
+
+ @Test
+ void shouldHandlePathVariables() {
+ String result = toolNameTestMapper.generateToolName("get", "/users/{id}", null, existingNames);
+ assertEquals("getUsersById", result);
+ }
+
+ @Test
+ void shouldDifferentiateByHttpMethod() {
+ String getName = toolNameTestMapper.generateToolName("get", "/users/{id}", null, existingNames);
+ String deleteName = toolNameTestMapper.generateToolName("delete", "/users/{id}", null, existingNames);
+
+ assertEquals("getUsersById", getName);
+ assertEquals("deleteUsersById", deleteName);
+ }
+
+ @Test
+ void shouldHandleDuplicateOperationIds() {
+ String first = toolNameTestMapper.generateToolName("get", "/users", "getUsers", existingNames);
+ String second = toolNameTestMapper.generateToolName("post", "/users", "getUsers", existingNames);
+
+ assertEquals("getUsers", first);
+ assertEquals("getUsersPost", second);
+ }
+
+ @Test
+ void shouldAppendLastSegmentIfStillClashing() {
+ existingNames.add("getUsersByIdDelete");
+
+ String result = toolNameTestMapper.generateToolName("delete", "/users/{id}", null, existingNames);
+ assertEquals("deleteUsersById", result);
+ }
+}
diff --git a/src/openapi-mcp-server/src/test/java/com/oracle/mcp/openapi/mapper/impl/OpenApiToMcpToolMapperTest.java b/src/openapi-mcp-server/src/test/java/com/oracle/mcp/openapi/mapper/impl/OpenApiToMcpToolMapperTest.java
new file mode 100644
index 0000000..c124721
--- /dev/null
+++ b/src/openapi-mcp-server/src/test/java/com/oracle/mcp/openapi/mapper/impl/OpenApiToMcpToolMapperTest.java
@@ -0,0 +1,713 @@
+/*
+ * --------------------------------------------------------------------------
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ * Licensed under the Universal Permissive License v1.0 as shown at http://oss.oracle.com/licenses/upl.
+ * --------------------------------------------------------------------------
+ */
+package com.oracle.mcp.openapi.mapper.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.oracle.mcp.openapi.cache.McpServerCacheService;
+import com.oracle.mcp.openapi.constants.ErrorMessage;
+import com.oracle.mcp.openapi.exception.McpServerToolInitializeException;
+import com.oracle.mcp.openapi.model.override.ToolOverride;
+import com.oracle.mcp.openapi.model.override.ToolOverridesConfig;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+class OpenApiToMcpToolMapperTest {
+
+ private McpServerCacheService cacheService;
+ private OpenApiToMcpToolMapper mapper;
+ private ObjectMapper objectMapper;
+
+ @BeforeEach
+ void setUp() {
+ cacheService = mock(McpServerCacheService.class);
+ mapper = new OpenApiToMcpToolMapper(cacheService);
+ objectMapper = new ObjectMapper();
+ }
+
+ @Test
+ void convert_ShouldReturnToolList_ForValidOpenApiJson1() throws McpServerToolInitializeException {
+ // Arrange: simple OpenAPI JSON with one GET /users path
+ ObjectNode openApiJson = objectMapper.createObjectNode();
+ openApiJson.put("openapi", "3.0.0");
+
+ ObjectNode paths = objectMapper.createObjectNode();
+ ObjectNode getOp = objectMapper.createObjectNode();
+ getOp.put("operationId", "getUsers");
+ paths.set("/users", objectMapper.createObjectNode().set("get", getOp));
+ openApiJson.set("paths", paths);
+
+ ToolOverridesConfig overrides = new ToolOverridesConfig(); // empty overrides
+
+ // Act
+ List tools = mapper.convert(openApiJson, overrides);
+
+ // Assert
+ assertNotNull(tools);
+ assertEquals(1, tools.size());
+ McpSchema.Tool tool = tools.getFirst();
+ assertEquals("getUsers", tool.name());
+ assertEquals("getUsers", tool.title()); // operationId used as fallback
+ verify(cacheService).putTool(eq("getUsers"), any(McpSchema.Tool.class));
+ }
+
+ @Test
+ void convert_ShouldThrowException_WhenPathsMissing() {
+ ObjectNode openApiJson = objectMapper.createObjectNode();
+ openApiJson.put("openapi", "3.0.0");
+
+ ToolOverridesConfig overrides = new ToolOverridesConfig();
+
+ McpServerToolInitializeException ex = assertThrows(McpServerToolInitializeException.class, () ->
+ mapper.convert(openApiJson, overrides)
+ );
+ assertEquals(ErrorMessage.MISSING_PATH_IN_SPEC, ex.getMessage());
+ }
+
+ @Test
+ void convert_ShouldSkipTool_WhenOverrideExists() throws McpServerToolInitializeException {
+ // Arrange OpenAPI JSON
+ ObjectNode openApiJson = objectMapper.createObjectNode();
+ openApiJson.put("openapi", "3.0.0");
+
+ ObjectNode paths = objectMapper.createObjectNode();
+ ObjectNode getOp = objectMapper.createObjectNode();
+ getOp.put("operationId", "skipTool"); // used to generate toolName
+ paths.set("/skip", objectMapper.createObjectNode().set("get", getOp));
+ openApiJson.set("paths", paths);
+
+ // Arrange override config to skip the tool
+ ToolOverridesConfig overrides = new ToolOverridesConfig();
+ overrides.setExclude(Set.of("skipTool")); // exact toolName generated by generateToolName()
+
+ // Act
+ List tools = mapper.convert(openApiJson, overrides);
+
+ // Assert
+ assertTrue(tools.isEmpty(), "Tool should be skipped due to exclude list");
+ verify(cacheService, never()).putTool(anyString(), any(McpSchema.Tool.class));
+ }
+
+ @Test
+ void convert_ShouldReturnToolList_ForValidOpenApiJson() throws Exception {
+ String openApi = """
+ {
+ "openapi": "3.0.0",
+ "paths": {
+ "/users": {
+ "get": {
+ "operationId": "getUsers",
+ "summary": "Fetch users"
+ }
+ }
+ }
+ }
+ """;
+
+ JsonNode node = objectMapper.readTree(openApi);
+ List tools = mapper.convert(node, new ToolOverridesConfig());
+
+ assertEquals(1, tools.size());
+ McpSchema.Tool tool = tools.getFirst();
+ assertEquals("getUsers", tool.name());
+ assertEquals("Fetch users", tool.title());
+ verify(cacheService).putTool(eq("getUsers"), any(McpSchema.Tool.class));
+ }
+
+ @Test
+ void convert_ShouldThrow_WhenPathsMissing() {
+ String openApi = """
+ { "openapi": "3.0.0" }
+ """;
+
+ JsonNode node;
+ try {
+ node = objectMapper.readTree(openApi);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ McpServerToolInitializeException ex =
+ assertThrows(McpServerToolInitializeException.class,
+ () -> mapper.convert(node, new ToolOverridesConfig()));
+
+ assertEquals(ErrorMessage.MISSING_PATH_IN_SPEC, ex.getMessage());
+ }
+
+ @Test
+ void convert_ShouldSkipTool_WhenExcluded() throws Exception {
+ String openApi = """
+ {
+ "openapi": "3.0.0",
+ "paths": {
+ "/skip": {
+ "get": { "operationId": "skipTool" }
+ }
+ }
+ }
+ """;
+
+ JsonNode node = objectMapper.readTree(openApi);
+ ToolOverridesConfig overrides = new ToolOverridesConfig();
+ overrides.setExclude(Set.of("skipTool"));
+
+ List tools = mapper.convert(node, overrides);
+
+ assertTrue(tools.isEmpty());
+ verify(cacheService, never()).putTool(anyString(), any(McpSchema.Tool.class));
+ }
+
+ @Test
+ void convert_ShouldParseParametersAndBody() throws Exception {
+ String openApi = """
+ {
+ "openapi": "3.0.0",
+ "paths": {
+ "/users/{id}": {
+ "post": {
+ "operationId": "createUser",
+ "parameters": [
+ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
+ { "name": "active", "in": "query", "schema": { "type": "boolean" } },
+ { "name": "traceId", "in": "header", "schema": { "type": "string" } },
+ { "name": "session", "in": "cookie", "schema": { "type": "string" } }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "name": { "type": "string" },
+ "age": { "type": "integer" }
+ },
+ "required": ["name"]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ """;
+
+ JsonNode node = objectMapper.readTree(openApi);
+ List tools = mapper.convert(node, new ToolOverridesConfig());
+
+ McpSchema.Tool tool = tools.getFirst();
+ assertEquals("createUser", tool.name());
+ assertNotNull(tool.inputSchema());
+ Map props = tool.inputSchema().properties();
+ assertTrue(props.containsKey("id"));
+ assertTrue(props.containsKey("active"));
+ assertTrue(props.containsKey("name"));
+ assertTrue(tool.inputSchema().required().contains("id"));
+ assertTrue(tool.inputSchema().required().contains("name"));
+ }
+
+ @Test
+ void convert_ShouldHandleOneOf() throws Exception {
+ String openApi = """
+ {
+ "openapi": "3.0.0",
+ "paths": {
+ "/shapes": {
+ "post": {
+ "operationId": "createShape",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "oneOf": [
+ { "type": "object", "properties": { "circle": { "type": "number" } } },
+ { "type": "object", "properties": { "square": { "type": "number" } } }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ """;
+
+ JsonNode node = objectMapper.readTree(openApi);
+ List tools = mapper.convert(node, new ToolOverridesConfig());
+
+ McpSchema.Tool tool = tools.getFirst();
+ McpSchema.JsonSchema schema = tool.inputSchema();
+
+ assertNotNull(schema);
+ }
+
+
+ @Test
+ void testPrimitiveRootTypeRequestBody() throws Exception {
+ String openApi = """
+ {
+ "openapi": "3.0.0",
+ "paths": {
+ "/ping": {
+ "post": {
+ "operationId": "ping",
+ "requestBody": {
+ "content": {
+ "application/json": { "schema": { "type": "string" } }
+ },
+ "required": true
+ }
+ }
+ }
+ }
+ }
+ """;
+
+ JsonNode node = objectMapper.readTree(openApi);
+ List tools = mapper.convert(node, new ToolOverridesConfig());
+
+ McpSchema.Tool tool = tools.getFirst();
+ assertEquals("ping", tool.name());
+ assertTrue(tool.inputSchema().properties().containsKey("body"));
+ }
+
+ @Test
+ void testArrayRootRequestBody() throws Exception {
+ String openApi = """
+ {
+ "openapi": "3.0.0",
+ "paths": {
+ "/numbers": {
+ "post": {
+ "operationId": "postNumbers",
+ "requestBody": {
+ "content": {
+ "application/json": { "schema": { "type": "array", "items": { "type": "integer" } } }
+ }
+ }
+ }
+ }
+ }
+ }
+ """;
+
+ JsonNode node = objectMapper.readTree(openApi);
+ List tools = mapper.convert(node, new ToolOverridesConfig());
+
+ McpSchema.Tool tool = tools.getFirst();
+ Map body = (Map) tool.inputSchema().properties().get("body");
+ assertEquals("array", body.get("type"));
+ }
+
+ @Test
+ void testOneOfNestedProperty() throws Exception {
+ String openApi = """
+ {
+ "openapi": "3.0.0",
+ "paths": {
+ "/shapes": {
+ "post": {
+ "operationId": "createShape",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "shape": {
+ "oneOf": [
+ { "type": "object", "properties": { "circle": { "type": "number" } } },
+ { "type": "object", "properties": { "square": { "type": "number" } } }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ """;
+
+ JsonNode node = objectMapper.readTree(openApi);
+ List tools = mapper.convert(node, new ToolOverridesConfig());
+ McpSchema.Tool tool = tools.getFirst();
+ Map shapeProp = (Map) tool.inputSchema().properties().get("shape");
+ assertTrue(shapeProp.containsKey("oneOf"));
+ }
+
+ void testAllOfAnyOfCombination() throws Exception {
+ String openApi = """
+ {
+ "openapi": "3.0.0",
+ "paths": {
+ "/complex": {
+ "post": {
+ "operationId": "complexBody",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "allOf": [
+ {
+ "type": "object",
+ "properties": { "a": { "type": "string" } }
+ },
+ {
+ "anyOf": [
+ { "type": "object", "properties": { "b": { "type": "integer" } } },
+ { "type": "object", "properties": { "c": { "type": "boolean" } } }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ """;
+
+ JsonNode node = objectMapper.readTree(openApi);
+ List tools = mapper.convert(node, new ToolOverridesConfig());
+ McpSchema.Tool tool = tools.getFirst();
+ Map schemaProps = tool.inputSchema().properties();
+ assertTrue(schemaProps.containsKey("a"));
+ // allOf and anyOf keys might be under schema object
+ assertTrue(schemaProps.values().stream().anyMatch(v -> v instanceof Map && (((Map, ?>) v).containsKey("anyOf"))));
+ }
+
+ @Test
+ void testParameterEnumAndArray() throws Exception {
+ String openApi = """
+ {
+ "openapi": "3.0.0",
+ "paths": {
+ "/query": {
+ "get": {
+ "operationId": "queryParams",
+ "parameters": [
+ { "name": "status", "in": "query", "schema": { "type": "string", "enum": ["open", "closed"] } },
+ { "name": "ids", "in": "query", "schema": { "type": "array", "items": { "type": "integer" } } }
+ ]
+ }
+ }
+ }
+ }
+ """;
+
+ JsonNode node = objectMapper.readTree(openApi);
+ List tools = mapper.convert(node, new ToolOverridesConfig());
+ McpSchema.Tool tool = tools.getFirst();
+ Map props = tool.inputSchema().properties();
+ Map statusProp = (Map) props.get("status");
+ Map idsProp = (Map) props.get("ids");
+ assertTrue(statusProp.containsKey("enum"));
+ assertEquals("array", idsProp.get("type"));
+ }
+
+ @Test
+ void testToolOverridesTitleDescription() throws Exception {
+ String openApi = """
+ {
+ "openapi": "3.0.0",
+ "paths": {
+ "/override": {
+ "get": {
+ "operationId": "overrideTool",
+ "summary": "Original summary",
+ "description": "Original description"
+ }
+ }
+ }
+ }
+ """;
+
+ JsonNode node = objectMapper.readTree(openApi);
+ ToolOverridesConfig overrides = new ToolOverridesConfig();
+ ToolOverride override = new ToolOverride();
+ override.setTitle("Overridden Title");
+ override.setDescription("Overridden Description");
+ overrides.setTools(Map.of("overrideTool", override));
+
+ List tools = mapper.convert(node, overrides);
+ McpSchema.Tool tool = tools.getFirst();
+ assertEquals("Overridden Title", tool.title());
+ assertEquals("Overridden Description", tool.description());
+ }
+
+ @Test
+ void testRefResolution() throws Exception {
+ String openApi = """
+ {
+ "openapi": "3.0.0",
+ "components": {
+ "schemas": {
+ "User": {
+ "type": "object",
+ "properties": { "name": { "type": "string" } },
+ "required": ["name"]
+ }
+ }
+ },
+ "paths": {
+ "/user": {
+ "post": {
+ "operationId": "createUserRef",
+ "requestBody": {
+ "content": {
+ "application/json": { "schema": { "$ref": "#/components/schemas/User" } }
+ },
+ "required": true
+ }
+ }
+ }
+ }
+ }
+ """;
+
+ JsonNode node = objectMapper.readTree(openApi);
+ List tools = mapper.convert(node, new ToolOverridesConfig());
+ McpSchema.Tool tool = tools.getFirst();
+ Map body = (Map) tool.inputSchema().properties().get("name");
+ assertNotNull(body);
+ }
+
+ @Test
+ void testEmptyPathsThrowsException() {
+ ObjectNode openApiJson = objectMapper.createObjectNode();
+ openApiJson.put("openapi", "3.0.0");
+ ToolOverridesConfig overrides = new ToolOverridesConfig();
+
+ McpServerToolInitializeException ex = assertThrows(McpServerToolInitializeException.class,
+ () -> mapper.convert(openApiJson, overrides));
+ assertEquals(ErrorMessage.MISSING_PATH_IN_SPEC, ex.getMessage());
+ }
+
+ @Test
+ void testUnknownParameterLocationDoesNotCrash() throws Exception {
+ String openApi = """
+ {
+ "openapi": "3.0.0",
+ "paths": {
+ "/weird": {
+ "get": {
+ "operationId": "weirdParam",
+ "parameters": [
+ { "name": "unknown", "in": "body", "schema": { "type": "string" } }
+ ]
+ }
+ }
+ }
+ }
+ """;
+
+ JsonNode node = objectMapper.readTree(openApi);
+ List tools = mapper.convert(node, new ToolOverridesConfig());
+ assertEquals(1, tools.size());
+ McpSchema.Tool tool = tools.getFirst();
+ assertNull(tool.inputSchema().properties());
+ }
+
+ @Test
+ void testCacheUpdatedForAllTools() throws Exception {
+ String openApi = """
+ {
+ "openapi": "3.0.0",
+ "paths": {
+ "/one": { "get": { "operationId": "toolOne" } },
+ "/two": { "get": { "operationId": "toolTwo" } }
+ }
+ }
+ """;
+
+ JsonNode node = objectMapper.readTree(openApi);
+ mapper.convert(node, new ToolOverridesConfig());
+
+ verify(cacheService).putTool(eq("toolOne"), any(McpSchema.Tool.class));
+ verify(cacheService).putTool(eq("toolTwo"), any(McpSchema.Tool.class));
+ }
+
+ @Test
+ void testConvert_WithEmptyRequestBody_DoesNotFail() throws Exception {
+ String openApi = """
+ {
+ "openapi": "3.0.0",
+ "paths": {
+ "/emptyBody": {
+ "post": {
+ "operationId": "emptyBodyTool"
+ }
+ }
+ }
+ }
+ """;
+ JsonNode node = objectMapper.readTree(openApi);
+ List tools = mapper.convert(node, new ToolOverridesConfig());
+
+ assertEquals(1, tools.size());
+ McpSchema.Tool tool = tools.getFirst();
+ assertNotNull(tool.inputSchema());
+ assertNull(tool.inputSchema().properties());
+ assertNull(tool.inputSchema().required());
+ }
+
+ @Test
+ void testConvert_WithNestedAllOf_ShouldMergeProperties() throws Exception {
+ String openApi = """
+ {
+ "openapi": "3.0.0",
+ "paths": {
+ "/nestedAllOf": {
+ "post": {
+ "operationId": "nestedAllOfTool",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "allOf": [
+ { "type": "object", "properties": { "a": { "type": "string" } } },
+ { "type": "object", "properties": { "b": { "type": "integer" } } }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ """;
+ JsonNode node = objectMapper.readTree(openApi);
+ List tools = mapper.convert(node, new ToolOverridesConfig());
+
+ McpSchema.Tool tool = tools.getFirst();
+ Map props = tool.inputSchema().properties();
+ assertTrue(props.containsKey("a"));
+ assertTrue(props.containsKey("b"));
+ }
+
+ @Test
+ void testConvert_WithEnumInRequestBodyArrayItems() throws Exception {
+ String openApi = """
+ {
+ "openapi": "3.0.0",
+ "paths": {
+ "/arrayEnum": {
+ "post": {
+ "operationId": "arrayEnumTool",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": { "type": "string", "enum": ["X", "Y"] }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ """;
+
+ JsonNode node = objectMapper.readTree(openApi);
+ List tools = mapper.convert(node, new ToolOverridesConfig());
+
+ McpSchema.Tool tool = tools.getFirst();
+ Map body = (Map) tool.inputSchema().properties().get("body");
+ assertEquals("array", body.get("type"));
+ Map items = (Map) body.get("items");
+ assertEquals(List.of("X", "Y"), items.get("enum"));
+ }
+
+ @Test
+ void testConvert_WithMultipleMethodsOnSamePath() throws Exception {
+ String openApi = """
+ {
+ "openapi": "3.0.0",
+ "paths": {
+ "/multiMethod": {
+ "get": { "operationId": "getMulti" },
+ "post": { "operationId": "postMulti" }
+ }
+ }
+ }
+ """;
+
+ JsonNode node = objectMapper.readTree(openApi);
+ List tools = mapper.convert(node, new ToolOverridesConfig());
+
+ assertEquals(2, tools.size());
+ assertTrue(tools.stream().anyMatch(t -> t.name().equals("getMulti")));
+ assertTrue(tools.stream().anyMatch(t -> t.name().equals("postMulti")));
+ verify(cacheService).putTool(eq("getMulti"), any(McpSchema.Tool.class));
+ verify(cacheService).putTool(eq("postMulti"), any(McpSchema.Tool.class));
+ }
+
+ @Test
+ void testConvert_WithEmptyComponents_ShouldNotFail() throws Exception {
+ String openApi = """
+ {
+ "openapi": "3.0.0",
+ "components": {},
+ "paths": {
+ "/simple": { "get": { "operationId": "simpleTool" } }
+ }
+ }
+ """;
+
+ JsonNode node = objectMapper.readTree(openApi);
+ List tools = mapper.convert(node, new ToolOverridesConfig());
+
+ assertEquals(1, tools.size());
+ McpSchema.Tool tool = tools.getFirst();
+ assertNotNull(tool.inputSchema());
+ verify(cacheService).putTool(eq("simpleTool"), any(McpSchema.Tool.class));
+ }
+
+ @Test
+ void testConvert_WithDuplicateToolNames_ShouldGenerateUniqueNames() throws Exception {
+ String openApi = """
+ {
+ "openapi": "3.0.0",
+ "paths": {
+ "/dup": { "get": { "operationId": "tool" } },
+ "/dup2": { "get": { "operationId": "tool" } }
+ }
+ }
+ """;
+
+ JsonNode node = objectMapper.readTree(openApi);
+ List tools = mapper.convert(node, new ToolOverridesConfig());
+
+ assertEquals(2, tools.size());
+ Set names = tools.stream().map(McpSchema.Tool::name).collect(java.util.stream.Collectors.toSet());
+ assertEquals(2, names.size()); // ensure uniqueness
+ }
+}
diff --git a/src/openapi-mcp-server/src/test/java/com/oracle/mcp/openapi/mapper/impl/SwaggerToMcpToolMapperTest.java b/src/openapi-mcp-server/src/test/java/com/oracle/mcp/openapi/mapper/impl/SwaggerToMcpToolMapperTest.java
new file mode 100644
index 0000000..cdf8675
--- /dev/null
+++ b/src/openapi-mcp-server/src/test/java/com/oracle/mcp/openapi/mapper/impl/SwaggerToMcpToolMapperTest.java
@@ -0,0 +1,99 @@
+/*
+ * --------------------------------------------------------------------------
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ * Licensed under the Universal Permissive License v1.0 as shown at http://oss.oracle.com/licenses/upl.
+ * --------------------------------------------------------------------------
+ */
+package com.oracle.mcp.openapi.mapper.impl;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.oracle.mcp.openapi.cache.McpServerCacheService;
+import com.oracle.mcp.openapi.constants.ErrorMessage;
+import com.oracle.mcp.openapi.exception.McpServerToolInitializeException;
+import com.oracle.mcp.openapi.model.override.ToolOverridesConfig;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+class SwaggerToMcpToolMapperTest {
+
+ private SwaggerToMcpToolMapper swaggerMapper;
+ private McpServerCacheService cacheService;
+ private ObjectMapper objectMapper;
+
+ @BeforeEach
+ void setUp() {
+ cacheService = mock(McpServerCacheService.class);
+ swaggerMapper = new SwaggerToMcpToolMapper(cacheService);
+ objectMapper = new ObjectMapper();
+ }
+
+ @Test
+ void convert_ShouldCreateTool_WhenOperationPresent() throws McpServerToolInitializeException {
+ // Arrange
+ ObjectNode swaggerJson = objectMapper.createObjectNode();
+ swaggerJson.put("swagger", "2.0");
+
+ ObjectNode paths = objectMapper.createObjectNode();
+ ObjectNode getOp = objectMapper.createObjectNode();
+ getOp.put("operationId", "testTool");
+ paths.set("/test", objectMapper.createObjectNode().set("get", getOp));
+ swaggerJson.set("paths", paths);
+
+ ToolOverridesConfig overrides = new ToolOverridesConfig();
+
+ // Act
+ List tools = swaggerMapper.convert(swaggerJson, overrides);
+
+ // Assert
+ assertEquals(1, tools.size(), "One tool should be created");
+ assertEquals("testTool", tools.get(0).name());
+ verify(cacheService).putTool("testTool", tools.get(0));
+ }
+
+ @Test
+ void convert_ShouldSkipTool_WhenInExcludeList() throws McpServerToolInitializeException {
+ // Arrange
+ ObjectNode swaggerJson = objectMapper.createObjectNode();
+ swaggerJson.put("swagger", "2.0");
+
+ ObjectNode paths = objectMapper.createObjectNode();
+ ObjectNode getOp = objectMapper.createObjectNode();
+ getOp.put("operationId", "skipTool");
+ paths.set("/skip", objectMapper.createObjectNode().set("get", getOp));
+ swaggerJson.set("paths", paths);
+
+ ToolOverridesConfig overrides = new ToolOverridesConfig();
+ overrides.setExclude(Set.of("skipTool"));
+
+ // Act
+ List tools = swaggerMapper.convert(swaggerJson, overrides);
+
+ // Assert
+ assertTrue(tools.isEmpty(), "Tool should be skipped due to exclude list");
+ verify(cacheService, never()).putTool(anyString(), any(McpSchema.Tool.class));
+ }
+
+ @Test
+ void convert_ShouldThrowException_WhenPathsMissing() {
+ // Arrange
+ ObjectNode swaggerJson = objectMapper.createObjectNode();
+ swaggerJson.put("swagger", "2.0");
+
+ ToolOverridesConfig overrides = new ToolOverridesConfig();
+
+ // Act & Assert
+ McpServerToolInitializeException ex = assertThrows(
+ McpServerToolInitializeException.class,
+ () -> swaggerMapper.convert(swaggerJson, overrides)
+ );
+ assertEquals(ErrorMessage.MISSING_PATH_IN_SPEC, ex.getMessage());
+ }
+}
diff --git a/src/openapi-mcp-server/src/test/java/com/oracle/mcp/openapi/model/McpServerConfigTest.java b/src/openapi-mcp-server/src/test/java/com/oracle/mcp/openapi/model/McpServerConfigTest.java
new file mode 100644
index 0000000..79ecc50
--- /dev/null
+++ b/src/openapi-mcp-server/src/test/java/com/oracle/mcp/openapi/model/McpServerConfigTest.java
@@ -0,0 +1,224 @@
+/*
+ * --------------------------------------------------------------------------
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ * Licensed under the Universal Permissive License v1.0 as shown at http://oss.oracle.com/licenses/upl.
+ * --------------------------------------------------------------------------
+ */
+package com.oracle.mcp.openapi.model;
+
+import com.oracle.mcp.openapi.constants.ErrorMessage;
+import com.oracle.mcp.openapi.exception.McpServerToolInitializeException;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Unit tests for {@link McpServerConfig}.
+ *
+ * @author Joby Wilson Mathews (joby.mathews@oracle.com)
+ */
+class McpServerConfigTest {
+
+ @Test
+ void fromArgs_whenAllCliArgsProvided_thenConfigIsCreatedCorrectly() throws McpServerToolInitializeException {
+ String[] args = {
+ "--api-name", "MyTestApi",
+ "--api-base-url", "https://api.example.com",
+ "--api-spec", "path/to/spec.json",
+ "--auth-type", "CUSTOM",
+ "--auth-custom-headers", "{\"X-Custom-Auth\":\"secret-value\"}",
+ "--connect-timeout", "5000",
+ "--response-timeout", "15000",
+ "--http-version", "HTTP_1_1",
+ "--http-redirect", "ALWAYS",
+ "--proxy-host", "proxy.example.com",
+ "--proxy-port", "8080"
+ };
+
+ McpServerConfig config = McpServerConfig.fromArgs(args);
+
+ assertThat(config.getApiName()).isEqualTo("MyTestApi");
+ assertThat(config.getApiBaseUrl()).isEqualTo("https://api.example.com");
+ assertThat(config.getApiSpec()).isEqualTo("path/to/spec.json");
+ assertThat(config.getRawAuthType()).isEqualTo("CUSTOM");
+ assertThat(config.getAuthCustomHeaders()).isEqualTo(Map.of("X-Custom-Auth", "secret-value"));
+ assertThat(config.getConnectTimeout()).isEqualTo("5000");
+ assertThat(config.getConnectTimeoutMs()).isEqualTo(5000L);
+ assertThat(config.getResponseTimeout()).isEqualTo("15000");
+ assertThat(config.getResponseTimeoutMs()).isEqualTo(15000L);
+ assertThat(config.getHttpVersion()).isEqualTo("HTTP_1_1");
+ assertThat(config.getRedirectPolicy()).isEqualTo("ALWAYS");
+ assertThat(config.getProxyHost()).isEqualTo("proxy.example.com");
+ assertThat(config.getProxyPort()).isEqualTo(8080);
+ }
+
+ @Test
+ void fromArgs_whenOptionalNetworkArgsAreMissing_thenDefaultsAreUsed() throws McpServerToolInitializeException {
+ String[] args = {
+ "--api-base-url", "https://api.example.com",
+ "--api-spec", "spec.json"
+ };
+
+ McpServerConfig config = McpServerConfig.fromArgs(args);
+
+ assertThat(config.getConnectTimeout()).isEqualTo("10000");
+ assertThat(config.getResponseTimeout()).isEqualTo("30000");
+ assertThat(config.getHttpVersion()).isEqualTo("HTTP_2");
+ assertThat(config.getRedirectPolicy()).isEqualTo("NORMAL");
+ assertThat(config.getProxyHost()).isNull();
+ assertThat(config.getProxyPort()).isNull();
+ }
+
+ @Test
+ void fromArgs_whenNoArgsProvided_thenThrowsException() {
+ String[] args = {};
+ assertThatThrownBy(() -> McpServerConfig.fromArgs(args))
+ .isInstanceOf(McpServerToolInitializeException.class)
+ .hasMessage(ErrorMessage.MISSING_API_BASE_URL);
+ }
+
+ @Test
+ void fromArgs_whenMissingApiBaseUrl_thenThrowsException() {
+ String[] args = {"--api-spec", "spec.json"};
+
+ assertThatThrownBy(() -> McpServerConfig.fromArgs(args))
+ .isInstanceOf(McpServerToolInitializeException.class)
+ .hasMessage(ErrorMessage.MISSING_API_BASE_URL);
+ }
+
+ @Test
+ void fromArgs_whenMissingApiSpec_thenThrowsException() {
+ String[] args = {"--api-base-url", "https://api.example.com"};
+
+ assertThatThrownBy(() -> McpServerConfig.fromArgs(args))
+ .isInstanceOf(McpServerToolInitializeException.class)
+ .hasMessage(ErrorMessage.MISSING_API_SPEC);
+ }
+
+ @Test
+ void fromArgs_whenAuthTypeApiKeyButMissingKey_thenThrowsException() {
+ String[] args = {
+ "--api-base-url", "url", "--api-spec", "spec",
+ "--auth-type", "API_KEY",
+ "--auth-api-key-name", "X-API-KEY",
+ "--auth-api-key-in", "header"
+ };
+ assertThatThrownBy(() -> McpServerConfig.fromArgs(args))
+ .isInstanceOf(McpServerToolInitializeException.class)
+ .hasMessage("Missing API Key value for auth type API_KEY");
+ }
+
+ @Test
+ void fromArgs_whenAuthTypeApiKeyButMissingKeyName_thenThrowsException() {
+ String[] args = {
+ "--api-base-url", "url", "--api-spec", "spec",
+ "--auth-type", "API_KEY",
+ "--auth-api-key", "secretkey",
+ "--auth-api-key-in", "header"
+ };
+ assertThatThrownBy(() -> McpServerConfig.fromArgs(args))
+ .isInstanceOf(McpServerToolInitializeException.class)
+ .hasMessage("Missing API Key name (--auth-api-key-name) for auth type API_KEY");
+ }
+
+ @Test
+ void fromArgs_whenAuthTypeApiKeyButInvalidLocation_thenThrowsException() {
+ String[] args = {
+ "--api-base-url", "url", "--api-spec", "spec",
+ "--auth-type", "API_KEY",
+ "--auth-api-key", "secretkey",
+ "--auth-api-key-name", "X-API-KEY",
+ "--auth-api-key-in", "cookie" // Invalid location
+ };
+ assertThatThrownBy(() -> McpServerConfig.fromArgs(args))
+ .isInstanceOf(McpServerToolInitializeException.class)
+ .hasMessage("Invalid or missing API Key location (--auth-api-key-in). Must be 'header' or 'query'.");
+ }
+
+ @Test
+ void fromArgs_whenAuthTypeBasicButMissingUsername_thenThrowsException() {
+ String[] args = {
+ "--api-base-url", "url", "--api-spec", "spec",
+ "--auth-type", "BASIC",
+ "--auth-password", "secretpass"
+ };
+ assertThatThrownBy(() -> McpServerConfig.fromArgs(args))
+ .isInstanceOf(McpServerToolInitializeException.class)
+ .hasMessage("Missing username for BASIC auth");
+ }
+
+ @Test
+ void fromArgs_whenAuthTypeBasicButMissingPassword_thenThrowsException() {
+ String[] args = {
+ "--api-base-url", "url", "--api-spec", "spec",
+ "--auth-type", "BASIC",
+ "--auth-username", "user"
+ };
+ assertThatThrownBy(() -> McpServerConfig.fromArgs(args))
+ .isInstanceOf(McpServerToolInitializeException.class)
+ .hasMessage("Missing password for BASIC auth");
+ }
+
+ @Test
+ void fromArgs_whenAuthTypeBearerButMissingToken_thenThrowsException() {
+ String[] args = {
+ "--api-base-url", "url", "--api-spec", "spec",
+ "--auth-type", "BEARER"
+ };
+ assertThatThrownBy(() -> McpServerConfig.fromArgs(args))
+ .isInstanceOf(McpServerToolInitializeException.class)
+ .hasMessage("Missing bearer token for BEARER auth");
+ }
+
+ @Test
+ void fromArgs_whenCustomHeadersAreInvalidJson_thenThrowsException() {
+ String[] args = {
+ "--api-base-url", "url", "--api-spec", "spec",
+ "--auth-type", "CUSTOM",
+ "--auth-custom-headers", "{not-valid-json}"
+ };
+ assertThatThrownBy(() -> McpServerConfig.fromArgs(args))
+ .isInstanceOf(McpServerToolInitializeException.class)
+ .hasMessageStartingWith("Invalid JSON format for --auth-custom-headers:");
+ }
+
+ @Test
+ void getTimeoutMs_whenValueIsInvalid_returnsDefaultValue() {
+ McpServerConfig config = McpServerConfig.builder()
+ .connectTimeout("invalid")
+ .responseTimeout("not-a-number")
+ .build();
+
+ assertThat(config.getConnectTimeoutMs()).isEqualTo(10_000L);
+ assertThat(config.getResponseTimeoutMs()).isEqualTo(30_000L);
+ }
+
+ @Test
+ void getProxyPort_whenValueIsInvalid_returnsNull() throws McpServerToolInitializeException {
+ String[] args = {
+ "--api-base-url", "url", "--api-spec", "spec",
+ "--proxy-port", "not-an-integer"
+ };
+
+ McpServerConfig config = McpServerConfig.fromArgs(args);
+ assertThat(config.getProxyPort()).isNull();
+ }
+
+ @Test
+ void builder_whenSecretsAreSet_clonesTheCharArrays() {
+ char[] originalToken = {'t', 'o', 'k', 'e', 'n'};
+ McpServerConfig config = McpServerConfig.builder()
+ .authToken(originalToken)
+ .build();
+
+ // Modify the original array after building
+ originalToken[0] = 'X';
+
+ // The config should hold the original, unmodified value
+ assertThat(config.getAuthToken()).isEqualTo(new char[]{'t', 'o', 'k', 'e', 'n'});
+ }
+}
+
diff --git a/src/openapi-mcp-server/src/test/java/com/oracle/mcp/openapi/tool/OpenApiMcpToolExecutorTest.java b/src/openapi-mcp-server/src/test/java/com/oracle/mcp/openapi/tool/OpenApiMcpToolExecutorTest.java
new file mode 100644
index 0000000..9a98c39
--- /dev/null
+++ b/src/openapi-mcp-server/src/test/java/com/oracle/mcp/openapi/tool/OpenApiMcpToolExecutorTest.java
@@ -0,0 +1,264 @@
+/*
+ * --------------------------------------------------------------------------
+ * Copyright (c) 2025, Oracle and/or its affiliates.
+ * Licensed under the Universal Permissive License v1.0 as shown at http://oss.oracle.com/licenses/upl.
+ * --------------------------------------------------------------------------
+ */
+package com.oracle.mcp.openapi.tool;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.oracle.mcp.openapi.cache.McpServerCacheService;
+import com.oracle.mcp.openapi.constants.CommonConstant;
+import com.oracle.mcp.openapi.enums.OpenApiSchemaAuthType;
+import com.oracle.mcp.openapi.model.McpServerConfig;
+import com.oracle.mcp.openapi.rest.RestApiAuthHandler;
+import com.oracle.mcp.openapi.rest.RestApiExecutionService;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.IOException;
+import java.net.http.HttpResponse;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link OpenApiMcpToolExecutor}.
+ *
+ * @author Joby Wilson Mathews (joby.mathews@oracle.com)
+ */
+@ExtendWith(MockitoExtension.class)
+class OpenApiMcpToolExecutorTest {
+
+ @Mock
+ private McpServerCacheService mcpServerCacheService;
+
+ @Mock
+ private RestApiExecutionService restApiExecutionService;
+
+ @Mock
+ private RestApiAuthHandler restApiAuthHandler;
+
+ @Spy
+ private ObjectMapper jsonMapper = new ObjectMapper();
+
+ @InjectMocks
+ private OpenApiMcpToolExecutor openApiMcpToolExecutor;
+
+ @Captor
+ private ArgumentCaptor urlCaptor;
+
+ @Captor
+ private ArgumentCaptor methodCaptor;
+
+ @Captor
+ private ArgumentCaptor bodyCaptor;
+
+ @Captor
+ private ArgumentCaptor> headersCaptor;
+
+ private McpServerConfig serverConfig;
+
+ @BeforeEach
+ void setUp() {
+ serverConfig = new McpServerConfig.Builder().apiBaseUrl("https://api.example.com").build();
+ }
+
+ /**
+ * Tests a successful execution of a POST request with path and query parameters.
+ */
+ @Test
+ void execute_PostRequest_Successful() throws IOException, InterruptedException {
+ // Arrange
+ Map arguments = new HashMap<>();
+ arguments.put("userId", 123);
+ arguments.put("filter", "active");
+ arguments.put("requestData", Map.of("name", "test"));
+
+ McpSchema.CallToolRequest callRequest = McpSchema.CallToolRequest.builder()
+ .name("createUser")
+ .arguments(arguments)
+ .build();
+
+ Map meta = new HashMap<>();
+ meta.put("httpMethod", "POST");
+ meta.put(CommonConstant.PATH, "/users/{userId}");
+ meta.put("pathParams", Map.of("userId", "integer"));
+ meta.put("queryParams", Map.of("filter", "string"));
+
+ McpSchema.Tool tool = McpSchema.Tool.builder()
+ .name("createUser")
+ .meta(meta)
+ .build();
+
+ when(mcpServerCacheService.getTool("createUser")).thenReturn(tool);
+ when(mcpServerCacheService.getServerConfig()).thenReturn(serverConfig);
+ Map headers = new HashMap<>();
+ headers.put("Authorization", "Bearer token");
+ when(restApiAuthHandler.extractAuthHeaders(serverConfig)).thenReturn(headers);
+ HttpResponse mockResponse = mock(HttpResponse.class);
+ when(mockResponse.statusCode()).thenReturn(200);
+ when(mockResponse.body()).thenReturn("{\"status\":\"success\"}");
+
+ when(restApiExecutionService.executeRequest(anyString(), anyString(), anyString(), any()))
+ .thenReturn(mockResponse);
+
+ // Act
+ McpSchema.CallToolResult result = openApiMcpToolExecutor.execute(callRequest);
+
+ // Assert
+ assertNotNull(result);
+ assertTrue(result.structuredContent().containsKey("response"));
+ assertEquals("{\"status\":\"success\"}", result.structuredContent().get("response"));
+
+ verify(restApiExecutionService).executeRequest(urlCaptor.capture(), methodCaptor.capture(), bodyCaptor.capture(), headersCaptor.capture());
+ assertEquals("https://api.example.com/users/123?filter=active", urlCaptor.getValue());
+ assertEquals("POST", methodCaptor.getValue());
+ assertEquals("{\"requestData\":{\"name\":\"test\"}}", bodyCaptor.getValue());
+ assertEquals("Bearer token", headersCaptor.getValue().get("Authorization"));
+ assertEquals("application/json", headersCaptor.getValue().get("Content-Type"));
+ }
+
+ /**
+ * Tests a successful execution of a GET request, ensuring no request body is sent.
+ */
+ @Test
+ void execute_GetRequest_Successful() throws IOException, InterruptedException {
+ // Arrange
+ McpSchema.CallToolRequest callRequest = McpSchema.CallToolRequest.builder()
+ .name("getUser")
+ .arguments(Collections.emptyMap())
+ .build();
+
+ Map meta = Map.of("httpMethod", "GET", CommonConstant.PATH, "/user");
+ McpSchema.Tool tool = McpSchema.Tool.builder().name("getUser").meta(meta).build();
+
+ when(mcpServerCacheService.getTool("getUser")).thenReturn(tool);
+ when(mcpServerCacheService.getServerConfig()).thenReturn(serverConfig);
+ HttpResponse mockResponse = mock(HttpResponse.class);
+ when(mockResponse.statusCode()).thenReturn(200);
+ when(mockResponse.body()).thenReturn("{\"id\":1}");
+
+ when(restApiExecutionService.executeRequest(anyString(), anyString(), any(), any()))
+ .thenReturn(mockResponse);
+
+ // Act
+ openApiMcpToolExecutor.execute(callRequest);
+
+ // Assert
+ verify(restApiExecutionService).executeRequest(urlCaptor.capture(), methodCaptor.capture(), bodyCaptor.capture(), any());
+ assertEquals("https://api.example.com/user", urlCaptor.getValue());
+ assertEquals("GET", methodCaptor.getValue());
+ assertNull(bodyCaptor.getValue());
+ }
+
+ /**
+ * Tests that an API key is correctly appended to the query parameters when configured.
+ */
+ @Test
+ void execute_ApiKeyInQuery_Successful() throws IOException, InterruptedException {
+ // Arrange
+ serverConfig = new McpServerConfig.Builder()
+ .apiBaseUrl("https://api.example.com")
+ .authType(OpenApiSchemaAuthType.API_KEY.name())
+ .authApiKeyIn("query")
+ .authApiKeyName("api_key")
+ .authApiKey("test-secret-key".toCharArray())
+ .build();
+
+ McpSchema.CallToolRequest callRequest = McpSchema.CallToolRequest.builder()
+ .name("getData")
+ .arguments(Collections.emptyMap())
+ .build();
+
+ Map meta = Map.of("httpMethod", "GET", CommonConstant.PATH, "/data");
+ McpSchema.Tool tool = McpSchema.Tool.builder().name("getData").meta(meta).build();
+
+ when(mcpServerCacheService.getTool("getData")).thenReturn(tool);
+ when(mcpServerCacheService.getServerConfig()).thenReturn(serverConfig);
+
+ // Mock HTTP response
+ HttpResponse mockResponse = mock(HttpResponse.class);
+ when(mockResponse.statusCode()).thenReturn(200);
+ when(mockResponse.body()).thenReturn("{\"status\":\"success\"}");
+
+ when(restApiExecutionService.executeRequest(anyString(), anyString(), any(), any()))
+ .thenReturn(mockResponse);
+
+ // Act
+ openApiMcpToolExecutor.execute(callRequest);
+
+ // Assert
+ verify(restApiExecutionService).executeRequest(urlCaptor.capture(), anyString(), any(), any());
+ assertEquals("https://api.example.com/data?api_key=test-secret-key", urlCaptor.getValue());
+ }
+
+ /**
+ * Tests proper URL encoding of path and query parameters.
+ */
+ @Test
+ void execute_UrlEncoding_Successful() throws IOException, InterruptedException {
+ // Arrange
+ Map arguments = new HashMap<>();
+ arguments.put("folderName", "my documents/work");
+ arguments.put("searchTerm", "a&b=c");
+
+ McpSchema.CallToolRequest callRequest = McpSchema.CallToolRequest.builder()
+ .name("searchFiles")
+ .arguments(arguments)
+ .build();
+
+ Map meta = new HashMap<>();
+ meta.put("httpMethod", "GET");
+ meta.put(CommonConstant.PATH, "/files/{folderName}");
+ meta.put("pathParams", Map.of("folderName", "string"));
+ meta.put("queryParams", Map.of("searchTerm", "string"));
+ McpSchema.Tool tool = McpSchema.Tool.builder()
+ .name("searchFiles")
+ .meta(meta)
+ .build();
+
+ when(mcpServerCacheService.getTool("searchFiles")).thenReturn(tool);
+ when(mcpServerCacheService.getServerConfig()).thenReturn(serverConfig);
+
+ // Mock HTTP response
+ HttpResponse mockResponse = mock(HttpResponse.class);
+ when(mockResponse.statusCode()).thenReturn(200);
+ when(mockResponse.body()).thenReturn("{\"result\":\"ok\"}");
+
+ when(restApiExecutionService.executeRequest(anyString(), anyString(), any(), any()))
+ .thenReturn(mockResponse);
+
+ // Act
+ McpSchema.CallToolResult result = openApiMcpToolExecutor.execute(callRequest);
+
+ // Assert
+ verify(restApiExecutionService).executeRequest(urlCaptor.capture(), anyString(), any(), any());
+ String expectedUrl = "https://api.example.com/files/my%20documents%2Fwork?searchTerm=a%26b%3Dc";
+ assertEquals(expectedUrl, urlCaptor.getValue());
+
+ // Verify executor processed the response correctly
+ assertFalse(result.isError());
+ assertEquals("{\"result\":\"ok\"}", result.structuredContent().get("response"));
+ }
+
+}
\ No newline at end of file
diff --git a/src/openapi-mcp-server/src/test/resources/__files/companies-response.json b/src/openapi-mcp-server/src/test/resources/__files/companies-response.json
new file mode 100644
index 0000000..9f34329
--- /dev/null
+++ b/src/openapi-mcp-server/src/test/resources/__files/companies-response.json
@@ -0,0 +1 @@
+[{"id":1,"name":"Acme Corp"},{"id":2,"name":"Globex Ltd"}]
\ No newline at end of file
diff --git a/src/openapi-mcp-server/src/test/resources/__files/company-1-response.json b/src/openapi-mcp-server/src/test/resources/__files/company-1-response.json
new file mode 100644
index 0000000..17a4938
--- /dev/null
+++ b/src/openapi-mcp-server/src/test/resources/__files/company-1-response.json
@@ -0,0 +1 @@
+{"id":1,"name":"Acme Corp","industry":"Technology"}
\ No newline at end of file
diff --git a/src/openapi-mcp-server/src/test/resources/__files/company-department-swagger.json b/src/openapi-mcp-server/src/test/resources/__files/company-department-swagger.json
new file mode 100644
index 0000000..8a5b10e
--- /dev/null
+++ b/src/openapi-mcp-server/src/test/resources/__files/company-department-swagger.json
@@ -0,0 +1,232 @@
+{
+ "openapi": "3.0.0",
+ "info": {
+ "title": "Company API",
+ "description": "A simple API to manage companies, departments, and users.",
+ "version": "1.0.0"
+ },
+ "servers": [
+ {
+ "url": "http://localhost:8080",
+ "description": "Mock server"
+ }
+ ],
+ "paths": {
+ "/rest/v1/companies": {
+ "get": {
+ "summary": "Get all companies",
+ "description": "Retrieves a list of all companies.",
+ "operationId": "getCompanies",
+ "responses": {
+ "200": {
+ "description": "A list of companies.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/CompanySummary"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "summary": "Create a new company",
+ "description": "Creates a new company and returns the created company object.",
+ "operationId": "createCompany",
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/CompanyCreateRequest"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Company created successfully.",
+ "headers": {
+ "Location": {
+ "description": "The URL of the newly created company.",
+ "schema": {
+ "type": "string",
+ "example": "/rest/v1/companies/123"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Company"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid request payload."
+ }
+ }
+ }
+ },
+ "/rest/v1/companies/{companyId}": {
+ "get": {
+ "summary": "Get a company by ID",
+ "description": "Retrieves a single company by its ID.",
+ "operationId": "getCompanyById",
+ "parameters": [
+ {
+ "name": "companyId",
+ "in": "path",
+ "required": true,
+ "description": "The ID of the company to retrieve.",
+ "schema": {
+ "type": "integer",
+ "example": 1
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "The company object.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Company"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Company not found."
+ }
+ }
+ },
+ "patch": {
+ "summary": "Update a company",
+ "description": "Partially updates an existing company. Only the fields provided in the request body will be updated.",
+ "operationId": "updateCompany",
+ "parameters": [
+ {
+ "name": "companyId",
+ "in": "path",
+ "required": true,
+ "description": "The ID of the company to update.",
+ "schema": {
+ "type": "integer",
+ "example": 1
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/CompanyUpdateRequest"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Company updated successfully.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Company"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid request payload."
+ },
+ "404": {
+ "description": "Company not found."
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "CompanySummary": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "example": 1
+ },
+ "name": {
+ "type": "string",
+ "example": "Acme Corp"
+ }
+ }
+ },
+ "Company": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "example": 123
+ },
+ "name": {
+ "type": "string",
+ "example": "New Company"
+ },
+ "industry": {
+ "type": "string",
+ "example": "Technology"
+ },
+ "address": {
+ "type": "string",
+ "example": "123 Test Street"
+ }
+ }
+ },
+ "CompanyCreateRequest": {
+ "type": "object",
+ "required": [
+ "name",
+ "industry"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "example": "New Company"
+ },
+ "industry": {
+ "type": "string",
+ "example": "Finance"
+ },
+ "address": {
+ "type": "string",
+ "example": "123 Test Street"
+ }
+ }
+ },
+ "CompanyUpdateRequest": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "example": "Updated Company Name"
+ },
+ "industry": {
+ "type": "string",
+ "example": "Manufacturing"
+ },
+ "address": {
+ "type": "string",
+ "example": "456 New Avenue"
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/openapi-mcp-server/src/test/resources/mappings/comapny-department-swagger.json b/src/openapi-mcp-server/src/test/resources/mappings/comapny-department-swagger.json
new file mode 100644
index 0000000..c97763f
--- /dev/null
+++ b/src/openapi-mcp-server/src/test/resources/mappings/comapny-department-swagger.json
@@ -0,0 +1,13 @@
+{
+ "request": {
+ "method": "GET",
+ "url": "/rest/v1/metadata-catalog/companies"
+ },
+ "response": {
+ "status": 200,
+ "bodyFileName": "company-department-swagger.json",
+ "headers": {
+ "Content-Type": "application/json"
+ }
+ }
+}
diff --git a/src/openapi-mcp-server/src/test/resources/mappings/companies.json b/src/openapi-mcp-server/src/test/resources/mappings/companies.json
new file mode 100644
index 0000000..6ee192a
--- /dev/null
+++ b/src/openapi-mcp-server/src/test/resources/mappings/companies.json
@@ -0,0 +1,17 @@
+{
+ "request": {
+ "method": "GET",
+ "url": "/rest/v1/companies",
+ "basicAuth": {
+ "username": "test-user",
+ "password": "test-password"
+ }
+ },
+ "response": {
+ "status": 200,
+ "bodyFileName": "companies-response.json",
+ "headers": {
+ "Content-Type": "application/json"
+ }
+ }
+}
diff --git a/src/openapi-mcp-server/src/test/resources/mappings/company-by-id.json b/src/openapi-mcp-server/src/test/resources/mappings/company-by-id.json
new file mode 100644
index 0000000..3d476e7
--- /dev/null
+++ b/src/openapi-mcp-server/src/test/resources/mappings/company-by-id.json
@@ -0,0 +1,18 @@
+{
+ "request": {
+ "method": "GET",
+ "urlPattern": "/rest/v1/companies/1",
+ "headers": {
+ "Authorization": {
+ "equalTo": "Bearer test-token"
+ }
+ }
+ },
+ "response": {
+ "status": 200,
+ "bodyFileName": "company-1-response.json",
+ "headers": {
+ "Content-Type": "application/json"
+ }
+ }
+}
diff --git a/src/openapi-mcp-server/src/test/resources/mappings/create-company.json b/src/openapi-mcp-server/src/test/resources/mappings/create-company.json
new file mode 100644
index 0000000..60266e6
--- /dev/null
+++ b/src/openapi-mcp-server/src/test/resources/mappings/create-company.json
@@ -0,0 +1,30 @@
+{
+ "request": {
+ "method": "POST",
+ "url": "/rest/v1/companies",
+ "headers": {
+ "X-API-KEY": {
+ "equalTo": "test-api-key"
+ },
+ "Content-Type": {
+ "equalTo": "application/json"
+ }
+ },
+ "bodyPatterns": [
+ {
+ "matchesJsonPath": "$.name"
+ },
+ {
+ "matchesJsonPath": "$.address"
+ }
+ ]
+ },
+ "response": {
+ "status": 201,
+ "bodyFileName": "company-1-response.json",
+ "headers": {
+ "Content-Type": "application/json",
+ "Location": "/rest/v1/companies/1"
+ }
+ }
+}
diff --git a/src/openapi-mcp-server/src/test/resources/mappings/update-company.json b/src/openapi-mcp-server/src/test/resources/mappings/update-company.json
new file mode 100644
index 0000000..a9b1761
--- /dev/null
+++ b/src/openapi-mcp-server/src/test/resources/mappings/update-company.json
@@ -0,0 +1,22 @@
+{
+ "request": {
+ "method": "PATCH",
+ "url": "/rest/v1/companies/1",
+ "headers": {
+ "CUSTOM-HEADER": {
+ "equalTo": "test-custom-key"
+ },
+ "Content-Type": {
+ "equalTo": "application/json"
+ }
+ }
+ },
+ "response": {
+ "status": 201,
+ "bodyFileName": "company-1-response.json",
+ "headers": {
+ "Content-Type": "application/json",
+ "Location": "/rest/v1/companies/1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/openapi-mcp-server/src/test/resources/tools/listTool.json b/src/openapi-mcp-server/src/test/resources/tools/listTool.json
new file mode 100644
index 0000000..f38aaa8
--- /dev/null
+++ b/src/openapi-mcp-server/src/test/resources/tools/listTool.json
@@ -0,0 +1 @@
+[{"tool":{"name":"getCompanies","title":"Get all companies","description":"Get all companies","inputSchema":{"type":"object","additionalProperties":false},"outputSchema":{"additionalProperties":true,"type":"object"},"_meta":{"httpMethod":"GET","path":"/rest/v1/companies"}},"call":null,"callHandler":{}},{"tool":{"name":"createCompany","title":"Create a new company","description":"Create a new company","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"industry":{"type":"string"},"address":{"type":"string"}},"required":["industry","name"],"additionalProperties":false},"outputSchema":{"additionalProperties":true,"type":"object"},"_meta":{"httpMethod":"POST","path":"/rest/v1/companies"}},"call":null,"callHandler":{}},{"tool":{"name":"getCompanyById","title":"Get a company by ID","description":"Get a company by ID","inputSchema":{"type":"object","properties":{"companyId":{"type":"integer"}},"required":["companyId"],"additionalProperties":false},"outputSchema":{"additionalProperties":true,"type":"object"},"_meta":{"httpMethod":"GET","path":"/rest/v1/companies/{companyId}","pathParams":{"companyId":{"type":"integer","description":"The ID of the company to retrieve."}}}},"call":null,"callHandler":{}},{"tool":{"name":"updateCompany","title":"Update a company","description":"Update a company","inputSchema":{"type":"object","properties":{"companyId":{"type":"integer"},"name":{"type":"string"},"industry":{"type":"string"},"address":{"type":"string"}},"required":["companyId"],"additionalProperties":false},"outputSchema":{"additionalProperties":true,"type":"object"},"_meta":{"httpMethod":"PATCH","path":"/rest/v1/companies/{companyId}","pathParams":{"companyId":{"type":"integer","description":"The ID of the company to update."}}}},"call":null,"callHandler":{}}]
\ No newline at end of file