214 lines
6.5 KiB
JavaScript
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();
|
|||
|
|
}
|