bookworm-smart-assistant/scripts/mcp-prune.js

180 lines
7.0 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
'use strict';
/**
* /mcp-prune MCP (Phase 1 · T1.4)
* sentinel: PHASE1_T1_4_MCP_PRUNE_2026_04_24
*
* 职责:
* - 基于 mcp-usage-tracker 的数据识别低频 MCP
* - 交叉 mcp-critical-allowlist.json 保护救命 MCP
* - 生成剪枝 plan 文件 (JSON)
* - 输出用户手动 apply 的指令
*
* 用法:
* node scripts/mcp-prune.js # 默认: 只报告
* node scripts/mcp-prune.js --days 30 # 30 天窗口
* node scripts/mcp-prune.js --plan # 写入 mcp-prune-plan-<date>.json
* node scripts/mcp-prune.js --confirm # 输出用户 apply 的完整步骤
*
* 安全:
* - 永不修改 ~/.claude.json ()
* - 永不自动删除 MCP
* - --confirm 只打印操作指南用户自行执行
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
const HOME = process.env.USERPROFILE || process.env.HOME || os.homedir();
const CLAUDE_ROOT = process.env.CLAUDE_HOME ||
(fs.existsSync(path.join(HOME, '.claude')) ? path.join(HOME, '.claude') : HOME);
const TRACKER_PATH = path.join(CLAUDE_ROOT, 'scripts', 'mcp-usage-tracker.js');
const ALLOWLIST_FILE = path.join(CLAUDE_ROOT, 'mcp-critical-allowlist.json');
const GLOBAL_CONFIG = path.join(HOME, '.claude.json');
function parseArgs(argv) {
const args = { days: 30, plan: false, confirm: false };
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
if (a === '--days' && argv[i + 1]) { args.days = parseInt(argv[++i], 10) || 30; }
else if (a === '--plan') { args.plan = true; }
else if (a === '--confirm') { args.confirm = true; }
else if (a === '-h' || a === '--help') { args.help = true; }
}
return args;
}
function safeReadJson(file, fallback) {
try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
catch { return fallback; }
}
function runAnalysis(days) {
const tracker = require(TRACKER_PATH);
const allowlist = safeReadJson(ALLOWLIST_FILE, { critical: [] });
const criticalSet = new Set((allowlist.critical || []).map(c => c.name));
const cfg = safeReadJson(GLOBAL_CONFIG, { mcpServers: {} });
const allServers = Object.keys(cfg.mcpServers || {});
const events = tracker.collectMcpEvents(days);
const stats = tracker.aggregate(events, allServers);
const candidates = tracker.identifyPruneCandidates(stats, criticalSet);
return { stats, candidates, criticalSet, allServers, cfg, days, events };
}
function report(result) {
console.log('');
console.log('═══════════════════════════════════════════════════════════');
console.log(' /mcp-prune — 剪枝分析 · ' + result.days + '天窗口');
console.log('═══════════════════════════════════════════════════════════');
console.log('');
console.log('总 MCP 数: ' + result.allServers.length);
console.log('★critical (永不剪枝): ' + result.criticalSet.size);
console.log('活跃 MCP (>0 调用): ' + Object.values(result.stats).filter(s => s.totalCalls > 0).length);
console.log('剪枝候选 (0 调用): ' + result.candidates.length);
console.log('');
if (result.candidates.length === 0) {
console.log('✅ 没有剪枝候选。所有 MCP 都在最近 ' + result.days + ' 天被使用或在 critical 清单。');
return;
}
console.log('剪枝候选清单:');
console.log('─'.repeat(60));
for (const c of result.candidates) {
console.log(' - ' + c.server.padEnd(24) + ' ' + c.reason);
}
console.log('');
const estTokens = result.candidates.length * 200;
console.log('预估节省 (tokens/cold start): ≈ ' + estTokens + ' tokens');
console.log(' (每 MCP schema 均值 200 tokens × ' + result.candidates.length + ')');
}
function writePlan(result, planFile) {
const plan = {
schema_version: 1,
generated: new Date().toISOString(),
tool: 'mcp-prune',
windowDays: result.days,
totalMcps: result.allServers.length,
critical: Array.from(result.criticalSet),
candidates: result.candidates.map(c => ({
server: c.server,
reason: c.reason,
recommendedAction: 'REMOVE from ~/.claude.json mcpServers (manual)',
backupHint: 'cp ~/.claude.json ~/.claude.json.bak-before-prune-' + new Date().toISOString().slice(0, 10)
})),
note: '此 plan 仅供参考mcp-prune 绝不自动修改 .claude.json。用户需人工 apply。'
};
const tmp = planFile + '.tmp';
fs.writeFileSync(tmp, JSON.stringify(plan, null, 2), 'utf8');
fs.renameSync(tmp, planFile);
console.log('');
console.log('✅ Plan 写入: ' + planFile);
return plan;
}
function printApplyInstructions(plan) {
console.log('');
console.log('═══════════════════════════════════════════════════════════');
console.log(' 用户 Apply 步骤 (请人工执行)');
console.log('═══════════════════════════════════════════════════════════');
console.log('');
console.log('1. 备份原配置:');
console.log(' Copy-Item ~/.claude.json ~/.claude.json.bak-$(Get-Date -Format yyyy-MM-dd)');
console.log('');
console.log('2. 用编辑器打开 ~/.claude.json在 mcpServers 下删除以下键:');
for (const c of plan.candidates) {
console.log(' • ' + c.server);
}
console.log('');
console.log('3. 重启 Claude Code 使改动生效');
console.log('');
console.log('4. 若需恢复某个 MCP, 从备份文件找回对应 JSON 片段即可');
console.log('');
console.log('⚠ 本工具不会自动修改 .claude.json — 这是故意设计的安全边界。');
}
function main() {
const args = parseArgs(process.argv);
if (args.help) {
console.log('用法: node scripts/mcp-prune.js [--days N] [--plan] [--confirm]');
console.log(' 默认: 只报告');
console.log(' --plan: 生成 mcp-prune-plan-<date>.json');
console.log(' --confirm: 打印用户手动 apply 的完整步骤');
process.exit(0);
}
if (!fs.existsSync(TRACKER_PATH)) {
console.error('错误: mcp-usage-tracker.js 不存在,需先完成 Phase 1 T1.1');
process.exit(2);
}
const result = runAnalysis(args.days);
report(result);
if (result.candidates.length === 0) {
process.exit(0);
}
if (args.plan || args.confirm) {
const planFile = path.join(CLAUDE_ROOT, 'mcp-prune-plan-' + new Date().toISOString().slice(0, 10) + '.json');
const plan = writePlan(result, planFile);
if (args.confirm) {
printApplyInstructions(plan);
}
} else {
console.log('');
console.log('提示: 运行 \'--plan\' 生成 plan 文件, \'--confirm\' 查看完整 apply 步骤');
}
process.exit(0);
}
if (require.main === module) main();