Skip to main content
Glama

OpenAPI to MCP Server

openapiProcessor.ts8.76 kB
import SwaggerParser from '@apidevtools/swagger-parser'; // Using our custom implementation instead of the external library import { OverlayApplier } from './overlay-applier'; import fs from 'fs/promises'; import path from 'path'; import YAML from 'js-yaml'; // npm install js-yaml @types/js-yaml import { config } from './config'; import type { ProcessedOpenAPI } from './types'; import { isHttpUrl, fetchFromUrl } from './utils/httpClient'; /** * Validates an OpenAPI specification for required elements * @param api The OpenAPI specification object * @throws Error if validation fails */ function validateOpenApiSpec(api: any): void { // Check for basic required OpenAPI elements if (!api) { throw new Error('OpenAPI specification is null or undefined'); } if (!api.openapi) { throw new Error('Missing OpenAPI version identifier. This doesn\'t appear to be a valid OpenAPI spec.'); } if (!api.info) { throw new Error('Missing info section in OpenAPI spec'); } if (!api.paths || Object.keys(api.paths).length === 0) { throw new Error('No paths defined in OpenAPI spec. There are no operations to expose as tools.'); } // Validate that at least one path has valid operations let hasValidOperation = false; const httpMethods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'trace']; Object.keys(api.paths).forEach(path => { const pathItem = api.paths[path]; if (!pathItem) return; Object.keys(pathItem).forEach(key => { if (httpMethods.includes(key.toLowerCase())) { const operation = pathItem[key]; if (operation) { hasValidOperation = true; } } }); }); if (!hasValidOperation) { throw new Error('No valid operations found in any path. Cannot create tools.'); } console.error('OpenAPI specification validation passed'); } async function loadSpec(filePath: string): Promise<any> { console.error(`Loading OpenAPI spec from: ${filePath}`); try { let api; // Handle HTTP URLs if (isHttpUrl(filePath)) { console.error(`Detected HTTP URL for spec: ${filePath}`); // Use our custom HTTP client instead of letting SwaggerParser handle URLs const content = await fetchFromUrl(filePath); // Parse the content based on file extension if (filePath.endsWith('.yaml') || filePath.endsWith('.yml')) { api = YAML.load(content); } else { api = JSON.parse(content); } // Manually resolve any references api = await SwaggerParser.dereference(api); } else { // Use SwaggerParser for local files api = await SwaggerParser.dereference(filePath); } console.error(`Successfully loaded and dereferenced spec: ${api.info.title} v${api.info.version}`); // Additional validation beyond what SwaggerParser does validateOpenApiSpec(api); return api; } catch (err: any) { console.error(`Error loading/parsing OpenAPI spec: ${filePath}`, err.message); throw err; } } /** * Validates an OpenAPI overlay file structure according to OpenAPI Overlay Specification 1.0.0 * @param overlay The overlay object to validate * @returns True if valid, throws error otherwise */ function validateOverlay(overlay: any): boolean { if (!overlay) { throw new Error('Overlay is null or undefined'); } // Check if it's a formal OpenAPI Overlay (should have overlay property) if (overlay.overlay) { // Validate formal overlay structure per spec if (typeof overlay.overlay !== 'string') { throw new Error('Overlay version must be a string (e.g., "1.0.0")'); } // Check for required info section if (!overlay.info || typeof overlay.info !== 'object') { throw new Error('Formal overlay is missing info object'); } if (!overlay.info.title || !overlay.info.version) { throw new Error('Overlay info must contain title and version'); } // Check for actions array if (!overlay.actions || !Array.isArray(overlay.actions)) { throw new Error('Formal overlay must have an actions array'); } // Validate each action for (const action of overlay.actions) { if (!action.target) { throw new Error('Each action must have a target JSONPath'); } // An action must have either update or remove property if (action.remove === undefined && action.update === undefined) { throw new Error('Each action must have either update or remove property'); } if (action.remove !== undefined && typeof action.remove !== 'boolean') { throw new Error('Action remove property must be a boolean'); } } } else { // For legacy/simple overlays without the formal structure, // verify that it has some properties that could modify an OpenAPI spec console.error('Warning: Using legacy overlay format, not compliant with OpenAPI Overlay Specification 1.0.0'); const hasValidProperties = overlay.info || overlay.paths || overlay.components || overlay.tags || overlay.servers; if (!hasValidProperties) { throw new Error('Overlay doesn\'t contain any valid OpenAPI modification properties'); } } return true; } async function loadOverlay(filePath: string): Promise<any> { console.error(`Loading overlay file: ${filePath}`); try { let content: string; if (isHttpUrl(filePath)) { // Fetch overlay from HTTP URL content = await fetchFromUrl(filePath); } else { // Load overlay from local file system content = await fs.readFile(filePath, 'utf-8'); } const ext = path.extname(filePath).toLowerCase(); let overlay; if (ext === '.yaml' || ext === '.yml') { overlay = YAML.load(content); } else if (ext === '.json') { overlay = JSON.parse(content); } else { throw new Error(`Unsupported overlay file extension: ${ext}`); } // Validate the overlay structure validateOverlay(overlay); return overlay; } catch (err: any) { console.error(`Error loading overlay file ${filePath}:`, err.message); throw err; } } export async function getProcessedOpenApi(): Promise<any> { let baseApi = await loadSpec(config.specPath); if (config.overlayPaths.length > 0) { console.error(`Applying overlays...`); // Apply each overlay sequentially for (const overlayPath of config.overlayPaths) { try { // Load the overlay const overlayJson = await loadOverlay(overlayPath); // Apply the overlay using the OverlayApplier instance const overlayApplier = new OverlayApplier(); baseApi = overlayApplier.apply(baseApi, overlayJson); console.error(`Applied overlay: ${overlayPath}`); } catch (err) { // Decide whether to continue or fail on overlay error console.error(`Failed to apply overlay ${overlayPath}. Continuing without it.`, err); // throw err; // Or re-throw to stop the process } } console.error('Overlays applied successfully.'); } // Ensure servers are present if needed and targetApiBaseUrl isn't set if (!config.targetApiBaseUrl && (!baseApi.servers || baseApi.servers.length === 0)) { console.error("Warning: No targetApiBaseUrl configured and OpenAPI spec has no servers defined."); // Potentially throw an error if a base URL is absolutely required throw new Error("Cannot determine target API URL. Either configure targetApiBaseUrl or ensure OpenAPI spec includes servers."); } else if (!config.targetApiBaseUrl) { console.error(`Using server URL from OpenAPI spec: ${baseApi.servers[0].url}`); } return baseApi; }

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