Wave C step C3: closes E14 with the user-facing reset command. After a legitimate MCP server upgrade the sticky baseline (added in C1) becomes a stale "what the tool used to say" anchor and every subsequent post-mcp-verify advisory will re-flag the change. /security mcp-baseline-reset lets the user acknowledge the upgrade so the next call seeds a fresh baseline. New files: - scanners/mcp-baseline-reset.mjs — small CLI wrapper around clearBaseline / listBaselines. Modes: --list (read-only), --target <name>, no-args (all). Outputs JSON summary on stdout. Exit 0 always (idempotent). - commands/mcp-baseline-reset.md — dispatcher following mcp-inspect.md shape. Frontmatter: name=security:mcp-baseline-reset, sonnet model, Read/Bash/AskUserQuestion tools. 4-step body (list -> confirm scope -> execute -> confirm result). - tests/scanners/mcp-baseline-reset.test.mjs — 10 CLI tests across --list, --target, clear-all, idempotency, history preservation, and bare-positional sugar. Updated: - commands/security.md — new row in commands table after mcp-inspect. - CLAUDE.md — new commands-table row + new v7.3.0 narrative section describing the baseline schema, cumulative-drift detection, reset semantics, and the LLM_SECURITY_MCP_CACHE_FILE override. - Plugin README.md — new MCP-baseline-reset row in commands table, scanner count 12 standalone -> 13 standalone, new "MCP Description Drift (E14, v7.3.0)" subsection explaining the sticky baseline, cumulative threshold, reset semantics, and env-var override. - Root marketplace README.md — scanner count 22 -> 23 (10 orchestrated + 13 standalone), command count 19 -> 20, test count 1511 -> 1768. Wave C complete: 1738 -> 1768 tests (+30 across C1/C2/C3). Per plan, Wave C does NOT bump the plugin version — that lands at the wave-bundle release. The advisory text in post-mcp-verify already references the new command path so the user has a ready remediation step. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
101 lines
2.8 KiB
JavaScript
101 lines
2.8 KiB
JavaScript
#!/usr/bin/env node
|
|
// mcp-baseline-reset.mjs — Reset MCP description-cache baselines.
|
|
//
|
|
// Purpose:
|
|
// The description cache (scanners/lib/mcp-description-cache.mjs) anchors a
|
|
// sticky baseline per MCP tool so that cumulative drift can be detected
|
|
// across many small updates. After a legitimate MCP server upgrade the
|
|
// baseline becomes a stale "what the tool used to say" reference and must
|
|
// be reset so the next call seeds a fresh baseline.
|
|
//
|
|
// Modes:
|
|
// --list Read-only — list current baselines as JSON.
|
|
// --target <toolName> Clear baseline for one tool.
|
|
// (no args) Clear baselines for all tools.
|
|
//
|
|
// Output: JSON summary on stdout. Exit 0 always (idempotent).
|
|
//
|
|
// Used by /security mcp-baseline-reset slash command. Not part of
|
|
// scan-orchestrator.
|
|
|
|
import {
|
|
clearBaseline,
|
|
listBaselines,
|
|
loadCache,
|
|
} from './lib/mcp-description-cache.mjs';
|
|
|
|
function parseArgs(argv) {
|
|
const args = { list: false, target: null };
|
|
for (let i = 2; i < argv.length; i++) {
|
|
const a = argv[i];
|
|
if (a === '--list') {
|
|
args.list = true;
|
|
} else if (a === '--target' || a === '-t') {
|
|
args.target = argv[++i] || null;
|
|
} else if (a === '--help' || a === '-h') {
|
|
args.help = true;
|
|
} else if (!a.startsWith('--')) {
|
|
// bare positional treated as target for convenience
|
|
args.target = a;
|
|
}
|
|
}
|
|
return args;
|
|
}
|
|
|
|
function help() {
|
|
process.stdout.write(
|
|
'mcp-baseline-reset.mjs — Reset MCP description-cache baselines.\n\n' +
|
|
'Usage:\n' +
|
|
' node scanners/mcp-baseline-reset.mjs --list\n' +
|
|
' node scanners/mcp-baseline-reset.mjs --target <tool>\n' +
|
|
' node scanners/mcp-baseline-reset.mjs # clear all\n\n' +
|
|
'Output: JSON. Exit code 0 always.\n',
|
|
);
|
|
}
|
|
|
|
function emit(obj) {
|
|
process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
|
|
}
|
|
|
|
function main() {
|
|
const args = parseArgs(process.argv);
|
|
if (args.help) {
|
|
help();
|
|
return 0;
|
|
}
|
|
|
|
if (args.list) {
|
|
const baselines = listBaselines();
|
|
emit({
|
|
mode: 'list',
|
|
count: baselines.length,
|
|
baselines: baselines.map((b) => ({
|
|
tool: b.tool,
|
|
baseline_excerpt: (b.baseline || '').slice(0, 120),
|
|
seen_at: b.seenAt,
|
|
last_seen: b.lastSeen,
|
|
history_events: b.history,
|
|
})),
|
|
});
|
|
return 0;
|
|
}
|
|
|
|
// Reset path
|
|
const result = clearBaseline(args.target || undefined);
|
|
// After clearing, count remaining baselines
|
|
const cache = loadCache();
|
|
let remaining = 0;
|
|
for (const entry of Object.values(cache)) {
|
|
if (entry && entry.baseline) remaining++;
|
|
}
|
|
emit({
|
|
mode: 'reset',
|
|
target: args.target || null,
|
|
cleared: result.cleared,
|
|
tools: result.tools,
|
|
remaining,
|
|
});
|
|
return 0;
|
|
}
|
|
|
|
process.exit(main());
|