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
JavaScript
;
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 || {};
}