Skip to main content
Glama
openapi.ts6.87 kB
import SwaggerParser from '@apidevtools/swagger-parser'; import { readFile } from 'node:fs/promises'; import yaml from 'yaml'; import type { OpenAPIDocument, PathItemObject, OperationObject, ParameterObject } from '../types/openapi.js'; import type { NormalizedOperation, NormalizedParameter, NormalizedRequestBody, SecurityRequirement, HttpMethod } from '../types/index.js'; import type { JSONSchema } from '../types/mcp-tool.js'; import { log } from '../utils/logger.js'; /** * HTTP methods to extract from path items */ const HTTP_METHODS: HttpMethod[] = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']; /** * Parse OpenAPI spec from URL */ export async function parseFromUrl(url: string): Promise<OpenAPIDocument> { log.info('Fetching OpenAPI spec from URL', { url }); const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch OpenAPI spec: ${response.status} ${response.statusText}`); } const content = await response.text(); return parseContent(content); } /** * Parse OpenAPI spec from file */ export async function parseFromFile(filePath: string): Promise<OpenAPIDocument> { log.info('Loading OpenAPI spec from file', { file: filePath }); const content = await readFile(filePath, 'utf-8'); return parseContent(content); } /** * Parse OpenAPI spec from inline content */ export async function parseContent(content: string): Promise<OpenAPIDocument> { // Try to parse as YAML first (also handles JSON) let parsed: unknown; try { parsed = yaml.parse(content); } catch { throw new Error('Failed to parse spec content as YAML or JSON'); } // Validate and dereference the spec try { const validated = await SwaggerParser.validate(parsed as OpenAPIDocument); const dereferenced = await SwaggerParser.dereference(validated); return dereferenced as OpenAPIDocument; } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; throw new Error(`Invalid OpenAPI specification: ${message}`); } } /** * Generate operationId from method and path if not provided */ function generateOperationId(method: string, path: string): string { // /pet/{petId}/uploadImage -> pet_petId_uploadImage // GET /pets -> get_pets const cleanPath = path .replace(/\{([^}]+)\}/g, '$1') // Remove braces from params .replace(/[^a-zA-Z0-9]/g, '_') // Replace non-alphanumeric .replace(/_+/g, '_') // Collapse multiple underscores .replace(/^_|_$/g, ''); // Trim leading/trailing return `${method.toLowerCase()}_${cleanPath}`; } /** * Convert OpenAPI parameter to normalized format */ function normalizeParameter(param: ParameterObject): NormalizedParameter { // Handle the schema - it might be a reference or inline const schema = 'schema' in param ? (param.schema as JSONSchema) : { type: 'string' as const }; return { name: param.name, in: param.in as 'path' | 'query' | 'header' | 'cookie', required: param.required ?? (param.in === 'path'), // Path params are always required description: param.description, schema, example: 'example' in param ? param.example : undefined, }; } /** * Extract request body from operation */ function extractRequestBody(operation: OperationObject): NormalizedRequestBody | undefined { if (!operation.requestBody) { return undefined; } const body = operation.requestBody as { required?: boolean; description?: string; content?: Record<string, { schema?: JSONSchema }>; }; // Prefer application/json, fall back to first available const contentType = body.content?.['application/json'] ? 'application/json' : Object.keys(body.content || {})[0]; if (!contentType || !body.content?.[contentType]?.schema) { return undefined; } return { required: body.required ?? false, description: body.description, contentType, schema: body.content[contentType].schema, }; } /** * Extract security requirements */ function extractSecurity( operationSecurity: OperationObject['security'], globalSecurity: OpenAPIDocument['security'] ): SecurityRequirement[] { // Use operation-level security if defined, otherwise global const securityReqs = operationSecurity ?? globalSecurity ?? []; return securityReqs.map((req) => { const [name, scopes] = Object.entries(req)[0] ?? ['', []]; return { name, scopes }; }); } /** * Extract all operations from OpenAPI document */ export function extractOperations(doc: OpenAPIDocument): NormalizedOperation[] { const operations: NormalizedOperation[] = []; if (!doc.paths) { log.warn('OpenAPI document has no paths defined'); return operations; } for (const [path, pathItem] of Object.entries(doc.paths)) { if (!pathItem) continue; const pathItemObj = pathItem as PathItemObject; // Get path-level parameters const pathParameters = (pathItemObj.parameters ?? []) as ParameterObject[]; for (const method of HTTP_METHODS) { const methodKey = method.toLowerCase() as keyof PathItemObject; const operation = pathItemObj[methodKey] as OperationObject | undefined; if (!operation) continue; // Merge path and operation parameters const operationParameters = (operation.parameters ?? []) as ParameterObject[]; const allParameters = [...pathParameters, ...operationParameters]; // Remove duplicates (operation params override path params) const paramMap = new Map<string, ParameterObject>(); for (const param of allParameters) { const key = `${param.in}:${param.name}`; paramMap.set(key, param); } const normalizedOp: NormalizedOperation = { operationId: operation.operationId ?? generateOperationId(method, path), method, path, summary: operation.summary, description: operation.description, parameters: Array.from(paramMap.values()).map(normalizeParameter), requestBody: extractRequestBody(operation), tags: operation.tags ?? [], deprecated: operation.deprecated ?? false, security: extractSecurity(operation.security, doc.security), }; operations.push(normalizedOp); } } log.info('Extracted operations from OpenAPI spec', { count: operations.length }); return operations; } /** * Parse and extract operations from OpenAPI spec */ export async function parseOpenAPI( source: { url?: string; file?: string; inline?: string } ): Promise<NormalizedOperation[]> { let doc: OpenAPIDocument; if (source.url) { doc = await parseFromUrl(source.url); } else if (source.file) { doc = await parseFromFile(source.file); } else if (source.inline) { doc = await parseContent(source.inline); } else { throw new Error('No spec source provided'); } return extractOperations(doc); }

Latest Blog Posts

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/procoders/openapi-mcp-ts'

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