UNPKG

skilled-feishu-mcp

Version:

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

475 lines (407 loc) 14.7 kB
// 从客户端视角测试MCP服务器的性能 import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import path from 'path'; import process from 'node:process'; import fs from 'fs'; import { spawn } from 'child_process'; // 设置测试参数 const TEST_ITERATIONS = 3; // 多次测试以获取平均值 const IS_DEV_MODE = process.env.NODE_ENV === 'development'; const TIMEOUT_MS = 30000; // 增加到30秒超时 // 保存性能指标 const metrics = { serverAndConnectionTime: [], clientCreationTime: [], toolsListingTime: [], totalProcessTime: [], failureCount: 0 }; // 获取命令行参数 const appId = process.env.FEISHU_APP_ID || 'cli_test_9e11c52b0e1c500e'; const appSecret = process.env.FEISHU_APP_SECRET || 'test_app_secret_for_development'; // 日志文件 const logFile = fs.createWriteStream('mcp_client_test.log'); /** * 记录日志 */ function log(message, isError = false) { const timestamp = new Date().toISOString(); const logMessage = `[${timestamp}] ${message}`; // 控制台输出 if (isError) { console.error(logMessage); } else { console.log(message); // 控制台显示不带时间戳 } // 写入日志文件(带时间戳) logFile.write(logMessage + '\n'); } /** * 直接测试MCP命令可执行性 */ async function testMCPCommand() { return new Promise((resolve) => { log('测试MCP命令可执行性...'); // 使用完整路径 const command = '/opt/homebrew/bin/skilled-feishu-mcp'; const testProcess = spawn(command, ['--help'], { stdio: ['ignore', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; testProcess.stdout.on('data', (data) => { stdout += data.toString(); }); testProcess.stderr.on('data', (data) => { stderr += data.toString(); }); testProcess.on('error', (error) => { log(`命令执行错误: ${error.message}`, true); resolve(false); }); testProcess.on('close', (code) => { if (code === 0) { log('命令测试成功'); resolve(true); } else { log(`命令测试失败 (退出码: ${code})`, true); if (stderr) { log(`错误输出: ${stderr}`, true); } resolve(false); } }); }); } /** * 主测试函数 */ async function runClientTest() { log('='.repeat(80)); log(`MCP 客户端性能测试 ${IS_DEV_MODE ? '(开发模式)' : '(生产模式)'}`); log('='.repeat(80)); log(`测试时间: ${new Date().toLocaleString()}`); log(`Feishu 凭据: ${maskCredential(appId)} / ${maskCredential(appSecret)}`); log(`测试迭代次数: ${TEST_ITERATIONS}`); log('='.repeat(80)); // 首先测试命令是否可执行 const commandWorks = await testMCPCommand(); if (!commandWorks) { log('MCP命令不可执行,无法继续测试', true); return; } // 多次迭代以获得平均值 for (let i = 0; i < TEST_ITERATIONS; i++) { log(`\n开始测试迭代 ${i + 1}/${TEST_ITERATIONS}`); try { await performTest(i); await new Promise(resolve => setTimeout(resolve, 500)); // 在测试之间暂停 } catch (error) { log(`测试迭代 ${i + 1} 失败: ${error.message}`, true); if (error.stack) { log(`堆栈跟踪: ${error.stack}`, true); } metrics.failureCount++; } } // 输出结果报告 generateReport(); } /** * 执行单次测试 */ async function performTest(iteration) { // 记录总测试时间 const startTotal = process.hrtime.bigint(); // 1. 创建客户端 log('1. 创建MCP客户端...'); const clientStartTime = process.hrtime.bigint(); const client = new Client({ name: "MCP Client Test", version: "1.0.0" }, { capabilities: { tools: true, resources: true, prompts: true } }); const clientCreationTime = Number(process.hrtime.bigint() - clientStartTime) / 1_000_000; metrics.clientCreationTime.push(clientCreationTime); log(` 完成 (${clientCreationTime.toFixed(3)} ms)`); // 2. 创建传输层并连接到服务器 log('2. 创建传输层并连接到MCP服务器...'); const connectStartTime = process.hrtime.bigint(); // 使用完整的路径 let serverCommand = '/opt/homebrew/bin/skilled-feishu-mcp'; const serverArgs = [ '--stdio', `--feishu-app-id=${appId}`, `--feishu-app-secret=${appSecret}`, '--verbose' // 增加详细日志 ]; // 开发模式参数 if (IS_DEV_MODE) { serverArgs.push('--development'); } log(` 启动命令: ${serverCommand} ${serverArgs.join(' ')}`); // 创建客户端传输层 const transport = new StdioClientTransport({ command: serverCommand, args: serverArgs, stderr: 'pipe', // 捕获stderr以分析问题 env: process.env // 传递当前环境变量 }); // 捕获和处理stderr输出 let stderrOutput = ''; let lastStderrTime = Date.now(); const stderrListener = (chunk) => { const text = chunk.toString(); stderrOutput += text; lastStderrTime = Date.now(); log(` [stderr] ${text.trim()}`); }; // 设置连接超时 const connectionPromise = (async () => { try { log(' 启动连接...'); // 进行实际连接 await client.connect(transport); if (transport.stderr) { transport.stderr.on('data', stderrListener); } log(' 连接成功!'); return true; } catch (error) { log(` 连接出错: ${error.message}`, true); if (error.stack) { log(` 堆栈跟踪: ${error.stack}`, true); } throw error; } })(); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { const timeSinceLastStderr = Date.now() - lastStderrTime; if (stderrOutput) { reject(new Error(`连接超时 (${TIMEOUT_MS}ms), 最后的stderr输出 ${timeSinceLastStderr}ms 前`)); } else { reject(new Error(`连接超时 (${TIMEOUT_MS}ms), 没有stderr输出`)); } }, TIMEOUT_MS); }); try { await Promise.race([connectionPromise, timeoutPromise]); const connectionTime = Number(process.hrtime.bigint() - connectStartTime) / 1_000_000; metrics.serverAndConnectionTime.push(connectionTime); log(` 连接成功 (${connectionTime.toFixed(3)} ms)`); // 获取服务器信息 const serverInfo = client.getServerVersion(); log(` 服务器信息: ${serverInfo?.name || 'unknown'} v${serverInfo?.version || 'unknown'}`); const capabilities = client.getServerCapabilities(); log(` 服务器功能: ${JSON.stringify(capabilities)}`); } catch (error) { log(' 连接失败:', true); log(` 错误: ${error.message}`, true); if (stderrOutput) { log(' 服务器错误输出:', true); log(` ${stderrOutput.split('\n').join('\n ')}`, true); } // 关闭所有连接 try { await transport.close(); await client.close(); } catch (closeError) { log(` 关闭连接时出错: ${closeError.message}`, true); } throw error; } // 3. 获取工具列表 log('3. 获取工具列表...'); const toolsStartTime = process.hrtime.bigint(); try { log(' 发送工具列表请求...'); const toolsListResponse = await client.listTools({}); const toolsListTime = Number(process.hrtime.bigint() - toolsStartTime) / 1_000_000; metrics.toolsListingTime.push(toolsListTime); log(` 获取工具列表成功 (${toolsListTime.toFixed(3)} ms)`); log(` 可用工具: ${toolsListResponse.tools.length} 个`); // 显示工具信息 if (toolsListResponse.tools.length > 0) { log(' 工具列表:'); toolsListResponse.tools.forEach((tool, index) => { if (index < 5) { // 只显示前5个 log(` - ${tool.name}`); } }); if (toolsListResponse.tools.length > 5) { log(` - ... 另外 ${toolsListResponse.tools.length - 5} 个工具`); } } } catch (error) { log(' 获取工具列表失败:', true); log(` 错误: ${error.message}`, true); // 关闭连接但不抛出异常 try { await client.close(); await transport.close(); } catch (closeError) { log(` 关闭连接时出错: ${closeError.message}`, true); } throw error; } // 4. 关闭连接 log('4. 关闭连接...'); try { await client.close(); await transport.close(); log(' 连接已关闭'); } catch (error) { log(` 关闭连接失败: ${error.message}`, true); } // 计算总处理时间 const totalTime = Number(process.hrtime.bigint() - startTotal) / 1_000_000; metrics.totalProcessTime.push(totalTime); log(`\n测试迭代 ${iteration + 1} 完成,总耗时: ${totalTime.toFixed(2)} ms`); } /** * 生成详细的性能报告 */ function generateReport() { // 计算平均值和中位数 const calculateStats = (values) => { if (values.length === 0) 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 serverAndConnectionStats = calculateStats(metrics.serverAndConnectionTime); const clientCreationStats = calculateStats(metrics.clientCreationTime); const toolsListingStats = calculateStats(metrics.toolsListingTime); const totalStats = calculateStats(metrics.totalProcessTime); // 生成报告 log('\n' + '='.repeat(80)); log('MCP客户端性能测试报告'); log('='.repeat(80)); log(`测试环境: ${IS_DEV_MODE ? '开发模式' : '生产模式'}`); log(`成功率: ${(((TEST_ITERATIONS - metrics.failureCount) / TEST_ITERATIONS) * 100).toFixed(1)}% (${TEST_ITERATIONS - metrics.failureCount}/${TEST_ITERATIONS})`); log('\n性能指标 (毫秒):'); // 表格头部 log('指标'.padEnd(25) + '平均值'.padEnd(12) + '中位数'.padEnd(12) + '最小值'.padEnd(12) + '最大值'.padEnd(12)); log('-'.repeat(80)); // 表格内容 const formatRow = (name, stats) => { return name.padEnd(25) + stats.avg.toFixed(2).padEnd(12) + stats.median.toFixed(2).padEnd(12) + stats.min.toFixed(2).padEnd(12) + stats.max.toFixed(2).padEnd(12); }; log(formatRow('客户端创建时间', clientCreationStats)); log(formatRow('服务器启动和连接时间', serverAndConnectionStats)); log(formatRow('工具列表获取时间', toolsListingStats)); log('-'.repeat(80)); log(formatRow('总处理时间', totalStats)); // 输出阶段分析 log('\n处理时间占比分析:'); if (totalStats.avg > 0) { log(`客户端创建: ${((clientCreationStats.avg / totalStats.avg) * 100).toFixed(1)}%`); log(`服务器启动和连接: ${((serverAndConnectionStats.avg / totalStats.avg) * 100).toFixed(1)}%`); log(`获取工具列表: ${((toolsListingStats.avg / totalStats.avg) * 100).toFixed(1)}%`); } // 总结 log('\n总结:'); if (metrics.failureCount > 0) { log(`❌ 测试过程中出现 ${metrics.failureCount} 次失败`); } else { log('✅ 所有测试迭代均成功完成'); } log(`- 客户端总处理时间平均为 ${totalStats.avg.toFixed(2)} ms`); const bottleneck = findBottleneck( clientCreationStats.avg, serverAndConnectionStats.avg, toolsListingStats.avg ); log(`- 性能瓶颈主要在: ${bottleneck}`); // 将报告保存到文件 const reportText = ` MCP客户端性能测试报告 ${'='.repeat(80)} 测试时间: ${new Date().toLocaleString()} 测试环境: ${IS_DEV_MODE ? '开发模式' : '生产模式'} 测试迭代: ${TEST_ITERATIONS} 成功率: ${(((TEST_ITERATIONS - metrics.failureCount) / TEST_ITERATIONS) * 100).toFixed(1)}% (${TEST_ITERATIONS - metrics.failureCount}/${TEST_ITERATIONS}) 性能指标 (毫秒): ${'指标'.padEnd(25) + '平均值'.padEnd(12) + '中位数'.padEnd(12) + '最小值'.padEnd(12) + '最大值'.padEnd(12)} ${'-'.repeat(80)} ${formatRow('客户端创建时间', clientCreationStats)} ${formatRow('服务器启动和连接时间', serverAndConnectionStats)} ${formatRow('工具列表获取时间', toolsListingStats)} ${'-'.repeat(80)} ${formatRow('总处理时间', totalStats)} 处理时间占比分析: 客户端创建: ${((clientCreationStats.avg / totalStats.avg) * 100).toFixed(1)}% 服务器启动和连接: ${((serverAndConnectionStats.avg / totalStats.avg) * 100).toFixed(1)}% 获取工具列表: ${((toolsListingStats.avg / totalStats.avg) * 100).toFixed(1)}% 总结: ${metrics.failureCount > 0 ? `❌ 测试过程中出现 ${metrics.failureCount} 次失败` : '✅ 所有测试迭代均成功完成'} - 客户端总处理时间平均为 ${totalStats.avg.toFixed(2)} ms - 性能瓶颈主要在: ${bottleneck} 注意事项: - 此测试是从客户端视角执行的,包括启动服务器进程和建立连接 - 服务器启动和连接时间包括了进程创建、资源初始化和协议握手 - 数值可能因系统环境、网络和负载而异 - 此测试可用于CherryStudio中集成MCP工具的性能分析 `; try { fs.writeFileSync('mcp_client_performance_report.txt', reportText); log('\n报告已保存到 mcp_client_performance_report.txt'); log('详细日志已保存到 mcp_client_test.log'); } catch (error) { log(`保存报告失败: ${error.message}`, true); } // 关闭日志文件 logFile.end(); } /** * 确定性能瓶颈在哪个环节 */ function findBottleneck(clientTime, serverConnectionTime, toolsTime) { const times = [ { name: '客户端创建', value: clientTime }, { name: '服务器启动和连接', value: serverConnectionTime }, { name: '获取工具列表', value: toolsTime } ]; // 按时间降序排序 times.sort((a, b) => b.value - a.value); // 主要瓶颈是耗时最长的环节 return times[0].name; } /** * 遮掩凭据显示 */ function maskCredential(credential) { if (!credential || credential.length <= 8) { return '********'; } return `${credential.substring(0, 4)}****${credential.substring(credential.length - 4)}`; } // 运行测试 runClientTest().catch(error => { log(`测试主程序失败: ${error.message}`, true); if (error.stack) { log(`堆栈跟踪: ${error.stack}`, true); } process.exit(1); });