UNPKG

combined-memory-mcp

Version:

MCP server for Combined Memory API - AI-powered chat with unlimited context, memory management, voice agents, and 500+ tool integrations

428 lines (427 loc) 22.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.mapOpenApiToMcpTools = mapOpenApiToMcpTools; const config_1 = require("./config"); const minimatch_1 = require("minimatch"); // Enhanced mapping from OpenAPI type/format to JSON Schema type // Now preserves format information function mapOpenApiTypeToJsonSchemaType(openApiSchema) { if (!openApiSchema || !openApiSchema.type) return { type: 'string' }; // Default to string if type is missing // Handle nullable types const nullable = openApiSchema.nullable === true; // Extract base type and format const { type, format } = openApiSchema; let jsonType; switch (type) { case 'integer': jsonType = 'integer'; break; case 'number': jsonType = 'number'; break; case 'boolean': jsonType = 'boolean'; break; case 'string': jsonType = 'string'; break; case 'array': jsonType = 'array'; break; case 'object': jsonType = 'object'; break; default: console.error(`Unsupported OpenAPI type: ${type}. Defaulting to string.`); jsonType = 'string'; } return { type: jsonType, format, nullable }; } // Convert OpenAPI Schema Object to JSON Schema // Enhanced to handle nullable types and preserve formats function openApiSchemaToJsonSchema(openApiSchema) { if (!openApiSchema) return undefined; const jsonSchema = {}; const { type, format, nullable } = mapOpenApiTypeToJsonSchemaType(openApiSchema); // Handle nullable types by making it a union type if (nullable) { // JSON Schema 7 supports type arrays for union types jsonSchema.type = [type, 'null']; // Using 'any' to bypass TypeScript's type checking } else { jsonSchema.type = type; } // Preserve format if available if (format) { jsonSchema.format = format; } // Map descriptions and metadata if (openApiSchema.description) { jsonSchema.description = openApiSchema.description; } if (openApiSchema.default !== undefined) { jsonSchema.default = openApiSchema.default; } if (openApiSchema.enum) { jsonSchema.enum = openApiSchema.enum; } if (openApiSchema.example !== undefined) { // Store example as an annotation jsonSchema.example = openApiSchema.example; } // Map constraints if (typeof openApiSchema.minimum === 'number') { jsonSchema.minimum = openApiSchema.minimum; } if (typeof openApiSchema.maximum === 'number') { jsonSchema.maximum = openApiSchema.maximum; } if (typeof openApiSchema.minLength === 'number') { jsonSchema.minLength = openApiSchema.minLength; } if (typeof openApiSchema.maxLength === 'number') { jsonSchema.maxLength = openApiSchema.maxLength; } if (openApiSchema.pattern) { jsonSchema.pattern = openApiSchema.pattern; } // Add multipleOf constraint if (typeof openApiSchema.multipleOf === 'number') { jsonSchema.multipleOf = openApiSchema.multipleOf; } // Add array constraints if (typeof openApiSchema.minItems === 'number') { jsonSchema.minItems = openApiSchema.minItems; } if (typeof openApiSchema.maxItems === 'number') { jsonSchema.maxItems = openApiSchema.maxItems; } if (typeof openApiSchema.uniqueItems === 'boolean') { jsonSchema.uniqueItems = openApiSchema.uniqueItems; } // Handle object properties if (openApiSchema.type === 'object' && openApiSchema.properties) { jsonSchema.properties = {}; for (const propName in openApiSchema.properties) { const propSchema = openApiSchema.properties[propName]; if (isSchemaObject(propSchema)) { jsonSchema.properties[propName] = safeJsonSchema(openApiSchemaToJsonSchema(propSchema)); } else { console.error(`Skipping non-schema property or reference: ${propName}`); } } if (openApiSchema.required) { jsonSchema.required = openApiSchema.required; } // Handle additionalProperties if (openApiSchema.additionalProperties !== undefined) { if (openApiSchema.additionalProperties === true) { jsonSchema.additionalProperties = true; } else if (openApiSchema.additionalProperties === false) { jsonSchema.additionalProperties = false; } else if (isSchemaObject(openApiSchema.additionalProperties)) { jsonSchema.additionalProperties = safeJsonSchema(openApiSchemaToJsonSchema(openApiSchema.additionalProperties)); } } } // Handle array items if (openApiSchema.type === 'array' && openApiSchema.items) { if (isSchemaObject(openApiSchema.items)) { jsonSchema.items = safeJsonSchema(openApiSchemaToJsonSchema(openApiSchema.items)); } else { console.error(`Skipping non-schema array item or reference.`); } } // Copy extensions (x-... properties) Object.keys(openApiSchema).forEach(key => { if (key.startsWith('x-')) { jsonSchema[key] = openApiSchema[key]; } }); return jsonSchema; } /** * Checks if an operation matches the whitelist or blacklist patterns * @param operationId The operation ID to check * @param path The URL path of the operation * @param method The HTTP method of the operation * @returns true if the operation should be included, false otherwise */ function shouldIncludeOperation(operationId, path, method) { // If no operationId and whitelist is enabled, use path+method as fallback for matching const opId = operationId || `${method.toUpperCase()}:${path}`; const urlPattern = `${method.toUpperCase()}:${path}`; // If whitelist is enabled, include only operations that match a whitelist pattern if (config_1.config.filter.whitelist) { return config_1.config.filter.whitelist.some((pattern) => { // Check if pattern matches operationId (if it exists) if (operationId && (0, minimatch_1.minimatch)(operationId, pattern)) { return true; } // Check if pattern matches urlPattern (method:path) return (0, minimatch_1.minimatch)(urlPattern, pattern); }); } // If only blacklist is enabled, exclude operations that match a blacklist pattern if (config_1.config.filter.blacklist.length > 0) { return !config_1.config.filter.blacklist.some((pattern) => { // Check if pattern matches operationId (if it exists) if (operationId && (0, minimatch_1.minimatch)(operationId, pattern)) { return true; } // Check if pattern matches urlPattern (method:path) return (0, minimatch_1.minimatch)(urlPattern, pattern); }); } // If no filtering is enabled, include all operations return true; } function mapOpenApiToMcpTools(openapi) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; const mappedTools = []; const globalSecurity = openapi.security || null; // Global security requirements const securitySchemes = ((_a = openapi.components) === null || _a === void 0 ? void 0 : _a.securitySchemes) || undefined; // Security scheme definitions if (!openapi.paths) { console.error("OpenAPI spec has no paths defined."); return []; } // Determine the base server URL // Priority: Configured URL > First Server URL > Error/Default let baseServerUrl = config_1.config.targetApiBaseUrl; if (!baseServerUrl) { // Extract URL template from servers, defaulting to '/' if not found baseServerUrl = (_d = (_c = (_b = openapi.servers) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.url) !== null && _d !== void 0 ? _d : '/'; } // Ensure it's not undefined before using replace baseServerUrl = (baseServerUrl || '/').replace(/\/$/, ''); for (const path in openapi.paths) { const pathItem = openapi.paths[path]; // Assuming dereferenced for (const method in pathItem) { // Check if the method is a valid HTTP method if (!['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'].includes(method.toLowerCase())) { continue; } const operation = pathItem[method]; if (!operation || typeof operation !== 'object') continue; const operationId = operation.operationId; // --- Filtering --- if (!shouldIncludeOperation(operationId, path, method)) { // If operationId is available, log it for better debugging if (operationId) { console.error(`Skipping operation ${operationId} (${method.toUpperCase()} ${path}) due to filter rules.`); } else { console.error(`Skipping operation ${method.toUpperCase()} ${path} due to filter rules.`); } continue; } // Skip operations without operationId as we need it for the tool name if (!operationId) { console.error(`Skipping operation ${method.toUpperCase()} ${path} due to missing operationId.`); continue; } // --- Mapping --- let toolName = operationId; // Debug logging to identify what summary/description fields are available console.error(`Tool: ${toolName} - Operation description: ${operation.description || 'N/A'}`); console.error(`Tool: ${toolName} - Operation summary: ${operation.summary || 'N/A'}`); console.error(`Tool: ${toolName} - Path summary: ${pathItem.summary || 'N/A'}`); // Check for custom MCP extensions at the operation level first, then path level const operationMcpExtension = operation['x-mcp']; const pathMcpExtension = pathItem['x-mcp']; // Priority: Operation-level extension > Path-level extension > Default if (operationMcpExtension && typeof operationMcpExtension === 'object') { if (operationMcpExtension.name && typeof operationMcpExtension.name === 'string') { console.error(`Tool: ${toolName} - Using custom name from operation-level x-mcp extension: ${operationMcpExtension.name}`); toolName = operationMcpExtension.name; } } else if (pathMcpExtension && typeof pathMcpExtension === 'object') { if (pathMcpExtension.name && typeof pathMcpExtension.name === 'string') { console.error(`Tool: ${toolName} - Using custom name from path-level x-mcp extension: ${pathMcpExtension.name}`); toolName = pathMcpExtension.name; } } let toolDescription = operation.description || operation.summary || pathItem.summary || 'No description available.'; // Check for custom description in MCP extension - operation level first, then path level if (operationMcpExtension && typeof operationMcpExtension === 'object') { if (operationMcpExtension.description && typeof operationMcpExtension.description === 'string') { console.error(`Tool: ${toolName} - Using custom description from operation-level x-mcp extension: ${operationMcpExtension.description}`); toolDescription = operationMcpExtension.description; } } else if (pathMcpExtension && typeof pathMcpExtension === 'object') { if (pathMcpExtension.description && typeof pathMcpExtension.description === 'string') { console.error(`Tool: ${toolName} - Using custom description from path-level x-mcp extension: ${pathMcpExtension.description}`); toolDescription = pathMcpExtension.description; } } console.error(`Tool: ${toolName} - Final description used: ${toolDescription}`); // --- Input Schema --- const inputJsonSchema = { type: 'object', properties: {}, required: [], }; const allParameters = [ ...(pathItem.parameters || []), // Parameters defined at path level ...(operation.parameters || []), // Parameters defined at operation level ].filter(isParameterObject); // Ensure they are actual parameter objects // Group parameters by their location (path, query, header, cookie) const parametersByLocation = {}; for (const param of allParameters) { const location = param.in || 'query'; // Default to query if not specified if (!parametersByLocation[location]) { parametersByLocation[location] = []; } parametersByLocation[location].push(param); } // Create separate property for each parameter location group for better organization for (const [location, params] of Object.entries(parametersByLocation)) { // If there are parameters in this location, create a property for them if (params.length > 0 && inputJsonSchema.properties) { // Process each parameter within its location group for (const param of params) { if (param.name && param.schema && inputJsonSchema.properties) { // Debug log to identify potential type issues console.error(`Processing parameter ${param.name} with schema type: ${param.schema.type} format: ${param.schema.format}`); // Convert OpenAPI schema to JSON Schema const paramSchema = openApiSchemaToJsonSchema(param.schema); if (paramSchema) { // Debug log for converted schema console.error(`Converted schema for ${param.name}: type=${paramSchema.type}, format=${paramSchema.format}`); // Add parameter to properties inputJsonSchema.properties[param.name] = paramSchema; // Add description if available if (param.description) { inputJsonSchema.properties[param.name].description = param.description; } // Add parameter location metadata as an annotation inputJsonSchema.properties[param.name]['x-parameter-location'] = location; // Add deprecated flag if needed if (param.deprecated) { inputJsonSchema.properties[param.name].deprecated = true; } // Add example if available if (param.example !== undefined) { inputJsonSchema.properties[param.name].example = param.example; } // Handle required parameters if (param.required && inputJsonSchema.required) { inputJsonSchema.required.push(param.name); } // Add any extensions (x-... properties) Object.keys(param).forEach(key => { if (key.startsWith('x-')) { inputJsonSchema.properties[param.name][key] = param[key]; } }); } } } } } // Handle Request Body if (isRequestBodyObject(operation.requestBody)) { // Look for application/json content first, fall back to any available content type const jsonContent = (_f = (_e = operation.requestBody.content) === null || _e === void 0 ? void 0 : _e['application/json']) === null || _f === void 0 ? void 0 : _f.schema; const anyContent = (_g = Object.values(operation.requestBody.content || {})[0]) === null || _g === void 0 ? void 0 : _g.schema; const requestBodySchema = jsonContent || anyContent; if (isSchemaObject(requestBodySchema)) { // Convert request body schema and add it to input schema const convertedSchema = openApiSchemaToJsonSchema(requestBodySchema); if (convertedSchema && inputJsonSchema.properties) { // Add as 'requestBody' property inputJsonSchema.properties['requestBody'] = convertedSchema; // Mark as required if specified if (operation.requestBody.required && inputJsonSchema.required) { inputJsonSchema.required.push('requestBody'); } // Add description if available if (operation.requestBody.description && inputJsonSchema.properties['requestBody']) { inputJsonSchema.properties['requestBody'].description = operation.requestBody.description; } // Add content type annotation const contentTypes = Object.keys(operation.requestBody.content || {}); if (contentTypes.length > 0) { inputJsonSchema.properties['requestBody']['x-content-types'] = contentTypes; } } } } // Remove empty required array if nothing is required if (((_h = inputJsonSchema.required) === null || _h === void 0 ? void 0 : _h.length) === 0) { delete inputJsonSchema.required; } // --- Output Schema (Primary Success Response, e.g., 200) --- let outputJsonSchema = undefined; const successResponseCode = Object.keys(operation.responses || {}).find(code => code.startsWith('2')); // Find first 2xx response if (successResponseCode) { const response = (_j = operation.responses) === null || _j === void 0 ? void 0 : _j[successResponseCode]; if (isResponseObject(response)) { const jsonContent = (_l = (_k = response.content) === null || _k === void 0 ? void 0 : _k['application/json']) === null || _l === void 0 ? void 0 : _l.schema; if (isSchemaObject(jsonContent)) { const tempSchema = openApiSchemaToJsonSchema(jsonContent); outputJsonSchema = tempSchema; // Cast to JSONSchema7 since we know it's a schema if (outputJsonSchema && response.description) { outputJsonSchema.description = response.description; // Add response description } } } } // --- Assemble MCP Tool Definition --- const mcpDefinition = { name: toolName, description: toolDescription, inputSchema: inputJsonSchema, outputSchema: outputJsonSchema, // Properly include the outputSchema annotations: { // Add any relevant annotations, e.g., from spec extensions 'x-openapi-path': path, 'x-openapi-method': method.toUpperCase(), } }; // --- Assemble API Call Details --- const apiDetails = { method: method.toUpperCase(), pathTemplate: path, serverUrl: baseServerUrl, // Use the determined base URL parameters: allParameters, // Store original params for mapping back requestBody: isRequestBodyObject(operation.requestBody) ? operation.requestBody : undefined, // Store original body info securityRequirements: operation.security !== undefined ? operation.security : globalSecurity, // Operation security overrides global securitySchemes, // Include security schemes from OpenAPI components }; mappedTools.push({ mcpToolDefinition: mcpDefinition, apiCallDetails: apiDetails }); console.error(`Mapped tool: ${toolName} (${method.toUpperCase()} ${path})`); } } console.error(`Total tools mapped: ${mappedTools.length}`); return mappedTools; } // Helper functions for type checking function isReferenceObject(obj) { return obj && typeof obj === 'object' && '$ref' in obj; } function isSchemaObject(obj) { return obj && typeof obj === 'object' && !Array.isArray(obj) && !isReferenceObject(obj); } function isRequestBodyObject(obj) { return obj && typeof obj === 'object' && !isReferenceObject(obj); } function isResponseObject(obj) { return obj && typeof obj === 'object' && !isReferenceObject(obj); } function isParameterObject(obj) { return obj && typeof obj === 'object' && !isReferenceObject(obj); } // Function to safely convert undefined to a valid JSONSchema7Definition function safeJsonSchema(schema) { return schema || {}; }