UNPKG

openapi-mcp-generator

Version:

Generates MCP server code from OpenAPI specifications

220 lines 8.84 kB
/** * Functions for extracting tools from an OpenAPI specification */ import { OpenAPIV3 } from 'openapi-types'; import { generateOperationId } from '../utils/code-gen.js'; import { shouldIncludeOperationForMcp } from '../utils/helpers.js'; /** * Extracts tool definitions from an OpenAPI document * * @param api OpenAPI document * @returns Array of MCP tool definitions */ export function extractToolsFromApi(api, defaultInclude = true) { const tools = []; const usedNames = new Set(); const globalSecurity = api.security || []; if (!api.paths) return tools; for (const [path, pathItem] of Object.entries(api.paths)) { if (!pathItem) continue; for (const method of Object.values(OpenAPIV3.HttpMethods)) { const operation = pathItem[method]; if (!operation) continue; // Apply x-mcp filtering, precedence: operation > path > root try { if (!shouldIncludeOperationForMcp(api, pathItem, operation, defaultInclude)) { continue; } } catch (error) { const loc = operation.operationId || `${method} ${path}`; const extVal = operation['x-mcp'] ?? pathItem['x-mcp'] ?? api['x-mcp']; let extPreview; try { extPreview = JSON.stringify(extVal); } catch { extPreview = String(extVal); } console.warn(`Error evaluating x-mcp extension for operation ${loc} (x-mcp=${extPreview}):`, error); if (!defaultInclude) { continue; } } // Generate a unique name for the tool let baseName = operation.operationId || generateOperationId(method, path); if (!baseName) continue; // Sanitize the name to be MCP-compatible (only a-z, 0-9, _, -) baseName = baseName.replace(/\./g, '_').replace(/[^a-z0-9_-]/gi, '_'); let finalToolName = baseName; let counter = 1; while (usedNames.has(finalToolName)) { finalToolName = `${baseName}_${counter++}`; } usedNames.add(finalToolName); // Get or create a description const description = operation.description || operation.summary || `Executes ${method.toUpperCase()} ${path}`; // Generate input schema and extract parameters const { inputSchema, parameters, requestBodyContentType } = generateInputSchemaAndDetails(operation); // Extract parameter details for execution const executionParameters = parameters.map((p) => ({ name: p.name, in: p.in })); // Determine security requirements const securityRequirements = operation.security === null ? globalSecurity : operation.security || globalSecurity; // Create the tool definition tools.push({ name: finalToolName, description, inputSchema, method, pathTemplate: path, parameters, executionParameters, requestBodyContentType, securityRequirements, operationId: baseName, }); } } return tools; } /** * Generates input schema and extracts parameter details from an operation * * @param operation OpenAPI operation object * @returns Input schema, parameters, and request body content type */ export function generateInputSchemaAndDetails(operation) { const properties = {}; const required = []; // Process parameters const allParameters = Array.isArray(operation.parameters) ? operation.parameters.map((p) => p) : []; allParameters.forEach((param) => { if (!param.name || !param.schema) return; const paramSchema = mapOpenApiSchemaToJsonSchema(param.schema); if (typeof paramSchema === 'object') { paramSchema.description = param.description || paramSchema.description; } properties[param.name] = paramSchema; if (param.required) required.push(param.name); }); // Process request body (if present) let requestBodyContentType = undefined; if (operation.requestBody) { const opRequestBody = operation.requestBody; const jsonContent = opRequestBody.content?.['application/json']; const firstContent = opRequestBody.content ? Object.entries(opRequestBody.content)[0] : undefined; if (jsonContent?.schema) { requestBodyContentType = 'application/json'; const bodySchema = mapOpenApiSchemaToJsonSchema(jsonContent.schema); if (typeof bodySchema === 'object') { bodySchema.description = opRequestBody.description || bodySchema.description || 'The JSON request body.'; } properties['requestBody'] = bodySchema; if (opRequestBody.required) required.push('requestBody'); } else if (firstContent) { const [contentType] = firstContent; requestBodyContentType = contentType; properties['requestBody'] = { type: 'string', description: opRequestBody.description || `Request body (content type: ${contentType})`, }; if (opRequestBody.required) required.push('requestBody'); } } // Combine everything into a JSON Schema const inputSchema = { type: 'object', properties, ...(required.length > 0 && { required }), }; return { inputSchema, parameters: allParameters, requestBodyContentType }; } /** * Maps an OpenAPI schema to a JSON Schema with cycle protection. * * @param schema OpenAPI schema object or reference * @param seen WeakSet tracking already visited schema objects * @returns JSON Schema representation */ export function mapOpenApiSchemaToJsonSchema(schema, seen = new WeakSet()) { // Handle reference objects if ('$ref' in schema) { console.warn(`Unresolved $ref '${schema.$ref}'.`); return { type: 'object' }; } // Handle boolean schemas if (typeof schema === 'boolean') return schema; // Detect cycles if (seen.has(schema)) { console.warn(`Cycle detected in schema${schema.title ? ` "${schema.title}"` : ''}, returning generic object to break recursion.`); return { type: 'object' }; } seen.add(schema); try { // Create a copy of the schema to modify const jsonSchema = { ...schema }; // Convert integer type to number (JSON Schema compatible) if (schema.type === 'integer') jsonSchema.type = 'number'; // Remove OpenAPI-specific properties that aren't in JSON Schema delete jsonSchema.nullable; delete jsonSchema.example; delete jsonSchema.xml; delete jsonSchema.externalDocs; delete jsonSchema.deprecated; delete jsonSchema.readOnly; delete jsonSchema.writeOnly; // Handle nullable properties by adding null to the type if (schema.nullable) { if (Array.isArray(jsonSchema.type)) { if (!jsonSchema.type.includes('null')) jsonSchema.type.push('null'); } else if (typeof jsonSchema.type === 'string') { jsonSchema.type = [jsonSchema.type, 'null']; } else if (!jsonSchema.type) { jsonSchema.type = 'null'; } } // Recursively process object properties if (jsonSchema.type === 'object' && jsonSchema.properties) { const mappedProps = {}; for (const [key, propSchema] of Object.entries(jsonSchema.properties)) { if (typeof propSchema === 'object' && propSchema !== null) { mappedProps[key] = mapOpenApiSchemaToJsonSchema(propSchema, seen); } else if (typeof propSchema === 'boolean') { mappedProps[key] = propSchema; } } jsonSchema.properties = mappedProps; } // Recursively process array items if (jsonSchema.type === 'array' && typeof jsonSchema.items === 'object' && jsonSchema.items !== null) { jsonSchema.items = mapOpenApiSchemaToJsonSchema(jsonSchema.items, seen); } return jsonSchema; } finally { seen.delete(schema); } } //# sourceMappingURL=extract-tools.js.map