#!/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(); }