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
JavaScript
// 从客户端视角测试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);
});