Skip to main content
Glama

Swagger MCP Server

by tuskermanshu
optimized-swagger-parser.ts18 kB
/** * 优化的Swagger/OpenAPI文档解析器 * 支持缓存、懒加载和大型文档解析 */ import { promises as fs } from 'fs'; import axios from 'axios'; import { OpenAPIV3 } from 'openapi-types'; import SwaggerParser from '@apidevtools/swagger-parser'; import * as path from 'path'; import * as crypto from 'crypto'; // 定义解析器选项 export interface OptimizedSwaggerApiParserOptions { /** * Swagger/OpenAPI文档URL或本地文件路径 */ url: string; /** * 请求头信息 */ headers?: Record<string, string>; /** * 是否跳过验证 */ skipValidation?: boolean; /** * 是否使用缓存 */ useCache?: boolean; /** * 缓存有效期(毫秒) */ cacheTTL?: number; /** * 是否启用懒加载 */ lazyLoading?: boolean; /** * 进度回调函数 */ progressCallback?: (progress: number, message: string) => void; } /** * API操作接口 */ export interface ApiOperation { operationId: string; method: string; path: string; summary?: string; description?: string; tags?: string[]; parameters?: OpenAPIV3.ParameterObject[]; requestBody?: OpenAPIV3.RequestBodyObject; responses?: Record<string, OpenAPIV3.ResponseObject>; } /** * 缓存项目结构 */ interface CacheItem { timestamp: number; data: any; } /** * 优化的Swagger API解析器类 */ export class OptimizedSwaggerApiParser { private url: string; private headers: Record<string, string>; private options: OptimizedSwaggerApiParserOptions; private api?: OpenAPIV3.Document; private rawApiData?: any; private static cache: Map<string, CacheItem> = new Map(); private static cacheDir = path.join(process.cwd(), '.api-cache'); /** * 构造函数 */ constructor(options: OptimizedSwaggerApiParserOptions) { this.url = options.url; this.headers = options.headers || {}; this.options = { skipValidation: false, useCache: true, cacheTTL: 3600000, // 默认缓存1小时 lazyLoading: true, ...options }; // 确保缓存目录存在 if (this.options.useCache) { this.ensureCacheDir(); } } /** * 确保缓存目录存在 */ private async ensureCacheDir(): Promise<void> { try { await fs.mkdir(OptimizedSwaggerApiParser.cacheDir, { recursive: true }); } catch (error) { console.error('创建缓存目录失败:', error); } } /** * 生成缓存键 */ private getCacheKey(): string { const data = this.url + JSON.stringify(this.headers); return crypto.createHash('md5').update(data).digest('hex'); } /** * 从缓存中获取数据 */ private async getFromCache(): Promise<any | null> { if (!this.options.useCache) return null; const cacheKey = this.getCacheKey(); // 检查内存缓存 if (OptimizedSwaggerApiParser.cache.has(cacheKey)) { const cachedItem = OptimizedSwaggerApiParser.cache.get(cacheKey)!; if (Date.now() - cachedItem.timestamp < this.options.cacheTTL!) { this.reportProgress(0.1, '从内存缓存加载数据'); return cachedItem.data; } } // 检查文件缓存 const cacheFilePath = path.join(OptimizedSwaggerApiParser.cacheDir, `${cacheKey}.json`); try { const stat = await fs.stat(cacheFilePath); if (Date.now() - stat.mtimeMs < this.options.cacheTTL!) { this.reportProgress(0.1, '从文件缓存加载数据'); const content = await fs.readFile(cacheFilePath, 'utf8'); const data = JSON.parse(content); // 更新内存缓存 OptimizedSwaggerApiParser.cache.set(cacheKey, { timestamp: Date.now(), data }); return data; } } catch (error) { // 缓存文件不存在或其他错误,忽略 } return null; } /** * 将数据保存到缓存 */ private async saveToCache(data: any): Promise<void> { if (!this.options.useCache) return; const cacheKey = this.getCacheKey(); // 更新内存缓存 OptimizedSwaggerApiParser.cache.set(cacheKey, { timestamp: Date.now(), data }); // 更新文件缓存 const cacheFilePath = path.join(OptimizedSwaggerApiParser.cacheDir, `${cacheKey}.json`); try { await fs.writeFile(cacheFilePath, JSON.stringify(data), 'utf8'); } catch (error) { console.error('保存缓存文件失败:', error); } } /** * 清除缓存 */ public static clearCache(url?: string): void { if (url) { // 生成特定URL的缓存键 const cacheKey = crypto.createHash('md5').update(url).digest('hex'); // 清除内存缓存 OptimizedSwaggerApiParser.cache.delete(cacheKey); // 清除文件缓存 const cacheFilePath = path.join(OptimizedSwaggerApiParser.cacheDir, `${cacheKey}.json`); fs.unlink(cacheFilePath).catch(() => {}); } else { // 清除所有内存缓存 OptimizedSwaggerApiParser.cache.clear(); // 清除所有文件缓存 fs.readdir(OptimizedSwaggerApiParser.cacheDir) .then(files => { for (const file of files) { fs.unlink(path.join(OptimizedSwaggerApiParser.cacheDir, file)).catch(() => {}); } }) .catch(() => {}); } } /** * 报告进度 */ private reportProgress(progress: number, message: string): void { if (this.options.progressCallback) { this.options.progressCallback(progress, message); } } /** * 修复Swagger UI URL以获取实际API文档 */ private async resolveSwaggerDocUrl(url: string): Promise<string> { // 如果已经是API文档URL,则不需要处理 if (url.includes('/api-docs') || url.includes('/swagger.json')) { return url; } // 处理常见的Swagger UI URL格式 if (url.includes('doc.html') || url.includes('swagger-ui.html') || url.includes('swagger-ui/') || url.includes('swagger/index.html')) { this.reportProgress(0.1, '检测到Swagger UI URL,尝试获取API文档地址'); // 提取基础URL let baseUrl; if (url.includes('swagger-ui/')) { baseUrl = url.substring(0, url.indexOf('swagger-ui/')); } else if (url.includes('swagger/')) { baseUrl = url.substring(0, url.indexOf('swagger/')); } else { baseUrl = url.substring(0, url.lastIndexOf('/')); } // 移除URL中的hash部分 if (baseUrl.includes('#')) { baseUrl = baseUrl.split('#')[0]; } // 可能的API文档端点 const possibleEndpoints = [ `${baseUrl}/v3/api-docs`, `${baseUrl}/v2/api-docs`, `${baseUrl}/api-docs`, `${baseUrl}/swagger/v3/api-docs`, `${baseUrl}/swagger/v2/api-docs`, `${baseUrl}/swagger/api-docs`, url.replace('swagger-ui/index.html', 'v3/api-docs'), url.replace('swagger-ui.html', 'v3/api-docs'), url.replace('doc.html', 'v3/api-docs') ]; // 尝试获取有效的API文档 for (const endpoint of possibleEndpoints) { try { this.reportProgress(0.15, `尝试API端点: ${endpoint}`); const response = await axios.get(endpoint, { headers: this.headers, timeout: 5000 // 5秒超时 }); // 验证是否是有效的Swagger/OpenAPI文档 if (response.data && ( response.data.swagger || response.data.openapi || (response.data.paths && Object.keys(response.data.paths).length > 0) )) { this.reportProgress(0.2, `找到有效的API文档: ${endpoint}`); return endpoint; } } catch (error) { // 忽略错误,继续尝试下一个端点 } } // 如果没有找到有效的API文档,则返回原始URL this.reportProgress(0.2, '未找到有效的API文档,使用原始URL'); } return url; } /** * 优化解析选项 */ private getParseOptions(): SwaggerParser.Options { return { validate: { schema: !this.options.skipValidation, spec: !this.options.skipValidation }, dereference: { circular: 'ignore' as 'ignore' } }; } /** * 获取API文档原始数据 */ private async fetchRawApiData(): Promise<any> { if (this.rawApiData) { return this.rawApiData; } // 尝试从缓存加载 const cachedData = await this.getFromCache(); if (cachedData) { this.rawApiData = cachedData; return this.rawApiData; } this.reportProgress(0.3, '开始获取API文档'); // 解析Swagger UI URL以获取实际API文档URL const resolvedUrl = await this.resolveSwaggerDocUrl(this.url); // 获取API文档 let apiData: any; if (resolvedUrl.startsWith('http')) { // 从URL获取 try { const response = await axios.get(resolvedUrl, { headers: this.headers, timeout: 30000, // 30秒超时 maxContentLength: 50 * 1024 * 1024 // 50MB }); apiData = response.data; } catch (error: any) { throw new Error(`获取API文档失败: ${error.message}`); } } else { // 从本地文件获取 try { const content = await fs.readFile(resolvedUrl, 'utf8'); apiData = JSON.parse(content); } catch (error: any) { throw new Error(`读取本地文件失败: ${error.message}`); } } this.reportProgress(0.5, 'API文档获取成功,保存到缓存'); // 保存到缓存 await this.saveToCache(apiData); this.rawApiData = apiData; return apiData; } /** * 轻量级解析API文档 */ private async parseApiLite(): Promise<OpenAPIV3.Document> { this.reportProgress(0.6, '开始轻量级解析API文档'); const apiData = await this.fetchRawApiData(); // 创建一个轻量级的文档对象,只包含基本信息和路径 const liteDoc: any = { openapi: apiData.openapi || apiData.swagger || '3.0.0', info: apiData.info || { title: 'API', version: '1.0.0' }, paths: apiData.paths || {}, components: { schemas: {} // 不包含复杂的schema定义 } }; // 使用不严格的解析选项 const parseOptions = this.getParseOptions(); try { this.reportProgress(0.7, '正在解析基本API结构'); // 使用parse而不是validate const result = await SwaggerParser.parse(liteDoc, parseOptions); return result as OpenAPIV3.Document; } catch (error: any) { throw new Error(`解析API文档失败: ${error.message}`); } } /** * 获取API文档 */ async fetchApi(forceRefresh: boolean = false): Promise<OpenAPIV3.Document> { // 如果已经解析过且不需要强制刷新,则直接返回 if (this.api && !forceRefresh) { return this.api; } try { // 如果启用了懒加载,则使用轻量级解析 if (this.options.lazyLoading) { this.api = await this.parseApiLite(); } else { // 获取原始API数据 const apiData = await this.fetchRawApiData(); // 使用不严格的解析选项 const parseOptions = this.getParseOptions(); this.reportProgress(0.6, '开始完整解析API文档'); // 解析API文档 try { if (this.options.skipValidation) { const result = await SwaggerParser.parse(apiData, parseOptions); this.api = result as OpenAPIV3.Document; } else { try { // 尝试验证(如果失败则退回到parse) const result = await SwaggerParser.validate(apiData, parseOptions); this.api = result as OpenAPIV3.Document; } catch (error) { this.reportProgress(0.7, '验证失败,使用不严格解析'); const result = await SwaggerParser.parse(apiData, parseOptions); this.api = result as OpenAPIV3.Document; } } } catch (error: any) { throw new Error(`解析API文档失败: ${error.message}`); } } this.reportProgress(1.0, 'API文档解析完成'); return this.api; } catch (error: any) { this.reportProgress(1.0, `解析失败: ${error.message}`); throw new Error(`Failed to fetch API: ${error.message}`); } } /** * 按需获取模式定义 */ async getSchema(schemaName: string): Promise<OpenAPIV3.SchemaObject | undefined> { // 确保已经获取了API文档 if (!this.api) { await this.fetchApi(); } // 如果模式已经加载,则直接返回 if (this.api?.components?.schemas?.[schemaName]) { return this.api.components.schemas[schemaName] as OpenAPIV3.SchemaObject; } // 如果未启用懒加载,则返回undefined if (!this.options.lazyLoading) { return undefined; } // 获取原始API数据 const apiData = await this.fetchRawApiData(); // 检查模式是否存在 if (!apiData.components?.schemas?.[schemaName]) { return undefined; } // 解析单个模式 try { const schemaDoc: any = { openapi: '3.0.0', info: { title: 'Schema', version: '1.0.0' }, paths: {}, components: { schemas: { [schemaName]: apiData.components.schemas[schemaName] } } }; const parseOptions = this.getParseOptions(); const result = await SwaggerParser.parse(schemaDoc, parseOptions) as any; // 将解析后的模式添加到API文档中 if (!this.api) { return result.components.schemas[schemaName] as OpenAPIV3.SchemaObject; } if (!this.api.components) { this.api.components = { schemas: {} }; } if (!this.api.components.schemas) { this.api.components.schemas = {}; } this.api.components.schemas[schemaName] = result.components.schemas[schemaName]; return this.api.components.schemas[schemaName] as OpenAPIV3.SchemaObject; } catch (error) { console.error(`解析模式 ${schemaName} 失败:`, error); return undefined; } } /** * 获取所有模式定义 */ async getAllSchemas(): Promise<Record<string, OpenAPIV3.SchemaObject>> { // 确保已经获取了API文档 if (!this.api) { await this.fetchApi(); } // 如果已经加载了所有模式,则直接返回 if (this.api?.components?.schemas && Object.keys(this.api.components.schemas).length > 0 && !this.options.lazyLoading) { return this.api.components.schemas as Record<string, OpenAPIV3.SchemaObject>; } // 获取原始API数据 const apiData = await this.fetchRawApiData(); // 如果没有模式,则返回空对象 if (!apiData.components?.schemas) { return {}; } // 解析所有模式 try { const schemasDoc: any = { openapi: '3.0.0', info: { title: 'Schemas', version: '1.0.0' }, paths: {}, components: { schemas: apiData.components.schemas } }; const parseOptions = this.getParseOptions(); const result = await SwaggerParser.parse(schemasDoc, parseOptions) as any; // 将解析后的模式添加到API文档中 if (!this.api) { return result.components.schemas as Record<string, OpenAPIV3.SchemaObject>; } if (!this.api.components) { this.api.components = { schemas: {} }; } this.api.components.schemas = result.components.schemas; return this.api.components.schemas as Record<string, OpenAPIV3.SchemaObject>; } catch (error) { console.error(`解析所有模式失败:`, error); return {}; } } /** * 获取所有API操作 */ async getAllOperations(): Promise<ApiOperation[]> { // 确保已经获取了API文档 if (!this.api) { await this.fetchApi(); } const operations: ApiOperation[] = []; // 遍历所有路径 for (const [path, pathItem] of Object.entries(this.api?.paths || {})) { if (!pathItem) continue; // 遍历所有HTTP方法 for (const method of ['get', 'post', 'put', 'delete', 'patch', 'options', 'head'] as const) { const operationObject = pathItem[method]; if (!operationObject) continue; // 添加到操作列表 operations.push({ operationId: operationObject.operationId || `${method}${path.replace(/[^a-zA-Z0-9]/g, '')}`, method, path, summary: operationObject.summary, description: operationObject.description, tags: operationObject.tags, parameters: (pathItem.parameters || []).concat(operationObject.parameters || []) as OpenAPIV3.ParameterObject[], requestBody: operationObject.requestBody as OpenAPIV3.RequestBodyObject, responses: operationObject.responses as Record<string, OpenAPIV3.ResponseObject> }); } } return operations; } /** * 获取特定标签的API操作 */ async getOperationsByTag(tag: string): Promise<ApiOperation[]> { const operations = await this.getAllOperations(); return operations.filter(op => op.tags?.includes(tag)); } /** * 获取特定路径前缀的API操作 */ async getOperationsByPathPrefix(prefix: string): Promise<ApiOperation[]> { const operations = await this.getAllOperations(); return operations.filter(op => op.path.startsWith(prefix)); } }

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

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