Skip to main content
Glama
metadata-extractor.ts9.63 kB
/** * Metadata extractor for OpenAPI specifications */ import type { OpenAPISpec, TagObject, ExternalDocumentationObject } from '../types/index'; export interface ApiMetadata { info: { title: string; version: string; description?: string; contact?: { name?: string; url?: string; email?: string; }; license?: { name: string; url?: string; }; termsOfService?: string; }; servers: { url: string; description?: string; variables?: Record<string, { enum?: string[]; default: string; description?: string; }>; }[]; tags: TagObject[]; externalDocs?: ExternalDocumentationObject; statistics: { pathCount: number; operationCount: number; schemaCount: number; securitySchemeCount: number; parameterCount: number; responseCount: number; operationsByMethod: Record<string, number>; operationsByTag: Record<string, number>; deprecatedOperations: number; }; } export class MetadataExtractor { /** * Extract comprehensive metadata from OpenAPI specification */ static extractMetadata(spec: OpenAPISpec): ApiMetadata { return { info: this.extractInfo(spec), servers: this.extractServers(spec), tags: this.extractTags(spec), externalDocs: spec.externalDocs, statistics: this.calculateStatistics(spec) }; } /** * Extract API information */ private static extractInfo(spec: OpenAPISpec): ApiMetadata['info'] { return { title: spec.info.title, version: spec.info.version, description: spec.info.description, contact: spec.info.contact, license: spec.info.license, termsOfService: spec.info.termsOfService }; } /** * Extract server information */ private static extractServers(spec: OpenAPISpec): ApiMetadata['servers'] { if (!spec.servers || spec.servers.length === 0) { return [{ url: 'http://localhost', description: 'Default server (no servers specified)' }]; } return spec.servers.map(server => ({ url: server.url, description: server.description, variables: server.variables })); } /** * Extract tag information */ private static extractTags(spec: OpenAPISpec): TagObject[] { const definedTags = spec.tags || []; const usedTags = new Set<string>(); // Collect tags used in operations if (spec.paths) { for (const pathItem of Object.values(spec.paths)) { const methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'] as const; for (const method of methods) { const operation = pathItem[method]; if (operation?.tags) { operation.tags.forEach(tag => usedTags.add(tag)); } } } } // Combine defined tags with used tags const allTags = new Map<string, TagObject>(); // Add defined tags definedTags.forEach(tag => { allTags.set(tag.name, tag); }); // Add used but not defined tags usedTags.forEach(tagName => { if (!allTags.has(tagName)) { allTags.set(tagName, { name: tagName, description: `Tag used in operations (not defined in global tags)` }); } }); return Array.from(allTags.values()).sort((a, b) => a.name.localeCompare(b.name)); } /** * Calculate comprehensive statistics */ private static calculateStatistics(spec: OpenAPISpec): ApiMetadata['statistics'] { const stats: ApiMetadata['statistics'] = { pathCount: 0, operationCount: 0, schemaCount: 0, securitySchemeCount: 0, parameterCount: 0, responseCount: 0, operationsByMethod: {}, operationsByTag: {}, deprecatedOperations: 0 }; // Count paths and operations if (spec.paths) { stats.pathCount = Object.keys(spec.paths).length; for (const pathItem of Object.values(spec.paths)) { const methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'] as const; for (const method of methods) { const operation = pathItem[method]; if (!operation) continue; stats.operationCount++; // Count by method const methodUpper = method.toUpperCase(); stats.operationsByMethod[methodUpper] = (stats.operationsByMethod[methodUpper] || 0) + 1; // Count by tag if (operation.tags) { operation.tags.forEach(tag => { stats.operationsByTag[tag] = (stats.operationsByTag[tag] || 0) + 1; }); } else { stats.operationsByTag['untagged'] = (stats.operationsByTag['untagged'] || 0) + 1; } // Count deprecated operations if (operation.deprecated) { stats.deprecatedOperations++; } // Count parameters if (operation.parameters) { stats.parameterCount += operation.parameters.length; } // Count responses if (operation.responses) { stats.responseCount += Object.keys(operation.responses).length; } } // Count path-level parameters if (pathItem.parameters) { stats.parameterCount += pathItem.parameters.length; } } } // Count components if (spec.components) { stats.schemaCount = Object.keys(spec.components.schemas || {}).length; stats.securitySchemeCount = Object.keys(spec.components.securitySchemes || {}).length; stats.parameterCount += Object.keys(spec.components.parameters || {}).length; stats.responseCount += Object.keys(spec.components.responses || {}).length; } return stats; } /** * Generate API summary */ static generateSummary(metadata: ApiMetadata): string { const { info, statistics } = metadata; const lines = [ `# ${info.title} (v${info.version})`, '' ]; if (info.description) { lines.push(info.description, ''); } lines.push( '## API Statistics', `- **Paths**: ${statistics.pathCount}`, `- **Operations**: ${statistics.operationCount}`, `- **Schemas**: ${statistics.schemaCount}`, `- **Security Schemes**: ${statistics.securitySchemeCount}`, '' ); if (statistics.deprecatedOperations > 0) { lines.push(`⚠️ **Deprecated Operations**: ${statistics.deprecatedOperations}`, ''); } // Operations by method if (Object.keys(statistics.operationsByMethod).length > 0) { lines.push('## Operations by Method'); Object.entries(statistics.operationsByMethod) .sort(([, a], [, b]) => b - a) .forEach(([method, count]) => { lines.push(`- **${method}**: ${count}`); }); lines.push(''); } // Operations by tag if (Object.keys(statistics.operationsByTag).length > 0) { lines.push('## Operations by Tag'); Object.entries(statistics.operationsByTag) .sort(([, a], [, b]) => b - a) .slice(0, 10) // Show top 10 tags .forEach(([tag, count]) => { lines.push(`- **${tag}**: ${count}`); }); lines.push(''); } // Servers if (metadata.servers.length > 0) { lines.push('## Servers'); metadata.servers.forEach(server => { const desc = server.description ? ` - ${server.description}` : ''; lines.push(`- ${server.url}${desc}`); }); lines.push(''); } return lines.join('\n'); } /** * Validate metadata completeness */ static validateMetadata(metadata: ApiMetadata): { score: number; suggestions: string[]; } { let score = 0; const maxScore = 100; const suggestions: string[] = []; // Check basic info completeness (40 points) if (metadata.info.description) score += 10; else suggestions.push('Add API description'); if (metadata.info.contact) score += 10; else suggestions.push('Add contact information'); if (metadata.info.license) score += 10; else suggestions.push('Add license information'); if (metadata.info.termsOfService) score += 10; else suggestions.push('Add terms of service'); // Check documentation (20 points) if (metadata.externalDocs) score += 10; else suggestions.push('Add external documentation links'); const taggedOperations = Object.values(metadata.statistics.operationsByTag) .reduce((sum, count) => sum + count, 0) - (metadata.statistics.operationsByTag['untagged'] || 0); if (taggedOperations === metadata.statistics.operationCount) score += 10; else suggestions.push('Tag all operations for better organization'); // Check operation quality (20 points) const deprecatedRatio = metadata.statistics.deprecatedOperations / metadata.statistics.operationCount; if (deprecatedRatio < 0.1) score += 10; else suggestions.push('Reduce number of deprecated operations'); if (metadata.statistics.operationCount > 0) score += 10; // Check server configuration (10 points) const hasProductionServer = metadata.servers.some(server => !server.url.includes('localhost') && !server.url.includes('127.0.0.1') ); if (hasProductionServer) score += 10; else suggestions.push('Add production server URL'); // Check security (10 points) if (metadata.statistics.securitySchemeCount > 0) score += 10; else suggestions.push('Define security schemes'); return { score: Math.round((score / maxScore) * 100), suggestions }; } }

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/zaizaizhao/mcp-swagger-server'

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