#!/usr/bin/env node // 引入必要的模块 import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import http from "http"; import { z } from "zod"; import { AsyncLocalStorage } from 'async_hooks'; import { ApifoxService } from './apifox.js' const requestContext = new AsyncLocalStorage(); // 创建 MCP 服务器实例 // 为其设置一个名字和版本 const server = new McpServer( { name: "apifox-mcp-server", version: "1.0.0", }, { capabilities: { prompts: {}, tools: {}, }, } ); function getApifoxService() { const store = requestContext.getStore(); const token = store?.token || process.env.APIFOX_TOKEN; const projectId = store?.projectId ? Number(store.projectId) : Number(process.env.APIFOX_PROJECT_ID); if (!token) { throw new Error("Apifox Token not configured (env: APIFOX_TOKEN or header: Authorization)"); } return new ApifoxService({ projectId, token }); } server.registerTool( 'get_api_by_path', { description: "根据接口path获取Apifox接口定义数据", inputSchema: z.object({ path: z.string().describe("接口路径"), projectId: z.number().optional().describe("Apifox项目ID"), }), }, async ({ path, projectId }) => { try { const apifoxService = getApifoxService(); const apiData = await apifoxService.getApiDetailByPath(path, projectId); return { content: [ { type: "text", text: JSON.stringify(apiData, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `获取接口详情失败: ${error.message}`, }, ], isError: true, }; } } ) server.registerTool( 'get_apis_by_folder', { description: "根据接口目录获取Apifox目录下所有接口定义数据", inputSchema: z.object({ folderName: z.string().describe("目录路径"), projectId: z.number().optional().describe("Apifox项目ID"), }), }, async ({ folderName, projectId }) => { try { const apifoxService = getApifoxService(); const apiData = await apifoxService.getApiListByFolderName(folderName, projectId); return { content: [ { type: "text", text: JSON.stringify(apiData, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `获取接口数据失败: ${error.message}`, }, ], isError: true, }; } } ) /** * 解析命令行参数 */ function parseArguments() { const args = process.argv.slice(2); // 检查是否需要帮助信息 if (args.includes('--help') || args.includes('-h')) { printHelp(); process.exit(0); } // 解析传输方式 const transportArgIndex = args.indexOf('--transport') !== -1 ? args.indexOf('--transport') : args.indexOf('-t'); const transportType = transportArgIndex !== -1 ? args[transportArgIndex + 1] : 'stdio'; // 验证传输方式 if (!['stdio', 'http'].includes(transportType)) { console.error(`错误: 不支持的传输方式 "${transportType}". 支持的方式: stdio, http`); process.exit(1); } // 解析端口号 const portArgIndex = args.indexOf('--port') !== -1 ? args.indexOf('--port') : args.indexOf('-p'); const port = portArgIndex !== -1 ? parseInt(args[portArgIndex + 1]) : 8889; // 验证端口号 if (isNaN(port) || port < 1 || port > 65535) { console.error(`错误: 无效的端口号 "${args[portArgIndex + 1]}". 端口号必须在 1-65535 之间`); process.exit(1); } return { transportType, port }; } /** * 打印帮助信息 */ function printHelp() { console.log(` Apifox MCP 服务器 - Model Context Protocol 服务器实现 用法: node index.js [选项] 选项: -t, --transport 传输方式 (stdio|http) [默认: stdio] -p, --port HTTP 服务器端口号 [默认: 8889] -h, --help 显示帮助信息 示例: # 使用 stdio 传输方式启动 (用于 Claude Desktop 等客户端) node index.js node index.js --transport stdio # 使用 HTTP 传输方式启动 (用于远程访问或调试) node index.js --transport http node index.js --transport http --port 3000 环境变量: APIFOX_TOKEN Apifox 个人访问令牌 (必需) APIFOX_PROJECT_ID 默认的 Apifox 项目 ID (可选) HTTP 模式端点: GET /health 健康检查 POST /sse MCP 服务端点 (Server-Sent Events) POST /messages MCP 消息端点 HTTP 模式请求头: APIFOX_TOKEN token (可选,覆盖 APIFOX_TOKEN) APIFOX_PROJECT_ID 项目 ID (可选,覆盖 APIFOX_PROJECT_ID) `); } /** * 启动 stdio 传输方式 */ async function startStdioTransport() { const transport = new StdioServerTransport(); await server.connect(transport); // stdio 模式下,日志必须输出到 stderr,避免干扰 stdout 上的 JSON-RPC 消息 console.error("========================================"); console.error("Apifox MCP 服务器已启动"); console.error("传输方式: stdio"); console.error("========================================"); } /** * 启动 HTTP 传输方式 */ async function startHttpTransport(port) { const transport = new StreamableHTTPServerTransport({}); await server.connect(transport); const httpServer = http.createServer(async (req, res) => { // 设置 CORS 头 res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, APIFOX_TOKEN, APIFOX_PROJECT_ID, mcp-session-id'); // 提取认证信息 const token = req.headers['APIFOX_TOKEN']; const projectId = req.headers['APIFOX_PROJECT_ID']; // 记录请求日志 const tokenStatus = token ? (token.substring(0, 4) + '***') : 'Missing'; console.log(`[HTTP] ${new Date().toISOString()} | ${req.method} ${req.url} | ProjectId: ${projectId || 'N/A'} | Token: ${tokenStatus}`); // 处理 CORS 预检请求 if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } // 健康检查端点 if (req.method === 'GET' && req.url === '/health') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'ok', server: 'apifox-mcp-server', version: '1.0.0', transport: 'http' })); return; } // 设置请求上下文 const context = {}; if (token) context.token = token; if (projectId) context.projectId = projectId; // 在上下文中处理 MCP 请求 requestContext.run(context, async () => { try { await transport.handleRequest(req, res); console.log(`[HTTP] ${new Date().toISOString()} | Success: ${req.method} ${req.url}`); } catch (error) { console.error(`[HTTP] ${new Date().toISOString()} | Error: ${req.method} ${req.url}`, error); if (!res.headersSent) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Internal Server Error', message: error.message })); } } }); }); // 启动 HTTP 服务器 return new Promise((resolve, reject) => { httpServer.listen(port, (err) => { if (err) { reject(err); } else { console.log("========================================"); console.log("Apifox MCP 服务器已启动"); console.log(`传输方式: HTTP`); console.log(`监听端口: ${port}`); console.log(`MCP 服务端点: http://localhost:${port}/sse`); console.log(`MCP 消息端点: http://localhost:${port}/messages`); console.log(`健康检查: http://localhost:${port}/health`); console.log("========================================"); resolve(); } }); // 处理服务器错误 httpServer.on('error', (error) => { if (error.code === 'EADDRINUSE') { console.error(`错误: 端口 ${port} 已被占用`); } else { console.error('HTTP 服务器错误:', error); } process.exit(1); }); }); } /** * 主函数 - 启动 MCP 服务器 */ async function main() { // 解析命令行参数 const { transportType, port } = parseArguments(); // 验证必需的环境变量 if (!process.env.APIFOX_TOKEN) { console.error("错误: 未设置环境变量 APIFOX_TOKEN"); console.error("请设置 Apifox 个人访问令牌:"); console.error(" export APIFOX_TOKEN=your_token_here"); process.exit(1); } // 根据传输方式启动服务器 if (transportType === 'http') { await startHttpTransport(port); } else { await startStdioTransport(); } } // 运行主函数,并捕获可能的错误 main().catch((error) => { console.error("服务器启动失败:", error); process.exit(1); }); export { server };