bookworm-smart-assistant/scripts/mcp-usage-analyzer.js

214 lines
6.5 KiB
JavaScript

#!/usr/bin/env node
/**
* MCP 使用分析器
*
* 分析 activity-*.jsonl 中 MCP 工具调用频率,
* 按 MCP server 分组统计,输出生命周期优化建议。
*
* 用法:
* node scripts/mcp-usage-analyzer.js # 文本报告
* node scripts/mcp-usage-analyzer.js --json # JSON 输出
* node scripts/mcp-usage-analyzer.js --days 30 # 最近 N 天
*/
const fs = require('fs');
const path = require('path');
const detectClaudeRoot = () => require('./paths.config.js').PATHS.root;
const CLAUDE_ROOT = detectClaudeRoot();
const DEBUG_DIR = path.join(CLAUDE_ROOT, 'debug');
const SETTINGS_FILE = path.join(CLAUDE_ROOT, 'settings.json');
const JSON_MODE = process.argv.includes('--json');
const daysArg = process.argv.indexOf('--days');
const DAYS = daysArg >= 0 ? parseInt(process.argv[daysArg + 1]) || 30 : 30;
// === 加载 settings.json 获取注册的 MCP 列表 ===
function loadRegisteredMcps() {
try {
const settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
return Object.keys(settings.mcpServers || {});
} catch { return []; }
}
// === 加载活动日志 ===
function loadActivityLogs(maxDays) {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - maxDays);
const cutoffStr = cutoff.toISOString().slice(0, 10);
const events = [];
try {
const files = fs.readdirSync(DEBUG_DIR)
.filter(f => f.startsWith('activity-') && f.endsWith('.jsonl'))
.sort();
for (const file of files) {
const dateMatch = file.match(/activity-(\d{4}-\d{2}-\d{2})/);
if (dateMatch && dateMatch[1] < cutoffStr) continue;
const lines = fs.readFileSync(path.join(DEBUG_DIR, file), 'utf8').trim().split('\n');
for (const line of lines) {
try { events.push(JSON.parse(line)); } catch {}
}
}
} catch {}
return events;
}
// === 从 tool 名提取 MCP server 名 ===
function extractMcpServer(toolName) {
// 格式: mcp__servername__toolname
const match = toolName.match(/^mcp__([^_]+(?:-[^_]+)*)__/);
return match ? match[1] : null;
}
// === 分析 ===
function analyze() {
const registeredMcps = loadRegisteredMcps();
const events = loadActivityLogs(DAYS);
// 按 MCP server 分组统计
const serverStats = {};
let totalMcpCalls = 0;
for (const evt of events) {
const tool = evt.tool || '';
const server = extractMcpServer(tool);
if (!server) continue;
totalMcpCalls++;
if (!serverStats[server]) {
serverStats[server] = { calls: 0, tools: {}, dates: new Set(), lastUsed: '' };
}
serverStats[server].calls++;
// 工具级统计
const toolShort = tool.replace(/^mcp__[^_]+(?:-[^_]+)*__/, '');
serverStats[server].tools[toolShort] = (serverStats[server].tools[toolShort] || 0) + 1;
// 日期追踪
const date = (evt.ts || '').slice(0, 10);
if (date) {
serverStats[server].dates.add(date);
if (date > serverStats[server].lastUsed) serverStats[server].lastUsed = date;
}
}
// 生成建议
const recommendations = [];
for (const mcp of registeredMcps) {
const stats = serverStats[mcp];
if (!stats || stats.calls === 0) {
recommendations.push({
server: mcp,
status: 'zero-usage',
advice: 'REMOVE',
reason: `过去 ${DAYS} 天零调用, 建议移除或转为按需模板`,
});
} else if (stats.calls <= 2 && stats.dates.size <= 1) {
recommendations.push({
server: mcp,
status: 'rare-usage',
advice: 'ON-DEMAND',
reason: `${stats.calls} 次调用, 建议改为按需启动`,
});
} else if (stats.calls >= 10 || stats.dates.size >= 3) {
recommendations.push({
server: mcp,
status: 'active',
advice: 'KEEP',
reason: `${stats.calls} 次调用, ${stats.dates.size} 天活跃, 保持全局常驻`,
});
} else {
recommendations.push({
server: mcp,
status: 'moderate',
advice: 'KEEP',
reason: `${stats.calls} 次调用, 使用频率中等`,
});
}
}
// 检测未注册但被调用的 MCP
for (const server of Object.keys(serverStats)) {
if (!registeredMcps.includes(server)) {
recommendations.push({
server,
status: 'unregistered',
advice: 'CHECK',
reason: `${serverStats[server].calls} 次调用但未在 settings.json 中注册`,
});
}
}
return {
period: `${DAYS} days`,
totalEvents: events.length,
totalMcpCalls,
registeredMcps: registeredMcps.length,
serverStats: Object.entries(serverStats).map(([server, s]) => ({
server,
calls: s.calls,
activeDays: s.dates.size,
lastUsed: s.lastUsed,
topTools: Object.entries(s.tools).sort((a, b) => b[1] - a[1]).slice(0, 5),
})).sort((a, b) => b.calls - a.calls),
recommendations: recommendations.sort((a, b) => {
const order = { REMOVE: 0, 'ON-DEMAND': 1, CHECK: 2, KEEP: 3 };
return (order[a.advice] ?? 9) - (order[b.advice] ?? 9);
}),
};
}
// === 输出 ===
function main() {
const report = analyze();
if (JSON_MODE) {
console.log(JSON.stringify(report, null, 2));
return;
}
console.log('=== MCP 使用分析报告 ===');
console.log(`分析期间: 最近 ${report.period} | 事件总数: ${report.totalEvents} | MCP 调用: ${report.totalMcpCalls}`);
console.log(`注册 MCP: ${report.registeredMcps}`);
console.log('');
// 使用统计
if (report.serverStats.length > 0) {
console.log('MCP 服务器调用统计:');
for (const s of report.serverStats) {
const tools = s.topTools.map(([t, c]) => `${t}(${c})`).join(', ');
console.log(` ${s.server.padEnd(25)} ${String(s.calls).padStart(4)} calls ${s.activeDays} days last: ${s.lastUsed}`);
if (tools) console.log(` ${''.padEnd(25)} tools: ${tools}`);
}
console.log('');
}
// 建议
console.log('生命周期建议:');
for (const r of report.recommendations) {
const icon = r.advice === 'REMOVE' ? 'X' : r.advice === 'ON-DEMAND' ? '~' : r.advice === 'CHECK' ? '?' : '+';
console.log(` [${icon}] ${r.server.padEnd(25)} ${r.advice.padEnd(10)} ${r.reason}`);
}
}
// 模块导出 (供测试使用)
if (typeof module !== 'undefined') {
module.exports = {
extractMcpServer,
loadRegisteredMcps,
loadActivityLogs,
analyze,
main,
};
}
if (require.main === module) {
main();
}