Files
everything-claude-code/scripts/mcp-inventory.js
Affaan Mustafa 7113b5bf63 feat: MCP inventory (ecc.mcp.v1) — unified cross-harness MCP config view (#2146)
* 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%.
2026-06-06 03:55:17 +08:00

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
};