mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-11 02:33:10 +08:00
* feat: add MCP inventory (ecc.mcp.v1) across harnesses Read-only MCP-gateway groundwork: discover MCP server configs across every installed harness, normalize to a canonical ecc.mcp.v1 inventory, redact secrets, and report which servers are configured in 2+ harnesses (the configure-N-times pain). The read+dedup side of a unified gateway, mirroring how the session-adapter layer started read-only. Readers (per-harness config formats): - claude-code: ~/.claude.json mcpServers + project .mcp.json - codex: ~/.codex/config.toml [mcp_servers.*] TOML via @iarna/toml - opencode: ~/.config/opencode/opencode.json mcp block (command ARRAY) canonical-mcp.js: - normalize transport labels (local=>stdio, remote=>http) to stdio/http/sse - merge servers by name across harnesses; flag DRIFT when signatures differ - fragmentation report + aggregates - SECRET REDACTION: env values stripped to key names; secrets in args (--modelApiKey sk-ant-...), inline --flag=secret, and URL userinfo/token query params all redacted before storage AND before the dedup signature. scripts/mcp-inventory.js: CLI (--json, --fragmented, --help). tests/lib/mcp-inventory.test.js: 12 tests incl. a regression for the real arg-carried-secret leak found while smoke-testing on live configs. Tests: 12/0. Real-data smoke: 33 servers across 3 harnesses, 21 configured in 2+ harnesses (7 drift); secret-leak audit clean. * test: cover reader error paths, collect skip-logic, and CLI main() for mcp-inventory Lift global branch coverage past the 80% gate (was 79.86%). Adds 6 tests exercising: missing-file/malformed-JSON/missing-block reader fallbacks, codex no-parser path, collect skipping non-function readers and swallowing reader errors, CLI usage()/main() help+json+human paths, and formatHumanReport no-fragmentation + fragmented-only branches. Also scrub a real API-key fragment that had leaked into a test fixture; all secret-like fixtures are now obviously-fake FAKE... tokens. mcp-inventory.js branch 30%->93%, collect.js ->100%. Global branch 80.33%.
107 lines
3.0 KiB
JavaScript
Executable File
107 lines
3.0 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
const { collectMcpInventory } = require('./lib/mcp-inventory/collect');
|
|
|
|
function parseArgs(argv = process.argv) {
|
|
const args = argv.slice(2);
|
|
const options = { json: false, fragmentedOnly: false, help: false };
|
|
|
|
for (const arg of args) {
|
|
if (arg === '--json') {
|
|
options.json = true;
|
|
} else if (arg === '--fragmented' || arg === '--fragmented-only') {
|
|
options.fragmentedOnly = true;
|
|
} else if (arg === '--help' || arg === '-h') {
|
|
options.help = true;
|
|
}
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
function usage() {
|
|
return [
|
|
'Usage: mcp-inventory [options]',
|
|
'',
|
|
'Read MCP server configs across every installed harness (Claude Code,',
|
|
'Codex, OpenCode), normalize them to ecc.mcp.v1, and report which servers',
|
|
'are configured in more than one harness. Secrets are never printed; only',
|
|
'env key names are shown.',
|
|
'',
|
|
'Options:',
|
|
' --json Print the full ecc.mcp.v1 inventory as JSON',
|
|
' --fragmented Only show servers configured in 2+ harnesses',
|
|
' -h, --help Show this help'
|
|
].join('\n');
|
|
}
|
|
|
|
function formatHumanReport(inventory, options = {}) {
|
|
const lines = [];
|
|
const { aggregates, servers, fragmentation } = inventory;
|
|
|
|
lines.push('MCP Inventory (ecc.mcp.v1)');
|
|
lines.push(
|
|
` ${aggregates.serverCount} servers across ${aggregates.harnessCount} harnesses, `
|
|
+ `${aggregates.duplicateServerCount} configured in 2+ harnesses `
|
|
+ `(${aggregates.inconsistentServerCount} inconsistent), `
|
|
+ `${aggregates.serversWithSecrets} carry secrets`
|
|
);
|
|
lines.push('');
|
|
|
|
if (fragmentation.length > 0) {
|
|
lines.push('Fragmented servers (configure-once candidates):');
|
|
for (const item of fragmentation) {
|
|
const flag = item.consistent ? 'consistent' : 'DRIFT';
|
|
lines.push(` ${item.name} x${item.harnessCount} [${item.harnesses.join(', ')}] ${flag}`);
|
|
}
|
|
lines.push('');
|
|
} else {
|
|
lines.push('No servers are configured in more than one harness.');
|
|
lines.push('');
|
|
}
|
|
|
|
if (!options.fragmentedOnly) {
|
|
lines.push('All servers:');
|
|
for (const server of servers) {
|
|
const transport = server.transport === 'stdio'
|
|
? `stdio:${[server.command, ...server.args].filter(Boolean).join(' ')}`
|
|
: `${server.transport}:${server.url || ''}`;
|
|
const secretFlag = server.hasSecrets ? ' (secrets)' : '';
|
|
const disabledFlag = server.enabled ? '' : ' (disabled)';
|
|
lines.push(` ${server.name} -> ${transport}${secretFlag}${disabledFlag}`);
|
|
}
|
|
}
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
function main(argv = process.argv) {
|
|
const options = parseArgs(argv);
|
|
|
|
if (options.help) {
|
|
console.log(usage());
|
|
return;
|
|
}
|
|
|
|
const inventory = collectMcpInventory();
|
|
|
|
if (options.json) {
|
|
console.log(JSON.stringify(inventory, null, 2));
|
|
return;
|
|
}
|
|
|
|
console.log(formatHumanReport(inventory, options));
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main();
|
|
}
|
|
|
|
module.exports = {
|
|
parseArgs,
|
|
usage,
|
|
formatHumanReport,
|
|
main
|
|
};
|