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
JavaScript
/**
* 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);
});