UNPKG

skilled-feishu-mcp

Version:

A Model Context Protocol (MCP) server that integrates with Feishu's Open Platform APIs

552 lines (474 loc) 16.7 kB
#!/usr/bin/env node /** * MCP性能测试脚本 * 测量MCP服务器启动和各种操作的性能指标 */ import { spawn } from 'child_process'; import fs from 'fs'; import path from 'path'; import { performance } from 'perf_hooks'; // 配置 const IS_DEV_MODE = process.env.NODE_ENV === 'development'; const SERVER_PATH = '/opt/homebrew/bin/skilled-feishu-mcp'; const APP_ID = process.env.FEISHU_APP_ID || 'cli_test_9e11c52b0e1c500e'; const APP_SECRET = process.env.FEISHU_APP_SECRET || 'test_app_secret_for_development'; const TEST_ITERATIONS = 3; const TIMEOUT_MS = 15000; // 性能指标 const metrics = { startupTime: [], // 从启动进程到出现第一行日志的时间 readyTime: [], // 从启动进程到服务器完成初始化的时间 initializeTime: [], // 发送initialize请求到收到响应的时间 listToolsTime: [], // 发送listTools请求到收到响应的时间 shutdownTime: [], // 发送shutdown请求到收到响应的时间 totalProcessingTime: [] // 整个测试流程的总时间 }; // 当前测试迭代 let currentIteration = 0; let failureCount = 0; // 创建日志目录 const logDir = path.join(process.cwd(), 'logs'); if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir); } // 日志文件 const logFile = fs.createWriteStream(path.join(logDir, 'mcp_perf_test.log')); // MCP请求ID计数器 let requestIdCounter = 1; /** * 记录日志 */ function log(message, isError = false) { const timestamp = new Date().toISOString(); const logMessage = `[${timestamp}] ${message}`; // 控制台输出 console[isError ? 'error' : 'log'](message); // 写入日志文件 logFile.write(logMessage + '\n'); } /** * 创建MCP协议消息 */ function createMcpMessage(method, params = {}) { const jsonContent = JSON.stringify({ jsonrpc: "2.0", id: requestIdCounter++, method, params }); // MCP协议格式: Content-Length头 + 两个CRLF + JSON内容 const contentLength = Buffer.byteLength(jsonContent, 'utf8'); return `Content-Length: ${contentLength}\r\n\r\n${jsonContent}`; } /** * 协议消息解析器 */ class MessageParser { constructor() { this.buffer = ''; this.contentLength = -1; this.messages = []; } append(data) { this.buffer += data; this.parse(); } parse() { while (true) { // 还没有读取到Content-Length if (this.contentLength === -1) { const headerEnd = this.buffer.indexOf('\r\n\r\n'); if (headerEnd === -1) { return; // 头部不完整,等待更多数据 } const header = this.buffer.substring(0, headerEnd); const match = /Content-Length: (\d+)/i.exec(header); if (!match) { log('无法解析Content-Length', true); this.buffer = this.buffer.substring(headerEnd + 4); continue; } this.contentLength = parseInt(match[1], 10); this.buffer = this.buffer.substring(headerEnd + 4); } // 检查消息体是否完整 if (this.buffer.length >= this.contentLength) { const content = this.buffer.substring(0, this.contentLength); this.buffer = this.buffer.substring(this.contentLength); this.contentLength = -1; try { const message = JSON.parse(content); this.messages.push(message); } catch (e) { log(`解析JSON失败: ${e.message}`, true); } } else { return; // 消息体不完整,等待更多数据 } } } hasMessages() { return this.messages.length > 0; } getNextMessage() { if (this.messages.length === 0) { return null; } return this.messages.shift(); } } /** * 测试单次迭代 */ async function runTestIteration(iteration) { currentIteration = iteration; log(`\n开始测试迭代 ${iteration + 1}/${TEST_ITERATIONS}`); // 性能计时 const startTime = performance.now(); let firstLogTime = null; let readyTime = null; let initializeStartTime = null; let initializeEndTime = null; let listToolsStartTime = null; let listToolsEndTime = null; let shutdownStartTime = null; let shutdownEndTime = null; // 状态标志 let serverReady = false; let pendingRequestId = null; log('\n1. 准备启动参数...'); const args = [ '--stdio', `--feishu-app-id=${APP_ID}`, `--feishu-app-secret=${APP_SECRET}`, '--verbose' ]; if (IS_DEV_MODE) { args.push('--development'); log(' 添加开发模式参数'); } log('\n2. 启动MCP服务器进程...'); const mcpProcess = spawn(SERVER_PATH, args, { stdio: ['pipe', 'pipe', 'pipe'], env: process.env }); // 设置编码 mcpProcess.stdin.setDefaultEncoding('utf8'); mcpProcess.stdout.setEncoding('utf8'); mcpProcess.stderr.setEncoding('utf8'); // 创建消息解析器 const parser = new MessageParser(); // 监听stdout(日志和JSON-RPC消息) mcpProcess.stdout.on('data', (data) => { // 记录第一行日志的时间 if (firstLogTime === null) { firstLogTime = performance.now(); metrics.startupTime[iteration] = firstLogTime - startTime; log(` 首次日志输出时间: ${metrics.startupTime[iteration].toFixed(3)}ms`); } // 处理数据 const lines = data.toString().trim().split('\n'); for (const line of lines) { if (line.trim()) { if (line.includes('MCP server started with stdio transport')) { readyTime = performance.now(); metrics.readyTime[iteration] = readyTime - startTime; log(` 服务器就绪时间: ${metrics.readyTime[iteration].toFixed(3)}ms`); serverReady = true; } else { try { // 尝试解析为JSON-RPC消息 parser.append(line); while (parser.hasMessages()) { const message = parser.getNextMessage(); if (message.id === pendingRequestId) { if (pendingRequestId === 1) { // initialize响应 initializeEndTime = performance.now(); metrics.initializeTime[iteration] = initializeEndTime - initializeStartTime; log(` initialize响应时间: ${metrics.initializeTime[iteration].toFixed(3)}ms`); pendingRequestId = null; // 发送listTools请求 setTimeout(() => sendListToolsRequest(), 100); } else if (pendingRequestId === 2) { // listTools响应 listToolsEndTime = performance.now(); metrics.listToolsTime[iteration] = listToolsEndTime - listToolsStartTime; log(` listTools响应时间: ${metrics.listToolsTime[iteration].toFixed(3)}ms`); pendingRequestId = null; if (message.result && message.result.tools) { log(` 获取到 ${message.result.tools.length} 个工具`); } // 发送shutdown请求 setTimeout(() => sendShutdownRequest(), 100); } else if (pendingRequestId === 3) { // shutdown响应 shutdownEndTime = performance.now(); metrics.shutdownTime[iteration] = shutdownEndTime - shutdownStartTime; log(` shutdown响应时间: ${metrics.shutdownTime[iteration].toFixed(3)}ms`); pendingRequestId = null; // 测试完成 const totalTime = performance.now() - startTime; metrics.totalProcessingTime[iteration] = totalTime; log(`\n测试迭代 ${iteration + 1} 完成,总耗时: ${totalTime.toFixed(2)}ms`); } } } } catch (e) { // 不是有效的JSON,作为日志处理 } } } } }); // 监听stderr(日志输出) mcpProcess.stderr.on('data', (data) => { const lines = data.toString().trim().split('\n'); lines.forEach(line => { if (line.trim()) { log(` [stderr] ${line}`); } }); }); // 监听进程退出 mcpProcess.on('exit', (code, signal) => { log(` 进程退出 - 代码: ${code}, 信号: ${signal}`); }); // 监听进程错误 mcpProcess.on('error', (err) => { log(` 进程错误: ${err.message}`, true); }); // 设置超时 const timeout = setTimeout(() => { log(`测试迭代 ${iteration + 1} 超时`, true); failureCount++; cleanup(); }, TIMEOUT_MS); /** * 发送initialize请求 */ async function sendInitializeRequest() { log('\n3. 发送initialize请求...'); initializeStartTime = performance.now(); pendingRequestId = 1; const initRequest = createMcpMessage('initialize', { client: { name: 'MCP Performance Test', version: '1.0.0' }, capabilities: { tools: true, resources: true, prompts: true } }); mcpProcess.stdin.write(initRequest); } /** * 发送listTools请求 */ function sendListToolsRequest() { log('\n4. 发送listTools请求...'); listToolsStartTime = performance.now(); pendingRequestId = 2; const toolsRequest = createMcpMessage('listTools', {}); mcpProcess.stdin.write(toolsRequest); } /** * 发送shutdown请求 */ function sendShutdownRequest() { log('\n5. 发送shutdown请求...'); shutdownStartTime = performance.now(); pendingRequestId = 3; const shutdownRequest = createMcpMessage('shutdown'); mcpProcess.stdin.write(shutdownRequest); } /** * 清理资源 */ function cleanup() { clearTimeout(timeout); try { if (!mcpProcess.killed) { mcpProcess.stdin.end(); setTimeout(() => { if (!mcpProcess.killed) { mcpProcess.kill(); } }, 500); } } catch (err) { log(` 清理错误: ${err.message}`, true); } } // 等待服务器准备就绪 await new Promise((resolve, reject) => { const readyCheckInterval = setInterval(() => { if (serverReady) { clearInterval(readyCheckInterval); resolve(); } }, 100); setTimeout(() => { clearInterval(readyCheckInterval); if (!serverReady) { reject(new Error('等待服务器就绪超时')); } }, 10000); }).then(() => { // 服务器就绪,发送initialize请求 setTimeout(() => sendInitializeRequest(), 100); }).catch((error) => { log(` 服务器初始化失败: ${error.message}`, true); failureCount++; cleanup(); }); // 等待测试完成 return new Promise((resolve) => { const checkInterval = setInterval(() => { if (shutdownEndTime !== null) { clearInterval(checkInterval); cleanup(); resolve(); } }, 100); setTimeout(() => { clearInterval(checkInterval); if (shutdownEndTime === null) { log(' 测试流程未能完成', true); failureCount++; cleanup(); resolve(); } }, TIMEOUT_MS); }); } /** * 生成性能报告 */ function generateReport() { const calculateStats = (values) => { if (!values.length) return { avg: 0, median: 0, min: 0, max: 0 }; const sorted = [...values].sort((a, b) => a - b); const sum = sorted.reduce((a, b) => a + b, 0); return { avg: sum / values.length, median: sorted[Math.floor(sorted.length / 2)], min: sorted[0], max: sorted[sorted.length - 1] }; }; // 计算统计数据 const startupStats = calculateStats(metrics.startupTime); const readyStats = calculateStats(metrics.readyTime); const initializeStats = calculateStats(metrics.initializeTime); const listToolsStats = calculateStats(metrics.listToolsTime); const shutdownStats = calculateStats(metrics.shutdownTime); const totalStats = calculateStats(metrics.totalProcessingTime); log('\n' + '='.repeat(80)); log('MCP性能测试报告'); log('='.repeat(80)); log(`测试环境: ${IS_DEV_MODE ? '开发模式' : '生产模式'}`); log(`测试时间: ${new Date().toLocaleString()}`); log(`成功率: ${(((TEST_ITERATIONS - failureCount) / TEST_ITERATIONS) * 100).toFixed(1)}% (${TEST_ITERATIONS - failureCount}/${TEST_ITERATIONS})`); // 表格头部 const header = '指标'.padEnd(25) + '平均值 (ms)'.padEnd(15) + '中位数 (ms)'.padEnd(15) + '最小值 (ms)'.padEnd(15) + '最大值 (ms)'.padEnd(15); log('\n性能指标:'); log(header); log('-'.repeat(85)); // 表格行 const formatRow = (name, stats) => { return name.padEnd(25) + stats.avg.toFixed(2).padEnd(15) + stats.median.toFixed(2).padEnd(15) + stats.min.toFixed(2).padEnd(15) + stats.max.toFixed(2).padEnd(15); }; log(formatRow('首次日志输出时间', startupStats)); log(formatRow('服务器就绪时间', readyStats)); log(formatRow('Initialize请求时间', initializeStats)); log(formatRow('ListTools请求时间', listToolsStats)); log(formatRow('Shutdown请求时间', shutdownStats)); log('-'.repeat(85)); log(formatRow('总处理时间', totalStats)); // 性能瓶颈分析 log('\n性能瓶颈分析:'); const phases = [ { name: '服务器启动', value: readyStats.avg }, { name: 'Initialize请求', value: initializeStats.avg }, { name: 'ListTools请求', value: listToolsStats.avg }, { name: 'Shutdown请求', value: shutdownStats.avg } ]; phases.sort((a, b) => b.value - a.value); for (let i = 0; i < phases.length; i++) { const phase = phases[i]; const percentage = (phase.value / totalStats.avg * 100).toFixed(1); log(`${i + 1}. ${phase.name}: ${phase.value.toFixed(2)}ms (${percentage}% 的总时间)`); } // 保存报告到文件 const reportText = ` MCP性能测试报告 ${'='.repeat(80)} 测试环境: ${IS_DEV_MODE ? '开发模式' : '生产模式'} 测试时间: ${new Date().toLocaleString()} 成功率: ${(((TEST_ITERATIONS - failureCount) / TEST_ITERATIONS) * 100).toFixed(1)}% (${TEST_ITERATIONS - failureCount}/${TEST_ITERATIONS}) 性能指标: ${header} ${'-'.repeat(85)} ${formatRow('首次日志输出时间', startupStats)} ${formatRow('服务器就绪时间', readyStats)} ${formatRow('Initialize请求时间', initializeStats)} ${formatRow('ListTools请求时间', listToolsStats)} ${formatRow('Shutdown请求时间', shutdownStats)} ${'-'.repeat(85)} ${formatRow('总处理时间', totalStats)} 性能瓶颈分析: ${phases.map((phase, i) => `${i + 1}. ${phase.name}: ${phase.value.toFixed(2)}ms (${(phase.value / totalStats.avg * 100).toFixed(1)}% 的总时间)`).join('\n')} 分析与建议: - 服务器启动过程包含Feishu客户端初始化、MCP服务加载和工具加载 - 首次通信可能受到JIT编译和其他初始化影响 - 对于生产环境,建议预热服务器并保持长连接以获得最佳性能 - 如果集成到CherryStudio,建议在应用启动时初始化MCP连接 测试注意事项: - 此测试是在单机环境进行的 - 性能可能受到系统负载和其他因素影响 - 测试使用了直接进程通信,实际RPC可能有不同的性能特征 `; fs.writeFileSync(path.join(logDir, 'mcp_performance_report.txt'), reportText); log(`\n报告已保存到 ${path.join(logDir, 'mcp_performance_report.txt')}`); } /** * 主函数 */ async function main() { log('======================================'); log('MCP服务器性能测试'); log('======================================'); log(`开始测试: ${new Date().toLocaleString()}`); log(`测试环境: ${IS_DEV_MODE ? '开发模式' : '生产模式'}`); log(`测试迭代次数: ${TEST_ITERATIONS}`); log(`服务器路径: ${SERVER_PATH}`); log(`App ID: ${APP_ID.substring(0, 4)}****${APP_ID.substring(APP_ID.length - 4)}`); // 运行多次迭代 for (let i = 0; i < TEST_ITERATIONS; i++) { await runTestIteration(i); // 迭代之间稍微暂停一下 if (i < TEST_ITERATIONS - 1) { await new Promise(resolve => setTimeout(resolve, 1000)); } } // 生成性能报告 generateReport(); // 关闭日志文件 logFile.end(); } // 执行主函数 main().catch(err => { log(`未捕获错误: ${err.message}`, true); if (err.stack) { log(err.stack, true); } process.exit(1); });