Skip to main content
Glama

OpenAPI to MCP Server

mcpMapper.ts22.8 kB
import type { JSONSchema7, JSONSchema7Definition, JSONSchema7TypeName } from 'json-schema'; import type { OpenAPIV3 } from 'openapi-types'; import type { ProcessedOpenAPI, MappedTool, ApiCallDetails, McpToolDefinition } from './types'; import { config } from './config'; import { minimatch } from 'minimatch'; // Enhanced mapping from OpenAPI type/format to JSON Schema type // Now preserves format information function mapOpenApiTypeToJsonSchemaType(openApiSchema?: OpenAPIV3.SchemaObject): { type: JSONSchema7TypeName | undefined, format?: string, nullable?: boolean } { 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: JSONSchema7TypeName | undefined; 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?: OpenAPIV3.SchemaObject): JSONSchema7 | undefined { if (!openApiSchema) return undefined; const jsonSchema: JSONSchema7 = {}; 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'] as any; // 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 as any).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 as any)[key] = openApiSchema[key as keyof OpenAPIV3.SchemaObject]; } }); 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: string | undefined, path: string, method: string): boolean { // 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.filter.whitelist) { return config.filter.whitelist.some((pattern: string) => { // Check if pattern matches operationId (if it exists) if (operationId && minimatch(operationId, pattern)) { return true; } // Check if pattern matches urlPattern (method:path) return minimatch(urlPattern, pattern); }); } // If only blacklist is enabled, exclude operations that match a blacklist pattern if (config.filter.blacklist.length > 0) { return !config.filter.blacklist.some((pattern: string) => { // Check if pattern matches operationId (if it exists) if (operationId && minimatch(operationId, pattern)) { return true; } // Check if pattern matches urlPattern (method:path) return minimatch(urlPattern, pattern); }); } // If no filtering is enabled, include all operations return true; } export function mapOpenApiToMcpTools(openapi: ProcessedOpenAPI): MappedTool[] { const mappedTools: MappedTool[] = []; const globalSecurity = openapi.security || null; // Global security requirements const securitySchemes = openapi.components?.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.targetApiBaseUrl; if (!baseServerUrl) { // Extract URL template from servers, defaulting to '/' if not found baseServerUrl = openapi.servers?.[0]?.url ?? '/'; } // Ensure it's not undefined before using replace baseServerUrl = (baseServerUrl || '/').replace(/\/$/, ''); for (const path in openapi.paths) { const pathItem = openapi.paths[path] as OpenAPIV3.PathItemObject; // 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 as keyof OpenAPIV3.PathItemObject] as OpenAPIV3.OperationObject; 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 as any)['x-mcp']; const pathMcpExtension = (pathItem as any)['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: JSONSchema7 = { type: 'object', properties: {}, required: [], }; const allParameters: OpenAPIV3.ParameterObject[] = [ ...(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: Record<string, OpenAPIV3.ParameterObject[]> = {}; 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 as any).type} format: ${(param.schema as any).format}`); // Convert OpenAPI schema to JSON Schema const paramSchema = openApiSchemaToJsonSchema(param.schema as OpenAPIV3.SchemaObject); if (paramSchema) { // Debug log for converted schema console.error(`Converted schema for ${param.name}: type=${paramSchema.type}, format=${(paramSchema as any).format}`); // Add parameter to properties inputJsonSchema.properties[param.name] = paramSchema; // Add description if available if (param.description) { (inputJsonSchema.properties[param.name] as JSONSchema7).description = param.description; } // Add parameter location metadata as an annotation (inputJsonSchema.properties[param.name] as any)['x-parameter-location'] = location; // Add deprecated flag if needed if (param.deprecated) { (inputJsonSchema.properties[param.name] as any).deprecated = true; } // Add example if available if (param.example !== undefined) { (inputJsonSchema.properties[param.name] as any).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] as any)[key] = param[key as keyof OpenAPIV3.ParameterObject]; } }); } } } } } // Handle Request Body if (isRequestBodyObject(operation.requestBody)) { // Look for application/json content first, fall back to any available content type const jsonContent = operation.requestBody.content?.['application/json']?.schema; const anyContent = Object.values(operation.requestBody.content || {})[0]?.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'] as any)['x-content-types'] = contentTypes; } } } } // Remove empty required array if nothing is required if (inputJsonSchema.required?.length === 0) { delete inputJsonSchema.required; } // --- Output Schema (Primary Success Response, e.g., 200) --- let outputJsonSchema: JSONSchema7 | undefined = undefined; const successResponseCode = Object.keys(operation.responses || {}).find(code => code.startsWith('2')); // Find first 2xx response if (successResponseCode) { const response = operation.responses?.[successResponseCode]; if (isResponseObject(response)) { const jsonContent = response.content?.['application/json']?.schema; if (isSchemaObject(jsonContent)) { const tempSchema = openApiSchemaToJsonSchema(jsonContent); outputJsonSchema = tempSchema as JSONSchema7; // 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: McpToolDefinition = { 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: ApiCallDetails = { 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: any): obj is OpenAPIV3.ReferenceObject { return obj && typeof obj === 'object' && '$ref' in obj; } function isSchemaObject(obj: any): obj is OpenAPIV3.SchemaObject { return obj && typeof obj === 'object' && !Array.isArray(obj) && !isReferenceObject(obj); } function isRequestBodyObject(obj: any): obj is OpenAPIV3.RequestBodyObject { return obj && typeof obj === 'object' && !isReferenceObject(obj); } function isResponseObject(obj: any): obj is OpenAPIV3.ResponseObject { return obj && typeof obj === 'object' && !isReferenceObject(obj); } function isParameterObject(obj: any): obj is OpenAPIV3.ParameterObject { return obj && typeof obj === 'object' && !isReferenceObject(obj); } // Function to safely convert undefined to a valid JSONSchema7Definition function safeJsonSchema(schema: JSONSchema7 | undefined): JSONSchema7Definition { return schema || {}; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TykTechnologies/api-to-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server