Skip to main content
Glama

Apifox MCP

by Warren-W
apifox-client.ts15.8 kB
import axios, { AxiosInstance } from 'axios'; /** * Apifox API 客户端配置 */ export interface ApifoxConfig { /** API 访问令牌 */ token: string; /** 项目 ID */ projectId: string; /** API 基础 URL,默认为 https://api.apifox.com */ baseUrl?: string; } /** * API 接口定义 */ export interface ApiDefinition { /** 接口 ID(更新和删除时需要) */ id?: string; /** 接口名称 */ name: string; /** 接口路径 */ path: string; /** HTTP 方法 */ method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'; /** 接口描述 */ description?: string; /** 目录 ID(可选) */ folderId?: string; /** 请求参数 */ parameters?: any[]; /** 请求体 */ requestBody?: any; /** 响应定义 */ responses?: any; /** 标签 */ tags?: string[]; } /** * API 列表查询参数 */ export interface ApiListQuery { /** 页码 */ page?: number; /** 每页数量 */ pageSize?: number; /** 搜索关键词 */ keyword?: string; /** 目录 ID */ folderId?: string; } /** * Apifox API 客户端 * 用于与 Apifox 开放 API 进行交互 * * 注意:Apifox Open API 功能有限,经过测试只有导入功能可用 */ export class ApifoxClient { private client: AxiosInstance; private projectId: string; constructor(config: ApifoxConfig) { this.projectId = config.projectId; const baseURL = config.baseUrl || 'https://api.apifox.com'; this.client = axios.create({ baseURL, headers: { 'Authorization': `Bearer ${config.token}`, 'Content-Type': 'application/json', 'X-Apifox-Api-Version': '2024-03-28' }, timeout: 30000 }); // 添加响应拦截器处理错误 this.client.interceptors.response.use( response => response, error => { const status = error.response?.status; const message = error.response?.data?.errorMessage || error.response?.data?.message || error.message; const errorCode = error.response?.data?.errorCode; let errorMsg = `Apifox API 错误`; if (errorCode) { errorMsg += ` (${errorCode})`; } if (status) { errorMsg += ` [${status}]`; } errorMsg += `: ${message}`; throw new Error(errorMsg); } ); } /** * 检测路径前缀,用于智能范围限定 * 自动识别导入规范涉及的路径范围 * * @param paths 路径列表 * @returns 路径前缀列表 */ private detectPathPrefixes(paths: string[]): string[] { if (paths.length === 0) { return []; } // 将所有路径按层级分组 const pathsBySegments: string[][] = paths.map(p => p.split('/').filter(s => s)); // 找出最长公共前缀深度 let commonDepth = 0; if (pathsBySegments.length > 0) { const minLength = Math.min(...pathsBySegments.map(p => p.length)); for (let depth = 0; depth < minLength; depth++) { const firstSegment = pathsBySegments[0][depth]; const allSame = pathsBySegments.every(p => p[depth] === firstSegment); if (allSame) { commonDepth = depth + 1; } else { break; } } } // 尝试找出合适的分组深度(比公共前缀深1-2层) const groupingDepth = Math.min(commonDepth + 2, Math.max(...pathsBySegments.map(p => p.length)) - 1); // 按分组深度提取前缀 const prefixSet = new Set<string>(); for (const segments of pathsBySegments) { const depth = Math.min(groupingDepth, segments.length); if (depth > 0) { const prefix = '/' + segments.slice(0, depth).join('/'); prefixSet.add(prefix); } } // 去重并过滤子前缀 const prefixes = Array.from(prefixSet).sort((a, b) => a.length - b.length); const finalPrefixes: string[] = []; for (const prefix of prefixes) { // 检查是否已经有更短的前缀包含了这个前缀 const isSubPrefix = finalPrefixes.some(existing => prefix.startsWith(existing + '/')); if (!isSubPrefix) { finalPrefixes.push(prefix); } } return finalPrefixes; } /** * 批量导入 OpenAPI/Swagger 规范到 Apifox 项目 * * @param spec OpenAPI 3.0/3.1 或 Swagger 2.0 规范(JSON 对象) * @param options 导入选项 * @returns 导入结果,包含各类资源的创建/更新/错误计数 */ async importOpenApi(spec: any, options?: { endpointOverwriteBehavior?: 'OVERWRITE_EXISTING' | 'AUTO_MERGE' | 'KEEP_EXISTING' | 'CREATE_NEW'; schemaOverwriteBehavior?: 'OVERWRITE_EXISTING' | 'AUTO_MERGE' | 'KEEP_EXISTING' | 'CREATE_NEW'; updateFolderOfChangedEndpoint?: boolean; prependBasePath?: boolean; targetBranchId?: number; markDeprecatedEndpoints?: boolean; confirmHighDeprecation?: boolean; }): Promise<any> { let finalSpec = spec; const warnings: string[] = []; // 收集警告信息 let deprecatedInfo: { count: number; ratio: number; scope: string[] } | null = null; // 如果启用了标记废弃接口功能 if (options?.markDeprecatedEndpoints) { try { // 1. 先导出现有的 OpenAPI 规范 const existingSpec = await this.exportOpenApi({ oasVersion: spec.openapi?.startsWith('3.1') ? '3.1' : '3.0', exportFormat: 'JSON' }); // 2. 智能检测路径前缀范围 const newPaths = spec.paths || {}; const newPathKeys = Object.keys(newPaths); // 安全检查:防止空 spec 导致全部标记为废弃 if (newPathKeys.length === 0) { const warning = '⚠️ 检测到空规范:新规范没有任何接口(paths 为空),已自动跳过废弃标记功能'; warnings.push(warning); warnings.push('💡 建议:只在导入包含实际接口的规范时启用 markDeprecatedEndpoints'); console.error(`\n⚠️ 警告:新规范没有任何接口(paths 为空)`); console.error(`⚠️ 启用 markDeprecatedEndpoints 时使用空规范会导致所有现有接口被标记为废弃`); console.error(`⚠️ 已自动跳过废弃标记功能,直接导入空规范`); console.error(`💡 建议:只在导入包含实际接口的规范时启用 markDeprecatedEndpoints\n`); // 跳过废弃标记逻辑,直接使用原始 spec finalSpec = spec; // 执行导入(跳到 catch 块后) const response = await this.client.post( `/v1/projects/${this.projectId}/import-openapi`, { input: JSON.stringify(finalSpec), options: { endpointOverwriteBehavior: options?.endpointOverwriteBehavior || 'OVERWRITE_EXISTING', schemaOverwriteBehavior: options?.schemaOverwriteBehavior || 'OVERWRITE_EXISTING', updateFolderOfChangedEndpoint: options?.updateFolderOfChangedEndpoint ?? false, prependBasePath: options?.prependBasePath ?? false, ...(options?.targetBranchId && { targetBranchId: options.targetBranchId }) } } ); return { ...response.data, _warnings: warnings }; } // 提取所有新路径的公共前缀 const pathPrefixes = this.detectPathPrefixes(newPathKeys); const scopeInfo = pathPrefixes.length > 0 ? pathPrefixes.join(', ') : '全部接口'; console.error(`\n🔍 检测到导入范围: ${scopeInfo}`); console.error(`📊 新规范包含 ${newPathKeys.length} 个接口路径`); // 3. 找出已删除的接口(只在相同路径前缀范围内对比) const existingPaths = existingSpec.paths || {}; const deprecatedPaths: any = {}; // 计算范围内的现有接口总数 let existingPathsInScope = 0; for (const path in existingPaths) { const isInScope = pathPrefixes.length === 0 || pathPrefixes.some(prefix => path.startsWith(prefix)); if (isInScope) { existingPathsInScope++; } } for (const path in existingPaths) { // 只对比在相同路径前缀范围内的接口 const isInScope = pathPrefixes.length === 0 || pathPrefixes.some(prefix => path.startsWith(prefix)); if (!isInScope) { // 不在导入范围内,跳过 continue; } if (!newPaths[path]) { // 整个路径被删除(在导入范围内) deprecatedPaths[path] = { ...existingPaths[path] }; // 标记所有方法为废弃 for (const method in deprecatedPaths[path]) { if (method !== 'parameters') { deprecatedPaths[path][method] = { ...deprecatedPaths[path][method], deprecated: true, summary: `[已废弃] ${deprecatedPaths[path][method].summary || ''}`, description: `⚠️ 此接口已废弃,不再维护。\n\n${deprecatedPaths[path][method].description || ''}` }; } } } else { // 路径存在,检查方法是否被删除 const existingMethods = Object.keys(existingPaths[path]).filter(m => m !== 'parameters'); const newMethods = Object.keys(newPaths[path]).filter(m => m !== 'parameters'); for (const method of existingMethods) { if (!newMethods.includes(method)) { // 方法被删除,标记为废弃 if (!deprecatedPaths[path]) { deprecatedPaths[path] = {}; } deprecatedPaths[path][method] = { ...existingPaths[path][method], deprecated: true, summary: `[已废弃] ${existingPaths[path][method].summary || ''}`, description: `⚠️ 此接口已废弃,不再维护。\n\n${existingPaths[path][method].description || ''}` }; } } } } // 4. 合并废弃接口到新规范 if (Object.keys(deprecatedPaths).length > 0) { // 计算统计信息(使用范围内的接口数计算比例) const deprecatedPathCount = Object.keys(deprecatedPaths).length; const deprecatedRatio = existingPathsInScope > 0 ? (deprecatedPathCount / existingPathsInScope) * 100 : 0; // 保存废弃信息用于返回 deprecatedInfo = { count: deprecatedPathCount, ratio: deprecatedRatio, scope: pathPrefixes }; // 安全检查:如果废弃比例过高,要求用户确认 if (deprecatedRatio > 50) { // 如果没有传递确认参数,抛出错误阻止执行 if (!options?.confirmHighDeprecation) { const errorMessage = [ `⚠️ 高比例废弃操作需要确认`, ``, `检测到即将标记 ${deprecatedPathCount} 个接口为废弃,占范围内接口的 ${deprecatedRatio.toFixed(1)}%`, `这个比例异常高,可能导致大量接口被误标记为废弃。`, ``, `📊 详细信息:`, ` - 影响范围: ${pathPrefixes.length > 0 ? pathPrefixes.join(', ') : '全部接口'}`, ` - 范围内现有接口: ${existingPathsInScope} 个`, ` - 新规范接口数量: ${newPathKeys.length} 个`, ` - 即将废弃接口: ${deprecatedPathCount} 个`, ` - 废弃比例: ${deprecatedRatio.toFixed(1)}%`, ``, `💡 可能原因:`, ` 1. 新规范的接口数量太少(当前仅 ${newPathKeys.length} 个)`, ` 2. 新规范的路径前缀与现有接口不匹配`, ` 3. 这是一次大规模重构,确实要废弃大量接口`, ``, `❌ 操作已阻止,需要用户确认后才能继续`, ``, `如需继续,请在 options 中设置 confirmHighDeprecation: true` ].join('\n'); throw new Error(errorMessage); } // 用户已确认,记录警告信息并继续执行 warnings.push(`⚠️ 高比例废弃警告:即将标记 ${deprecatedPathCount} 个接口为废弃(${deprecatedRatio.toFixed(1)}%)`); warnings.push(`✅ 用户已确认,继续执行`); console.error(`\n⚠️ 警告:即将标记 ${deprecatedPathCount} 个接口为废弃(占现有接口的 ${deprecatedRatio.toFixed(1)}%)`); console.error(`✅ 用户已确认,继续执行\n`); } // 深度合并:保留新规范的所有内容,同时添加废弃的方法 const mergedPaths: any = { ...spec.paths }; for (const path in deprecatedPaths) { if (!mergedPaths[path]) { // 整个路径被删除,添加所有废弃方法 mergedPaths[path] = deprecatedPaths[path]; } else { // 路径存在,只添加被删除的方法 mergedPaths[path] = { ...mergedPaths[path], ...deprecatedPaths[path] }; } } finalSpec = { ...spec, paths: mergedPaths }; console.error(`\n🔖 标记了 ${deprecatedPathCount} 个废弃接口路径(${deprecatedRatio.toFixed(1)}%)`); } else { console.error(`\n✅ 没有接口被删除,无需标记废弃`); } } catch (error) { // 如果获取现有规范失败(比如项目是空的),忽略错误继续导入 console.error(`⚠️ 无法获取现有规范来标记废弃接口: ${error instanceof Error ? error.message : String(error)}`); } } // 执行导入 const response = await this.client.post( `/v1/projects/${this.projectId}/import-openapi`, { input: JSON.stringify(finalSpec), options: { endpointOverwriteBehavior: options?.endpointOverwriteBehavior || 'OVERWRITE_EXISTING', schemaOverwriteBehavior: options?.schemaOverwriteBehavior || 'OVERWRITE_EXISTING', updateFolderOfChangedEndpoint: options?.updateFolderOfChangedEndpoint ?? false, prependBasePath: options?.prependBasePath ?? false, ...(options?.targetBranchId && { targetBranchId: options.targetBranchId }) } } ); // 附加警告信息和废弃统计到返回值 return { ...response.data, _warnings: warnings.length > 0 ? warnings : undefined, _deprecatedInfo: deprecatedInfo }; } /** * 导出 Apifox 项目为 OpenAPI/Swagger 规范 * * @param options 导出选项 * @returns OpenAPI 规范对象 */ async exportOpenApi(options?: { oasVersion?: '2.0' | '3.0' | '3.1'; exportFormat?: 'JSON' | 'YAML'; scope?: { type?: 'ALL' | 'SELECTED_FOLDERS' | 'SELECTED_ENDPOINTS'; excludedByTags?: string[]; }; options?: { includeApifoxExtensionProperties?: boolean; addFoldersToTags?: boolean; }; }): Promise<any> { const response = await this.client.post( `/v1/projects/${this.projectId}/export-openapi`, { oasVersion: options?.oasVersion || '3.0', exportFormat: options?.exportFormat || 'JSON', ...(options?.scope && { scope: options.scope }), ...(options?.options && { options: options.options }) } ); return response.data; } }

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/Warren-W/apifox-mcp'

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