bookworm-smart-assistant/scripts/mcp-prune.js
Bookworm Admin b7a8e29d21 release: v6.7.0 - OTA E2E test release
- VERSION file as authoritative version source
- export.mjs reads VERSION with package.json fallback
- bw-ota.ps1 DryRun mode for safe testing
- auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
2026-04-27 17:59:44 +08:00

180 lines
7.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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