Sign In

@getmcpm/cli

Package Overview
Dependencies
Maintainers
1
Versions
46
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@getmcpm/cli - npm Package Compare versions

Comparing version
0.29.0
to
0.29.1
+455
dist/chunk-DR7HERUD.js
#!/usr/bin/env node
import {
coloredOutput
} from "./chunk-E3T224S3.js";
import {
isConfineBackendAvailable,
isWrapped
} from "./chunk-WYSMWP2R.js";
import {
sanitizeForTerminal
} from "./chunk-FEXJHHDM.js";
import {
getAdapter
} from "./chunk-W4IAFBUN.js";
import {
isSupportedPlatform,
parsePlaceholder
} from "./chunk-NPJ3SGGS.js";
import {
CLIENT_IDS,
getConfigPath
} from "./chunk-R4R2VPDA.js";
import {
detectSecretLabels
} from "./chunk-U7N6FRYF.js";
// src/utils/format-entry.ts
function formatMcpEntryCommand(entry, fallback = "\u2014") {
if (entry.url) return entry.url;
if (entry.command) {
const args = entry.args?.join(" ") ?? "";
return args ? `${entry.command} ${args}` : entry.command;
}
return fallback;
}
// src/commands/doctor.ts
import { access } from "fs/promises";
// src/config/drift.ts
async function collectClientStates(deps) {
const clients = await deps.detectClients();
const states = [];
for (const clientId of clients) {
try {
const servers = await deps.getAdapter(clientId).read(deps.getPath(clientId));
states.push({ clientId, servers });
} catch {
}
}
return states;
}
function fieldProjection(entry) {
return {
command: entry.command ?? "",
args: JSON.stringify(entry.args ?? []),
"env keys": JSON.stringify(Object.keys(entry.env ?? {}).sort()),
url: entry.url ?? "",
"header keys": JSON.stringify(Object.keys(entry.headers ?? {}).sort())
};
}
var COMPARED_FIELDS = ["command", "args", "env keys", "url", "header keys"];
function divergingFields(entries) {
const projections = entries.map(fieldProjection);
return COMPARED_FIELDS.filter((field) => {
const distinct = new Set(projections.map((p) => p[field]));
return distinct.size > 1;
});
}
function buildDriftModel(states) {
const clients = states.map((s) => s.clientId).sort();
const byName = /* @__PURE__ */ new Map();
for (const { clientId, servers: servers2 } of states) {
for (const [name, entry] of Object.entries(servers2)) {
const list = byName.get(name) ?? [];
list.push({ clientId, entry });
byName.set(name, list);
}
}
const servers = [];
for (const name of [...byName.keys()].sort()) {
const holders = byName.get(name);
const present = holders.map((h) => h.clientId).sort();
const presentSet = new Set(present);
const absent = clients.filter((c) => !presentSet.has(c));
const fields = holders.length > 1 ? divergingFields(holders.map((h) => h.entry)) : [];
const conflict = fields.length > 0;
servers.push({
name,
present,
absent,
conflict,
...conflict ? { conflictFields: fields } : {}
});
}
const drifted = servers.filter((s) => s.absent.length > 0 || s.conflict).length;
return { clients, servers, inSync: servers.length - drifted, drifted };
}
// src/scanner/config-secrets.ts
var GENERIC_LABEL = "secret-named key holds a plaintext value";
var SECRET_KEY_RE = /(?:^|_)(?:PASSWORD|PASSWD|PASSPHRASE|SECRET|TOKEN|PAT|APIKEY|AUTHORIZATION|CREDENTIALS?|(?:API|ACCESS|PRIVATE|SECRET|SESSION|SIGNING|ENCRYPTION)_KEY)(?:_|$)/;
var NON_SECRET_QUALIFIER_RE = /(?:^|_)(?:URL|URI|ENDPOINT|HOST|PORT|ID|NAME|PATH|FILE|DIR|ENABLED|DISABLED|TYPE|MODE|REGION|TIMEOUT|VERSION|PUBLIC|FORMAT|HEADER|PREFIX|SUFFIX|COUNT|SIZE|TTL|EXPIRY|EXPIRES|ISSUER|AUDIENCE|ALGORITHM|ALG|SCOPE|METHOD)(?:_|$)/;
function normalizeKey(key) {
return key.toUpperCase().replace(/-/g, "_");
}
function keyLooksSecret(key) {
const k = normalizeKey(key);
return SECRET_KEY_RE.test(k) && !NON_SECRET_QUALIFIER_RE.test(k);
}
function valueLooksPlaintextSecret(value) {
const v = value.trim();
if (v.length < 6) return false;
if (parsePlaceholder(value) !== null) return false;
if (/\$\{[^}]*\}/.test(v)) return false;
if (/^\$[A-Za-z_]/.test(v)) return false;
if (/^%[A-Za-z_][A-Za-z0-9_]*%([\\/].*)?$/.test(v)) return false;
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(v)) return false;
if (/^[~./]/.test(v) || /^[A-Za-z]:[\\/]/.test(v) || /^\\\\/.test(v)) return false;
if (/^(true|false|\d+)$/i.test(v)) return false;
return true;
}
function scanMap(server, field, map) {
if (!map) return [];
const out = [];
for (const [key, value] of Object.entries(map)) {
if (typeof value !== "string") continue;
if (parsePlaceholder(value) !== null) continue;
const labels = detectSecretLabels(value);
if (labels.length > 0) {
out.push({ server, field, key, label: labels.join(", ") });
continue;
}
if (keyLooksSecret(key) && valueLooksPlaintextSecret(value)) {
out.push({ server, field, key, label: GENERIC_LABEL });
}
}
return out;
}
function scanServerConfigSecrets(server, entry) {
return [...scanMap(server, "env", entry.env), ...scanMap(server, "header", entry.headers)];
}
function scanConfigSecrets(servers) {
return Object.entries(servers).flatMap(([name, entry]) => scanServerConfigSecrets(name, entry));
}
// src/commands/doctor.ts
import "commander";
import os from "os";
import { execFile } from "child_process";
var RUNTIMES = ["npx", "uvx", "docker"];
var CLIENT_LABELS = {
"claude-desktop": "Claude Desktop",
"claude-code": "Claude Code",
cursor: "Cursor",
vscode: "VS Code",
windsurf: "Windsurf",
"gemini-cli": "Gemini CLI"
};
var RUNTIME_INSTALL_HINTS = {
npx: "install Node.js from https://nodejs.org",
uvx: "install uv from https://docs.astral.sh/uv/",
docker: "install Docker from https://docs.docker.com/get-docker/"
};
async function buildDoctorModel(deps) {
const { getAdapter: getAdapter2, getConfigPath: getConfigPath2, checkConfigExists, execCheck } = deps;
const reads = await Promise.all(
CLIENT_IDS.map(async (clientId) => {
const exists = await checkConfigExists(clientId);
if (!exists) return { clientId, read: { exists: false, malformed: false, servers: null } };
try {
const servers = await getAdapter2(clientId).read(getConfigPath2(clientId));
return { clientId, read: { exists: true, malformed: false, servers } };
} catch {
return { clientId, read: { exists: true, malformed: true, servers: null } };
}
})
);
const issues = [];
const clients = reads.map(({ clientId, read }) => {
const label = CLIENT_LABELS[clientId];
if (read.malformed) {
issues.push({
kind: "malformed-config",
message: `Config file for ${label} is malformed \u2014 fix the JSON syntax.`
});
}
const servers = read.servers ?? {};
const entries = Object.values(servers);
return {
id: clientId,
label,
exists: read.exists,
malformed: read.malformed,
serverCount: entries.length,
guardedCount: entries.filter(isWrapped).length
};
});
const runtimes = await Promise.all(
RUNTIMES.map(async (name) => ({ name, available: await execCheck(name) }))
);
const runtimeAvailable = new Map(runtimes.map((r) => [r.name, r.available]));
for (const { clientId, read } of reads) {
if (!read.servers) continue;
for (const [serverName, entry] of Object.entries(read.servers)) {
const cmd = entry.command;
if (!cmd) continue;
if (RUNTIMES.includes(cmd) && runtimeAvailable.get(cmd) === false) {
issues.push({
kind: "missing-runtime",
message: `Server '${serverName}' in ${CLIENT_LABELS[clientId]} uses '${cmd}' but ${cmd} is not installed.`
});
}
}
}
const driftStates = reads.flatMap(
({ clientId, read }) => read.servers ? [{ clientId, servers: read.servers }] : []
);
const crossClient = driftStates.length >= 2 ? toCrossClient(driftStates) : null;
const secrets = reads.flatMap(
({ clientId, read }) => read.servers ? scanConfigSecrets(read.servers).map((f) => ({ client: clientId, ...f })) : []
);
return {
schemaVersion: 1,
clients,
runtimes,
crossClient,
secrets,
issues,
ok: issues.length === 0
};
}
function toCrossClient(states) {
const drift = buildDriftModel(states);
const entries = [];
for (const server of drift.servers) {
if (server.conflict) {
entries.push({
name: server.name,
kind: "conflict",
present: [...server.present],
absent: [...server.absent],
fields: server.conflictFields ? [...server.conflictFields] : void 0
});
} else if (server.absent.length > 0) {
entries.push({
name: server.name,
kind: "absent",
present: [...server.present],
absent: [...server.absent]
});
}
}
return {
consistent: drift.drifted === 0,
clientCount: drift.clients.length,
serverCount: drift.servers.length,
drift: entries
};
}
function renderDoctorText(model, output) {
output("");
output("mcpm doctor");
output("");
for (const c of model.clients) {
if (!c.exists) {
output(` \u2717 ${c.label} \u2014 config not found`);
} else if (c.malformed) {
output(` \u2717 ${c.label} \u2014 config malformed (JSON parse error)`);
} else {
const word = c.serverCount === 1 ? "server" : "servers";
output(` \u2713 ${c.label} \u2014 config found, ${c.serverCount} ${word}`);
}
}
output("");
output("Runtimes:");
for (const r of model.runtimes) {
if (r.available) {
output(` \u2713 ${r.name} available`);
} else {
output(` \u2717 ${r.name} not found \u2014 ${RUNTIME_INSTALL_HINTS[r.name]}`);
}
}
if (model.crossClient) {
const cc = model.crossClient;
output("");
output("Cross-client (advisory):");
if (cc.consistent) {
const word = cc.serverCount === 1 ? "server" : "servers";
output(` \u2713 ${cc.serverCount} ${word} consistent across ${cc.clientCount} clients`);
} else {
for (const d of cc.drift) {
if (d.kind === "conflict") {
output(` \u26A0 ${d.name} \u2014 config differs (${d.fields.join(", ")}) across ${d.present.join(", ")}`);
} else {
output(` \u26A0 ${d.name} \u2014 in ${d.present.join(", ")}; missing in ${d.absent.join(", ")}`);
}
}
output(" Run `mcpm sync --check` for the full matrix (advisory, not a failure).");
}
}
if (model.secrets.length > 0) {
output("");
output("Plaintext secrets (advisory):");
for (const s of model.secrets) {
output(
` \u26A0 ${s.client} \xB7 ${sanitizeForTerminal(s.server)} \xB7 ${s.field} '${sanitizeForTerminal(s.key)}' \u2014 ${s.label}`
);
}
if (model.secrets.some((s) => s.field === "env")) {
output(
" Move env secrets to the encrypted store: `mcpm secrets set <server> <KEY>` or re-install with `--secrets keychain`."
);
}
if (model.secrets.some((s) => s.field === "header")) {
output(
" Header secrets have no keychain path yet \u2014 rotate the credential and keep it out of committed config."
);
}
}
if (model.issues.length > 0) {
output("");
output("Issues:");
for (const issue of model.issues) {
output(` \u26A0 ${issue.message}`);
}
output("");
output("Critical issues found. Run the commands above to resolve them.");
return;
}
output("");
output("No critical issues found.");
}
function buildDoctorReport(model, env) {
return {
schemaVersion: 1,
mcpm: env.mcpm,
node: env.node,
os: `${env.platform} ${env.arch} ${env.osRelease}`,
confineBackend: env.confineBackend,
secretStore: env.secretStore,
// Redaction: drop the label + every server name; keep only counts.
clients: model.clients.map(({ id, exists, malformed, serverCount, guardedCount }) => ({
id,
exists,
malformed,
serverCount,
guardedCount
})),
runtimes: model.runtimes,
issues: {
malformedConfigs: model.issues.filter((i) => i.kind === "malformed-config").length,
missingRuntime: model.issues.filter((i) => i.kind === "missing-runtime").length,
plaintextSecrets: model.secrets.length
}
};
}
function renderReportText(r) {
const lines = [];
lines.push("mcpm doctor --report (redacted \u2014 no server names or args)");
lines.push(`mcpm: ${r.mcpm}`);
lines.push(`node: ${r.node}`);
lines.push(`os: ${r.os}`);
lines.push(`confine backend: ${r.confineBackend ? "available" : "unavailable"}`);
lines.push(`secret store: ${r.secretStore}`);
lines.push("");
lines.push("clients:");
for (const c of r.clients) {
if (!c.exists) {
lines.push(` ${c.id}: not found`);
} else if (c.malformed) {
lines.push(` ${c.id}: config malformed`);
} else {
const guarded = c.guardedCount > 0 ? `, ${c.guardedCount} guarded` : "";
lines.push(` ${c.id}: ${c.serverCount} servers${guarded}`);
}
}
lines.push("runtimes:");
for (const rt of r.runtimes) {
lines.push(` ${rt.name}: ${rt.available ? "available" : "missing"}`);
}
lines.push(
`issues: ${r.issues.malformedConfigs} malformed config(s), ${r.issues.missingRuntime} missing-runtime, ${r.issues.plaintextSecrets} plaintext secret(s)`
);
return lines.join("\n");
}
async function doctorHandler(deps, opts = {}) {
const model = await buildDoctorModel(deps);
if (opts.report) {
const env = opts.reportEnv ?? gatherReportEnv();
deps.output(renderReportText(buildDoctorReport(model, env)));
} else if (opts.json) {
deps.output(JSON.stringify(model, null, 2));
} else {
renderDoctorText(model, deps.output);
}
return model.ok ? 0 : 1;
}
function makeCheckConfigExists(getConfigPathFn) {
return async (clientId) => {
try {
await access(getConfigPathFn(clientId));
return true;
} catch {
return false;
}
};
}
var checkConfigExistsDefault = makeCheckConfigExists(getConfigPath);
var ALLOWED_RUNTIME_CMDS = /* @__PURE__ */ new Set(["npx", "uvx", "docker"]);
function execCheckDefault(cmd) {
if (!ALLOWED_RUNTIME_CMDS.has(cmd)) return Promise.resolve(false);
return new Promise((resolve) => {
const which = process.platform === "win32" ? "where" : "which";
execFile(which, [cmd], (err) => {
resolve(err === null);
});
});
}
function gatherReportEnv() {
return {
mcpm: "0.29.1",
node: process.version,
platform: process.platform,
arch: process.arch,
osRelease: os.release(),
confineBackend: isConfineBackendAvailable(),
secretStore: isSupportedPlatform() ? "os-keychain" : "machine-key"
};
}
function registerDoctorCommand(program) {
program.command("doctor").description("Check MCP setup health and report issues").option("--json", "emit the structured DoctorModel as JSON (shape UNSTABLE; NOT redacted \u2014 includes server names, use --report to share publicly)").option("--report", "emit a redacted, pasteable env snapshot for bug reports (no server names/args)").action(async (options) => {
const plain = options.json || options.report;
const deps = {
getAdapter,
getConfigPath,
checkConfigExists: checkConfigExistsDefault,
execCheck: execCheckDefault,
output: plain ? (t) => console.log(t) : coloredOutput
};
const exitCode = await doctorHandler(deps, { json: options.json, report: options.report });
process.exit(exitCode);
});
}
export {
formatMcpEntryCommand,
collectClientStates,
buildDriftModel,
buildDoctorModel,
makeCheckConfigExists,
execCheckDefault,
registerDoctorCommand
};
//# sourceMappingURL=chunk-DR7HERUD.js.map
{"version":3,"sources":["../src/utils/format-entry.ts","../src/commands/doctor.ts","../src/config/drift.ts","../src/scanner/config-secrets.ts"],"sourcesContent":["/**\n * Shared formatting helpers for McpServerEntry display.\n */\n\nimport type { McpServerEntry } from \"../config/adapters/index.js\";\n\n/**\n * Returns the display string for an MCP server entry's command/URL column.\n *\n * @param entry - The server entry to format.\n * @param fallback - String to return when neither url nor command is present.\n */\nexport function formatMcpEntryCommand(\n entry: McpServerEntry,\n fallback = \"\\u2014\"\n): string {\n if (entry.url) return entry.url;\n if (entry.command) {\n const args = entry.args?.join(\" \") ?? \"\";\n return args ? `${entry.command} ${args}` : entry.command;\n }\n return fallback;\n}\n","/**\n * `mcpm doctor` command handler.\n *\n * Checks MCP setup health and reports issues:\n * - Which AI clients have config files\n * - Whether config files are valid JSON\n * - Which runtimes (npx, uvx, docker) are available\n * - Whether installed servers reference available runtimes\n *\n * Returns 0 for no critical issues, 1 for critical issues.\n * All external dependencies are injected for testability.\n *\n * D7: the check logic is split into a pure `buildDoctorModel` (a structured\n * `DoctorModel`) and renderers. `--json` emits the model; `--report` emits a\n * redacted, name-free env snapshot for bug reports; the MCP-server `handleDoctor`\n * reuses the same model (fixing its formerly-hardcoded `issues: []`).\n */\n\nimport { access } from \"fs/promises\";\nimport type { ClientId } from \"../config/paths.js\";\nimport type { ConfigAdapter, McpServerEntry } from \"../config/adapters/index.js\";\nimport type { getConfigPath } from \"../config/paths.js\";\nimport { buildDriftModel, type ClientState } from \"../config/drift.js\";\nimport { isWrapped } from \"../guard/wrap.js\";\nimport { scanConfigSecrets, type ConfigSecretFinding } from \"../scanner/config-secrets.js\";\nimport { sanitizeForTerminal } from \"../guard/sanitize.js\";\n\n// ---------------------------------------------------------------------------\n// Deps interface\n// ---------------------------------------------------------------------------\n\nexport interface DoctorDeps {\n getAdapter: (clientId: ClientId) => ConfigAdapter;\n getConfigPath: typeof getConfigPath;\n /** Returns true if the config file exists for this client. */\n checkConfigExists: (clientId: ClientId) => Promise<boolean>;\n /** Returns true if the given executable is available on PATH. */\n execCheck: (cmd: string) => Promise<boolean>;\n output: (text: string) => void;\n}\n\n/** The subset of deps the pure model builder needs (no output, no detector). */\nexport type DoctorModelDeps = Pick<\n DoctorDeps,\n \"getAdapter\" | \"getConfigPath\" | \"checkConfigExists\" | \"execCheck\"\n>;\n\n// ---------------------------------------------------------------------------\n// Structured model (D7 — one shape for text/json/report/MCP consumers)\n// ---------------------------------------------------------------------------\n\nexport interface DoctorClientHealth {\n id: ClientId;\n label: string;\n exists: boolean;\n malformed: boolean;\n serverCount: number;\n /** Servers wrapped by the guard relay (subset of serverCount). */\n guardedCount: number;\n}\n\nexport interface DoctorRuntimeHealth {\n name: Runtime;\n available: boolean;\n}\n\nexport interface DoctorDriftEntry {\n name: string;\n kind: \"conflict\" | \"absent\";\n present: string[];\n absent: string[];\n /** Present only for `kind: \"conflict\"`. */\n fields?: string[];\n}\n\nexport interface DoctorCrossClient {\n consistent: boolean;\n clientCount: number;\n serverCount: number;\n drift: DoctorDriftEntry[];\n}\n\nexport interface DoctorIssue {\n kind: \"malformed-config\" | \"missing-runtime\";\n message: string;\n}\n\nexport interface DoctorSecretFinding {\n client: ClientId;\n server: string;\n field: ConfigSecretFinding[\"field\"];\n /** The env var / header NAME — never the value (F9 redaction contract). */\n key: string;\n label: string;\n}\n\nexport interface DoctorModel {\n schemaVersion: 1;\n clients: DoctorClientHealth[];\n runtimes: DoctorRuntimeHealth[];\n /** Advisory cross-client consistency; null when <2 clients have a readable config. */\n crossClient: DoctorCrossClient | null;\n /** Plaintext secrets in client config — advisory (F9); does NOT affect `ok`/exit. */\n secrets: DoctorSecretFinding[];\n /** Critical issues — these drive the exit code. */\n issues: DoctorIssue[];\n /** true iff issues is empty. */\n ok: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst RUNTIMES = [\"npx\", \"uvx\", \"docker\"] as const;\n\ntype Runtime = (typeof RUNTIMES)[number];\n\nconst CLIENT_LABELS: Record<ClientId, string> = {\n \"claude-desktop\": \"Claude Desktop\",\n \"claude-code\": \"Claude Code\",\n cursor: \"Cursor\",\n vscode: \"VS Code\",\n windsurf: \"Windsurf\",\n \"gemini-cli\": \"Gemini CLI\",\n};\n\nconst RUNTIME_INSTALL_HINTS: Record<Runtime, string> = {\n npx: \"install Node.js from https://nodejs.org\",\n uvx: \"install uv from https://docs.astral.sh/uv/\",\n docker: \"install Docker from https://docs.docker.com/get-docker/\",\n};\n\n// ---------------------------------------------------------------------------\n// Model builder (pure — no output)\n// ---------------------------------------------------------------------------\n\ninterface ClientRead {\n exists: boolean;\n malformed: boolean;\n servers: Record<string, McpServerEntry> | null;\n}\n\n/**\n * Runs every health check and returns the structured model. No side effects\n * beyond the injected reads; safe to call from the CLI, `--json`, `--report`,\n * and the MCP `handleDoctor` tool.\n */\nexport async function buildDoctorModel(deps: DoctorModelDeps): Promise<DoctorModel> {\n const { getAdapter, getConfigPath, checkConfigExists, execCheck } = deps;\n\n // 1. Read each known client's config.\n const reads = await Promise.all(\n CLIENT_IDS.map(async (clientId): Promise<{ clientId: ClientId; read: ClientRead }> => {\n const exists = await checkConfigExists(clientId);\n if (!exists) return { clientId, read: { exists: false, malformed: false, servers: null } };\n try {\n const servers = await getAdapter(clientId).read(getConfigPath(clientId));\n return { clientId, read: { exists: true, malformed: false, servers } };\n } catch {\n return { clientId, read: { exists: true, malformed: true, servers: null } };\n }\n })\n );\n\n const issues: DoctorIssue[] = [];\n\n const clients: DoctorClientHealth[] = reads.map(({ clientId, read }) => {\n const label = CLIENT_LABELS[clientId];\n if (read.malformed) {\n issues.push({\n kind: \"malformed-config\",\n message: `Config file for ${label} is malformed — fix the JSON syntax.`,\n });\n }\n const servers = read.servers ?? {};\n const entries = Object.values(servers);\n return {\n id: clientId,\n label,\n exists: read.exists,\n malformed: read.malformed,\n serverCount: entries.length,\n guardedCount: entries.filter(isWrapped).length,\n };\n });\n\n // 2. Runtime availability.\n const runtimes: DoctorRuntimeHealth[] = await Promise.all(\n RUNTIMES.map(async (name) => ({ name, available: await execCheck(name) }))\n );\n const runtimeAvailable = new Map(runtimes.map((r) => [r.name as string, r.available]));\n\n // 3. Cross-check: servers whose command is a tracked-but-unavailable runtime.\n for (const { clientId, read } of reads) {\n if (!read.servers) continue;\n for (const [serverName, entry] of Object.entries(read.servers)) {\n const cmd = entry.command;\n if (!cmd) continue; // HTTP/URL server — no runtime needed.\n if (RUNTIMES.includes(cmd as Runtime) && runtimeAvailable.get(cmd) === false) {\n issues.push({\n kind: \"missing-runtime\",\n message: `Server '${serverName}' in ${CLIENT_LABELS[clientId]} uses '${cmd}' but ${cmd} is not installed.`,\n });\n }\n }\n }\n\n // 4. Cross-client consistency (advisory — never an issue, never fails doctor).\n const driftStates: ClientState[] = reads.flatMap(({ clientId, read }) =>\n read.servers ? [{ clientId, servers: read.servers }] : []\n );\n const crossClient = driftStates.length >= 2 ? toCrossClient(driftStates) : null;\n\n // 5. Plaintext-secret scan (advisory — never an issue, never fails doctor).\n const secrets: DoctorSecretFinding[] = reads.flatMap(({ clientId, read }) =>\n read.servers ? scanConfigSecrets(read.servers).map((f) => ({ client: clientId, ...f })) : []\n );\n\n return {\n schemaVersion: 1,\n clients,\n runtimes,\n crossClient,\n secrets,\n issues,\n ok: issues.length === 0,\n };\n}\n\nfunction toCrossClient(states: ClientState[]): DoctorCrossClient {\n const drift = buildDriftModel(states);\n const entries: DoctorDriftEntry[] = [];\n for (const server of drift.servers) {\n // buildDriftModel returns readonly arrays — copy into the mutable public model.\n if (server.conflict) {\n entries.push({\n name: server.name,\n kind: \"conflict\",\n present: [...server.present],\n absent: [...server.absent],\n fields: server.conflictFields ? [...server.conflictFields] : undefined,\n });\n } else if (server.absent.length > 0) {\n entries.push({\n name: server.name,\n kind: \"absent\",\n present: [...server.present],\n absent: [...server.absent],\n });\n }\n }\n return {\n consistent: drift.drifted === 0,\n clientCount: drift.clients.length,\n serverCount: drift.servers.length,\n drift: entries,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Human-readable renderer (byte-identical to the pre-D7 output)\n// ---------------------------------------------------------------------------\n\nexport function renderDoctorText(model: DoctorModel, output: (text: string) => void): void {\n output(\"\");\n output(\"mcpm doctor\");\n output(\"\");\n\n for (const c of model.clients) {\n if (!c.exists) {\n output(` ✗ ${c.label} — config not found`);\n } else if (c.malformed) {\n output(` ✗ ${c.label} — config malformed (JSON parse error)`);\n } else {\n const word = c.serverCount === 1 ? \"server\" : \"servers\";\n output(` ✓ ${c.label} — config found, ${c.serverCount} ${word}`);\n }\n }\n\n output(\"\");\n output(\"Runtimes:\");\n for (const r of model.runtimes) {\n if (r.available) {\n output(` ✓ ${r.name} available`);\n } else {\n output(` ✗ ${r.name} not found — ${RUNTIME_INSTALL_HINTS[r.name]}`);\n }\n }\n\n if (model.crossClient) {\n const cc = model.crossClient;\n output(\"\");\n output(\"Cross-client (advisory):\");\n if (cc.consistent) {\n const word = cc.serverCount === 1 ? \"server\" : \"servers\";\n output(` ✓ ${cc.serverCount} ${word} consistent across ${cc.clientCount} clients`);\n } else {\n for (const d of cc.drift) {\n if (d.kind === \"conflict\") {\n output(` ⚠ ${d.name} — config differs (${d.fields!.join(\", \")}) across ${d.present.join(\", \")}`);\n } else {\n output(` ⚠ ${d.name} — in ${d.present.join(\", \")}; missing in ${d.absent.join(\", \")}`);\n }\n }\n output(\" Run `mcpm sync --check` for the full matrix (advisory, not a failure).\");\n }\n }\n\n if (model.secrets.length > 0) {\n output(\"\");\n output(\"Plaintext secrets (advisory):\");\n for (const s of model.secrets) {\n // s.server / s.key are attacker-influenceable (registry env-var names, imported\n // configs) — strip ANSI/OSC so a crafted key can't erase or spoof the advisory.\n output(\n ` ⚠ ${s.client} · ${sanitizeForTerminal(s.server)} · ${s.field} '${sanitizeForTerminal(s.key)}' — ${s.label}`\n );\n }\n // Remediation is field-specific: the keychain/placeholder path is env-only\n // (guard resolves placeholders in env, not headers; HTTP servers aren't wrapped).\n if (model.secrets.some((s) => s.field === \"env\")) {\n output(\n \" Move env secrets to the encrypted store: `mcpm secrets set <server> <KEY>` or re-install with `--secrets keychain`.\"\n );\n }\n if (model.secrets.some((s) => s.field === \"header\")) {\n output(\n \" Header secrets have no keychain path yet — rotate the credential and keep it out of committed config.\"\n );\n }\n }\n\n if (model.issues.length > 0) {\n output(\"\");\n output(\"Issues:\");\n for (const issue of model.issues) {\n output(` ⚠ ${issue.message}`);\n }\n output(\"\");\n output(\"Critical issues found. Run the commands above to resolve them.\");\n return;\n }\n\n output(\"\");\n output(\"No critical issues found.\");\n}\n\n// ---------------------------------------------------------------------------\n// Redacted report (D7 — pasteable env snapshot, NO server names/args)\n// ---------------------------------------------------------------------------\n\nexport interface DoctorReportEnv {\n mcpm: string;\n node: string;\n platform: string;\n arch: string;\n osRelease: string;\n confineBackend: boolean;\n secretStore: \"os-keychain\" | \"machine-key\";\n}\n\nexport interface DoctorReport {\n schemaVersion: 1;\n mcpm: string;\n node: string;\n os: string;\n confineBackend: boolean;\n secretStore: \"os-keychain\" | \"machine-key\";\n clients: Array<Omit<DoctorClientHealth, \"label\">>;\n runtimes: DoctorRuntimeHealth[];\n /** Counts only — issue messages + secret keys embed server names, so NOT included. */\n issues: { malformedConfigs: number; missingRuntime: number; plaintextSecrets: number };\n}\n\nexport function buildDoctorReport(model: DoctorModel, env: DoctorReportEnv): DoctorReport {\n return {\n schemaVersion: 1,\n mcpm: env.mcpm,\n node: env.node,\n os: `${env.platform} ${env.arch} ${env.osRelease}`,\n confineBackend: env.confineBackend,\n secretStore: env.secretStore,\n // Redaction: drop the label + every server name; keep only counts.\n clients: model.clients.map(({ id, exists, malformed, serverCount, guardedCount }) => ({\n id,\n exists,\n malformed,\n serverCount,\n guardedCount,\n })),\n runtimes: model.runtimes,\n issues: {\n malformedConfigs: model.issues.filter((i) => i.kind === \"malformed-config\").length,\n missingRuntime: model.issues.filter((i) => i.kind === \"missing-runtime\").length,\n plaintextSecrets: model.secrets.length,\n },\n };\n}\n\nexport function renderReportText(r: DoctorReport): string {\n const lines: string[] = [];\n lines.push(\"mcpm doctor --report (redacted — no server names or args)\");\n lines.push(`mcpm: ${r.mcpm}`);\n lines.push(`node: ${r.node}`);\n lines.push(`os: ${r.os}`);\n lines.push(`confine backend: ${r.confineBackend ? \"available\" : \"unavailable\"}`);\n lines.push(`secret store: ${r.secretStore}`);\n lines.push(\"\");\n lines.push(\"clients:\");\n for (const c of r.clients) {\n if (!c.exists) {\n lines.push(` ${c.id}: not found`);\n } else if (c.malformed) {\n lines.push(` ${c.id}: config malformed`);\n } else {\n const guarded = c.guardedCount > 0 ? `, ${c.guardedCount} guarded` : \"\";\n lines.push(` ${c.id}: ${c.serverCount} servers${guarded}`);\n }\n }\n lines.push(\"runtimes:\");\n for (const rt of r.runtimes) {\n lines.push(` ${rt.name}: ${rt.available ? \"available\" : \"missing\"}`);\n }\n lines.push(\n `issues: ${r.issues.malformedConfigs} malformed config(s), ${r.issues.missingRuntime} missing-runtime, ${r.issues.plaintextSecrets} plaintext secret(s)`\n );\n return lines.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Handler\n// ---------------------------------------------------------------------------\n\nexport interface DoctorOpts {\n json?: boolean;\n report?: boolean;\n /** Injected in --report mode; the Commander action supplies the real env. */\n reportEnv?: DoctorReportEnv;\n}\n\n/**\n * Core logic for `mcpm doctor`.\n * @returns Exit code: 0 = healthy, 1 = critical issues found.\n */\nexport async function doctorHandler(deps: DoctorDeps, opts: DoctorOpts = {}): Promise<number> {\n const model = await buildDoctorModel(deps);\n\n if (opts.report) {\n const env = opts.reportEnv ?? gatherReportEnv();\n deps.output(renderReportText(buildDoctorReport(model, env)));\n } else if (opts.json) {\n deps.output(JSON.stringify(model, null, 2));\n } else {\n renderDoctorText(model, deps.output);\n }\n\n return model.ok ? 0 : 1;\n}\n\n// ---------------------------------------------------------------------------\n// Commander registration\n// ---------------------------------------------------------------------------\n\nimport { Command } from \"commander\";\nimport os from \"os\";\nimport { execFile } from \"child_process\";\nimport { getConfigPath as _getConfigPath, CLIENT_IDS } from \"../config/paths.js\";\nimport { getAdapter as getAdapterDefault } from \"../config/index.js\";\nimport { coloredOutput } from \"../utils/output.js\";\nimport { isConfineBackendAvailable } from \"../guard/confine/apply.js\";\nimport { isSupportedPlatform as isKeychainSupported } from \"../store/os-keychain.js\";\n\n/** Factory so callers that inject a custom getConfigPath (e.g. the MCP server) get honored. */\nexport function makeCheckConfigExists(\n getConfigPathFn: (clientId: ClientId) => string\n): (clientId: ClientId) => Promise<boolean> {\n return async (clientId: ClientId): Promise<boolean> => {\n try {\n await access(getConfigPathFn(clientId));\n return true;\n } catch {\n return false;\n }\n };\n}\n\nconst checkConfigExistsDefault = makeCheckConfigExists(_getConfigPath);\n\nconst ALLOWED_RUNTIME_CMDS = new Set<string>([\"npx\", \"uvx\", \"docker\"]);\n\nexport function execCheckDefault(cmd: string): Promise<boolean> {\n if (!ALLOWED_RUNTIME_CMDS.has(cmd)) return Promise.resolve(false);\n return new Promise((resolve) => {\n const which = process.platform === \"win32\" ? \"where\" : \"which\";\n execFile(which, [cmd], (err) => {\n resolve(err === null);\n });\n });\n}\n\n/** Gathers the impure environment fields for `--report`. */\nfunction gatherReportEnv(): DoctorReportEnv {\n return {\n mcpm: __PKG_VERSION__,\n node: process.version,\n platform: process.platform,\n arch: process.arch,\n osRelease: os.release(),\n confineBackend: isConfineBackendAvailable(),\n secretStore: isKeychainSupported() ? \"os-keychain\" : \"machine-key\",\n };\n}\n\nexport function registerDoctorCommand(program: Command): void {\n program\n .command(\"doctor\")\n .description(\"Check MCP setup health and report issues\")\n .option(\"--json\", \"emit the structured DoctorModel as JSON (shape UNSTABLE; NOT redacted — includes server names, use --report to share publicly)\")\n .option(\"--report\", \"emit a redacted, pasteable env snapshot for bug reports (no server names/args)\")\n .action(async (options: { json?: boolean; report?: boolean }) => {\n // --json / --report are machine/paste output — never colorize.\n const plain = options.json || options.report;\n const deps: DoctorDeps = {\n getAdapter: getAdapterDefault,\n getConfigPath: _getConfigPath,\n checkConfigExists: checkConfigExistsDefault,\n execCheck: execCheckDefault,\n output: plain ? (t) => console.log(t) : coloredOutput,\n };\n\n const exitCode = await doctorHandler(deps, { json: options.json, report: options.report });\n process.exit(exitCode);\n });\n}\n","/**\n * Cross-client config-drift model (pure, injectable).\n *\n * `mcpm diff` answers \"installed vs declared stack\" in ONE direction. This module\n * answers the symmetric N-client question: for every server name, which clients\n * have it, which are missing it, and do the clients that DO have it agree on the\n * server's shape? It is the shared core behind `mcpm sync --check` and the doctor\n * \"Cross-client\" section.\n *\n * Design notes:\n * - Read-only. No writes, no registry/lock/network — it only reads client configs\n * (the collect loop mirrors diff.ts:76-93 / export.ts).\n * - `buildDriftModel` is pure and takes already-collected `ClientState[]` so the\n * doctor command can feed it the reads it already did (no double I/O).\n * - Conflict comparison is over command + ordered args + env KEY set + url +\n * header KEY set. It NEVER compares env / header VALUES — those are secrets, and\n * two clients legitimately hold the same key with a per-machine value.\n *\n * Exports: DriftDeps, ClientState, ServerDrift, DriftModel, collectClientStates,\n * buildDriftModel.\n */\n\nimport type { ClientId } from \"./paths.js\";\nimport type { ConfigAdapter, McpServerEntry } from \"./adapters/index.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface DriftDeps {\n detectClients: () => Promise<ClientId[]>;\n getAdapter: (clientId: ClientId) => Pick<ConfigAdapter, \"read\">;\n getPath: (clientId: ClientId) => string;\n}\n\n/** A single client's full set of MCP server entries (one successful read). */\nexport interface ClientState {\n readonly clientId: ClientId;\n readonly servers: Record<string, McpServerEntry>;\n}\n\nexport interface ServerDrift {\n readonly name: string;\n /** Clients (with readable configs) that declare this server. */\n readonly present: readonly ClientId[];\n /** Clients (with readable configs) that lack this server. */\n readonly absent: readonly ClientId[];\n /** True when the `present` clients disagree on the server's shape. */\n readonly conflict: boolean;\n /** Which fields diverge among the `present` clients (only when conflict). */\n readonly conflictFields?: readonly string[];\n}\n\nexport interface DriftModel {\n /** Clients considered — those whose config was readable. Sorted. */\n readonly clients: readonly ClientId[];\n /** One entry per distinct server name, sorted by name. */\n readonly servers: readonly ServerDrift[];\n /** Servers present in every considered client with no shape conflict. */\n readonly inSync: number;\n /** Servers with at least one absence or a shape conflict. */\n readonly drifted: number;\n}\n\n// ---------------------------------------------------------------------------\n// Collection (I/O)\n// ---------------------------------------------------------------------------\n\n/**\n * Read each detected client's config into a `ClientState`. Clients whose config\n * is unreadable (missing / malformed) are skipped — never throws — so a single\n * broken config can't blind the whole cross-client view (same posture as\n * `diff` / `export`).\n */\nexport async function collectClientStates(deps: DriftDeps): Promise<ClientState[]> {\n const clients = await deps.detectClients();\n const states: ClientState[] = [];\n for (const clientId of clients) {\n try {\n const servers = await deps.getAdapter(clientId).read(deps.getPath(clientId));\n states.push({ clientId, servers });\n } catch {\n // Skip unreadable clients (missing or malformed config).\n }\n }\n return states;\n}\n\n// ---------------------------------------------------------------------------\n// Drift model (pure)\n// ---------------------------------------------------------------------------\n\n/**\n * Per-field canonical projection used for conflict detection. Each value is a\n * stable string; two entries conflict on a field iff their projected strings\n * differ. Deliberately excludes env / header VALUES (secrets) and the per-client\n * `disabled` flag (an intentional per-client toggle, not a definition drift).\n */\nfunction fieldProjection(entry: McpServerEntry): Record<string, string> {\n return {\n command: entry.command ?? \"\",\n args: JSON.stringify(entry.args ?? []),\n \"env keys\": JSON.stringify(Object.keys(entry.env ?? {}).sort()),\n url: entry.url ?? \"\",\n \"header keys\": JSON.stringify(Object.keys(entry.headers ?? {}).sort()),\n };\n}\n\nconst COMPARED_FIELDS = [\"command\", \"args\", \"env keys\", \"url\", \"header keys\"] as const;\n\n/** Fields on which the given entries (≥1) disagree. Empty ⇒ all identical. */\nfunction divergingFields(entries: readonly McpServerEntry[]): string[] {\n const projections = entries.map(fieldProjection);\n return COMPARED_FIELDS.filter((field) => {\n const distinct = new Set(projections.map((p) => p[field]));\n return distinct.size > 1;\n });\n}\n\nexport function buildDriftModel(states: readonly ClientState[]): DriftModel {\n const clients = states.map((s) => s.clientId).sort();\n\n // Gather, per server name, the clients that declare it and their entries.\n const byName = new Map<string, Array<{ clientId: ClientId; entry: McpServerEntry }>>();\n for (const { clientId, servers } of states) {\n for (const [name, entry] of Object.entries(servers)) {\n const list = byName.get(name) ?? [];\n list.push({ clientId, entry });\n byName.set(name, list);\n }\n }\n\n const servers: ServerDrift[] = [];\n for (const name of [...byName.keys()].sort()) {\n const holders = byName.get(name)!;\n const present = holders.map((h) => h.clientId).sort();\n const presentSet = new Set(present);\n const absent = clients.filter((c) => !presentSet.has(c));\n\n const fields = holders.length > 1 ? divergingFields(holders.map((h) => h.entry)) : [];\n const conflict = fields.length > 0;\n\n servers.push({\n name,\n present,\n absent,\n conflict,\n ...(conflict ? { conflictFields: fields } : {}),\n });\n }\n\n const drifted = servers.filter((s) => s.absent.length > 0 || s.conflict).length;\n return { clients, servers, inSync: servers.length - drifted, drifted };\n}\n","/**\n * Plaintext-secret scan over client MCP config (F9 · PR1).\n *\n * mcpm ships an encrypted secret store + OS keychain, but a server's env/header\n * values are routinely pasted in plaintext (24k+ such leaks documented in the\n * wild). This read-only scan flags them so `doctor` can nudge the user toward\n * `mcpm secrets` / keychain mode.\n *\n * REDACTION CONTRACT: a finding carries the KEY name and a LABEL only — NEVER the\n * matched value. Values already stored as `mcpm:keychain:` placeholders are\n * skipped (they are the safe state, not a leak).\n *\n * Two detectors:\n * 1. value-shape — the sweep-hardened `detectSecretLabels` patterns (AWS /\n * GitHub / OpenAI / … keys). Near-zero false positives.\n * 2. secret-named key — a tight key-name heuristic for generic passwords/tokens\n * no value-regex matches, gated by strong non-secret-qualifier (URL/ID/NAME/…)\n * and non-secret-value (reference/URL/path/flag) exclusions + a benign corpus.\n *\n * Pure: no I/O. The caller (doctor) supplies the already-read config.\n */\n\nimport type { McpServerEntry } from \"../config/adapters/index.js\";\nimport { detectSecretLabels } from \"./patterns.js\";\nimport { parsePlaceholder } from \"../store/keychain.js\";\n\nexport interface ConfigSecretFinding {\n /** Server name as it appears in the client config. */\n server: string;\n /** Which value map the secret sits in. */\n field: \"env\" | \"header\";\n /** The env var / header NAME. Never the value. */\n key: string;\n /** What was matched (e.g. \"AWS access key\"). Never the value. */\n label: string;\n}\n\n/** Label for a key-heuristic hit (detector 2). Value-free by construction. */\nconst GENERIC_LABEL = \"secret-named key holds a plaintext value\";\n\n// Secret-indicating whole words. Matched against the key normalized to\n// upper-case with '-'→'_' (so `X-API-Key` reads as `X_API_KEY`). Bare `KEY` is\n// deliberately NOT a word (PUBLIC_KEY / KEY_ID / SORT_KEY are not secrets) — only\n// the listed `*_KEY` compounds count.\nconst SECRET_KEY_RE =\n /(?:^|_)(?:PASSWORD|PASSWD|PASSPHRASE|SECRET|TOKEN|PAT|APIKEY|AUTHORIZATION|CREDENTIALS?|(?:API|ACCESS|PRIVATE|SECRET|SESSION|SIGNING|ENCRYPTION)_KEY)(?:_|$)/;\n\n// Tokens that mean the field is a descriptor of a secret, not the secret itself\n// (an id, url, name, endpoint, …). Any one vetoes a key-name match, so\n// `TOKEN_URL` / `AWS_ACCESS_KEY_ID` / `SECRET_NAME` / `PUBLIC_KEY` do not fire.\n// KNOWN GAP (advisory tool, accepted): the veto matches a qualifier ANYWHERE in the\n// key, so `ID_TOKEN` (where `ID` is the credential TYPE, not a descriptor) is missed.\n// A suffix-anchored fix would newly false-POSITIVE on `MAPBOX_PUBLIC_TOKEN`; since a\n// false negative in an advisory scan is acceptable but a false positive is not, we\n// keep the anywhere-match.\nconst NON_SECRET_QUALIFIER_RE =\n /(?:^|_)(?:URL|URI|ENDPOINT|HOST|PORT|ID|NAME|PATH|FILE|DIR|ENABLED|DISABLED|TYPE|MODE|REGION|TIMEOUT|VERSION|PUBLIC|FORMAT|HEADER|PREFIX|SUFFIX|COUNT|SIZE|TTL|EXPIRY|EXPIRES|ISSUER|AUDIENCE|ALGORITHM|ALG|SCOPE|METHOD)(?:_|$)/;\n\nfunction normalizeKey(key: string): string {\n return key.toUpperCase().replace(/-/g, \"_\");\n}\n\nfunction keyLooksSecret(key: string): boolean {\n const k = normalizeKey(key);\n return SECRET_KEY_RE.test(k) && !NON_SECRET_QUALIFIER_RE.test(k);\n}\n\n/** True when the value is plausibly a real plaintext secret (not a ref/URL/flag). */\nfunction valueLooksPlaintextSecret(value: string): boolean {\n const v = value.trim();\n if (v.length < 6) return false; // too short to be a credential\n if (parsePlaceholder(value) !== null) return false; // mcpm keychain placeholder\n // Reference, not a literal secret. `${...}` is matched ANYWHERE (not just leading):\n // `Bearer ${input:key}` / `Bearer ${env:VAR}` is VS Code / Cursor / Claude Code's\n // documented header idiom — the recommended SAFE state. Detector 1 already ran on\n // the raw value, so a shaped credential embedded alongside a ref is still caught.\n if (/\\$\\{[^}]*\\}/.test(v)) return false; // ${VAR} template (embedded or leading)\n if (/^\\$[A-Za-z_]/.test(v)) return false; // leading $VAR reference\n if (/^%[A-Za-z_][A-Za-z0-9_]*%([\\\\/].*)?$/.test(v)) return false; // %VAR% ref or %VAR%-rooted path\n // A URI of ANY scheme: real endpoints AND secret-manager references that are the\n // safe state — op:// (1Password), vault:// (Vault). ACCEPTED FALSE-NEGATIVE: a URI\n // that itself CARRIES a credential (connection-string userinfo postgres://u:p@host,\n // or a query-param secret like otpauth://…?secret=SEED) is excluded too. Detector 1\n // still catches any prefix-shaped credential embedded in the value, and the bare\n // (non-URI) secret form is still caught by detector 2. Zero-FP is the hard invariant;\n // re-catching these would need query-param parsing that risks FPs on real endpoints.\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(v)) return false;\n // Filesystem path — POSIX (~ . /) or Windows (drive-letter, UNC).\n if (/^[~./]/.test(v) || /^[A-Za-z]:[\\\\/]/.test(v) || /^\\\\\\\\/.test(v)) return false;\n if (/^(true|false|\\d+)$/i.test(v)) return false; // boolean / plain number\n return true;\n}\n\nfunction scanMap(\n server: string,\n field: \"env\" | \"header\",\n map: Record<string, string> | undefined\n): ConfigSecretFinding[] {\n if (!map) return [];\n const out: ConfigSecretFinding[] = [];\n for (const [key, value] of Object.entries(map)) {\n if (typeof value !== \"string\") continue;\n if (parsePlaceholder(value) !== null) continue; // already stored safely — not a leak\n const labels = detectSecretLabels(value);\n if (labels.length > 0) {\n // Value-shape is the more specific, higher-confidence signal — ONE finding per\n // (field, key) even when several patterns match (e.g. a Bearer-wrapped ghp_\n // token hits both), so the --report count is not inflated. Skip the heuristic.\n out.push({ server, field, key, label: labels.join(\", \") });\n continue;\n }\n if (keyLooksSecret(key) && valueLooksPlaintextSecret(value)) {\n out.push({ server, field, key, label: GENERIC_LABEL });\n }\n }\n return out;\n}\n\n/** Scan one server's env + headers for plaintext secrets. */\nexport function scanServerConfigSecrets(\n server: string,\n entry: McpServerEntry\n): ConfigSecretFinding[] {\n return [...scanMap(server, \"env\", entry.env), ...scanMap(server, \"header\", entry.headers)];\n}\n\n/** Scan every server in a client's config. */\nexport function scanConfigSecrets(\n servers: Record<string, McpServerEntry>\n): ConfigSecretFinding[] {\n return Object.entries(servers).flatMap(([name, entry]) => scanServerConfigSecrets(name, entry));\n}\n"],"mappings":"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","names":["servers","getAdapter","getConfigPath"]}
#!/usr/bin/env node
import {
handleLock,
lockPathFor
} from "./chunk-C6CAHFQX.js";
import {
parseSecretsMode,
resolveInstallEntry,
validateRemoteUrl
} from "./chunk-LNGTDYGN.js";
import {
readPins
} from "./chunk-DDCTUMSZ.js";
import {
DEFAULT_MIN_RELEASE_AGE_HOURS,
assessReleaseAge,
stdoutOutput
} from "./chunk-E3T224S3.js";
import {
fetchNpmProvenance,
isEnoent,
isLockedRegistryServer,
isRegistryServer,
isUrlServer,
parseLockFile,
parseStackFile
} from "./chunk-RTI2GLYX.js";
import {
checkScannerAvailable,
scanTier2
} from "./chunk-F6CHEUGO.js";
import {
EXTERNAL_SCAN_MAX,
computeTrustScore,
nativeTrustScore
} from "./chunk-LSNEZAFR.js";
import {
getAdapter
} from "./chunk-W4IAFBUN.js";
import {
confirm
} from "./chunk-2PWW3Q5Q.js";
import {
isNewUnguarded
} from "./chunk-MLVDFLDQ.js";
import {
compareIntegrity,
fetchNpmIntegrity
} from "./chunk-7RJXJERN.js";
import {
RegistryClient
} from "./chunk-V4AA4ZL5.js";
import {
applyKeychainSecrets,
setSecrets
} from "./chunk-NPJ3SGGS.js";
import {
detectInstalledClients
} from "./chunk-6R7TL5O2.js";
import {
getConfigPath
} from "./chunk-R4R2VPDA.js";
import {
assessServerStatus,
extractRegistryMeta,
scanTier1
} from "./chunk-U7N6FRYF.js";
// src/stack/policy.ts
function checkTrustPolicy(input2) {
const { serverName, currentScore, currentMaxPossible, lockedSnapshot, policy } = input2;
if (policy === void 0) {
return { pass: true };
}
const currentPct = toPct(currentScore, currentMaxPossible);
if (policy.minTrustScore !== void 0 && currentPct < policy.minTrustScore) {
return {
pass: false,
reason: `"${serverName}" trust score ${currentPct}% is below the minimum policy threshold of ${policy.minTrustScore}%.`
};
}
if (policy.blockOnScoreDrop === true && lockedSnapshot !== void 0) {
const { currentNativeScore, currentNativeMaxPossible } = input2;
if (currentNativeScore === void 0 || currentNativeMaxPossible === void 0) {
throw new Error(
"blockOnScoreDrop requires the current native trust figures (currentNativeScore / currentNativeMaxPossible). This is a bug \u2014 report it rather than working around it."
);
}
const lockedNative = recoverLockedNative(
lockedSnapshot,
currentNativeMaxPossible
);
const curPct = toPct(currentNativeScore, currentNativeMaxPossible);
const lockPct = toPct(lockedNative.score, lockedNative.maxPossible);
if (curPct < lockPct) {
return {
pass: false,
reason: `"${serverName}" trust score dropped from ${lockPct}% to ${curPct}% (mcpm-native evidence, excluding unverifiable external-scanner credit) since the lock file was created.` + (lockedNative.basis === "legacy-bound" ? ` This lock predates native-evidence drop checks and was written with an external scanner credited, so the baseline is an upper bound \u2014 re-run \`mcpm lock\` to record an exact one.` : lockedNative.basis === "out-of-range" ? (
// Deliberately NOT phrased as tampering: an upward re-weighting of the
// external bucket would make that accusation false for an older mcpm
// reading a newer lock. Re-locking is the remedy either way.
` This lock records trust figures outside the range this version can interpret, so the baseline is an upper bound \u2014 re-run \`mcpm lock\` to record an exact one.`
) : ` If you recently upgraded mcpm, new scanner findings can lower scores \u2014 re-run \`mcpm lock\` to refresh snapshots if the drop is expected.`)
};
}
}
if (policy.minReleaseAgeHours !== void 0 && input2.releaseAge?.blocksArmedGate === true) {
const { ageHours, status } = input2.releaseAge;
if (status === "future") {
return {
pass: false,
reason: `"${serverName}" has a publish timestamp in the future; treated as within the minimum release age of ${policy.minReleaseAgeHours} hour(s) required by policy.`
};
}
if (ageHours === null) {
return {
pass: false,
reason: `"${serverName}" release is of unverifiable age (publish timestamp ${status === "absent" ? "missing from registry metadata" : "could not be parsed"}), and the policy requires a minimum release age of ${policy.minReleaseAgeHours} hour(s).`
};
}
return {
pass: false,
reason: `"${serverName}" release is ${ageHours} hour(s) old, below the minimum release age of ${policy.minReleaseAgeHours} hour(s) required by policy.`
};
}
if (policy.blockInstallScripts === true && input2.hasInstallScriptFindings === true) {
return {
pass: false,
reason: `"${serverName}" resolves to a launcher that runs install scripts, and the policy blocks install scripts.`
};
}
return { pass: true };
}
function toPct(score, maxPossible) {
if (maxPossible <= 0) return 0;
return Math.round(score / maxPossible * 100);
}
function isUsableCredit(credit, locked, nativeMax) {
if (locked.maxPossible === nativeMax && credit > 0) return false;
return credit >= 0 && credit <= EXTERNAL_SCAN_MAX && credit <= locked.score;
}
function recoverLockedNative(locked, nativeMax) {
const bounded = (score, basis) => {
const clamped = Math.max(0, Math.min(nativeMax, score));
return {
score: clamped,
maxPossible: nativeMax,
// A computation that had to be clamped was not exact, whatever produced it — an
// out-of-range `score` reaches here the same way an out-of-range credit does,
// since both are unbounded `z.number()`. Reporting it as exact would hand the
// user the "you upgraded mcpm" remedy for a lock that actually needs re-locking.
basis: basis === "exact" && clamped !== score ? "out-of-range" : basis
};
};
if (locked.externalScanCredit !== void 0) {
return isUsableCredit(locked.externalScanCredit, locked, nativeMax) ? bounded(locked.score - locked.externalScanCredit, "exact") : bounded(locked.score, "out-of-range");
}
if (locked.maxPossible === nativeMax) {
return bounded(locked.score, "exact");
}
return bounded(locked.score, "legacy-bound");
}
// src/stack/env.ts
import { readFile } from "fs/promises";
var ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "__defineGetter__", "__defineSetter__"]);
async function parseEnvFile(filePath) {
let raw;
try {
raw = await readFile(filePath, "utf-8");
} catch (err) {
if (isEnoent(err)) {
return { vars: {}, warnings: [] };
}
throw err;
}
return parseEnvString(raw);
}
function parseEnvString(content) {
const vars = {};
const warnings = [];
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
const lineNum = i + 1;
const raw = lines[i];
const trimmed = raw.trim();
if (trimmed === "" || trimmed.startsWith("#")) {
continue;
}
const eqIndex = trimmed.indexOf("=");
if (eqIndex === -1) {
warnings.push(`Line ${lineNum}: skipped malformed line (no = sign)`);
continue;
}
const key = trimmed.slice(0, eqIndex).trim();
if (key === "") {
warnings.push(`Line ${lineNum}: skipped line with empty key`);
continue;
}
if (!ENV_KEY_RE.test(key) || UNSAFE_KEYS.has(key)) {
warnings.push(
`Line ${lineNum}: skipped invalid key "${key}"`
);
continue;
}
let value = trimmed.slice(eqIndex + 1).trim();
if (!value.startsWith('"') && !value.startsWith("'")) {
const commentIndex = value.indexOf(" #");
if (commentIndex !== -1) {
value = value.slice(0, commentIndex).trim();
}
}
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
value = value.slice(1, -1);
}
vars[key] = value;
}
return { vars, warnings };
}
// src/stack/frozen-verify.ts
function memoizeIntegrity(fetch) {
const cache = /* @__PURE__ */ new Map();
return (identifier, npmVersion) => {
const key = `${identifier}\0${npmVersion}`;
let p = cache.get(key);
if (p === void 0) {
p = fetch(identifier, npmVersion);
cache.set(key, p);
}
return p;
};
}
async function classifyIntegrity(lockFile, fetchNpmIntegrity2) {
const registryEntries = Object.entries(lockFile.servers).filter(
([, locked]) => isLockedRegistryServer(locked)
);
const npmEntries = registryEntries.filter(([, l]) => l.registryType === "npm");
const npmNames = new Set(npmEntries.map(([name]) => name));
const unenforceable = Object.keys(lockFile.servers).filter((name) => !npmNames.has(name));
const checkable = npmEntries.filter(([, l]) => l.npmIntegrity !== void 0);
const absentBaseline = npmEntries.filter(([, l]) => l.npmIntegrity === void 0).map(([name]) => name);
const fresh = await Promise.all(
checkable.map(([, l]) => fetchNpmIntegrity2(l.identifier, l.npmIntegrity.npmVersion))
);
const drift = [];
const formatOnly = [];
const couldNotVerify = [];
for (let i = 0; i < checkable.length; i++) {
const [name, locked] = checkable[i];
const baseline = locked.npmIntegrity;
const snap = fresh[i];
const coord = { name, identifier: locked.identifier, npmVersion: baseline.npmVersion };
if (snap === void 0) {
couldNotVerify.push(coord);
continue;
}
const cmp = compareIntegrity(baseline.integrity, snap.integrity);
if (cmp === "equal") continue;
if (cmp === "differ") {
drift.push({ ...coord, oldIntegrity: baseline.integrity, newIntegrity: snap.integrity });
} else {
formatOnly.push(coord);
}
}
return { drift, formatOnly, couldNotVerify, absentBaseline, unenforceable, checkedNpmCount: checkable.length };
}
function frozenVerdict(c) {
const noBaselines = c.absentBaseline.length > 0 && c.checkedNpmCount === 0;
const blocks = [];
for (const d of c.drift) {
blocks.push({
name: d.name,
reason: "drift",
identifier: d.identifier,
npmVersion: d.npmVersion,
oldIntegrity: d.oldIntegrity,
newIntegrity: d.newIntegrity
});
}
for (const f of c.formatOnly) {
blocks.push({ name: f.name, reason: "format", identifier: f.identifier, npmVersion: f.npmVersion });
}
for (const v of c.couldNotVerify) {
blocks.push({ name: v.name, reason: "could-not-verify", identifier: v.identifier, npmVersion: v.npmVersion });
}
if (!noBaselines) {
for (const name of c.absentBaseline) {
blocks.push({ name, reason: "missing-baseline" });
}
}
return {
ok: !noBaselines && blocks.length === 0,
noBaselines,
blocks,
unenforceable: c.unenforceable,
checkedNpmCount: c.checkedNpmCount
};
}
// src/stack/frozen-provenance.ts
function verifiedBaseline(locked) {
const prov = locked.provenance;
if (prov?.status !== "attested" || prov.verification?.outcome !== "verified") {
return void 0;
}
return {
npmVersion: prov.npmVersion,
signerSan: prov.verification.signerSan,
signerIssuer: prov.verification.signerIssuer
};
}
async function classifyProvenance(lockFile, fetchNpmIntegrity2, fetchNpmProvenance2) {
const checked = Object.entries(lockFile.servers).filter(
([, l]) => isLockedRegistryServer(l)
).filter(([, l]) => l.registryType === "npm").map(([name, l]) => ({ name, locked: l, baseline: verifiedBaseline(l) })).filter(
(e) => e.baseline !== void 0
);
const blocks = (await Promise.all(
checked.map(async ({ name, locked, baseline }) => {
const coord = { name, identifier: locked.identifier, npmVersion: baseline.npmVersion };
try {
const integ = await fetchNpmIntegrity2(locked.identifier, baseline.npmVersion);
if (integ === void 0) {
return {
...coord,
reason: "unverifiable",
detail: "could not fetch npm's published integrity to bind the attestation"
};
}
const fresh = await fetchNpmProvenance2(locked.identifier, baseline.npmVersion, {
integritySri: integ.integrity
});
return classifyOne(coord, baseline, fresh);
} catch {
return {
...coord,
reason: "unverifiable",
detail: "re-verification errored this run (fetcher threw)"
};
}
})
)).filter((b) => b !== void 0);
return { ok: blocks.length === 0, blocks, checkedVerifiedCount: checked.length };
}
function classifyOne(coord, baseline, fresh) {
if (fresh === void 0) {
return { ...coord, reason: "unverifiable", detail: "no fresh attestation record this run (offline or endpoint error)" };
}
if (fresh.status === "unsigned") {
return { ...coord, reason: "regression", detail: "the attestation that verified at lock time is no longer published (now unsigned)" };
}
if (fresh.status !== "attested") {
return { ...coord, reason: "unverifiable", detail: "attestation shape is no longer a recognizable SLSA record" };
}
const v = fresh.verification;
if (v === void 0) {
return { ...coord, reason: "unverifiable", detail: "attestation present but cryptographic verification did not run this fetch" };
}
if (v.outcome === "could-not-verify") {
return { ...coord, reason: "regression", detail: `attestation no longer cryptographically verifies (${v.reason ?? "crypto failure"})` };
}
if (baseline.signerSan === void 0) {
return { ...coord, reason: "unverifiable", detail: "verified baseline lacks a recorded signer SAN \u2014 cannot assert signer equality; re-lock to record it" };
}
if (v.signerSan !== baseline.signerSan || v.signerIssuer !== baseline.signerIssuer) {
const deltas = [];
if (v.signerSan !== baseline.signerSan) {
deltas.push(`SAN ${baseline.signerSan ?? "(none)"} \u2192 ${v.signerSan ?? "(none)"}`);
}
if (v.signerIssuer !== baseline.signerIssuer) {
deltas.push(`issuer ${baseline.signerIssuer ?? "(none)"} \u2192 ${v.signerIssuer ?? "(none)"}`);
}
return { ...coord, reason: "signer-changed", detail: `signer identity changed: ${deltas.join("; ")}` };
}
return void 0;
}
// src/guard/shadow.ts
function detectNameCollisions(inventory) {
const ownersByTool = /* @__PURE__ */ new Map();
for (const [server, tools] of inventory) {
for (const tool of tools) {
let owners = ownersByTool.get(tool);
if (owners === void 0) {
owners = /* @__PURE__ */ new Set();
ownersByTool.set(tool, owners);
}
owners.add(server);
}
}
const findings = [];
for (const [toolName, owners] of ownersByTool) {
if (owners.size >= 2) {
findings.push({ toolName, servers: [...owners].sort() });
}
}
return findings.sort((a, b) => a.toolName.localeCompare(b.toolName));
}
function buildInventoryFromPins(pins, serverNames) {
const inventory = /* @__PURE__ */ new Map();
for (const name of serverNames) {
inventory.set(name, toolNamesFor(pins, name));
}
return inventory;
}
function toolNamesFor(pins, name) {
return Object.hasOwn(pins.servers, name) ? Object.keys(pins.servers[name]) : [];
}
function serversWithoutBaseline(pins, serverNames) {
return serverNames.filter((name) => toolNamesFor(pins, name).length === 0);
}
// src/commands/up.ts
import "commander";
import chalk from "chalk";
import { input, password } from "@inquirer/prompts";
function trustFigure(trust, options) {
if (options.minTrustFloor === void 0) {
return `${trust.score}/${trust.maxPossible}`;
}
const native = nativeTrustScore(trust);
if (native.excludedExternalCredit === 0) {
return `${trust.score}/${trust.maxPossible}`;
}
return `${native.score}/${native.maxPossible} against the floor, ${trust.score}/${trust.maxPossible} with the external scanner`;
}
async function handleUp(options, deps) {
if (options.secrets === "keychain" && options.ci) {
throw new Error(
"--secrets keychain cannot be combined with --ci (it would persist secrets to the CI runner's keychain). Use --secrets plaintext in CI."
);
}
const stackPath = options.stackFile ?? "mcpm.yaml";
const lockPath = lockPathFor(stackPath);
const stackFile = await parseStackFile(stackPath);
let lockFile = await parseLockFile(lockPath);
if (lockFile === null) {
deps.output("No lock file found. Running mcpm lock first...");
await deps.runLock(stackPath);
lockFile = await parseLockFile(lockPath);
if (lockFile === null) {
throw new Error("Failed to create lock file.");
}
}
const clients = await deps.detectClients();
if (clients.length === 0) {
throw new Error("No supported AI clients found.");
}
const serverEntries = filterByProfile(stackFile, options.profile);
if (serverEntries.length === 0) {
deps.output("No servers match the selected profile.");
return;
}
if (options.frozen === true || stackFile.policy?.frozen === true) {
await runFrozenPass(lockFile, deps);
}
const envFileVars = options.allowEnvFile === false ? { vars: {}, warnings: [] } : await parseEnvFile(".env");
const scannerAvailable = await deps.checkScannerAvailable();
if (options.dryRun) {
deps.output("Dry run \u2014 no changes will be made.\n");
}
if (!options.dryRun) {
await backupConfigs(clients, deps);
}
const results = [];
const previousConsented = deps.readUnguardedConsent ? await deps.readUnguardedConsent() : [];
const consentedUnguarded = new Set(previousConsented);
for (const [name, server] of serverEntries) {
const locked = lockFile.servers[name];
try {
const result = await processServer({
name,
server,
locked,
policy: stackFile.policy,
clients,
scannerAvailable,
envFileVars: envFileVars.vars,
consentedUnguarded,
options,
deps
});
results.push(result);
deps.recordResult?.({ name, status: result.status });
deps.output(` ${statusIcon(result.status)} ${name}: ${result.message}`);
} catch (err) {
const failure = {
name,
status: "failed",
message: err instanceof Error ? err.message : String(err)
};
results.push(failure);
deps.recordResult?.({ name, status: "failed" });
deps.output(` ${statusIcon("failed")} ${name}: ${failure.message}`);
}
}
if (options.strict && !options.dryRun) {
await handleStrictRemoval(stackFile, clients, options, deps, results);
}
const urlServerNames = new Set(
serverEntries.filter(([, s]) => isUrlServer(s)).map(([n]) => n)
);
const installedUnguarded = results.filter((r) => r.status === "installed" && urlServerNames.has(r.name)).map((r) => r.name).sort();
if (installedUnguarded.length > 0 && !options.dryRun) {
const newlyConsented = installedUnguarded.filter((n) => !consentedUnguarded.has(n));
if (isNewUnguarded(installedUnguarded, previousConsented)) {
const alreadyCount = installedUnguarded.length - newlyConsented.length;
const alreadyNote = alreadyCount > 0 ? ` (+${alreadyCount} previously consented)` : "";
deps.output(
`
\u26A0 UNGUARDED: the following URL/HTTP-transport server(s) now run WITHOUT runtime inspection (no relay wraps a non-stdio transport): ${newlyConsented.join(", ")}${alreadyNote}. This grants consent \u2014 it does NOT add protection. The only true fix is a streamable-HTTP relay (not yet implemented). Future \`up\` runs stay quiet unless a NEW unguarded server appears.`
);
if (deps.recordUnguardedConsent) {
await deps.recordUnguardedConsent(newlyConsented).catch(() => void 0);
}
} else {
deps.output(
`
${installedUnguarded.length} server(s) running unguarded (previously consented): ${installedUnguarded.join(", ")}`
);
}
}
if (options.frozen !== true && stackFile.policy?.frozen !== true) {
await runIntegrityPass(lockFile, deps);
}
let shadowCollisions = 0;
if (options.checkShadowing === true || stackFile.policy?.checkShadowing === true) {
shadowCollisions = await runShadowPass(
serverEntries.map(([name]) => name),
deps
);
}
const installed = results.filter((r) => r.status === "installed").length;
const blocked = results.filter((r) => r.status === "blocked").length;
const failed = results.filter((r) => r.status === "failed").length;
const skipped = results.filter((r) => r.status === "skipped").length;
const removed = results.filter((r) => r.status === "removed").length;
const unguarded = installedUnguarded.length;
deps.output(
`
${installed} installed, ${skipped} skipped, ${blocked} blocked, ${failed} failed` + (removed > 0 ? `, ${removed} removed` : "") + (unguarded > 0 ? `, ${unguarded} unguarded` : "")
);
const totalSecretsStored = results.reduce(
(sum, r) => sum + (r.storedSecrets ?? 0),
0
);
if (options.secrets === "keychain" && totalSecretsStored > 0 && !options.dryRun) {
deps.output(
"Secrets stored encrypted at rest in ~/.mcpm. With an OS keychain this protects against other-user/offline access (not same-user processes); without one a machine-derived key is used that guards casual local inspection only, NOT file exfiltration \u2014 run `mcpm secrets migrate` once a keychain is available. Run `mcpm guard enable` (then restart your IDE) so they resolve at launch."
);
}
if (blocked > 0 || failed > 0) {
throw new Error(`${blocked + failed} server(s) could not be installed.`);
}
if (shadowCollisions > 0 && options.ci) {
throw new Error(
`${shadowCollisions} cross-server tool-name collision(s) detected (--ci). Resolve the shadowing (rename/remove a duplicate tool) or drop --check-shadowing.`
);
}
}
function filterByProfile(stackFile, profile) {
return Object.entries(stackFile.servers).filter(([, server]) => {
const profiles = isRegistryServer(server) || isUrlServer(server) ? server.profiles : void 0;
if (!profiles) return true;
if (!profile) return true;
return profiles.includes(profile);
});
}
async function backupConfigs(clients, deps) {
const { readFile: readFile2, writeFile } = await import("fs/promises");
for (const clientId of clients) {
try {
const adapter = deps.getAdapter(clientId);
const configPath = deps.getPath(clientId);
const content = await readFile2(configPath, "utf-8");
await writeFile(`${configPath}.bak`, content, {
encoding: "utf-8",
mode: 384
});
} catch {
}
}
}
async function processServer(input2) {
const { name, server, locked, policy, clients, scannerAvailable, envFileVars, options, deps } = input2;
if (isUrlServer(server)) {
return processUrlServer(name, server.url, clients, policy, input2.consentedUnguarded, options, deps);
}
if (!locked || !isLockedRegistryServer(locked)) {
return { name, status: "failed", message: "Not found in lock file. Run mcpm lock." };
}
const serverEntry = await deps.getServer(name, locked.version);
const statusGate = assessServerStatus(serverEntry);
if (statusGate.blocks) {
return {
name,
status: "blocked",
message: `deleted from the MCP registry${statusGate.statusMessage ? ` (${statusGate.statusMessage})` : ""}`
};
}
const tier1 = deps.scanTier1(serverEntry);
let findings = [...tier1];
if (scannerAvailable) {
const tier2 = await deps.scanTier2(name);
findings = [...findings, ...tier2];
}
const registryMeta = extractRegistryMeta(serverEntry);
const releaseAge = assessReleaseAge({
publishedAt: registryMeta.publishedAt,
now: (deps.now ?? Date.now)(),
minAgeHours: options.minTrustFloor !== void 0 ? DEFAULT_MIN_RELEASE_AGE_HOURS : policy?.minReleaseAgeHours ?? DEFAULT_MIN_RELEASE_AGE_HOURS
});
if (releaseAge.finding) {
findings = [...findings, releaseAge.finding];
}
const trustInput = {
findings,
healthCheckPassed: null,
hasExternalScanner: scannerAvailable,
registryMeta
};
const trustScore = deps.computeTrustScore(trustInput);
const nativeTrust = nativeTrustScore(trustScore);
if (options.minTrustFloor !== void 0 && nativeTrust.score < options.minTrustFloor) {
return {
name,
status: "blocked",
message: `trust score ${nativeTrust.score}/${nativeTrust.maxPossible} is below the required floor of ${options.minTrustFloor}` + (nativeTrust.excludedExternalCredit > 0 ? ` (the external scanner's ${nativeTrust.excludedExternalCredit} points do not count toward the floor)` : "")
};
}
const policyResult = checkTrustPolicy({
serverName: name,
currentScore: trustScore.score,
currentMaxPossible: trustScore.maxPossible,
// TODOS #35: the blockOnScoreDrop tripwire compares native evidence, so a fake
// MCPM_EXTERNAL_SCANNER cannot mask a drop. `nativeTrust` is computed above.
currentNativeScore: nativeTrust.score,
currentNativeMaxPossible: nativeTrust.maxPossible,
lockedSnapshot: locked.trust,
policy,
releaseAge: {
ageHours: releaseAge.ageHours,
status: releaseAge.status,
blocksArmedGate: releaseAge.blocksArmedGate
},
hasInstallScriptFindings: findings.some((f) => f.type === "install-script")
});
if (!policyResult.pass) {
return { name, status: "blocked", message: policyResult.reason };
}
if (options.dryRun) {
return {
name,
status: "skipped",
message: `would install v${locked.version} (trust: ${trustFigure(trustScore, options)})`
};
}
const { env: envVars, storedCount } = await resolveEnvVars(name, server, envFileVars, options, deps);
const installedClients = [];
const clientErrors = [];
for (const clientId of clients) {
try {
const entry = resolveInstallEntry(serverEntry, clientId);
const entryWithEnv = {
...entry,
...Object.keys(envVars).length > 0 ? { env: { ...entry.env, ...envVars } } : {}
};
const adapter = deps.getAdapter(clientId);
const configPath = deps.getPath(clientId);
await adapter.addServer(configPath, name, entryWithEnv, { force: true });
installedClients.push(clientId);
} catch (err) {
clientErrors.push(`${clientId}: ${err instanceof Error ? err.message : String(err)}`);
}
}
if (installedClients.length === 0) {
return {
name,
status: "failed",
message: `could not write to any client (${clientErrors.join("; ")})`
};
}
const partialNote = clientErrors.length > 0 ? ` (warning: failed on ${clientErrors.join("; ")})` : "";
return {
name,
status: "installed",
message: `v${locked.version} (trust: ${trustFigure(trustScore, options)})${partialNote}`,
storedSecrets: storedCount
};
}
async function runIntegrityPass(lockFile, deps) {
const c = await classifyIntegrity(lockFile, deps.fetchNpmIntegrity);
for (const d of c.drift) {
const oldShort = d.oldIntegrity.slice(0, 16);
const newShort = d.newIntegrity.slice(0, 16);
deps.output(
`
\u26A0 INTEGRITY DRIFT: npm's published record for ${d.identifier}@${d.npmVersion} changed since you locked it (dist.integrity ${oldShort}\u2026 \u2192 ${newShort}\u2026). A published version's integrity is meant to be immutable, so this can mean a supply-chain republish \u2014 but it can also be a legitimate republish or a different registry. mcpm checks the registry's published record, not the code your agent runs. This is a warning only \u2014 it does not block \`mcpm up\`; npx/uvx fetch and run the actual package independently when the server starts (possibly from a different mirror). Re-run \`mcpm lock\` if this change is expected.`
);
}
for (const f of c.formatOnly) {
deps.output(
`
\u26A0 ${f.name}: npm changed the integrity format for ${f.identifier}@${f.npmVersion}, so mcpm cannot compare its published record against your locked baseline (mcpm checks the registry's published record, not the code your agent runs). Re-run \`mcpm lock\` to refresh the baseline.`
);
}
if (c.couldNotVerify.length > 0) {
deps.output(
`
could not verify npm integrity for ${c.couldNotVerify.length} server(s) this run (no drift result is not proof of integrity).`
);
}
if (c.absentBaseline.length > 0) {
deps.output(
`
integrity baseline missing for ${c.absentBaseline.length} npm server(s) \u2014 re-run \`mcpm lock\` with network access to enable drift detection.`
);
}
}
async function runFrozenPass(lockFile, deps) {
const fetchIntegrity = memoizeIntegrity(deps.fetchNpmIntegrity);
const [v, pv] = await Promise.all([
classifyIntegrity(lockFile, fetchIntegrity).then(frozenVerdict),
classifyProvenance(lockFile, fetchIntegrity, deps.fetchNpmProvenance)
]);
const provBlocks = pv.blocks;
if (v.unenforceable.length > 0) {
deps.output(
`
${v.unenforceable.length} server(s) (pypi/oci/url) have no integrity baseline mechanism \u2014 \`--frozen\` cannot enforce them (multi-registry pinning is deferred).`
);
}
if (v.noBaselines && provBlocks.length === 0) {
throw new Error(
"--frozen: this lock has no integrity baselines (it predates them, or was last locked offline). Run `mcpm lock` online once to record them, then `mcpm up --frozen`."
);
}
if (v.ok && provBlocks.length === 0) return;
const integrityMessages = v.blocks.map((b) => {
switch (b.reason) {
case "drift":
return `\u2717 FROZEN: npm's published record for ${b.identifier}@${b.npmVersion} changed since you locked it (dist.integrity ${b.oldIntegrity.slice(0, 16)}\u2026 \u2192 ${b.newIntegrity.slice(0, 16)}\u2026). --frozen refuses to install on integrity drift. Re-pin with \`mcpm lock\` only if this change is expected.`;
case "format":
return `\u2717 FROZEN: cannot compare npm's published record for ${b.identifier}@${b.npmVersion} against your locked baseline (integrity format changed). Re-run \`mcpm lock\` to refresh it.`;
case "could-not-verify":
return `\u2717 FROZEN: could not verify npm's published record for ${b.identifier}@${b.npmVersion} this run (offline, a yanked version, or no comparable dist.integrity). --frozen requires proof the record matches your lock \u2014 this may be a transient registry error, so re-run; if it persists, drop --frozen.`;
case "missing-baseline":
return `\u2717 FROZEN: no integrity baseline recorded for ${b.name}, though other servers in this lock have one. Re-run \`mcpm lock\` online to record it, then \`mcpm up --frozen\`.`;
default: {
const _never = b;
throw new Error(`unhandled frozen block reason: ${JSON.stringify(_never)}`);
}
}
});
const provenanceMessages = provBlocks.map(frozenProvenanceMessage);
const noticeMessages = v.noBaselines ? [
"\u26A0 FROZEN: this lock has no integrity baselines (predates them / locked offline) \u2014 run `mcpm lock` online to record them."
] : [];
const allMessages = [...noticeMessages, ...integrityMessages, ...provenanceMessages];
deps.output(`
${allMessages.join("\n")}`);
deps.output("\nmcpm verifies the registry's published record, not the code your agent runs at launch.");
const failed = /* @__PURE__ */ new Set([...v.blocks.map((b) => b.name), ...provBlocks.map((b) => b.name)]);
throw new Error(
`frozen: ${failed.size} server(s) failed verification; nothing was installed.`
);
}
function frozenProvenanceMessage(b) {
switch (b.reason) {
case "signer-changed":
return `\u2717 FROZEN: the cryptographic signer for ${b.identifier}@${b.npmVersion} changed since you locked it (${b.detail}). --frozen refuses to install on a provenance signer swap. Re-pin with \`mcpm lock\` only if this re-sign is expected.`;
case "regression":
return `\u2717 FROZEN: provenance for ${b.identifier}@${b.npmVersion} regressed \u2014 it cryptographically verified when you locked it and no longer does (${b.detail}). --frozen refuses to install. If npm's record is unchanged, your mcpm/@sigstore version may have changed since you locked (e.g. after an mcpm upgrade); if that regression is expected, remove this server's stale lock entry and re-lock to re-baseline (a plain \`mcpm lock\` keeps the prior verified baseline).`;
case "unverifiable":
return `\u2717 FROZEN: could not cryptographically re-verify provenance for ${b.identifier}@${b.npmVersion} this run (${b.detail}). --frozen requires proof the attestation still verifies \u2014 this may be a transient error, so re-run; if it persists, investigate before dropping --frozen.`;
default: {
const _never = b.reason;
throw new Error(`unhandled provenance block reason: ${JSON.stringify(_never)}`);
}
}
}
async function runShadowPass(serverNames, deps) {
if (deps.readPins === void 0) {
deps.output("\n\u26A0 shadow check skipped: no pins reader available in this context.");
return 0;
}
let pins;
try {
pins = await deps.readPins();
} catch {
deps.output(
"\n\u26A0 shadow check skipped: ~/.mcpm/pins.json is unreadable (integrity check or corruption)."
);
return 0;
}
const findings = detectNameCollisions(buildInventoryFromPins(pins, serverNames));
const noBaseline = serversWithoutBaseline(pins, serverNames);
const checked = serverNames.length - noBaseline.length;
deps.output(
`
Shadow check: compared guarded tool inventories for ${checked} of ${serverNames.length} server(s).`
);
if (noBaseline.length > 0) {
deps.output(
` ${noBaseline.length} server(s) have NO guard baseline yet (${noBaseline.join(", ")}) \u2014 this check cannot see their tools, so a clean result does NOT mean no shadowing. Run them under \`mcpm guard\` (then re-run \`mcpm up\`) to include them.`
);
}
for (const f of findings) {
deps.output(
`
\u26A0 SHADOW: tool "${f.toolName}" is exposed by ${f.servers.length} servers (${f.servers.join(", ")}). A lower-trust server can shadow a tool meant for another, so agent calls to "${f.toolName}" are ambiguous. This can also be benign (two servers of the same kind legitimately export the same tool). Review which server should own it. (Exact-name match only \u2014 a homoglyph/case variant evades this check.)`
);
}
return findings.length;
}
async function processUrlServer(name, url, clients, policy, consentedUnguarded, options, deps) {
if (options.allowUrlServers === false) {
return {
name,
status: "blocked",
message: "URL servers are not permitted via the MCP surface"
};
}
const consented = options.allowUnguarded === true || policy?.allowUrlServers === true || consentedUnguarded.has(name);
if (!consented) {
return {
name,
status: "blocked",
message: "URL/HTTP-transport server runs UNGUARDED \u2014 no runtime inspection is possible (mcpm's guard relay only wraps stdio servers). Re-run with --allow-unguarded or set policy.allowUrlServers: true to install it WITHOUT protection."
};
}
let urlError;
try {
validateRemoteUrl(url);
} catch (err) {
urlError = err instanceof Error ? err.message : String(err);
}
const cursorClients = clients.filter((c) => c === "cursor");
if (cursorClients.length === 0) {
return {
name,
status: "skipped",
message: "URL server \u2014 no Cursor client detected (only Cursor supports URL transport)"
};
}
if (options.dryRun) {
return urlError ? { name, status: "skipped", message: `would reject URL ${url}: ${urlError}` } : { name, status: "skipped", message: `would install URL ${url} to Cursor` };
}
if (urlError) {
return { name, status: "blocked", message: urlError };
}
for (const clientId of cursorClients) {
const adapter = deps.getAdapter(clientId);
const configPath = deps.getPath(clientId);
await adapter.addServer(configPath, name, { url }, { force: true });
}
return { name, status: "installed", message: `URL ${url} \u2192 Cursor` };
}
async function resolveEnvVars(serverName, server, envFileVars, options, deps) {
const envDecl = isRegistryServer(server) || isUrlServer(server) ? server.env : void 0;
if (!envDecl) return { env: {}, storedCount: 0 };
const resolved = {};
const secretKeys = /* @__PURE__ */ new Set();
for (const [key, decl] of Object.entries(envDecl)) {
const fromEnv = options.allowProcessEnv === false ? void 0 : process.env[key];
const fromFile = envFileVars[key];
const fromDefault = decl.default;
let value;
if (fromEnv !== void 0) {
value = fromEnv;
} else if (fromFile !== void 0) {
value = fromFile;
} else if (fromDefault !== void 0) {
value = fromDefault;
} else if (decl.required) {
if (options.ci) {
throw new Error(
`Required env var "${key}" for "${serverName}" is not set. Set it in process.env or .env file (--ci mode, no interactive prompt).`
);
}
value = await deps.promptEnvVar(key, decl.secret);
}
if (value === void 0) continue;
resolved[key] = value;
if (decl.secret) secretKeys.add(key);
}
return applyKeychainSecrets({
serverName,
resolvedEnv: resolved,
isSecret: (key) => secretKeys.has(key),
mode: options.secrets ?? "plaintext",
setSecrets: deps.setSecrets
});
}
async function handleStrictRemoval(stackFile, clients, options, deps, results) {
const declaredNames = new Set(Object.keys(stackFile.servers));
for (const clientId of clients) {
const adapter = deps.getAdapter(clientId);
const configPath = deps.getPath(clientId);
const installed = await adapter.read(configPath);
for (const name of Object.keys(installed)) {
if (declaredNames.has(name)) continue;
if (options.ci && !options.yes) {
throw new Error(
`--strict --ci requires --yes to remove servers not in mcpm.yaml. Server "${name}" in ${clientId} would be removed.`
);
}
if (!options.ci && options.yes !== true) {
const confirmed = await deps.confirm(
`Remove "${name}" from ${clientId}? (not in mcpm.yaml)`
);
if (!confirmed) continue;
}
await adapter.removeServer(configPath, name);
results.push({
name,
status: "removed",
message: `removed from ${clientId} (not in mcpm.yaml)`
});
deps.recordResult?.({ name, status: "removed" });
deps.output(` - ${name}: removed from ${clientId}`);
}
}
}
function statusIcon(status) {
switch (status) {
case "installed":
return "\u2713";
case "removed":
return "\u2212";
case "skipped":
return "\u2022";
case "blocked":
return "\u2717";
case "failed":
return "\u2717";
default:
return "?";
}
}
function registerUpCommand(program) {
program.command("up").description("Install all servers from mcpm.yaml with trust verification").option("-f, --file <path>", "path to mcpm.yaml", "mcpm.yaml").option("-p, --profile <name>", "install only servers matching this profile").option("--dry-run", "show what would be installed without making changes").option("--ci", "CI mode: no interactive prompts, exit nonzero on failure").option("--strict", "remove servers not declared in mcpm.yaml").option("-y, --yes", "skip confirmation prompts (required with --strict --ci)").option("--secrets <mode>", "where to store secret env vars: 'keychain' (encrypted in ~/.mcpm, resolved by mcpm guard at launch) or 'plaintext' (default); 'keychain' is rejected with --ci", parseSecretsMode).option("--allow-unguarded", "permit URL/HTTP-transport servers to run WITHOUT runtime guard inspection (no relay wraps a non-stdio transport); records consent so future runs stay quiet").option("--check-shadowing", "report tool-name collisions across guarded servers (a shadowing signal); advisory interactively, exits nonzero under --ci").option("--frozen", "fail closed: BEFORE installing, verify every locked npm server's published integrity AND re-verify Sigstore provenance for crypto-verified servers, then BLOCK (install nothing, exit nonzero) on integrity drift / provenance regression / unverifiable / missing baseline \u2014 the CI supply-chain freeze gate").action(
async (opts) => {
const client = new RegistryClient();
const { readUnguardedConsent, writeUnguardedConsent, mergeUnguarded } = await import("./unguarded-GJO5WRM7.js");
try {
await handleUp(
{
stackFile: opts.file,
profile: opts.profile,
dryRun: opts.dryRun,
ci: opts.ci,
strict: opts.strict,
yes: opts.yes,
secrets: opts.secrets,
allowUnguarded: opts.allowUnguarded,
checkShadowing: opts.checkShadowing,
frozen: opts.frozen
},
{
detectClients: detectInstalledClients,
getAdapter,
getPath: getConfigPath,
getServer: (name, version) => client.getServer(name, version),
scanTier1,
checkScannerAvailable,
scanTier2: (name) => scanTier2(name),
computeTrustScore,
now: () => Date.now(),
runLock: async (stackFile) => {
const { writeFile } = await import("fs/promises");
await handleLock(
{ stackFile },
{
getServerVersions: (name) => client.getServerVersions(name),
getServer: (name, v) => client.getServer(name, v),
scanTier1,
checkScannerAvailable,
scanTier2: (name) => scanTier2(name),
computeTrustScore,
now: () => Date.now(),
writeLockFile: (path, content) => writeFile(path, content, { encoding: "utf-8", mode: 384 }),
fetchNpmIntegrity,
// F8/B3: auto-lock must record the crypto-`verified` provenance
// baseline too, or the verify-time gate is vacuous for up-locked repos.
fetchNpmProvenance: (id, ver, sri) => fetchNpmProvenance(id, ver, { integritySri: sri }),
output: stdoutOutput
}
);
},
confirm,
promptEnvVar: async (name, isSecret) => {
if (isSecret) {
return password({ message: `${name}:` });
}
return input({ message: `${name}:` });
},
output: stdoutOutput,
setSecrets,
fetchNpmIntegrity,
fetchNpmProvenance: (id, v, o) => fetchNpmProvenance(id, v, o),
readPins,
readUnguardedConsent,
recordUnguardedConsent: async (names) => {
const previous = await readUnguardedConsent();
await writeUnguardedConsent(mergeUnguarded(previous, names));
}
}
);
} catch (err) {
console.error(chalk.red(err.message));
process.exit(1);
}
}
);
}
export {
memoizeIntegrity,
classifyIntegrity,
frozenVerdict,
classifyProvenance,
handleUp,
registerUpCommand
};
//# sourceMappingURL=chunk-KGCQLI22.js.map

Sorry, the diff of this file is too big to display

#!/usr/bin/env node
import {
DEFAULT_MIN_RELEASE_AGE_HOURS,
assessReleaseAge,
stdoutOutput
} from "./chunk-E3T224S3.js";
import {
checkScannerAvailable,
scanTier2
} from "./chunk-F6CHEUGO.js";
import {
computeTrustScore
} from "./chunk-LSNEZAFR.js";
import {
getAdapter
} from "./chunk-W4IAFBUN.js";
import {
confirm
} from "./chunk-2PWW3Q5Q.js";
import {
applyKeychainSecrets,
setSecrets
} from "./chunk-NPJ3SGGS.js";
import {
detectInstalledClients
} from "./chunk-6R7TL5O2.js";
import {
CLIENT_IDS,
getConfigPath
} from "./chunk-R4R2VPDA.js";
import {
addInstalledServer
} from "./chunk-4QDJ3I7X.js";
import {
DANGEROUS_FLAG_PREFIXES,
argvTokens,
assessServerStatus,
extractRegistryMeta,
levelColor,
scanTier1,
scoreBar
} from "./chunk-U7N6FRYF.js";
// src/commands/install.ts
import { InvalidArgumentError } from "commander";
import chalk from "chalk";
import { input, password } from "@inquirer/prompts";
function validateRemoteUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`Invalid remote URL: "${url}"`);
}
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
throw new Error(
`Remote URL must use http or https protocol, got: "${parsed.protocol}"`
);
}
if (parsed.protocol === "http:" && !isLoopbackHost(parsed.hostname)) {
throw new Error(
`Remote URL must use https for non-loopback hosts (plaintext http is vulnerable to interception), got: "${url}"`
);
}
}
function isLoopbackHost(hostname) {
const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
return h === "localhost" || h.endsWith(".localhost") || h === "127.0.0.1" || h === "::1";
}
var NPM_IDENTIFIER_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
var PYPI_IDENTIFIER_RE = /^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$/;
var OCI_IDENTIFIER_RE = /^[a-z0-9]+([._-][a-z0-9]+)*(\/[a-z0-9]+([._-][a-z0-9]+)*)*:[a-zA-Z0-9._-]+$/;
function validateIdentifier(identifier, registryType) {
const patterns = {
npm: NPM_IDENTIFIER_RE,
pypi: PYPI_IDENTIFIER_RE,
oci: OCI_IDENTIFIER_RE
};
const re = patterns[registryType];
if (re && !re.test(identifier)) {
throw new Error(
`Rejected potentially malicious ${registryType} identifier: "${identifier}"`
);
}
}
function normalizeRuntimeArgs(args) {
return args.flatMap(argvTokens);
}
var SAFE_ARG_PATTERNS = [
// Generic boolean flags (--allow-write, --read-only, --no-sandbox, etc.)
/^--[a-zA-Z][\w-]*$/,
// Single-dash short flags the live registry legitimately declares (-i, -y, -p).
// EXACTLY one alpha char — no bundled tail. Allowing a tail (-rmodule, -eCODE)
// would let a dangerous flag bundle its payload and slip past the Layer-1
// DANGEROUS_FLAG_PREFIXES check, which only rejects the exact token (-e/-r) or
// its '=' form. The live registry's short flags are all single-letter, so the
// narrow form loses no real coverage while closing the bundling bypass.
/^-[a-zA-Z]$/,
// Generic --key=value flags with safe value characters
// Blocks shell metacharacters: ; | $ ` & ( ) { } < > ! ' "
/^--[a-zA-Z][\w-]+=[\w./@:, -]+$/,
// Bare absolute paths (Unix: /path/to/dir)
/^\/[\w.@/ -]+$/,
// Home-relative paths (~/Documents)
/^~[\w.@/ -]*$/,
// Bare positional arguments (no dashes, no path traversal)
/^[a-zA-Z0-9][\w.@/-]*$/
];
function validateRuntimeArgs(args) {
for (const arg of args) {
if (/(?:^|[=\\/])\.\.(?:[\\/]|$)/.test(arg)) {
throw new Error(`Rejected path traversal in runtime argument: "${arg}"`);
}
const isDangerous = DANGEROUS_FLAG_PREFIXES.some(
(prefix) => arg === prefix || arg.startsWith(`${prefix}=`)
);
if (isDangerous) {
throw new Error(`Rejected dangerous runtime argument: "${arg}"`);
}
const isSafe = SAFE_ARG_PATTERNS.some((pattern) => pattern.test(arg));
if (!isSafe) {
throw new Error(`Rejected unrecognized runtime argument: "${arg}"`);
}
}
}
function resolveInstallEntry(serverEntry, clientId) {
const { server } = serverEntry;
if (clientId === "cursor" && server.remotes && server.remotes.length > 0) {
const httpRemote = server.remotes.find(
(r) => r.type === "streamable-http" || r.type === "sse"
);
if (httpRemote) {
validateRemoteUrl(httpRemote.url);
const headers = {};
for (const h of httpRemote.headers) {
headers[h.name] = "";
}
return {
url: httpRemote.url,
...Object.keys(headers).length > 0 ? { headers } : {}
};
}
}
const npmPkg = server.packages.find((p) => p.registryType === "npm");
const pypiPkg = server.packages.find((p) => p.registryType === "pypi");
const ociPkg = server.packages.find((p) => p.registryType === "oci");
if (npmPkg) {
validateIdentifier(npmPkg.identifier, "npm");
const rtArgs = normalizeRuntimeArgs(npmPkg.runtimeArguments ?? []);
validateRuntimeArgs(rtArgs);
return {
command: "npx",
args: ["-y", npmPkg.identifier, ...rtArgs]
};
}
if (pypiPkg) {
validateIdentifier(pypiPkg.identifier, "pypi");
const rtArgs = normalizeRuntimeArgs(pypiPkg.runtimeArguments ?? []);
validateRuntimeArgs(rtArgs);
return {
command: "uvx",
args: [pypiPkg.identifier, ...rtArgs]
};
}
if (ociPkg) {
validateIdentifier(ociPkg.identifier, "oci");
const rtArgs = normalizeRuntimeArgs(ociPkg.runtimeArguments ?? []);
validateRuntimeArgs(rtArgs);
return {
command: "docker",
args: ["run", "--rm", "-i", ociPkg.identifier, ...rtArgs]
};
}
if (clientId === "cursor" && server.remotes && server.remotes.length > 0) {
const remote = server.remotes[0];
validateRemoteUrl(remote.url);
return { url: remote.url };
}
throw new Error(
`No install path found for server "${server.name}": no packages and no compatible remotes.`
);
}
function formatTrustScore(trustScore) {
const { score, maxPossible, level, breakdown } = trustScore;
const levelLabel = levelColor(level.toUpperCase());
const bar = scoreBar(score, maxPossible);
const lines = [
`${bar} ${score}/${maxPossible} ${levelLabel}`,
` \u251C\u2500 Health check: ${breakdown.healthCheck > 0 ? "not yet run" : "failed or skipped"}`,
` \u251C\u2500 Tool descriptions: ${breakdown.staticScan === 40 ? "CLEAN (no injection patterns)" : `score ${breakdown.staticScan}/40`}`,
` \u251C\u2500 Package: publisher verification ${breakdown.registryMeta > 0 ? "passed" : "unverified"}`,
` \u2514\u2500 External scan: ${breakdown.externalScan > 0 ? `passed (${breakdown.externalScan}/20)` : "not available (set MCPM_EXTERNAL_SCANNER for deeper analysis)"}`
];
return lines.join("\n");
}
async function handleInstall(name, options, deps) {
const {
registryClient,
detectClients,
getAdapter: getAdapter2,
getConfigPath: getConfigPath2,
scanTier1: scanTier12,
checkScannerAvailable: checkScannerAvailable2,
scanTier2: scanTier22,
computeTrustScore: computeTrustScore2,
addToStore,
confirm: confirm2,
promptEnvVars,
output
} = deps;
const serverEntry = await registryClient.getServer(name);
const statusGate = assessServerStatus(serverEntry);
if (statusGate.blocks) {
if (options.json === true) {
output(
JSON.stringify(
{
name,
error: "server_delisted",
status: statusGate.status,
message: statusGate.statusMessage ?? null
},
null,
2
)
);
}
throw new Error(
`"${name}" has been deleted from the MCP registry${statusGate.statusMessage ? ` (${statusGate.statusMessage})` : ""}. Installation aborted.`
);
}
const tier1Findings = scanTier12(serverEntry);
const scannerAvailable = await checkScannerAvailable2();
let allFindings = [...tier1Findings];
if (scannerAvailable) {
const tier2Findings = await scanTier22(name);
allFindings = [...allFindings, ...tier2Findings];
}
const registryMeta = extractRegistryMeta(serverEntry);
const releaseAge = assessReleaseAge({
publishedAt: registryMeta.publishedAt,
now: (deps.now ?? Date.now)(),
minAgeHours: options.minReleaseAge ?? DEFAULT_MIN_RELEASE_AGE_HOURS
});
if (releaseAge.finding) {
allFindings = [...allFindings, releaseAge.finding];
}
const trustScoreInput = {
findings: allFindings,
healthCheckPassed: null,
// health check not yet run at this point
hasExternalScanner: scannerAvailable,
registryMeta
};
const trustScore = computeTrustScore2(trustScoreInput);
if (options.minTrust !== void 0 && trustScore.score < options.minTrust) {
if (options.json === true) {
output(
JSON.stringify(
{
name,
error: "min_trust_not_met",
score: trustScore.score,
maxPossible: trustScore.maxPossible,
required: options.minTrust,
level: trustScore.level
},
null,
2
)
);
}
throw new Error(
`Trust score ${trustScore.score}/${trustScore.maxPossible} is below the required minimum of ${options.minTrust}. Installation aborted.`
);
}
if (options.minReleaseAge !== void 0 && options.allowFresh !== true && releaseAge.blocksArmedGate) {
if (options.json === true) {
output(
JSON.stringify(
{
name,
error: "release_age_not_met",
ageHours: releaseAge.ageHours,
required: options.minReleaseAge,
reason: releaseAge.status
},
null,
2
)
);
}
const tail = "Installation aborted. Use --allow-fresh to bypass.";
throw new Error(
releaseAge.status === "future" ? `Release publish timestamp is in the future (clock skew or forged metadata); treated as within the ${options.minReleaseAge}-hour minimum release age. ${tail}` : releaseAge.status === "unparseable" ? `Release publish timestamp could not be parsed; treated as within the ${options.minReleaseAge}-hour minimum release age. ${tail}` : releaseAge.status === "absent" ? `Release publish timestamp is missing from the registry metadata, so release age cannot be verified against the ${options.minReleaseAge}-hour minimum. ${tail}` : `Release age ${releaseAge.ageHours}h is below the required minimum of ${options.minReleaseAge}h. ${tail}`
);
}
const jsonMode = options.json === true;
if (!jsonMode) {
output(formatTrustScore(trustScore));
output("");
}
if (options.yes !== true) {
let shouldProceed;
if (trustScore.level === "risky") {
if (!jsonMode) {
output("\x1B[31mWARNING: This server has a low trust score and may be risky to install.\x1B[0m");
output("\x1B[31mSecurity findings indicate potential dangers. Proceed with extreme caution.\x1B[0m");
}
shouldProceed = await confirm2(
"I understand the risks and want to install this server anyway. Continue?"
);
} else if (trustScore.level === "caution") {
if (!jsonMode) {
output("\x1B[33mCAUTION: This server has a moderate trust score. Review the details above.\x1B[0m");
}
shouldProceed = await confirm2(`Install '${name}'? (caution recommended)`);
} else {
shouldProceed = await confirm2(`Install '${name}'?`);
}
if (!shouldProceed) {
if (!jsonMode) output("Installation cancelled.");
return;
}
}
let targetClients = await detectClients();
if (targetClients.length === 0) {
throw new Error(
"No supported AI clients found. Install Claude Desktop, Cursor, VS Code, or Windsurf first."
);
}
if (options.client !== void 0) {
if (!CLIENT_IDS.includes(options.client)) {
throw new Error(
`Unknown client "${options.client}". Valid values: ${CLIENT_IDS.join(", ")}.`
);
}
const requestedId = options.client;
if (!targetClients.includes(requestedId)) {
throw new Error(
`Client "${requestedId}" is not installed on this machine.`
);
}
targetClients = [requestedId];
}
if (options.force !== true) {
for (const clientId of targetClients) {
const adapter = getAdapter2(clientId);
const configPath = getConfigPath2(clientId);
const existing = await adapter.read(configPath);
if (Object.prototype.hasOwnProperty.call(existing, name)) {
throw new Error(
`Server '${name}' is already installed in ${clientId}. Use --force to overwrite.`
);
}
}
}
const { server } = serverEntry;
const bestPkg = server.packages.find((p) => p.registryType === "npm") ?? server.packages.find((p) => p.registryType === "pypi") ?? server.packages.find((p) => p.registryType === "oci") ?? server.packages[0];
const envVarDefs = bestPkg?.environmentVariables ?? [];
const resolvedEnvVars = await promptEnvVars(envVarDefs);
const secretsMode = options.secrets ?? "plaintext";
const { env: envForConfig, storedCount: storedSecretCount } = await applyKeychainSecrets({
serverName: name,
resolvedEnv: resolvedEnvVars,
isSecret: (key) => envVarDefs.find((d) => d.name === key)?.isSecret === true,
mode: secretsMode,
setSecrets: deps.setSecrets
});
const resolvedEntries = /* @__PURE__ */ new Map();
for (const clientId of targetClients) {
resolvedEntries.set(clientId, resolveInstallEntry(serverEntry, clientId));
}
const isUnguardedEntry = [...resolvedEntries.values()].some(
(e) => e.url !== void 0 && e.command === void 0
);
if (isUnguardedEntry) {
if (options.allowUrlServers === false) {
throw new Error(
`Server '${name}' uses a URL/HTTP transport and is not permitted via the MCP surface.`
);
}
const previousConsented = deps.readUnguardedConsent ? await deps.readUnguardedConsent() : [];
const alreadyConsented = previousConsented.includes(name);
const consented = options.allowUnguarded === true || alreadyConsented;
if (!consented) {
throw new Error(
`Server '${name}' uses a URL/HTTP transport and runs UNGUARDED \u2014 no runtime inspection is possible (mcpm's guard relay only wraps stdio servers). Re-run with --allow-unguarded to install it WITHOUT protection.`
);
}
if (!alreadyConsented) {
if (!jsonMode) {
output(
"\x1B[33m\u26A0 UNGUARDED: this URL/HTTP-transport server runs WITHOUT runtime inspection (the guard relay only wraps stdio servers). This grants consent \u2014 it does NOT add protection. The only true fix is a streamable-HTTP relay (not yet implemented).\x1B[0m"
);
}
if (deps.recordUnguardedConsent) {
await deps.recordUnguardedConsent([name]).catch(() => void 0);
}
}
}
const installedClients = [];
for (const clientId of targetClients) {
const adapter = getAdapter2(clientId);
const configPath = getConfigPath2(clientId);
const rawEntry = resolvedEntries.get(clientId);
const entry = {
...rawEntry,
...Object.keys(envForConfig).length > 0 ? { env: { ...rawEntry.env ?? {}, ...envForConfig } } : {}
};
await adapter.addServer(configPath, name, entry, { force: options.force });
installedClients.push(clientId);
}
if (!options.json) {
if (secretsMode === "keychain" && storedSecretCount > 0) {
output(
`\x1B[32mStored ${storedSecretCount} secret(s) encrypted at rest in ~/.mcpm. With an OS keychain this protects against other-user/offline access (not same-user processes); without one a machine-derived key is used that guards casual local inspection only, NOT file exfiltration \u2014 run \`mcpm secrets migrate\` once a keychain is available. Run \`mcpm guard enable\` (then restart your IDE) so they resolve at launch \u2014 until guard wraps this server it receives the literal placeholder.\x1B[0m`
);
} else {
const hasSecrets = envVarDefs.some((ev) => ev.isSecret && resolvedEnvVars[ev.name]);
if (hasSecrets) {
output(
"\x1B[33mNote: API keys are stored as plaintext in client config files. Ensure config files have appropriate permissions (chmod 600).\x1B[0m"
);
}
}
}
const storeEntry = {
name,
version: serverEntry.server.version,
clients: [...installedClients],
installedAt: (/* @__PURE__ */ new Date()).toISOString()
};
await addToStore(storeEntry);
if (options.json === true) {
const result = {
name,
version: serverEntry.server.version,
clients: installedClients,
trustScore: {
score: trustScore.score,
maxPossible: trustScore.maxPossible,
level: trustScore.level
}
};
output(JSON.stringify(result, null, 2));
return;
}
const clientList = installedClients.join(", ");
output(`\x1B[32mInstalled '${name}' successfully into: ${clientList}\x1B[0m`);
}
async function promptEnvVarsDefault(vars) {
if (vars.length === 0) return {};
const result = {};
for (const envVar of vars) {
if (!envVar.isRequired && !envVar.isSecret) continue;
const defaultVal = envVar.default ?? "";
const promptMessage = envVar.description ? `${envVar.name} (${envVar.description}):` : `${envVar.name}:`;
let prompted;
if (envVar.isSecret) {
prompted = await password({ message: promptMessage });
if (!prompted && defaultVal) {
prompted = defaultVal;
}
} else {
prompted = await input({ message: promptMessage, default: defaultVal });
}
if (prompted) {
result[envVar.name] = prompted;
}
}
return result;
}
function parseSecretsMode(raw) {
if (raw !== "keychain" && raw !== "plaintext") {
throw new InvalidArgumentError(
`--secrets must be "keychain" or "plaintext", got: "${raw}"`
);
}
return raw;
}
function parseMinTrust(raw) {
if (!/^\d+$/.test(raw)) {
throw new InvalidArgumentError(
`--min-trust must be an integer between 0 and 100, got: "${raw}"`
);
}
const n = Number(raw);
if (n < 0 || n > 100) {
throw new InvalidArgumentError(
`--min-trust must be an integer between 0 and 100, got: "${raw}"`
);
}
return n;
}
function parseMinReleaseAge(raw) {
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(Number(raw))) {
throw new InvalidArgumentError(
`--min-release-age must be a non-negative integer number of hours, got: "${raw}"`
);
}
return Number(raw);
}
function registerInstallCommand(program) {
program.command("install <name>").description("Install an MCP server from the registry").option("-c, --client <id>", "install to a specific client only").option("-y, --yes", "skip all confirmation prompts").option("-f, --force", "overwrite if server already installed").option("--skip-health-check", "skip post-install health check").option("--json", "output result as JSON").option("--min-trust <n>", "abort install if pre-install trust score is below this threshold (0-100; health check runs after install)", parseMinTrust).option("--min-release-age <hours>", "abort install if the release is younger than this many hours OR its publish timestamp is missing/unparseable (fail-closed when set; also sets the scoring cooldown threshold; bypass with --allow-fresh)", parseMinReleaseAge).option("--allow-fresh", "bypass the --min-release-age gate (including the missing-timestamp block)").option("--secrets <mode>", "where to store secret env vars: 'keychain' (encrypted in ~/.mcpm, resolved by mcpm guard at launch) or 'plaintext' (default)", parseSecretsMode).option("--allow-unguarded", "permit a URL/HTTP-transport server to run WITHOUT runtime guard inspection (no relay wraps a non-stdio transport); records consent so future installs stay quiet").action(async (name, opts) => {
const { RegistryClient } = await import("./client-3RPMRFZL.js");
const client = new RegistryClient();
const { readUnguardedConsent, writeUnguardedConsent, mergeUnguarded } = await import("./unguarded-GJO5WRM7.js");
const installOptions = {
client: opts.client,
yes: opts.yes,
force: opts.force,
skipHealthCheck: opts.skipHealthCheck,
json: opts.json,
minTrust: opts.minTrust,
minReleaseAge: opts.minReleaseAge,
allowFresh: opts.allowFresh,
secrets: opts.secrets,
allowUnguarded: opts.allowUnguarded
};
const installDeps = {
registryClient: client,
detectClients: detectInstalledClients,
getAdapter,
getConfigPath,
scanTier1,
checkScannerAvailable,
scanTier2: (serverName) => scanTier2(serverName),
computeTrustScore,
addToStore: addInstalledServer,
confirm,
promptEnvVars: promptEnvVarsDefault,
output: stdoutOutput,
setSecrets,
now: () => Date.now(),
readUnguardedConsent,
recordUnguardedConsent: async (names) => {
const previous = await readUnguardedConsent();
await writeUnguardedConsent(mergeUnguarded(previous, names));
}
};
try {
await handleInstall(name, installOptions, installDeps);
} catch (err) {
if (installOptions.json !== true) {
console.error(chalk.red(err.message));
}
process.exit(1);
}
});
}
export {
validateRemoteUrl,
resolveInstallEntry,
parseSecretsMode,
parseMinTrust,
registerInstallCommand
};
//# sourceMappingURL=chunk-LNGTDYGN.js.map
{"version":3,"sources":["../src/commands/install.ts"],"sourcesContent":["/**\n * `mcpm install <name>` command handler.\n *\n * Wires together: registry fetch → trust assessment → user confirmation →\n * client detection → env var prompting → config write → store record.\n *\n * All external dependencies are injected for testability.\n *\n * Exports:\n * - handleInstall() — injectable handler for testing\n * - resolveInstallEntry() — pure function: ServerEntry + ClientId → McpServerEntry\n * - formatTrustScore() — pure function: TrustScore → formatted string\n * - registerInstallCommand() — Commander registration\n */\n\nimport { CLIENT_IDS } from \"../config/paths.js\";\nimport type { ClientId } from \"../config/paths.js\";\nimport type { ConfigAdapter, McpServerEntry } from \"../config/adapters/index.js\";\nimport type { ServerEntry, EnvVar } from \"../registry/types.js\";\nimport { argvTokens, type RuntimeArgument } from \"../registry/argument-tokens.js\";\nimport type { Finding } from \"../scanner/tier1.js\";\nimport type { TrustScore, TrustScoreInput } from \"../scanner/trust-score.js\";\nimport type { InstalledServer } from \"../store/servers.js\";\nimport { scoreBar, levelColor, extractRegistryMeta } from \"../utils/format-trust.js\";\nimport { assessReleaseAge, DEFAULT_MIN_RELEASE_AGE_HOURS } from \"../scanner/cooldown.js\";\nimport { assessServerStatus } from \"../scanner/registry-status.js\";\nimport { DANGEROUS_FLAG_PREFIXES } from \"../scanner/patterns.js\";\nimport { applyKeychainSecrets, type SecretsMode, setSecrets as _setSecrets } from \"../store/keychain.js\";\n\n// ---------------------------------------------------------------------------\n// URL validation — guard against malicious remote URLs\n// ---------------------------------------------------------------------------\n\n/**\n * Validate a remote URL before it is written to any IDE config file.\n * Only http: and https: protocols are permitted.\n */\nexport function validateRemoteUrl(url: string): void {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new Error(`Invalid remote URL: \"${url}\"`);\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") {\n throw new Error(\n `Remote URL must use http or https protocol, got: \"${parsed.protocol}\"`\n );\n }\n // M4a: plaintext http to a non-loopback host is interceptable once written to an\n // IDE config. Allow http only for loopback (local dev servers); require https for\n // every other host. https is always allowed.\n if (parsed.protocol === \"http:\" && !isLoopbackHost(parsed.hostname)) {\n throw new Error(\n `Remote URL must use https for non-loopback hosts (plaintext http is ` +\n `vulnerable to interception), got: \"${url}\"`\n );\n }\n}\n\n/**\n * True for localhost / loopback literals, where plaintext http is acceptable.\n * Recognizes localhost / *.localhost / 127.0.0.1 / ::1. Exotic loopback spellings\n * (IPv4-mapped `::ffff:127.0.0.1`, `127.x.x.x`, decimal/octal/hex IPs) are NOT\n * recognized and fall through to the https requirement — over-rejection only, never\n * a bypass (a non-loopback host can never be mistaken for loopback).\n */\nfunction isLoopbackHost(hostname: string): boolean {\n const h = hostname.toLowerCase().replace(/^\\[|\\]$/g, \"\"); // strip IPv6 brackets\n return (\n h === \"localhost\" ||\n h.endsWith(\".localhost\") ||\n h === \"127.0.0.1\" ||\n h === \"::1\"\n );\n}\n\nconst NPM_IDENTIFIER_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\\/)?[a-z0-9-~][a-z0-9-._~]*$/;\nconst PYPI_IDENTIFIER_RE = /^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$/;\nconst OCI_IDENTIFIER_RE =\n /^[a-z0-9]+([._-][a-z0-9]+)*(\\/[a-z0-9]+([._-][a-z0-9]+)*)*:[a-zA-Z0-9._-]+$/;\n\n/**\n * Validate a package identifier against the expected pattern for its registry\n * type. Throws if the identifier looks potentially malicious.\n */\nexport function validateIdentifier(identifier: string, registryType: string): void {\n const patterns: Record<string, RegExp> = {\n npm: NPM_IDENTIFIER_RE,\n pypi: PYPI_IDENTIFIER_RE,\n oci: OCI_IDENTIFIER_RE,\n };\n const re = patterns[registryType];\n if (re && !re.test(identifier)) {\n throw new Error(\n `Rejected potentially malicious ${registryType} identifier: \"${identifier}\"`\n );\n }\n}\n\n/**\n * Render runtimeArguments from the registry into a launch argv slice.\n *\n * Delegates to argvTokens (name + value, never valueHint) so the SAME function\n * defines both what gets executed here and what the F4 dangerous-flag scan\n * matches in scanner/patterns.ts — they cannot diverge. valueHint (a\n * documentation placeholder like \"directory\") is deliberately not rendered:\n * emitting it would inject a bogus literal argument. The injection scanner\n * (scanner/tier1.ts) uses argumentTokens instead, which DOES read valueHint as\n * user-facing text; that divergence is intentional and documented there.\n */\nfunction normalizeRuntimeArgs(\n args: ReadonlyArray<RuntimeArgument>\n): string[] {\n return args.flatMap(argvTokens);\n}\n\n/**\n * Allowlist of safe runtime argument shapes.\n * After dangerous flags are rejected, arguments must match one of these\n * patterns. This blocks shell metacharacters and path traversal while\n * allowing the wide range of flags real MCP servers use.\n */\nconst SAFE_ARG_PATTERNS: readonly RegExp[] = [\n // Generic boolean flags (--allow-write, --read-only, --no-sandbox, etc.)\n /^--[a-zA-Z][\\w-]*$/,\n // Single-dash short flags the live registry legitimately declares (-i, -y, -p).\n // EXACTLY one alpha char — no bundled tail. Allowing a tail (-rmodule, -eCODE)\n // would let a dangerous flag bundle its payload and slip past the Layer-1\n // DANGEROUS_FLAG_PREFIXES check, which only rejects the exact token (-e/-r) or\n // its '=' form. The live registry's short flags are all single-letter, so the\n // narrow form loses no real coverage while closing the bundling bypass.\n /^-[a-zA-Z]$/,\n // Generic --key=value flags with safe value characters\n // Blocks shell metacharacters: ; | $ ` & ( ) { } < > ! ' \"\n /^--[a-zA-Z][\\w-]+=[\\w./@:, -]+$/,\n // Bare absolute paths (Unix: /path/to/dir)\n /^\\/[\\w.@/ -]+$/,\n // Home-relative paths (~/Documents)\n /^~[\\w.@/ -]*$/,\n // Bare positional arguments (no dashes, no path traversal)\n /^[a-zA-Z0-9][\\w.@/-]*$/,\n];\n\n/**\n * Validate runtime arguments from the registry.\n * Two-layer defense: reject known-dangerous Node.js flags first,\n * then require remaining args to match safe structural patterns.\n */\nexport function validateRuntimeArgs(args: string[]): void {\n for (const arg of args) {\n // Layer 0 (M4b): reject a \"..\" path-traversal segment anywhere in the argument\n // — \"../x\", \"a/../../etc/passwd\", \"--config=../secret\". A \"..\" segment is one\n // bounded by start-of-arg, \"=\" (flag value), or a path separator on the left,\n // and a separator or end-of-arg on the right. The Layer-2 allowlist permits \".\"\n // and \"/\" inside values, so without this a traversal would slip through; a\n // non-traversal double dot like \"--range=1..10\" is left untouched.\n if (/(?:^|[=\\\\/])\\.\\.(?:[\\\\/]|$)/.test(arg)) {\n throw new Error(`Rejected path traversal in runtime argument: \"${arg}\"`);\n }\n\n // Layer 1: reject dangerous Node.js flags\n const isDangerous = DANGEROUS_FLAG_PREFIXES.some(\n (prefix) => arg === prefix || arg.startsWith(`${prefix}=`)\n );\n if (isDangerous) {\n throw new Error(`Rejected dangerous runtime argument: \"${arg}\"`);\n }\n\n // Layer 2: require safe structural pattern\n const isSafe = SAFE_ARG_PATTERNS.some((pattern) => pattern.test(arg));\n if (!isSafe) {\n throw new Error(`Rejected unrecognized runtime argument: \"${arg}\"`);\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport interface InstallOptions {\n client?: string;\n yes?: boolean;\n force?: boolean;\n skipHealthCheck?: boolean;\n json?: boolean;\n minTrust?: number;\n minReleaseAge?: number;\n allowFresh?: boolean;\n secrets?: SecretsMode;\n /**\n * H9 (fail-closed): per-invocation consent (`--allow-unguarded`) to install a\n * URL/HTTP-transport server that runs UNGUARDED (the guard relay only wraps a\n * stdio transport — a non-stdio remote gets ZERO runtime inspection). When\n * neither this nor a name already in the persistent consent store grants it,\n * such a server is DENIED. DISTINCT from `allowUrlServers`, the MCP-surface\n * kill-switch: `allowUrlServers === false` ALWAYS wins.\n */\n allowUnguarded?: boolean;\n /**\n * Whether URL/HTTP-transport servers may be installed at all. DEFAULT\n * (undefined/true) preserves CLI behavior. The MCP surface passes `false` so a\n * url-transport server is recorded as blocked instead of written to a config —\n * an untrusted caller can never reach the unguarded run path.\n */\n allowUrlServers?: boolean;\n}\n\nexport interface InstallDeps {\n registryClient: { getServer: (name: string) => Promise<ServerEntry> };\n detectClients: () => Promise<ClientId[]>;\n getAdapter: (clientId: ClientId) => ConfigAdapter;\n getConfigPath: (clientId: ClientId) => string;\n scanTier1: (server: ServerEntry) => Finding[];\n checkScannerAvailable: () => Promise<boolean>;\n scanTier2: (name: string) => Promise<Finding[]>;\n computeTrustScore: (input: TrustScoreInput) => TrustScore;\n addToStore: (server: InstalledServer) => Promise<void>;\n confirm: (message: string) => Promise<boolean>;\n promptEnvVars: (vars: EnvVar[]) => Promise<Record<string, string>>;\n output: (text: string) => void;\n /** Optional; required only when options.secrets === \"keychain\". */\n setSecrets?: (server: string, values: Record<string, string>) => Promise<void>;\n /** Epoch-ms clock for release-age assessment; defaults to Date.now at the CLI boundary. */\n now?: () => number;\n /**\n * H9: read the persistent set of server names previously consented to run\n * unguarded. Injectable for tests; defaults to the real store at the CLI\n * boundary. When omitted, no server is treated as previously-consented.\n */\n readUnguardedConsent?: () => Promise<string[]>;\n /**\n * H9: persist (union into the store) the name newly consented to run\n * unguarded. Injectable for tests; defaults to the real store. Called once\n * after a url server is installed under fresh consent.\n */\n recordUnguardedConsent?: (names: readonly string[]) => Promise<void>;\n}\n\n// ---------------------------------------------------------------------------\n// resolveInstallEntry — pure function, no I/O\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve the McpServerEntry for a given server + clientId.\n *\n * Decision tree:\n * 1. Cursor + server has HTTP remote → produce { url, headers } entry\n * 2. Otherwise pick from packages[]: npm → pypi → oci (first available)\n * 3. npm: { command: 'npx', args: ['-y', identifier, ...runtimeArgs], env }\n * 4. pypi: { command: 'uvx', args: [identifier, ...runtimeArgs], env }\n * 5. docker: { command: 'docker', args: ['run', '--rm', '-i', image], env }\n * 6. If no packages and no usable remote: throw\n */\nexport function resolveInstallEntry(\n serverEntry: ServerEntry,\n clientId: ClientId\n): McpServerEntry {\n const { server } = serverEntry;\n\n // Rule 1: Cursor + HTTP remote → streamable-http entry\n if (clientId === \"cursor\" && server.remotes && server.remotes.length > 0) {\n const httpRemote = server.remotes.find(\n (r) => r.type === \"streamable-http\" || r.type === \"sse\"\n );\n if (httpRemote) {\n validateRemoteUrl(httpRemote.url);\n // Build headers record if any\n const headers: Record<string, string> = {};\n for (const h of httpRemote.headers) {\n headers[h.name] = \"\";\n }\n return {\n url: httpRemote.url,\n ...(Object.keys(headers).length > 0 ? { headers } : {}),\n };\n }\n }\n\n // Rule 2: Pick best package by priority: npm → pypi → oci\n const npmPkg = server.packages.find((p) => p.registryType === \"npm\");\n const pypiPkg = server.packages.find((p) => p.registryType === \"pypi\");\n const ociPkg = server.packages.find((p) => p.registryType === \"oci\");\n\n if (npmPkg) {\n validateIdentifier(npmPkg.identifier, \"npm\");\n const rtArgs = normalizeRuntimeArgs(npmPkg.runtimeArguments ?? []);\n validateRuntimeArgs(rtArgs);\n return {\n command: \"npx\",\n args: [\"-y\", npmPkg.identifier, ...rtArgs],\n };\n }\n\n if (pypiPkg) {\n validateIdentifier(pypiPkg.identifier, \"pypi\");\n const rtArgs = normalizeRuntimeArgs(pypiPkg.runtimeArguments ?? []);\n validateRuntimeArgs(rtArgs);\n return {\n command: \"uvx\",\n args: [pypiPkg.identifier, ...rtArgs],\n };\n }\n\n if (ociPkg) {\n validateIdentifier(ociPkg.identifier, \"oci\");\n const rtArgs = normalizeRuntimeArgs(ociPkg.runtimeArguments ?? []);\n validateRuntimeArgs(rtArgs);\n return {\n command: \"docker\",\n args: [\"run\", \"--rm\", \"-i\", ociPkg.identifier, ...rtArgs],\n };\n }\n\n // Rule 3: Cursor-only path — HTTP remote with no packages\n if (clientId === \"cursor\" && server.remotes && server.remotes.length > 0) {\n const remote = server.remotes[0];\n validateRemoteUrl(remote.url);\n return { url: remote.url };\n }\n\n throw new Error(\n `No install path found for server \"${server.name}\": no packages and no compatible remotes.`\n );\n}\n\n// ---------------------------------------------------------------------------\n// formatTrustScore — pure function, rich display\n// ---------------------------------------------------------------------------\n\n/**\n * Format a trust score as a visual progress bar with breakdown details.\n */\nexport function formatTrustScore(trustScore: TrustScore): string {\n const { score, maxPossible, level, breakdown } = trustScore;\n\n const levelLabel = levelColor(level.toUpperCase());\n const bar = scoreBar(score, maxPossible);\n\n const lines: string[] = [\n `${bar} ${score}/${maxPossible} ${levelLabel}`,\n ` \\u251C\\u2500 Health check: ${breakdown.healthCheck > 0 ? \"not yet run\" : \"failed or skipped\"}`,\n ` \\u251C\\u2500 Tool descriptions: ${breakdown.staticScan === 40 ? \"CLEAN (no injection patterns)\" : `score ${breakdown.staticScan}/40`}`,\n ` \\u251C\\u2500 Package: publisher verification ${breakdown.registryMeta > 0 ? \"passed\" : \"unverified\"}`,\n ` \\u2514\\u2500 External scan: ${breakdown.externalScan > 0 ? `passed (${breakdown.externalScan}/20)` : \"not available (set MCPM_EXTERNAL_SCANNER for deeper analysis)\"}`,\n ];\n\n return lines.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// handleInstall — main handler\n// ---------------------------------------------------------------------------\n\n/**\n * Core handler for `mcpm install <name>`.\n * All dependencies are injected for hermetic testability.\n */\nexport async function handleInstall(\n name: string,\n options: InstallOptions,\n deps: InstallDeps\n): Promise<void> {\n const {\n registryClient,\n detectClients,\n getAdapter,\n getConfigPath,\n scanTier1,\n checkScannerAvailable,\n scanTier2,\n computeTrustScore,\n addToStore,\n confirm,\n promptEnvVars,\n output,\n } = deps;\n\n // -------------------------------------------------------------------------\n // Step 1: Fetch server metadata\n // -------------------------------------------------------------------------\n const serverEntry = await registryClient.getServer(name);\n\n // -------------------------------------------------------------------------\n // Step 1b: registry-delisting gate (fail closed, before any scan/output)\n // -------------------------------------------------------------------------\n // If the registry itself marks this server \"deleted\" (removed/withdrawn),\n // refuse to install. Fail-SAFE: ONLY an explicit \"deleted\" blocks; a\n // \"deprecated\" or absent/unknown status does not (surfaced as an advisory\n // finding by scanTier1 instead). See scanner/registry-status.ts.\n const statusGate = assessServerStatus(serverEntry);\n if (statusGate.blocks) {\n if (options.json === true) {\n output(\n JSON.stringify(\n {\n name,\n error: \"server_delisted\",\n status: statusGate.status,\n message: statusGate.statusMessage ?? null,\n },\n null,\n 2\n )\n );\n }\n throw new Error(\n `\"${name}\" has been deleted from the MCP registry${statusGate.statusMessage ? ` (${statusGate.statusMessage})` : \"\"}. Installation aborted.`\n );\n }\n\n // -------------------------------------------------------------------------\n // Step 2: Trust assessment\n // -------------------------------------------------------------------------\n const tier1Findings = scanTier1(serverEntry);\n const scannerAvailable = await checkScannerAvailable();\n\n let allFindings: Finding[] = [...tier1Findings];\n if (scannerAvailable) {\n const tier2Findings = await scanTier2(name);\n allFindings = [...allFindings, ...tier2Findings];\n }\n\n // Release-age cooldown: assessed ONCE so the score finding and the Step 2c\n // gate can never disagree — passing --min-release-age below 24 therefore also\n // lowers the scoring cooldown threshold (documented in the flag help text).\n // The medium finding lands unconditionally for fresh releases, with or\n // without the gate — that is the inversion fix, independent of the gate.\n const registryMeta = extractRegistryMeta(serverEntry);\n const releaseAge = assessReleaseAge({\n publishedAt: registryMeta.publishedAt,\n now: (deps.now ?? Date.now)(),\n minAgeHours: options.minReleaseAge ?? DEFAULT_MIN_RELEASE_AGE_HOURS,\n });\n if (releaseAge.finding) {\n allFindings = [...allFindings, releaseAge.finding];\n }\n\n const trustScoreInput: TrustScoreInput = {\n findings: allFindings,\n healthCheckPassed: null, // health check not yet run at this point\n hasExternalScanner: scannerAvailable,\n registryMeta,\n };\n\n const trustScore = computeTrustScore(trustScoreInput);\n\n // -------------------------------------------------------------------------\n // Step 2b: --min-trust gate (checked before any output or confirmation)\n // -------------------------------------------------------------------------\n if (options.minTrust !== undefined && trustScore.score < options.minTrust) {\n if (options.json === true) {\n output(\n JSON.stringify(\n {\n name,\n error: \"min_trust_not_met\",\n score: trustScore.score,\n maxPossible: trustScore.maxPossible,\n required: options.minTrust,\n level: trustScore.level,\n },\n null,\n 2\n )\n );\n }\n // The denominator is 80 unless the external-scanner bucket was credited, which is\n // the default — so a hardcoded /100 understated every score by 20 points of scale\n // and made a flawless 62/80 (77.5%) read as 62%.\n throw new Error(\n `Trust score ${trustScore.score}/${trustScore.maxPossible} is below the required minimum of ${options.minTrust}. Installation aborted.`\n );\n }\n\n // -------------------------------------------------------------------------\n // Step 2c: --min-release-age gate (checked before any output or confirmation)\n // -------------------------------------------------------------------------\n // Fail-closed when armed: a MISSING publish timestamp blocks too (blocksArmedGate)\n // — otherwise a registry/compromised mirror could defeat the gate by omitting\n // _meta (publishedAt is .optional() in OfficialMetaSchema). The score finding\n // stays fail-open for absent; only the explicitly armed gate hardens.\n if (\n options.minReleaseAge !== undefined &&\n options.allowFresh !== true &&\n releaseAge.blocksArmedGate\n ) {\n if (options.json === true) {\n output(\n JSON.stringify(\n {\n name,\n error: \"release_age_not_met\",\n ageHours: releaseAge.ageHours,\n required: options.minReleaseAge,\n reason: releaseAge.status,\n },\n null,\n 2\n )\n );\n }\n const tail = \"Installation aborted. Use --allow-fresh to bypass.\";\n throw new Error(\n releaseAge.status === \"future\"\n ? `Release publish timestamp is in the future (clock skew or forged metadata); treated as within the ${options.minReleaseAge}-hour minimum release age. ${tail}`\n : releaseAge.status === \"unparseable\"\n ? `Release publish timestamp could not be parsed; treated as within the ${options.minReleaseAge}-hour minimum release age. ${tail}`\n : releaseAge.status === \"absent\"\n ? `Release publish timestamp is missing from the registry metadata, so release age cannot be verified against the ${options.minReleaseAge}-hour minimum. ${tail}`\n : `Release age ${releaseAge.ageHours}h is below the required minimum of ${options.minReleaseAge}h. ${tail}`\n );\n }\n\n // -------------------------------------------------------------------------\n // Step 3: Display trust score and confirm\n // -------------------------------------------------------------------------\n // In --json mode suppress all human-readable output; only the final JSON\n // is written to stdout.\n const jsonMode = options.json === true;\n\n if (!jsonMode) {\n output(formatTrustScore(trustScore));\n output(\"\");\n }\n\n if (options.yes !== true) {\n let shouldProceed: boolean;\n\n if (trustScore.level === \"risky\") {\n if (!jsonMode) {\n output(\"\\u001b[31mWARNING: This server has a low trust score and may be risky to install.\\u001b[0m\");\n output(\"\\u001b[31mSecurity findings indicate potential dangers. Proceed with extreme caution.\\u001b[0m\");\n }\n shouldProceed = await confirm(\n \"I understand the risks and want to install this server anyway. Continue?\"\n );\n } else if (trustScore.level === \"caution\") {\n if (!jsonMode) {\n output(\"\\u001b[33mCAUTION: This server has a moderate trust score. Review the details above.\\u001b[0m\");\n }\n shouldProceed = await confirm(`Install '${name}'? (caution recommended)`);\n } else {\n // GREEN — brief display, proceed\n shouldProceed = await confirm(`Install '${name}'?`);\n }\n\n if (!shouldProceed) {\n if (!jsonMode) output(\"Installation cancelled.\");\n return;\n }\n }\n\n // -------------------------------------------------------------------------\n // Step 4: Detect and filter clients\n // -------------------------------------------------------------------------\n let targetClients = await detectClients();\n\n if (targetClients.length === 0) {\n throw new Error(\n \"No supported AI clients found. Install Claude Desktop, Cursor, VS Code, or Windsurf first.\"\n );\n }\n\n if (options.client !== undefined) {\n if (!CLIENT_IDS.includes(options.client as ClientId)) {\n throw new Error(\n `Unknown client \"${options.client}\". Valid values: ${CLIENT_IDS.join(\", \")}.`\n );\n }\n const requestedId = options.client as ClientId;\n if (!targetClients.includes(requestedId)) {\n throw new Error(\n `Client \"${requestedId}\" is not installed on this machine.`\n );\n }\n targetClients = [requestedId];\n }\n\n // -------------------------------------------------------------------------\n // Step 5: Check for already-installed (unless --force)\n // -------------------------------------------------------------------------\n if (options.force !== true) {\n for (const clientId of targetClients) {\n const adapter = getAdapter(clientId);\n const configPath = getConfigPath(clientId);\n const existing = await adapter.read(configPath);\n if (Object.prototype.hasOwnProperty.call(existing, name)) {\n throw new Error(\n `Server '${name}' is already installed in ${clientId}. Use --force to overwrite.`\n );\n }\n }\n }\n\n // -------------------------------------------------------------------------\n // Step 6: Resolve env vars to prompt for\n // -------------------------------------------------------------------------\n // Collect env vars from the best-match package\n const { server } = serverEntry;\n const bestPkg =\n server.packages.find((p) => p.registryType === \"npm\") ??\n server.packages.find((p) => p.registryType === \"pypi\") ??\n server.packages.find((p) => p.registryType === \"oci\") ??\n server.packages[0];\n\n const envVarDefs: EnvVar[] = bestPkg?.environmentVariables ?? [];\n const resolvedEnvVars = await promptEnvVars(envVarDefs);\n\n // Step 6b: In keychain mode, persist secret-flagged values encrypted and swap\n // them for `mcpm:keychain:…` placeholders, so no plaintext is written to any\n // client config. Non-secret vars stay inline; each secret is stored once and\n // reused for every client. The placeholder resolves at launch only while mcpm\n // guard wraps the server (run-inner.ts → resolveEnvPlaceholders). The swap\n // (and the \"no plaintext in config\" invariant) lives in applyKeychainSecrets.\n const secretsMode: SecretsMode = options.secrets ?? \"plaintext\";\n const { env: envForConfig, storedCount: storedSecretCount } = await applyKeychainSecrets({\n serverName: name,\n resolvedEnv: resolvedEnvVars,\n isSecret: (key) => envVarDefs.find((d) => d.name === key)?.isSecret === true,\n mode: secretsMode,\n setSecrets: deps.setSecrets,\n });\n\n // -------------------------------------------------------------------------\n // Step 7: Resolve (and thereby validate) each client's entry up front\n // -------------------------------------------------------------------------\n // resolveInstallEntry throws on an invalid identifier, so resolving here\n // before any config is written preserves fail-fast validation. The resolved\n // entries are reused in Step 8 to avoid recomputing them.\n const resolvedEntries = new Map<ClientId, McpServerEntry>();\n for (const clientId of targetClients) {\n resolvedEntries.set(clientId, resolveInstallEntry(serverEntry, clientId));\n }\n\n // -------------------------------------------------------------------------\n // Step 7b: H9 fail-closed gate for URL/HTTP-transport servers\n // -------------------------------------------------------------------------\n // A resolved entry with a `url` and no `command` runs UNGUARDED — the guard\n // relay only wraps a stdio process, so a non-stdio remote gets ZERO runtime\n // inspection. Mirror processUrlServer (up.ts): the MCP-surface kill-switch\n // (allowUrlServers === false) ALWAYS wins; otherwise DENY unless explicit\n // informed consent (`--allow-unguarded` this run, or a name already in the\n // persistent consent store). This is informed consent, NOT protection.\n const isUnguardedEntry = [...resolvedEntries.values()].some(\n (e) => e.url !== undefined && e.command === undefined\n );\n if (isUnguardedEntry) {\n if (options.allowUrlServers === false) {\n throw new Error(\n `Server '${name}' uses a URL/HTTP transport and is not permitted via the MCP surface.`\n );\n }\n const previousConsented = deps.readUnguardedConsent\n ? await deps.readUnguardedConsent()\n : [];\n const alreadyConsented = previousConsented.includes(name);\n const consented = options.allowUnguarded === true || alreadyConsented;\n if (!consented) {\n throw new Error(\n `Server '${name}' uses a URL/HTTP transport and runs UNGUARDED — no runtime ` +\n `inspection is possible (mcpm's guard relay only wraps stdio servers). ` +\n `Re-run with --allow-unguarded to install it WITHOUT protection.`\n );\n }\n // First-time consent: warn once and persist so a future install stays quiet.\n if (!alreadyConsented) {\n if (!jsonMode) {\n output(\n \"\\x1b[33m⚠ UNGUARDED: this URL/HTTP-transport server runs WITHOUT runtime \" +\n \"inspection (the guard relay only wraps stdio servers). This grants consent — \" +\n \"it does NOT add protection. The only true fix is a streamable-HTTP relay \" +\n \"(not yet implemented).\\x1b[0m\"\n );\n }\n if (deps.recordUnguardedConsent) {\n await deps.recordUnguardedConsent([name]).catch(() => undefined);\n }\n }\n }\n\n // -------------------------------------------------------------------------\n // Step 8: Write config to each client and record in store\n // -------------------------------------------------------------------------\n const installedClients: ClientId[] = [];\n\n for (const clientId of targetClients) {\n const adapter = getAdapter(clientId);\n const configPath = getConfigPath(clientId);\n const rawEntry = resolvedEntries.get(clientId)!;\n\n // Merge env vars into the entry (immutable). In keychain mode envForConfig\n // carries placeholders in place of secret values; otherwise it === resolvedEnvVars.\n const entry: McpServerEntry = {\n ...rawEntry,\n ...(Object.keys(envForConfig).length > 0\n ? { env: { ...(rawEntry.env ?? {}), ...envForConfig } }\n : {}),\n };\n\n await adapter.addServer(configPath, name, entry, { force: options.force });\n installedClients.push(clientId);\n }\n\n // -------------------------------------------------------------------------\n // Step 8b: Secret-storage notice\n // -------------------------------------------------------------------------\n if (!options.json) {\n if (secretsMode === \"keychain\" && storedSecretCount > 0) {\n output(\n `\\x1b[32mStored ${storedSecretCount} secret(s) encrypted at rest in ~/.mcpm. ` +\n \"With an OS keychain this protects against other-user/offline access (not \" +\n \"same-user processes); without one a machine-derived key is used that guards \" +\n \"casual local inspection only, NOT file exfiltration — run `mcpm secrets migrate` \" +\n \"once a keychain is available. \" +\n \"Run `mcpm guard enable` (then restart your IDE) so they resolve at launch — \" +\n \"until guard wraps this server it receives the literal placeholder.\\x1b[0m\"\n );\n } else {\n const hasSecrets = envVarDefs.some((ev) => ev.isSecret && resolvedEnvVars[ev.name]);\n if (hasSecrets) {\n output(\n \"\\x1b[33mNote: API keys are stored as plaintext in client config files. \" +\n \"Ensure config files have appropriate permissions (chmod 600).\\x1b[0m\"\n );\n }\n }\n }\n\n // -------------------------------------------------------------------------\n // Step 9: Record in store\n // -------------------------------------------------------------------------\n const storeEntry: InstalledServer = {\n name,\n version: serverEntry.server.version,\n clients: [...installedClients],\n installedAt: new Date().toISOString(),\n };\n await addToStore(storeEntry);\n\n // -------------------------------------------------------------------------\n // Step 10: Output result\n // -------------------------------------------------------------------------\n if (options.json === true) {\n const result = {\n name,\n version: serverEntry.server.version,\n clients: installedClients,\n trustScore: {\n score: trustScore.score,\n maxPossible: trustScore.maxPossible,\n level: trustScore.level,\n },\n };\n output(JSON.stringify(result, null, 2));\n return;\n }\n\n const clientList = installedClients.join(\", \");\n output(`\\u001b[32mInstalled '${name}' successfully into: ${clientList}\\u001b[0m`);\n}\n\n// ---------------------------------------------------------------------------\n// Commander registration\n// ---------------------------------------------------------------------------\n\nimport { Command, InvalidArgumentError } from \"commander\";\nimport chalk from \"chalk\";\nimport { input, password } from \"@inquirer/prompts\";\nimport { detectInstalledClients as _detectClients } from \"../config/detector.js\";\nimport { getConfigPath as _getConfigPath } from \"../config/paths.js\";\nimport { addInstalledServer as _addToStore } from \"../store/servers.js\";\nimport { scanTier1 as _scanTier1 } from \"../scanner/tier1.js\";\nimport { checkScannerAvailable as _checkScannerAvailable, scanTier2 as _scanTier2 } from \"../scanner/tier2.js\";\nimport { computeTrustScore as _computeTrustScore } from \"../scanner/trust-score.js\";\nimport { getAdapter as getAdapterDefault } from \"../config/index.js\";\nimport { confirm } from \"../utils/confirm.js\";\nimport { stdoutOutput } from \"../utils/output.js\";\n\nasync function promptEnvVarsDefault(\n vars: EnvVar[]\n): Promise<Record<string, string>> {\n if (vars.length === 0) return {};\n\n const result: Record<string, string> = {};\n for (const envVar of vars) {\n if (!envVar.isRequired && !envVar.isSecret) continue;\n\n const defaultVal = envVar.default ?? \"\";\n const promptMessage = envVar.description\n ? `${envVar.name} (${envVar.description}):`\n : `${envVar.name}:`;\n\n let prompted: string;\n if (envVar.isSecret) {\n // Use password prompt to mask secret input — value is never echoed to the terminal\n prompted = await password({ message: promptMessage });\n if (!prompted && defaultVal) {\n prompted = defaultVal;\n }\n } else {\n prompted = await input({ message: promptMessage, default: defaultVal });\n }\n\n if (prompted) {\n result[envVar.name] = prompted;\n }\n }\n return result;\n}\n\nexport function parseSecretsMode(raw: string): SecretsMode {\n if (raw !== \"keychain\" && raw !== \"plaintext\") {\n throw new InvalidArgumentError(\n `--secrets must be \"keychain\" or \"plaintext\", got: \"${raw}\"`\n );\n }\n return raw;\n}\n\nexport function parseMinTrust(raw: string): number {\n // Reject anything that isn't plain decimal digits (blocks hex \"0x50\", scientific\n // notation \"1e2\", spaces, empty string, and negative sign before range check).\n if (!/^\\d+$/.test(raw)) {\n throw new InvalidArgumentError(\n `--min-trust must be an integer between 0 and 100, got: \"${raw}\"`\n );\n }\n const n = Number(raw);\n if (n < 0 || n > 100) {\n throw new InvalidArgumentError(\n `--min-trust must be an integer between 0 and 100, got: \"${raw}\"`\n );\n }\n return n;\n}\n\nexport function parseMinReleaseAge(raw: string): number {\n // Same regex-first discipline as parseMinTrust: blocks hex \"0x18\", \"1e2\",\n // spaces, empty string, negatives. Safe-integer check guards absurd lengths.\n if (!/^\\d+$/.test(raw) || !Number.isSafeInteger(Number(raw))) {\n throw new InvalidArgumentError(\n `--min-release-age must be a non-negative integer number of hours, got: \"${raw}\"`\n );\n }\n return Number(raw);\n}\n\nexport function registerInstallCommand(program: Command): void {\n program\n .command(\"install <name>\")\n .description(\"Install an MCP server from the registry\")\n .option(\"-c, --client <id>\", \"install to a specific client only\")\n .option(\"-y, --yes\", \"skip all confirmation prompts\")\n .option(\"-f, --force\", \"overwrite if server already installed\")\n .option(\"--skip-health-check\", \"skip post-install health check\")\n .option(\"--json\", \"output result as JSON\")\n .option(\"--min-trust <n>\", \"abort install if pre-install trust score is below this threshold (0-100; health check runs after install)\", parseMinTrust)\n .option(\"--min-release-age <hours>\", \"abort install if the release is younger than this many hours OR its publish timestamp is missing/unparseable (fail-closed when set; also sets the scoring cooldown threshold; bypass with --allow-fresh)\", parseMinReleaseAge)\n .option(\"--allow-fresh\", \"bypass the --min-release-age gate (including the missing-timestamp block)\")\n .option(\"--secrets <mode>\", \"where to store secret env vars: 'keychain' (encrypted in ~/.mcpm, resolved by mcpm guard at launch) or 'plaintext' (default)\", parseSecretsMode)\n .option(\"--allow-unguarded\", \"permit a URL/HTTP-transport server to run WITHOUT runtime guard inspection (no relay wraps a non-stdio transport); records consent so future installs stay quiet\")\n .action(async (name: string, opts: { client?: string; yes?: boolean; force?: boolean; skipHealthCheck?: boolean; json?: boolean; minTrust?: number; minReleaseAge?: number; allowFresh?: boolean; secrets?: SecretsMode; allowUnguarded?: boolean }) => {\n const { RegistryClient } = await import(\"../registry/client.js\");\n const client = new RegistryClient();\n const { readUnguardedConsent, writeUnguardedConsent, mergeUnguarded } = await import(\n \"../guard/unguarded.js\"\n );\n\n const installOptions: InstallOptions = {\n client: opts.client,\n yes: opts.yes,\n force: opts.force,\n skipHealthCheck: opts.skipHealthCheck,\n json: opts.json,\n minTrust: opts.minTrust,\n minReleaseAge: opts.minReleaseAge,\n allowFresh: opts.allowFresh,\n secrets: opts.secrets,\n allowUnguarded: opts.allowUnguarded,\n };\n\n const installDeps: InstallDeps = {\n registryClient: client,\n detectClients: _detectClients,\n getAdapter: getAdapterDefault,\n getConfigPath: _getConfigPath,\n scanTier1: _scanTier1,\n checkScannerAvailable: _checkScannerAvailable,\n scanTier2: (serverName: string) => _scanTier2(serverName),\n computeTrustScore: _computeTrustScore,\n addToStore: _addToStore,\n confirm,\n promptEnvVars: promptEnvVarsDefault,\n output: stdoutOutput,\n setSecrets: _setSecrets,\n now: () => Date.now(),\n readUnguardedConsent,\n recordUnguardedConsent: async (names) => {\n const previous = await readUnguardedConsent();\n await writeUnguardedConsent(mergeUnguarded(previous, names));\n },\n };\n\n try {\n await handleInstall(name, installOptions, installDeps);\n } catch (err) {\n if (installOptions.json !== true) {\n console.error(chalk.red((err as Error).message));\n }\n process.exit(1);\n }\n });\n}\n"],"mappings":"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","names":["getAdapter","getConfigPath","scanTier1","checkScannerAvailable","scanTier2","computeTrustScore","confirm"]}
#!/usr/bin/env node
import {
getStorePath,
readJson,
withStoreLock,
writeJson
} from "./chunk-3X76P3FG.js";
// src/store/keychain.ts
import { createHash, randomBytes, webcrypto } from "crypto";
import os from "os";
// src/store/os-keychain.ts
import { spawn } from "child_process";
import { readFile, writeFile } from "fs/promises";
import path from "path";
var SERVICE = "mcpm";
var ACCOUNT = "secret-store-master-key";
var DPAPI_BLOB_FILE = "master-key.dpapi";
var EXEC_TIMEOUT_MS = 5e3;
function run(command, args, opts = {}) {
return new Promise((resolve) => {
const child = spawn(command, args, {
timeout: EXEC_TIMEOUT_MS,
env: opts.env ?? process.env,
stdio: ["pipe", "pipe", "pipe"]
});
let stdout = "";
let stderr = "";
let settled = false;
const settle = (r) => {
if (settled) return;
settled = true;
resolve(r);
};
child.stdout.on("data", (d) => stdout += d.toString());
child.stderr.on("data", (d) => stderr += d.toString());
child.on("error", () => settle({ code: null, stdout, stderr }));
child.on("close", (code) => settle({ code, stdout, stderr }));
if (opts.input !== void 0) {
child.stdin.on("error", () => {
});
child.stdin.end(opts.input);
} else {
child.stdin.end();
}
});
}
function isSupportedPlatform() {
if (process.env.MCPM_DISABLE_OS_KEYCHAIN === "1") return false;
return process.platform === "darwin" || process.platform === "linux" || process.platform === "win32";
}
async function darwinGet() {
const r = await run("security", [
"find-generic-password",
"-a",
ACCOUNT,
"-s",
SERVICE,
"-w"
]);
if (r.code !== 0) return null;
return decodeKey(r.stdout.trim());
}
async function darwinStore(keyB64) {
const r = await run("security", [
"add-generic-password",
"-a",
ACCOUNT,
"-s",
SERVICE,
"-U",
"-w",
keyB64
]);
return r.code === 0;
}
async function linuxGet() {
const r = await run("secret-tool", ["lookup", "service", SERVICE, "account", ACCOUNT]);
if (r.code !== 0) return null;
const value = r.stdout.trim();
if (value.length === 0) return null;
return decodeKey(value);
}
async function linuxStore(keyB64) {
const r = await run(
"secret-tool",
["store", "--label=mcpm secret store master key", "service", SERVICE, "account", ACCOUNT],
{ input: keyB64 }
);
return r.code === 0;
}
var PS_PROTECT = "$ErrorActionPreference='Stop';Add-Type -AssemblyName System.Security;$b=[Convert]::FromBase64String($env:MCPM_KEY_B64);$p=[Security.Cryptography.ProtectedData]::Protect($b,$null,'CurrentUser');[Convert]::ToBase64String($p)";
var PS_UNPROTECT = "$ErrorActionPreference='Stop';Add-Type -AssemblyName System.Security;$b=[Convert]::FromBase64String($env:MCPM_BLOB_B64);$p=[Security.Cryptography.ProtectedData]::Unprotect($b,$null,'CurrentUser');[Convert]::ToBase64String($p)";
async function blobPath() {
return path.join(await getStorePath(), DPAPI_BLOB_FILE);
}
async function windowsGet() {
let blob;
try {
blob = (await readFile(await blobPath(), "utf8")).trim();
} catch {
return null;
}
if (blob.length === 0) return null;
const r = await run("powershell", ["-NoProfile", "-NonInteractive", "-Command", PS_UNPROTECT], {
env: { ...process.env, MCPM_BLOB_B64: blob }
});
if (r.code !== 0) return null;
return decodeKey(r.stdout.trim());
}
async function windowsStore(keyB64) {
const r = await run("powershell", ["-NoProfile", "-NonInteractive", "-Command", PS_PROTECT], {
env: { ...process.env, MCPM_KEY_B64: keyB64 }
});
if (r.code !== 0 || r.stdout.trim().length === 0) return false;
try {
await writeFile(await blobPath(), r.stdout.trim(), { mode: 384 });
return true;
} catch {
return false;
}
}
function decodeKey(b64) {
try {
const buf = Buffer.from(b64, "base64");
return buf.length === 32 ? buf : null;
} catch {
return null;
}
}
async function getStoredKey() {
if (!isSupportedPlatform()) return null;
switch (process.platform) {
case "darwin":
return darwinGet();
case "linux":
return linuxGet();
case "win32":
return windowsGet();
default:
return null;
}
}
async function storeKey(key) {
if (!isSupportedPlatform()) return false;
if (key.length !== 32) return false;
const keyB64 = key.toString("base64");
switch (process.platform) {
case "darwin":
return darwinStore(keyB64);
case "linux":
return linuxStore(keyB64);
case "win32":
return windowsStore(keyB64);
default:
return false;
}
}
// src/store/keychain.ts
var STORE_FILE = "secrets.enc.json";
var PLACEHOLDER_PREFIX = "mcpm:keychain:";
var PBKDF2_ITERATIONS = 6e5;
var SCHEME_KEYCHAIN = "k1";
var HKDF_INFO = new TextEncoder().encode("mcpm-secret-store-v1");
var MACHINE_PASSPHRASE = new TextEncoder().encode(
`mcpm:${os.hostname()}:${os.userInfo().username}`
);
var SAFE_ID_RE = /^[a-zA-Z0-9._-]{1,256}$/;
function assertSafeId(value, label) {
if (!SAFE_ID_RE.test(value)) {
throw new Error(`Invalid ${label}: "${value}" \u2014 must match [a-zA-Z0-9._-], max 256 chars`);
}
}
function validatedStoreKey(server, key) {
assertSafeId(server, "server");
assertSafeId(key, "key");
return `${server}/${key}`;
}
var _masterKey;
async function readMasterKey() {
if (_masterKey) return _masterKey.value;
const value = await getStoredKey();
_masterKey = { value };
return value;
}
async function getOrCreateMasterKey() {
const existing = await readMasterKey();
if (existing) return existing;
if (!isSupportedPlatform()) return null;
return withStoreLock(async () => {
const raced = await getStoredKey();
if (raced) {
_masterKey = { value: raced };
return raced;
}
const key = randomBytes(32);
const value = await storeKey(key) ? key : null;
_masterKey = { value };
return value;
});
}
var _hkdfMaterial;
function hkdfMaterial(masterKey) {
if (!_hkdfMaterial || !_hkdfMaterial.key.equals(masterKey)) {
_hkdfMaterial = {
key: masterKey,
material: webcrypto.subtle.importKey("raw", masterKey, "HKDF", false, ["deriveKey"])
};
}
return _hkdfMaterial.material;
}
async function deriveKeychainKey(masterKey, salt) {
const material = await hkdfMaterial(masterKey);
return webcrypto.subtle.deriveKey(
{ name: "HKDF", salt, info: HKDF_INFO, hash: "SHA-256" },
material,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
}
var _keyMaterialPromise = null;
function getMachineKeyMaterial() {
if (!_keyMaterialPromise) {
_keyMaterialPromise = webcrypto.subtle.importKey(
"raw",
MACHINE_PASSPHRASE,
{ name: "PBKDF2" },
false,
["deriveKey"]
);
}
return _keyMaterialPromise;
}
async function deriveMachineKey(salt) {
const keyMaterial = await getMachineKeyMaterial();
return webcrypto.subtle.deriveKey(
{ name: "PBKDF2", salt, iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
}
var hex = (buf) => Buffer.from(buf instanceof Uint8Array ? buf : new Uint8Array(buf)).toString("hex");
async function encrypt(plaintext) {
const salt = randomBytes(16);
const iv = randomBytes(12);
const masterKey = await getOrCreateMasterKey();
const key = masterKey ? await deriveKeychainKey(masterKey, salt) : await deriveMachineKey(salt);
const cipherBuf = await webcrypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
new TextEncoder().encode(plaintext)
);
const body = [hex(salt), hex(iv), hex(cipherBuf)].join(":");
return masterKey ? `${SCHEME_KEYCHAIN}:${body}` : body;
}
async function decryptWith(key, ivHex, cipherHex) {
const plainBuf = await webcrypto.subtle.decrypt(
{ name: "AES-GCM", iv: Buffer.from(ivHex, "hex") },
key,
Buffer.from(cipherHex, "hex")
);
return new TextDecoder().decode(plainBuf);
}
async function decrypt(stored) {
const parts = stored.split(":");
if (parts.length === 4 && parts[0] === SCHEME_KEYCHAIN) {
const [, saltHex, ivHex, cipherHex] = parts;
const masterKey = await readMasterKey();
if (!masterKey) {
throw new Error(
"Cannot decrypt: this secret is protected by the OS keychain master key, which is unavailable on this machine/account (or the keychain entry was removed)."
);
}
const key = await deriveKeychainKey(masterKey, Buffer.from(saltHex, "hex"));
return decryptWith(key, ivHex, cipherHex);
}
if (parts.length === 3) {
const [saltHex, ivHex, cipherHex] = parts;
const key = await deriveMachineKey(Buffer.from(saltHex, "hex"));
return decryptWith(key, ivHex, cipherHex);
}
throw new Error("Invalid ciphertext format");
}
async function readStore() {
return await readJson(STORE_FILE) ?? {};
}
async function decryptFromSnapshot(store, sk) {
const stored = store[sk];
return stored ? decrypt(stored) : null;
}
async function setSecret(server, key, value) {
const sk = validatedStoreKey(server, key);
const encrypted = await encrypt(value);
await withStoreLock(async () => {
const store = await readStore();
await writeJson(STORE_FILE, { ...store, [sk]: encrypted });
});
}
async function setSecrets(server, values) {
const encrypted = {};
for (const [key, value] of Object.entries(values)) {
encrypted[validatedStoreKey(server, key)] = await encrypt(value);
}
await withStoreLock(async () => {
const store = await readStore();
await writeJson(STORE_FILE, { ...store, ...encrypted });
});
}
async function getSecret(server, key) {
const sk = validatedStoreKey(server, key);
return decryptFromSnapshot(await readStore(), sk);
}
async function deleteSecret(server, key) {
const sk = validatedStoreKey(server, key);
let removed = false;
await withStoreLock(async () => {
const store = await readStore();
if (!(sk in store)) return;
const { [sk]: _removed, ...rest } = store;
await writeJson(STORE_FILE, rest);
removed = true;
});
return removed;
}
function toPlaceholder(server, key) {
return `${PLACEHOLDER_PREFIX}${server}/${key}`;
}
function parsePlaceholder(value) {
if (!value.startsWith(PLACEHOLDER_PREFIX)) return null;
const rest = value.slice(PLACEHOLDER_PREFIX.length);
const slashIdx = rest.indexOf("/");
if (slashIdx === -1) return null;
return { server: rest.slice(0, slashIdx), key: rest.slice(slashIdx + 1) };
}
async function resolveEnvPlaceholders(env) {
const passthrough = {};
const placeholders = [];
for (const [name, value] of Object.entries(env)) {
if (value === void 0) continue;
const placeholder = parsePlaceholder(value);
if (placeholder === null) {
passthrough[name] = value;
continue;
}
placeholders.push({ name, ...placeholder });
}
if (placeholders.length === 0) return passthrough;
return withStoreLock(async () => {
const store = await readStore();
const resolved = { ...passthrough };
for (const { name, server, key } of placeholders) {
const sk = validatedStoreKey(server, key);
const secret = await decryptFromSnapshot(store, sk);
if (secret === null) {
throw new Error(
`Secret "${server}/${key}" not found. Run \`mcpm secrets set ${server} ${key}\` to store it.`
);
}
resolved[name] = secret;
}
return resolved;
});
}
function deriveKeychainId(name) {
const sanitized = name.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 200);
const hash = createHash("sha256").update(name).digest("hex").slice(0, 12);
return `${sanitized}-${hash}`;
}
async function listAll() {
const store = await readStore();
const grouped = {};
for (const storeKey2 of Object.keys(store)) {
const slashIdx = storeKey2.indexOf("/");
if (slashIdx === -1) continue;
const server = storeKey2.slice(0, slashIdx);
const key = storeKey2.slice(slashIdx + 1);
(grouped[server] ??= []).push(key);
}
return grouped;
}
async function activeSecretBackend() {
return await readMasterKey() !== null ? "os-keychain" : "machine-key";
}
async function migrateToKeychain() {
const masterKey = await getOrCreateMasterKey();
if (!masterKey) return { migrated: 0, failed: 0, total: 0, usingKeychain: false };
return withStoreLock(async () => {
const store = await readStore();
const entries = Object.entries(store);
const next = {};
let migrated = 0;
let failed = 0;
for (const [sk, value] of entries) {
const isLegacy = value.split(":").length === 3;
if (!isLegacy) {
next[sk] = value;
continue;
}
try {
const plain = await decrypt(value);
next[sk] = await encrypt(plain);
migrated++;
} catch {
next[sk] = value;
failed++;
}
}
if (migrated > 0) await writeJson(STORE_FILE, next);
return { migrated, failed, total: entries.length, usingKeychain: true };
});
}
async function applyKeychainSecrets(opts) {
if (opts.mode !== "keychain") {
return { env: opts.resolvedEnv, storedCount: 0 };
}
if (!opts.setSecrets) {
throw new Error("Keychain secret storage is unavailable.");
}
const keychainId = deriveKeychainId(opts.serverName);
const env = {};
const toStore = {};
for (const [key, value] of Object.entries(opts.resolvedEnv)) {
if (opts.isSecret(key)) {
toStore[key] = value;
env[key] = toPlaceholder(keychainId, key);
} else {
env[key] = value;
}
}
const storedCount = Object.keys(toStore).length;
if (storedCount > 0) {
await opts.setSecrets(keychainId, toStore);
}
return { env, storedCount };
}
function placeholderEnvKeys(env) {
if (!env) return [];
return Object.entries(env).filter(([, v]) => typeof v === "string" && parsePlaceholder(v) !== null).map(([k]) => k);
}
export {
isSupportedPlatform,
setSecret,
setSecrets,
getSecret,
deleteSecret,
toPlaceholder,
parsePlaceholder,
resolveEnvPlaceholders,
deriveKeychainId,
listAll,
activeSecretBackend,
migrateToKeychain,
applyKeychainSecrets,
placeholderEnvKeys
};
//# sourceMappingURL=chunk-NPJ3SGGS.js.map
{"version":3,"sources":["../src/store/keychain.ts","../src/store/os-keychain.ts"],"sourcesContent":["/**\n * Encrypted-at-rest secret storage using Node's built-in crypto.subtle.\n * No native dependencies (contrast: keytar requires node-gyp).\n *\n * Two encryption schemes (security #15):\n * - keychain (\"k1:\" tag): AES-GCM key derived via HKDF from a random 32-byte\n * master key held in the OS credential store (store/os-keychain.ts). The\n * master key never touches disk, so a copied secrets.enc.json cannot be\n * decrypted on another machine/account — real exfiltration resistance.\n * - machine (legacy, untagged): AES-GCM key derived via PBKDF2 from\n * hostname+username. This is NOT a secret (it is recoverable by anyone who\n * copies the store file), so it guards only against casual local inspection.\n * Used as a fallback where no OS keychain is available (headless/CI) and to\n * decrypt entries written before the keychain upgrade.\n *\n * New secrets use the keychain scheme whenever an OS keychain is available;\n * `migrateToKeychain()` upgrades pre-existing machine-scheme entries.\n *\n * Storage format: ~/.mcpm/secrets.enc.json\n * { \"server/KEY\": \"k1:<salt_hex>:<iv_hex>:<ct_hex>\" } // keychain scheme\n * { \"server/KEY\": \"<salt_hex>:<iv_hex>:<ct_hex>\" } // legacy machine scheme\n *\n * Placeholder format used in config files:\n * \"mcpm:keychain:server/KEY\"\n */\n\nimport { createHash, randomBytes, webcrypto } from \"node:crypto\";\nimport os from \"node:os\";\nimport { readJson, writeJson } from \"./index.js\";\nimport { withStoreLock } from \"./atomic.js\";\nimport { getStoredKey, isSupportedPlatform, storeKey } from \"./os-keychain.js\";\n\nconst STORE_FILE = \"secrets.enc.json\";\nconst PLACEHOLDER_PREFIX = \"mcpm:keychain:\";\nconst PBKDF2_ITERATIONS = 600_000;\n\n// Scheme tag prefixing keychain-scheme entries: \"k1:<salt>:<iv>:<ct>\". Legacy\n// machine-scheme entries are unprefixed (\"<salt>:<iv>:<ct>\"), so decrypt() routes\n// on the part count — keeping old entries readable after the upgrade (issue #15).\nconst SCHEME_KEYCHAIN = \"k1\";\nconst HKDF_INFO = new TextEncoder().encode(\"mcpm-secret-store-v1\");\n\n// Legacy/fallback machine passphrase. This is NOT a secret: hostname + username\n// are recoverable by anyone who copies the store file, so the machine scheme\n// guards only against casual local inspection — never file exfiltration. The\n// keychain scheme (OS-held master key, below) supersedes it whenever an OS\n// credential store is available; this remains for environments without one\n// (headless/CI) and to decrypt pre-existing entries (issue #15).\nconst MACHINE_PASSPHRASE = new TextEncoder().encode(\n `mcpm:${os.hostname()}:${os.userInfo().username}`\n);\n\n// ---------------------------------------------------------------------------\n// Input validation\n// ---------------------------------------------------------------------------\n\nconst SAFE_ID_RE = /^[a-zA-Z0-9._-]{1,256}$/;\n\nfunction assertSafeId(value: string, label: string): void {\n if (!SAFE_ID_RE.test(value)) {\n throw new Error(`Invalid ${label}: \"${value}\" — must match [a-zA-Z0-9._-], max 256 chars`);\n }\n}\n\nfunction validatedStoreKey(server: string, key: string): string {\n assertSafeId(server, \"server\");\n assertSafeId(key, \"key\");\n return `${server}/${key}`;\n}\n\n// ---------------------------------------------------------------------------\n// Master key (OS credential store) — security #15\n// ---------------------------------------------------------------------------\n//\n// A single random 32-byte key lives in the OS keychain (store/os-keychain.ts);\n// per-value AES keys are derived from it via HKDF. Because the master key is\n// never written to ~/.mcpm, a copied secrets.enc.json cannot be decrypted on\n// another machine/account. When no OS keychain is available, encrypt() falls\n// back to the machine scheme below.\n//\n// Byte annotations below spell out `<ArrayBuffer>` on purpose. Bare `Buffer` /\n// `Uint8Array` default to `ArrayBufferLike` (which includes SharedArrayBuffer),\n// and @types/node 26 narrowed WebCrypto's `BufferSource` to reject shared views,\n// so a bare annotation no longer assigns to importKey/deriveKey. The values were\n// always correct — `randomBytes()` and `Buffer.from(hex)` are both ArrayBuffer-\n// backed — only the annotations were wider. Do not widen them back: under the\n// pinned @types/node 22 that compiles clean, and CI's per-leg typecheck against\n// @types/node@<matrix node> is the only thing that catches it.\n\nlet _masterKey: { value: Buffer<ArrayBuffer> | null } | undefined;\n\n/** Read the stored master key (never creates one); memoized per process. */\nasync function readMasterKey(): Promise<Buffer<ArrayBuffer> | null> {\n if (_masterKey) return _masterKey.value;\n const value = await getStoredKey();\n _masterKey = { value };\n return value;\n}\n\n/**\n * Return the master key, creating and persisting a fresh random one if none\n * exists yet and the platform has a usable credential store. Creation runs\n * under the store lock so two concurrent first-writes cannot generate two keys\n * and leave one writer's secret undecryptable.\n */\nasync function getOrCreateMasterKey(): Promise<Buffer<ArrayBuffer> | null> {\n const existing = await readMasterKey();\n if (existing) return existing;\n if (!isSupportedPlatform()) return null;\n return withStoreLock(async () => {\n const raced = await getStoredKey(); // another process may have just created it\n if (raced) {\n _masterKey = { value: raced };\n return raced;\n }\n const key = randomBytes(32);\n // If the keychain write fails, memoize null: this session falls back to the\n // machine scheme for the rest of its life (no retry). That is the safe,\n // honest outcome — activeSecretBackend() will report \"machine-key\" and\n // `secrets set` warns the user — rather than half-using an unpersisted key.\n const value = (await storeKey(key)) ? key : null;\n _masterKey = { value };\n return value;\n });\n}\n\n// ---------------------------------------------------------------------------\n// Key derivation — keychain scheme (HKDF) and machine scheme (PBKDF2)\n// ---------------------------------------------------------------------------\n\n// HKDF importKey is cheap but stable per master key; cache it per-process.\nlet _hkdfMaterial: { key: Buffer<ArrayBuffer>; material: Promise<webcrypto.CryptoKey> } | undefined;\n\nfunction hkdfMaterial(masterKey: Buffer<ArrayBuffer>): Promise<webcrypto.CryptoKey> {\n if (!_hkdfMaterial || !_hkdfMaterial.key.equals(masterKey)) {\n _hkdfMaterial = {\n key: masterKey,\n material: webcrypto.subtle.importKey(\"raw\", masterKey, \"HKDF\", false, [\"deriveKey\"]),\n };\n }\n return _hkdfMaterial.material;\n}\n\nasync function deriveKeychainKey(\n masterKey: Buffer<ArrayBuffer>,\n salt: Uint8Array<ArrayBuffer>\n): Promise<webcrypto.CryptoKey> {\n const material = await hkdfMaterial(masterKey);\n return webcrypto.subtle.deriveKey(\n { name: \"HKDF\", salt, info: HKDF_INFO, hash: \"SHA-256\" },\n material,\n { name: \"AES-GCM\", length: 256 },\n false,\n [\"encrypt\", \"decrypt\"]\n );\n}\n\n// Cache the PBKDF2 importKey step — it is cheap but never varies within a\n// process. The expensive derivation still runs per value (random per-value salt).\nlet _keyMaterialPromise: Promise<webcrypto.CryptoKey> | null = null;\n\nfunction getMachineKeyMaterial(): Promise<webcrypto.CryptoKey> {\n if (!_keyMaterialPromise) {\n _keyMaterialPromise = webcrypto.subtle.importKey(\n \"raw\",\n MACHINE_PASSPHRASE,\n { name: \"PBKDF2\" },\n false,\n [\"deriveKey\"]\n );\n }\n return _keyMaterialPromise;\n}\n\nasync function deriveMachineKey(salt: Uint8Array<ArrayBuffer>): Promise<webcrypto.CryptoKey> {\n const keyMaterial = await getMachineKeyMaterial();\n return webcrypto.subtle.deriveKey(\n { name: \"PBKDF2\", salt, iterations: PBKDF2_ITERATIONS, hash: \"SHA-256\" },\n keyMaterial,\n { name: \"AES-GCM\", length: 256 },\n false,\n [\"encrypt\", \"decrypt\"]\n );\n}\n\n// ---------------------------------------------------------------------------\n// Encryption helpers\n// ---------------------------------------------------------------------------\n\nconst hex = (buf: ArrayBuffer | Uint8Array): string =>\n Buffer.from(buf instanceof Uint8Array ? buf : new Uint8Array(buf)).toString(\"hex\");\n\nasync function encrypt(plaintext: string): Promise<string> {\n const salt = randomBytes(16);\n const iv = randomBytes(12);\n const masterKey = await getOrCreateMasterKey();\n const key = masterKey\n ? await deriveKeychainKey(masterKey, salt)\n : await deriveMachineKey(salt);\n const cipherBuf = await webcrypto.subtle.encrypt(\n { name: \"AES-GCM\", iv },\n key,\n new TextEncoder().encode(plaintext)\n );\n const body = [hex(salt), hex(iv), hex(cipherBuf)].join(\":\");\n // Keychain-scheme entries are tagged so decrypt() can route; machine-scheme\n // entries stay in the legacy unprefixed format for backward compatibility.\n return masterKey ? `${SCHEME_KEYCHAIN}:${body}` : body;\n}\n\nasync function decryptWith(\n key: webcrypto.CryptoKey,\n ivHex: string,\n cipherHex: string\n): Promise<string> {\n const plainBuf = await webcrypto.subtle.decrypt(\n { name: \"AES-GCM\", iv: Buffer.from(ivHex, \"hex\") },\n key,\n Buffer.from(cipherHex, \"hex\")\n );\n return new TextDecoder().decode(plainBuf);\n}\n\nasync function decrypt(stored: string): Promise<string> {\n const parts = stored.split(\":\");\n // Keychain scheme: \"k1:<salt>:<iv>:<ct>\"\n if (parts.length === 4 && parts[0] === SCHEME_KEYCHAIN) {\n const [, saltHex, ivHex, cipherHex] = parts;\n const masterKey = await readMasterKey();\n if (!masterKey) {\n throw new Error(\n \"Cannot decrypt: this secret is protected by the OS keychain master key, \" +\n \"which is unavailable on this machine/account (or the keychain entry was removed).\"\n );\n }\n const key = await deriveKeychainKey(masterKey, Buffer.from(saltHex, \"hex\"));\n return decryptWith(key, ivHex, cipherHex);\n }\n // Legacy machine scheme: \"<salt>:<iv>:<ct>\"\n if (parts.length === 3) {\n const [saltHex, ivHex, cipherHex] = parts;\n const key = await deriveMachineKey(Buffer.from(saltHex, \"hex\"));\n return decryptWith(key, ivHex, cipherHex);\n }\n throw new Error(\"Invalid ciphertext format\");\n}\n\n// ---------------------------------------------------------------------------\n// Store helpers\n// ---------------------------------------------------------------------------\n\nasync function readStore(): Promise<Record<string, string>> {\n return (await readJson<Record<string, string>>(STORE_FILE)) ?? {};\n}\n\n// Decrypt a single stored value identified by its store key against an\n// already-loaded store snapshot. Shared by the unlocked per-key getSecret and\n// the locked snapshot resolver so both decode entries identically.\nasync function decryptFromSnapshot(\n store: Record<string, string>,\n sk: string\n): Promise<string | null> {\n const stored = store[sk];\n return stored ? decrypt(stored) : null;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\nexport async function setSecret(server: string, key: string, value: string): Promise<void> {\n const sk = validatedStoreKey(server, key);\n // Encrypt outside the lock (it does not depend on stored state) to keep the\n // critical section short, then read-merge-write atomically under the lock.\n const encrypted = await encrypt(value);\n await withStoreLock(async () => {\n const store = await readStore();\n await writeJson(STORE_FILE, { ...store, [sk]: encrypted });\n });\n}\n\n/**\n * Store multiple secrets for one server in a single read-modify-write, so the\n * batch is all-or-nothing: either every value is persisted or none is (no\n * orphaned half-written secrets if one encrypt fails — security review MED-1).\n */\nexport async function setSecrets(\n server: string,\n values: Record<string, string>\n): Promise<void> {\n // Encrypt every value first (no dependency on stored state), then read-merge-\n // write atomically under the lock so a concurrent writer cannot lost-update\n // this batch.\n const encrypted: Record<string, string> = {};\n for (const [key, value] of Object.entries(values)) {\n encrypted[validatedStoreKey(server, key)] = await encrypt(value);\n }\n await withStoreLock(async () => {\n const store = await readStore();\n await writeJson(STORE_FILE, { ...store, ...encrypted });\n });\n}\n\n// Intentionally UNLOCKED: a read-only single-key lookup. It accepts eventual\n// consistency (it may observe a concurrent writer's snapshot before or after a\n// mutation, never a torn one — writeJson swaps the file atomically via rename).\n// The consistency-sensitive path is resolveEnvPlaceholders, which takes the\n// lock and reads one snapshot for all keys.\nexport async function getSecret(server: string, key: string): Promise<string | null> {\n const sk = validatedStoreKey(server, key);\n return decryptFromSnapshot(await readStore(), sk);\n}\n\n/** Returns true if a secret was removed, false if no such secret existed. */\nexport async function deleteSecret(server: string, key: string): Promise<boolean> {\n const sk = validatedStoreKey(server, key);\n let removed = false;\n await withStoreLock(async () => {\n const store = await readStore();\n if (!(sk in store)) return;\n const { [sk]: _removed, ...rest } = store;\n await writeJson(STORE_FILE, rest);\n removed = true;\n });\n return removed;\n}\n\n/** Produces the placeholder string stored in config files. */\nexport function toPlaceholder(server: string, key: string): string {\n return `${PLACEHOLDER_PREFIX}${server}/${key}`;\n}\n\n/** Parses a placeholder string. Returns null if not a placeholder. */\nexport function parsePlaceholder(value: string): { server: string; key: string } | null {\n if (!value.startsWith(PLACEHOLDER_PREFIX)) return null;\n const rest = value.slice(PLACEHOLDER_PREFIX.length);\n const slashIdx = rest.indexOf(\"/\");\n if (slashIdx === -1) return null;\n return { server: rest.slice(0, slashIdx), key: rest.slice(slashIdx + 1) };\n}\n\n/**\n * Resolve any `mcpm:keychain:server/KEY` placeholder values in an env map to\n * their decrypted secrets. Non-placeholder values pass through unchanged;\n * `undefined` values are dropped. Throws if a placeholder references a secret\n * that is not stored.\n *\n * The decrypted values exist only in the returned in-memory object — they are\n * never written to disk. `mcpm guard run --inner` calls this to inject secrets\n * into a wrapped server's child process without storing plaintext in client\n * config files.\n *\n * Reads the store as a single CONSISTENT SNAPSHOT under `withStoreLock`: all\n * placeholders are resolved against one read taken while holding the same lock\n * the write paths (setSecret/setSecrets/deleteSecret) hold. This closes the\n * read-after-delete race where a concurrent `secrets delete` during guard\n * startup made a per-key unlocked lookup observe a torn state and throw \"Secret\n * not found\" — the exact race the lock was added to prevent. This is the only\n * caller (guard/run-inner.ts), invoked at the top level and NOT from inside an\n * already-held store lock, so acquiring the lock here cannot self-deadlock.\n */\nexport async function resolveEnvPlaceholders(\n env: NodeJS.ProcessEnv\n): Promise<Record<string, string>> {\n // Parse placeholders up front so the locked critical section is just one read\n // plus decryption — no validation or iteration over non-placeholder values.\n const passthrough: Record<string, string> = {};\n const placeholders: Array<{ name: string; server: string; key: string }> = [];\n for (const [name, value] of Object.entries(env)) {\n if (value === undefined) continue;\n const placeholder = parsePlaceholder(value);\n if (placeholder === null) {\n passthrough[name] = value;\n continue;\n }\n placeholders.push({ name, ...placeholder });\n }\n\n // No secrets to resolve — skip the lock entirely.\n if (placeholders.length === 0) return passthrough;\n\n return withStoreLock(async () => {\n const store = await readStore();\n const resolved: Record<string, string> = { ...passthrough };\n for (const { name, server, key } of placeholders) {\n const sk = validatedStoreKey(server, key);\n const secret = await decryptFromSnapshot(store, sk);\n if (secret === null) {\n throw new Error(\n `Secret \"${server}/${key}\" not found. ` +\n `Run \\`mcpm secrets set ${server} ${key}\\` to store it.`\n );\n }\n resolved[name] = secret;\n }\n return resolved;\n });\n}\n\n/**\n * Derive a keychain-safe server id from a (possibly slash-containing) server\n * name. Registry ids like \"io.github.owner/repo\" contain `/`, which is invalid\n * for a keychain id (assertSafeId) and would break placeholder parsing (which\n * splits on the first `/`).\n *\n * The sanitised prefix keeps the id human-recognisable; a sha256 suffix makes\n * the mapping INJECTIVE, so two names that differ only in unsafe characters\n * (e.g. \"owner/repo\" vs \"owner_repo\") can never collide into one secret\n * namespace (security review CRIT-1). Deterministic: the same name always maps\n * to the same id, so a placeholder written at install resolves at launch.\n */\nexport function deriveKeychainId(name: string): string {\n const sanitized = name.replace(/[^a-zA-Z0-9._-]/g, \"_\").slice(0, 200);\n const hash = createHash(\"sha256\").update(name).digest(\"hex\").slice(0, 12);\n return `${sanitized}-${hash}`;\n}\n\n/**\n * List all stored secrets grouped by server name. Returns only key names —\n * decrypted values are never read or returned.\n *\n * Intentionally UNLOCKED: read-only enumeration, eventual consistency.\n */\nexport async function listAll(): Promise<Record<string, string[]>> {\n const store = await readStore();\n const grouped: Record<string, string[]> = {};\n for (const storeKey of Object.keys(store)) {\n const slashIdx = storeKey.indexOf(\"/\");\n if (slashIdx === -1) continue;\n const server = storeKey.slice(0, slashIdx);\n const key = storeKey.slice(slashIdx + 1);\n (grouped[server] ??= []).push(key);\n }\n return grouped;\n}\n\n// ---------------------------------------------------------------------------\n// Backend status + migration (security #15)\n// ---------------------------------------------------------------------------\n\n/**\n * Which backend protected the most recent write — a backward-looking reflection,\n * not a forward-looking prediction. Returns \"os-keychain\" only when a master key\n * is actually present. Intended to be called AFTER a `setSecret`/`migrate` (as\n * `mcpm secrets set` does), where it accurately reports the scheme just used —\n * including reporting \"machine-key\" when a keychain write silently failed and\n * fell back. Called standalone before any write on a keychain-capable machine\n * with no key yet, it returns \"machine-key\" (no key created yet); the next write\n * would create one and use \"os-keychain\".\n */\nexport async function activeSecretBackend(): Promise<\"os-keychain\" | \"machine-key\"> {\n return (await readMasterKey()) !== null ? \"os-keychain\" : \"machine-key\";\n}\n\n/**\n * Re-encrypt every legacy machine-scheme entry under the OS keychain master\n * key, so existing secrets gain the same exfiltration resistance as new ones.\n *\n * No-op (`usingKeychain: false`) when no OS keychain is available. Per-entry\n * failures are isolated: a legacy entry this machine key can no longer decrypt\n * (e.g. written on a different machine) is counted in `failed` and left\n * untouched rather than aborting the whole migration.\n */\nexport async function migrateToKeychain(): Promise<{\n migrated: number;\n failed: number;\n total: number;\n usingKeychain: boolean;\n}> {\n const masterKey = await getOrCreateMasterKey();\n if (!masterKey) return { migrated: 0, failed: 0, total: 0, usingKeychain: false };\n return withStoreLock(async () => {\n const store = await readStore();\n const entries = Object.entries(store);\n const next: Record<string, string> = {};\n let migrated = 0;\n let failed = 0;\n for (const [sk, value] of entries) {\n const isLegacy = value.split(\":\").length === 3;\n if (!isLegacy) {\n next[sk] = value; // already keychain-scheme (or unknown) — leave as-is\n continue;\n }\n try {\n const plain = await decrypt(value); // legacy machine scheme\n next[sk] = await encrypt(plain); // re-encrypt under the keychain master key\n migrated++;\n } catch {\n next[sk] = value; // undecryptable legacy entry — leave untouched\n failed++;\n }\n }\n if (migrated > 0) await writeJson(STORE_FILE, next);\n return { migrated, failed, total: entries.length, usingKeychain: true };\n });\n}\n\n/**\n * How secret-flagged env vars are persisted.\n * - \"plaintext\": written directly into the client config (legacy default).\n * - \"keychain\": stored AES-GCM-encrypted; config gets a `mcpm:keychain:…`\n * placeholder that mcpm guard resolves at launch.\n */\nexport type SecretsMode = \"plaintext\" | \"keychain\";\n\n/**\n * Resolve a server's env map for writing to a client config under the given\n * secrets mode. In \"keychain\" mode, every key for which `isSecret(key)` is true\n * is stored encrypted via `setSecret` and replaced with a `mcpm:keychain:…`\n * placeholder; all other values pass through. In \"plaintext\" mode the input is\n * returned unchanged. This is the single place the \"no plaintext secret in\n * config\" invariant is enforced — install and up both go through it.\n *\n * Throws if keychain mode is requested without a `setSecret` implementation.\n */\nexport async function applyKeychainSecrets(opts: {\n serverName: string;\n resolvedEnv: Record<string, string>;\n isSecret: (key: string) => boolean;\n mode: SecretsMode;\n setSecrets?: (server: string, values: Record<string, string>) => Promise<void>;\n}): Promise<{ env: Record<string, string>; storedCount: number }> {\n if (opts.mode !== \"keychain\") {\n return { env: opts.resolvedEnv, storedCount: 0 };\n }\n if (!opts.setSecrets) {\n throw new Error(\"Keychain secret storage is unavailable.\");\n }\n const keychainId = deriveKeychainId(opts.serverName);\n const env: Record<string, string> = {};\n const toStore: Record<string, string> = {};\n for (const [key, value] of Object.entries(opts.resolvedEnv)) {\n if (opts.isSecret(key)) {\n toStore[key] = value;\n env[key] = toPlaceholder(keychainId, key);\n } else {\n env[key] = value;\n }\n }\n const storedCount = Object.keys(toStore).length;\n // Persist all secrets in one atomic batch BEFORE returning the env that the\n // caller writes to config — so we never write a placeholder for a secret that\n // failed to store (all-or-nothing; security review MED-1).\n if (storedCount > 0) {\n await opts.setSecrets(keychainId, toStore);\n }\n return { env, storedCount };\n}\n\n/**\n * Return the keys of `env` whose value is a `mcpm:keychain:…` placeholder.\n * Used by `mcpm guard disable` to warn about secrets that will no longer\n * resolve once guard stops wrapping the server.\n */\nexport function placeholderEnvKeys(env: Record<string, string> | undefined): string[] {\n if (!env) return [];\n return Object.entries(env)\n .filter(([, v]) => typeof v === \"string\" && parsePlaceholder(v) !== null)\n .map(([k]) => k);\n}\n","/**\n * Zero-native-dependency access to the operating system's credential store.\n *\n * The secret store (store/keychain.ts) holds a single random 32-byte *master\n * key* here; every stored secret is AES-GCM-encrypted with a subkey derived\n * from it. Keeping the master key in the OS credential store — not on disk —\n * is what makes a copied `~/.mcpm/secrets.enc.json` undecryptable off-machine\n * (security issue #15).\n *\n * No native modules (no `keytar`/node-gyp). We shell out to the platform's\n * built-in tooling:\n * - macOS: `security` (login Keychain) — generic password item\n * - Linux: `secret-tool` (libsecret/Secret Service) — schema attributes\n * - Windows: DPAPI via PowerShell `ProtectedData` — blob in ~/.mcpm\n *\n * Every operation is best-effort: if the platform tool is missing, the Secret\n * Service is unavailable (headless Linux, CI), or any call fails, the function\n * resolves to \"unavailable\"/null and the caller falls back to the legacy\n * machine-derived key (casual-inspection only — see store/keychain.ts).\n *\n * Set `MCPM_DISABLE_OS_KEYCHAIN=1` to force the fallback (used by the test\n * suite so it never touches the developer's real Keychain, and available to\n * users who prefer not to use the OS store).\n */\n\nimport { spawn } from \"node:child_process\";\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { getStorePath } from \"./index.js\";\n\nconst SERVICE = \"mcpm\";\nconst ACCOUNT = \"secret-store-master-key\";\nconst DPAPI_BLOB_FILE = \"master-key.dpapi\";\nconst EXEC_TIMEOUT_MS = 5_000;\n\n/** Result of a child-process run. `code === null` means spawn failed (ENOENT). */\ninterface RunResult {\n code: number | null;\n stdout: string;\n stderr: string;\n}\n\n/**\n * Run a command to completion, optionally feeding `input` to stdin. Never\n * rejects: a missing binary (ENOENT) or any spawn error resolves to\n * `{ code: null }` so callers treat it uniformly as \"unavailable\".\n */\nfunction run(\n command: string,\n args: string[],\n opts: { input?: string; env?: NodeJS.ProcessEnv } = {}\n): Promise<RunResult> {\n return new Promise((resolve) => {\n const child = spawn(command, args, {\n timeout: EXEC_TIMEOUT_MS,\n env: opts.env ?? process.env,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n let stdout = \"\";\n let stderr = \"\";\n // `error` (ENOENT/EACCES) and `close` can both fire for one spawn; settle\n // exactly once. `close` (not `exit`) is used so stdout is fully drained\n // before we resolve.\n let settled = false;\n const settle = (r: RunResult): void => {\n if (settled) return;\n settled = true;\n resolve(r);\n };\n child.stdout.on(\"data\", (d) => (stdout += d.toString()));\n child.stderr.on(\"data\", (d) => (stderr += d.toString()));\n child.on(\"error\", () => settle({ code: null, stdout, stderr }));\n child.on(\"close\", (code) => settle({ code, stdout, stderr }));\n if (opts.input !== undefined) {\n child.stdin.on(\"error\", () => {\n /* child may have exited before stdin flush; swallow EPIPE */\n });\n child.stdin.end(opts.input);\n } else {\n child.stdin.end();\n }\n });\n}\n\n/** True on a platform we know how to drive (and not explicitly disabled). */\nexport function isSupportedPlatform(): boolean {\n if (process.env.MCPM_DISABLE_OS_KEYCHAIN === \"1\") return false;\n return (\n process.platform === \"darwin\" ||\n process.platform === \"linux\" ||\n process.platform === \"win32\"\n );\n}\n\n// ---------------------------------------------------------------------------\n// macOS — security(1) generic-password items in the login Keychain\n// ---------------------------------------------------------------------------\n\nasync function darwinGet(): Promise<Buffer<ArrayBuffer> | null> {\n // -w prints only the password; exit 44 when the item does not exist.\n const r = await run(\"security\", [\n \"find-generic-password\",\n \"-a\",\n ACCOUNT,\n \"-s\",\n SERVICE,\n \"-w\",\n ]);\n if (r.code !== 0) return null;\n return decodeKey(r.stdout.trim());\n}\n\nasync function darwinStore(keyB64: string): Promise<boolean> {\n // -U updates the item if it already exists instead of erroring.\n //\n // Tradeoff: `security` has no reliable non-interactive stdin path for the\n // password, so the (base64) master key is passed in argv and is briefly\n // visible to a *same-user* `ps` during the child's lifetime. This is a narrow,\n // write-only window: the read path (darwinGet) returns the key on stdout to\n // this parent only, and cross-user argv is not readable. A same-user attacker\n // who can `ps` can already read this process's memory, so this does not widen\n // the trust boundary the keychain establishes (off-machine file exfiltration).\n const r = await run(\"security\", [\n \"add-generic-password\",\n \"-a\",\n ACCOUNT,\n \"-s\",\n SERVICE,\n \"-U\",\n \"-w\",\n keyB64,\n ]);\n return r.code === 0;\n}\n\n// ---------------------------------------------------------------------------\n// Linux — secret-tool (libsecret). Value is read from / written to stdin.\n// ---------------------------------------------------------------------------\n\nasync function linuxGet(): Promise<Buffer<ArrayBuffer> | null> {\n const r = await run(\"secret-tool\", [\"lookup\", \"service\", SERVICE, \"account\", ACCOUNT]);\n if (r.code !== 0) return null;\n const value = r.stdout.trim(); // consistent with darwin/windows; tolerate \\r\\n / spaces\n if (value.length === 0) return null;\n return decodeKey(value);\n}\n\nasync function linuxStore(keyB64: string): Promise<boolean> {\n const r = await run(\n \"secret-tool\",\n [\"store\", \"--label=mcpm secret store master key\", \"service\", SERVICE, \"account\", ACCOUNT],\n { input: keyB64 }\n );\n return r.code === 0;\n}\n\n// ---------------------------------------------------------------------------\n// Windows — DPAPI (CurrentUser scope) via PowerShell. The protected blob is\n// stored in ~/.mcpm; DPAPI ties decryption to the Windows user account, so a\n// copied blob cannot be unprotected by another account or on another machine.\n// The plaintext key is passed through an env var, never argv (process list).\n// ---------------------------------------------------------------------------\n\nconst PS_PROTECT =\n \"$ErrorActionPreference='Stop';\" +\n \"Add-Type -AssemblyName System.Security;\" +\n \"$b=[Convert]::FromBase64String($env:MCPM_KEY_B64);\" +\n \"$p=[Security.Cryptography.ProtectedData]::Protect($b,$null,'CurrentUser');\" +\n \"[Convert]::ToBase64String($p)\";\n\nconst PS_UNPROTECT =\n \"$ErrorActionPreference='Stop';\" +\n \"Add-Type -AssemblyName System.Security;\" +\n \"$b=[Convert]::FromBase64String($env:MCPM_BLOB_B64);\" +\n \"$p=[Security.Cryptography.ProtectedData]::Unprotect($b,$null,'CurrentUser');\" +\n \"[Convert]::ToBase64String($p)\";\n\nasync function blobPath(): Promise<string> {\n return path.join(await getStorePath(), DPAPI_BLOB_FILE);\n}\n\nasync function windowsGet(): Promise<Buffer<ArrayBuffer> | null> {\n let blob: string;\n try {\n blob = (await readFile(await blobPath(), \"utf8\")).trim();\n } catch {\n return null; // no blob stored yet\n }\n if (blob.length === 0) return null;\n const r = await run(\"powershell\", [\"-NoProfile\", \"-NonInteractive\", \"-Command\", PS_UNPROTECT], {\n env: { ...process.env, MCPM_BLOB_B64: blob },\n });\n if (r.code !== 0) return null;\n return decodeKey(r.stdout.trim());\n}\n\nasync function windowsStore(keyB64: string): Promise<boolean> {\n const r = await run(\"powershell\", [\"-NoProfile\", \"-NonInteractive\", \"-Command\", PS_PROTECT], {\n env: { ...process.env, MCPM_KEY_B64: keyB64 },\n });\n if (r.code !== 0 || r.stdout.trim().length === 0) return false;\n try {\n await writeFile(await blobPath(), r.stdout.trim(), { mode: 0o600 });\n return true;\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Shared helpers + public dispatch\n// ---------------------------------------------------------------------------\n\n/**\n * Decode a base64 master key, rejecting anything that is not exactly 32 bytes.\n * Returns `Buffer<ArrayBuffer>` rather than bare `Buffer` so the key still\n * assigns to WebCrypto's `BufferSource` in keychain.ts — see the note there.\n */\nfunction decodeKey(b64: string): Buffer<ArrayBuffer> | null {\n try {\n const buf = Buffer.from(b64, \"base64\");\n return buf.length === 32 ? buf : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Read the stored master key, or null if none is stored / the store is\n * unavailable. Never creates a key.\n */\nexport async function getStoredKey(): Promise<Buffer<ArrayBuffer> | null> {\n if (!isSupportedPlatform()) return null;\n switch (process.platform) {\n case \"darwin\":\n return darwinGet();\n case \"linux\":\n return linuxGet();\n case \"win32\":\n return windowsGet();\n default:\n return null;\n }\n}\n\n/**\n * Persist `key` (exactly 32 bytes) in the OS credential store. Returns true on\n * success, false if the store is unavailable or the write failed.\n */\nexport async function storeKey(key: Buffer): Promise<boolean> {\n if (!isSupportedPlatform()) return false;\n if (key.length !== 32) return false;\n const keyB64 = key.toString(\"base64\");\n switch (process.platform) {\n case \"darwin\":\n return darwinStore(keyB64);\n case \"linux\":\n return linuxStore(keyB64);\n case \"win32\":\n return windowsStore(keyB64);\n default:\n return false;\n }\n}\n"],"mappings":"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","names":["storeKey"]}
#!/usr/bin/env node
import {
confineSandboxRoot,
hashConfineProfile,
readConfineStore,
withProfile,
writeConfineStore
} from "./chunk-544DEV2D.js";
import {
SANDBOX_EXEC_PATH,
defaultWrapContext,
isConfineBackendAvailable,
isWrapped,
unwrapEntry,
wrapEntry
} from "./chunk-WYSMWP2R.js";
import "./chunk-OIFKZA4V.js";
import {
sanitizeForTerminal
} from "./chunk-FEXJHHDM.js";
import {
getAdapter
} from "./chunk-W4IAFBUN.js";
import {
isNewUnguarded,
mergeUnguarded,
readUnguardedConsent,
writeUnguardedConsent
} from "./chunk-MLVDFLDQ.js";
import {
placeholderEnvKeys
} from "./chunk-NPJ3SGGS.js";
import {
detectInstalledClients
} from "./chunk-6R7TL5O2.js";
import {
getConfigPath
} from "./chunk-R4R2VPDA.js";
import "./chunk-3X76P3FG.js";
// src/guard/cli.ts
import chalk from "chalk";
// src/guard/orchestrator.ts
import { copyFile } from "fs/promises";
function enableGuardAcrossClients(deps, filter) {
return runAcrossClients(deps, "enable", filter);
}
function disableGuardAcrossClients(deps, filter) {
return runAcrossClients(deps, "disable", filter);
}
async function runAcrossClients(deps, action, filter) {
const targetClients = await selectTargetClients(deps, filter?.client);
const consentedUnguarded = action === "enable" && deps.readUnguardedConsent ? await deps.readUnguardedConsent() : [];
const plans = await Promise.all(
targetClients.map(
(clientId) => planForClient(clientId, deps, action, filter?.server, consentedUnguarded)
)
);
for (const plan of plans) {
if (plan.transforms.length === 0) continue;
const configPath = deps.getConfigPath(plan.clientId);
await copyFile(configPath, `${configPath}.guard-${action}.bak`).catch(() => void 0);
}
const reports = [];
for (const plan of plans) {
reports.push(await applyPlan(plan, deps));
}
if (filter?.server !== void 0) {
const matched = reports.some((r) => r.servers.some((s) => s.name === filter.server));
if (!matched) {
throw new Error(
`--server "${filter.server}" not found in any detected client config. Run \`mcpm guard status\` to see available servers.`
);
}
}
if (deps.recordUnguardedConsent) {
const nowUnguarded = [
...new Set(
reports.flatMap((r) => r.servers.filter((s) => s.status === "unguarded").map((s) => s.name))
)
];
const previous = new Set(consentedUnguarded);
const newlyConsented = nowUnguarded.filter((n) => !previous.has(n));
if (newlyConsented.length > 0) {
await deps.recordUnguardedConsent(newlyConsented).catch(() => void 0);
}
}
return summarize(action, reports);
}
async function statusAcrossClients(deps) {
const targetClients = await deps.detectClients();
const clients = await Promise.all(
targetClients.map(async (clientId) => {
try {
const adapter = deps.getAdapter(clientId);
const entries = await adapter.read(deps.getConfigPath(clientId));
const servers = Object.entries(entries).map(([name, entry]) => ({
name,
wrapped: isWrapped(entry),
// A non-stdio (url-transport) entry has no command — it cannot be
// wrapped, so even when "not wrapped" it is specifically UNGUARDED.
unguarded: !isWrapped(entry) && !entry.command
}));
return {
clientId,
wrapped: servers.filter((s) => s.wrapped).length,
unwrapped: servers.filter((s) => !s.wrapped).length,
servers
};
} catch (err) {
return {
clientId,
wrapped: 0,
unwrapped: 0,
servers: [],
error: err instanceof Error ? err.message : String(err)
};
}
})
);
return {
clients,
totalWrapped: clients.reduce((sum, c) => sum + c.wrapped, 0),
totalUnwrapped: clients.reduce((sum, c) => sum + c.unwrapped, 0)
};
}
async function selectTargetClients(deps, clientFilter) {
const detected = await deps.detectClients();
if (clientFilter === void 0) return detected;
if (!detected.includes(clientFilter)) {
throw new Error(
`Client "${clientFilter}" not detected. Available: ${detected.join(", ") || "(none)"}`
);
}
return [clientFilter];
}
async function planForClient(clientId, deps, action, serverFilter, consentedUnguarded) {
const adapter = deps.getAdapter(clientId);
let entries;
try {
entries = await adapter.read(deps.getConfigPath(clientId));
} catch (err) {
return {
clientId,
action,
transforms: [],
skipped: [],
unguarded: [],
readError: err instanceof Error ? err.message : String(err)
};
}
const transforms = [];
const skipped = [];
const unguarded = [];
const consentedSet = new Set(consentedUnguarded);
for (const [name, entry] of Object.entries(entries)) {
if (serverFilter !== void 0 && name !== serverFilter) continue;
if (action === "enable") {
if (isWrapped(entry)) {
skipped.push({ name, reason: "already wrapped" });
continue;
}
if (!entry.command) {
if (deps.allowUnguarded === true || consentedSet.has(name)) {
unguarded.push({
name,
reason: "unguarded (consented) \u2014 runs WITHOUT runtime inspection; this grants consent, it does not add protection. The only true fix is a streamable-HTTP relay (not yet implemented)."
});
} else {
skipped.push({
name,
reason: "DENIED: URL/HTTP-transport server runs UNGUARDED \u2014 no runtime inspection is possible (the guard relay only wraps stdio servers). Re-run `mcpm guard enable --allow-unguarded` to permit it without protection."
});
}
continue;
}
transforms.push({
name,
nextEntry: wrapEntry(name, entry, deps.wrapContext, deps.confineMarkers?.get(name))
});
} else {
if (!isWrapped(entry)) {
skipped.push({ name, reason: "not wrapped" });
continue;
}
const unwrapped = unwrapEntry(entry);
if (unwrapped === null) {
skipped.push({ name, reason: "wrap marker malformed; .bak restore may be required" });
continue;
}
transforms.push({ name, nextEntry: unwrapped });
}
}
return { clientId, action, transforms, skipped, unguarded };
}
async function applyPlan(plan, deps) {
if (plan.readError) {
return {
clientId: plan.clientId,
servers: [],
error: plan.readError
};
}
const adapter = deps.getAdapter(plan.clientId);
const configPath = deps.getConfigPath(plan.clientId);
const servers = [];
for (const { name, nextEntry } of plan.transforms) {
try {
await adapter.replaceServer(configPath, name, nextEntry);
servers.push({ name, status: plan.action === "enable" ? "wrapped" : "unwrapped" });
} catch (err) {
servers.push({
name,
status: "skipped",
reason: err instanceof Error ? err.message : String(err)
});
}
}
for (const skip of plan.skipped) {
servers.push({ name: skip.name, status: "skipped", reason: skip.reason });
}
for (const u of plan.unguarded) {
servers.push({ name: u.name, status: "unguarded", reason: u.reason });
}
return { clientId: plan.clientId, servers };
}
function summarize(action, reports) {
let totalChanged = 0;
let totalSkipped = 0;
let totalUnguarded = 0;
let errors = 0;
for (const report of reports) {
if (report.error) errors++;
for (const server of report.servers) {
if (server.status === "wrapped" || server.status === "unwrapped") totalChanged++;
else if (server.status === "unguarded") totalUnguarded++;
else totalSkipped++;
}
}
return { action, clients: reports, totalChanged, totalSkipped, totalUnguarded, errors };
}
// src/guard/cli.ts
import os from "os";
// src/guard/confine/derive.ts
import { createHash } from "crypto";
import path from "path";
var SECRET_DIR_SEGMENTS = [
// SSH / cloud / package-registry / signing credentials.
".ssh",
".aws",
".gnupg",
".config/gh",
".config/gcloud",
".npmrc",
".docker",
".kube",
".netrc",
".git-credentials",
".cargo/credentials",
".cargo/credentials.toml",
".pypirc",
// OS keychains + browser cookie stores (highest-value credential theft).
"Library/Keychains",
"Library/Application Support/Google/Chrome",
"Library/Application Support/Firefox",
"Library/Cookies",
// mcpm's own store (secrets.enc.json, pins, policy, the confine store itself).
".mcpm",
// Sibling MCP client configs (each can hold another server's plaintext secrets).
"Library/Application Support/Claude",
"Library/Application Support/Cursor",
"Library/Application Support/Code/User",
"Library/Application Support/Windsurf",
".cursor",
".vscode",
".codeium",
".claude.json",
// Claude Code user-global config (see src/config/paths.ts getConfigPath)
".claude",
// Claude Code also keeps state under ~/.claude/
".gemini"
// Gemini CLI user-global config (~/.gemini/settings.json)
];
var WRITE_ALLOW_HOME_SEGMENTS = [".npm", ".cache", "Library/Caches"];
var WRITE_ALLOW_STATIC = [
"/tmp",
"/private/tmp",
"/var/tmp",
"/var/folders",
"/private/var/folders",
"/dev"
];
var LAUNCHER_COMMANDS = /* @__PURE__ */ new Set([
"npx",
"npm",
"pnpm",
"yarn",
"bun",
"bunx",
"uv",
"uvx",
"pip",
"pip3",
"pipx",
"pipenv",
"poetry",
"docker"
]);
function commandBasename(command) {
const base = path.basename(command).toLowerCase();
const dot = base.indexOf(".");
return dot === -1 ? base : base.slice(0, dot);
}
function classifyNet(command) {
return LAUNCHER_COMMANDS.has(commandBasename(command)) ? "all" : "none";
}
function safeServerSegment(serverName) {
if (serverName.length === 0) throw new Error("confine: empty server name");
const cleaned = serverName.replace(/[^A-Za-z0-9._@-]/g, "_").replace(/\.{2,}/g, "_").replace(/^\.+/, "_");
const suffix = createHash("sha256").update(serverName).digest("hex").slice(0, 8);
return `${cleaned}-${suffix}`;
}
function deriveDefaultProfile(input) {
if (input.command.length === 0) throw new Error("confine: empty command");
const scratchDir = path.join(input.sandboxRoot, safeServerSegment(input.serverName));
const readDeny = SECRET_DIR_SEGMENTS.map((seg) => path.join(input.home, seg));
const writeAllow = [
scratchDir,
...WRITE_ALLOW_STATIC,
input.tmpDir,
...WRITE_ALLOW_HOME_SEGMENTS.map((seg) => path.join(input.home, seg))
];
return {
tier: "standard",
require_confine: input.requireConfine === true,
read_deny: dedupeSorted(readDeny),
write_allow: dedupeSorted(writeAllow),
net: input.net ?? classifyNet(input.command),
scratch_dir: scratchDir,
captured_at: input.capturedAt
};
}
function dedupeSorted(paths) {
return [...new Set(paths)].sort();
}
// src/guard/cli.ts
var CLIENT_LABELS = {
"claude-desktop": "Claude Desktop",
"claude-code": "Claude Code",
cursor: "Cursor",
vscode: "VS Code",
windsurf: "Windsurf",
"gemini-cli": "Gemini CLI"
};
function buildDeps(extra = {}) {
return {
detectClients: detectInstalledClients,
getAdapter,
getConfigPath,
wrapContext: defaultWrapContext(),
readUnguardedConsent,
recordUnguardedConsent: async (names) => {
const previous = await readUnguardedConsent();
await writeUnguardedConsent(mergeUnguarded(previous, names));
},
...extra
};
}
async function runEnableCommand(opts) {
const previousConsented = await readUnguardedConsent();
if (opts.dryRun === true) {
await printEnableDryRun(opts);
return;
}
if (opts.confine === "off") {
opts.write("OS confinement: skipped (--confine off).\n");
}
let confineMarkers;
if (opts.confine === "standard") {
try {
confineMarkers = await computeConfineMarkers({
client: opts.client,
server: opts.server,
write: opts.write
});
} catch (err) {
opts.write(
chalk.yellow(`
\u26A0 OS confinement aborted: ${sanitizeForTerminal(err.message)}`) + "\n"
);
return;
}
}
const deps = buildDeps({ allowUnguarded: opts.allowUnguarded, confineMarkers });
const summary = await enableGuardAcrossClients(deps, opts);
printEnableDisable(summary, opts);
printUnguardedWarning(summary, previousConsented, opts);
if (confineMarkers !== void 0) printConfineNotice(confineMarkers, opts);
printRestartReminder(opts);
}
async function printEnableDryRun(opts) {
const deps = buildDeps({ allowUnguarded: opts.allowUnguarded });
const status = await statusAcrossClients(deps);
const confineNote = opts.confine === "standard" ? " (with OS confinement)" : "";
opts.write(`Dry-run: planned wraps${confineNote}
`);
for (const c of status.clients) {
if (opts.client !== void 0 && c.clientId !== opts.client) continue;
const candidates = c.servers.filter((s) => {
if (opts.server !== void 0 && s.name !== opts.server) return false;
return !s.wrapped;
});
opts.write(` ${CLIENT_LABELS[c.clientId]}: would wrap ${candidates.length} server(s)
`);
for (const s of candidates) opts.write(` + ${s.name}
`);
}
}
async function collectConfineTargets(opts) {
const targets = /* @__PURE__ */ new Map();
let clients;
try {
clients = await detectInstalledClients();
} catch {
return targets;
}
if (opts.client !== void 0) clients = clients.filter((c) => c === opts.client);
for (const clientId of clients) {
let entries;
try {
entries = await getAdapter(clientId).read(getConfigPath(clientId));
} catch {
continue;
}
for (const [name, entry] of Object.entries(entries)) {
if (opts.server !== void 0 && name !== opts.server) continue;
if (!entry.command || isWrapped(entry)) continue;
if (!targets.has(name)) targets.set(name, { command: entry.command, args: entry.args });
}
}
return targets;
}
async function computeConfineMarkers(opts) {
const targets = await collectConfineTargets(opts);
if (targets.size === 0) return /* @__PURE__ */ new Map();
let store;
try {
store = await readConfineStore();
} catch (err) {
throw new Error(
`cannot read the confine store (~/.mcpm/guard-confine.yaml): ${err.message} Review it for unauthorized changes; if you edited it intentionally, restore or remove it. Refusing to enroll \u2014 the store was left untouched.`
);
}
const { markers, storeToWrite } = await resolveConfineMarkers(targets, store, opts);
if (storeToWrite !== null) await writeConfineStore(storeToWrite);
return markers;
}
async function resolveConfineMarkers(targets, store, opts) {
const markers = /* @__PURE__ */ new Map();
const home = os.homedir();
const sandboxRoot = await confineSandboxRoot();
const tmpDir = os.tmpdir();
const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
let next = store;
let added = 0;
for (const [name, target] of targets) {
const existing = Object.hasOwn(store.servers, name) ? store.servers[name] : void 0;
if (existing !== void 0) {
markers.set(name, {
profileHash: hashConfineProfile(existing),
required: existing.require_confine
});
continue;
}
const profile = deriveDefaultProfile({
serverName: name,
command: target.command,
args: target.args,
home,
sandboxRoot,
tmpDir,
capturedAt
});
next = withProfile(next, name, profile);
added += 1;
markers.set(name, {
profileHash: hashConfineProfile(profile),
required: profile.require_confine
});
}
return { markers, storeToWrite: added > 0 ? next : null };
}
function printConfineNotice(confineMarkers, opts) {
if (confineMarkers.size === 0) {
opts.write("\nOS confinement: no unwrapped stdio servers to enroll.\n");
return;
}
opts.write(
`
\u{1F512} ${confineMarkers.size} server(s) enrolled in OS confinement (standard tier). Run \`mcpm guard doctor-confine\` for status.
`
);
if (!isConfineBackendAvailable()) {
opts.write(
chalk.yellow(
"\u26A0 No OS sandbox backend on this platform \u2014 enrolled servers run UNCONFINED until a backend is present (they are NOT blocked; a require_confine server would fail closed)."
) + "\n"
);
}
}
async function runDoctorConfineCommand(opts) {
const backendAvailable = isConfineBackendAvailable();
let servers = [];
let storeError;
try {
const store = await readConfineStore();
servers = Object.entries(store.servers).map(([name, p]) => ({
name,
tier: p.tier,
net: p.net,
requireConfine: p.require_confine
}));
} catch (err) {
storeError = err.message;
}
opts.write(
opts.json === true ? renderDoctorConfineJson(backendAvailable, servers, storeError) : renderDoctorConfineText(backendAvailable, servers, storeError)
);
}
function renderDoctorConfineJson(backendAvailable, servers, storeError) {
return JSON.stringify(
{
platform: process.platform,
backendAvailable,
sandboxExecPath: process.platform === "darwin" ? SANDBOX_EXEC_PATH : null,
// sanitize: an OS error message can carry control chars / ANSI (parity
// with the text branch, which already sanitizes).
storeError: storeError !== void 0 ? sanitizeForTerminal(storeError) : null,
servers
},
null,
2
) + "\n";
}
function renderDoctorConfineText(backendAvailable, servers, storeError) {
const out = ["mcpm guard doctor-confine", ""];
out.push(` platform : ${process.platform}`);
let backendLine = ` sandbox backend : ${backendAvailable ? "available" : "UNAVAILABLE"}`;
if (process.platform === "darwin") backendLine += ` (${SANDBOX_EXEC_PATH})`;
out.push(backendLine);
if (!backendAvailable) {
out.push(
" \u2192 enrolled servers run UNCONFINED here (hybrid posture); a require_confine server fails closed."
);
}
out.push("");
if (storeError !== void 0) {
out.push(` \u26A0 could not read the confine store: ${sanitizeForTerminal(storeError)}`, "");
return out.join("\n");
}
if (servers.length === 0) {
out.push(" No servers enrolled in confinement. Enroll with `mcpm guard enable --confine`.", "");
return out.join("\n");
}
out.push(` Enrolled servers (${servers.length}):`);
for (const s of servers) {
out.push(` ${sanitizeForTerminal(s.name)} \u2014 tier=${s.tier} net=${s.net} require_confine=${s.requireConfine}`);
}
out.push("", " Run `mcpm guard status` for per-client wrap state.", "");
return out.join("\n");
}
function printUnguardedWarning(summary, previousConsented, opts) {
const current = [
...new Set(
summary.clients.flatMap(
(c) => c.servers.filter((s) => s.status === "unguarded").map((s) => s.name)
)
)
].sort();
if (current.length === 0) return;
if (isNewUnguarded(current, previousConsented)) {
const prev = new Set(previousConsented);
const newlyConsented = current.filter((n) => !prev.has(n));
const alreadyCount = current.length - newlyConsented.length;
opts.write(
chalk.yellow(
"\n\u26A0 UNGUARDED: the following server(s) run WITHOUT runtime inspection \u2014 the guard relay cannot wrap a non-stdio (URL/HTTP) transport:"
) + "\n"
);
for (const name of newlyConsented) opts.write(` \u26A0 ${sanitizeForTerminal(name)}
`);
if (alreadyCount > 0) {
opts.write(` (+${alreadyCount} previously consented)
`);
}
opts.write(
"This grants consent to run them UNGUARDED \u2014 it does NOT add protection. The only true fix is a streamable-HTTP relay (not yet implemented). Future `enable` runs stay quiet unless a NEW unguarded server appears.\n"
);
} else {
opts.write(
`
${current.length} server(s) running unguarded (previously consented): ${current.map((n) => sanitizeForTerminal(n)).join(", ")}
`
);
}
}
async function runDisableCommand(opts) {
const deps = buildDeps();
const summary = await disableGuardAcrossClients(deps, opts);
printEnableDisable(summary, opts);
printRestartReminder(opts);
await warnUnresolvablePlaceholders(opts);
}
async function warnUnresolvablePlaceholders(opts) {
let clients;
try {
clients = await detectInstalledClients();
} catch {
return;
}
const affected = [];
for (const clientId of clients) {
if (opts.client !== void 0 && clientId !== opts.client) continue;
let entries;
try {
entries = await getAdapter(clientId).read(getConfigPath(clientId));
} catch (err) {
if (err.code !== "ENOENT") {
opts.write(
` (could not read ${CLIENT_LABELS[clientId]} config: ${sanitizeForTerminal(err.message)})
`
);
}
continue;
}
for (const [name, entry] of Object.entries(entries)) {
if (opts.server !== void 0 && name !== opts.server) continue;
const keys = placeholderEnvKeys(entry.env);
if (keys.length > 0) affected.push({ client: clientId, server: name, keys });
}
}
if (affected.length === 0) return;
opts.write(
"\n\x1B[33mwarning: these servers reference encrypted secrets (mcpm:keychain:\u2026) that only resolve while mcpm guard is enabled. Without guard they receive the literal placeholder and will fail to start:\x1B[0m\n"
);
for (const a of affected) {
opts.write(
` - ${sanitizeForTerminal(a.server)} (${CLIENT_LABELS[a.client]}): ${formatAffectedKeys(a.keys)}
`
);
}
opts.write(
"Re-enable with `mcpm guard enable`, or replace those env values with plaintext.\n"
);
}
function formatAffectedKeys(keys) {
return keys.map((k) => sanitizeForTerminal(k)).join(", ");
}
async function runStatusCommand(opts) {
const deps = buildDeps();
const status = await statusAcrossClients(deps);
if (status.clients.length === 0) {
if (opts.helpFallback) {
opts.helpFallback();
return;
}
opts.write("No MCP clients detected.\n");
return;
}
if (status.totalWrapped === 0 && opts.helpFallback) {
opts.helpFallback();
return;
}
printStatus(status, opts);
}
function printEnableDisable(summary, opts) {
const verb = summary.action === "enable" ? "wrapped" : "unwrapped";
opts.write(`mcpm guard ${summary.action}: ${summary.totalChanged} ${verb}, `);
opts.write(`${summary.totalSkipped} skipped`);
if (summary.totalUnguarded > 0) {
opts.write(`, ${summary.totalUnguarded} unguarded`);
}
opts.write(`, ${summary.errors} error(s)
`);
for (const client of summary.clients) {
printClientReport(client, opts);
}
}
function printClientReport(report, opts) {
opts.write(` ${CLIENT_LABELS[report.clientId]}:
`);
if (report.error !== void 0) {
opts.write(` error: ${sanitizeForTerminal(report.error)}
`);
return;
}
if (report.servers.length === 0) {
opts.write(` (no servers)
`);
return;
}
for (const s of report.servers) {
const symbol = s.status === "wrapped" ? "+" : s.status === "unwrapped" ? "-" : s.status === "unguarded" ? "\u26A0" : "\xB7";
const reason = s.reason !== void 0 ? ` (${sanitizeForTerminal(s.reason)})` : "";
opts.write(` ${symbol} ${sanitizeForTerminal(s.name)}${reason}
`);
}
}
function printStatus(status, opts) {
opts.write(`mcpm guard status: ${status.totalWrapped} wrapped, ${status.totalUnwrapped} unwrapped
`);
for (const c of status.clients) {
opts.write(` ${CLIENT_LABELS[c.clientId]}: ${c.wrapped} wrapped / ${c.unwrapped} unwrapped
`);
if (c.error !== void 0) {
opts.write(` error: ${sanitizeForTerminal(c.error)}
`);
continue;
}
for (const s of c.servers) {
const marker = s.wrapped ? "+" : s.unguarded ? "\u26A0 UNGUARDED" : "\xB7";
opts.write(` ${marker} ${sanitizeForTerminal(s.name)}
`);
}
}
}
function printRestartReminder(opts) {
opts.write(
"\n\u2192 Restart your IDE (Claude Desktop / Cursor / VS Code / Windsurf) for changes to take effect.\n"
);
}
async function runCleanupCommand(opts) {
const deps = buildDeps();
const status = await statusAcrossClients(deps);
const installedServerNames = /* @__PURE__ */ new Set();
for (const c of status.clients) {
for (const s of c.servers) installedServerNames.add(s.name);
}
const { readPins, writePins, clearServerPins, PinsIntegrityError } = await import("./pins-ETT4XWEP.js");
let pins;
try {
pins = await readPins();
} catch (err) {
if (err instanceof PinsIntegrityError) {
opts.write(
`mcpm guard cleanup: cannot read ~/.mcpm/pins.json \u2014 integrity check failed.
${err.message}
Refusing to prune until this is resolved.
`
);
} else {
opts.write(
`mcpm guard cleanup: cannot read ~/.mcpm/pins.json \u2014 ${err.message}
Refusing to prune until this is resolved.
`
);
}
return;
}
const orphanPinned = [];
for (const serverName of Object.keys(pins.servers)) {
if (!installedServerNames.has(serverName)) orphanPinned.push(serverName);
}
if (orphanPinned.length === 0) {
opts.write("mcpm guard cleanup: nothing to prune (0 orphan pins, 0 orphan wraps).\n");
return;
}
opts.write(`mcpm guard cleanup: ${orphanPinned.length} orphan pin entr${orphanPinned.length === 1 ? "y" : "ies"} found:
`);
for (const s of orphanPinned) opts.write(` - ${sanitizeForTerminal(s)}
`);
if (!opts.apply) {
opts.write("\nDry run. Re-run with --yes to prune.\n");
return;
}
let next = pins;
for (const serverName of orphanPinned) next = clearServerPins(next, serverName);
await writePins(next);
opts.write(`
Pruned ${orphanPinned.length} orphan pin entr${orphanPinned.length === 1 ? "y" : "ies"} from ~/.mcpm/pins.json.
`);
}
export {
computeConfineMarkers,
formatAffectedKeys,
printUnguardedWarning,
runCleanupCommand,
runDisableCommand,
runDoctorConfineCommand,
runEnableCommand,
runStatusCommand
};
//# sourceMappingURL=cli-UNXIMIVR.js.map

Sorry, the diff of this file is too big to display

#!/usr/bin/env node
import {
activeSecretBackend,
applyKeychainSecrets,
deleteSecret,
deriveKeychainId,
getSecret,
listAll,
migrateToKeychain,
parsePlaceholder,
placeholderEnvKeys,
resolveEnvPlaceholders,
setSecret,
setSecrets,
toPlaceholder
} from "./chunk-NPJ3SGGS.js";
import "./chunk-3X76P3FG.js";
export {
activeSecretBackend,
applyKeychainSecrets,
deleteSecret,
deriveKeychainId,
getSecret,
listAll,
migrateToKeychain,
parsePlaceholder,
placeholderEnvKeys,
resolveEnvPlaceholders,
setSecret,
setSecrets,
toPlaceholder
};
//# sourceMappingURL=keychain-KDOUTVCT.js.map
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
#!/usr/bin/env node
import {
buildDriftFinding,
buildHandshakeDriftFinding,
classifyDrift,
classifyHandshakeDrift,
inspectForDrift,
inspectHandshakeForDrift
} from "./chunk-5W5Z3VZG.js";
import {
PolicyIntegrityError,
expireStale,
readPolicy
} from "./chunk-CYYYMOUS.js";
import {
hasToolsList,
inspectFrame,
mergeInspect,
withReplyToOrigin
} from "./chunk-XLPT6EJQ.js";
import {
hashConfineProfile,
loadProfile
} from "./chunk-544DEV2D.js";
import {
OWASP_MCP_TOP_10
} from "./chunk-4ANBMGU5.js";
import {
fieldHashesOf,
handshakeCapabilityKeys,
handshakeFieldHashesOf,
hashHandshake,
hashToolDefinition,
lookupHandshake,
readPins,
writePins
} from "./chunk-DDCTUMSZ.js";
import {
hashOriginalEntry,
isConfineBackendAvailable,
wrapForConfinement
} from "./chunk-WYSMWP2R.js";
import "./chunk-OIFKZA4V.js";
import {
sanitizeForTerminal
} from "./chunk-FEXJHHDM.js";
import {
resolveEnvPlaceholders
} from "./chunk-NPJ3SGGS.js";
import {
getStorePath
} from "./chunk-3X76P3FG.js";
import {
ACTION_RANK,
defaultActionForFinding,
inspectMessage
} from "./chunk-WT6V33F2.js";
// src/guard/relay.ts
import { spawn } from "child_process";
import { ReadBuffer, serializeMessage } from "@modelcontextprotocol/sdk/shared/stdio.js";
var GUARD_BLOCK_ERROR_CODE = -32099;
function makeBlockResponse(blocked, result) {
if (!("id" in blocked) || blocked.id === void 0) return null;
const finding = result.findings[0];
return {
jsonrpc: "2.0",
id: blocked.id,
error: {
code: GUARD_BLOCK_ERROR_CODE,
message: "BLOCKED by mcpm-guard",
data: finding ? {
signature_id: finding.signature_id,
category: finding.category,
severity: finding.severity,
matched_text_excerpt: finding.matched_text_excerpt,
remediation: finding.remediation
} : void 0
}
};
}
var SAFE_ENV_PASSTHROUGH = /* @__PURE__ */ new Set([
"PATH",
"HOME",
"TMPDIR",
"TEMP",
"TMP",
"LANG",
"LC_ALL",
"USER",
"SHELL"
]);
function buildSafeEnv(source = process.env) {
const out = {};
for (const [k, v] of Object.entries(source)) {
if (SAFE_ENV_PASSTHROUGH.has(k) || k.startsWith("LC_")) out[k] = v;
}
return out;
}
var MAX_BUFFER_BYTES = 64 * 1024 * 1024;
function startRelay(opts) {
const env = opts.env ?? buildSafeEnv();
const child = opts.spawnChild ? opts.spawnChild(opts.command, opts.args, env) : spawn(opts.command, [...opts.args], {
env,
stdio: ["pipe", "pipe", "inherit"]
// stderr passthrough — preserves IDE diagnostics
});
const forwardSignal = (sig) => {
if (!child.killed) child.kill(sig);
};
let settled = false;
let resolveExit;
const exit = new Promise((resolve) => {
resolveExit = resolve;
});
child.on("error", (err) => {
if (settled) return;
settled = true;
process.off("SIGTERM", forwardSignal);
process.off("SIGINT", forwardSignal);
const code = err.code ?? "SPAWN-FAILED";
opts.onEvent?.({
ts: (/* @__PURE__ */ new Date()).toISOString(),
direction: "child->parent",
action: "block",
findings: [
{
signature_id: "spawn-failure",
category: "RELAY",
severity: "critical",
target: "tool_response",
matched_text_excerpt: `${code}: ${err.message}`,
remediation: "The wrapped MCP server binary failed to start. Verify the command exists and is executable."
}
]
});
process.stderr.write(`[mcpm-guard] SPAWN-FAILED ${opts.command}: ${code}
`);
child.stdout?.destroy();
child.stdin?.destroy();
resolveExit(1);
});
child.stdin?.on("error", (err) => {
const code = err.code;
if (code !== "EPIPE" && code !== "ERR_STREAM_DESTROYED") {
opts.onEvent?.({
ts: (/* @__PURE__ */ new Date()).toISOString(),
direction: "parent->child",
action: "warn",
findings: []
});
}
});
const writeToChild = (bytes) => {
if (child.stdin && !child.stdin.destroyed) child.stdin.write(bytes);
};
wireDirection({
source: opts.parentIn,
target: writeToChild,
targetEnd: () => child.stdin?.end(),
parentOut: opts.parentOut,
inspect: opts.inspectParentRequest,
direction: "parent->child",
onEvent: opts.onEvent,
// Symmetry only — a parent-INITIATED block replies to the client (parentOut),
// so this is unused for this direction (no replyToOrigin on parent requests).
replyToSource: (bytes) => opts.parentOut.write(bytes)
});
if (child.stdout) {
wireDirection({
source: child.stdout,
target: (bytes) => opts.parentOut.write(bytes),
targetEnd: () => void 0,
// never end parentOut on child exit
parentOut: opts.parentOut,
inspect: opts.inspectChildResponse,
direction: "child->parent",
onEvent: opts.onEvent,
// H7: a blocked server-INITIATED request (sampling/elicitation) errors
// back to the SERVER (child.stdin), not the client.
replyToSource: writeToChild
});
}
process.on("SIGTERM", forwardSignal);
process.on("SIGINT", forwardSignal);
child.on("exit", (code) => {
if (settled) return;
settled = true;
process.off("SIGTERM", forwardSignal);
process.off("SIGINT", forwardSignal);
resolveExit(code ?? 0);
});
return { child, exit };
}
function wireDirection(w) {
const buffer = new ReadBuffer();
let bufferedBytes = 0;
w.source.on("data", (chunk) => {
bufferedBytes += chunk.byteLength;
if (bufferedBytes > MAX_BUFFER_BYTES) {
w.onEvent?.({
ts: (/* @__PURE__ */ new Date()).toISOString(),
direction: w.direction,
action: "block",
findings: []
});
w.source.destroy();
return;
}
buffer.append(chunk);
let msg;
try {
msg = buffer.readMessage();
} catch {
w.onEvent?.(malformedFrameEvent(w.direction));
w.source.destroy();
return;
}
while (msg !== null) {
bufferedBytes = 0;
const decision = w.inspect?.(msg);
if (decision?.action === "block") {
logEvent(decision, w.direction, w.onEvent);
const errResp = makeBlockResponse(msg, decision);
if (errResp !== null) {
if (decision.replyToOrigin === true) w.replyToSource(serializeMessage(errResp));
else w.parentOut.write(serializeMessage(errResp));
}
} else {
logEvent(decision, w.direction, w.onEvent);
w.target(serializeMessage(msg));
}
try {
msg = buffer.readMessage();
} catch {
w.onEvent?.(malformedFrameEvent(w.direction));
w.source.destroy();
return;
}
}
});
w.source.on("end", () => {
w.targetEnd();
});
}
function malformedFrameEvent(direction) {
return {
ts: (/* @__PURE__ */ new Date()).toISOString(),
direction,
action: "block",
findings: [
{
signature_id: "malformed-frame",
category: "RELAY",
severity: "critical",
target: "tool_response",
matched_text_excerpt: "malformed JSON-RPC frame on stdio",
remediation: "The wrapped MCP server emitted a non-JSON-RPC line (e.g. a startup banner). It must write only JSON-RPC frames to stdout."
}
]
};
}
function logEvent(result, direction, onEvent) {
if (!result || result.findings.length === 0) return;
onEvent?.({
ts: (/* @__PURE__ */ new Date()).toISOString(),
direction,
action: result.action,
findings: result.findings
});
}
// src/guard/event-log.ts
import { appendFile, mkdir } from "fs/promises";
import path from "path";
var EVENT_LOG_FILENAME = "guard-events.jsonl";
var _warnedOnFailure = false;
async function eventLogPath() {
return path.join(await getStorePath(), EVENT_LOG_FILENAME);
}
function buildEventLogEntry(event, serverName) {
return {
ts: event.ts,
server_name: sanitizeForTerminal(serverName),
direction: event.direction,
action: event.action,
findings: event.findings.map((f) => ({
signature_id: f.signature_id,
category: f.category,
severity: f.severity,
target: f.target,
matched_text_excerpt: f.matched_text_excerpt
}))
};
}
async function appendEvent(event, serverName) {
try {
const filePath = await eventLogPath();
await mkdir(path.dirname(filePath), { recursive: true, mode: 448 });
const line = `${JSON.stringify(buildEventLogEntry(event, serverName))}
`;
await appendFile(filePath, line, { encoding: "utf-8", mode: 384 });
} catch (err) {
if (!_warnedOnFailure) {
_warnedOnFailure = true;
process.stderr.write(
`[mcpm-guard] event log write failed (logging will continue silently): ${err instanceof Error ? err.message : String(err)}
`
);
}
}
}
// src/guard/confine/decide.ts
function decideConfine(input) {
const { profile, markerHash, markerRequired, backendAvailable } = input;
const mustConfine = markerRequired || profile?.require_confine === true;
if (profile !== null) {
if (markerHash === null) {
return mustConfine ? { action: "fail-closed", reason: "confine marker stripped on a required server", event: "confine-marker-stripped" } : { action: "unconfined", reason: "confine marker stripped", event: "confine-marker-stripped" };
}
if (hashConfineProfile(profile) !== markerHash) {
return { action: "fail-closed", reason: "confine profile hash mismatch (tamper)", event: "confine-hash-mismatch" };
}
if (!backendAvailable) {
return mustConfine ? { action: "fail-closed", reason: "no confine backend on a required server", event: "confine-backend-missing" } : { action: "unconfined", reason: "no confine backend on this platform", event: "confine-backend-missing" };
}
return { action: "confine", reason: "confined", event: "confine-applied" };
}
if (markerRequired) {
return { action: "fail-closed", reason: "confine required but no stored profile (store missing?)", event: "confine-profile-missing" };
}
if (markerHash !== null) {
return { action: "unconfined", reason: "confine marker present but no stored profile", event: "confine-profile-missing" };
}
return { action: "unconfined", reason: "not confined" };
}
// src/guard/run-inner.ts
var SIGNATURE_LIST_VERSION = "owasp-mcp-top-10@v0.5.0";
function applyPolicy(result, policy) {
const overrides = policy.signature_overrides ?? [];
if (overrides.length === 0) return result;
const byId = new Map(overrides.map((o) => [o.id, o]));
let highest = "pass";
const kept = [];
for (const f of result.findings) {
const o = byId.get(f.signature_id);
let perFindingAction;
if (o === void 0) {
perFindingAction = defaultActionForFinding(f);
kept.push(f);
} else if (o.action === "ignore") {
continue;
} else if (o.action === "log_only") {
perFindingAction = "pass";
kept.push(f);
} else {
perFindingAction = o.action;
kept.push(f);
}
if (ACTION_RANK[perFindingAction] > ACTION_RANK[highest]) highest = perFindingAction;
}
return withReplyToOrigin({ action: highest, findings: kept }, result.replyToOrigin === true);
}
function confineGuardEvent(event, reason, action, severity) {
return {
ts: (/* @__PURE__ */ new Date()).toISOString(),
direction: "parent->child",
action,
findings: [
{
signature_id: event,
category: "CONFINE",
severity,
target: "tool_response",
matched_text_excerpt: reason,
remediation: "See docs/GUARD.md \u2014 `mcpm guard confine`."
}
]
};
}
async function runInner(parsed) {
const safeName = sanitizeForTerminal(parsed.serverName);
if (typeof parsed.origHash === "string" && parsed.origHash.length > 0) {
const recomputed = hashOriginalEntry(parsed.command, parsed.args, parsed.declaredEnvKeys);
if (recomputed !== parsed.origHash) {
process.stderr.write(
`[mcpm-guard] ORIG-HASH-MISMATCH ${safeName}: the wrapped command/args/declared-env no longer match the integrity hash embedded at \`mcpm guard enable\` time \u2014 the client config entry may have been edited or tampered with. Starting anyway (advisory); a future mcpm release will refuse to start on mismatch. Review ~/.mcpm/guard-events.jsonl, and if you changed the entry on purpose re-run \`mcpm guard enable\` to re-pin it.
`
);
void appendEvent(
{
ts: (/* @__PURE__ */ new Date()).toISOString(),
direction: "parent->child",
action: "warn",
findings: [
{
signature_id: "orig-hash-mismatch",
category: "RELAY",
severity: "high",
target: "tool_response",
matched_text_excerpt: "wrap-marker integrity: recomputed hash != embedded --orig-hash",
remediation: "Re-run `mcpm guard enable` to re-pin, or restore the original wrapped entry in the client config."
}
]
},
parsed.serverName
);
}
}
const logEvent2 = (event) => {
if (event.action === "block" || event.action === "warn") {
process.stderr.write(
`[mcpm-guard] ${event.action.toUpperCase()} ${safeName} ${event.findings.map((f) => f.signature_id).join(",")}
`
);
void appendEvent(event, parsed.serverName);
}
};
let pinsSnapshot;
try {
pinsSnapshot = await readPins();
} catch (err) {
process.stderr.write(
`[mcpm-guard] PINS-READ-ERROR: ${safeName} could not load ~/.mcpm/pins.json: ${err.message}
Refusing to start the relay \u2014 running with rug-pull (schema-drift) protection silently disabled is more dangerous than not starting. Review ~/.mcpm/guard-events.jsonl for unauthorized activity. If you intentionally changed pins.json, run \`mcpm guard reset-integrity\`.
`
);
process.exit(1);
}
const policy = expireStale(
await readPolicy().catch((err) => {
if (err instanceof PolicyIntegrityError) {
process.stderr.write(
`[mcpm-guard] POLICY-INTEGRITY-ERROR: ${safeName} ${err.message}
Falling back to full enforcement (ignoring guard-policy.yaml) for this session.
`
);
} else {
process.stderr.write(
`[mcpm-guard] POLICY-READ-ERROR: ${err.message}
`
);
}
return {};
})
);
const pausedUntilFuture = policy.paused_until !== void 0 && new Date(policy.paused_until) > /* @__PURE__ */ new Date();
const sessionState = {
firstHashes: /* @__PURE__ */ new Map(),
revalidationArmed: false,
handshakeSeenHash: null
};
const baselineForDrift = pinsSnapshot;
const inspectChild = (msg) => {
if (pausedUntilFuture) return { action: "pass", findings: [] };
if (isToolsListChangedNotification(msg)) {
sessionState.revalidationArmed = true;
return { action: "pass", findings: [] };
}
const statelessResult = inspectFrame(msg);
let driftResult = { action: "pass", findings: [] };
if (hasToolsList(msg)) {
driftResult = inspectForDriftSync(msg, parsed.serverName, baselineForDrift, sessionState);
void (async () => {
await inspectForDrift(msg, parsed.serverName, {
read: () => readPins().catch(() => pinsSnapshot),
write: writePins,
signatureListVersion: SIGNATURE_LIST_VERSION
});
pinsSnapshot = await readPins().catch(() => pinsSnapshot);
})();
} else if (isInitializeResult(msg)) {
driftResult = inspectHandshakeDriftSync(msg, parsed.serverName, baselineForDrift, sessionState);
void (async () => {
await inspectHandshakeForDrift(msg, parsed.serverName, {
read: () => readPins().catch(() => pinsSnapshot),
write: writePins,
signatureListVersion: SIGNATURE_LIST_VERSION
});
pinsSnapshot = await readPins().catch(() => pinsSnapshot);
})();
}
return applyPolicy(mergeInspect(statelessResult, driftResult), policy);
};
const inspectParent = (msg) => {
if (pausedUntilFuture) return { action: "pass", findings: [] };
return applyPolicy(inspectMessage(msg, OWASP_MCP_TOP_10), policy);
};
const baselineEnv = buildSafeEnv(process.env);
const childEnvSource = { ...baselineEnv };
for (const key of parsed.declaredEnvKeys) {
const value = process.env[key];
if (value !== void 0) childEnvSource[key] = value;
}
let childEnv;
try {
childEnv = await resolveEnvPlaceholders(childEnvSource);
} catch (err) {
process.stderr.write(
`[mcpm-guard] SECRET-MISSING ${safeName} ${err.message}
`
);
return 1;
}
if (parsed.confineProfileHash !== void 0 && !/^[0-9a-f]{64}$/.test(parsed.confineProfileHash)) {
process.stderr.write(
`[mcpm-guard] CONFINE-BLOCK ${safeName}: malformed --confine-profile-hash in the wrap marker (the client config entry may be tampered or corrupt). Refusing to start.
`
);
void appendEvent(
confineGuardEvent(
"confine-marker-malformed",
"malformed confine profile hash",
"block",
"critical"
),
parsed.serverName
);
process.exit(1);
}
let spawnCommand = parsed.command;
let spawnArgs = parsed.args;
let confineProfile = null;
try {
confineProfile = await loadProfile(parsed.serverName);
} catch (err) {
process.stderr.write(
`[mcpm-guard] CONFINE-STORE-ERROR ${safeName}: ${err.message}
`
);
}
const confineDecision = decideConfine({
profile: confineProfile,
markerHash: parsed.confineProfileHash ?? null,
markerRequired: parsed.confineRequired === true,
backendAvailable: isConfineBackendAvailable()
});
if (confineDecision.action === "fail-closed") {
process.stderr.write(
`[mcpm-guard] CONFINE-BLOCK ${safeName}: ${confineDecision.reason}. Refusing to start (this server is marked require-confine). Run \`mcpm guard doctor-confine\` to check the backend, and review ~/.mcpm/guard-events.jsonl.
`
);
if (confineDecision.event !== void 0) {
void appendEvent(
confineGuardEvent(confineDecision.event, confineDecision.reason, "block", "critical"),
parsed.serverName
);
}
process.exit(1);
}
if (confineDecision.action === "confine" && confineProfile !== null) {
const wrapped = wrapForConfinement(confineProfile, parsed.command, parsed.args);
if (wrapped !== null) {
spawnCommand = wrapped.command;
spawnArgs = wrapped.args;
void appendEvent(
confineGuardEvent(
confineDecision.event ?? "confine-applied",
confineDecision.reason,
"pass",
"low"
),
parsed.serverName
);
} else {
const required = parsed.confineRequired === true || confineProfile.require_confine;
if (required) {
process.stderr.write(
`[mcpm-guard] CONFINE-BLOCK ${safeName}: sandbox backend became unavailable at spawn (require-confine). Refusing to start.
`
);
void appendEvent(
confineGuardEvent(
"confine-backend-missing",
"backend unavailable at wrap",
"block",
"critical"
),
parsed.serverName
);
process.exit(1);
}
process.stderr.write(
`[mcpm-guard] CONFINE-UNCONFINED ${safeName}: sandbox backend unavailable at wrap \u2014 running unconfined.
`
);
void appendEvent(
confineGuardEvent("confine-backend-missing", "backend unavailable at wrap", "warn", "high"),
parsed.serverName
);
}
} else if (confineDecision.event !== void 0) {
process.stderr.write(
`[mcpm-guard] CONFINE-UNCONFINED ${safeName}: ${confineDecision.reason} \u2014 running unconfined.
`
);
void appendEvent(
confineGuardEvent(confineDecision.event, confineDecision.reason, "warn", "high"),
parsed.serverName
);
}
const handle = startRelay({
command: spawnCommand,
args: spawnArgs,
env: childEnv,
parentIn: process.stdin,
parentOut: process.stdout,
inspectChildResponse: inspectChild,
inspectParentRequest: inspectParent,
onEvent: logEvent2
});
return handle.exit;
}
function sanitizeLabel(s) {
return sanitizeForTerminal(s, 128);
}
function inspectForDriftSync(msg, serverName, baseline, state) {
const armed = state.revalidationArmed;
state.revalidationArmed = false;
const result = msg.result;
const tools = Array.isArray(result?.tools) ? result.tools : [];
const findings = [];
for (const rawTool of tools) {
const finding = inspectToolDrift(rawTool, serverName, baseline, state, armed);
if (finding !== null) findings.push(finding);
}
const action = findings.reduce((acc, f) => {
const a = defaultActionForFinding(f);
return ACTION_RANK[a] > ACTION_RANK[acc] ? a : acc;
}, "pass");
return { action, findings };
}
function inspectToolDrift(rawTool, serverName, baseline, state, armed) {
if (rawTool === null || typeof rawTool !== "object") return null;
const tool = rawTool;
const toolName = typeof tool.name === "string" ? tool.name : null;
if (toolName === null) return null;
const fields = {
description: typeof tool.description === "string" ? tool.description : null,
schema: tool.inputSchema ?? tool.schema,
annotations: tool.annotations
};
const liveWhole = hashToolDefinition(fields);
const liveFields = fieldHashesOf(fields);
const serverPins = Object.hasOwn(baseline.servers, serverName) ? baseline.servers[serverName] : void 0;
const pinned = serverPins && Object.hasOwn(serverPins, toolName) ? serverPins[toolName] : void 0;
const sessionKey = `${serverName}::${toolName}`;
const firstSeen = state.firstHashes.get(sessionKey);
if (!armed && firstSeen !== void 0 && firstSeen !== liveWhole) {
return inSessionDriftFinding(serverName, toolName, firstSeen, liveWhole);
}
if (firstSeen === void 0 || armed) state.firstHashes.set(sessionKey, liveWhole);
if (!pinned || pinned.current_hash === null) return null;
if (liveWhole === pinned.current_hash) return null;
const cls = classifyDrift(pinned, liveFields);
const newDescriptionExcerpt = typeof tool.description === "string" ? sanitizeForTerminal(tool.description, 80) : void 0;
return buildDriftFinding({
cls,
safeServer: sanitizeLabel(serverName),
safeTool: sanitizeLabel(toolName),
expected: pinned.current_hash,
actual: liveWhole,
newDescriptionExcerpt
});
}
function inSessionDriftFinding(serverName, toolName, firstSeen, liveWhole) {
return {
signature_id: "schema-drift-in-session",
category: "OWASP-MCP-1",
severity: "critical",
target: "tool_description",
matched_text_excerpt: `${sanitizeLabel(toolName)}: ${firstSeen.slice(7, 19)}\u2026 \u2192 ${liveWhole.slice(7, 19)}\u2026 (same session)`,
remediation: `Server "${sanitizeLabel(serverName)}" delivered two different schemas for tool "${sanitizeLabel(toolName)}" in the same session. This is a rug-pull attempt; restart the IDE and reinspect the server's source.`
};
}
function isToolsListChangedNotification(msg) {
if (!("method" in msg)) return false;
if (msg.method !== "notifications/tools/list_changed") return false;
return !("result" in msg);
}
function isInitializeResult(msg) {
if (!("result" in msg)) return false;
const result = msg.result;
return result !== null && typeof result === "object" && typeof result.protocolVersion === "string";
}
function inspectHandshakeDriftSync(msg, serverName, baseline, state) {
const result = msg.result;
if (result === null || typeof result !== "object") return { action: "pass", findings: [] };
const liveFields = handshakeFieldHashesOf(result);
const liveCapKeys = handshakeCapabilityKeys(result);
const liveWhole = hashHandshake(liveFields);
const seen = state.handshakeSeenHash;
if (seen !== null && seen !== liveWhole) {
return warnResult(handshakeInSessionFinding(serverName, seen, liveWhole));
}
if (seen === null) state.handshakeSeenHash = liveWhole;
const pinned = lookupHandshake(baseline, serverName);
if (pinned === void 0) return { action: "pass", findings: [] };
if (liveWhole === pinned.current_hash || pinned.previous_hashes.includes(liveWhole)) {
return { action: "pass", findings: [] };
}
const cls = classifyHandshakeDrift(pinned, liveFields, liveCapKeys);
const findings = buildHandshakeDriftFinding({
cls,
safeServer: sanitizeLabel(serverName)
});
const action = findings.reduce((acc, f) => {
const a = defaultActionForFinding(f);
return ACTION_RANK[a] > ACTION_RANK[acc] ? a : acc;
}, "pass");
return { action, findings };
}
function warnResult(finding) {
return { action: defaultActionForFinding(finding), findings: [finding] };
}
function handshakeInSessionFinding(serverName, firstSeen, liveWhole) {
return {
signature_id: "handshake-drift-in-session",
category: "OWASP-MCP-1",
severity: "high",
target: "initialize_instructions",
matched_text_excerpt: `${sanitizeLabel(serverName)}: ${firstSeen.slice(7, 19)}\u2026 \u2192 ${liveWhole.slice(7, 19)}\u2026 (same session)`,
remediation: `Server "${sanitizeLabel(serverName)}" delivered two different initialize handshakes in the same session \u2014 initialize should occur once. Inspect the wrapped command; this is a warn-only signal and does not block the session.`
};
}
export {
applyPolicy,
inspectForDriftSync,
inspectHandshakeDriftSync,
isInitializeResult,
isToolsListChangedNotification,
runInner
};
//# sourceMappingURL=run-inner-E2H7X7PM.js.map

Sorry, the diff of this file is too big to display

#!/usr/bin/env node
import {
buildDoctorModel,
execCheckDefault,
formatMcpEntryCommand,
makeCheckConfigExists
} from "./chunk-DR7HERUD.js";
import {
resolveInstallEntry
} from "./chunk-LNGTDYGN.js";
import {
readPins
} from "./chunk-DDCTUMSZ.js";
import "./chunk-E3T224S3.js";
import {
fetchNpmProvenance
} from "./chunk-RTI2GLYX.js";
import "./chunk-WYSMWP2R.js";
import "./chunk-OIFKZA4V.js";
import "./chunk-FEXJHHDM.js";
import "./chunk-F6CHEUGO.js";
import {
nativeTrustScore
} from "./chunk-LSNEZAFR.js";
import "./chunk-UNGY7RTE.js";
import "./chunk-W4IAFBUN.js";
import "./chunk-2PWW3Q5Q.js";
import {
fetchNpmIntegrity
} from "./chunk-7RJXJERN.js";
import "./chunk-K4U7EXLG.js";
import "./chunk-NPJ3SGGS.js";
import "./chunk-6R7TL5O2.js";
import {
CLIENT_IDS
} from "./chunk-R4R2VPDA.js";
import "./chunk-4QDJ3I7X.js";
import "./chunk-3X76P3FG.js";
import {
extractRegistryMeta
} from "./chunk-U7N6FRYF.js";
import "./chunk-WT6V33F2.js";
// src/server/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
// src/server/tools.ts
import { z } from "zod";
var serverName = z.string().min(1).max(256);
var clientId = z.enum(CLIENT_IDS);
var NoArgsInput = z.strictObject({});
var SearchInput = z.strictObject({
query: z.string().min(1).max(200),
limit: z.number().int().min(1).max(100).optional().default(20)
});
var InstallInput = z.strictObject({
name: serverName,
client: clientId.optional(),
minTrustScore: z.number().min(0).max(100).optional().default(50)
});
var InfoInput = z.strictObject({
name: serverName
});
var ListInput = z.strictObject({
client: clientId.optional()
});
var RemoveInput = z.strictObject({
name: serverName,
client: clientId.optional()
});
var SetupInput = z.strictObject({
description: z.string().min(1).max(1e3),
client: clientId.optional(),
minTrustScore: z.number().min(0).max(100).optional().default(50)
});
var UpInput = z.strictObject({
stackFile: z.string().optional().default("mcpm.yaml"),
profile: z.string().optional(),
dryRun: z.boolean().optional().default(false)
});
// src/server/handlers.ts
import path from "path";
var SERVER_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,126}\/[a-zA-Z0-9][a-zA-Z0-9._-]{0,126}$/;
function validateMcpServerName(name) {
if (typeof name !== "string" || name.length === 0 || name.length > 256) {
throw new Error(`Invalid server name: must be a non-empty string under 256 characters.`);
}
if (!SERVER_NAME_RE.test(name)) {
throw new Error(
`Invalid server name format: "${name}". Expected format: "namespace/server-name" (alphanumeric, dots, hyphens, underscores only).`
);
}
}
function computeTrust(entry, deps) {
const findings = deps.scanTier1(entry);
return deps.computeTrustScore({
findings,
healthCheckPassed: null,
hasExternalScanner: false,
registryMeta: extractRegistryMeta(entry)
});
}
async function resolveClients(requestedClient, deps) {
const detected = await deps.detectClients();
if (detected.length === 0) {
throw new Error("No supported AI clients found.");
}
if (requestedClient !== void 0) {
if (!CLIENT_IDS.includes(requestedClient)) {
throw new Error(
`Unknown client "${requestedClient}". Valid values: ${CLIENT_IDS.join(", ")}.`
);
}
const id = requestedClient;
if (!detected.includes(id)) {
throw new Error(`Client "${requestedClient}" is not installed.`);
}
return [id];
}
return detected;
}
async function handleSearch(args, deps) {
const entries = await deps.registrySearch(args.query, args.limit);
const servers = entries.map((entry) => {
const trust = computeTrust(entry, deps);
return {
name: entry.server.name,
description: entry.server.description ?? "",
version: entry.server.version,
trustScore: trust.score
};
});
return { servers };
}
var DEFAULT_MIN_TRUST_SCORE = 50;
var HARD_TRUST_FLOOR = 25;
function effectiveMinTrustScore(requested) {
return Math.max(requested ?? DEFAULT_MIN_TRUST_SCORE, HARD_TRUST_FLOOR);
}
async function handleInstall(args, deps, preResolved) {
validateMcpServerName(args.name);
const entry = preResolved?.entry ?? await deps.registryGetServer(args.name);
const trust = preResolved?.trust ?? computeTrust(entry, deps);
const minScore = effectiveMinTrustScore(args.minTrustScore);
const nativeTrust = nativeTrustScore(trust);
if (nativeTrust.score < minScore) {
throw new Error(
`Server "${args.name}" has trust score ${nativeTrust.score}/${nativeTrust.maxPossible} (level: ${trust.level}), which is below the minimum threshold of ${minScore}. ` + (nativeTrust.excludedExternalCredit > 0 ? `An external scanner's ${nativeTrust.excludedExternalCredit} points are excluded from this floor because mcpm cannot verify them. ` : "") + `Install rejected for safety. Use mcpm CLI with --yes to override after manual review.`
);
}
const clients = await resolveClients(args.client, deps);
const planned = clients.map((clientId2) => {
const mcpEntry = resolveInstallEntry(entry, clientId2);
if (mcpEntry.url !== void 0 && mcpEntry.command === void 0) {
throw new Error(
`Server "${args.name}" uses a URL/HTTP transport and runs UNGUARDED (the guard relay only wraps stdio servers). Installing it is not permitted via the MCP surface. Use the mcpm CLI with --allow-unguarded after manual review.`
);
}
return {
clientId: clientId2,
adapter: deps.getAdapter(clientId2),
configPath: deps.getConfigPath(clientId2),
mcpEntry
};
});
const done = [];
try {
for (const p of planned) {
await p.adapter.addServer(p.configPath, args.name, p.mcpEntry);
done.push(p);
}
await deps.addToStore({
name: args.name,
version: entry.server.version,
clients: done.map((p) => p.clientId),
installedAt: (/* @__PURE__ */ new Date()).toISOString()
});
} catch (err) {
const stranded = await rollbackInstall(done, args.name);
if (stranded.length > 0) {
throw new Error(
`${err instanceof Error ? err.message : String(err)}
Rollback incomplete: "${args.name}" is STILL INSTALLED in ${stranded.join(", ")}. Remove it with \`mcpm remove ${args.name}\` before retrying.`
);
}
throw err;
}
return {
installed: true,
name: args.name,
version: entry.server.version,
clients: done.map((p) => p.clientId),
trustScore: trust
};
}
async function rollbackInstall(done, name) {
const stranded = [];
for (const p of done) {
try {
await p.adapter.removeServer(p.configPath, name);
} catch {
stranded.push(p.clientId);
}
}
return stranded;
}
async function handleInfo(args, deps) {
validateMcpServerName(args.name);
const entry = await deps.registryGetServer(args.name);
const trust = computeTrust(entry, deps);
return {
name: entry.server.name,
description: entry.server.description ?? "",
version: entry.server.version,
packages: entry.server.packages.map((p) => ({
registryType: p.registryType,
identifier: p.identifier
})),
trustScore: trust
};
}
async function handleList(args, deps) {
const clients = await resolveClients(args.client, deps);
const servers = [];
for (const clientId2 of clients) {
const adapter = deps.getAdapter(clientId2);
const configPath = deps.getConfigPath(clientId2);
const installed = await adapter.read(configPath);
for (const [name, entry] of Object.entries(installed)) {
const command = formatMcpEntryCommand(entry, "unknown");
servers.push({ name, client: clientId2, command });
}
}
return { servers };
}
async function handleRemove(args, deps) {
validateMcpServerName(args.name);
const clients = await resolveClients(args.client, deps);
const removedClients = [];
for (const clientId2 of clients) {
const adapter = deps.getAdapter(clientId2);
const configPath = deps.getConfigPath(clientId2);
try {
await adapter.removeServer(configPath, args.name);
removedClients.push(clientId2);
} catch {
}
}
if (removedClients.length === 0) {
throw new Error(`Server "${args.name}" not found in any client config.`);
}
try {
await deps.removeFromStore(args.name);
} catch {
}
return { removed: true, name: args.name, clients: removedClients };
}
async function handleAudit(deps) {
const clients = await deps.detectClients();
const results = [];
for (const clientId2 of clients) {
const adapter = deps.getAdapter(clientId2);
const configPath = deps.getConfigPath(clientId2);
const installed = await adapter.read(configPath);
for (const name of Object.keys(installed)) {
try {
const entry = await deps.registryGetServer(name);
const trust = computeTrust(entry, deps);
results.push({ name, client: clientId2, trustScore: trust });
} catch {
results.push({
name,
client: clientId2,
trustScore: { score: 0, maxPossible: 80, level: "risky", breakdown: { healthCheck: 0, staticScan: 0, externalScan: 0, registryMeta: 0 } }
});
}
}
}
return { results };
}
async function handleDoctor(deps) {
return buildDoctorModel({
getAdapter: deps.getAdapter,
getConfigPath: deps.getConfigPath,
checkConfigExists: makeCheckConfigExists(deps.getConfigPath),
execCheck: execCheckDefault
});
}
async function handleSetup(args, deps) {
if (!args.description.trim()) {
throw new Error("Could not extract any keywords from empty description.");
}
const keywords = extractKeywords(args.description);
const minScore = Math.max(
effectiveMinTrustScore(args.minTrustScore),
DEFAULT_MIN_TRUST_SCORE
);
const installed = [];
const skipped = [];
const searchResults = await Promise.all(
keywords.map(
(kw) => deps.registrySearch(kw, 5).then((entries) => ({ ok: true, entries })).catch((err) => ({
ok: false,
error: err instanceof Error ? err.message : String(err)
}))
)
);
const seenNames = /* @__PURE__ */ new Set();
for (let i = 0; i < keywords.length; i++) {
const keyword = keywords[i];
const outcome = searchResults[i];
if (!outcome.ok) {
skipped.push({ name: keyword, reason: `Registry search failed: ${outcome.error}` });
continue;
}
const entries = outcome.entries;
if (entries.length === 0) {
skipped.push({ name: keyword, reason: `No servers found for "${keyword}"` });
continue;
}
let bestEntry = null;
let bestTrust = null;
for (const entry of entries) {
if (seenNames.has(entry.server.name)) continue;
const trust = computeTrust(entry, deps);
if (bestTrust === null || trust.score > bestTrust.score) {
bestEntry = entry;
bestTrust = trust;
}
}
if (bestEntry === null || bestTrust === null) {
skipped.push({ name: keyword, reason: "All results already installed or duplicated" });
continue;
}
const bestNative = nativeTrustScore(bestTrust);
if (bestNative.score < minScore) {
skipped.push({
name: bestEntry.server.name,
reason: `Trust score ${bestNative.score}/${bestNative.maxPossible} is below minimum ${minScore}` + (bestNative.excludedExternalCredit > 0 ? ` (an external scanner's ${bestNative.excludedExternalCredit} points are excluded \u2014 mcpm cannot verify them)` : "")
});
continue;
}
try {
await handleInstall(
{ name: bestEntry.server.name, client: args.client },
deps,
{ entry: bestEntry, trust: bestTrust }
);
seenNames.add(bestEntry.server.name);
installed.push({ name: bestEntry.server.name, trustScore: bestTrust });
} catch (err) {
skipped.push({
name: bestEntry.server.name,
reason: `Install failed: ${err.message}`
});
}
}
const note = installed.length > 0 ? "Restart your AI client to use the newly installed servers." : void 0;
return { installed, skipped, ...note ? { note } : {} };
}
async function handleMcpUp(args, deps) {
const stackFile = args.stackFile ?? "mcpm.yaml";
const resolved = path.resolve(process.cwd(), stackFile);
if (resolved !== process.cwd() && !resolved.startsWith(process.cwd() + path.sep)) {
throw new Error("stackFile must be within the working directory");
}
{
const { realpath } = await import("fs/promises");
try {
const [realStack, realCwd] = await Promise.all([
realpath(resolved),
realpath(process.cwd())
]);
if (realStack !== realCwd && !realStack.startsWith(realCwd + path.sep)) {
throw new Error("stackFile must be within the working directory");
}
} catch (err) {
const code = err.code ?? "";
if (!["ENOENT", "ELOOP", "ENOTDIR"].includes(code)) throw err;
}
}
const { handleUp } = await import("./up-IF5B5AKH.js");
const { writeFile } = await import("fs/promises");
const { handleLock } = await import("./lock-3JKW72C5.js");
const { RegistryClient } = await import("./client-3RPMRFZL.js");
const { scanTier1: st1 } = await import("./tier1-OPUMS3NX.js");
const { checkScannerAvailable: csa, scanTier2: st2 } = await import("./tier2-PI43NCHZ.js");
const { computeTrustScore: cts } = await import("./trust-score-BAGF67DE.js");
const client = new RegistryClient();
const outputLines = [];
const records = [];
let thrownError;
try {
await handleUp(
{
stackFile,
profile: args.profile,
dryRun: args.dryRun,
ci: true,
yes: false,
// MCP surface lockdown (fixes C, D & H1): never auto-read ambient
// secrets from process.env OR the working-directory .env file, and never
// install URL servers (they bypass the registry trust gate). All three
// default to true on the CLI; the MCP (untrusted-caller) surface opts in
// to the locked-down behavior.
allowProcessEnv: false,
allowUrlServers: false,
allowEnvFile: false,
// M2: the batch `up` path must honor the same non-overridable trust floor
// the single-install MCP tool enforces (issue #24), so a low-trust server
// an agent could not install via mcpm_install can't slip in via mcpm_up.
minTrustFloor: HARD_TRUST_FLOOR
},
{
detectClients: deps.detectClients,
getAdapter: deps.getAdapter,
getPath: deps.getConfigPath,
getServer: (name, version) => client.getServer(name, version),
scanTier1: st1,
checkScannerAvailable: csa,
scanTier2: (name) => st2(name),
computeTrustScore: cts,
runLock: async (stackFile2) => {
await handleLock(
{ stackFile: stackFile2 },
{
getServerVersions: (name) => client.getServerVersions(name),
getServer: (name, v) => client.getServer(name, v),
scanTier1: st1,
checkScannerAvailable: csa,
scanTier2: (name) => st2(name),
computeTrustScore: cts,
writeLockFile: (path2, content) => writeFile(path2, content, { encoding: "utf-8", mode: 384 }),
fetchNpmIntegrity,
fetchNpmProvenance: (id, ver, sri) => fetchNpmProvenance(id, ver, { integritySri: sri }),
output: (text) => outputLines.push(text)
}
);
},
// Issue #22: never auto-confirm on the MCP (no-human-in-loop) surface.
// The previous `async () => true` blanket-approved every confirmation,
// including strict-mode *removals* of servers not in mcpm.yaml — a
// prompt-injected agent could silently mutate client configs. Refusing
// confirmation here means destructive prompts are declined; the trust
// policy still gates installs via checkTrustPolicy in handleUp.
confirm: async () => false,
promptEnvVar: async () => "",
output: (text) => outputLines.push(text),
fetchNpmIntegrity,
// F8/B3: wire the provenance re-check on the MCP surface too, or a
// policy.frozen: true stack run through mcpm_up would silently skip it.
fetchNpmProvenance: (id, v, o) => fetchNpmProvenance(id, v, o),
readPins,
recordResult: (r) => records.push(r)
}
);
} catch (err) {
thrownError = err instanceof Error ? err.message : String(err);
}
const installed = [];
const blocked = [];
const failed = [];
const skipped = [];
if (records.length > 0) {
for (const r of records) {
switch (r.status) {
case "installed":
installed.push(r.name);
break;
case "blocked":
blocked.push(r.name);
break;
case "failed":
failed.push(r.name);
break;
case "skipped":
case "removed":
skipped.push(r.name);
break;
}
}
} else {
for (const line of outputLines) {
if (line.includes("\u2713")) installed.push(line.trim());
else if (line.includes("\u2717") && line.includes("blocked")) blocked.push(line.trim());
else if (line.includes("\u2717")) failed.push(line.trim());
else if (line.includes("\u2022")) skipped.push(line.trim());
}
}
return {
installed,
blocked,
failed,
skipped,
...thrownError !== void 0 ? { error: thrownError } : {},
...installed.length > 0 ? { note: "Restart your AI client to use the newly installed servers." } : {}
};
}
var STOPWORDS = /\b(i need|set up|access|work with|connect to|a server that|a server for|to|the|a|an|my|for|and|with)\b/gi;
function extractKeywords(description) {
const cleaned = description.toLowerCase().replace(STOPWORDS, " ").replace(/[,&]/g, " ");
const tokens = cleaned.split(/\s+/).map((s) => s.trim()).filter((s) => s.length > 2);
if (tokens.length > 5) {
return [cleaned.replace(/\s+/g, " ").trim()];
}
return tokens.length > 0 ? tokens : [description.trim()];
}
// src/server/index.ts
async function createDeps() {
const { RegistryClient } = await import("./client-3RPMRFZL.js");
const { detectInstalledClients } = await import("./detector-ZI4OWRCJ.js");
const { getConfigPath } = await import("./paths-US27HRTP.js");
const { getAdapter } = await import("./config-XMU247VO.js");
const { scanTier1 } = await import("./tier1-OPUMS3NX.js");
const { computeTrustScore } = await import("./trust-score-BAGF67DE.js");
const { addInstalledServer, removeInstalledServer } = await import("./servers-IS6ZWSYC.js");
const client = new RegistryClient();
return {
registrySearch: async (query, limit) => {
const result = await client.searchServers(query, { limit });
return result.servers;
},
registryGetServer: (name) => client.getServer(name),
detectClients: detectInstalledClients,
getAdapter,
getConfigPath,
scanTier1,
computeTrustScore,
addToStore: addInstalledServer,
removeFromStore: removeInstalledServer
};
}
function registerTools(server, deps) {
server.registerTool("mcpm_search", {
description: "Search the MCP registry for servers with trust scores",
inputSchema: SearchInput,
annotations: { readOnlyHint: true }
}, async (args) => {
const result = await handleSearch(args, deps);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
});
server.registerTool("mcpm_install", {
description: "Install an MCP server with trust assessment",
inputSchema: InstallInput,
annotations: { destructiveHint: true }
}, async (args) => {
const result = await handleInstall(args, deps);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
});
server.registerTool("mcpm_info", {
description: "Show full details and trust score for an MCP server",
inputSchema: InfoInput,
annotations: { readOnlyHint: true }
}, async (args) => {
const result = await handleInfo(args, deps);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
});
server.registerTool("mcpm_list", {
description: "List installed MCP servers across AI clients",
inputSchema: ListInput,
annotations: { readOnlyHint: true }
}, async (args) => {
const result = await handleList(args, deps);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
});
server.registerTool("mcpm_remove", {
description: "Remove an MCP server from client configs",
inputSchema: RemoveInput,
annotations: { destructiveHint: true }
}, async (args) => {
const result = await handleRemove(args, deps);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
});
server.registerTool("mcpm_audit", {
inputSchema: NoArgsInput,
description: "Scan all installed servers and produce trust report",
annotations: { readOnlyHint: true }
}, async () => {
const result = await handleAudit(deps);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
});
server.registerTool("mcpm_doctor", {
inputSchema: NoArgsInput,
description: "Check MCP setup health",
annotations: { readOnlyHint: true }
}, async () => {
const result = await handleDoctor(deps);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
});
server.registerTool("mcpm_setup", {
description: "Install MCP servers from a natural language description",
inputSchema: SetupInput,
annotations: { destructiveHint: true }
}, async (args) => {
const result = await handleSetup(args, deps);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
});
server.registerTool("mcpm_up", {
description: "Install all servers from an mcpm.yaml stack file with trust verification. Equivalent to docker-compose up for MCP servers. Runs trust re-assessment and blocks servers that violate the trust policy. Pass profile to install only servers matching that profile, or dryRun to preview what would be installed without making changes.",
inputSchema: UpInput,
annotations: { destructiveHint: true }
}, async (args) => {
const result = await handleMcpUp(args, deps);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
});
}
async function startServer() {
const deps = await createDeps();
const server = new McpServer({
name: "mcpm",
// Issue #22: advertise the real package version (injected by tsup at build),
// not a hardcoded stale "0.1.0".
version: "0.29.1"
});
registerTools(server, deps);
const transport = new StdioServerTransport();
await server.connect(transport);
}
export {
registerTools,
startServer
};
//# sourceMappingURL=server-B4TJ4OGL.js.map
{"version":3,"sources":["../src/server/index.ts","../src/server/tools.ts","../src/server/handlers.ts"],"sourcesContent":["/**\n * MCP server for mcpm — exposes search, install, audit, and setup as tools.\n *\n * Uses @modelcontextprotocol/sdk with stdio transport.\n * All logic delegates to handlers.ts which wraps existing mcpm functions.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport {\n NoArgsInput,\n SearchInput,\n InstallInput,\n InfoInput,\n ListInput,\n RemoveInput,\n SetupInput,\n UpInput,\n} from \"./tools.js\";\nimport {\n handleSearch,\n handleInstall,\n handleInfo,\n handleList,\n handleRemove,\n handleAudit,\n handleDoctor,\n handleSetup,\n handleMcpUp,\n} from \"./handlers.js\";\nimport type { ServerDeps } from \"./handlers.js\";\n\n// ---------------------------------------------------------------------------\n// Wire up real dependencies\n// ---------------------------------------------------------------------------\n\nasync function createDeps(): Promise<ServerDeps> {\n const { RegistryClient } = await import(\"../registry/client.js\");\n const { detectInstalledClients } = await import(\"../config/detector.js\");\n const { getConfigPath } = await import(\"../config/paths.js\");\n const { getAdapter } = await import(\"../config/index.js\");\n const { scanTier1 } = await import(\"../scanner/tier1.js\");\n const { computeTrustScore } = await import(\"../scanner/trust-score.js\");\n const { addInstalledServer, removeInstalledServer } = await import(\"../store/servers.js\");\n\n const client = new RegistryClient();\n\n return {\n registrySearch: async (query, limit) => {\n const result = await client.searchServers(query, { limit });\n return result.servers;\n },\n registryGetServer: (name) => client.getServer(name),\n detectClients: detectInstalledClients,\n getAdapter,\n getConfigPath,\n scanTier1,\n computeTrustScore,\n addToStore: addInstalledServer,\n removeFromStore: removeInstalledServer,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Server setup\n// ---------------------------------------------------------------------------\n\n/**\n * Register every mcpm tool on the server. Extracted from startServer so the\n * registration can be unit-tested (fix F.1): a test spies registerTool and\n * asserts every TOOL_DEFINITIONS name is registered exactly once, guarding\n * against future tool/registration divergence.\n *\n * `server` is typed loosely as `Pick<McpServer, \"registerTool\">` so tests can\n * pass a lightweight spy without constructing a full McpServer.\n */\nexport function registerTools(\n server: Pick<McpServer, \"registerTool\">,\n deps: ServerDeps\n): void {\n // Register tools using registerTool API\n server.registerTool(\"mcpm_search\", {\n description: \"Search the MCP registry for servers with trust scores\",\n inputSchema: SearchInput,\n annotations: { readOnlyHint: true },\n }, async (args) => {\n const result = await handleSearch(args, deps);\n return { content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }] };\n });\n\n server.registerTool(\"mcpm_install\", {\n description: \"Install an MCP server with trust assessment\",\n inputSchema: InstallInput,\n annotations: { destructiveHint: true },\n }, async (args) => {\n const result = await handleInstall(args, deps);\n return { content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }] };\n });\n\n server.registerTool(\"mcpm_info\", {\n description: \"Show full details and trust score for an MCP server\",\n inputSchema: InfoInput,\n annotations: { readOnlyHint: true },\n }, async (args) => {\n const result = await handleInfo(args, deps);\n return { content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }] };\n });\n\n server.registerTool(\"mcpm_list\", {\n description: \"List installed MCP servers across AI clients\",\n inputSchema: ListInput,\n annotations: { readOnlyHint: true },\n }, async (args) => {\n const result = await handleList(args, deps);\n return { content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }] };\n });\n\n server.registerTool(\"mcpm_remove\", {\n description: \"Remove an MCP server from client configs\",\n inputSchema: RemoveInput,\n annotations: { destructiveHint: true },\n }, async (args) => {\n const result = await handleRemove(args, deps);\n return { content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }] };\n });\n\n server.registerTool(\"mcpm_audit\", {\n inputSchema: NoArgsInput,\n description: \"Scan all installed servers and produce trust report\",\n annotations: { readOnlyHint: true },\n }, async () => {\n const result = await handleAudit(deps);\n return { content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }] };\n });\n\n server.registerTool(\"mcpm_doctor\", {\n inputSchema: NoArgsInput,\n description: \"Check MCP setup health\",\n annotations: { readOnlyHint: true },\n }, async () => {\n const result = await handleDoctor(deps);\n return { content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }] };\n });\n\n server.registerTool(\"mcpm_setup\", {\n description: \"Install MCP servers from a natural language description\",\n inputSchema: SetupInput,\n annotations: { destructiveHint: true },\n }, async (args) => {\n const result = await handleSetup(args, deps);\n return { content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }] };\n });\n\n server.registerTool(\"mcpm_up\", {\n description: \"Install all servers from an mcpm.yaml stack file with trust verification. Equivalent to docker-compose up for MCP servers. Runs trust re-assessment and blocks servers that violate the trust policy. Pass profile to install only servers matching that profile, or dryRun to preview what would be installed without making changes.\",\n inputSchema: UpInput,\n annotations: { destructiveHint: true },\n }, async (args) => {\n const result = await handleMcpUp(args, deps);\n return { content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }] };\n });\n}\n\nexport async function startServer(): Promise<void> {\n const deps = await createDeps();\n\n const server = new McpServer({\n name: \"mcpm\",\n // Issue #22: advertise the real package version (injected by tsup at build),\n // not a hardcoded stale \"0.1.0\".\n version: __PKG_VERSION__,\n });\n\n registerTools(server, deps);\n\n // Start stdio transport\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n","/**\n * MCP tool definitions for mcpm serve.\n *\n * Each tool has a name, description, and Zod input schema.\n * Handlers are in handlers.ts.\n */\n\nimport { z } from \"zod\";\nimport { CLIENT_IDS } from \"../config/paths.js\";\n\nexport const TOOL_DEFINITIONS = [\n {\n name: \"mcpm_search\",\n description: \"Search the MCP registry for servers. Returns results with trust scores.\",\n inputSchema: {\n type: \"object\" as const,\n properties: {\n query: { type: \"string\", description: \"Search query (substring match on server name)\" },\n limit: { type: \"number\", description: \"Max results to return (default 20)\" },\n },\n required: [\"query\"],\n },\n },\n {\n name: \"mcpm_install\",\n description: \"Install an MCP server from the registry into detected AI client configs. Runs trust assessment automatically. Rejects servers below the minimum trust score (default 50).\",\n inputSchema: {\n type: \"object\" as const,\n properties: {\n name: { type: \"string\", description: \"Server name (e.g. io.github.domdomegg/filesystem-mcp)\" },\n client: { type: \"string\", description: \"Install to specific client only (claude-desktop, cursor, vscode, windsurf)\" },\n minTrustScore: { type: \"number\", description: \"Minimum trust score to allow install (default 50, range 0-100)\" },\n },\n required: [\"name\"],\n },\n },\n {\n name: \"mcpm_info\",\n description: \"Show full details for an MCP server including trust score breakdown.\",\n inputSchema: {\n type: \"object\" as const,\n properties: {\n name: { type: \"string\", description: \"Server name\" },\n },\n required: [\"name\"],\n },\n },\n {\n name: \"mcpm_list\",\n description: \"List all installed MCP servers across detected AI clients.\",\n inputSchema: {\n type: \"object\" as const,\n properties: {\n client: { type: \"string\", description: \"Filter to specific client\" },\n },\n required: [],\n },\n },\n {\n name: \"mcpm_remove\",\n description: \"Remove an MCP server from AI client configs.\",\n inputSchema: {\n type: \"object\" as const,\n properties: {\n name: { type: \"string\", description: \"Server name to remove\" },\n client: { type: \"string\", description: \"Remove from specific client only\" },\n },\n required: [\"name\"],\n },\n },\n {\n name: \"mcpm_audit\",\n description: \"Scan all installed MCP servers and produce a trust report with scores.\",\n inputSchema: {\n type: \"object\" as const,\n properties: {},\n required: [],\n },\n },\n {\n name: \"mcpm_doctor\",\n description: \"Check MCP setup health: detected clients, available runtimes, configuration issues.\",\n inputSchema: {\n type: \"object\" as const,\n properties: {},\n required: [],\n },\n },\n {\n name: \"mcpm_setup\",\n description: \"Install MCP servers from a natural language description. Searches, evaluates trust, installs the best match for each keyword. Example: 'filesystem and GitHub' installs filesystem + GitHub servers.\",\n inputSchema: {\n type: \"object\" as const,\n properties: {\n description: { type: \"string\", description: \"What you need (e.g. 'filesystem access and GitHub integration')\" },\n client: { type: \"string\", description: \"Install to specific client only\" },\n minTrustScore: { type: \"number\", description: \"Minimum trust score to auto-install (default 50, range 0-100)\" },\n },\n required: [\"description\"],\n },\n },\n {\n name: \"mcpm_up\",\n description: \"Install all servers from an mcpm.yaml stack file with trust verification. Equivalent to docker-compose up for MCP servers. Runs trust re-assessment and blocks servers that violate the trust policy. Pass profile to install only servers matching that profile, or dryRun to preview what would be installed without making changes.\",\n inputSchema: {\n type: \"object\" as const,\n properties: {\n stackFile: { type: \"string\", description: \"Path to mcpm.yaml (default: mcpm.yaml in CWD)\" },\n profile: { type: \"string\", description: \"Install only servers matching this profile\" },\n dryRun: { type: \"boolean\", description: \"Show what would be installed without making changes\" },\n },\n required: [],\n },\n },\n] as const;\n\n// Shared field schemas (security #31): a bounded server-name string and a closed\n// client enum, so the Zod layer — not just the runtime `validateMcpServerName` /\n// `CLIENT_IDS.includes` checks in handlers.ts — is the declarative enforcement\n// point. The objects below are `strictObject` so unknown keys are rejected\n// instead of silently dropped.\n//\n// These are passed to `registerTool` WHOLE (not via `.shape`) — see\n// server/index.ts. That distinction is load-bearing: the SDK accepts either a\n// raw shape or a full schema, but a raw shape is rebuilt as a plain\n// `z.object(shape)`, which silently DROPS the object-level `strict` setting.\n// Per-field constraints (the length bound, the client enum) survive either way;\n// strictness does not.\n//\n// Passing the whole schema means the SDK rejects unknown keys with a JSON-RPC\n// -32602 `unrecognized_keys` error, AND advertises `additionalProperties: false`\n// in `tools/list` so a caller can see the contract before calling. Verified over\n// a real in-memory MCP transport in server-strict-schema.test.ts.\n//\n// The runtime guards in handlers.ts (`validateMcpServerName`, `CLIENT_IDS`)\n// remain as defence in depth.\nconst serverName = z.string().min(1).max(256);\nconst clientId = z.enum(CLIENT_IDS);\n\n/**\n * Zero-argument tools (`mcpm_audit`, `mcpm_doctor`) still declare a CLOSED\n * schema rather than omitting `inputSchema` entirely. Omitting it advertises no\n * `additionalProperties: false`, so any argument a caller passes is silently\n * ignored — for a tool that takes nothing, that means EVERY argument is\n * silently ignored. An empty strict object makes the contract explicit and\n * turns a mistaken call into a clear error.\n */\nexport const NoArgsInput = z.strictObject({});\n\nexport const SearchInput = z.strictObject({\n query: z.string().min(1).max(200),\n limit: z.number().int().min(1).max(100).optional().default(20),\n});\n\nexport const InstallInput = z.strictObject({\n name: serverName,\n client: clientId.optional(),\n minTrustScore: z.number().min(0).max(100).optional().default(50),\n});\n\nexport const InfoInput = z.strictObject({\n name: serverName,\n});\n\nexport const ListInput = z.strictObject({\n client: clientId.optional(),\n});\n\nexport const RemoveInput = z.strictObject({\n name: serverName,\n client: clientId.optional(),\n});\n\nexport const SetupInput = z.strictObject({\n description: z.string().min(1).max(1000),\n client: clientId.optional(),\n minTrustScore: z.number().min(0).max(100).optional().default(50),\n});\n\nexport const UpInput = z.strictObject({\n stackFile: z.string().optional().default(\"mcpm.yaml\"),\n profile: z.string().optional(),\n dryRun: z.boolean().optional().default(false),\n});\n","/**\n * MCP tool handlers for mcpm serve.\n *\n * Each handler wraps existing mcpm logic and returns structured JSON.\n * All dependencies are injectable for testability.\n */\n\nimport path from \"node:path\";\nimport type { ClientId } from \"../config/paths.js\";\nimport { CLIENT_IDS } from \"../config/paths.js\";\nimport type { ConfigAdapter, McpServerEntry } from \"../config/adapters/index.js\";\nimport type { ServerEntry } from \"../registry/types.js\";\nimport type { Finding } from \"../scanner/tier1.js\";\nimport type { TrustScore, TrustScoreInput } from \"../scanner/trust-score.js\";\nimport { nativeTrustScore } from \"../scanner/trust-score.js\";\nimport { extractRegistryMeta } from \"../utils/format-trust.js\";\nimport { formatMcpEntryCommand } from \"../utils/format-entry.js\";\nimport { resolveInstallEntry } from \"../commands/install.js\";\nimport { buildDoctorModel, makeCheckConfigExists, execCheckDefault } from \"../commands/doctor.js\";\nimport { fetchNpmIntegrity as _fetchNpmIntegrity } from \"../registry/npm-integrity.js\";\nimport { fetchNpmProvenance as _fetchNpmProvenance } from \"../registry/npm-provenance.js\";\nimport { readPins as _readPins } from \"../guard/pins.js\";\n\n// ---------------------------------------------------------------------------\n// Input validation for MCP server tool arguments\n// ---------------------------------------------------------------------------\n\n/**\n * Server name pattern for MCP registry names.\n * Format: \"namespace/server-name\" — alphanumeric with dots, hyphens, underscores.\n * Max length 256 to prevent abuse. Must not contain shell metacharacters,\n * path traversal sequences, or control characters.\n */\nconst SERVER_NAME_RE =\n /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,126}\\/[a-zA-Z0-9][a-zA-Z0-9._-]{0,126}$/;\n\n/**\n * Validate a server name received from an MCP tool call.\n * This is the trust boundary — AI agents provide these strings, and they\n * could be influenced by prompt injection or adversarial inputs.\n */\nfunction validateMcpServerName(name: string): void {\n if (typeof name !== \"string\" || name.length === 0 || name.length > 256) {\n throw new Error(`Invalid server name: must be a non-empty string under 256 characters.`);\n }\n if (!SERVER_NAME_RE.test(name)) {\n throw new Error(\n `Invalid server name format: \"${name}\". Expected format: \"namespace/server-name\" ` +\n `(alphanumeric, dots, hyphens, underscores only).`\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Dependency injection types\n// ---------------------------------------------------------------------------\n\nexport interface ServerDeps {\n registrySearch: (query: string, limit: number) => Promise<ServerEntry[]>;\n registryGetServer: (name: string) => Promise<ServerEntry>;\n detectClients: () => Promise<ClientId[]>;\n getAdapter: (clientId: ClientId) => ConfigAdapter;\n getConfigPath: (clientId: ClientId) => string;\n scanTier1: (server: ServerEntry) => Finding[];\n computeTrustScore: (input: TrustScoreInput) => TrustScore;\n addToStore: (server: { name: string; version: string; clients: ClientId[]; installedAt: string }) => Promise<void>;\n removeFromStore: (name: string) => Promise<void>;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * F4 scope note: this helper deliberately does NOT include the\n * release-cooldown finding (ServerDeps has no injectable clock; the F4 spec\n * file list excludes server/). Consequence: mcpm_install / mcpm_search score\n * a fresh (<24h) package up to 5 points higher than CLI install/why AND than\n * the sibling mcpm_up tool (which inherits the finding via up.ts\n * processServer), and HARD_TRUST_FLOOR evaluates that inflated score — do NOT\n * compensate by raising the floor. Fast-follow is mechanical:\n * ServerDeps += now?: () => number, then append\n * assessReleaseAge({...}).finding here; no schema changes.\n */\nfunction computeTrust(entry: ServerEntry, deps: ServerDeps): TrustScore {\n const findings = deps.scanTier1(entry);\n return deps.computeTrustScore({\n findings,\n healthCheckPassed: null,\n hasExternalScanner: false,\n registryMeta: extractRegistryMeta(entry),\n });\n}\n\nasync function resolveClients(\n requestedClient: string | undefined,\n deps: ServerDeps\n): Promise<ClientId[]> {\n const detected = await deps.detectClients();\n if (detected.length === 0) {\n throw new Error(\"No supported AI clients found.\");\n }\n if (requestedClient !== undefined) {\n if (!CLIENT_IDS.includes(requestedClient as ClientId)) {\n throw new Error(\n `Unknown client \"${requestedClient}\". Valid values: ${CLIENT_IDS.join(\", \")}.`\n );\n }\n const id = requestedClient as ClientId;\n if (!detected.includes(id)) {\n throw new Error(`Client \"${requestedClient}\" is not installed.`);\n }\n return [id];\n }\n return detected;\n}\n\n// ---------------------------------------------------------------------------\n// Handlers\n// ---------------------------------------------------------------------------\n\nexport async function handleSearch(\n args: { query: string; limit: number },\n deps: ServerDeps\n): Promise<object> {\n const entries = await deps.registrySearch(args.query, args.limit);\n const servers = entries.map((entry) => {\n const trust = computeTrust(entry, deps);\n return {\n name: entry.server.name,\n description: entry.server.description ?? \"\",\n version: entry.server.version,\n trustScore: trust.score,\n };\n });\n return { servers };\n}\n\n/** Default minimum trust score for MCP server tool installs (no human in the loop). */\nconst DEFAULT_MIN_TRUST_SCORE = 50;\n\n/**\n * Hard, non-overridable trust floor for the MCP server surface (issue #24).\n *\n * The MCP `minTrustScore` input accepts `0`, which a prompt-injected agent could\n * pass to disable the install gate entirely. We clamp the effective threshold to\n * `Math.max(userValue, HARD_TRUST_FLOOR)` so no caller-supplied value can lower\n * the gate below this floor. This protects the no-human-in-loop path; the CLI\n * (with a human confirmation prompt) is the only place to install below it.\n *\n * Lowering the gate is only half of it. A score can also be pushed UP to meet\n * the gate, and `MCPM_EXTERNAL_SCANNER` is caller-supplied too — so the floor is\n * evaluated against `nativeTrustScore`, which excludes the external bucket's\n * unverifiable credit (TODOS #33).\n */\nconst HARD_TRUST_FLOOR = 25;\n\n/** Clamp a requested minimum trust score so it can never sink below the floor. */\nfunction effectiveMinTrustScore(requested: number | undefined): number {\n return Math.max(requested ?? DEFAULT_MIN_TRUST_SCORE, HARD_TRUST_FLOOR);\n}\n\nexport async function handleInstall(\n args: { name: string; client?: string; minTrustScore?: number },\n deps: ServerDeps,\n preResolved?: { entry: ServerEntry; trust: TrustScore }\n): Promise<object> {\n validateMcpServerName(args.name);\n const entry = preResolved?.entry ?? await deps.registryGetServer(args.name);\n const trust = preResolved?.trust ?? computeTrust(entry, deps);\n\n // Security gate: reject servers below the minimum trust score.\n // Unlike the CLI path which has a human confirmation prompt, the MCP server\n // path is driven by AI agents with no human in the loop. A malicious prompt\n // could trick an agent into installing a dangerous server, so we enforce a\n // hard trust floor here. Issue #24: minTrustScore:0 must NOT disable the gate —\n // the effective threshold is clamped to HARD_TRUST_FLOOR.\n //\n // TODOS #33: compared against mcpm's OWN evidence. `computeTrust` above already\n // passes `hasExternalScanner: false`, so today this subtracts nothing — it is\n // here so that wiring a scanner into this path later cannot silently reopen the\n // floor, which is the failure mode #33 found on the sibling `mcpm_up` path.\n const minScore = effectiveMinTrustScore(args.minTrustScore);\n const nativeTrust = nativeTrustScore(trust);\n if (nativeTrust.score < minScore) {\n throw new Error(\n `Server \"${args.name}\" has trust score ${nativeTrust.score}/${nativeTrust.maxPossible} ` +\n `(level: ${trust.level}), which is below the minimum threshold of ${minScore}. ` +\n (nativeTrust.excludedExternalCredit > 0\n ? `An external scanner's ${nativeTrust.excludedExternalCredit} points are excluded from this floor because mcpm cannot verify them. `\n : \"\") +\n `Install rejected for safety. Use mcpm CLI with --yes to override after manual review.`\n );\n }\n\n const clients = await resolveClients(args.client, deps);\n\n // PRE-FLIGHT: resolve and validate EVERY client's entry before touching a single\n // config. resolveInstallEntry's URL rule is cursor-ONLY, so a server carrying both\n // an npm package and an http remote used to install cleanly on claude-desktop and\n // only THEN hit the H9 deny on cursor — the agent was told the install failed while\n // a live execution surface sat in Claude Desktop, with no store record of it.\n const planned = clients.map((clientId) => {\n const mcpEntry = resolveInstallEntry(entry, clientId);\n // H9 (fail-closed): a URL/HTTP-transport entry (url, no command) runs\n // UNGUARDED — the guard relay only wraps a stdio process. The MCP surface is\n // driven by an untrusted agent with no human in the loop and no\n // `--allow-unguarded` opt-in, so url-transport installs are HARD-DENIED here\n // (mirrors the batch `up` MCP wiring's allowUrlServers:false kill-switch).\n if (mcpEntry.url !== undefined && mcpEntry.command === undefined) {\n throw new Error(\n `Server \"${args.name}\" uses a URL/HTTP transport and runs UNGUARDED ` +\n `(the guard relay only wraps stdio servers). Installing it is not permitted ` +\n `via the MCP surface. Use the mcpm CLI with --allow-unguarded after manual review.`\n );\n }\n return {\n clientId,\n adapter: deps.getAdapter(clientId),\n configPath: deps.getConfigPath(clientId),\n mcpEntry,\n };\n });\n\n // APPLY as a unit. Any failure — a client write or the store write — unwinds every\n // write already made, so this tool never reports failure over a live install.\n // The store write is inside the transaction on purpose: configs written without a\n // store record are invisible to `mcpm list` / `audit` and survive `mcpm remove`.\n const done: PlannedInstall[] = [];\n try {\n for (const p of planned) {\n await p.adapter.addServer(p.configPath, args.name, p.mcpEntry);\n done.push(p);\n }\n await deps.addToStore({\n name: args.name,\n version: entry.server.version,\n clients: done.map((p) => p.clientId),\n installedAt: new Date().toISOString(),\n });\n } catch (err) {\n const stranded = await rollbackInstall(done, args.name);\n if (stranded.length > 0) {\n // Rollback is best-effort and can fail too. Swallowing that would report a\n // clean failure over a server that is still installed — name it instead.\n throw new Error(\n `${err instanceof Error ? err.message : String(err)}\\n\\n` +\n `Rollback incomplete: \"${args.name}\" is STILL INSTALLED in ${stranded.join(\", \")}. ` +\n `Remove it with \\`mcpm remove ${args.name}\\` before retrying.`\n );\n }\n throw err;\n }\n\n return {\n installed: true,\n name: args.name,\n version: entry.server.version,\n clients: done.map((p) => p.clientId),\n trustScore: trust,\n };\n}\n\n/** One client's fully-resolved install, validated and ready to write. */\ntype PlannedInstall = {\n clientId: ClientId;\n adapter: ConfigAdapter;\n configPath: string;\n mcpEntry: McpServerEntry;\n};\n\n/**\n * Undo the client-config writes already made by a failed install.\n * @returns the clients that could NOT be rolled back (still installed).\n */\nasync function rollbackInstall(done: PlannedInstall[], name: string): Promise<ClientId[]> {\n const stranded: ClientId[] = [];\n for (const p of done) {\n try {\n await p.adapter.removeServer(p.configPath, name);\n } catch {\n stranded.push(p.clientId);\n }\n }\n return stranded;\n}\n\nexport async function handleInfo(\n args: { name: string },\n deps: ServerDeps\n): Promise<object> {\n validateMcpServerName(args.name);\n const entry = await deps.registryGetServer(args.name);\n const trust = computeTrust(entry, deps);\n return {\n name: entry.server.name,\n description: entry.server.description ?? \"\",\n version: entry.server.version,\n packages: entry.server.packages.map((p) => ({\n registryType: p.registryType,\n identifier: p.identifier,\n })),\n trustScore: trust,\n };\n}\n\nexport async function handleList(\n args: { client?: string },\n deps: ServerDeps\n): Promise<object> {\n const clients = await resolveClients(args.client, deps);\n const servers: Array<{ name: string; client: string; command: string }> = [];\n\n for (const clientId of clients) {\n const adapter = deps.getAdapter(clientId);\n const configPath = deps.getConfigPath(clientId);\n const installed = await adapter.read(configPath);\n\n for (const [name, entry] of Object.entries(installed)) {\n const command = formatMcpEntryCommand(entry, \"unknown\");\n servers.push({ name, client: clientId, command });\n }\n }\n\n return { servers };\n}\n\nexport async function handleRemove(\n args: { name: string; client?: string },\n deps: ServerDeps\n): Promise<object> {\n validateMcpServerName(args.name);\n const clients = await resolveClients(args.client, deps);\n const removedClients: ClientId[] = [];\n\n for (const clientId of clients) {\n const adapter = deps.getAdapter(clientId);\n const configPath = deps.getConfigPath(clientId);\n try {\n await adapter.removeServer(configPath, args.name);\n removedClients.push(clientId);\n } catch {\n // Server not in this client, skip\n }\n }\n\n if (removedClients.length === 0) {\n throw new Error(`Server \"${args.name}\" not found in any client config.`);\n }\n\n try {\n await deps.removeFromStore(args.name);\n } catch {\n // Not in store, fine\n }\n\n return { removed: true, name: args.name, clients: removedClients };\n}\n\nexport async function handleAudit(deps: ServerDeps): Promise<object> {\n const clients = await deps.detectClients();\n const results: Array<{ name: string; client: string; trustScore: TrustScore }> = [];\n\n for (const clientId of clients) {\n const adapter = deps.getAdapter(clientId);\n const configPath = deps.getConfigPath(clientId);\n const installed = await adapter.read(configPath);\n\n for (const name of Object.keys(installed)) {\n try {\n const entry = await deps.registryGetServer(name);\n const trust = computeTrust(entry, deps);\n results.push({ name, client: clientId, trustScore: trust });\n } catch {\n results.push({\n name,\n client: clientId,\n trustScore: { score: 0, maxPossible: 80, level: \"risky\", breakdown: { healthCheck: 0, staticScan: 0, externalScan: 0, registryMeta: 0 } },\n });\n }\n }\n }\n\n return { results };\n}\n\nexport async function handleDoctor(deps: ServerDeps): Promise<object> {\n // Reuse the CLI's structured model so this tool reports real issues instead of\n // the formerly-hardcoded `issues: []` (D7). Honors the injected getConfigPath.\n return buildDoctorModel({\n getAdapter: deps.getAdapter,\n getConfigPath: deps.getConfigPath,\n checkConfigExists: makeCheckConfigExists(deps.getConfigPath),\n execCheck: execCheckDefault,\n });\n}\n\nexport async function handleSetup(\n args: { description: string; client?: string; minTrustScore: number },\n deps: ServerDeps\n): Promise<object> {\n if (!args.description.trim()) {\n throw new Error(\"Could not extract any keywords from empty description.\");\n }\n const keywords = extractKeywords(args.description);\n\n // Issue #24: clamp to the hard floor so minTrustScore:0 can't disable the gate\n // on the no-human-in-loop setup path either.\n //\n // Also clamp UP to handleInstall's own default. This pre-filter delegates to\n // handleInstall without forwarding minTrustScore, so the enforcing gate always\n // applies DEFAULT_MIN_TRUST_SCORE. With a requested 25-49 the two disagreed:\n // the pre-filter waved the server through and handleInstall then refused it,\n // reporting a trust rejection as \"Install failed\" and quoting a threshold the\n // caller never asked for. Taking the stricter of the two makes the pre-filter\n // report exactly what the enforcing gate will do. Deliberately NOT fixed by\n // forwarding minTrustScore instead -- that would let a caller-supplied 30\n // LOWER the gate this path enforces today.\n const minScore = Math.max(\n effectiveMinTrustScore(args.minTrustScore),\n DEFAULT_MIN_TRUST_SCORE,\n );\n\n const installed: Array<{ name: string; trustScore: TrustScore }> = [];\n const skipped: Array<{ name: string; reason: string }> = [];\n\n // Parallel search pass — all keywords searched concurrently. Capture the\n // thrown error per keyword so a registry outage is distinguishable from a\n // genuine empty result (both otherwise look like \"no servers\").\n type SearchOutcome =\n | { ok: true; entries: ServerEntry[] }\n | { ok: false; error: string };\n const searchResults: SearchOutcome[] = await Promise.all(\n keywords.map((kw) =>\n deps\n .registrySearch(kw, 5)\n .then((entries): SearchOutcome => ({ ok: true, entries }))\n .catch((err): SearchOutcome => ({\n ok: false,\n error: err instanceof Error ? err.message : String(err),\n }))\n )\n );\n\n const seenNames = new Set<string>();\n\n // Sequential evaluate/install pass (installs depend on previous state)\n for (let i = 0; i < keywords.length; i++) {\n const keyword = keywords[i];\n const outcome = searchResults[i];\n\n if (!outcome.ok) {\n skipped.push({ name: keyword, reason: `Registry search failed: ${outcome.error}` });\n continue;\n }\n\n const entries = outcome.entries;\n\n if (entries.length === 0) {\n skipped.push({ name: keyword, reason: `No servers found for \"${keyword}\"` });\n continue;\n }\n\n let bestEntry: ServerEntry | null = null;\n let bestTrust: TrustScore | null = null;\n\n for (const entry of entries) {\n if (seenNames.has(entry.server.name)) continue;\n const trust = computeTrust(entry, deps);\n if (bestTrust === null || trust.score > bestTrust.score) {\n bestEntry = entry;\n bestTrust = trust;\n }\n }\n\n if (bestEntry === null || bestTrust === null) {\n skipped.push({ name: keyword, reason: \"All results already installed or duplicated\" });\n continue;\n }\n\n // TODOS #33: the same native-evidence rule as the sibling gates. handleInstall\n // below re-checks and is the enforcing gate, so this pre-filter exists to\n // produce an accurate \"skipped\" reason rather than an \"Install failed\" one —\n // but it must agree with it, or a server rejected downstream gets reported\n // under the wrong heading with a score that was never compared.\n const bestNative = nativeTrustScore(bestTrust);\n if (bestNative.score < minScore) {\n skipped.push({\n name: bestEntry.server.name,\n reason:\n `Trust score ${bestNative.score}/${bestNative.maxPossible} is below minimum ${minScore}` +\n (bestNative.excludedExternalCredit > 0\n ? ` (an external scanner's ${bestNative.excludedExternalCredit} points are excluded — mcpm cannot verify them)`\n : \"\"),\n });\n continue;\n }\n\n try {\n await handleInstall(\n { name: bestEntry.server.name, client: args.client },\n deps,\n { entry: bestEntry, trust: bestTrust }\n );\n seenNames.add(bestEntry.server.name);\n installed.push({ name: bestEntry.server.name, trustScore: bestTrust });\n } catch (err) {\n skipped.push({\n name: bestEntry.server.name,\n reason: `Install failed: ${(err as Error).message}`,\n });\n }\n }\n\n const note = installed.length > 0\n ? \"Restart your AI client to use the newly installed servers.\"\n : undefined;\n\n return { installed, skipped, ...(note ? { note } : {}) };\n}\n\n// ---------------------------------------------------------------------------\n// mcpm_up — batch install from stack file\n// ---------------------------------------------------------------------------\n\nexport async function handleMcpUp(\n args: { stackFile?: string; profile?: string; dryRun?: boolean },\n deps: ServerDeps\n): Promise<{\n installed: string[];\n blocked: string[];\n failed: string[];\n skipped: string[];\n error?: string;\n note?: string;\n}> {\n // Validate stackFile path (AI agent trust boundary). Zod defaults stackFile to\n // \"mcpm.yaml\", so the old `if (args.stackFile !== undefined)` guard was dead.\n // Enforce real containment unconditionally via resolved paths: path.resolve\n // normalizes Windows backslashes and \"..\", so this catches traversal and\n // absolute escapes that string-only checks miss.\n const stackFile = args.stackFile ?? \"mcpm.yaml\";\n const resolved = path.resolve(process.cwd(), stackFile);\n if (\n resolved !== process.cwd() &&\n !resolved.startsWith(process.cwd() + path.sep)\n ) {\n throw new Error(\"stackFile must be within the working directory\");\n }\n // M3: the lexical check above catches \"../\" and absolute escapes, but NOT a\n // symlink that lives inside cwd yet points outside it — the file reader would\n // follow it (arbitrary out-of-tree read). Resolve the REAL path and re-check.\n // realpath throws ENOENT when the file does not exist yet; that's fine — handleUp\n // reports the missing file. A containment failure thrown inside the try is not\n // an ErrnoException, so the catch re-throws it.\n {\n const { realpath } = await import(\"node:fs/promises\");\n try {\n const [realStack, realCwd] = await Promise.all([\n realpath(resolved),\n realpath(process.cwd()),\n ]);\n if (realStack !== realCwd && !realStack.startsWith(realCwd + path.sep)) {\n throw new Error(\"stackFile must be within the working directory\");\n }\n } catch (err) {\n // ENOENT (no such file), ELOOP (circular symlink), and ENOTDIR (a path\n // component is a file) all mean \"no real path to contain\" — fall through and\n // let handleUp report the missing/invalid file. Re-throwing them would leak a\n // raw internal ErrnoException (with stack) to the untrusted caller. The\n // containment Error thrown just above has no `.code`, so it still propagates.\n const code = (err as NodeJS.ErrnoException).code ?? \"\";\n if (![\"ENOENT\", \"ELOOP\", \"ENOTDIR\"].includes(code)) throw err;\n }\n }\n\n const { handleUp } = await import(\"../commands/up.js\");\n const { writeFile } = await import(\"fs/promises\");\n const { handleLock } = await import(\"../commands/lock.js\");\n const { RegistryClient } = await import(\"../registry/client.js\");\n const { scanTier1: st1 } = await import(\"../scanner/tier1.js\");\n const { checkScannerAvailable: csa, scanTier2: st2 } = await import(\"../scanner/tier2.js\");\n const { computeTrustScore: cts } = await import(\"../scanner/trust-score.js\");\n\n const client = new RegistryClient();\n const outputLines: string[] = [];\n // Fix A/D: structured per-server results from handleUp. Authoritative source\n // for categorization — emoji-scraping cannot distinguish blocked from failed.\n const records: Array<{ name: string; status: string }> = [];\n let thrownError: string | undefined;\n\n try {\n await handleUp(\n {\n stackFile,\n profile: args.profile,\n dryRun: args.dryRun,\n ci: true,\n yes: false,\n // MCP surface lockdown (fixes C, D & H1): never auto-read ambient\n // secrets from process.env OR the working-directory .env file, and never\n // install URL servers (they bypass the registry trust gate). All three\n // default to true on the CLI; the MCP (untrusted-caller) surface opts in\n // to the locked-down behavior.\n allowProcessEnv: false,\n allowUrlServers: false,\n allowEnvFile: false,\n // M2: the batch `up` path must honor the same non-overridable trust floor\n // the single-install MCP tool enforces (issue #24), so a low-trust server\n // an agent could not install via mcpm_install can't slip in via mcpm_up.\n minTrustFloor: HARD_TRUST_FLOOR,\n },\n {\n detectClients: deps.detectClients,\n getAdapter: deps.getAdapter,\n getPath: deps.getConfigPath,\n getServer: (name, version?) => client.getServer(name, version),\n scanTier1: st1,\n checkScannerAvailable: csa,\n scanTier2: (name) => st2(name),\n computeTrustScore: cts,\n runLock: async (stackFile) => {\n await handleLock(\n { stackFile },\n {\n getServerVersions: (name) => client.getServerVersions(name),\n getServer: (name, v?) => client.getServer(name, v),\n scanTier1: st1,\n checkScannerAvailable: csa,\n scanTier2: (name) => st2(name),\n computeTrustScore: cts,\n writeLockFile: (path, content) =>\n writeFile(path, content, { encoding: \"utf-8\", mode: 0o600 }),\n fetchNpmIntegrity: _fetchNpmIntegrity,\n fetchNpmProvenance: (id, ver, sri) => _fetchNpmProvenance(id, ver, { integritySri: sri }),\n output: (text) => outputLines.push(text),\n }\n );\n },\n // Issue #22: never auto-confirm on the MCP (no-human-in-loop) surface.\n // The previous `async () => true` blanket-approved every confirmation,\n // including strict-mode *removals* of servers not in mcpm.yaml — a\n // prompt-injected agent could silently mutate client configs. Refusing\n // confirmation here means destructive prompts are declined; the trust\n // policy still gates installs via checkTrustPolicy in handleUp.\n confirm: async () => false,\n promptEnvVar: async () => \"\",\n output: (text) => outputLines.push(text),\n fetchNpmIntegrity: _fetchNpmIntegrity,\n // F8/B3: wire the provenance re-check on the MCP surface too, or a\n // policy.frozen: true stack run through mcpm_up would silently skip it.\n fetchNpmProvenance: (id, v, o) => _fetchNpmProvenance(id, v, o),\n readPins: _readPins,\n recordResult: (r) => records.push(r),\n }\n );\n } catch (err) {\n // Fix A: handleUp throws on early/whole-batch failures (no clients, lock-file\n // creation failure, missing required env in CI, the summary \"N could not be\n // installed\" throw, etc.). The previous bare catch swallowed these into a\n // clean-looking empty result. Capture the message so the caller can never\n // mistake a thrown failure for success.\n thrownError = err instanceof Error ? err.message : String(err);\n }\n\n const installed: string[] = [];\n const blocked: string[] = [];\n const failed: string[] = [];\n const skipped: string[] = [];\n\n if (records.length > 0) {\n // Authoritative path (fix D, F.3/F.5): categorize from handleUp's typed\n // per-server statuses. Unlike emoji-scraping, this reliably separates\n // \"blocked\" (policy/URL-lockdown) from \"failed\".\n for (const r of records) {\n switch (r.status) {\n case \"installed\": installed.push(r.name); break;\n case \"blocked\": blocked.push(r.name); break;\n case \"failed\": failed.push(r.name); break;\n case \"skipped\":\n case \"removed\": skipped.push(r.name); break;\n }\n }\n } else {\n // Fallback for the no-record path (e.g. a throw before any server is\n // processed): preserve the original output-line parsing.\n for (const line of outputLines) {\n if (line.includes(\"\\u2713\")) installed.push(line.trim());\n else if (line.includes(\"\\u2717\") && line.includes(\"blocked\")) blocked.push(line.trim());\n else if (line.includes(\"\\u2717\")) failed.push(line.trim());\n else if (line.includes(\"\\u2022\")) skipped.push(line.trim());\n }\n }\n\n // Fix A, refined for M1: a thrown handleUp failure MUST be signaled \\u2014 but only\n // via the top-level `error` field (set in the return below). The previous\n // version pushed the error *message* into `failed`, which is contracted to hold\n // server NAMES; a consumer iterating it as names got a stray sentence. `error`\n // is the authoritative batch-failure signal; `failed` stays names-only (genuine\n // per-server failures are already recorded into it above via `records`).\n\n return {\n installed,\n blocked,\n failed,\n skipped,\n ...(thrownError !== undefined ? { error: thrownError } : {}),\n ...(installed.length > 0\n ? { note: \"Restart your AI client to use the newly installed servers.\" }\n : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Keyword extraction\n// ---------------------------------------------------------------------------\n\nconst STOPWORDS = /\\b(i need|set up|access|work with|connect to|a server that|a server for|to|the|a|an|my|for|and|with)\\b/gi;\n\nexport function extractKeywords(description: string): string[] {\n const cleaned = description\n .toLowerCase()\n .replace(STOPWORDS, \" \")\n .replace(/[,&]/g, \" \");\n\n const tokens = cleaned\n .split(/\\s+/)\n .map((s) => s.trim())\n .filter((s) => s.length > 2);\n\n // If splitting produced too many tokens, use the full cleaned string\n if (tokens.length > 5) {\n return [cleaned.replace(/\\s+/g, \" \").trim()];\n }\n\n return tokens.length > 0 ? tokens : [description.trim()];\n}\n\n"],"mappings":"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","names":["clientId","stackFile","path"]}
#!/usr/bin/env node
import {
handleUp,
registerUpCommand
} from "./chunk-KGCQLI22.js";
import "./chunk-C6CAHFQX.js";
import "./chunk-LNGTDYGN.js";
import "./chunk-DDCTUMSZ.js";
import "./chunk-E3T224S3.js";
import "./chunk-RTI2GLYX.js";
import "./chunk-OIFKZA4V.js";
import "./chunk-FEXJHHDM.js";
import "./chunk-F6CHEUGO.js";
import "./chunk-LSNEZAFR.js";
import "./chunk-UNGY7RTE.js";
import "./chunk-W4IAFBUN.js";
import "./chunk-2PWW3Q5Q.js";
import "./chunk-MLVDFLDQ.js";
import "./chunk-7RJXJERN.js";
import "./chunk-V4AA4ZL5.js";
import "./chunk-32VRWVOF.js";
import "./chunk-K4U7EXLG.js";
import "./chunk-NPJ3SGGS.js";
import "./chunk-6R7TL5O2.js";
import "./chunk-R4R2VPDA.js";
import "./chunk-4QDJ3I7X.js";
import "./chunk-3X76P3FG.js";
import "./chunk-U7N6FRYF.js";
import "./chunk-WT6V33F2.js";
export {
handleUp,
registerUpCommand
};
//# sourceMappingURL=up-IF5B5AKH.js.map
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
+2
-2
{
"name": "@getmcpm/cli",
"version": "0.29.0",
"version": "0.29.1",
"mcpName": "io.github.getmcpm/cli",

@@ -35,3 +35,3 @@ "description": "MCP package manager — search, install, and audit MCP servers across Claude Desktop, Cursor, VS Code, and Windsurf",

"engines": {
"node": ">=22.9.0"
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
},

@@ -38,0 +38,0 @@ "dependencies": {

#!/usr/bin/env node
import {
DEFAULT_MIN_RELEASE_AGE_HOURS,
assessReleaseAge,
stdoutOutput
} from "./chunk-E3T224S3.js";
import {
checkScannerAvailable,
scanTier2
} from "./chunk-F6CHEUGO.js";
import {
computeTrustScore
} from "./chunk-LSNEZAFR.js";
import {
getAdapter
} from "./chunk-W4IAFBUN.js";
import {
confirm
} from "./chunk-2PWW3Q5Q.js";
import {
applyKeychainSecrets,
setSecrets
} from "./chunk-GZ3WCRLG.js";
import {
detectInstalledClients
} from "./chunk-6R7TL5O2.js";
import {
CLIENT_IDS,
getConfigPath
} from "./chunk-R4R2VPDA.js";
import {
addInstalledServer
} from "./chunk-4QDJ3I7X.js";
import {
DANGEROUS_FLAG_PREFIXES,
argvTokens,
assessServerStatus,
extractRegistryMeta,
levelColor,
scanTier1,
scoreBar
} from "./chunk-U7N6FRYF.js";
// src/commands/install.ts
import { InvalidArgumentError } from "commander";
import chalk from "chalk";
import { input, password } from "@inquirer/prompts";
function validateRemoteUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`Invalid remote URL: "${url}"`);
}
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
throw new Error(
`Remote URL must use http or https protocol, got: "${parsed.protocol}"`
);
}
if (parsed.protocol === "http:" && !isLoopbackHost(parsed.hostname)) {
throw new Error(
`Remote URL must use https for non-loopback hosts (plaintext http is vulnerable to interception), got: "${url}"`
);
}
}
function isLoopbackHost(hostname) {
const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
return h === "localhost" || h.endsWith(".localhost") || h === "127.0.0.1" || h === "::1";
}
var NPM_IDENTIFIER_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
var PYPI_IDENTIFIER_RE = /^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$/;
var OCI_IDENTIFIER_RE = /^[a-z0-9]+([._-][a-z0-9]+)*(\/[a-z0-9]+([._-][a-z0-9]+)*)*:[a-zA-Z0-9._-]+$/;
function validateIdentifier(identifier, registryType) {
const patterns = {
npm: NPM_IDENTIFIER_RE,
pypi: PYPI_IDENTIFIER_RE,
oci: OCI_IDENTIFIER_RE
};
const re = patterns[registryType];
if (re && !re.test(identifier)) {
throw new Error(
`Rejected potentially malicious ${registryType} identifier: "${identifier}"`
);
}
}
function normalizeRuntimeArgs(args) {
return args.flatMap(argvTokens);
}
var SAFE_ARG_PATTERNS = [
// Generic boolean flags (--allow-write, --read-only, --no-sandbox, etc.)
/^--[a-zA-Z][\w-]*$/,
// Single-dash short flags the live registry legitimately declares (-i, -y, -p).
// EXACTLY one alpha char — no bundled tail. Allowing a tail (-rmodule, -eCODE)
// would let a dangerous flag bundle its payload and slip past the Layer-1
// DANGEROUS_FLAG_PREFIXES check, which only rejects the exact token (-e/-r) or
// its '=' form. The live registry's short flags are all single-letter, so the
// narrow form loses no real coverage while closing the bundling bypass.
/^-[a-zA-Z]$/,
// Generic --key=value flags with safe value characters
// Blocks shell metacharacters: ; | $ ` & ( ) { } < > ! ' "
/^--[a-zA-Z][\w-]+=[\w./@:, -]+$/,
// Bare absolute paths (Unix: /path/to/dir)
/^\/[\w.@/ -]+$/,
// Home-relative paths (~/Documents)
/^~[\w.@/ -]*$/,
// Bare positional arguments (no dashes, no path traversal)
/^[a-zA-Z0-9][\w.@/-]*$/
];
function validateRuntimeArgs(args) {
for (const arg of args) {
if (/(?:^|[=\\/])\.\.(?:[\\/]|$)/.test(arg)) {
throw new Error(`Rejected path traversal in runtime argument: "${arg}"`);
}
const isDangerous = DANGEROUS_FLAG_PREFIXES.some(
(prefix) => arg === prefix || arg.startsWith(`${prefix}=`)
);
if (isDangerous) {
throw new Error(`Rejected dangerous runtime argument: "${arg}"`);
}
const isSafe = SAFE_ARG_PATTERNS.some((pattern) => pattern.test(arg));
if (!isSafe) {
throw new Error(`Rejected unrecognized runtime argument: "${arg}"`);
}
}
}
function resolveInstallEntry(serverEntry, clientId) {
const { server } = serverEntry;
if (clientId === "cursor" && server.remotes && server.remotes.length > 0) {
const httpRemote = server.remotes.find(
(r) => r.type === "streamable-http" || r.type === "sse"
);
if (httpRemote) {
validateRemoteUrl(httpRemote.url);
const headers = {};
for (const h of httpRemote.headers) {
headers[h.name] = "";
}
return {
url: httpRemote.url,
...Object.keys(headers).length > 0 ? { headers } : {}
};
}
}
const npmPkg = server.packages.find((p) => p.registryType === "npm");
const pypiPkg = server.packages.find((p) => p.registryType === "pypi");
const ociPkg = server.packages.find((p) => p.registryType === "oci");
if (npmPkg) {
validateIdentifier(npmPkg.identifier, "npm");
const rtArgs = normalizeRuntimeArgs(npmPkg.runtimeArguments ?? []);
validateRuntimeArgs(rtArgs);
return {
command: "npx",
args: ["-y", npmPkg.identifier, ...rtArgs]
};
}
if (pypiPkg) {
validateIdentifier(pypiPkg.identifier, "pypi");
const rtArgs = normalizeRuntimeArgs(pypiPkg.runtimeArguments ?? []);
validateRuntimeArgs(rtArgs);
return {
command: "uvx",
args: [pypiPkg.identifier, ...rtArgs]
};
}
if (ociPkg) {
validateIdentifier(ociPkg.identifier, "oci");
const rtArgs = normalizeRuntimeArgs(ociPkg.runtimeArguments ?? []);
validateRuntimeArgs(rtArgs);
return {
command: "docker",
args: ["run", "--rm", "-i", ociPkg.identifier, ...rtArgs]
};
}
if (clientId === "cursor" && server.remotes && server.remotes.length > 0) {
const remote = server.remotes[0];
validateRemoteUrl(remote.url);
return { url: remote.url };
}
throw new Error(
`No install path found for server "${server.name}": no packages and no compatible remotes.`
);
}
function formatTrustScore(trustScore) {
const { score, maxPossible, level, breakdown } = trustScore;
const levelLabel = levelColor(level.toUpperCase());
const bar = scoreBar(score, maxPossible);
const lines = [
`${bar} ${score}/${maxPossible} ${levelLabel}`,
` \u251C\u2500 Health check: ${breakdown.healthCheck > 0 ? "not yet run" : "failed or skipped"}`,
` \u251C\u2500 Tool descriptions: ${breakdown.staticScan === 40 ? "CLEAN (no injection patterns)" : `score ${breakdown.staticScan}/40`}`,
` \u251C\u2500 Package: publisher verification ${breakdown.registryMeta > 0 ? "passed" : "unverified"}`,
` \u2514\u2500 External scan: ${breakdown.externalScan > 0 ? `passed (${breakdown.externalScan}/20)` : "not available (set MCPM_EXTERNAL_SCANNER for deeper analysis)"}`
];
return lines.join("\n");
}
async function handleInstall(name, options, deps) {
const {
registryClient,
detectClients,
getAdapter: getAdapter2,
getConfigPath: getConfigPath2,
scanTier1: scanTier12,
checkScannerAvailable: checkScannerAvailable2,
scanTier2: scanTier22,
computeTrustScore: computeTrustScore2,
addToStore,
confirm: confirm2,
promptEnvVars,
output
} = deps;
const serverEntry = await registryClient.getServer(name);
const statusGate = assessServerStatus(serverEntry);
if (statusGate.blocks) {
if (options.json === true) {
output(
JSON.stringify(
{
name,
error: "server_delisted",
status: statusGate.status,
message: statusGate.statusMessage ?? null
},
null,
2
)
);
}
throw new Error(
`"${name}" has been deleted from the MCP registry${statusGate.statusMessage ? ` (${statusGate.statusMessage})` : ""}. Installation aborted.`
);
}
const tier1Findings = scanTier12(serverEntry);
const scannerAvailable = await checkScannerAvailable2();
let allFindings = [...tier1Findings];
if (scannerAvailable) {
const tier2Findings = await scanTier22(name);
allFindings = [...allFindings, ...tier2Findings];
}
const registryMeta = extractRegistryMeta(serverEntry);
const releaseAge = assessReleaseAge({
publishedAt: registryMeta.publishedAt,
now: (deps.now ?? Date.now)(),
minAgeHours: options.minReleaseAge ?? DEFAULT_MIN_RELEASE_AGE_HOURS
});
if (releaseAge.finding) {
allFindings = [...allFindings, releaseAge.finding];
}
const trustScoreInput = {
findings: allFindings,
healthCheckPassed: null,
// health check not yet run at this point
hasExternalScanner: scannerAvailable,
registryMeta
};
const trustScore = computeTrustScore2(trustScoreInput);
if (options.minTrust !== void 0 && trustScore.score < options.minTrust) {
if (options.json === true) {
output(
JSON.stringify(
{
name,
error: "min_trust_not_met",
score: trustScore.score,
maxPossible: trustScore.maxPossible,
required: options.minTrust,
level: trustScore.level
},
null,
2
)
);
}
throw new Error(
`Trust score ${trustScore.score}/${trustScore.maxPossible} is below the required minimum of ${options.minTrust}. Installation aborted.`
);
}
if (options.minReleaseAge !== void 0 && options.allowFresh !== true && releaseAge.blocksArmedGate) {
if (options.json === true) {
output(
JSON.stringify(
{
name,
error: "release_age_not_met",
ageHours: releaseAge.ageHours,
required: options.minReleaseAge,
reason: releaseAge.status
},
null,
2
)
);
}
const tail = "Installation aborted. Use --allow-fresh to bypass.";
throw new Error(
releaseAge.status === "future" ? `Release publish timestamp is in the future (clock skew or forged metadata); treated as within the ${options.minReleaseAge}-hour minimum release age. ${tail}` : releaseAge.status === "unparseable" ? `Release publish timestamp could not be parsed; treated as within the ${options.minReleaseAge}-hour minimum release age. ${tail}` : releaseAge.status === "absent" ? `Release publish timestamp is missing from the registry metadata, so release age cannot be verified against the ${options.minReleaseAge}-hour minimum. ${tail}` : `Release age ${releaseAge.ageHours}h is below the required minimum of ${options.minReleaseAge}h. ${tail}`
);
}
const jsonMode = options.json === true;
if (!jsonMode) {
output(formatTrustScore(trustScore));
output("");
}
if (options.yes !== true) {
let shouldProceed;
if (trustScore.level === "risky") {
if (!jsonMode) {
output("\x1B[31mWARNING: This server has a low trust score and may be risky to install.\x1B[0m");
output("\x1B[31mSecurity findings indicate potential dangers. Proceed with extreme caution.\x1B[0m");
}
shouldProceed = await confirm2(
"I understand the risks and want to install this server anyway. Continue?"
);
} else if (trustScore.level === "caution") {
if (!jsonMode) {
output("\x1B[33mCAUTION: This server has a moderate trust score. Review the details above.\x1B[0m");
}
shouldProceed = await confirm2(`Install '${name}'? (caution recommended)`);
} else {
shouldProceed = await confirm2(`Install '${name}'?`);
}
if (!shouldProceed) {
if (!jsonMode) output("Installation cancelled.");
return;
}
}
let targetClients = await detectClients();
if (targetClients.length === 0) {
throw new Error(
"No supported AI clients found. Install Claude Desktop, Cursor, VS Code, or Windsurf first."
);
}
if (options.client !== void 0) {
if (!CLIENT_IDS.includes(options.client)) {
throw new Error(
`Unknown client "${options.client}". Valid values: ${CLIENT_IDS.join(", ")}.`
);
}
const requestedId = options.client;
if (!targetClients.includes(requestedId)) {
throw new Error(
`Client "${requestedId}" is not installed on this machine.`
);
}
targetClients = [requestedId];
}
if (options.force !== true) {
for (const clientId of targetClients) {
const adapter = getAdapter2(clientId);
const configPath = getConfigPath2(clientId);
const existing = await adapter.read(configPath);
if (Object.prototype.hasOwnProperty.call(existing, name)) {
throw new Error(
`Server '${name}' is already installed in ${clientId}. Use --force to overwrite.`
);
}
}
}
const { server } = serverEntry;
const bestPkg = server.packages.find((p) => p.registryType === "npm") ?? server.packages.find((p) => p.registryType === "pypi") ?? server.packages.find((p) => p.registryType === "oci") ?? server.packages[0];
const envVarDefs = bestPkg?.environmentVariables ?? [];
const resolvedEnvVars = await promptEnvVars(envVarDefs);
const secretsMode = options.secrets ?? "plaintext";
const { env: envForConfig, storedCount: storedSecretCount } = await applyKeychainSecrets({
serverName: name,
resolvedEnv: resolvedEnvVars,
isSecret: (key) => envVarDefs.find((d) => d.name === key)?.isSecret === true,
mode: secretsMode,
setSecrets: deps.setSecrets
});
const resolvedEntries = /* @__PURE__ */ new Map();
for (const clientId of targetClients) {
resolvedEntries.set(clientId, resolveInstallEntry(serverEntry, clientId));
}
const isUnguardedEntry = [...resolvedEntries.values()].some(
(e) => e.url !== void 0 && e.command === void 0
);
if (isUnguardedEntry) {
if (options.allowUrlServers === false) {
throw new Error(
`Server '${name}' uses a URL/HTTP transport and is not permitted via the MCP surface.`
);
}
const previousConsented = deps.readUnguardedConsent ? await deps.readUnguardedConsent() : [];
const alreadyConsented = previousConsented.includes(name);
const consented = options.allowUnguarded === true || alreadyConsented;
if (!consented) {
throw new Error(
`Server '${name}' uses a URL/HTTP transport and runs UNGUARDED \u2014 no runtime inspection is possible (mcpm's guard relay only wraps stdio servers). Re-run with --allow-unguarded to install it WITHOUT protection.`
);
}
if (!alreadyConsented) {
if (!jsonMode) {
output(
"\x1B[33m\u26A0 UNGUARDED: this URL/HTTP-transport server runs WITHOUT runtime inspection (the guard relay only wraps stdio servers). This grants consent \u2014 it does NOT add protection. The only true fix is a streamable-HTTP relay (not yet implemented).\x1B[0m"
);
}
if (deps.recordUnguardedConsent) {
await deps.recordUnguardedConsent([name]).catch(() => void 0);
}
}
}
const installedClients = [];
for (const clientId of targetClients) {
const adapter = getAdapter2(clientId);
const configPath = getConfigPath2(clientId);
const rawEntry = resolvedEntries.get(clientId);
const entry = {
...rawEntry,
...Object.keys(envForConfig).length > 0 ? { env: { ...rawEntry.env ?? {}, ...envForConfig } } : {}
};
await adapter.addServer(configPath, name, entry, { force: options.force });
installedClients.push(clientId);
}
if (!options.json) {
if (secretsMode === "keychain" && storedSecretCount > 0) {
output(
`\x1B[32mStored ${storedSecretCount} secret(s) encrypted at rest in ~/.mcpm. With an OS keychain this protects against other-user/offline access (not same-user processes); without one a machine-derived key is used that guards casual local inspection only, NOT file exfiltration \u2014 run \`mcpm secrets migrate\` once a keychain is available. Run \`mcpm guard enable\` (then restart your IDE) so they resolve at launch \u2014 until guard wraps this server it receives the literal placeholder.\x1B[0m`
);
} else {
const hasSecrets = envVarDefs.some((ev) => ev.isSecret && resolvedEnvVars[ev.name]);
if (hasSecrets) {
output(
"\x1B[33mNote: API keys are stored as plaintext in client config files. Ensure config files have appropriate permissions (chmod 600).\x1B[0m"
);
}
}
}
const storeEntry = {
name,
version: serverEntry.server.version,
clients: [...installedClients],
installedAt: (/* @__PURE__ */ new Date()).toISOString()
};
await addToStore(storeEntry);
if (options.json === true) {
const result = {
name,
version: serverEntry.server.version,
clients: installedClients,
trustScore: {
score: trustScore.score,
maxPossible: trustScore.maxPossible,
level: trustScore.level
}
};
output(JSON.stringify(result, null, 2));
return;
}
const clientList = installedClients.join(", ");
output(`\x1B[32mInstalled '${name}' successfully into: ${clientList}\x1B[0m`);
}
async function promptEnvVarsDefault(vars) {
if (vars.length === 0) return {};
const result = {};
for (const envVar of vars) {
if (!envVar.isRequired && !envVar.isSecret) continue;
const defaultVal = envVar.default ?? "";
const promptMessage = envVar.description ? `${envVar.name} (${envVar.description}):` : `${envVar.name}:`;
let prompted;
if (envVar.isSecret) {
prompted = await password({ message: promptMessage });
if (!prompted && defaultVal) {
prompted = defaultVal;
}
} else {
prompted = await input({ message: promptMessage, default: defaultVal });
}
if (prompted) {
result[envVar.name] = prompted;
}
}
return result;
}
function parseSecretsMode(raw) {
if (raw !== "keychain" && raw !== "plaintext") {
throw new InvalidArgumentError(
`--secrets must be "keychain" or "plaintext", got: "${raw}"`
);
}
return raw;
}
function parseMinTrust(raw) {
if (!/^\d+$/.test(raw)) {
throw new InvalidArgumentError(
`--min-trust must be an integer between 0 and 100, got: "${raw}"`
);
}
const n = Number(raw);
if (n < 0 || n > 100) {
throw new InvalidArgumentError(
`--min-trust must be an integer between 0 and 100, got: "${raw}"`
);
}
return n;
}
function parseMinReleaseAge(raw) {
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(Number(raw))) {
throw new InvalidArgumentError(
`--min-release-age must be a non-negative integer number of hours, got: "${raw}"`
);
}
return Number(raw);
}
function registerInstallCommand(program) {
program.command("install <name>").description("Install an MCP server from the registry").option("-c, --client <id>", "install to a specific client only").option("-y, --yes", "skip all confirmation prompts").option("-f, --force", "overwrite if server already installed").option("--skip-health-check", "skip post-install health check").option("--json", "output result as JSON").option("--min-trust <n>", "abort install if pre-install trust score is below this threshold (0-100; health check runs after install)", parseMinTrust).option("--min-release-age <hours>", "abort install if the release is younger than this many hours OR its publish timestamp is missing/unparseable (fail-closed when set; also sets the scoring cooldown threshold; bypass with --allow-fresh)", parseMinReleaseAge).option("--allow-fresh", "bypass the --min-release-age gate (including the missing-timestamp block)").option("--secrets <mode>", "where to store secret env vars: 'keychain' (encrypted in ~/.mcpm, resolved by mcpm guard at launch) or 'plaintext' (default)", parseSecretsMode).option("--allow-unguarded", "permit a URL/HTTP-transport server to run WITHOUT runtime guard inspection (no relay wraps a non-stdio transport); records consent so future installs stay quiet").action(async (name, opts) => {
const { RegistryClient } = await import("./client-3RPMRFZL.js");
const client = new RegistryClient();
const { readUnguardedConsent, writeUnguardedConsent, mergeUnguarded } = await import("./unguarded-GJO5WRM7.js");
const installOptions = {
client: opts.client,
yes: opts.yes,
force: opts.force,
skipHealthCheck: opts.skipHealthCheck,
json: opts.json,
minTrust: opts.minTrust,
minReleaseAge: opts.minReleaseAge,
allowFresh: opts.allowFresh,
secrets: opts.secrets,
allowUnguarded: opts.allowUnguarded
};
const installDeps = {
registryClient: client,
detectClients: detectInstalledClients,
getAdapter,
getConfigPath,
scanTier1,
checkScannerAvailable,
scanTier2: (serverName) => scanTier2(serverName),
computeTrustScore,
addToStore: addInstalledServer,
confirm,
promptEnvVars: promptEnvVarsDefault,
output: stdoutOutput,
setSecrets,
now: () => Date.now(),
readUnguardedConsent,
recordUnguardedConsent: async (names) => {
const previous = await readUnguardedConsent();
await writeUnguardedConsent(mergeUnguarded(previous, names));
}
};
try {
await handleInstall(name, installOptions, installDeps);
} catch (err) {
if (installOptions.json !== true) {
console.error(chalk.red(err.message));
}
process.exit(1);
}
});
}
export {
validateRemoteUrl,
resolveInstallEntry,
parseSecretsMode,
parseMinTrust,
registerInstallCommand
};
//# sourceMappingURL=chunk-7QRDQF55.js.map
{"version":3,"sources":["../src/commands/install.ts"],"sourcesContent":["/**\n * `mcpm install <name>` command handler.\n *\n * Wires together: registry fetch → trust assessment → user confirmation →\n * client detection → env var prompting → config write → store record.\n *\n * All external dependencies are injected for testability.\n *\n * Exports:\n * - handleInstall() — injectable handler for testing\n * - resolveInstallEntry() — pure function: ServerEntry + ClientId → McpServerEntry\n * - formatTrustScore() — pure function: TrustScore → formatted string\n * - registerInstallCommand() — Commander registration\n */\n\nimport { CLIENT_IDS } from \"../config/paths.js\";\nimport type { ClientId } from \"../config/paths.js\";\nimport type { ConfigAdapter, McpServerEntry } from \"../config/adapters/index.js\";\nimport type { ServerEntry, EnvVar } from \"../registry/types.js\";\nimport { argvTokens, type RuntimeArgument } from \"../registry/argument-tokens.js\";\nimport type { Finding } from \"../scanner/tier1.js\";\nimport type { TrustScore, TrustScoreInput } from \"../scanner/trust-score.js\";\nimport type { InstalledServer } from \"../store/servers.js\";\nimport { scoreBar, levelColor, extractRegistryMeta } from \"../utils/format-trust.js\";\nimport { assessReleaseAge, DEFAULT_MIN_RELEASE_AGE_HOURS } from \"../scanner/cooldown.js\";\nimport { assessServerStatus } from \"../scanner/registry-status.js\";\nimport { DANGEROUS_FLAG_PREFIXES } from \"../scanner/patterns.js\";\nimport { applyKeychainSecrets, type SecretsMode, setSecrets as _setSecrets } from \"../store/keychain.js\";\n\n// ---------------------------------------------------------------------------\n// URL validation — guard against malicious remote URLs\n// ---------------------------------------------------------------------------\n\n/**\n * Validate a remote URL before it is written to any IDE config file.\n * Only http: and https: protocols are permitted.\n */\nexport function validateRemoteUrl(url: string): void {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new Error(`Invalid remote URL: \"${url}\"`);\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") {\n throw new Error(\n `Remote URL must use http or https protocol, got: \"${parsed.protocol}\"`\n );\n }\n // M4a: plaintext http to a non-loopback host is interceptable once written to an\n // IDE config. Allow http only for loopback (local dev servers); require https for\n // every other host. https is always allowed.\n if (parsed.protocol === \"http:\" && !isLoopbackHost(parsed.hostname)) {\n throw new Error(\n `Remote URL must use https for non-loopback hosts (plaintext http is ` +\n `vulnerable to interception), got: \"${url}\"`\n );\n }\n}\n\n/**\n * True for localhost / loopback literals, where plaintext http is acceptable.\n * Recognizes localhost / *.localhost / 127.0.0.1 / ::1. Exotic loopback spellings\n * (IPv4-mapped `::ffff:127.0.0.1`, `127.x.x.x`, decimal/octal/hex IPs) are NOT\n * recognized and fall through to the https requirement — over-rejection only, never\n * a bypass (a non-loopback host can never be mistaken for loopback).\n */\nfunction isLoopbackHost(hostname: string): boolean {\n const h = hostname.toLowerCase().replace(/^\\[|\\]$/g, \"\"); // strip IPv6 brackets\n return (\n h === \"localhost\" ||\n h.endsWith(\".localhost\") ||\n h === \"127.0.0.1\" ||\n h === \"::1\"\n );\n}\n\nconst NPM_IDENTIFIER_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\\/)?[a-z0-9-~][a-z0-9-._~]*$/;\nconst PYPI_IDENTIFIER_RE = /^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$/;\nconst OCI_IDENTIFIER_RE =\n /^[a-z0-9]+([._-][a-z0-9]+)*(\\/[a-z0-9]+([._-][a-z0-9]+)*)*:[a-zA-Z0-9._-]+$/;\n\n/**\n * Validate a package identifier against the expected pattern for its registry\n * type. Throws if the identifier looks potentially malicious.\n */\nexport function validateIdentifier(identifier: string, registryType: string): void {\n const patterns: Record<string, RegExp> = {\n npm: NPM_IDENTIFIER_RE,\n pypi: PYPI_IDENTIFIER_RE,\n oci: OCI_IDENTIFIER_RE,\n };\n const re = patterns[registryType];\n if (re && !re.test(identifier)) {\n throw new Error(\n `Rejected potentially malicious ${registryType} identifier: \"${identifier}\"`\n );\n }\n}\n\n/**\n * Render runtimeArguments from the registry into a launch argv slice.\n *\n * Delegates to argvTokens (name + value, never valueHint) so the SAME function\n * defines both what gets executed here and what the F4 dangerous-flag scan\n * matches in scanner/patterns.ts — they cannot diverge. valueHint (a\n * documentation placeholder like \"directory\") is deliberately not rendered:\n * emitting it would inject a bogus literal argument. The injection scanner\n * (scanner/tier1.ts) uses argumentTokens instead, which DOES read valueHint as\n * user-facing text; that divergence is intentional and documented there.\n */\nfunction normalizeRuntimeArgs(\n args: ReadonlyArray<RuntimeArgument>\n): string[] {\n return args.flatMap(argvTokens);\n}\n\n/**\n * Allowlist of safe runtime argument shapes.\n * After dangerous flags are rejected, arguments must match one of these\n * patterns. This blocks shell metacharacters and path traversal while\n * allowing the wide range of flags real MCP servers use.\n */\nconst SAFE_ARG_PATTERNS: readonly RegExp[] = [\n // Generic boolean flags (--allow-write, --read-only, --no-sandbox, etc.)\n /^--[a-zA-Z][\\w-]*$/,\n // Single-dash short flags the live registry legitimately declares (-i, -y, -p).\n // EXACTLY one alpha char — no bundled tail. Allowing a tail (-rmodule, -eCODE)\n // would let a dangerous flag bundle its payload and slip past the Layer-1\n // DANGEROUS_FLAG_PREFIXES check, which only rejects the exact token (-e/-r) or\n // its '=' form. The live registry's short flags are all single-letter, so the\n // narrow form loses no real coverage while closing the bundling bypass.\n /^-[a-zA-Z]$/,\n // Generic --key=value flags with safe value characters\n // Blocks shell metacharacters: ; | $ ` & ( ) { } < > ! ' \"\n /^--[a-zA-Z][\\w-]+=[\\w./@:, -]+$/,\n // Bare absolute paths (Unix: /path/to/dir)\n /^\\/[\\w.@/ -]+$/,\n // Home-relative paths (~/Documents)\n /^~[\\w.@/ -]*$/,\n // Bare positional arguments (no dashes, no path traversal)\n /^[a-zA-Z0-9][\\w.@/-]*$/,\n];\n\n/**\n * Validate runtime arguments from the registry.\n * Two-layer defense: reject known-dangerous Node.js flags first,\n * then require remaining args to match safe structural patterns.\n */\nexport function validateRuntimeArgs(args: string[]): void {\n for (const arg of args) {\n // Layer 0 (M4b): reject a \"..\" path-traversal segment anywhere in the argument\n // — \"../x\", \"a/../../etc/passwd\", \"--config=../secret\". A \"..\" segment is one\n // bounded by start-of-arg, \"=\" (flag value), or a path separator on the left,\n // and a separator or end-of-arg on the right. The Layer-2 allowlist permits \".\"\n // and \"/\" inside values, so without this a traversal would slip through; a\n // non-traversal double dot like \"--range=1..10\" is left untouched.\n if (/(?:^|[=\\\\/])\\.\\.(?:[\\\\/]|$)/.test(arg)) {\n throw new Error(`Rejected path traversal in runtime argument: \"${arg}\"`);\n }\n\n // Layer 1: reject dangerous Node.js flags\n const isDangerous = DANGEROUS_FLAG_PREFIXES.some(\n (prefix) => arg === prefix || arg.startsWith(`${prefix}=`)\n );\n if (isDangerous) {\n throw new Error(`Rejected dangerous runtime argument: \"${arg}\"`);\n }\n\n // Layer 2: require safe structural pattern\n const isSafe = SAFE_ARG_PATTERNS.some((pattern) => pattern.test(arg));\n if (!isSafe) {\n throw new Error(`Rejected unrecognized runtime argument: \"${arg}\"`);\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport interface InstallOptions {\n client?: string;\n yes?: boolean;\n force?: boolean;\n skipHealthCheck?: boolean;\n json?: boolean;\n minTrust?: number;\n minReleaseAge?: number;\n allowFresh?: boolean;\n secrets?: SecretsMode;\n /**\n * H9 (fail-closed): per-invocation consent (`--allow-unguarded`) to install a\n * URL/HTTP-transport server that runs UNGUARDED (the guard relay only wraps a\n * stdio transport — a non-stdio remote gets ZERO runtime inspection). When\n * neither this nor a name already in the persistent consent store grants it,\n * such a server is DENIED. DISTINCT from `allowUrlServers`, the MCP-surface\n * kill-switch: `allowUrlServers === false` ALWAYS wins.\n */\n allowUnguarded?: boolean;\n /**\n * Whether URL/HTTP-transport servers may be installed at all. DEFAULT\n * (undefined/true) preserves CLI behavior. The MCP surface passes `false` so a\n * url-transport server is recorded as blocked instead of written to a config —\n * an untrusted caller can never reach the unguarded run path.\n */\n allowUrlServers?: boolean;\n}\n\nexport interface InstallDeps {\n registryClient: { getServer: (name: string) => Promise<ServerEntry> };\n detectClients: () => Promise<ClientId[]>;\n getAdapter: (clientId: ClientId) => ConfigAdapter;\n getConfigPath: (clientId: ClientId) => string;\n scanTier1: (server: ServerEntry) => Finding[];\n checkScannerAvailable: () => Promise<boolean>;\n scanTier2: (name: string) => Promise<Finding[]>;\n computeTrustScore: (input: TrustScoreInput) => TrustScore;\n addToStore: (server: InstalledServer) => Promise<void>;\n confirm: (message: string) => Promise<boolean>;\n promptEnvVars: (vars: EnvVar[]) => Promise<Record<string, string>>;\n output: (text: string) => void;\n /** Optional; required only when options.secrets === \"keychain\". */\n setSecrets?: (server: string, values: Record<string, string>) => Promise<void>;\n /** Epoch-ms clock for release-age assessment; defaults to Date.now at the CLI boundary. */\n now?: () => number;\n /**\n * H9: read the persistent set of server names previously consented to run\n * unguarded. Injectable for tests; defaults to the real store at the CLI\n * boundary. When omitted, no server is treated as previously-consented.\n */\n readUnguardedConsent?: () => Promise<string[]>;\n /**\n * H9: persist (union into the store) the name newly consented to run\n * unguarded. Injectable for tests; defaults to the real store. Called once\n * after a url server is installed under fresh consent.\n */\n recordUnguardedConsent?: (names: readonly string[]) => Promise<void>;\n}\n\n// ---------------------------------------------------------------------------\n// resolveInstallEntry — pure function, no I/O\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve the McpServerEntry for a given server + clientId.\n *\n * Decision tree:\n * 1. Cursor + server has HTTP remote → produce { url, headers } entry\n * 2. Otherwise pick from packages[]: npm → pypi → oci (first available)\n * 3. npm: { command: 'npx', args: ['-y', identifier, ...runtimeArgs], env }\n * 4. pypi: { command: 'uvx', args: [identifier, ...runtimeArgs], env }\n * 5. docker: { command: 'docker', args: ['run', '--rm', '-i', image], env }\n * 6. If no packages and no usable remote: throw\n */\nexport function resolveInstallEntry(\n serverEntry: ServerEntry,\n clientId: ClientId\n): McpServerEntry {\n const { server } = serverEntry;\n\n // Rule 1: Cursor + HTTP remote → streamable-http entry\n if (clientId === \"cursor\" && server.remotes && server.remotes.length > 0) {\n const httpRemote = server.remotes.find(\n (r) => r.type === \"streamable-http\" || r.type === \"sse\"\n );\n if (httpRemote) {\n validateRemoteUrl(httpRemote.url);\n // Build headers record if any\n const headers: Record<string, string> = {};\n for (const h of httpRemote.headers) {\n headers[h.name] = \"\";\n }\n return {\n url: httpRemote.url,\n ...(Object.keys(headers).length > 0 ? { headers } : {}),\n };\n }\n }\n\n // Rule 2: Pick best package by priority: npm → pypi → oci\n const npmPkg = server.packages.find((p) => p.registryType === \"npm\");\n const pypiPkg = server.packages.find((p) => p.registryType === \"pypi\");\n const ociPkg = server.packages.find((p) => p.registryType === \"oci\");\n\n if (npmPkg) {\n validateIdentifier(npmPkg.identifier, \"npm\");\n const rtArgs = normalizeRuntimeArgs(npmPkg.runtimeArguments ?? []);\n validateRuntimeArgs(rtArgs);\n return {\n command: \"npx\",\n args: [\"-y\", npmPkg.identifier, ...rtArgs],\n };\n }\n\n if (pypiPkg) {\n validateIdentifier(pypiPkg.identifier, \"pypi\");\n const rtArgs = normalizeRuntimeArgs(pypiPkg.runtimeArguments ?? []);\n validateRuntimeArgs(rtArgs);\n return {\n command: \"uvx\",\n args: [pypiPkg.identifier, ...rtArgs],\n };\n }\n\n if (ociPkg) {\n validateIdentifier(ociPkg.identifier, \"oci\");\n const rtArgs = normalizeRuntimeArgs(ociPkg.runtimeArguments ?? []);\n validateRuntimeArgs(rtArgs);\n return {\n command: \"docker\",\n args: [\"run\", \"--rm\", \"-i\", ociPkg.identifier, ...rtArgs],\n };\n }\n\n // Rule 3: Cursor-only path — HTTP remote with no packages\n if (clientId === \"cursor\" && server.remotes && server.remotes.length > 0) {\n const remote = server.remotes[0];\n validateRemoteUrl(remote.url);\n return { url: remote.url };\n }\n\n throw new Error(\n `No install path found for server \"${server.name}\": no packages and no compatible remotes.`\n );\n}\n\n// ---------------------------------------------------------------------------\n// formatTrustScore — pure function, rich display\n// ---------------------------------------------------------------------------\n\n/**\n * Format a trust score as a visual progress bar with breakdown details.\n */\nexport function formatTrustScore(trustScore: TrustScore): string {\n const { score, maxPossible, level, breakdown } = trustScore;\n\n const levelLabel = levelColor(level.toUpperCase());\n const bar = scoreBar(score, maxPossible);\n\n const lines: string[] = [\n `${bar} ${score}/${maxPossible} ${levelLabel}`,\n ` \\u251C\\u2500 Health check: ${breakdown.healthCheck > 0 ? \"not yet run\" : \"failed or skipped\"}`,\n ` \\u251C\\u2500 Tool descriptions: ${breakdown.staticScan === 40 ? \"CLEAN (no injection patterns)\" : `score ${breakdown.staticScan}/40`}`,\n ` \\u251C\\u2500 Package: publisher verification ${breakdown.registryMeta > 0 ? \"passed\" : \"unverified\"}`,\n ` \\u2514\\u2500 External scan: ${breakdown.externalScan > 0 ? `passed (${breakdown.externalScan}/20)` : \"not available (set MCPM_EXTERNAL_SCANNER for deeper analysis)\"}`,\n ];\n\n return lines.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// handleInstall — main handler\n// ---------------------------------------------------------------------------\n\n/**\n * Core handler for `mcpm install <name>`.\n * All dependencies are injected for hermetic testability.\n */\nexport async function handleInstall(\n name: string,\n options: InstallOptions,\n deps: InstallDeps\n): Promise<void> {\n const {\n registryClient,\n detectClients,\n getAdapter,\n getConfigPath,\n scanTier1,\n checkScannerAvailable,\n scanTier2,\n computeTrustScore,\n addToStore,\n confirm,\n promptEnvVars,\n output,\n } = deps;\n\n // -------------------------------------------------------------------------\n // Step 1: Fetch server metadata\n // -------------------------------------------------------------------------\n const serverEntry = await registryClient.getServer(name);\n\n // -------------------------------------------------------------------------\n // Step 1b: registry-delisting gate (fail closed, before any scan/output)\n // -------------------------------------------------------------------------\n // If the registry itself marks this server \"deleted\" (removed/withdrawn),\n // refuse to install. Fail-SAFE: ONLY an explicit \"deleted\" blocks; a\n // \"deprecated\" or absent/unknown status does not (surfaced as an advisory\n // finding by scanTier1 instead). See scanner/registry-status.ts.\n const statusGate = assessServerStatus(serverEntry);\n if (statusGate.blocks) {\n if (options.json === true) {\n output(\n JSON.stringify(\n {\n name,\n error: \"server_delisted\",\n status: statusGate.status,\n message: statusGate.statusMessage ?? null,\n },\n null,\n 2\n )\n );\n }\n throw new Error(\n `\"${name}\" has been deleted from the MCP registry${statusGate.statusMessage ? ` (${statusGate.statusMessage})` : \"\"}. Installation aborted.`\n );\n }\n\n // -------------------------------------------------------------------------\n // Step 2: Trust assessment\n // -------------------------------------------------------------------------\n const tier1Findings = scanTier1(serverEntry);\n const scannerAvailable = await checkScannerAvailable();\n\n let allFindings: Finding[] = [...tier1Findings];\n if (scannerAvailable) {\n const tier2Findings = await scanTier2(name);\n allFindings = [...allFindings, ...tier2Findings];\n }\n\n // Release-age cooldown: assessed ONCE so the score finding and the Step 2c\n // gate can never disagree — passing --min-release-age below 24 therefore also\n // lowers the scoring cooldown threshold (documented in the flag help text).\n // The medium finding lands unconditionally for fresh releases, with or\n // without the gate — that is the inversion fix, independent of the gate.\n const registryMeta = extractRegistryMeta(serverEntry);\n const releaseAge = assessReleaseAge({\n publishedAt: registryMeta.publishedAt,\n now: (deps.now ?? Date.now)(),\n minAgeHours: options.minReleaseAge ?? DEFAULT_MIN_RELEASE_AGE_HOURS,\n });\n if (releaseAge.finding) {\n allFindings = [...allFindings, releaseAge.finding];\n }\n\n const trustScoreInput: TrustScoreInput = {\n findings: allFindings,\n healthCheckPassed: null, // health check not yet run at this point\n hasExternalScanner: scannerAvailable,\n registryMeta,\n };\n\n const trustScore = computeTrustScore(trustScoreInput);\n\n // -------------------------------------------------------------------------\n // Step 2b: --min-trust gate (checked before any output or confirmation)\n // -------------------------------------------------------------------------\n if (options.minTrust !== undefined && trustScore.score < options.minTrust) {\n if (options.json === true) {\n output(\n JSON.stringify(\n {\n name,\n error: \"min_trust_not_met\",\n score: trustScore.score,\n maxPossible: trustScore.maxPossible,\n required: options.minTrust,\n level: trustScore.level,\n },\n null,\n 2\n )\n );\n }\n // The denominator is 80 unless the external-scanner bucket was credited, which is\n // the default — so a hardcoded /100 understated every score by 20 points of scale\n // and made a flawless 62/80 (77.5%) read as 62%.\n throw new Error(\n `Trust score ${trustScore.score}/${trustScore.maxPossible} is below the required minimum of ${options.minTrust}. Installation aborted.`\n );\n }\n\n // -------------------------------------------------------------------------\n // Step 2c: --min-release-age gate (checked before any output or confirmation)\n // -------------------------------------------------------------------------\n // Fail-closed when armed: a MISSING publish timestamp blocks too (blocksArmedGate)\n // — otherwise a registry/compromised mirror could defeat the gate by omitting\n // _meta (publishedAt is .optional() in OfficialMetaSchema). The score finding\n // stays fail-open for absent; only the explicitly armed gate hardens.\n if (\n options.minReleaseAge !== undefined &&\n options.allowFresh !== true &&\n releaseAge.blocksArmedGate\n ) {\n if (options.json === true) {\n output(\n JSON.stringify(\n {\n name,\n error: \"release_age_not_met\",\n ageHours: releaseAge.ageHours,\n required: options.minReleaseAge,\n reason: releaseAge.status,\n },\n null,\n 2\n )\n );\n }\n const tail = \"Installation aborted. Use --allow-fresh to bypass.\";\n throw new Error(\n releaseAge.status === \"future\"\n ? `Release publish timestamp is in the future (clock skew or forged metadata); treated as within the ${options.minReleaseAge}-hour minimum release age. ${tail}`\n : releaseAge.status === \"unparseable\"\n ? `Release publish timestamp could not be parsed; treated as within the ${options.minReleaseAge}-hour minimum release age. ${tail}`\n : releaseAge.status === \"absent\"\n ? `Release publish timestamp is missing from the registry metadata, so release age cannot be verified against the ${options.minReleaseAge}-hour minimum. ${tail}`\n : `Release age ${releaseAge.ageHours}h is below the required minimum of ${options.minReleaseAge}h. ${tail}`\n );\n }\n\n // -------------------------------------------------------------------------\n // Step 3: Display trust score and confirm\n // -------------------------------------------------------------------------\n // In --json mode suppress all human-readable output; only the final JSON\n // is written to stdout.\n const jsonMode = options.json === true;\n\n if (!jsonMode) {\n output(formatTrustScore(trustScore));\n output(\"\");\n }\n\n if (options.yes !== true) {\n let shouldProceed: boolean;\n\n if (trustScore.level === \"risky\") {\n if (!jsonMode) {\n output(\"\\u001b[31mWARNING: This server has a low trust score and may be risky to install.\\u001b[0m\");\n output(\"\\u001b[31mSecurity findings indicate potential dangers. Proceed with extreme caution.\\u001b[0m\");\n }\n shouldProceed = await confirm(\n \"I understand the risks and want to install this server anyway. Continue?\"\n );\n } else if (trustScore.level === \"caution\") {\n if (!jsonMode) {\n output(\"\\u001b[33mCAUTION: This server has a moderate trust score. Review the details above.\\u001b[0m\");\n }\n shouldProceed = await confirm(`Install '${name}'? (caution recommended)`);\n } else {\n // GREEN — brief display, proceed\n shouldProceed = await confirm(`Install '${name}'?`);\n }\n\n if (!shouldProceed) {\n if (!jsonMode) output(\"Installation cancelled.\");\n return;\n }\n }\n\n // -------------------------------------------------------------------------\n // Step 4: Detect and filter clients\n // -------------------------------------------------------------------------\n let targetClients = await detectClients();\n\n if (targetClients.length === 0) {\n throw new Error(\n \"No supported AI clients found. Install Claude Desktop, Cursor, VS Code, or Windsurf first.\"\n );\n }\n\n if (options.client !== undefined) {\n if (!CLIENT_IDS.includes(options.client as ClientId)) {\n throw new Error(\n `Unknown client \"${options.client}\". Valid values: ${CLIENT_IDS.join(\", \")}.`\n );\n }\n const requestedId = options.client as ClientId;\n if (!targetClients.includes(requestedId)) {\n throw new Error(\n `Client \"${requestedId}\" is not installed on this machine.`\n );\n }\n targetClients = [requestedId];\n }\n\n // -------------------------------------------------------------------------\n // Step 5: Check for already-installed (unless --force)\n // -------------------------------------------------------------------------\n if (options.force !== true) {\n for (const clientId of targetClients) {\n const adapter = getAdapter(clientId);\n const configPath = getConfigPath(clientId);\n const existing = await adapter.read(configPath);\n if (Object.prototype.hasOwnProperty.call(existing, name)) {\n throw new Error(\n `Server '${name}' is already installed in ${clientId}. Use --force to overwrite.`\n );\n }\n }\n }\n\n // -------------------------------------------------------------------------\n // Step 6: Resolve env vars to prompt for\n // -------------------------------------------------------------------------\n // Collect env vars from the best-match package\n const { server } = serverEntry;\n const bestPkg =\n server.packages.find((p) => p.registryType === \"npm\") ??\n server.packages.find((p) => p.registryType === \"pypi\") ??\n server.packages.find((p) => p.registryType === \"oci\") ??\n server.packages[0];\n\n const envVarDefs: EnvVar[] = bestPkg?.environmentVariables ?? [];\n const resolvedEnvVars = await promptEnvVars(envVarDefs);\n\n // Step 6b: In keychain mode, persist secret-flagged values encrypted and swap\n // them for `mcpm:keychain:…` placeholders, so no plaintext is written to any\n // client config. Non-secret vars stay inline; each secret is stored once and\n // reused for every client. The placeholder resolves at launch only while mcpm\n // guard wraps the server (run-inner.ts → resolveEnvPlaceholders). The swap\n // (and the \"no plaintext in config\" invariant) lives in applyKeychainSecrets.\n const secretsMode: SecretsMode = options.secrets ?? \"plaintext\";\n const { env: envForConfig, storedCount: storedSecretCount } = await applyKeychainSecrets({\n serverName: name,\n resolvedEnv: resolvedEnvVars,\n isSecret: (key) => envVarDefs.find((d) => d.name === key)?.isSecret === true,\n mode: secretsMode,\n setSecrets: deps.setSecrets,\n });\n\n // -------------------------------------------------------------------------\n // Step 7: Resolve (and thereby validate) each client's entry up front\n // -------------------------------------------------------------------------\n // resolveInstallEntry throws on an invalid identifier, so resolving here\n // before any config is written preserves fail-fast validation. The resolved\n // entries are reused in Step 8 to avoid recomputing them.\n const resolvedEntries = new Map<ClientId, McpServerEntry>();\n for (const clientId of targetClients) {\n resolvedEntries.set(clientId, resolveInstallEntry(serverEntry, clientId));\n }\n\n // -------------------------------------------------------------------------\n // Step 7b: H9 fail-closed gate for URL/HTTP-transport servers\n // -------------------------------------------------------------------------\n // A resolved entry with a `url` and no `command` runs UNGUARDED — the guard\n // relay only wraps a stdio process, so a non-stdio remote gets ZERO runtime\n // inspection. Mirror processUrlServer (up.ts): the MCP-surface kill-switch\n // (allowUrlServers === false) ALWAYS wins; otherwise DENY unless explicit\n // informed consent (`--allow-unguarded` this run, or a name already in the\n // persistent consent store). This is informed consent, NOT protection.\n const isUnguardedEntry = [...resolvedEntries.values()].some(\n (e) => e.url !== undefined && e.command === undefined\n );\n if (isUnguardedEntry) {\n if (options.allowUrlServers === false) {\n throw new Error(\n `Server '${name}' uses a URL/HTTP transport and is not permitted via the MCP surface.`\n );\n }\n const previousConsented = deps.readUnguardedConsent\n ? await deps.readUnguardedConsent()\n : [];\n const alreadyConsented = previousConsented.includes(name);\n const consented = options.allowUnguarded === true || alreadyConsented;\n if (!consented) {\n throw new Error(\n `Server '${name}' uses a URL/HTTP transport and runs UNGUARDED — no runtime ` +\n `inspection is possible (mcpm's guard relay only wraps stdio servers). ` +\n `Re-run with --allow-unguarded to install it WITHOUT protection.`\n );\n }\n // First-time consent: warn once and persist so a future install stays quiet.\n if (!alreadyConsented) {\n if (!jsonMode) {\n output(\n \"\\x1b[33m⚠ UNGUARDED: this URL/HTTP-transport server runs WITHOUT runtime \" +\n \"inspection (the guard relay only wraps stdio servers). This grants consent — \" +\n \"it does NOT add protection. The only true fix is a streamable-HTTP relay \" +\n \"(not yet implemented).\\x1b[0m\"\n );\n }\n if (deps.recordUnguardedConsent) {\n await deps.recordUnguardedConsent([name]).catch(() => undefined);\n }\n }\n }\n\n // -------------------------------------------------------------------------\n // Step 8: Write config to each client and record in store\n // -------------------------------------------------------------------------\n const installedClients: ClientId[] = [];\n\n for (const clientId of targetClients) {\n const adapter = getAdapter(clientId);\n const configPath = getConfigPath(clientId);\n const rawEntry = resolvedEntries.get(clientId)!;\n\n // Merge env vars into the entry (immutable). In keychain mode envForConfig\n // carries placeholders in place of secret values; otherwise it === resolvedEnvVars.\n const entry: McpServerEntry = {\n ...rawEntry,\n ...(Object.keys(envForConfig).length > 0\n ? { env: { ...(rawEntry.env ?? {}), ...envForConfig } }\n : {}),\n };\n\n await adapter.addServer(configPath, name, entry, { force: options.force });\n installedClients.push(clientId);\n }\n\n // -------------------------------------------------------------------------\n // Step 8b: Secret-storage notice\n // -------------------------------------------------------------------------\n if (!options.json) {\n if (secretsMode === \"keychain\" && storedSecretCount > 0) {\n output(\n `\\x1b[32mStored ${storedSecretCount} secret(s) encrypted at rest in ~/.mcpm. ` +\n \"With an OS keychain this protects against other-user/offline access (not \" +\n \"same-user processes); without one a machine-derived key is used that guards \" +\n \"casual local inspection only, NOT file exfiltration — run `mcpm secrets migrate` \" +\n \"once a keychain is available. \" +\n \"Run `mcpm guard enable` (then restart your IDE) so they resolve at launch — \" +\n \"until guard wraps this server it receives the literal placeholder.\\x1b[0m\"\n );\n } else {\n const hasSecrets = envVarDefs.some((ev) => ev.isSecret && resolvedEnvVars[ev.name]);\n if (hasSecrets) {\n output(\n \"\\x1b[33mNote: API keys are stored as plaintext in client config files. \" +\n \"Ensure config files have appropriate permissions (chmod 600).\\x1b[0m\"\n );\n }\n }\n }\n\n // -------------------------------------------------------------------------\n // Step 9: Record in store\n // -------------------------------------------------------------------------\n const storeEntry: InstalledServer = {\n name,\n version: serverEntry.server.version,\n clients: [...installedClients],\n installedAt: new Date().toISOString(),\n };\n await addToStore(storeEntry);\n\n // -------------------------------------------------------------------------\n // Step 10: Output result\n // -------------------------------------------------------------------------\n if (options.json === true) {\n const result = {\n name,\n version: serverEntry.server.version,\n clients: installedClients,\n trustScore: {\n score: trustScore.score,\n maxPossible: trustScore.maxPossible,\n level: trustScore.level,\n },\n };\n output(JSON.stringify(result, null, 2));\n return;\n }\n\n const clientList = installedClients.join(\", \");\n output(`\\u001b[32mInstalled '${name}' successfully into: ${clientList}\\u001b[0m`);\n}\n\n// ---------------------------------------------------------------------------\n// Commander registration\n// ---------------------------------------------------------------------------\n\nimport { Command, InvalidArgumentError } from \"commander\";\nimport chalk from \"chalk\";\nimport { input, password } from \"@inquirer/prompts\";\nimport { detectInstalledClients as _detectClients } from \"../config/detector.js\";\nimport { getConfigPath as _getConfigPath } from \"../config/paths.js\";\nimport { addInstalledServer as _addToStore } from \"../store/servers.js\";\nimport { scanTier1 as _scanTier1 } from \"../scanner/tier1.js\";\nimport { checkScannerAvailable as _checkScannerAvailable, scanTier2 as _scanTier2 } from \"../scanner/tier2.js\";\nimport { computeTrustScore as _computeTrustScore } from \"../scanner/trust-score.js\";\nimport { getAdapter as getAdapterDefault } from \"../config/index.js\";\nimport { confirm } from \"../utils/confirm.js\";\nimport { stdoutOutput } from \"../utils/output.js\";\n\nasync function promptEnvVarsDefault(\n vars: EnvVar[]\n): Promise<Record<string, string>> {\n if (vars.length === 0) return {};\n\n const result: Record<string, string> = {};\n for (const envVar of vars) {\n if (!envVar.isRequired && !envVar.isSecret) continue;\n\n const defaultVal = envVar.default ?? \"\";\n const promptMessage = envVar.description\n ? `${envVar.name} (${envVar.description}):`\n : `${envVar.name}:`;\n\n let prompted: string;\n if (envVar.isSecret) {\n // Use password prompt to mask secret input — value is never echoed to the terminal\n prompted = await password({ message: promptMessage });\n if (!prompted && defaultVal) {\n prompted = defaultVal;\n }\n } else {\n prompted = await input({ message: promptMessage, default: defaultVal });\n }\n\n if (prompted) {\n result[envVar.name] = prompted;\n }\n }\n return result;\n}\n\nexport function parseSecretsMode(raw: string): SecretsMode {\n if (raw !== \"keychain\" && raw !== \"plaintext\") {\n throw new InvalidArgumentError(\n `--secrets must be \"keychain\" or \"plaintext\", got: \"${raw}\"`\n );\n }\n return raw;\n}\n\nexport function parseMinTrust(raw: string): number {\n // Reject anything that isn't plain decimal digits (blocks hex \"0x50\", scientific\n // notation \"1e2\", spaces, empty string, and negative sign before range check).\n if (!/^\\d+$/.test(raw)) {\n throw new InvalidArgumentError(\n `--min-trust must be an integer between 0 and 100, got: \"${raw}\"`\n );\n }\n const n = Number(raw);\n if (n < 0 || n > 100) {\n throw new InvalidArgumentError(\n `--min-trust must be an integer between 0 and 100, got: \"${raw}\"`\n );\n }\n return n;\n}\n\nexport function parseMinReleaseAge(raw: string): number {\n // Same regex-first discipline as parseMinTrust: blocks hex \"0x18\", \"1e2\",\n // spaces, empty string, negatives. Safe-integer check guards absurd lengths.\n if (!/^\\d+$/.test(raw) || !Number.isSafeInteger(Number(raw))) {\n throw new InvalidArgumentError(\n `--min-release-age must be a non-negative integer number of hours, got: \"${raw}\"`\n );\n }\n return Number(raw);\n}\n\nexport function registerInstallCommand(program: Command): void {\n program\n .command(\"install <name>\")\n .description(\"Install an MCP server from the registry\")\n .option(\"-c, --client <id>\", \"install to a specific client only\")\n .option(\"-y, --yes\", \"skip all confirmation prompts\")\n .option(\"-f, --force\", \"overwrite if server already installed\")\n .option(\"--skip-health-check\", \"skip post-install health check\")\n .option(\"--json\", \"output result as JSON\")\n .option(\"--min-trust <n>\", \"abort install if pre-install trust score is below this threshold (0-100; health check runs after install)\", parseMinTrust)\n .option(\"--min-release-age <hours>\", \"abort install if the release is younger than this many hours OR its publish timestamp is missing/unparseable (fail-closed when set; also sets the scoring cooldown threshold; bypass with --allow-fresh)\", parseMinReleaseAge)\n .option(\"--allow-fresh\", \"bypass the --min-release-age gate (including the missing-timestamp block)\")\n .option(\"--secrets <mode>\", \"where to store secret env vars: 'keychain' (encrypted in ~/.mcpm, resolved by mcpm guard at launch) or 'plaintext' (default)\", parseSecretsMode)\n .option(\"--allow-unguarded\", \"permit a URL/HTTP-transport server to run WITHOUT runtime guard inspection (no relay wraps a non-stdio transport); records consent so future installs stay quiet\")\n .action(async (name: string, opts: { client?: string; yes?: boolean; force?: boolean; skipHealthCheck?: boolean; json?: boolean; minTrust?: number; minReleaseAge?: number; allowFresh?: boolean; secrets?: SecretsMode; allowUnguarded?: boolean }) => {\n const { RegistryClient } = await import(\"../registry/client.js\");\n const client = new RegistryClient();\n const { readUnguardedConsent, writeUnguardedConsent, mergeUnguarded } = await import(\n \"../guard/unguarded.js\"\n );\n\n const installOptions: InstallOptions = {\n client: opts.client,\n yes: opts.yes,\n force: opts.force,\n skipHealthCheck: opts.skipHealthCheck,\n json: opts.json,\n minTrust: opts.minTrust,\n minReleaseAge: opts.minReleaseAge,\n allowFresh: opts.allowFresh,\n secrets: opts.secrets,\n allowUnguarded: opts.allowUnguarded,\n };\n\n const installDeps: InstallDeps = {\n registryClient: client,\n detectClients: _detectClients,\n getAdapter: getAdapterDefault,\n getConfigPath: _getConfigPath,\n scanTier1: _scanTier1,\n checkScannerAvailable: _checkScannerAvailable,\n scanTier2: (serverName: string) => _scanTier2(serverName),\n computeTrustScore: _computeTrustScore,\n addToStore: _addToStore,\n confirm,\n promptEnvVars: promptEnvVarsDefault,\n output: stdoutOutput,\n setSecrets: _setSecrets,\n now: () => Date.now(),\n readUnguardedConsent,\n recordUnguardedConsent: async (names) => {\n const previous = await readUnguardedConsent();\n await writeUnguardedConsent(mergeUnguarded(previous, names));\n },\n };\n\n try {\n await handleInstall(name, installOptions, installDeps);\n } catch (err) {\n if (installOptions.json !== true) {\n console.error(chalk.red((err as Error).message));\n }\n process.exit(1);\n }\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+vBA,SAAkB,4BAA4B;AAC9C,OAAO,WAAW;AAClB,SAAS,OAAO,gBAAgB;AA5tBzB,SAAS,kBAAkB,KAAmB;AACnD,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,GAAG;AAAA,EACtB,QAAQ;AACN,UAAM,IAAI,MAAM,wBAAwB,GAAG,GAAG;AAAA,EAChD;AACA,MAAI,OAAO,aAAa,YAAY,OAAO,aAAa,SAAS;AAC/D,UAAM,IAAI;AAAA,MACR,qDAAqD,OAAO,QAAQ;AAAA,IACtE;AAAA,EACF;AAIA,MAAI,OAAO,aAAa,WAAW,CAAC,eAAe,OAAO,QAAQ,GAAG;AACnE,UAAM,IAAI;AAAA,MACR,0GACwC,GAAG;AAAA,IAC7C;AAAA,EACF;AACF;AASA,SAAS,eAAe,UAA2B;AACjD,QAAM,IAAI,SAAS,YAAY,EAAE,QAAQ,YAAY,EAAE;AACvD,SACE,MAAM,eACN,EAAE,SAAS,YAAY,KACvB,MAAM,eACN,MAAM;AAEV;AAEA,IAAM,oBAAoB;AAC1B,IAAM,qBAAqB;AAC3B,IAAM,oBACJ;AAMK,SAAS,mBAAmB,YAAoB,cAA4B;AACjF,QAAM,WAAmC;AAAA,IACvC,KAAK;AAAA,IACL,MAAM;AAAA,IACN,KAAK;AAAA,EACP;AACA,QAAM,KAAK,SAAS,YAAY;AAChC,MAAI,MAAM,CAAC,GAAG,KAAK,UAAU,GAAG;AAC9B,UAAM,IAAI;AAAA,MACR,kCAAkC,YAAY,iBAAiB,UAAU;AAAA,IAC3E;AAAA,EACF;AACF;AAaA,SAAS,qBACP,MACU;AACV,SAAO,KAAK,QAAQ,UAAU;AAChC;AAQA,IAAM,oBAAuC;AAAA;AAAA,EAE3C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA;AAAA;AAAA;AAAA,EAGA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AACF;AAOO,SAAS,oBAAoB,MAAsB;AACxD,aAAW,OAAO,MAAM;AAOtB,QAAI,8BAA8B,KAAK,GAAG,GAAG;AAC3C,YAAM,IAAI,MAAM,iDAAiD,GAAG,GAAG;AAAA,IACzE;AAGA,UAAM,cAAc,wBAAwB;AAAA,MAC1C,CAAC,WAAW,QAAQ,UAAU,IAAI,WAAW,GAAG,MAAM,GAAG;AAAA,IAC3D;AACA,QAAI,aAAa;AACf,YAAM,IAAI,MAAM,yCAAyC,GAAG,GAAG;AAAA,IACjE;AAGA,UAAM,SAAS,kBAAkB,KAAK,CAAC,YAAY,QAAQ,KAAK,GAAG,CAAC;AACpE,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,MAAM,4CAA4C,GAAG,GAAG;AAAA,IACpE;AAAA,EACF;AACF;AAgFO,SAAS,oBACd,aACA,UACgB;AAChB,QAAM,EAAE,OAAO,IAAI;AAGnB,MAAI,aAAa,YAAY,OAAO,WAAW,OAAO,QAAQ,SAAS,GAAG;AACxE,UAAM,aAAa,OAAO,QAAQ;AAAA,MAChC,CAAC,MAAM,EAAE,SAAS,qBAAqB,EAAE,SAAS;AAAA,IACpD;AACA,QAAI,YAAY;AACd,wBAAkB,WAAW,GAAG;AAEhC,YAAM,UAAkC,CAAC;AACzC,iBAAW,KAAK,WAAW,SAAS;AAClC,gBAAQ,EAAE,IAAI,IAAI;AAAA,MACpB;AACA,aAAO;AAAA,QACL,KAAK,WAAW;AAAA,QAChB,GAAI,OAAO,KAAK,OAAO,EAAE,SAAS,IAAI,EAAE,QAAQ,IAAI,CAAC;AAAA,MACvD;AAAA,IACF;AAAA,EACF;AAGA,QAAM,SAAS,OAAO,SAAS,KAAK,CAAC,MAAM,EAAE,iBAAiB,KAAK;AACnE,QAAM,UAAU,OAAO,SAAS,KAAK,CAAC,MAAM,EAAE,iBAAiB,MAAM;AACrE,QAAM,SAAS,OAAO,SAAS,KAAK,CAAC,MAAM,EAAE,iBAAiB,KAAK;AAEnE,MAAI,QAAQ;AACV,uBAAmB,OAAO,YAAY,KAAK;AAC3C,UAAM,SAAS,qBAAqB,OAAO,oBAAoB,CAAC,CAAC;AACjE,wBAAoB,MAAM;AAC1B,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM,CAAC,MAAM,OAAO,YAAY,GAAG,MAAM;AAAA,IAC3C;AAAA,EACF;AAEA,MAAI,SAAS;AACX,uBAAmB,QAAQ,YAAY,MAAM;AAC7C,UAAM,SAAS,qBAAqB,QAAQ,oBAAoB,CAAC,CAAC;AAClE,wBAAoB,MAAM;AAC1B,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM,CAAC,QAAQ,YAAY,GAAG,MAAM;AAAA,IACtC;AAAA,EACF;AAEA,MAAI,QAAQ;AACV,uBAAmB,OAAO,YAAY,KAAK;AAC3C,UAAM,SAAS,qBAAqB,OAAO,oBAAoB,CAAC,CAAC;AACjE,wBAAoB,MAAM;AAC1B,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM,CAAC,OAAO,QAAQ,MAAM,OAAO,YAAY,GAAG,MAAM;AAAA,IAC1D;AAAA,EACF;AAGA,MAAI,aAAa,YAAY,OAAO,WAAW,OAAO,QAAQ,SAAS,GAAG;AACxE,UAAM,SAAS,OAAO,QAAQ,CAAC;AAC/B,sBAAkB,OAAO,GAAG;AAC5B,WAAO,EAAE,KAAK,OAAO,IAAI;AAAA,EAC3B;AAEA,QAAM,IAAI;AAAA,IACR,qCAAqC,OAAO,IAAI;AAAA,EAClD;AACF;AASO,SAAS,iBAAiB,YAAgC;AAC/D,QAAM,EAAE,OAAO,aAAa,OAAO,UAAU,IAAI;AAEjD,QAAM,aAAa,WAAW,MAAM,YAAY,CAAC;AACjD,QAAM,MAAM,SAAS,OAAO,WAAW;AAEvC,QAAM,QAAkB;AAAA,IACtB,GAAG,GAAG,IAAI,KAAK,IAAI,WAAW,IAAI,UAAU;AAAA,IAC5C,gCAAgC,UAAU,cAAc,IAAI,gBAAgB,mBAAmB;AAAA,IAC/F,qCAAqC,UAAU,eAAe,KAAK,kCAAkC,SAAS,UAAU,UAAU,KAAK;AAAA,IACvI,kDAAkD,UAAU,eAAe,IAAI,WAAW,YAAY;AAAA,IACtG,iCAAiC,UAAU,eAAe,IAAI,WAAW,UAAU,YAAY,SAAS,+DAA+D;AAAA,EACzK;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAUA,eAAsB,cACpB,MACA,SACA,MACe;AACf,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA,YAAAA;AAAA,IACA,eAAAC;AAAA,IACA,WAAAC;AAAA,IACA,uBAAAC;AAAA,IACA,WAAAC;AAAA,IACA,mBAAAC;AAAA,IACA;AAAA,IACA,SAAAC;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAKJ,QAAM,cAAc,MAAM,eAAe,UAAU,IAAI;AASvD,QAAM,aAAa,mBAAmB,WAAW;AACjD,MAAI,WAAW,QAAQ;AACrB,QAAI,QAAQ,SAAS,MAAM;AACzB;AAAA,QACE,KAAK;AAAA,UACH;AAAA,YACE;AAAA,YACA,OAAO;AAAA,YACP,QAAQ,WAAW;AAAA,YACnB,SAAS,WAAW,iBAAiB;AAAA,UACvC;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA,UAAM,IAAI;AAAA,MACR,IAAI,IAAI,2CAA2C,WAAW,gBAAgB,KAAK,WAAW,aAAa,MAAM,EAAE;AAAA,IACrH;AAAA,EACF;AAKA,QAAM,gBAAgBJ,WAAU,WAAW;AAC3C,QAAM,mBAAmB,MAAMC,uBAAsB;AAErD,MAAI,cAAyB,CAAC,GAAG,aAAa;AAC9C,MAAI,kBAAkB;AACpB,UAAM,gBAAgB,MAAMC,WAAU,IAAI;AAC1C,kBAAc,CAAC,GAAG,aAAa,GAAG,aAAa;AAAA,EACjD;AAOA,QAAM,eAAe,oBAAoB,WAAW;AACpD,QAAM,aAAa,iBAAiB;AAAA,IAClC,aAAa,aAAa;AAAA,IAC1B,MAAM,KAAK,OAAO,KAAK,KAAK;AAAA,IAC5B,aAAa,QAAQ,iBAAiB;AAAA,EACxC,CAAC;AACD,MAAI,WAAW,SAAS;AACtB,kBAAc,CAAC,GAAG,aAAa,WAAW,OAAO;AAAA,EACnD;AAEA,QAAM,kBAAmC;AAAA,IACvC,UAAU;AAAA,IACV,mBAAmB;AAAA;AAAA,IACnB,oBAAoB;AAAA,IACpB;AAAA,EACF;AAEA,QAAM,aAAaC,mBAAkB,eAAe;AAKpD,MAAI,QAAQ,aAAa,UAAa,WAAW,QAAQ,QAAQ,UAAU;AACzE,QAAI,QAAQ,SAAS,MAAM;AACzB;AAAA,QACE,KAAK;AAAA,UACH;AAAA,YACE;AAAA,YACA,OAAO;AAAA,YACP,OAAO,WAAW;AAAA,YAClB,aAAa,WAAW;AAAA,YACxB,UAAU,QAAQ;AAAA,YAClB,OAAO,WAAW;AAAA,UACpB;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAIA,UAAM,IAAI;AAAA,MACR,eAAe,WAAW,KAAK,IAAI,WAAW,WAAW,qCAAqC,QAAQ,QAAQ;AAAA,IAChH;AAAA,EACF;AASA,MACE,QAAQ,kBAAkB,UAC1B,QAAQ,eAAe,QACvB,WAAW,iBACX;AACA,QAAI,QAAQ,SAAS,MAAM;AACzB;AAAA,QACE,KAAK;AAAA,UACH;AAAA,YACE;AAAA,YACA,OAAO;AAAA,YACP,UAAU,WAAW;AAAA,YACrB,UAAU,QAAQ;AAAA,YAClB,QAAQ,WAAW;AAAA,UACrB;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA,UAAM,OAAO;AACb,UAAM,IAAI;AAAA,MACR,WAAW,WAAW,WAClB,qGAAqG,QAAQ,aAAa,8BAA8B,IAAI,KAC5J,WAAW,WAAW,gBACpB,wEAAwE,QAAQ,aAAa,8BAA8B,IAAI,KAC/H,WAAW,WAAW,WACpB,kHAAkH,QAAQ,aAAa,kBAAkB,IAAI,KAC7J,eAAe,WAAW,QAAQ,sCAAsC,QAAQ,aAAa,MAAM,IAAI;AAAA,IACjH;AAAA,EACF;AAOA,QAAM,WAAW,QAAQ,SAAS;AAElC,MAAI,CAAC,UAAU;AACb,WAAO,iBAAiB,UAAU,CAAC;AACnC,WAAO,EAAE;AAAA,EACX;AAEA,MAAI,QAAQ,QAAQ,MAAM;AACxB,QAAI;AAEJ,QAAI,WAAW,UAAU,SAAS;AAChC,UAAI,CAAC,UAAU;AACb,eAAO,wFAA4F;AACnG,eAAO,4FAAgG;AAAA,MACzG;AACA,sBAAgB,MAAMC;AAAA,QACpB;AAAA,MACF;AAAA,IACF,WAAW,WAAW,UAAU,WAAW;AACzC,UAAI,CAAC,UAAU;AACb,eAAO,2FAA+F;AAAA,MACxG;AACA,sBAAgB,MAAMA,SAAQ,YAAY,IAAI,0BAA0B;AAAA,IAC1E,OAAO;AAEL,sBAAgB,MAAMA,SAAQ,YAAY,IAAI,IAAI;AAAA,IACpD;AAEA,QAAI,CAAC,eAAe;AAClB,UAAI,CAAC,SAAU,QAAO,yBAAyB;AAC/C;AAAA,IACF;AAAA,EACF;AAKA,MAAI,gBAAgB,MAAM,cAAc;AAExC,MAAI,cAAc,WAAW,GAAG;AAC9B,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,MAAI,QAAQ,WAAW,QAAW;AAChC,QAAI,CAAC,WAAW,SAAS,QAAQ,MAAkB,GAAG;AACpD,YAAM,IAAI;AAAA,QACR,mBAAmB,QAAQ,MAAM,oBAAoB,WAAW,KAAK,IAAI,CAAC;AAAA,MAC5E;AAAA,IACF;AACA,UAAM,cAAc,QAAQ;AAC5B,QAAI,CAAC,cAAc,SAAS,WAAW,GAAG;AACxC,YAAM,IAAI;AAAA,QACR,WAAW,WAAW;AAAA,MACxB;AAAA,IACF;AACA,oBAAgB,CAAC,WAAW;AAAA,EAC9B;AAKA,MAAI,QAAQ,UAAU,MAAM;AAC1B,eAAW,YAAY,eAAe;AACpC,YAAM,UAAUN,YAAW,QAAQ;AACnC,YAAM,aAAaC,eAAc,QAAQ;AACzC,YAAM,WAAW,MAAM,QAAQ,KAAK,UAAU;AAC9C,UAAI,OAAO,UAAU,eAAe,KAAK,UAAU,IAAI,GAAG;AACxD,cAAM,IAAI;AAAA,UACR,WAAW,IAAI,6BAA6B,QAAQ;AAAA,QACtD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAMA,QAAM,EAAE,OAAO,IAAI;AACnB,QAAM,UACJ,OAAO,SAAS,KAAK,CAAC,MAAM,EAAE,iBAAiB,KAAK,KACpD,OAAO,SAAS,KAAK,CAAC,MAAM,EAAE,iBAAiB,MAAM,KACrD,OAAO,SAAS,KAAK,CAAC,MAAM,EAAE,iBAAiB,KAAK,KACpD,OAAO,SAAS,CAAC;AAEnB,QAAM,aAAuB,SAAS,wBAAwB,CAAC;AAC/D,QAAM,kBAAkB,MAAM,cAAc,UAAU;AAQtD,QAAM,cAA2B,QAAQ,WAAW;AACpD,QAAM,EAAE,KAAK,cAAc,aAAa,kBAAkB,IAAI,MAAM,qBAAqB;AAAA,IACvF,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,UAAU,CAAC,QAAQ,WAAW,KAAK,CAAC,MAAM,EAAE,SAAS,GAAG,GAAG,aAAa;AAAA,IACxE,MAAM;AAAA,IACN,YAAY,KAAK;AAAA,EACnB,CAAC;AAQD,QAAM,kBAAkB,oBAAI,IAA8B;AAC1D,aAAW,YAAY,eAAe;AACpC,oBAAgB,IAAI,UAAU,oBAAoB,aAAa,QAAQ,CAAC;AAAA,EAC1E;AAWA,QAAM,mBAAmB,CAAC,GAAG,gBAAgB,OAAO,CAAC,EAAE;AAAA,IACrD,CAAC,MAAM,EAAE,QAAQ,UAAa,EAAE,YAAY;AAAA,EAC9C;AACA,MAAI,kBAAkB;AACpB,QAAI,QAAQ,oBAAoB,OAAO;AACrC,YAAM,IAAI;AAAA,QACR,WAAW,IAAI;AAAA,MACjB;AAAA,IACF;AACA,UAAM,oBAAoB,KAAK,uBAC3B,MAAM,KAAK,qBAAqB,IAChC,CAAC;AACL,UAAM,mBAAmB,kBAAkB,SAAS,IAAI;AACxD,UAAM,YAAY,QAAQ,mBAAmB,QAAQ;AACrD,QAAI,CAAC,WAAW;AACd,YAAM,IAAI;AAAA,QACR,WAAW,IAAI;AAAA,MAGjB;AAAA,IACF;AAEA,QAAI,CAAC,kBAAkB;AACrB,UAAI,CAAC,UAAU;AACb;AAAA,UACE;AAAA,QAIF;AAAA,MACF;AACA,UAAI,KAAK,wBAAwB;AAC/B,cAAM,KAAK,uBAAuB,CAAC,IAAI,CAAC,EAAE,MAAM,MAAM,MAAS;AAAA,MACjE;AAAA,IACF;AAAA,EACF;AAKA,QAAM,mBAA+B,CAAC;AAEtC,aAAW,YAAY,eAAe;AACpC,UAAM,UAAUD,YAAW,QAAQ;AACnC,UAAM,aAAaC,eAAc,QAAQ;AACzC,UAAM,WAAW,gBAAgB,IAAI,QAAQ;AAI7C,UAAM,QAAwB;AAAA,MAC5B,GAAG;AAAA,MACH,GAAI,OAAO,KAAK,YAAY,EAAE,SAAS,IACnC,EAAE,KAAK,EAAE,GAAI,SAAS,OAAO,CAAC,GAAI,GAAG,aAAa,EAAE,IACpD,CAAC;AAAA,IACP;AAEA,UAAM,QAAQ,UAAU,YAAY,MAAM,OAAO,EAAE,OAAO,QAAQ,MAAM,CAAC;AACzE,qBAAiB,KAAK,QAAQ;AAAA,EAChC;AAKA,MAAI,CAAC,QAAQ,MAAM;AACjB,QAAI,gBAAgB,cAAc,oBAAoB,GAAG;AACvD;AAAA,QACE,kBAAkB,iBAAiB;AAAA,MAOrC;AAAA,IACF,OAAO;AACL,YAAM,aAAa,WAAW,KAAK,CAAC,OAAO,GAAG,YAAY,gBAAgB,GAAG,IAAI,CAAC;AAClF,UAAI,YAAY;AACd;AAAA,UACE;AAAA,QAEF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAKA,QAAM,aAA8B;AAAA,IAClC;AAAA,IACA,SAAS,YAAY,OAAO;AAAA,IAC5B,SAAS,CAAC,GAAG,gBAAgB;AAAA,IAC7B,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,EACtC;AACA,QAAM,WAAW,UAAU;AAK3B,MAAI,QAAQ,SAAS,MAAM;AACzB,UAAM,SAAS;AAAA,MACb;AAAA,MACA,SAAS,YAAY,OAAO;AAAA,MAC5B,SAAS;AAAA,MACT,YAAY;AAAA,QACV,OAAO,WAAW;AAAA,QAClB,aAAa,WAAW;AAAA,QACxB,OAAO,WAAW;AAAA,MACpB;AAAA,IACF;AACA,WAAO,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AACtC;AAAA,EACF;AAEA,QAAM,aAAa,iBAAiB,KAAK,IAAI;AAC7C,SAAO,sBAAwB,IAAI,wBAAwB,UAAU,SAAW;AAClF;AAmBA,eAAe,qBACb,MACiC;AACjC,MAAI,KAAK,WAAW,EAAG,QAAO,CAAC;AAE/B,QAAM,SAAiC,CAAC;AACxC,aAAW,UAAU,MAAM;AACzB,QAAI,CAAC,OAAO,cAAc,CAAC,OAAO,SAAU;AAE5C,UAAM,aAAa,OAAO,WAAW;AACrC,UAAM,gBAAgB,OAAO,cACzB,GAAG,OAAO,IAAI,KAAK,OAAO,WAAW,OACrC,GAAG,OAAO,IAAI;AAElB,QAAI;AACJ,QAAI,OAAO,UAAU;AAEnB,iBAAW,MAAM,SAAS,EAAE,SAAS,cAAc,CAAC;AACpD,UAAI,CAAC,YAAY,YAAY;AAC3B,mBAAW;AAAA,MACb;AAAA,IACF,OAAO;AACL,iBAAW,MAAM,MAAM,EAAE,SAAS,eAAe,SAAS,WAAW,CAAC;AAAA,IACxE;AAEA,QAAI,UAAU;AACZ,aAAO,OAAO,IAAI,IAAI;AAAA,IACxB;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,iBAAiB,KAA0B;AACzD,MAAI,QAAQ,cAAc,QAAQ,aAAa;AAC7C,UAAM,IAAI;AAAA,MACR,sDAAsD,GAAG;AAAA,IAC3D;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,cAAc,KAAqB;AAGjD,MAAI,CAAC,QAAQ,KAAK,GAAG,GAAG;AACtB,UAAM,IAAI;AAAA,MACR,2DAA2D,GAAG;AAAA,IAChE;AAAA,EACF;AACA,QAAM,IAAI,OAAO,GAAG;AACpB,MAAI,IAAI,KAAK,IAAI,KAAK;AACpB,UAAM,IAAI;AAAA,MACR,2DAA2D,GAAG;AAAA,IAChE;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,mBAAmB,KAAqB;AAGtD,MAAI,CAAC,QAAQ,KAAK,GAAG,KAAK,CAAC,OAAO,cAAc,OAAO,GAAG,CAAC,GAAG;AAC5D,UAAM,IAAI;AAAA,MACR,2EAA2E,GAAG;AAAA,IAChF;AAAA,EACF;AACA,SAAO,OAAO,GAAG;AACnB;AAEO,SAAS,uBAAuB,SAAwB;AAC7D,UACG,QAAQ,gBAAgB,EACxB,YAAY,yCAAyC,EACrD,OAAO,qBAAqB,mCAAmC,EAC/D,OAAO,aAAa,+BAA+B,EACnD,OAAO,eAAe,uCAAuC,EAC7D,OAAO,uBAAuB,gCAAgC,EAC9D,OAAO,UAAU,uBAAuB,EACxC,OAAO,mBAAmB,6GAA6G,aAAa,EACpJ,OAAO,6BAA6B,4MAA4M,kBAAkB,EAClQ,OAAO,iBAAiB,2EAA2E,EACnG,OAAO,oBAAoB,gIAAgI,gBAAgB,EAC3K,OAAO,qBAAqB,kKAAkK,EAC9L,OAAO,OAAO,MAAc,SAA2N;AACtP,UAAM,EAAE,eAAe,IAAI,MAAM,OAAO,sBAAuB;AAC/D,UAAM,SAAS,IAAI,eAAe;AAClC,UAAM,EAAE,sBAAsB,uBAAuB,eAAe,IAAI,MAAM,OAC5E,yBACF;AAEA,UAAM,iBAAiC;AAAA,MACrC,QAAQ,KAAK;AAAA,MACb,KAAK,KAAK;AAAA,MACV,OAAO,KAAK;AAAA,MACZ,iBAAiB,KAAK;AAAA,MACtB,MAAM,KAAK;AAAA,MACX,UAAU,KAAK;AAAA,MACf,eAAe,KAAK;AAAA,MACpB,YAAY,KAAK;AAAA,MACjB,SAAS,KAAK;AAAA,MACd,gBAAgB,KAAK;AAAA,IACvB;AAEA,UAAM,cAA2B;AAAA,MAC/B,gBAAgB;AAAA,MAChB,eAAe;AAAA,MACf;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW,CAAC,eAAuB,UAAW,UAAU;AAAA,MACxD;AAAA,MACA,YAAY;AAAA,MACZ;AAAA,MACA,eAAe;AAAA,MACf,QAAQ;AAAA,MACR;AAAA,MACA,KAAK,MAAM,KAAK,IAAI;AAAA,MACpB;AAAA,MACA,wBAAwB,OAAO,UAAU;AACvC,cAAM,WAAW,MAAM,qBAAqB;AAC5C,cAAM,sBAAsB,eAAe,UAAU,KAAK,CAAC;AAAA,MAC7D;AAAA,IACF;AAEA,QAAI;AACF,YAAM,cAAc,MAAM,gBAAgB,WAAW;AAAA,IACvD,SAAS,KAAK;AACZ,UAAI,eAAe,SAAS,MAAM;AAChC,gBAAQ,MAAM,MAAM,IAAK,IAAc,OAAO,CAAC;AAAA,MACjD;AACA,cAAQ,KAAK,CAAC;AAAA,IAChB;AAAA,EACF,CAAC;AACL;","names":["getAdapter","getConfigPath","scanTier1","checkScannerAvailable","scanTier2","computeTrustScore","confirm"]}
#!/usr/bin/env node
import {
getStorePath,
readJson,
withStoreLock,
writeJson
} from "./chunk-3X76P3FG.js";
// src/store/keychain.ts
import { createHash, randomBytes, webcrypto } from "crypto";
import os from "os";
// src/store/os-keychain.ts
import { spawn } from "child_process";
import { readFile, writeFile } from "fs/promises";
import path from "path";
var SERVICE = "mcpm";
var ACCOUNT = "secret-store-master-key";
var DPAPI_BLOB_FILE = "master-key.dpapi";
var EXEC_TIMEOUT_MS = 5e3;
function run(command, args, opts = {}) {
return new Promise((resolve) => {
const child = spawn(command, args, {
timeout: EXEC_TIMEOUT_MS,
env: opts.env ?? process.env,
stdio: ["pipe", "pipe", "pipe"]
});
let stdout = "";
let stderr = "";
let settled = false;
const settle = (r) => {
if (settled) return;
settled = true;
resolve(r);
};
child.stdout.on("data", (d) => stdout += d.toString());
child.stderr.on("data", (d) => stderr += d.toString());
child.on("error", () => settle({ code: null, stdout, stderr }));
child.on("close", (code) => settle({ code, stdout, stderr }));
if (opts.input !== void 0) {
child.stdin.on("error", () => {
});
child.stdin.end(opts.input);
} else {
child.stdin.end();
}
});
}
function isSupportedPlatform() {
if (process.env.MCPM_DISABLE_OS_KEYCHAIN === "1") return false;
return process.platform === "darwin" || process.platform === "linux" || process.platform === "win32";
}
async function darwinGet() {
const r = await run("security", [
"find-generic-password",
"-a",
ACCOUNT,
"-s",
SERVICE,
"-w"
]);
if (r.code !== 0) return null;
return decodeKey(r.stdout.trim());
}
async function darwinStore(keyB64) {
const r = await run("security", [
"add-generic-password",
"-a",
ACCOUNT,
"-s",
SERVICE,
"-U",
"-w",
keyB64
]);
return r.code === 0;
}
async function linuxGet() {
const r = await run("secret-tool", ["lookup", "service", SERVICE, "account", ACCOUNT]);
if (r.code !== 0) return null;
const value = r.stdout.trim();
if (value.length === 0) return null;
return decodeKey(value);
}
async function linuxStore(keyB64) {
const r = await run(
"secret-tool",
["store", "--label=mcpm secret store master key", "service", SERVICE, "account", ACCOUNT],
{ input: keyB64 }
);
return r.code === 0;
}
var PS_PROTECT = "$ErrorActionPreference='Stop';Add-Type -AssemblyName System.Security;$b=[Convert]::FromBase64String($env:MCPM_KEY_B64);$p=[Security.Cryptography.ProtectedData]::Protect($b,$null,'CurrentUser');[Convert]::ToBase64String($p)";
var PS_UNPROTECT = "$ErrorActionPreference='Stop';Add-Type -AssemblyName System.Security;$b=[Convert]::FromBase64String($env:MCPM_BLOB_B64);$p=[Security.Cryptography.ProtectedData]::Unprotect($b,$null,'CurrentUser');[Convert]::ToBase64String($p)";
async function blobPath() {
return path.join(await getStorePath(), DPAPI_BLOB_FILE);
}
async function windowsGet() {
let blob;
try {
blob = (await readFile(await blobPath(), "utf8")).trim();
} catch {
return null;
}
if (blob.length === 0) return null;
const r = await run("powershell", ["-NoProfile", "-NonInteractive", "-Command", PS_UNPROTECT], {
env: { ...process.env, MCPM_BLOB_B64: blob }
});
if (r.code !== 0) return null;
return decodeKey(r.stdout.trim());
}
async function windowsStore(keyB64) {
const r = await run("powershell", ["-NoProfile", "-NonInteractive", "-Command", PS_PROTECT], {
env: { ...process.env, MCPM_KEY_B64: keyB64 }
});
if (r.code !== 0 || r.stdout.trim().length === 0) return false;
try {
await writeFile(await blobPath(), r.stdout.trim(), { mode: 384 });
return true;
} catch {
return false;
}
}
function decodeKey(b64) {
try {
const buf = Buffer.from(b64, "base64");
return buf.length === 32 ? buf : null;
} catch {
return null;
}
}
async function getStoredKey() {
if (!isSupportedPlatform()) return null;
switch (process.platform) {
case "darwin":
return darwinGet();
case "linux":
return linuxGet();
case "win32":
return windowsGet();
default:
return null;
}
}
async function storeKey(key) {
if (!isSupportedPlatform()) return false;
if (key.length !== 32) return false;
const keyB64 = key.toString("base64");
switch (process.platform) {
case "darwin":
return darwinStore(keyB64);
case "linux":
return linuxStore(keyB64);
case "win32":
return windowsStore(keyB64);
default:
return false;
}
}
// src/store/keychain.ts
var STORE_FILE = "secrets.enc.json";
var PLACEHOLDER_PREFIX = "mcpm:keychain:";
var PBKDF2_ITERATIONS = 6e5;
var SCHEME_KEYCHAIN = "k1";
var HKDF_INFO = new TextEncoder().encode("mcpm-secret-store-v1");
var MACHINE_PASSPHRASE = new TextEncoder().encode(
`mcpm:${os.hostname()}:${os.userInfo().username}`
);
var SAFE_ID_RE = /^[a-zA-Z0-9._-]{1,256}$/;
function assertSafeId(value, label) {
if (!SAFE_ID_RE.test(value)) {
throw new Error(`Invalid ${label}: "${value}" \u2014 must match [a-zA-Z0-9._-], max 256 chars`);
}
}
function validatedStoreKey(server, key) {
assertSafeId(server, "server");
assertSafeId(key, "key");
return `${server}/${key}`;
}
var _masterKey;
async function readMasterKey() {
if (_masterKey) return _masterKey.value;
const value = await getStoredKey();
_masterKey = { value };
return value;
}
async function getOrCreateMasterKey() {
const existing = await readMasterKey();
if (existing) return existing;
if (!isSupportedPlatform()) return null;
return withStoreLock(async () => {
const raced = await getStoredKey();
if (raced) {
_masterKey = { value: raced };
return raced;
}
const key = randomBytes(32);
const value = await storeKey(key) ? key : null;
_masterKey = { value };
return value;
});
}
var _hkdfMaterial;
function hkdfMaterial(masterKey) {
if (!_hkdfMaterial || !_hkdfMaterial.key.equals(masterKey)) {
_hkdfMaterial = {
key: masterKey,
material: webcrypto.subtle.importKey("raw", masterKey, "HKDF", false, ["deriveKey"])
};
}
return _hkdfMaterial.material;
}
async function deriveKeychainKey(masterKey, salt) {
const material = await hkdfMaterial(masterKey);
return webcrypto.subtle.deriveKey(
{ name: "HKDF", salt, info: HKDF_INFO, hash: "SHA-256" },
material,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
}
var _keyMaterialPromise = null;
function getMachineKeyMaterial() {
if (!_keyMaterialPromise) {
_keyMaterialPromise = webcrypto.subtle.importKey(
"raw",
MACHINE_PASSPHRASE,
{ name: "PBKDF2" },
false,
["deriveKey"]
);
}
return _keyMaterialPromise;
}
async function deriveMachineKey(salt) {
const keyMaterial = await getMachineKeyMaterial();
return webcrypto.subtle.deriveKey(
{ name: "PBKDF2", salt, iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
}
var hex = (buf) => Buffer.from(buf instanceof Uint8Array ? buf : new Uint8Array(buf)).toString("hex");
async function encrypt(plaintext) {
const salt = randomBytes(16);
const iv = randomBytes(12);
const masterKey = await getOrCreateMasterKey();
const key = masterKey ? await deriveKeychainKey(masterKey, salt) : await deriveMachineKey(salt);
const cipherBuf = await webcrypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
new TextEncoder().encode(plaintext)
);
const body = [hex(salt), hex(iv), hex(cipherBuf)].join(":");
return masterKey ? `${SCHEME_KEYCHAIN}:${body}` : body;
}
async function decryptWith(key, ivHex, cipherHex) {
const plainBuf = await webcrypto.subtle.decrypt(
{ name: "AES-GCM", iv: Buffer.from(ivHex, "hex") },
key,
Buffer.from(cipherHex, "hex")
);
return new TextDecoder().decode(plainBuf);
}
async function decrypt(stored) {
const parts = stored.split(":");
if (parts.length === 4 && parts[0] === SCHEME_KEYCHAIN) {
const [, saltHex, ivHex, cipherHex] = parts;
const masterKey = await readMasterKey();
if (!masterKey) {
throw new Error(
"Cannot decrypt: this secret is protected by the OS keychain master key, which is unavailable on this machine/account (or the keychain entry was removed)."
);
}
const key = await deriveKeychainKey(masterKey, Buffer.from(saltHex, "hex"));
return decryptWith(key, ivHex, cipherHex);
}
if (parts.length === 3) {
const [saltHex, ivHex, cipherHex] = parts;
const key = await deriveMachineKey(Buffer.from(saltHex, "hex"));
return decryptWith(key, ivHex, cipherHex);
}
throw new Error("Invalid ciphertext format");
}
async function readStore() {
return await readJson(STORE_FILE) ?? {};
}
async function decryptFromSnapshot(store, sk) {
const stored = store[sk];
return stored ? decrypt(stored) : null;
}
async function setSecret(server, key, value) {
const sk = validatedStoreKey(server, key);
const encrypted = await encrypt(value);
await withStoreLock(async () => {
const store = await readStore();
await writeJson(STORE_FILE, { ...store, [sk]: encrypted });
});
}
async function setSecrets(server, values) {
const encrypted = {};
for (const [key, value] of Object.entries(values)) {
encrypted[validatedStoreKey(server, key)] = await encrypt(value);
}
await withStoreLock(async () => {
const store = await readStore();
await writeJson(STORE_FILE, { ...store, ...encrypted });
});
}
async function getSecret(server, key) {
const sk = validatedStoreKey(server, key);
return decryptFromSnapshot(await readStore(), sk);
}
async function deleteSecret(server, key) {
const sk = validatedStoreKey(server, key);
let removed = false;
await withStoreLock(async () => {
const store = await readStore();
if (!(sk in store)) return;
const { [sk]: _removed, ...rest } = store;
await writeJson(STORE_FILE, rest);
removed = true;
});
return removed;
}
function toPlaceholder(server, key) {
return `${PLACEHOLDER_PREFIX}${server}/${key}`;
}
function parsePlaceholder(value) {
if (!value.startsWith(PLACEHOLDER_PREFIX)) return null;
const rest = value.slice(PLACEHOLDER_PREFIX.length);
const slashIdx = rest.indexOf("/");
if (slashIdx === -1) return null;
return { server: rest.slice(0, slashIdx), key: rest.slice(slashIdx + 1) };
}
async function resolveEnvPlaceholders(env) {
const passthrough = {};
const placeholders = [];
for (const [name, value] of Object.entries(env)) {
if (value === void 0) continue;
const placeholder = parsePlaceholder(value);
if (placeholder === null) {
passthrough[name] = value;
continue;
}
placeholders.push({ name, ...placeholder });
}
if (placeholders.length === 0) return passthrough;
return withStoreLock(async () => {
const store = await readStore();
const resolved = { ...passthrough };
for (const { name, server, key } of placeholders) {
const sk = validatedStoreKey(server, key);
const secret = await decryptFromSnapshot(store, sk);
if (secret === null) {
throw new Error(
`Secret "${server}/${key}" not found. Run \`mcpm secrets set ${server} ${key}\` to store it.`
);
}
resolved[name] = secret;
}
return resolved;
});
}
function deriveKeychainId(name) {
const sanitized = name.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 200);
const hash = createHash("sha256").update(name).digest("hex").slice(0, 12);
return `${sanitized}-${hash}`;
}
async function listAll() {
const store = await readStore();
const grouped = {};
for (const storeKey2 of Object.keys(store)) {
const slashIdx = storeKey2.indexOf("/");
if (slashIdx === -1) continue;
const server = storeKey2.slice(0, slashIdx);
const key = storeKey2.slice(slashIdx + 1);
(grouped[server] ??= []).push(key);
}
return grouped;
}
async function activeSecretBackend() {
return await readMasterKey() !== null ? "os-keychain" : "machine-key";
}
async function migrateToKeychain() {
const masterKey = await getOrCreateMasterKey();
if (!masterKey) return { migrated: 0, failed: 0, total: 0, usingKeychain: false };
return withStoreLock(async () => {
const store = await readStore();
const entries = Object.entries(store);
const next = {};
let migrated = 0;
let failed = 0;
for (const [sk, value] of entries) {
const isLegacy = value.split(":").length === 3;
if (!isLegacy) {
next[sk] = value;
continue;
}
try {
const plain = await decrypt(value);
next[sk] = await encrypt(plain);
migrated++;
} catch {
next[sk] = value;
failed++;
}
}
if (migrated > 0) await writeJson(STORE_FILE, next);
return { migrated, failed, total: entries.length, usingKeychain: true };
});
}
async function applyKeychainSecrets(opts) {
if (opts.mode !== "keychain") {
return { env: opts.resolvedEnv, storedCount: 0 };
}
if (!opts.setSecrets) {
throw new Error("Keychain secret storage is unavailable.");
}
const keychainId = deriveKeychainId(opts.serverName);
const env = {};
const toStore = {};
for (const [key, value] of Object.entries(opts.resolvedEnv)) {
if (opts.isSecret(key)) {
toStore[key] = value;
env[key] = toPlaceholder(keychainId, key);
} else {
env[key] = value;
}
}
const storedCount = Object.keys(toStore).length;
if (storedCount > 0) {
await opts.setSecrets(keychainId, toStore);
}
return { env, storedCount };
}
function placeholderEnvKeys(env) {
if (!env) return [];
return Object.entries(env).filter(([, v]) => typeof v === "string" && parsePlaceholder(v) !== null).map(([k]) => k);
}
export {
isSupportedPlatform,
setSecret,
setSecrets,
getSecret,
deleteSecret,
toPlaceholder,
parsePlaceholder,
resolveEnvPlaceholders,
deriveKeychainId,
listAll,
activeSecretBackend,
migrateToKeychain,
applyKeychainSecrets,
placeholderEnvKeys
};
//# sourceMappingURL=chunk-GZ3WCRLG.js.map
{"version":3,"sources":["../src/store/keychain.ts","../src/store/os-keychain.ts"],"sourcesContent":["/**\n * Encrypted-at-rest secret storage using Node's built-in crypto.subtle.\n * No native dependencies (contrast: keytar requires node-gyp).\n *\n * Two encryption schemes (security #15):\n * - keychain (\"k1:\" tag): AES-GCM key derived via HKDF from a random 32-byte\n * master key held in the OS credential store (store/os-keychain.ts). The\n * master key never touches disk, so a copied secrets.enc.json cannot be\n * decrypted on another machine/account — real exfiltration resistance.\n * - machine (legacy, untagged): AES-GCM key derived via PBKDF2 from\n * hostname+username. This is NOT a secret (it is recoverable by anyone who\n * copies the store file), so it guards only against casual local inspection.\n * Used as a fallback where no OS keychain is available (headless/CI) and to\n * decrypt entries written before the keychain upgrade.\n *\n * New secrets use the keychain scheme whenever an OS keychain is available;\n * `migrateToKeychain()` upgrades pre-existing machine-scheme entries.\n *\n * Storage format: ~/.mcpm/secrets.enc.json\n * { \"server/KEY\": \"k1:<salt_hex>:<iv_hex>:<ct_hex>\" } // keychain scheme\n * { \"server/KEY\": \"<salt_hex>:<iv_hex>:<ct_hex>\" } // legacy machine scheme\n *\n * Placeholder format used in config files:\n * \"mcpm:keychain:server/KEY\"\n */\n\nimport { createHash, randomBytes, webcrypto } from \"node:crypto\";\nimport os from \"node:os\";\nimport { readJson, writeJson } from \"./index.js\";\nimport { withStoreLock } from \"./atomic.js\";\nimport { getStoredKey, isSupportedPlatform, storeKey } from \"./os-keychain.js\";\n\nconst STORE_FILE = \"secrets.enc.json\";\nconst PLACEHOLDER_PREFIX = \"mcpm:keychain:\";\nconst PBKDF2_ITERATIONS = 600_000;\n\n// Scheme tag prefixing keychain-scheme entries: \"k1:<salt>:<iv>:<ct>\". Legacy\n// machine-scheme entries are unprefixed (\"<salt>:<iv>:<ct>\"), so decrypt() routes\n// on the part count — keeping old entries readable after the upgrade (issue #15).\nconst SCHEME_KEYCHAIN = \"k1\";\nconst HKDF_INFO = new TextEncoder().encode(\"mcpm-secret-store-v1\");\n\n// Legacy/fallback machine passphrase. This is NOT a secret: hostname + username\n// are recoverable by anyone who copies the store file, so the machine scheme\n// guards only against casual local inspection — never file exfiltration. The\n// keychain scheme (OS-held master key, below) supersedes it whenever an OS\n// credential store is available; this remains for environments without one\n// (headless/CI) and to decrypt pre-existing entries (issue #15).\nconst MACHINE_PASSPHRASE = new TextEncoder().encode(\n `mcpm:${os.hostname()}:${os.userInfo().username}`\n);\n\n// ---------------------------------------------------------------------------\n// Input validation\n// ---------------------------------------------------------------------------\n\nconst SAFE_ID_RE = /^[a-zA-Z0-9._-]{1,256}$/;\n\nfunction assertSafeId(value: string, label: string): void {\n if (!SAFE_ID_RE.test(value)) {\n throw new Error(`Invalid ${label}: \"${value}\" — must match [a-zA-Z0-9._-], max 256 chars`);\n }\n}\n\nfunction validatedStoreKey(server: string, key: string): string {\n assertSafeId(server, \"server\");\n assertSafeId(key, \"key\");\n return `${server}/${key}`;\n}\n\n// ---------------------------------------------------------------------------\n// Master key (OS credential store) — security #15\n// ---------------------------------------------------------------------------\n//\n// A single random 32-byte key lives in the OS keychain (store/os-keychain.ts);\n// per-value AES keys are derived from it via HKDF. Because the master key is\n// never written to ~/.mcpm, a copied secrets.enc.json cannot be decrypted on\n// another machine/account. When no OS keychain is available, encrypt() falls\n// back to the machine scheme below.\n\nlet _masterKey: { value: Buffer | null } | undefined;\n\n/** Read the stored master key (never creates one); memoized per process. */\nasync function readMasterKey(): Promise<Buffer | null> {\n if (_masterKey) return _masterKey.value;\n const value = await getStoredKey();\n _masterKey = { value };\n return value;\n}\n\n/**\n * Return the master key, creating and persisting a fresh random one if none\n * exists yet and the platform has a usable credential store. Creation runs\n * under the store lock so two concurrent first-writes cannot generate two keys\n * and leave one writer's secret undecryptable.\n */\nasync function getOrCreateMasterKey(): Promise<Buffer | null> {\n const existing = await readMasterKey();\n if (existing) return existing;\n if (!isSupportedPlatform()) return null;\n return withStoreLock(async () => {\n const raced = await getStoredKey(); // another process may have just created it\n if (raced) {\n _masterKey = { value: raced };\n return raced;\n }\n const key = randomBytes(32);\n // If the keychain write fails, memoize null: this session falls back to the\n // machine scheme for the rest of its life (no retry). That is the safe,\n // honest outcome — activeSecretBackend() will report \"machine-key\" and\n // `secrets set` warns the user — rather than half-using an unpersisted key.\n const value = (await storeKey(key)) ? key : null;\n _masterKey = { value };\n return value;\n });\n}\n\n// ---------------------------------------------------------------------------\n// Key derivation — keychain scheme (HKDF) and machine scheme (PBKDF2)\n// ---------------------------------------------------------------------------\n\n// HKDF importKey is cheap but stable per master key; cache it per-process.\nlet _hkdfMaterial: { key: Buffer; material: Promise<webcrypto.CryptoKey> } | undefined;\n\nfunction hkdfMaterial(masterKey: Buffer): Promise<webcrypto.CryptoKey> {\n if (!_hkdfMaterial || !_hkdfMaterial.key.equals(masterKey)) {\n _hkdfMaterial = {\n key: masterKey,\n material: webcrypto.subtle.importKey(\"raw\", masterKey, \"HKDF\", false, [\"deriveKey\"]),\n };\n }\n return _hkdfMaterial.material;\n}\n\nasync function deriveKeychainKey(masterKey: Buffer, salt: Uint8Array): Promise<webcrypto.CryptoKey> {\n const material = await hkdfMaterial(masterKey);\n return webcrypto.subtle.deriveKey(\n { name: \"HKDF\", salt, info: HKDF_INFO, hash: \"SHA-256\" },\n material,\n { name: \"AES-GCM\", length: 256 },\n false,\n [\"encrypt\", \"decrypt\"]\n );\n}\n\n// Cache the PBKDF2 importKey step — it is cheap but never varies within a\n// process. The expensive derivation still runs per value (random per-value salt).\nlet _keyMaterialPromise: Promise<webcrypto.CryptoKey> | null = null;\n\nfunction getMachineKeyMaterial(): Promise<webcrypto.CryptoKey> {\n if (!_keyMaterialPromise) {\n _keyMaterialPromise = webcrypto.subtle.importKey(\n \"raw\",\n MACHINE_PASSPHRASE,\n { name: \"PBKDF2\" },\n false,\n [\"deriveKey\"]\n );\n }\n return _keyMaterialPromise;\n}\n\nasync function deriveMachineKey(salt: Uint8Array): Promise<webcrypto.CryptoKey> {\n const keyMaterial = await getMachineKeyMaterial();\n return webcrypto.subtle.deriveKey(\n { name: \"PBKDF2\", salt, iterations: PBKDF2_ITERATIONS, hash: \"SHA-256\" },\n keyMaterial,\n { name: \"AES-GCM\", length: 256 },\n false,\n [\"encrypt\", \"decrypt\"]\n );\n}\n\n// ---------------------------------------------------------------------------\n// Encryption helpers\n// ---------------------------------------------------------------------------\n\nconst hex = (buf: ArrayBuffer | Uint8Array): string =>\n Buffer.from(buf instanceof Uint8Array ? buf : new Uint8Array(buf)).toString(\"hex\");\n\nasync function encrypt(plaintext: string): Promise<string> {\n const salt = randomBytes(16);\n const iv = randomBytes(12);\n const masterKey = await getOrCreateMasterKey();\n const key = masterKey\n ? await deriveKeychainKey(masterKey, salt)\n : await deriveMachineKey(salt);\n const cipherBuf = await webcrypto.subtle.encrypt(\n { name: \"AES-GCM\", iv },\n key,\n new TextEncoder().encode(plaintext)\n );\n const body = [hex(salt), hex(iv), hex(cipherBuf)].join(\":\");\n // Keychain-scheme entries are tagged so decrypt() can route; machine-scheme\n // entries stay in the legacy unprefixed format for backward compatibility.\n return masterKey ? `${SCHEME_KEYCHAIN}:${body}` : body;\n}\n\nasync function decryptWith(\n key: webcrypto.CryptoKey,\n ivHex: string,\n cipherHex: string\n): Promise<string> {\n const plainBuf = await webcrypto.subtle.decrypt(\n { name: \"AES-GCM\", iv: Buffer.from(ivHex, \"hex\") },\n key,\n Buffer.from(cipherHex, \"hex\")\n );\n return new TextDecoder().decode(plainBuf);\n}\n\nasync function decrypt(stored: string): Promise<string> {\n const parts = stored.split(\":\");\n // Keychain scheme: \"k1:<salt>:<iv>:<ct>\"\n if (parts.length === 4 && parts[0] === SCHEME_KEYCHAIN) {\n const [, saltHex, ivHex, cipherHex] = parts;\n const masterKey = await readMasterKey();\n if (!masterKey) {\n throw new Error(\n \"Cannot decrypt: this secret is protected by the OS keychain master key, \" +\n \"which is unavailable on this machine/account (or the keychain entry was removed).\"\n );\n }\n const key = await deriveKeychainKey(masterKey, Buffer.from(saltHex, \"hex\"));\n return decryptWith(key, ivHex, cipherHex);\n }\n // Legacy machine scheme: \"<salt>:<iv>:<ct>\"\n if (parts.length === 3) {\n const [saltHex, ivHex, cipherHex] = parts;\n const key = await deriveMachineKey(Buffer.from(saltHex, \"hex\"));\n return decryptWith(key, ivHex, cipherHex);\n }\n throw new Error(\"Invalid ciphertext format\");\n}\n\n// ---------------------------------------------------------------------------\n// Store helpers\n// ---------------------------------------------------------------------------\n\nasync function readStore(): Promise<Record<string, string>> {\n return (await readJson<Record<string, string>>(STORE_FILE)) ?? {};\n}\n\n// Decrypt a single stored value identified by its store key against an\n// already-loaded store snapshot. Shared by the unlocked per-key getSecret and\n// the locked snapshot resolver so both decode entries identically.\nasync function decryptFromSnapshot(\n store: Record<string, string>,\n sk: string\n): Promise<string | null> {\n const stored = store[sk];\n return stored ? decrypt(stored) : null;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\nexport async function setSecret(server: string, key: string, value: string): Promise<void> {\n const sk = validatedStoreKey(server, key);\n // Encrypt outside the lock (it does not depend on stored state) to keep the\n // critical section short, then read-merge-write atomically under the lock.\n const encrypted = await encrypt(value);\n await withStoreLock(async () => {\n const store = await readStore();\n await writeJson(STORE_FILE, { ...store, [sk]: encrypted });\n });\n}\n\n/**\n * Store multiple secrets for one server in a single read-modify-write, so the\n * batch is all-or-nothing: either every value is persisted or none is (no\n * orphaned half-written secrets if one encrypt fails — security review MED-1).\n */\nexport async function setSecrets(\n server: string,\n values: Record<string, string>\n): Promise<void> {\n // Encrypt every value first (no dependency on stored state), then read-merge-\n // write atomically under the lock so a concurrent writer cannot lost-update\n // this batch.\n const encrypted: Record<string, string> = {};\n for (const [key, value] of Object.entries(values)) {\n encrypted[validatedStoreKey(server, key)] = await encrypt(value);\n }\n await withStoreLock(async () => {\n const store = await readStore();\n await writeJson(STORE_FILE, { ...store, ...encrypted });\n });\n}\n\n// Intentionally UNLOCKED: a read-only single-key lookup. It accepts eventual\n// consistency (it may observe a concurrent writer's snapshot before or after a\n// mutation, never a torn one — writeJson swaps the file atomically via rename).\n// The consistency-sensitive path is resolveEnvPlaceholders, which takes the\n// lock and reads one snapshot for all keys.\nexport async function getSecret(server: string, key: string): Promise<string | null> {\n const sk = validatedStoreKey(server, key);\n return decryptFromSnapshot(await readStore(), sk);\n}\n\n/** Returns true if a secret was removed, false if no such secret existed. */\nexport async function deleteSecret(server: string, key: string): Promise<boolean> {\n const sk = validatedStoreKey(server, key);\n let removed = false;\n await withStoreLock(async () => {\n const store = await readStore();\n if (!(sk in store)) return;\n const { [sk]: _removed, ...rest } = store;\n await writeJson(STORE_FILE, rest);\n removed = true;\n });\n return removed;\n}\n\n/** Produces the placeholder string stored in config files. */\nexport function toPlaceholder(server: string, key: string): string {\n return `${PLACEHOLDER_PREFIX}${server}/${key}`;\n}\n\n/** Parses a placeholder string. Returns null if not a placeholder. */\nexport function parsePlaceholder(value: string): { server: string; key: string } | null {\n if (!value.startsWith(PLACEHOLDER_PREFIX)) return null;\n const rest = value.slice(PLACEHOLDER_PREFIX.length);\n const slashIdx = rest.indexOf(\"/\");\n if (slashIdx === -1) return null;\n return { server: rest.slice(0, slashIdx), key: rest.slice(slashIdx + 1) };\n}\n\n/**\n * Resolve any `mcpm:keychain:server/KEY` placeholder values in an env map to\n * their decrypted secrets. Non-placeholder values pass through unchanged;\n * `undefined` values are dropped. Throws if a placeholder references a secret\n * that is not stored.\n *\n * The decrypted values exist only in the returned in-memory object — they are\n * never written to disk. `mcpm guard run --inner` calls this to inject secrets\n * into a wrapped server's child process without storing plaintext in client\n * config files.\n *\n * Reads the store as a single CONSISTENT SNAPSHOT under `withStoreLock`: all\n * placeholders are resolved against one read taken while holding the same lock\n * the write paths (setSecret/setSecrets/deleteSecret) hold. This closes the\n * read-after-delete race where a concurrent `secrets delete` during guard\n * startup made a per-key unlocked lookup observe a torn state and throw \"Secret\n * not found\" — the exact race the lock was added to prevent. This is the only\n * caller (guard/run-inner.ts), invoked at the top level and NOT from inside an\n * already-held store lock, so acquiring the lock here cannot self-deadlock.\n */\nexport async function resolveEnvPlaceholders(\n env: NodeJS.ProcessEnv\n): Promise<Record<string, string>> {\n // Parse placeholders up front so the locked critical section is just one read\n // plus decryption — no validation or iteration over non-placeholder values.\n const passthrough: Record<string, string> = {};\n const placeholders: Array<{ name: string; server: string; key: string }> = [];\n for (const [name, value] of Object.entries(env)) {\n if (value === undefined) continue;\n const placeholder = parsePlaceholder(value);\n if (placeholder === null) {\n passthrough[name] = value;\n continue;\n }\n placeholders.push({ name, ...placeholder });\n }\n\n // No secrets to resolve — skip the lock entirely.\n if (placeholders.length === 0) return passthrough;\n\n return withStoreLock(async () => {\n const store = await readStore();\n const resolved: Record<string, string> = { ...passthrough };\n for (const { name, server, key } of placeholders) {\n const sk = validatedStoreKey(server, key);\n const secret = await decryptFromSnapshot(store, sk);\n if (secret === null) {\n throw new Error(\n `Secret \"${server}/${key}\" not found. ` +\n `Run \\`mcpm secrets set ${server} ${key}\\` to store it.`\n );\n }\n resolved[name] = secret;\n }\n return resolved;\n });\n}\n\n/**\n * Derive a keychain-safe server id from a (possibly slash-containing) server\n * name. Registry ids like \"io.github.owner/repo\" contain `/`, which is invalid\n * for a keychain id (assertSafeId) and would break placeholder parsing (which\n * splits on the first `/`).\n *\n * The sanitised prefix keeps the id human-recognisable; a sha256 suffix makes\n * the mapping INJECTIVE, so two names that differ only in unsafe characters\n * (e.g. \"owner/repo\" vs \"owner_repo\") can never collide into one secret\n * namespace (security review CRIT-1). Deterministic: the same name always maps\n * to the same id, so a placeholder written at install resolves at launch.\n */\nexport function deriveKeychainId(name: string): string {\n const sanitized = name.replace(/[^a-zA-Z0-9._-]/g, \"_\").slice(0, 200);\n const hash = createHash(\"sha256\").update(name).digest(\"hex\").slice(0, 12);\n return `${sanitized}-${hash}`;\n}\n\n/**\n * List all stored secrets grouped by server name. Returns only key names —\n * decrypted values are never read or returned.\n *\n * Intentionally UNLOCKED: read-only enumeration, eventual consistency.\n */\nexport async function listAll(): Promise<Record<string, string[]>> {\n const store = await readStore();\n const grouped: Record<string, string[]> = {};\n for (const storeKey of Object.keys(store)) {\n const slashIdx = storeKey.indexOf(\"/\");\n if (slashIdx === -1) continue;\n const server = storeKey.slice(0, slashIdx);\n const key = storeKey.slice(slashIdx + 1);\n (grouped[server] ??= []).push(key);\n }\n return grouped;\n}\n\n// ---------------------------------------------------------------------------\n// Backend status + migration (security #15)\n// ---------------------------------------------------------------------------\n\n/**\n * Which backend protected the most recent write — a backward-looking reflection,\n * not a forward-looking prediction. Returns \"os-keychain\" only when a master key\n * is actually present. Intended to be called AFTER a `setSecret`/`migrate` (as\n * `mcpm secrets set` does), where it accurately reports the scheme just used —\n * including reporting \"machine-key\" when a keychain write silently failed and\n * fell back. Called standalone before any write on a keychain-capable machine\n * with no key yet, it returns \"machine-key\" (no key created yet); the next write\n * would create one and use \"os-keychain\".\n */\nexport async function activeSecretBackend(): Promise<\"os-keychain\" | \"machine-key\"> {\n return (await readMasterKey()) !== null ? \"os-keychain\" : \"machine-key\";\n}\n\n/**\n * Re-encrypt every legacy machine-scheme entry under the OS keychain master\n * key, so existing secrets gain the same exfiltration resistance as new ones.\n *\n * No-op (`usingKeychain: false`) when no OS keychain is available. Per-entry\n * failures are isolated: a legacy entry this machine key can no longer decrypt\n * (e.g. written on a different machine) is counted in `failed` and left\n * untouched rather than aborting the whole migration.\n */\nexport async function migrateToKeychain(): Promise<{\n migrated: number;\n failed: number;\n total: number;\n usingKeychain: boolean;\n}> {\n const masterKey = await getOrCreateMasterKey();\n if (!masterKey) return { migrated: 0, failed: 0, total: 0, usingKeychain: false };\n return withStoreLock(async () => {\n const store = await readStore();\n const entries = Object.entries(store);\n const next: Record<string, string> = {};\n let migrated = 0;\n let failed = 0;\n for (const [sk, value] of entries) {\n const isLegacy = value.split(\":\").length === 3;\n if (!isLegacy) {\n next[sk] = value; // already keychain-scheme (or unknown) — leave as-is\n continue;\n }\n try {\n const plain = await decrypt(value); // legacy machine scheme\n next[sk] = await encrypt(plain); // re-encrypt under the keychain master key\n migrated++;\n } catch {\n next[sk] = value; // undecryptable legacy entry — leave untouched\n failed++;\n }\n }\n if (migrated > 0) await writeJson(STORE_FILE, next);\n return { migrated, failed, total: entries.length, usingKeychain: true };\n });\n}\n\n/**\n * How secret-flagged env vars are persisted.\n * - \"plaintext\": written directly into the client config (legacy default).\n * - \"keychain\": stored AES-GCM-encrypted; config gets a `mcpm:keychain:…`\n * placeholder that mcpm guard resolves at launch.\n */\nexport type SecretsMode = \"plaintext\" | \"keychain\";\n\n/**\n * Resolve a server's env map for writing to a client config under the given\n * secrets mode. In \"keychain\" mode, every key for which `isSecret(key)` is true\n * is stored encrypted via `setSecret` and replaced with a `mcpm:keychain:…`\n * placeholder; all other values pass through. In \"plaintext\" mode the input is\n * returned unchanged. This is the single place the \"no plaintext secret in\n * config\" invariant is enforced — install and up both go through it.\n *\n * Throws if keychain mode is requested without a `setSecret` implementation.\n */\nexport async function applyKeychainSecrets(opts: {\n serverName: string;\n resolvedEnv: Record<string, string>;\n isSecret: (key: string) => boolean;\n mode: SecretsMode;\n setSecrets?: (server: string, values: Record<string, string>) => Promise<void>;\n}): Promise<{ env: Record<string, string>; storedCount: number }> {\n if (opts.mode !== \"keychain\") {\n return { env: opts.resolvedEnv, storedCount: 0 };\n }\n if (!opts.setSecrets) {\n throw new Error(\"Keychain secret storage is unavailable.\");\n }\n const keychainId = deriveKeychainId(opts.serverName);\n const env: Record<string, string> = {};\n const toStore: Record<string, string> = {};\n for (const [key, value] of Object.entries(opts.resolvedEnv)) {\n if (opts.isSecret(key)) {\n toStore[key] = value;\n env[key] = toPlaceholder(keychainId, key);\n } else {\n env[key] = value;\n }\n }\n const storedCount = Object.keys(toStore).length;\n // Persist all secrets in one atomic batch BEFORE returning the env that the\n // caller writes to config — so we never write a placeholder for a secret that\n // failed to store (all-or-nothing; security review MED-1).\n if (storedCount > 0) {\n await opts.setSecrets(keychainId, toStore);\n }\n return { env, storedCount };\n}\n\n/**\n * Return the keys of `env` whose value is a `mcpm:keychain:…` placeholder.\n * Used by `mcpm guard disable` to warn about secrets that will no longer\n * resolve once guard stops wrapping the server.\n */\nexport function placeholderEnvKeys(env: Record<string, string> | undefined): string[] {\n if (!env) return [];\n return Object.entries(env)\n .filter(([, v]) => typeof v === \"string\" && parsePlaceholder(v) !== null)\n .map(([k]) => k);\n}\n","/**\n * Zero-native-dependency access to the operating system's credential store.\n *\n * The secret store (store/keychain.ts) holds a single random 32-byte *master\n * key* here; every stored secret is AES-GCM-encrypted with a subkey derived\n * from it. Keeping the master key in the OS credential store — not on disk —\n * is what makes a copied `~/.mcpm/secrets.enc.json` undecryptable off-machine\n * (security issue #15).\n *\n * No native modules (no `keytar`/node-gyp). We shell out to the platform's\n * built-in tooling:\n * - macOS: `security` (login Keychain) — generic password item\n * - Linux: `secret-tool` (libsecret/Secret Service) — schema attributes\n * - Windows: DPAPI via PowerShell `ProtectedData` — blob in ~/.mcpm\n *\n * Every operation is best-effort: if the platform tool is missing, the Secret\n * Service is unavailable (headless Linux, CI), or any call fails, the function\n * resolves to \"unavailable\"/null and the caller falls back to the legacy\n * machine-derived key (casual-inspection only — see store/keychain.ts).\n *\n * Set `MCPM_DISABLE_OS_KEYCHAIN=1` to force the fallback (used by the test\n * suite so it never touches the developer's real Keychain, and available to\n * users who prefer not to use the OS store).\n */\n\nimport { spawn } from \"node:child_process\";\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { getStorePath } from \"./index.js\";\n\nconst SERVICE = \"mcpm\";\nconst ACCOUNT = \"secret-store-master-key\";\nconst DPAPI_BLOB_FILE = \"master-key.dpapi\";\nconst EXEC_TIMEOUT_MS = 5_000;\n\n/** Result of a child-process run. `code === null` means spawn failed (ENOENT). */\ninterface RunResult {\n code: number | null;\n stdout: string;\n stderr: string;\n}\n\n/**\n * Run a command to completion, optionally feeding `input` to stdin. Never\n * rejects: a missing binary (ENOENT) or any spawn error resolves to\n * `{ code: null }` so callers treat it uniformly as \"unavailable\".\n */\nfunction run(\n command: string,\n args: string[],\n opts: { input?: string; env?: NodeJS.ProcessEnv } = {}\n): Promise<RunResult> {\n return new Promise((resolve) => {\n const child = spawn(command, args, {\n timeout: EXEC_TIMEOUT_MS,\n env: opts.env ?? process.env,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n let stdout = \"\";\n let stderr = \"\";\n // `error` (ENOENT/EACCES) and `close` can both fire for one spawn; settle\n // exactly once. `close` (not `exit`) is used so stdout is fully drained\n // before we resolve.\n let settled = false;\n const settle = (r: RunResult): void => {\n if (settled) return;\n settled = true;\n resolve(r);\n };\n child.stdout.on(\"data\", (d) => (stdout += d.toString()));\n child.stderr.on(\"data\", (d) => (stderr += d.toString()));\n child.on(\"error\", () => settle({ code: null, stdout, stderr }));\n child.on(\"close\", (code) => settle({ code, stdout, stderr }));\n if (opts.input !== undefined) {\n child.stdin.on(\"error\", () => {\n /* child may have exited before stdin flush; swallow EPIPE */\n });\n child.stdin.end(opts.input);\n } else {\n child.stdin.end();\n }\n });\n}\n\n/** True on a platform we know how to drive (and not explicitly disabled). */\nexport function isSupportedPlatform(): boolean {\n if (process.env.MCPM_DISABLE_OS_KEYCHAIN === \"1\") return false;\n return (\n process.platform === \"darwin\" ||\n process.platform === \"linux\" ||\n process.platform === \"win32\"\n );\n}\n\n// ---------------------------------------------------------------------------\n// macOS — security(1) generic-password items in the login Keychain\n// ---------------------------------------------------------------------------\n\nasync function darwinGet(): Promise<Buffer | null> {\n // -w prints only the password; exit 44 when the item does not exist.\n const r = await run(\"security\", [\n \"find-generic-password\",\n \"-a\",\n ACCOUNT,\n \"-s\",\n SERVICE,\n \"-w\",\n ]);\n if (r.code !== 0) return null;\n return decodeKey(r.stdout.trim());\n}\n\nasync function darwinStore(keyB64: string): Promise<boolean> {\n // -U updates the item if it already exists instead of erroring.\n //\n // Tradeoff: `security` has no reliable non-interactive stdin path for the\n // password, so the (base64) master key is passed in argv and is briefly\n // visible to a *same-user* `ps` during the child's lifetime. This is a narrow,\n // write-only window: the read path (darwinGet) returns the key on stdout to\n // this parent only, and cross-user argv is not readable. A same-user attacker\n // who can `ps` can already read this process's memory, so this does not widen\n // the trust boundary the keychain establishes (off-machine file exfiltration).\n const r = await run(\"security\", [\n \"add-generic-password\",\n \"-a\",\n ACCOUNT,\n \"-s\",\n SERVICE,\n \"-U\",\n \"-w\",\n keyB64,\n ]);\n return r.code === 0;\n}\n\n// ---------------------------------------------------------------------------\n// Linux — secret-tool (libsecret). Value is read from / written to stdin.\n// ---------------------------------------------------------------------------\n\nasync function linuxGet(): Promise<Buffer | null> {\n const r = await run(\"secret-tool\", [\"lookup\", \"service\", SERVICE, \"account\", ACCOUNT]);\n if (r.code !== 0) return null;\n const value = r.stdout.trim(); // consistent with darwin/windows; tolerate \\r\\n / spaces\n if (value.length === 0) return null;\n return decodeKey(value);\n}\n\nasync function linuxStore(keyB64: string): Promise<boolean> {\n const r = await run(\n \"secret-tool\",\n [\"store\", \"--label=mcpm secret store master key\", \"service\", SERVICE, \"account\", ACCOUNT],\n { input: keyB64 }\n );\n return r.code === 0;\n}\n\n// ---------------------------------------------------------------------------\n// Windows — DPAPI (CurrentUser scope) via PowerShell. The protected blob is\n// stored in ~/.mcpm; DPAPI ties decryption to the Windows user account, so a\n// copied blob cannot be unprotected by another account or on another machine.\n// The plaintext key is passed through an env var, never argv (process list).\n// ---------------------------------------------------------------------------\n\nconst PS_PROTECT =\n \"$ErrorActionPreference='Stop';\" +\n \"Add-Type -AssemblyName System.Security;\" +\n \"$b=[Convert]::FromBase64String($env:MCPM_KEY_B64);\" +\n \"$p=[Security.Cryptography.ProtectedData]::Protect($b,$null,'CurrentUser');\" +\n \"[Convert]::ToBase64String($p)\";\n\nconst PS_UNPROTECT =\n \"$ErrorActionPreference='Stop';\" +\n \"Add-Type -AssemblyName System.Security;\" +\n \"$b=[Convert]::FromBase64String($env:MCPM_BLOB_B64);\" +\n \"$p=[Security.Cryptography.ProtectedData]::Unprotect($b,$null,'CurrentUser');\" +\n \"[Convert]::ToBase64String($p)\";\n\nasync function blobPath(): Promise<string> {\n return path.join(await getStorePath(), DPAPI_BLOB_FILE);\n}\n\nasync function windowsGet(): Promise<Buffer | null> {\n let blob: string;\n try {\n blob = (await readFile(await blobPath(), \"utf8\")).trim();\n } catch {\n return null; // no blob stored yet\n }\n if (blob.length === 0) return null;\n const r = await run(\"powershell\", [\"-NoProfile\", \"-NonInteractive\", \"-Command\", PS_UNPROTECT], {\n env: { ...process.env, MCPM_BLOB_B64: blob },\n });\n if (r.code !== 0) return null;\n return decodeKey(r.stdout.trim());\n}\n\nasync function windowsStore(keyB64: string): Promise<boolean> {\n const r = await run(\"powershell\", [\"-NoProfile\", \"-NonInteractive\", \"-Command\", PS_PROTECT], {\n env: { ...process.env, MCPM_KEY_B64: keyB64 },\n });\n if (r.code !== 0 || r.stdout.trim().length === 0) return false;\n try {\n await writeFile(await blobPath(), r.stdout.trim(), { mode: 0o600 });\n return true;\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Shared helpers + public dispatch\n// ---------------------------------------------------------------------------\n\n/** Decode a base64 master key, rejecting anything that is not exactly 32 bytes. */\nfunction decodeKey(b64: string): Buffer | null {\n try {\n const buf = Buffer.from(b64, \"base64\");\n return buf.length === 32 ? buf : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Read the stored master key, or null if none is stored / the store is\n * unavailable. Never creates a key.\n */\nexport async function getStoredKey(): Promise<Buffer | null> {\n if (!isSupportedPlatform()) return null;\n switch (process.platform) {\n case \"darwin\":\n return darwinGet();\n case \"linux\":\n return linuxGet();\n case \"win32\":\n return windowsGet();\n default:\n return null;\n }\n}\n\n/**\n * Persist `key` (exactly 32 bytes) in the OS credential store. Returns true on\n * success, false if the store is unavailable or the write failed.\n */\nexport async function storeKey(key: Buffer): Promise<boolean> {\n if (!isSupportedPlatform()) return false;\n if (key.length !== 32) return false;\n const keyB64 = key.toString(\"base64\");\n switch (process.platform) {\n case \"darwin\":\n return darwinStore(keyB64);\n case \"linux\":\n return linuxStore(keyB64);\n case \"win32\":\n return windowsStore(keyB64);\n default:\n return false;\n }\n}\n"],"mappings":"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","names":["storeKey"]}
#!/usr/bin/env node
import {
coloredOutput
} from "./chunk-E3T224S3.js";
import {
isConfineBackendAvailable,
isWrapped
} from "./chunk-WYSMWP2R.js";
import {
sanitizeForTerminal
} from "./chunk-FEXJHHDM.js";
import {
getAdapter
} from "./chunk-W4IAFBUN.js";
import {
isSupportedPlatform,
parsePlaceholder
} from "./chunk-GZ3WCRLG.js";
import {
CLIENT_IDS,
getConfigPath
} from "./chunk-R4R2VPDA.js";
import {
detectSecretLabels
} from "./chunk-U7N6FRYF.js";
// src/utils/format-entry.ts
function formatMcpEntryCommand(entry, fallback = "\u2014") {
if (entry.url) return entry.url;
if (entry.command) {
const args = entry.args?.join(" ") ?? "";
return args ? `${entry.command} ${args}` : entry.command;
}
return fallback;
}
// src/commands/doctor.ts
import { access } from "fs/promises";
// src/config/drift.ts
async function collectClientStates(deps) {
const clients = await deps.detectClients();
const states = [];
for (const clientId of clients) {
try {
const servers = await deps.getAdapter(clientId).read(deps.getPath(clientId));
states.push({ clientId, servers });
} catch {
}
}
return states;
}
function fieldProjection(entry) {
return {
command: entry.command ?? "",
args: JSON.stringify(entry.args ?? []),
"env keys": JSON.stringify(Object.keys(entry.env ?? {}).sort()),
url: entry.url ?? "",
"header keys": JSON.stringify(Object.keys(entry.headers ?? {}).sort())
};
}
var COMPARED_FIELDS = ["command", "args", "env keys", "url", "header keys"];
function divergingFields(entries) {
const projections = entries.map(fieldProjection);
return COMPARED_FIELDS.filter((field) => {
const distinct = new Set(projections.map((p) => p[field]));
return distinct.size > 1;
});
}
function buildDriftModel(states) {
const clients = states.map((s) => s.clientId).sort();
const byName = /* @__PURE__ */ new Map();
for (const { clientId, servers: servers2 } of states) {
for (const [name, entry] of Object.entries(servers2)) {
const list = byName.get(name) ?? [];
list.push({ clientId, entry });
byName.set(name, list);
}
}
const servers = [];
for (const name of [...byName.keys()].sort()) {
const holders = byName.get(name);
const present = holders.map((h) => h.clientId).sort();
const presentSet = new Set(present);
const absent = clients.filter((c) => !presentSet.has(c));
const fields = holders.length > 1 ? divergingFields(holders.map((h) => h.entry)) : [];
const conflict = fields.length > 0;
servers.push({
name,
present,
absent,
conflict,
...conflict ? { conflictFields: fields } : {}
});
}
const drifted = servers.filter((s) => s.absent.length > 0 || s.conflict).length;
return { clients, servers, inSync: servers.length - drifted, drifted };
}
// src/scanner/config-secrets.ts
var GENERIC_LABEL = "secret-named key holds a plaintext value";
var SECRET_KEY_RE = /(?:^|_)(?:PASSWORD|PASSWD|PASSPHRASE|SECRET|TOKEN|PAT|APIKEY|AUTHORIZATION|CREDENTIALS?|(?:API|ACCESS|PRIVATE|SECRET|SESSION|SIGNING|ENCRYPTION)_KEY)(?:_|$)/;
var NON_SECRET_QUALIFIER_RE = /(?:^|_)(?:URL|URI|ENDPOINT|HOST|PORT|ID|NAME|PATH|FILE|DIR|ENABLED|DISABLED|TYPE|MODE|REGION|TIMEOUT|VERSION|PUBLIC|FORMAT|HEADER|PREFIX|SUFFIX|COUNT|SIZE|TTL|EXPIRY|EXPIRES|ISSUER|AUDIENCE|ALGORITHM|ALG|SCOPE|METHOD)(?:_|$)/;
function normalizeKey(key) {
return key.toUpperCase().replace(/-/g, "_");
}
function keyLooksSecret(key) {
const k = normalizeKey(key);
return SECRET_KEY_RE.test(k) && !NON_SECRET_QUALIFIER_RE.test(k);
}
function valueLooksPlaintextSecret(value) {
const v = value.trim();
if (v.length < 6) return false;
if (parsePlaceholder(value) !== null) return false;
if (/\$\{[^}]*\}/.test(v)) return false;
if (/^\$[A-Za-z_]/.test(v)) return false;
if (/^%[A-Za-z_][A-Za-z0-9_]*%([\\/].*)?$/.test(v)) return false;
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(v)) return false;
if (/^[~./]/.test(v) || /^[A-Za-z]:[\\/]/.test(v) || /^\\\\/.test(v)) return false;
if (/^(true|false|\d+)$/i.test(v)) return false;
return true;
}
function scanMap(server, field, map) {
if (!map) return [];
const out = [];
for (const [key, value] of Object.entries(map)) {
if (typeof value !== "string") continue;
if (parsePlaceholder(value) !== null) continue;
const labels = detectSecretLabels(value);
if (labels.length > 0) {
out.push({ server, field, key, label: labels.join(", ") });
continue;
}
if (keyLooksSecret(key) && valueLooksPlaintextSecret(value)) {
out.push({ server, field, key, label: GENERIC_LABEL });
}
}
return out;
}
function scanServerConfigSecrets(server, entry) {
return [...scanMap(server, "env", entry.env), ...scanMap(server, "header", entry.headers)];
}
function scanConfigSecrets(servers) {
return Object.entries(servers).flatMap(([name, entry]) => scanServerConfigSecrets(name, entry));
}
// src/commands/doctor.ts
import "commander";
import os from "os";
import { execFile } from "child_process";
var RUNTIMES = ["npx", "uvx", "docker"];
var CLIENT_LABELS = {
"claude-desktop": "Claude Desktop",
"claude-code": "Claude Code",
cursor: "Cursor",
vscode: "VS Code",
windsurf: "Windsurf",
"gemini-cli": "Gemini CLI"
};
var RUNTIME_INSTALL_HINTS = {
npx: "install Node.js from https://nodejs.org",
uvx: "install uv from https://docs.astral.sh/uv/",
docker: "install Docker from https://docs.docker.com/get-docker/"
};
async function buildDoctorModel(deps) {
const { getAdapter: getAdapter2, getConfigPath: getConfigPath2, checkConfigExists, execCheck } = deps;
const reads = await Promise.all(
CLIENT_IDS.map(async (clientId) => {
const exists = await checkConfigExists(clientId);
if (!exists) return { clientId, read: { exists: false, malformed: false, servers: null } };
try {
const servers = await getAdapter2(clientId).read(getConfigPath2(clientId));
return { clientId, read: { exists: true, malformed: false, servers } };
} catch {
return { clientId, read: { exists: true, malformed: true, servers: null } };
}
})
);
const issues = [];
const clients = reads.map(({ clientId, read }) => {
const label = CLIENT_LABELS[clientId];
if (read.malformed) {
issues.push({
kind: "malformed-config",
message: `Config file for ${label} is malformed \u2014 fix the JSON syntax.`
});
}
const servers = read.servers ?? {};
const entries = Object.values(servers);
return {
id: clientId,
label,
exists: read.exists,
malformed: read.malformed,
serverCount: entries.length,
guardedCount: entries.filter(isWrapped).length
};
});
const runtimes = await Promise.all(
RUNTIMES.map(async (name) => ({ name, available: await execCheck(name) }))
);
const runtimeAvailable = new Map(runtimes.map((r) => [r.name, r.available]));
for (const { clientId, read } of reads) {
if (!read.servers) continue;
for (const [serverName, entry] of Object.entries(read.servers)) {
const cmd = entry.command;
if (!cmd) continue;
if (RUNTIMES.includes(cmd) && runtimeAvailable.get(cmd) === false) {
issues.push({
kind: "missing-runtime",
message: `Server '${serverName}' in ${CLIENT_LABELS[clientId]} uses '${cmd}' but ${cmd} is not installed.`
});
}
}
}
const driftStates = reads.flatMap(
({ clientId, read }) => read.servers ? [{ clientId, servers: read.servers }] : []
);
const crossClient = driftStates.length >= 2 ? toCrossClient(driftStates) : null;
const secrets = reads.flatMap(
({ clientId, read }) => read.servers ? scanConfigSecrets(read.servers).map((f) => ({ client: clientId, ...f })) : []
);
return {
schemaVersion: 1,
clients,
runtimes,
crossClient,
secrets,
issues,
ok: issues.length === 0
};
}
function toCrossClient(states) {
const drift = buildDriftModel(states);
const entries = [];
for (const server of drift.servers) {
if (server.conflict) {
entries.push({
name: server.name,
kind: "conflict",
present: [...server.present],
absent: [...server.absent],
fields: server.conflictFields ? [...server.conflictFields] : void 0
});
} else if (server.absent.length > 0) {
entries.push({
name: server.name,
kind: "absent",
present: [...server.present],
absent: [...server.absent]
});
}
}
return {
consistent: drift.drifted === 0,
clientCount: drift.clients.length,
serverCount: drift.servers.length,
drift: entries
};
}
function renderDoctorText(model, output) {
output("");
output("mcpm doctor");
output("");
for (const c of model.clients) {
if (!c.exists) {
output(` \u2717 ${c.label} \u2014 config not found`);
} else if (c.malformed) {
output(` \u2717 ${c.label} \u2014 config malformed (JSON parse error)`);
} else {
const word = c.serverCount === 1 ? "server" : "servers";
output(` \u2713 ${c.label} \u2014 config found, ${c.serverCount} ${word}`);
}
}
output("");
output("Runtimes:");
for (const r of model.runtimes) {
if (r.available) {
output(` \u2713 ${r.name} available`);
} else {
output(` \u2717 ${r.name} not found \u2014 ${RUNTIME_INSTALL_HINTS[r.name]}`);
}
}
if (model.crossClient) {
const cc = model.crossClient;
output("");
output("Cross-client (advisory):");
if (cc.consistent) {
const word = cc.serverCount === 1 ? "server" : "servers";
output(` \u2713 ${cc.serverCount} ${word} consistent across ${cc.clientCount} clients`);
} else {
for (const d of cc.drift) {
if (d.kind === "conflict") {
output(` \u26A0 ${d.name} \u2014 config differs (${d.fields.join(", ")}) across ${d.present.join(", ")}`);
} else {
output(` \u26A0 ${d.name} \u2014 in ${d.present.join(", ")}; missing in ${d.absent.join(", ")}`);
}
}
output(" Run `mcpm sync --check` for the full matrix (advisory, not a failure).");
}
}
if (model.secrets.length > 0) {
output("");
output("Plaintext secrets (advisory):");
for (const s of model.secrets) {
output(
` \u26A0 ${s.client} \xB7 ${sanitizeForTerminal(s.server)} \xB7 ${s.field} '${sanitizeForTerminal(s.key)}' \u2014 ${s.label}`
);
}
if (model.secrets.some((s) => s.field === "env")) {
output(
" Move env secrets to the encrypted store: `mcpm secrets set <server> <KEY>` or re-install with `--secrets keychain`."
);
}
if (model.secrets.some((s) => s.field === "header")) {
output(
" Header secrets have no keychain path yet \u2014 rotate the credential and keep it out of committed config."
);
}
}
if (model.issues.length > 0) {
output("");
output("Issues:");
for (const issue of model.issues) {
output(` \u26A0 ${issue.message}`);
}
output("");
output("Critical issues found. Run the commands above to resolve them.");
return;
}
output("");
output("No critical issues found.");
}
function buildDoctorReport(model, env) {
return {
schemaVersion: 1,
mcpm: env.mcpm,
node: env.node,
os: `${env.platform} ${env.arch} ${env.osRelease}`,
confineBackend: env.confineBackend,
secretStore: env.secretStore,
// Redaction: drop the label + every server name; keep only counts.
clients: model.clients.map(({ id, exists, malformed, serverCount, guardedCount }) => ({
id,
exists,
malformed,
serverCount,
guardedCount
})),
runtimes: model.runtimes,
issues: {
malformedConfigs: model.issues.filter((i) => i.kind === "malformed-config").length,
missingRuntime: model.issues.filter((i) => i.kind === "missing-runtime").length,
plaintextSecrets: model.secrets.length
}
};
}
function renderReportText(r) {
const lines = [];
lines.push("mcpm doctor --report (redacted \u2014 no server names or args)");
lines.push(`mcpm: ${r.mcpm}`);
lines.push(`node: ${r.node}`);
lines.push(`os: ${r.os}`);
lines.push(`confine backend: ${r.confineBackend ? "available" : "unavailable"}`);
lines.push(`secret store: ${r.secretStore}`);
lines.push("");
lines.push("clients:");
for (const c of r.clients) {
if (!c.exists) {
lines.push(` ${c.id}: not found`);
} else if (c.malformed) {
lines.push(` ${c.id}: config malformed`);
} else {
const guarded = c.guardedCount > 0 ? `, ${c.guardedCount} guarded` : "";
lines.push(` ${c.id}: ${c.serverCount} servers${guarded}`);
}
}
lines.push("runtimes:");
for (const rt of r.runtimes) {
lines.push(` ${rt.name}: ${rt.available ? "available" : "missing"}`);
}
lines.push(
`issues: ${r.issues.malformedConfigs} malformed config(s), ${r.issues.missingRuntime} missing-runtime, ${r.issues.plaintextSecrets} plaintext secret(s)`
);
return lines.join("\n");
}
async function doctorHandler(deps, opts = {}) {
const model = await buildDoctorModel(deps);
if (opts.report) {
const env = opts.reportEnv ?? gatherReportEnv();
deps.output(renderReportText(buildDoctorReport(model, env)));
} else if (opts.json) {
deps.output(JSON.stringify(model, null, 2));
} else {
renderDoctorText(model, deps.output);
}
return model.ok ? 0 : 1;
}
function makeCheckConfigExists(getConfigPathFn) {
return async (clientId) => {
try {
await access(getConfigPathFn(clientId));
return true;
} catch {
return false;
}
};
}
var checkConfigExistsDefault = makeCheckConfigExists(getConfigPath);
var ALLOWED_RUNTIME_CMDS = /* @__PURE__ */ new Set(["npx", "uvx", "docker"]);
function execCheckDefault(cmd) {
if (!ALLOWED_RUNTIME_CMDS.has(cmd)) return Promise.resolve(false);
return new Promise((resolve) => {
const which = process.platform === "win32" ? "where" : "which";
execFile(which, [cmd], (err) => {
resolve(err === null);
});
});
}
function gatherReportEnv() {
return {
mcpm: "0.29.0",
node: process.version,
platform: process.platform,
arch: process.arch,
osRelease: os.release(),
confineBackend: isConfineBackendAvailable(),
secretStore: isSupportedPlatform() ? "os-keychain" : "machine-key"
};
}
function registerDoctorCommand(program) {
program.command("doctor").description("Check MCP setup health and report issues").option("--json", "emit the structured DoctorModel as JSON (shape UNSTABLE; NOT redacted \u2014 includes server names, use --report to share publicly)").option("--report", "emit a redacted, pasteable env snapshot for bug reports (no server names/args)").action(async (options) => {
const plain = options.json || options.report;
const deps = {
getAdapter,
getConfigPath,
checkConfigExists: checkConfigExistsDefault,
execCheck: execCheckDefault,
output: plain ? (t) => console.log(t) : coloredOutput
};
const exitCode = await doctorHandler(deps, { json: options.json, report: options.report });
process.exit(exitCode);
});
}
export {
formatMcpEntryCommand,
collectClientStates,
buildDriftModel,
buildDoctorModel,
makeCheckConfigExists,
execCheckDefault,
registerDoctorCommand
};
//# sourceMappingURL=chunk-RUSWATRY.js.map
{"version":3,"sources":["../src/utils/format-entry.ts","../src/commands/doctor.ts","../src/config/drift.ts","../src/scanner/config-secrets.ts"],"sourcesContent":["/**\n * Shared formatting helpers for McpServerEntry display.\n */\n\nimport type { McpServerEntry } from \"../config/adapters/index.js\";\n\n/**\n * Returns the display string for an MCP server entry's command/URL column.\n *\n * @param entry - The server entry to format.\n * @param fallback - String to return when neither url nor command is present.\n */\nexport function formatMcpEntryCommand(\n entry: McpServerEntry,\n fallback = \"\\u2014\"\n): string {\n if (entry.url) return entry.url;\n if (entry.command) {\n const args = entry.args?.join(\" \") ?? \"\";\n return args ? `${entry.command} ${args}` : entry.command;\n }\n return fallback;\n}\n","/**\n * `mcpm doctor` command handler.\n *\n * Checks MCP setup health and reports issues:\n * - Which AI clients have config files\n * - Whether config files are valid JSON\n * - Which runtimes (npx, uvx, docker) are available\n * - Whether installed servers reference available runtimes\n *\n * Returns 0 for no critical issues, 1 for critical issues.\n * All external dependencies are injected for testability.\n *\n * D7: the check logic is split into a pure `buildDoctorModel` (a structured\n * `DoctorModel`) and renderers. `--json` emits the model; `--report` emits a\n * redacted, name-free env snapshot for bug reports; the MCP-server `handleDoctor`\n * reuses the same model (fixing its formerly-hardcoded `issues: []`).\n */\n\nimport { access } from \"fs/promises\";\nimport type { ClientId } from \"../config/paths.js\";\nimport type { ConfigAdapter, McpServerEntry } from \"../config/adapters/index.js\";\nimport type { getConfigPath } from \"../config/paths.js\";\nimport { buildDriftModel, type ClientState } from \"../config/drift.js\";\nimport { isWrapped } from \"../guard/wrap.js\";\nimport { scanConfigSecrets, type ConfigSecretFinding } from \"../scanner/config-secrets.js\";\nimport { sanitizeForTerminal } from \"../guard/sanitize.js\";\n\n// ---------------------------------------------------------------------------\n// Deps interface\n// ---------------------------------------------------------------------------\n\nexport interface DoctorDeps {\n getAdapter: (clientId: ClientId) => ConfigAdapter;\n getConfigPath: typeof getConfigPath;\n /** Returns true if the config file exists for this client. */\n checkConfigExists: (clientId: ClientId) => Promise<boolean>;\n /** Returns true if the given executable is available on PATH. */\n execCheck: (cmd: string) => Promise<boolean>;\n output: (text: string) => void;\n}\n\n/** The subset of deps the pure model builder needs (no output, no detector). */\nexport type DoctorModelDeps = Pick<\n DoctorDeps,\n \"getAdapter\" | \"getConfigPath\" | \"checkConfigExists\" | \"execCheck\"\n>;\n\n// ---------------------------------------------------------------------------\n// Structured model (D7 — one shape for text/json/report/MCP consumers)\n// ---------------------------------------------------------------------------\n\nexport interface DoctorClientHealth {\n id: ClientId;\n label: string;\n exists: boolean;\n malformed: boolean;\n serverCount: number;\n /** Servers wrapped by the guard relay (subset of serverCount). */\n guardedCount: number;\n}\n\nexport interface DoctorRuntimeHealth {\n name: Runtime;\n available: boolean;\n}\n\nexport interface DoctorDriftEntry {\n name: string;\n kind: \"conflict\" | \"absent\";\n present: string[];\n absent: string[];\n /** Present only for `kind: \"conflict\"`. */\n fields?: string[];\n}\n\nexport interface DoctorCrossClient {\n consistent: boolean;\n clientCount: number;\n serverCount: number;\n drift: DoctorDriftEntry[];\n}\n\nexport interface DoctorIssue {\n kind: \"malformed-config\" | \"missing-runtime\";\n message: string;\n}\n\nexport interface DoctorSecretFinding {\n client: ClientId;\n server: string;\n field: ConfigSecretFinding[\"field\"];\n /** The env var / header NAME — never the value (F9 redaction contract). */\n key: string;\n label: string;\n}\n\nexport interface DoctorModel {\n schemaVersion: 1;\n clients: DoctorClientHealth[];\n runtimes: DoctorRuntimeHealth[];\n /** Advisory cross-client consistency; null when <2 clients have a readable config. */\n crossClient: DoctorCrossClient | null;\n /** Plaintext secrets in client config — advisory (F9); does NOT affect `ok`/exit. */\n secrets: DoctorSecretFinding[];\n /** Critical issues — these drive the exit code. */\n issues: DoctorIssue[];\n /** true iff issues is empty. */\n ok: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst RUNTIMES = [\"npx\", \"uvx\", \"docker\"] as const;\n\ntype Runtime = (typeof RUNTIMES)[number];\n\nconst CLIENT_LABELS: Record<ClientId, string> = {\n \"claude-desktop\": \"Claude Desktop\",\n \"claude-code\": \"Claude Code\",\n cursor: \"Cursor\",\n vscode: \"VS Code\",\n windsurf: \"Windsurf\",\n \"gemini-cli\": \"Gemini CLI\",\n};\n\nconst RUNTIME_INSTALL_HINTS: Record<Runtime, string> = {\n npx: \"install Node.js from https://nodejs.org\",\n uvx: \"install uv from https://docs.astral.sh/uv/\",\n docker: \"install Docker from https://docs.docker.com/get-docker/\",\n};\n\n// ---------------------------------------------------------------------------\n// Model builder (pure — no output)\n// ---------------------------------------------------------------------------\n\ninterface ClientRead {\n exists: boolean;\n malformed: boolean;\n servers: Record<string, McpServerEntry> | null;\n}\n\n/**\n * Runs every health check and returns the structured model. No side effects\n * beyond the injected reads; safe to call from the CLI, `--json`, `--report`,\n * and the MCP `handleDoctor` tool.\n */\nexport async function buildDoctorModel(deps: DoctorModelDeps): Promise<DoctorModel> {\n const { getAdapter, getConfigPath, checkConfigExists, execCheck } = deps;\n\n // 1. Read each known client's config.\n const reads = await Promise.all(\n CLIENT_IDS.map(async (clientId): Promise<{ clientId: ClientId; read: ClientRead }> => {\n const exists = await checkConfigExists(clientId);\n if (!exists) return { clientId, read: { exists: false, malformed: false, servers: null } };\n try {\n const servers = await getAdapter(clientId).read(getConfigPath(clientId));\n return { clientId, read: { exists: true, malformed: false, servers } };\n } catch {\n return { clientId, read: { exists: true, malformed: true, servers: null } };\n }\n })\n );\n\n const issues: DoctorIssue[] = [];\n\n const clients: DoctorClientHealth[] = reads.map(({ clientId, read }) => {\n const label = CLIENT_LABELS[clientId];\n if (read.malformed) {\n issues.push({\n kind: \"malformed-config\",\n message: `Config file for ${label} is malformed — fix the JSON syntax.`,\n });\n }\n const servers = read.servers ?? {};\n const entries = Object.values(servers);\n return {\n id: clientId,\n label,\n exists: read.exists,\n malformed: read.malformed,\n serverCount: entries.length,\n guardedCount: entries.filter(isWrapped).length,\n };\n });\n\n // 2. Runtime availability.\n const runtimes: DoctorRuntimeHealth[] = await Promise.all(\n RUNTIMES.map(async (name) => ({ name, available: await execCheck(name) }))\n );\n const runtimeAvailable = new Map(runtimes.map((r) => [r.name as string, r.available]));\n\n // 3. Cross-check: servers whose command is a tracked-but-unavailable runtime.\n for (const { clientId, read } of reads) {\n if (!read.servers) continue;\n for (const [serverName, entry] of Object.entries(read.servers)) {\n const cmd = entry.command;\n if (!cmd) continue; // HTTP/URL server — no runtime needed.\n if (RUNTIMES.includes(cmd as Runtime) && runtimeAvailable.get(cmd) === false) {\n issues.push({\n kind: \"missing-runtime\",\n message: `Server '${serverName}' in ${CLIENT_LABELS[clientId]} uses '${cmd}' but ${cmd} is not installed.`,\n });\n }\n }\n }\n\n // 4. Cross-client consistency (advisory — never an issue, never fails doctor).\n const driftStates: ClientState[] = reads.flatMap(({ clientId, read }) =>\n read.servers ? [{ clientId, servers: read.servers }] : []\n );\n const crossClient = driftStates.length >= 2 ? toCrossClient(driftStates) : null;\n\n // 5. Plaintext-secret scan (advisory — never an issue, never fails doctor).\n const secrets: DoctorSecretFinding[] = reads.flatMap(({ clientId, read }) =>\n read.servers ? scanConfigSecrets(read.servers).map((f) => ({ client: clientId, ...f })) : []\n );\n\n return {\n schemaVersion: 1,\n clients,\n runtimes,\n crossClient,\n secrets,\n issues,\n ok: issues.length === 0,\n };\n}\n\nfunction toCrossClient(states: ClientState[]): DoctorCrossClient {\n const drift = buildDriftModel(states);\n const entries: DoctorDriftEntry[] = [];\n for (const server of drift.servers) {\n // buildDriftModel returns readonly arrays — copy into the mutable public model.\n if (server.conflict) {\n entries.push({\n name: server.name,\n kind: \"conflict\",\n present: [...server.present],\n absent: [...server.absent],\n fields: server.conflictFields ? [...server.conflictFields] : undefined,\n });\n } else if (server.absent.length > 0) {\n entries.push({\n name: server.name,\n kind: \"absent\",\n present: [...server.present],\n absent: [...server.absent],\n });\n }\n }\n return {\n consistent: drift.drifted === 0,\n clientCount: drift.clients.length,\n serverCount: drift.servers.length,\n drift: entries,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Human-readable renderer (byte-identical to the pre-D7 output)\n// ---------------------------------------------------------------------------\n\nexport function renderDoctorText(model: DoctorModel, output: (text: string) => void): void {\n output(\"\");\n output(\"mcpm doctor\");\n output(\"\");\n\n for (const c of model.clients) {\n if (!c.exists) {\n output(` ✗ ${c.label} — config not found`);\n } else if (c.malformed) {\n output(` ✗ ${c.label} — config malformed (JSON parse error)`);\n } else {\n const word = c.serverCount === 1 ? \"server\" : \"servers\";\n output(` ✓ ${c.label} — config found, ${c.serverCount} ${word}`);\n }\n }\n\n output(\"\");\n output(\"Runtimes:\");\n for (const r of model.runtimes) {\n if (r.available) {\n output(` ✓ ${r.name} available`);\n } else {\n output(` ✗ ${r.name} not found — ${RUNTIME_INSTALL_HINTS[r.name]}`);\n }\n }\n\n if (model.crossClient) {\n const cc = model.crossClient;\n output(\"\");\n output(\"Cross-client (advisory):\");\n if (cc.consistent) {\n const word = cc.serverCount === 1 ? \"server\" : \"servers\";\n output(` ✓ ${cc.serverCount} ${word} consistent across ${cc.clientCount} clients`);\n } else {\n for (const d of cc.drift) {\n if (d.kind === \"conflict\") {\n output(` ⚠ ${d.name} — config differs (${d.fields!.join(\", \")}) across ${d.present.join(\", \")}`);\n } else {\n output(` ⚠ ${d.name} — in ${d.present.join(\", \")}; missing in ${d.absent.join(\", \")}`);\n }\n }\n output(\" Run `mcpm sync --check` for the full matrix (advisory, not a failure).\");\n }\n }\n\n if (model.secrets.length > 0) {\n output(\"\");\n output(\"Plaintext secrets (advisory):\");\n for (const s of model.secrets) {\n // s.server / s.key are attacker-influenceable (registry env-var names, imported\n // configs) — strip ANSI/OSC so a crafted key can't erase or spoof the advisory.\n output(\n ` ⚠ ${s.client} · ${sanitizeForTerminal(s.server)} · ${s.field} '${sanitizeForTerminal(s.key)}' — ${s.label}`\n );\n }\n // Remediation is field-specific: the keychain/placeholder path is env-only\n // (guard resolves placeholders in env, not headers; HTTP servers aren't wrapped).\n if (model.secrets.some((s) => s.field === \"env\")) {\n output(\n \" Move env secrets to the encrypted store: `mcpm secrets set <server> <KEY>` or re-install with `--secrets keychain`.\"\n );\n }\n if (model.secrets.some((s) => s.field === \"header\")) {\n output(\n \" Header secrets have no keychain path yet — rotate the credential and keep it out of committed config.\"\n );\n }\n }\n\n if (model.issues.length > 0) {\n output(\"\");\n output(\"Issues:\");\n for (const issue of model.issues) {\n output(` ⚠ ${issue.message}`);\n }\n output(\"\");\n output(\"Critical issues found. Run the commands above to resolve them.\");\n return;\n }\n\n output(\"\");\n output(\"No critical issues found.\");\n}\n\n// ---------------------------------------------------------------------------\n// Redacted report (D7 — pasteable env snapshot, NO server names/args)\n// ---------------------------------------------------------------------------\n\nexport interface DoctorReportEnv {\n mcpm: string;\n node: string;\n platform: string;\n arch: string;\n osRelease: string;\n confineBackend: boolean;\n secretStore: \"os-keychain\" | \"machine-key\";\n}\n\nexport interface DoctorReport {\n schemaVersion: 1;\n mcpm: string;\n node: string;\n os: string;\n confineBackend: boolean;\n secretStore: \"os-keychain\" | \"machine-key\";\n clients: Array<Omit<DoctorClientHealth, \"label\">>;\n runtimes: DoctorRuntimeHealth[];\n /** Counts only — issue messages + secret keys embed server names, so NOT included. */\n issues: { malformedConfigs: number; missingRuntime: number; plaintextSecrets: number };\n}\n\nexport function buildDoctorReport(model: DoctorModel, env: DoctorReportEnv): DoctorReport {\n return {\n schemaVersion: 1,\n mcpm: env.mcpm,\n node: env.node,\n os: `${env.platform} ${env.arch} ${env.osRelease}`,\n confineBackend: env.confineBackend,\n secretStore: env.secretStore,\n // Redaction: drop the label + every server name; keep only counts.\n clients: model.clients.map(({ id, exists, malformed, serverCount, guardedCount }) => ({\n id,\n exists,\n malformed,\n serverCount,\n guardedCount,\n })),\n runtimes: model.runtimes,\n issues: {\n malformedConfigs: model.issues.filter((i) => i.kind === \"malformed-config\").length,\n missingRuntime: model.issues.filter((i) => i.kind === \"missing-runtime\").length,\n plaintextSecrets: model.secrets.length,\n },\n };\n}\n\nexport function renderReportText(r: DoctorReport): string {\n const lines: string[] = [];\n lines.push(\"mcpm doctor --report (redacted — no server names or args)\");\n lines.push(`mcpm: ${r.mcpm}`);\n lines.push(`node: ${r.node}`);\n lines.push(`os: ${r.os}`);\n lines.push(`confine backend: ${r.confineBackend ? \"available\" : \"unavailable\"}`);\n lines.push(`secret store: ${r.secretStore}`);\n lines.push(\"\");\n lines.push(\"clients:\");\n for (const c of r.clients) {\n if (!c.exists) {\n lines.push(` ${c.id}: not found`);\n } else if (c.malformed) {\n lines.push(` ${c.id}: config malformed`);\n } else {\n const guarded = c.guardedCount > 0 ? `, ${c.guardedCount} guarded` : \"\";\n lines.push(` ${c.id}: ${c.serverCount} servers${guarded}`);\n }\n }\n lines.push(\"runtimes:\");\n for (const rt of r.runtimes) {\n lines.push(` ${rt.name}: ${rt.available ? \"available\" : \"missing\"}`);\n }\n lines.push(\n `issues: ${r.issues.malformedConfigs} malformed config(s), ${r.issues.missingRuntime} missing-runtime, ${r.issues.plaintextSecrets} plaintext secret(s)`\n );\n return lines.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Handler\n// ---------------------------------------------------------------------------\n\nexport interface DoctorOpts {\n json?: boolean;\n report?: boolean;\n /** Injected in --report mode; the Commander action supplies the real env. */\n reportEnv?: DoctorReportEnv;\n}\n\n/**\n * Core logic for `mcpm doctor`.\n * @returns Exit code: 0 = healthy, 1 = critical issues found.\n */\nexport async function doctorHandler(deps: DoctorDeps, opts: DoctorOpts = {}): Promise<number> {\n const model = await buildDoctorModel(deps);\n\n if (opts.report) {\n const env = opts.reportEnv ?? gatherReportEnv();\n deps.output(renderReportText(buildDoctorReport(model, env)));\n } else if (opts.json) {\n deps.output(JSON.stringify(model, null, 2));\n } else {\n renderDoctorText(model, deps.output);\n }\n\n return model.ok ? 0 : 1;\n}\n\n// ---------------------------------------------------------------------------\n// Commander registration\n// ---------------------------------------------------------------------------\n\nimport { Command } from \"commander\";\nimport os from \"os\";\nimport { execFile } from \"child_process\";\nimport { getConfigPath as _getConfigPath, CLIENT_IDS } from \"../config/paths.js\";\nimport { getAdapter as getAdapterDefault } from \"../config/index.js\";\nimport { coloredOutput } from \"../utils/output.js\";\nimport { isConfineBackendAvailable } from \"../guard/confine/apply.js\";\nimport { isSupportedPlatform as isKeychainSupported } from \"../store/os-keychain.js\";\n\n/** Factory so callers that inject a custom getConfigPath (e.g. the MCP server) get honored. */\nexport function makeCheckConfigExists(\n getConfigPathFn: (clientId: ClientId) => string\n): (clientId: ClientId) => Promise<boolean> {\n return async (clientId: ClientId): Promise<boolean> => {\n try {\n await access(getConfigPathFn(clientId));\n return true;\n } catch {\n return false;\n }\n };\n}\n\nconst checkConfigExistsDefault = makeCheckConfigExists(_getConfigPath);\n\nconst ALLOWED_RUNTIME_CMDS = new Set<string>([\"npx\", \"uvx\", \"docker\"]);\n\nexport function execCheckDefault(cmd: string): Promise<boolean> {\n if (!ALLOWED_RUNTIME_CMDS.has(cmd)) return Promise.resolve(false);\n return new Promise((resolve) => {\n const which = process.platform === \"win32\" ? \"where\" : \"which\";\n execFile(which, [cmd], (err) => {\n resolve(err === null);\n });\n });\n}\n\n/** Gathers the impure environment fields for `--report`. */\nfunction gatherReportEnv(): DoctorReportEnv {\n return {\n mcpm: __PKG_VERSION__,\n node: process.version,\n platform: process.platform,\n arch: process.arch,\n osRelease: os.release(),\n confineBackend: isConfineBackendAvailable(),\n secretStore: isKeychainSupported() ? \"os-keychain\" : \"machine-key\",\n };\n}\n\nexport function registerDoctorCommand(program: Command): void {\n program\n .command(\"doctor\")\n .description(\"Check MCP setup health and report issues\")\n .option(\"--json\", \"emit the structured DoctorModel as JSON (shape UNSTABLE; NOT redacted — includes server names, use --report to share publicly)\")\n .option(\"--report\", \"emit a redacted, pasteable env snapshot for bug reports (no server names/args)\")\n .action(async (options: { json?: boolean; report?: boolean }) => {\n // --json / --report are machine/paste output — never colorize.\n const plain = options.json || options.report;\n const deps: DoctorDeps = {\n getAdapter: getAdapterDefault,\n getConfigPath: _getConfigPath,\n checkConfigExists: checkConfigExistsDefault,\n execCheck: execCheckDefault,\n output: plain ? (t) => console.log(t) : coloredOutput,\n };\n\n const exitCode = await doctorHandler(deps, { json: options.json, report: options.report });\n process.exit(exitCode);\n });\n}\n","/**\n * Cross-client config-drift model (pure, injectable).\n *\n * `mcpm diff` answers \"installed vs declared stack\" in ONE direction. This module\n * answers the symmetric N-client question: for every server name, which clients\n * have it, which are missing it, and do the clients that DO have it agree on the\n * server's shape? It is the shared core behind `mcpm sync --check` and the doctor\n * \"Cross-client\" section.\n *\n * Design notes:\n * - Read-only. No writes, no registry/lock/network — it only reads client configs\n * (the collect loop mirrors diff.ts:76-93 / export.ts).\n * - `buildDriftModel` is pure and takes already-collected `ClientState[]` so the\n * doctor command can feed it the reads it already did (no double I/O).\n * - Conflict comparison is over command + ordered args + env KEY set + url +\n * header KEY set. It NEVER compares env / header VALUES — those are secrets, and\n * two clients legitimately hold the same key with a per-machine value.\n *\n * Exports: DriftDeps, ClientState, ServerDrift, DriftModel, collectClientStates,\n * buildDriftModel.\n */\n\nimport type { ClientId } from \"./paths.js\";\nimport type { ConfigAdapter, McpServerEntry } from \"./adapters/index.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface DriftDeps {\n detectClients: () => Promise<ClientId[]>;\n getAdapter: (clientId: ClientId) => Pick<ConfigAdapter, \"read\">;\n getPath: (clientId: ClientId) => string;\n}\n\n/** A single client's full set of MCP server entries (one successful read). */\nexport interface ClientState {\n readonly clientId: ClientId;\n readonly servers: Record<string, McpServerEntry>;\n}\n\nexport interface ServerDrift {\n readonly name: string;\n /** Clients (with readable configs) that declare this server. */\n readonly present: readonly ClientId[];\n /** Clients (with readable configs) that lack this server. */\n readonly absent: readonly ClientId[];\n /** True when the `present` clients disagree on the server's shape. */\n readonly conflict: boolean;\n /** Which fields diverge among the `present` clients (only when conflict). */\n readonly conflictFields?: readonly string[];\n}\n\nexport interface DriftModel {\n /** Clients considered — those whose config was readable. Sorted. */\n readonly clients: readonly ClientId[];\n /** One entry per distinct server name, sorted by name. */\n readonly servers: readonly ServerDrift[];\n /** Servers present in every considered client with no shape conflict. */\n readonly inSync: number;\n /** Servers with at least one absence or a shape conflict. */\n readonly drifted: number;\n}\n\n// ---------------------------------------------------------------------------\n// Collection (I/O)\n// ---------------------------------------------------------------------------\n\n/**\n * Read each detected client's config into a `ClientState`. Clients whose config\n * is unreadable (missing / malformed) are skipped — never throws — so a single\n * broken config can't blind the whole cross-client view (same posture as\n * `diff` / `export`).\n */\nexport async function collectClientStates(deps: DriftDeps): Promise<ClientState[]> {\n const clients = await deps.detectClients();\n const states: ClientState[] = [];\n for (const clientId of clients) {\n try {\n const servers = await deps.getAdapter(clientId).read(deps.getPath(clientId));\n states.push({ clientId, servers });\n } catch {\n // Skip unreadable clients (missing or malformed config).\n }\n }\n return states;\n}\n\n// ---------------------------------------------------------------------------\n// Drift model (pure)\n// ---------------------------------------------------------------------------\n\n/**\n * Per-field canonical projection used for conflict detection. Each value is a\n * stable string; two entries conflict on a field iff their projected strings\n * differ. Deliberately excludes env / header VALUES (secrets) and the per-client\n * `disabled` flag (an intentional per-client toggle, not a definition drift).\n */\nfunction fieldProjection(entry: McpServerEntry): Record<string, string> {\n return {\n command: entry.command ?? \"\",\n args: JSON.stringify(entry.args ?? []),\n \"env keys\": JSON.stringify(Object.keys(entry.env ?? {}).sort()),\n url: entry.url ?? \"\",\n \"header keys\": JSON.stringify(Object.keys(entry.headers ?? {}).sort()),\n };\n}\n\nconst COMPARED_FIELDS = [\"command\", \"args\", \"env keys\", \"url\", \"header keys\"] as const;\n\n/** Fields on which the given entries (≥1) disagree. Empty ⇒ all identical. */\nfunction divergingFields(entries: readonly McpServerEntry[]): string[] {\n const projections = entries.map(fieldProjection);\n return COMPARED_FIELDS.filter((field) => {\n const distinct = new Set(projections.map((p) => p[field]));\n return distinct.size > 1;\n });\n}\n\nexport function buildDriftModel(states: readonly ClientState[]): DriftModel {\n const clients = states.map((s) => s.clientId).sort();\n\n // Gather, per server name, the clients that declare it and their entries.\n const byName = new Map<string, Array<{ clientId: ClientId; entry: McpServerEntry }>>();\n for (const { clientId, servers } of states) {\n for (const [name, entry] of Object.entries(servers)) {\n const list = byName.get(name) ?? [];\n list.push({ clientId, entry });\n byName.set(name, list);\n }\n }\n\n const servers: ServerDrift[] = [];\n for (const name of [...byName.keys()].sort()) {\n const holders = byName.get(name)!;\n const present = holders.map((h) => h.clientId).sort();\n const presentSet = new Set(present);\n const absent = clients.filter((c) => !presentSet.has(c));\n\n const fields = holders.length > 1 ? divergingFields(holders.map((h) => h.entry)) : [];\n const conflict = fields.length > 0;\n\n servers.push({\n name,\n present,\n absent,\n conflict,\n ...(conflict ? { conflictFields: fields } : {}),\n });\n }\n\n const drifted = servers.filter((s) => s.absent.length > 0 || s.conflict).length;\n return { clients, servers, inSync: servers.length - drifted, drifted };\n}\n","/**\n * Plaintext-secret scan over client MCP config (F9 · PR1).\n *\n * mcpm ships an encrypted secret store + OS keychain, but a server's env/header\n * values are routinely pasted in plaintext (24k+ such leaks documented in the\n * wild). This read-only scan flags them so `doctor` can nudge the user toward\n * `mcpm secrets` / keychain mode.\n *\n * REDACTION CONTRACT: a finding carries the KEY name and a LABEL only — NEVER the\n * matched value. Values already stored as `mcpm:keychain:` placeholders are\n * skipped (they are the safe state, not a leak).\n *\n * Two detectors:\n * 1. value-shape — the sweep-hardened `detectSecretLabels` patterns (AWS /\n * GitHub / OpenAI / … keys). Near-zero false positives.\n * 2. secret-named key — a tight key-name heuristic for generic passwords/tokens\n * no value-regex matches, gated by strong non-secret-qualifier (URL/ID/NAME/…)\n * and non-secret-value (reference/URL/path/flag) exclusions + a benign corpus.\n *\n * Pure: no I/O. The caller (doctor) supplies the already-read config.\n */\n\nimport type { McpServerEntry } from \"../config/adapters/index.js\";\nimport { detectSecretLabels } from \"./patterns.js\";\nimport { parsePlaceholder } from \"../store/keychain.js\";\n\nexport interface ConfigSecretFinding {\n /** Server name as it appears in the client config. */\n server: string;\n /** Which value map the secret sits in. */\n field: \"env\" | \"header\";\n /** The env var / header NAME. Never the value. */\n key: string;\n /** What was matched (e.g. \"AWS access key\"). Never the value. */\n label: string;\n}\n\n/** Label for a key-heuristic hit (detector 2). Value-free by construction. */\nconst GENERIC_LABEL = \"secret-named key holds a plaintext value\";\n\n// Secret-indicating whole words. Matched against the key normalized to\n// upper-case with '-'→'_' (so `X-API-Key` reads as `X_API_KEY`). Bare `KEY` is\n// deliberately NOT a word (PUBLIC_KEY / KEY_ID / SORT_KEY are not secrets) — only\n// the listed `*_KEY` compounds count.\nconst SECRET_KEY_RE =\n /(?:^|_)(?:PASSWORD|PASSWD|PASSPHRASE|SECRET|TOKEN|PAT|APIKEY|AUTHORIZATION|CREDENTIALS?|(?:API|ACCESS|PRIVATE|SECRET|SESSION|SIGNING|ENCRYPTION)_KEY)(?:_|$)/;\n\n// Tokens that mean the field is a descriptor of a secret, not the secret itself\n// (an id, url, name, endpoint, …). Any one vetoes a key-name match, so\n// `TOKEN_URL` / `AWS_ACCESS_KEY_ID` / `SECRET_NAME` / `PUBLIC_KEY` do not fire.\n// KNOWN GAP (advisory tool, accepted): the veto matches a qualifier ANYWHERE in the\n// key, so `ID_TOKEN` (where `ID` is the credential TYPE, not a descriptor) is missed.\n// A suffix-anchored fix would newly false-POSITIVE on `MAPBOX_PUBLIC_TOKEN`; since a\n// false negative in an advisory scan is acceptable but a false positive is not, we\n// keep the anywhere-match.\nconst NON_SECRET_QUALIFIER_RE =\n /(?:^|_)(?:URL|URI|ENDPOINT|HOST|PORT|ID|NAME|PATH|FILE|DIR|ENABLED|DISABLED|TYPE|MODE|REGION|TIMEOUT|VERSION|PUBLIC|FORMAT|HEADER|PREFIX|SUFFIX|COUNT|SIZE|TTL|EXPIRY|EXPIRES|ISSUER|AUDIENCE|ALGORITHM|ALG|SCOPE|METHOD)(?:_|$)/;\n\nfunction normalizeKey(key: string): string {\n return key.toUpperCase().replace(/-/g, \"_\");\n}\n\nfunction keyLooksSecret(key: string): boolean {\n const k = normalizeKey(key);\n return SECRET_KEY_RE.test(k) && !NON_SECRET_QUALIFIER_RE.test(k);\n}\n\n/** True when the value is plausibly a real plaintext secret (not a ref/URL/flag). */\nfunction valueLooksPlaintextSecret(value: string): boolean {\n const v = value.trim();\n if (v.length < 6) return false; // too short to be a credential\n if (parsePlaceholder(value) !== null) return false; // mcpm keychain placeholder\n // Reference, not a literal secret. `${...}` is matched ANYWHERE (not just leading):\n // `Bearer ${input:key}` / `Bearer ${env:VAR}` is VS Code / Cursor / Claude Code's\n // documented header idiom — the recommended SAFE state. Detector 1 already ran on\n // the raw value, so a shaped credential embedded alongside a ref is still caught.\n if (/\\$\\{[^}]*\\}/.test(v)) return false; // ${VAR} template (embedded or leading)\n if (/^\\$[A-Za-z_]/.test(v)) return false; // leading $VAR reference\n if (/^%[A-Za-z_][A-Za-z0-9_]*%([\\\\/].*)?$/.test(v)) return false; // %VAR% ref or %VAR%-rooted path\n // A URI of ANY scheme: real endpoints AND secret-manager references that are the\n // safe state — op:// (1Password), vault:// (Vault). ACCEPTED FALSE-NEGATIVE: a URI\n // that itself CARRIES a credential (connection-string userinfo postgres://u:p@host,\n // or a query-param secret like otpauth://…?secret=SEED) is excluded too. Detector 1\n // still catches any prefix-shaped credential embedded in the value, and the bare\n // (non-URI) secret form is still caught by detector 2. Zero-FP is the hard invariant;\n // re-catching these would need query-param parsing that risks FPs on real endpoints.\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(v)) return false;\n // Filesystem path — POSIX (~ . /) or Windows (drive-letter, UNC).\n if (/^[~./]/.test(v) || /^[A-Za-z]:[\\\\/]/.test(v) || /^\\\\\\\\/.test(v)) return false;\n if (/^(true|false|\\d+)$/i.test(v)) return false; // boolean / plain number\n return true;\n}\n\nfunction scanMap(\n server: string,\n field: \"env\" | \"header\",\n map: Record<string, string> | undefined\n): ConfigSecretFinding[] {\n if (!map) return [];\n const out: ConfigSecretFinding[] = [];\n for (const [key, value] of Object.entries(map)) {\n if (typeof value !== \"string\") continue;\n if (parsePlaceholder(value) !== null) continue; // already stored safely — not a leak\n const labels = detectSecretLabels(value);\n if (labels.length > 0) {\n // Value-shape is the more specific, higher-confidence signal — ONE finding per\n // (field, key) even when several patterns match (e.g. a Bearer-wrapped ghp_\n // token hits both), so the --report count is not inflated. Skip the heuristic.\n out.push({ server, field, key, label: labels.join(\", \") });\n continue;\n }\n if (keyLooksSecret(key) && valueLooksPlaintextSecret(value)) {\n out.push({ server, field, key, label: GENERIC_LABEL });\n }\n }\n return out;\n}\n\n/** Scan one server's env + headers for plaintext secrets. */\nexport function scanServerConfigSecrets(\n server: string,\n entry: McpServerEntry\n): ConfigSecretFinding[] {\n return [...scanMap(server, \"env\", entry.env), ...scanMap(server, \"header\", entry.headers)];\n}\n\n/** Scan every server in a client's config. */\nexport function scanConfigSecrets(\n servers: Record<string, McpServerEntry>\n): ConfigSecretFinding[] {\n return Object.entries(servers).flatMap(([name, entry]) => scanServerConfigSecrets(name, entry));\n}\n"],"mappings":"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","names":["servers","getAdapter","getConfigPath"]}
#!/usr/bin/env node
import {
handleLock,
lockPathFor
} from "./chunk-C6CAHFQX.js";
import {
parseSecretsMode,
resolveInstallEntry,
validateRemoteUrl
} from "./chunk-7QRDQF55.js";
import {
readPins
} from "./chunk-DDCTUMSZ.js";
import {
DEFAULT_MIN_RELEASE_AGE_HOURS,
assessReleaseAge,
stdoutOutput
} from "./chunk-E3T224S3.js";
import {
fetchNpmProvenance,
isEnoent,
isLockedRegistryServer,
isRegistryServer,
isUrlServer,
parseLockFile,
parseStackFile
} from "./chunk-RTI2GLYX.js";
import {
checkScannerAvailable,
scanTier2
} from "./chunk-F6CHEUGO.js";
import {
EXTERNAL_SCAN_MAX,
computeTrustScore,
nativeTrustScore
} from "./chunk-LSNEZAFR.js";
import {
getAdapter
} from "./chunk-W4IAFBUN.js";
import {
confirm
} from "./chunk-2PWW3Q5Q.js";
import {
isNewUnguarded
} from "./chunk-MLVDFLDQ.js";
import {
compareIntegrity,
fetchNpmIntegrity
} from "./chunk-7RJXJERN.js";
import {
RegistryClient
} from "./chunk-V4AA4ZL5.js";
import {
applyKeychainSecrets,
setSecrets
} from "./chunk-GZ3WCRLG.js";
import {
detectInstalledClients
} from "./chunk-6R7TL5O2.js";
import {
getConfigPath
} from "./chunk-R4R2VPDA.js";
import {
assessServerStatus,
extractRegistryMeta,
scanTier1
} from "./chunk-U7N6FRYF.js";
// src/stack/policy.ts
function checkTrustPolicy(input2) {
const { serverName, currentScore, currentMaxPossible, lockedSnapshot, policy } = input2;
if (policy === void 0) {
return { pass: true };
}
const currentPct = toPct(currentScore, currentMaxPossible);
if (policy.minTrustScore !== void 0 && currentPct < policy.minTrustScore) {
return {
pass: false,
reason: `"${serverName}" trust score ${currentPct}% is below the minimum policy threshold of ${policy.minTrustScore}%.`
};
}
if (policy.blockOnScoreDrop === true && lockedSnapshot !== void 0) {
const { currentNativeScore, currentNativeMaxPossible } = input2;
if (currentNativeScore === void 0 || currentNativeMaxPossible === void 0) {
throw new Error(
"blockOnScoreDrop requires the current native trust figures (currentNativeScore / currentNativeMaxPossible). This is a bug \u2014 report it rather than working around it."
);
}
const lockedNative = recoverLockedNative(
lockedSnapshot,
currentNativeMaxPossible
);
const curPct = toPct(currentNativeScore, currentNativeMaxPossible);
const lockPct = toPct(lockedNative.score, lockedNative.maxPossible);
if (curPct < lockPct) {
return {
pass: false,
reason: `"${serverName}" trust score dropped from ${lockPct}% to ${curPct}% (mcpm-native evidence, excluding unverifiable external-scanner credit) since the lock file was created.` + (lockedNative.basis === "legacy-bound" ? ` This lock predates native-evidence drop checks and was written with an external scanner credited, so the baseline is an upper bound \u2014 re-run \`mcpm lock\` to record an exact one.` : lockedNative.basis === "out-of-range" ? (
// Deliberately NOT phrased as tampering: an upward re-weighting of the
// external bucket would make that accusation false for an older mcpm
// reading a newer lock. Re-locking is the remedy either way.
` This lock records trust figures outside the range this version can interpret, so the baseline is an upper bound \u2014 re-run \`mcpm lock\` to record an exact one.`
) : ` If you recently upgraded mcpm, new scanner findings can lower scores \u2014 re-run \`mcpm lock\` to refresh snapshots if the drop is expected.`)
};
}
}
if (policy.minReleaseAgeHours !== void 0 && input2.releaseAge?.blocksArmedGate === true) {
const { ageHours, status } = input2.releaseAge;
if (status === "future") {
return {
pass: false,
reason: `"${serverName}" has a publish timestamp in the future; treated as within the minimum release age of ${policy.minReleaseAgeHours} hour(s) required by policy.`
};
}
if (ageHours === null) {
return {
pass: false,
reason: `"${serverName}" release is of unverifiable age (publish timestamp ${status === "absent" ? "missing from registry metadata" : "could not be parsed"}), and the policy requires a minimum release age of ${policy.minReleaseAgeHours} hour(s).`
};
}
return {
pass: false,
reason: `"${serverName}" release is ${ageHours} hour(s) old, below the minimum release age of ${policy.minReleaseAgeHours} hour(s) required by policy.`
};
}
if (policy.blockInstallScripts === true && input2.hasInstallScriptFindings === true) {
return {
pass: false,
reason: `"${serverName}" resolves to a launcher that runs install scripts, and the policy blocks install scripts.`
};
}
return { pass: true };
}
function toPct(score, maxPossible) {
if (maxPossible <= 0) return 0;
return Math.round(score / maxPossible * 100);
}
function isUsableCredit(credit, locked, nativeMax) {
if (locked.maxPossible === nativeMax && credit > 0) return false;
return credit >= 0 && credit <= EXTERNAL_SCAN_MAX && credit <= locked.score;
}
function recoverLockedNative(locked, nativeMax) {
const bounded = (score, basis) => {
const clamped = Math.max(0, Math.min(nativeMax, score));
return {
score: clamped,
maxPossible: nativeMax,
// A computation that had to be clamped was not exact, whatever produced it — an
// out-of-range `score` reaches here the same way an out-of-range credit does,
// since both are unbounded `z.number()`. Reporting it as exact would hand the
// user the "you upgraded mcpm" remedy for a lock that actually needs re-locking.
basis: basis === "exact" && clamped !== score ? "out-of-range" : basis
};
};
if (locked.externalScanCredit !== void 0) {
return isUsableCredit(locked.externalScanCredit, locked, nativeMax) ? bounded(locked.score - locked.externalScanCredit, "exact") : bounded(locked.score, "out-of-range");
}
if (locked.maxPossible === nativeMax) {
return bounded(locked.score, "exact");
}
return bounded(locked.score, "legacy-bound");
}
// src/stack/env.ts
import { readFile } from "fs/promises";
var ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "__defineGetter__", "__defineSetter__"]);
async function parseEnvFile(filePath) {
let raw;
try {
raw = await readFile(filePath, "utf-8");
} catch (err) {
if (isEnoent(err)) {
return { vars: {}, warnings: [] };
}
throw err;
}
return parseEnvString(raw);
}
function parseEnvString(content) {
const vars = {};
const warnings = [];
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
const lineNum = i + 1;
const raw = lines[i];
const trimmed = raw.trim();
if (trimmed === "" || trimmed.startsWith("#")) {
continue;
}
const eqIndex = trimmed.indexOf("=");
if (eqIndex === -1) {
warnings.push(`Line ${lineNum}: skipped malformed line (no = sign)`);
continue;
}
const key = trimmed.slice(0, eqIndex).trim();
if (key === "") {
warnings.push(`Line ${lineNum}: skipped line with empty key`);
continue;
}
if (!ENV_KEY_RE.test(key) || UNSAFE_KEYS.has(key)) {
warnings.push(
`Line ${lineNum}: skipped invalid key "${key}"`
);
continue;
}
let value = trimmed.slice(eqIndex + 1).trim();
if (!value.startsWith('"') && !value.startsWith("'")) {
const commentIndex = value.indexOf(" #");
if (commentIndex !== -1) {
value = value.slice(0, commentIndex).trim();
}
}
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
value = value.slice(1, -1);
}
vars[key] = value;
}
return { vars, warnings };
}
// src/stack/frozen-verify.ts
function memoizeIntegrity(fetch) {
const cache = /* @__PURE__ */ new Map();
return (identifier, npmVersion) => {
const key = `${identifier}\0${npmVersion}`;
let p = cache.get(key);
if (p === void 0) {
p = fetch(identifier, npmVersion);
cache.set(key, p);
}
return p;
};
}
async function classifyIntegrity(lockFile, fetchNpmIntegrity2) {
const registryEntries = Object.entries(lockFile.servers).filter(
([, locked]) => isLockedRegistryServer(locked)
);
const npmEntries = registryEntries.filter(([, l]) => l.registryType === "npm");
const npmNames = new Set(npmEntries.map(([name]) => name));
const unenforceable = Object.keys(lockFile.servers).filter((name) => !npmNames.has(name));
const checkable = npmEntries.filter(([, l]) => l.npmIntegrity !== void 0);
const absentBaseline = npmEntries.filter(([, l]) => l.npmIntegrity === void 0).map(([name]) => name);
const fresh = await Promise.all(
checkable.map(([, l]) => fetchNpmIntegrity2(l.identifier, l.npmIntegrity.npmVersion))
);
const drift = [];
const formatOnly = [];
const couldNotVerify = [];
for (let i = 0; i < checkable.length; i++) {
const [name, locked] = checkable[i];
const baseline = locked.npmIntegrity;
const snap = fresh[i];
const coord = { name, identifier: locked.identifier, npmVersion: baseline.npmVersion };
if (snap === void 0) {
couldNotVerify.push(coord);
continue;
}
const cmp = compareIntegrity(baseline.integrity, snap.integrity);
if (cmp === "equal") continue;
if (cmp === "differ") {
drift.push({ ...coord, oldIntegrity: baseline.integrity, newIntegrity: snap.integrity });
} else {
formatOnly.push(coord);
}
}
return { drift, formatOnly, couldNotVerify, absentBaseline, unenforceable, checkedNpmCount: checkable.length };
}
function frozenVerdict(c) {
const noBaselines = c.absentBaseline.length > 0 && c.checkedNpmCount === 0;
const blocks = [];
for (const d of c.drift) {
blocks.push({
name: d.name,
reason: "drift",
identifier: d.identifier,
npmVersion: d.npmVersion,
oldIntegrity: d.oldIntegrity,
newIntegrity: d.newIntegrity
});
}
for (const f of c.formatOnly) {
blocks.push({ name: f.name, reason: "format", identifier: f.identifier, npmVersion: f.npmVersion });
}
for (const v of c.couldNotVerify) {
blocks.push({ name: v.name, reason: "could-not-verify", identifier: v.identifier, npmVersion: v.npmVersion });
}
if (!noBaselines) {
for (const name of c.absentBaseline) {
blocks.push({ name, reason: "missing-baseline" });
}
}
return {
ok: !noBaselines && blocks.length === 0,
noBaselines,
blocks,
unenforceable: c.unenforceable,
checkedNpmCount: c.checkedNpmCount
};
}
// src/stack/frozen-provenance.ts
function verifiedBaseline(locked) {
const prov = locked.provenance;
if (prov?.status !== "attested" || prov.verification?.outcome !== "verified") {
return void 0;
}
return {
npmVersion: prov.npmVersion,
signerSan: prov.verification.signerSan,
signerIssuer: prov.verification.signerIssuer
};
}
async function classifyProvenance(lockFile, fetchNpmIntegrity2, fetchNpmProvenance2) {
const checked = Object.entries(lockFile.servers).filter(
([, l]) => isLockedRegistryServer(l)
).filter(([, l]) => l.registryType === "npm").map(([name, l]) => ({ name, locked: l, baseline: verifiedBaseline(l) })).filter(
(e) => e.baseline !== void 0
);
const blocks = (await Promise.all(
checked.map(async ({ name, locked, baseline }) => {
const coord = { name, identifier: locked.identifier, npmVersion: baseline.npmVersion };
try {
const integ = await fetchNpmIntegrity2(locked.identifier, baseline.npmVersion);
if (integ === void 0) {
return {
...coord,
reason: "unverifiable",
detail: "could not fetch npm's published integrity to bind the attestation"
};
}
const fresh = await fetchNpmProvenance2(locked.identifier, baseline.npmVersion, {
integritySri: integ.integrity
});
return classifyOne(coord, baseline, fresh);
} catch {
return {
...coord,
reason: "unverifiable",
detail: "re-verification errored this run (fetcher threw)"
};
}
})
)).filter((b) => b !== void 0);
return { ok: blocks.length === 0, blocks, checkedVerifiedCount: checked.length };
}
function classifyOne(coord, baseline, fresh) {
if (fresh === void 0) {
return { ...coord, reason: "unverifiable", detail: "no fresh attestation record this run (offline or endpoint error)" };
}
if (fresh.status === "unsigned") {
return { ...coord, reason: "regression", detail: "the attestation that verified at lock time is no longer published (now unsigned)" };
}
if (fresh.status !== "attested") {
return { ...coord, reason: "unverifiable", detail: "attestation shape is no longer a recognizable SLSA record" };
}
const v = fresh.verification;
if (v === void 0) {
return { ...coord, reason: "unverifiable", detail: "attestation present but cryptographic verification did not run this fetch" };
}
if (v.outcome === "could-not-verify") {
return { ...coord, reason: "regression", detail: `attestation no longer cryptographically verifies (${v.reason ?? "crypto failure"})` };
}
if (baseline.signerSan === void 0) {
return { ...coord, reason: "unverifiable", detail: "verified baseline lacks a recorded signer SAN \u2014 cannot assert signer equality; re-lock to record it" };
}
if (v.signerSan !== baseline.signerSan || v.signerIssuer !== baseline.signerIssuer) {
const deltas = [];
if (v.signerSan !== baseline.signerSan) {
deltas.push(`SAN ${baseline.signerSan ?? "(none)"} \u2192 ${v.signerSan ?? "(none)"}`);
}
if (v.signerIssuer !== baseline.signerIssuer) {
deltas.push(`issuer ${baseline.signerIssuer ?? "(none)"} \u2192 ${v.signerIssuer ?? "(none)"}`);
}
return { ...coord, reason: "signer-changed", detail: `signer identity changed: ${deltas.join("; ")}` };
}
return void 0;
}
// src/guard/shadow.ts
function detectNameCollisions(inventory) {
const ownersByTool = /* @__PURE__ */ new Map();
for (const [server, tools] of inventory) {
for (const tool of tools) {
let owners = ownersByTool.get(tool);
if (owners === void 0) {
owners = /* @__PURE__ */ new Set();
ownersByTool.set(tool, owners);
}
owners.add(server);
}
}
const findings = [];
for (const [toolName, owners] of ownersByTool) {
if (owners.size >= 2) {
findings.push({ toolName, servers: [...owners].sort() });
}
}
return findings.sort((a, b) => a.toolName.localeCompare(b.toolName));
}
function buildInventoryFromPins(pins, serverNames) {
const inventory = /* @__PURE__ */ new Map();
for (const name of serverNames) {
inventory.set(name, toolNamesFor(pins, name));
}
return inventory;
}
function toolNamesFor(pins, name) {
return Object.hasOwn(pins.servers, name) ? Object.keys(pins.servers[name]) : [];
}
function serversWithoutBaseline(pins, serverNames) {
return serverNames.filter((name) => toolNamesFor(pins, name).length === 0);
}
// src/commands/up.ts
import "commander";
import chalk from "chalk";
import { input, password } from "@inquirer/prompts";
function trustFigure(trust, options) {
if (options.minTrustFloor === void 0) {
return `${trust.score}/${trust.maxPossible}`;
}
const native = nativeTrustScore(trust);
if (native.excludedExternalCredit === 0) {
return `${trust.score}/${trust.maxPossible}`;
}
return `${native.score}/${native.maxPossible} against the floor, ${trust.score}/${trust.maxPossible} with the external scanner`;
}
async function handleUp(options, deps) {
if (options.secrets === "keychain" && options.ci) {
throw new Error(
"--secrets keychain cannot be combined with --ci (it would persist secrets to the CI runner's keychain). Use --secrets plaintext in CI."
);
}
const stackPath = options.stackFile ?? "mcpm.yaml";
const lockPath = lockPathFor(stackPath);
const stackFile = await parseStackFile(stackPath);
let lockFile = await parseLockFile(lockPath);
if (lockFile === null) {
deps.output("No lock file found. Running mcpm lock first...");
await deps.runLock(stackPath);
lockFile = await parseLockFile(lockPath);
if (lockFile === null) {
throw new Error("Failed to create lock file.");
}
}
const clients = await deps.detectClients();
if (clients.length === 0) {
throw new Error("No supported AI clients found.");
}
const serverEntries = filterByProfile(stackFile, options.profile);
if (serverEntries.length === 0) {
deps.output("No servers match the selected profile.");
return;
}
if (options.frozen === true || stackFile.policy?.frozen === true) {
await runFrozenPass(lockFile, deps);
}
const envFileVars = options.allowEnvFile === false ? { vars: {}, warnings: [] } : await parseEnvFile(".env");
const scannerAvailable = await deps.checkScannerAvailable();
if (options.dryRun) {
deps.output("Dry run \u2014 no changes will be made.\n");
}
if (!options.dryRun) {
await backupConfigs(clients, deps);
}
const results = [];
const previousConsented = deps.readUnguardedConsent ? await deps.readUnguardedConsent() : [];
const consentedUnguarded = new Set(previousConsented);
for (const [name, server] of serverEntries) {
const locked = lockFile.servers[name];
try {
const result = await processServer({
name,
server,
locked,
policy: stackFile.policy,
clients,
scannerAvailable,
envFileVars: envFileVars.vars,
consentedUnguarded,
options,
deps
});
results.push(result);
deps.recordResult?.({ name, status: result.status });
deps.output(` ${statusIcon(result.status)} ${name}: ${result.message}`);
} catch (err) {
const failure = {
name,
status: "failed",
message: err instanceof Error ? err.message : String(err)
};
results.push(failure);
deps.recordResult?.({ name, status: "failed" });
deps.output(` ${statusIcon("failed")} ${name}: ${failure.message}`);
}
}
if (options.strict && !options.dryRun) {
await handleStrictRemoval(stackFile, clients, options, deps, results);
}
const urlServerNames = new Set(
serverEntries.filter(([, s]) => isUrlServer(s)).map(([n]) => n)
);
const installedUnguarded = results.filter((r) => r.status === "installed" && urlServerNames.has(r.name)).map((r) => r.name).sort();
if (installedUnguarded.length > 0 && !options.dryRun) {
const newlyConsented = installedUnguarded.filter((n) => !consentedUnguarded.has(n));
if (isNewUnguarded(installedUnguarded, previousConsented)) {
const alreadyCount = installedUnguarded.length - newlyConsented.length;
const alreadyNote = alreadyCount > 0 ? ` (+${alreadyCount} previously consented)` : "";
deps.output(
`
\u26A0 UNGUARDED: the following URL/HTTP-transport server(s) now run WITHOUT runtime inspection (no relay wraps a non-stdio transport): ${newlyConsented.join(", ")}${alreadyNote}. This grants consent \u2014 it does NOT add protection. The only true fix is a streamable-HTTP relay (not yet implemented). Future \`up\` runs stay quiet unless a NEW unguarded server appears.`
);
if (deps.recordUnguardedConsent) {
await deps.recordUnguardedConsent(newlyConsented).catch(() => void 0);
}
} else {
deps.output(
`
${installedUnguarded.length} server(s) running unguarded (previously consented): ${installedUnguarded.join(", ")}`
);
}
}
if (options.frozen !== true && stackFile.policy?.frozen !== true) {
await runIntegrityPass(lockFile, deps);
}
let shadowCollisions = 0;
if (options.checkShadowing === true || stackFile.policy?.checkShadowing === true) {
shadowCollisions = await runShadowPass(
serverEntries.map(([name]) => name),
deps
);
}
const installed = results.filter((r) => r.status === "installed").length;
const blocked = results.filter((r) => r.status === "blocked").length;
const failed = results.filter((r) => r.status === "failed").length;
const skipped = results.filter((r) => r.status === "skipped").length;
const removed = results.filter((r) => r.status === "removed").length;
const unguarded = installedUnguarded.length;
deps.output(
`
${installed} installed, ${skipped} skipped, ${blocked} blocked, ${failed} failed` + (removed > 0 ? `, ${removed} removed` : "") + (unguarded > 0 ? `, ${unguarded} unguarded` : "")
);
const totalSecretsStored = results.reduce(
(sum, r) => sum + (r.storedSecrets ?? 0),
0
);
if (options.secrets === "keychain" && totalSecretsStored > 0 && !options.dryRun) {
deps.output(
"Secrets stored encrypted at rest in ~/.mcpm. With an OS keychain this protects against other-user/offline access (not same-user processes); without one a machine-derived key is used that guards casual local inspection only, NOT file exfiltration \u2014 run `mcpm secrets migrate` once a keychain is available. Run `mcpm guard enable` (then restart your IDE) so they resolve at launch."
);
}
if (blocked > 0 || failed > 0) {
throw new Error(`${blocked + failed} server(s) could not be installed.`);
}
if (shadowCollisions > 0 && options.ci) {
throw new Error(
`${shadowCollisions} cross-server tool-name collision(s) detected (--ci). Resolve the shadowing (rename/remove a duplicate tool) or drop --check-shadowing.`
);
}
}
function filterByProfile(stackFile, profile) {
return Object.entries(stackFile.servers).filter(([, server]) => {
const profiles = isRegistryServer(server) || isUrlServer(server) ? server.profiles : void 0;
if (!profiles) return true;
if (!profile) return true;
return profiles.includes(profile);
});
}
async function backupConfigs(clients, deps) {
const { readFile: readFile2, writeFile } = await import("fs/promises");
for (const clientId of clients) {
try {
const adapter = deps.getAdapter(clientId);
const configPath = deps.getPath(clientId);
const content = await readFile2(configPath, "utf-8");
await writeFile(`${configPath}.bak`, content, {
encoding: "utf-8",
mode: 384
});
} catch {
}
}
}
async function processServer(input2) {
const { name, server, locked, policy, clients, scannerAvailable, envFileVars, options, deps } = input2;
if (isUrlServer(server)) {
return processUrlServer(name, server.url, clients, policy, input2.consentedUnguarded, options, deps);
}
if (!locked || !isLockedRegistryServer(locked)) {
return { name, status: "failed", message: "Not found in lock file. Run mcpm lock." };
}
const serverEntry = await deps.getServer(name, locked.version);
const statusGate = assessServerStatus(serverEntry);
if (statusGate.blocks) {
return {
name,
status: "blocked",
message: `deleted from the MCP registry${statusGate.statusMessage ? ` (${statusGate.statusMessage})` : ""}`
};
}
const tier1 = deps.scanTier1(serverEntry);
let findings = [...tier1];
if (scannerAvailable) {
const tier2 = await deps.scanTier2(name);
findings = [...findings, ...tier2];
}
const registryMeta = extractRegistryMeta(serverEntry);
const releaseAge = assessReleaseAge({
publishedAt: registryMeta.publishedAt,
now: (deps.now ?? Date.now)(),
minAgeHours: options.minTrustFloor !== void 0 ? DEFAULT_MIN_RELEASE_AGE_HOURS : policy?.minReleaseAgeHours ?? DEFAULT_MIN_RELEASE_AGE_HOURS
});
if (releaseAge.finding) {
findings = [...findings, releaseAge.finding];
}
const trustInput = {
findings,
healthCheckPassed: null,
hasExternalScanner: scannerAvailable,
registryMeta
};
const trustScore = deps.computeTrustScore(trustInput);
const nativeTrust = nativeTrustScore(trustScore);
if (options.minTrustFloor !== void 0 && nativeTrust.score < options.minTrustFloor) {
return {
name,
status: "blocked",
message: `trust score ${nativeTrust.score}/${nativeTrust.maxPossible} is below the required floor of ${options.minTrustFloor}` + (nativeTrust.excludedExternalCredit > 0 ? ` (the external scanner's ${nativeTrust.excludedExternalCredit} points do not count toward the floor)` : "")
};
}
const policyResult = checkTrustPolicy({
serverName: name,
currentScore: trustScore.score,
currentMaxPossible: trustScore.maxPossible,
// TODOS #35: the blockOnScoreDrop tripwire compares native evidence, so a fake
// MCPM_EXTERNAL_SCANNER cannot mask a drop. `nativeTrust` is computed above.
currentNativeScore: nativeTrust.score,
currentNativeMaxPossible: nativeTrust.maxPossible,
lockedSnapshot: locked.trust,
policy,
releaseAge: {
ageHours: releaseAge.ageHours,
status: releaseAge.status,
blocksArmedGate: releaseAge.blocksArmedGate
},
hasInstallScriptFindings: findings.some((f) => f.type === "install-script")
});
if (!policyResult.pass) {
return { name, status: "blocked", message: policyResult.reason };
}
if (options.dryRun) {
return {
name,
status: "skipped",
message: `would install v${locked.version} (trust: ${trustFigure(trustScore, options)})`
};
}
const { env: envVars, storedCount } = await resolveEnvVars(name, server, envFileVars, options, deps);
const installedClients = [];
const clientErrors = [];
for (const clientId of clients) {
try {
const entry = resolveInstallEntry(serverEntry, clientId);
const entryWithEnv = {
...entry,
...Object.keys(envVars).length > 0 ? { env: { ...entry.env, ...envVars } } : {}
};
const adapter = deps.getAdapter(clientId);
const configPath = deps.getPath(clientId);
await adapter.addServer(configPath, name, entryWithEnv, { force: true });
installedClients.push(clientId);
} catch (err) {
clientErrors.push(`${clientId}: ${err instanceof Error ? err.message : String(err)}`);
}
}
if (installedClients.length === 0) {
return {
name,
status: "failed",
message: `could not write to any client (${clientErrors.join("; ")})`
};
}
const partialNote = clientErrors.length > 0 ? ` (warning: failed on ${clientErrors.join("; ")})` : "";
return {
name,
status: "installed",
message: `v${locked.version} (trust: ${trustFigure(trustScore, options)})${partialNote}`,
storedSecrets: storedCount
};
}
async function runIntegrityPass(lockFile, deps) {
const c = await classifyIntegrity(lockFile, deps.fetchNpmIntegrity);
for (const d of c.drift) {
const oldShort = d.oldIntegrity.slice(0, 16);
const newShort = d.newIntegrity.slice(0, 16);
deps.output(
`
\u26A0 INTEGRITY DRIFT: npm's published record for ${d.identifier}@${d.npmVersion} changed since you locked it (dist.integrity ${oldShort}\u2026 \u2192 ${newShort}\u2026). A published version's integrity is meant to be immutable, so this can mean a supply-chain republish \u2014 but it can also be a legitimate republish or a different registry. mcpm checks the registry's published record, not the code your agent runs. This is a warning only \u2014 it does not block \`mcpm up\`; npx/uvx fetch and run the actual package independently when the server starts (possibly from a different mirror). Re-run \`mcpm lock\` if this change is expected.`
);
}
for (const f of c.formatOnly) {
deps.output(
`
\u26A0 ${f.name}: npm changed the integrity format for ${f.identifier}@${f.npmVersion}, so mcpm cannot compare its published record against your locked baseline (mcpm checks the registry's published record, not the code your agent runs). Re-run \`mcpm lock\` to refresh the baseline.`
);
}
if (c.couldNotVerify.length > 0) {
deps.output(
`
could not verify npm integrity for ${c.couldNotVerify.length} server(s) this run (no drift result is not proof of integrity).`
);
}
if (c.absentBaseline.length > 0) {
deps.output(
`
integrity baseline missing for ${c.absentBaseline.length} npm server(s) \u2014 re-run \`mcpm lock\` with network access to enable drift detection.`
);
}
}
async function runFrozenPass(lockFile, deps) {
const fetchIntegrity = memoizeIntegrity(deps.fetchNpmIntegrity);
const [v, pv] = await Promise.all([
classifyIntegrity(lockFile, fetchIntegrity).then(frozenVerdict),
classifyProvenance(lockFile, fetchIntegrity, deps.fetchNpmProvenance)
]);
const provBlocks = pv.blocks;
if (v.unenforceable.length > 0) {
deps.output(
`
${v.unenforceable.length} server(s) (pypi/oci/url) have no integrity baseline mechanism \u2014 \`--frozen\` cannot enforce them (multi-registry pinning is deferred).`
);
}
if (v.noBaselines && provBlocks.length === 0) {
throw new Error(
"--frozen: this lock has no integrity baselines (it predates them, or was last locked offline). Run `mcpm lock` online once to record them, then `mcpm up --frozen`."
);
}
if (v.ok && provBlocks.length === 0) return;
const integrityMessages = v.blocks.map((b) => {
switch (b.reason) {
case "drift":
return `\u2717 FROZEN: npm's published record for ${b.identifier}@${b.npmVersion} changed since you locked it (dist.integrity ${b.oldIntegrity.slice(0, 16)}\u2026 \u2192 ${b.newIntegrity.slice(0, 16)}\u2026). --frozen refuses to install on integrity drift. Re-pin with \`mcpm lock\` only if this change is expected.`;
case "format":
return `\u2717 FROZEN: cannot compare npm's published record for ${b.identifier}@${b.npmVersion} against your locked baseline (integrity format changed). Re-run \`mcpm lock\` to refresh it.`;
case "could-not-verify":
return `\u2717 FROZEN: could not verify npm's published record for ${b.identifier}@${b.npmVersion} this run (offline, a yanked version, or no comparable dist.integrity). --frozen requires proof the record matches your lock \u2014 this may be a transient registry error, so re-run; if it persists, drop --frozen.`;
case "missing-baseline":
return `\u2717 FROZEN: no integrity baseline recorded for ${b.name}, though other servers in this lock have one. Re-run \`mcpm lock\` online to record it, then \`mcpm up --frozen\`.`;
default: {
const _never = b;
throw new Error(`unhandled frozen block reason: ${JSON.stringify(_never)}`);
}
}
});
const provenanceMessages = provBlocks.map(frozenProvenanceMessage);
const noticeMessages = v.noBaselines ? [
"\u26A0 FROZEN: this lock has no integrity baselines (predates them / locked offline) \u2014 run `mcpm lock` online to record them."
] : [];
const allMessages = [...noticeMessages, ...integrityMessages, ...provenanceMessages];
deps.output(`
${allMessages.join("\n")}`);
deps.output("\nmcpm verifies the registry's published record, not the code your agent runs at launch.");
const failed = /* @__PURE__ */ new Set([...v.blocks.map((b) => b.name), ...provBlocks.map((b) => b.name)]);
throw new Error(
`frozen: ${failed.size} server(s) failed verification; nothing was installed.`
);
}
function frozenProvenanceMessage(b) {
switch (b.reason) {
case "signer-changed":
return `\u2717 FROZEN: the cryptographic signer for ${b.identifier}@${b.npmVersion} changed since you locked it (${b.detail}). --frozen refuses to install on a provenance signer swap. Re-pin with \`mcpm lock\` only if this re-sign is expected.`;
case "regression":
return `\u2717 FROZEN: provenance for ${b.identifier}@${b.npmVersion} regressed \u2014 it cryptographically verified when you locked it and no longer does (${b.detail}). --frozen refuses to install. If npm's record is unchanged, your mcpm/@sigstore version may have changed since you locked (e.g. after an mcpm upgrade); if that regression is expected, remove this server's stale lock entry and re-lock to re-baseline (a plain \`mcpm lock\` keeps the prior verified baseline).`;
case "unverifiable":
return `\u2717 FROZEN: could not cryptographically re-verify provenance for ${b.identifier}@${b.npmVersion} this run (${b.detail}). --frozen requires proof the attestation still verifies \u2014 this may be a transient error, so re-run; if it persists, investigate before dropping --frozen.`;
default: {
const _never = b.reason;
throw new Error(`unhandled provenance block reason: ${JSON.stringify(_never)}`);
}
}
}
async function runShadowPass(serverNames, deps) {
if (deps.readPins === void 0) {
deps.output("\n\u26A0 shadow check skipped: no pins reader available in this context.");
return 0;
}
let pins;
try {
pins = await deps.readPins();
} catch {
deps.output(
"\n\u26A0 shadow check skipped: ~/.mcpm/pins.json is unreadable (integrity check or corruption)."
);
return 0;
}
const findings = detectNameCollisions(buildInventoryFromPins(pins, serverNames));
const noBaseline = serversWithoutBaseline(pins, serverNames);
const checked = serverNames.length - noBaseline.length;
deps.output(
`
Shadow check: compared guarded tool inventories for ${checked} of ${serverNames.length} server(s).`
);
if (noBaseline.length > 0) {
deps.output(
` ${noBaseline.length} server(s) have NO guard baseline yet (${noBaseline.join(", ")}) \u2014 this check cannot see their tools, so a clean result does NOT mean no shadowing. Run them under \`mcpm guard\` (then re-run \`mcpm up\`) to include them.`
);
}
for (const f of findings) {
deps.output(
`
\u26A0 SHADOW: tool "${f.toolName}" is exposed by ${f.servers.length} servers (${f.servers.join(", ")}). A lower-trust server can shadow a tool meant for another, so agent calls to "${f.toolName}" are ambiguous. This can also be benign (two servers of the same kind legitimately export the same tool). Review which server should own it. (Exact-name match only \u2014 a homoglyph/case variant evades this check.)`
);
}
return findings.length;
}
async function processUrlServer(name, url, clients, policy, consentedUnguarded, options, deps) {
if (options.allowUrlServers === false) {
return {
name,
status: "blocked",
message: "URL servers are not permitted via the MCP surface"
};
}
const consented = options.allowUnguarded === true || policy?.allowUrlServers === true || consentedUnguarded.has(name);
if (!consented) {
return {
name,
status: "blocked",
message: "URL/HTTP-transport server runs UNGUARDED \u2014 no runtime inspection is possible (mcpm's guard relay only wraps stdio servers). Re-run with --allow-unguarded or set policy.allowUrlServers: true to install it WITHOUT protection."
};
}
let urlError;
try {
validateRemoteUrl(url);
} catch (err) {
urlError = err instanceof Error ? err.message : String(err);
}
const cursorClients = clients.filter((c) => c === "cursor");
if (cursorClients.length === 0) {
return {
name,
status: "skipped",
message: "URL server \u2014 no Cursor client detected (only Cursor supports URL transport)"
};
}
if (options.dryRun) {
return urlError ? { name, status: "skipped", message: `would reject URL ${url}: ${urlError}` } : { name, status: "skipped", message: `would install URL ${url} to Cursor` };
}
if (urlError) {
return { name, status: "blocked", message: urlError };
}
for (const clientId of cursorClients) {
const adapter = deps.getAdapter(clientId);
const configPath = deps.getPath(clientId);
await adapter.addServer(configPath, name, { url }, { force: true });
}
return { name, status: "installed", message: `URL ${url} \u2192 Cursor` };
}
async function resolveEnvVars(serverName, server, envFileVars, options, deps) {
const envDecl = isRegistryServer(server) || isUrlServer(server) ? server.env : void 0;
if (!envDecl) return { env: {}, storedCount: 0 };
const resolved = {};
const secretKeys = /* @__PURE__ */ new Set();
for (const [key, decl] of Object.entries(envDecl)) {
const fromEnv = options.allowProcessEnv === false ? void 0 : process.env[key];
const fromFile = envFileVars[key];
const fromDefault = decl.default;
let value;
if (fromEnv !== void 0) {
value = fromEnv;
} else if (fromFile !== void 0) {
value = fromFile;
} else if (fromDefault !== void 0) {
value = fromDefault;
} else if (decl.required) {
if (options.ci) {
throw new Error(
`Required env var "${key}" for "${serverName}" is not set. Set it in process.env or .env file (--ci mode, no interactive prompt).`
);
}
value = await deps.promptEnvVar(key, decl.secret);
}
if (value === void 0) continue;
resolved[key] = value;
if (decl.secret) secretKeys.add(key);
}
return applyKeychainSecrets({
serverName,
resolvedEnv: resolved,
isSecret: (key) => secretKeys.has(key),
mode: options.secrets ?? "plaintext",
setSecrets: deps.setSecrets
});
}
async function handleStrictRemoval(stackFile, clients, options, deps, results) {
const declaredNames = new Set(Object.keys(stackFile.servers));
for (const clientId of clients) {
const adapter = deps.getAdapter(clientId);
const configPath = deps.getPath(clientId);
const installed = await adapter.read(configPath);
for (const name of Object.keys(installed)) {
if (declaredNames.has(name)) continue;
if (options.ci && !options.yes) {
throw new Error(
`--strict --ci requires --yes to remove servers not in mcpm.yaml. Server "${name}" in ${clientId} would be removed.`
);
}
if (!options.ci && options.yes !== true) {
const confirmed = await deps.confirm(
`Remove "${name}" from ${clientId}? (not in mcpm.yaml)`
);
if (!confirmed) continue;
}
await adapter.removeServer(configPath, name);
results.push({
name,
status: "removed",
message: `removed from ${clientId} (not in mcpm.yaml)`
});
deps.recordResult?.({ name, status: "removed" });
deps.output(` - ${name}: removed from ${clientId}`);
}
}
}
function statusIcon(status) {
switch (status) {
case "installed":
return "\u2713";
case "removed":
return "\u2212";
case "skipped":
return "\u2022";
case "blocked":
return "\u2717";
case "failed":
return "\u2717";
default:
return "?";
}
}
function registerUpCommand(program) {
program.command("up").description("Install all servers from mcpm.yaml with trust verification").option("-f, --file <path>", "path to mcpm.yaml", "mcpm.yaml").option("-p, --profile <name>", "install only servers matching this profile").option("--dry-run", "show what would be installed without making changes").option("--ci", "CI mode: no interactive prompts, exit nonzero on failure").option("--strict", "remove servers not declared in mcpm.yaml").option("-y, --yes", "skip confirmation prompts (required with --strict --ci)").option("--secrets <mode>", "where to store secret env vars: 'keychain' (encrypted in ~/.mcpm, resolved by mcpm guard at launch) or 'plaintext' (default); 'keychain' is rejected with --ci", parseSecretsMode).option("--allow-unguarded", "permit URL/HTTP-transport servers to run WITHOUT runtime guard inspection (no relay wraps a non-stdio transport); records consent so future runs stay quiet").option("--check-shadowing", "report tool-name collisions across guarded servers (a shadowing signal); advisory interactively, exits nonzero under --ci").option("--frozen", "fail closed: BEFORE installing, verify every locked npm server's published integrity AND re-verify Sigstore provenance for crypto-verified servers, then BLOCK (install nothing, exit nonzero) on integrity drift / provenance regression / unverifiable / missing baseline \u2014 the CI supply-chain freeze gate").action(
async (opts) => {
const client = new RegistryClient();
const { readUnguardedConsent, writeUnguardedConsent, mergeUnguarded } = await import("./unguarded-GJO5WRM7.js");
try {
await handleUp(
{
stackFile: opts.file,
profile: opts.profile,
dryRun: opts.dryRun,
ci: opts.ci,
strict: opts.strict,
yes: opts.yes,
secrets: opts.secrets,
allowUnguarded: opts.allowUnguarded,
checkShadowing: opts.checkShadowing,
frozen: opts.frozen
},
{
detectClients: detectInstalledClients,
getAdapter,
getPath: getConfigPath,
getServer: (name, version) => client.getServer(name, version),
scanTier1,
checkScannerAvailable,
scanTier2: (name) => scanTier2(name),
computeTrustScore,
now: () => Date.now(),
runLock: async (stackFile) => {
const { writeFile } = await import("fs/promises");
await handleLock(
{ stackFile },
{
getServerVersions: (name) => client.getServerVersions(name),
getServer: (name, v) => client.getServer(name, v),
scanTier1,
checkScannerAvailable,
scanTier2: (name) => scanTier2(name),
computeTrustScore,
now: () => Date.now(),
writeLockFile: (path, content) => writeFile(path, content, { encoding: "utf-8", mode: 384 }),
fetchNpmIntegrity,
// F8/B3: auto-lock must record the crypto-`verified` provenance
// baseline too, or the verify-time gate is vacuous for up-locked repos.
fetchNpmProvenance: (id, ver, sri) => fetchNpmProvenance(id, ver, { integritySri: sri }),
output: stdoutOutput
}
);
},
confirm,
promptEnvVar: async (name, isSecret) => {
if (isSecret) {
return password({ message: `${name}:` });
}
return input({ message: `${name}:` });
},
output: stdoutOutput,
setSecrets,
fetchNpmIntegrity,
fetchNpmProvenance: (id, v, o) => fetchNpmProvenance(id, v, o),
readPins,
readUnguardedConsent,
recordUnguardedConsent: async (names) => {
const previous = await readUnguardedConsent();
await writeUnguardedConsent(mergeUnguarded(previous, names));
}
}
);
} catch (err) {
console.error(chalk.red(err.message));
process.exit(1);
}
}
);
}
export {
memoizeIntegrity,
classifyIntegrity,
frozenVerdict,
classifyProvenance,
handleUp,
registerUpCommand
};
//# sourceMappingURL=chunk-TX4RGSW7.js.map

Sorry, the diff of this file is too big to display

#!/usr/bin/env node
import {
confineSandboxRoot,
hashConfineProfile,
readConfineStore,
withProfile,
writeConfineStore
} from "./chunk-544DEV2D.js";
import {
SANDBOX_EXEC_PATH,
defaultWrapContext,
isConfineBackendAvailable,
isWrapped,
unwrapEntry,
wrapEntry
} from "./chunk-WYSMWP2R.js";
import "./chunk-OIFKZA4V.js";
import {
sanitizeForTerminal
} from "./chunk-FEXJHHDM.js";
import {
getAdapter
} from "./chunk-W4IAFBUN.js";
import {
isNewUnguarded,
mergeUnguarded,
readUnguardedConsent,
writeUnguardedConsent
} from "./chunk-MLVDFLDQ.js";
import {
placeholderEnvKeys
} from "./chunk-GZ3WCRLG.js";
import {
detectInstalledClients
} from "./chunk-6R7TL5O2.js";
import {
getConfigPath
} from "./chunk-R4R2VPDA.js";
import "./chunk-3X76P3FG.js";
// src/guard/cli.ts
import chalk from "chalk";
// src/guard/orchestrator.ts
import { copyFile } from "fs/promises";
function enableGuardAcrossClients(deps, filter) {
return runAcrossClients(deps, "enable", filter);
}
function disableGuardAcrossClients(deps, filter) {
return runAcrossClients(deps, "disable", filter);
}
async function runAcrossClients(deps, action, filter) {
const targetClients = await selectTargetClients(deps, filter?.client);
const consentedUnguarded = action === "enable" && deps.readUnguardedConsent ? await deps.readUnguardedConsent() : [];
const plans = await Promise.all(
targetClients.map(
(clientId) => planForClient(clientId, deps, action, filter?.server, consentedUnguarded)
)
);
for (const plan of plans) {
if (plan.transforms.length === 0) continue;
const configPath = deps.getConfigPath(plan.clientId);
await copyFile(configPath, `${configPath}.guard-${action}.bak`).catch(() => void 0);
}
const reports = [];
for (const plan of plans) {
reports.push(await applyPlan(plan, deps));
}
if (filter?.server !== void 0) {
const matched = reports.some((r) => r.servers.some((s) => s.name === filter.server));
if (!matched) {
throw new Error(
`--server "${filter.server}" not found in any detected client config. Run \`mcpm guard status\` to see available servers.`
);
}
}
if (deps.recordUnguardedConsent) {
const nowUnguarded = [
...new Set(
reports.flatMap((r) => r.servers.filter((s) => s.status === "unguarded").map((s) => s.name))
)
];
const previous = new Set(consentedUnguarded);
const newlyConsented = nowUnguarded.filter((n) => !previous.has(n));
if (newlyConsented.length > 0) {
await deps.recordUnguardedConsent(newlyConsented).catch(() => void 0);
}
}
return summarize(action, reports);
}
async function statusAcrossClients(deps) {
const targetClients = await deps.detectClients();
const clients = await Promise.all(
targetClients.map(async (clientId) => {
try {
const adapter = deps.getAdapter(clientId);
const entries = await adapter.read(deps.getConfigPath(clientId));
const servers = Object.entries(entries).map(([name, entry]) => ({
name,
wrapped: isWrapped(entry),
// A non-stdio (url-transport) entry has no command — it cannot be
// wrapped, so even when "not wrapped" it is specifically UNGUARDED.
unguarded: !isWrapped(entry) && !entry.command
}));
return {
clientId,
wrapped: servers.filter((s) => s.wrapped).length,
unwrapped: servers.filter((s) => !s.wrapped).length,
servers
};
} catch (err) {
return {
clientId,
wrapped: 0,
unwrapped: 0,
servers: [],
error: err instanceof Error ? err.message : String(err)
};
}
})
);
return {
clients,
totalWrapped: clients.reduce((sum, c) => sum + c.wrapped, 0),
totalUnwrapped: clients.reduce((sum, c) => sum + c.unwrapped, 0)
};
}
async function selectTargetClients(deps, clientFilter) {
const detected = await deps.detectClients();
if (clientFilter === void 0) return detected;
if (!detected.includes(clientFilter)) {
throw new Error(
`Client "${clientFilter}" not detected. Available: ${detected.join(", ") || "(none)"}`
);
}
return [clientFilter];
}
async function planForClient(clientId, deps, action, serverFilter, consentedUnguarded) {
const adapter = deps.getAdapter(clientId);
let entries;
try {
entries = await adapter.read(deps.getConfigPath(clientId));
} catch (err) {
return {
clientId,
action,
transforms: [],
skipped: [],
unguarded: [],
readError: err instanceof Error ? err.message : String(err)
};
}
const transforms = [];
const skipped = [];
const unguarded = [];
const consentedSet = new Set(consentedUnguarded);
for (const [name, entry] of Object.entries(entries)) {
if (serverFilter !== void 0 && name !== serverFilter) continue;
if (action === "enable") {
if (isWrapped(entry)) {
skipped.push({ name, reason: "already wrapped" });
continue;
}
if (!entry.command) {
if (deps.allowUnguarded === true || consentedSet.has(name)) {
unguarded.push({
name,
reason: "unguarded (consented) \u2014 runs WITHOUT runtime inspection; this grants consent, it does not add protection. The only true fix is a streamable-HTTP relay (not yet implemented)."
});
} else {
skipped.push({
name,
reason: "DENIED: URL/HTTP-transport server runs UNGUARDED \u2014 no runtime inspection is possible (the guard relay only wraps stdio servers). Re-run `mcpm guard enable --allow-unguarded` to permit it without protection."
});
}
continue;
}
transforms.push({
name,
nextEntry: wrapEntry(name, entry, deps.wrapContext, deps.confineMarkers?.get(name))
});
} else {
if (!isWrapped(entry)) {
skipped.push({ name, reason: "not wrapped" });
continue;
}
const unwrapped = unwrapEntry(entry);
if (unwrapped === null) {
skipped.push({ name, reason: "wrap marker malformed; .bak restore may be required" });
continue;
}
transforms.push({ name, nextEntry: unwrapped });
}
}
return { clientId, action, transforms, skipped, unguarded };
}
async function applyPlan(plan, deps) {
if (plan.readError) {
return {
clientId: plan.clientId,
servers: [],
error: plan.readError
};
}
const adapter = deps.getAdapter(plan.clientId);
const configPath = deps.getConfigPath(plan.clientId);
const servers = [];
for (const { name, nextEntry } of plan.transforms) {
try {
await adapter.replaceServer(configPath, name, nextEntry);
servers.push({ name, status: plan.action === "enable" ? "wrapped" : "unwrapped" });
} catch (err) {
servers.push({
name,
status: "skipped",
reason: err instanceof Error ? err.message : String(err)
});
}
}
for (const skip of plan.skipped) {
servers.push({ name: skip.name, status: "skipped", reason: skip.reason });
}
for (const u of plan.unguarded) {
servers.push({ name: u.name, status: "unguarded", reason: u.reason });
}
return { clientId: plan.clientId, servers };
}
function summarize(action, reports) {
let totalChanged = 0;
let totalSkipped = 0;
let totalUnguarded = 0;
let errors = 0;
for (const report of reports) {
if (report.error) errors++;
for (const server of report.servers) {
if (server.status === "wrapped" || server.status === "unwrapped") totalChanged++;
else if (server.status === "unguarded") totalUnguarded++;
else totalSkipped++;
}
}
return { action, clients: reports, totalChanged, totalSkipped, totalUnguarded, errors };
}
// src/guard/cli.ts
import os from "os";
// src/guard/confine/derive.ts
import { createHash } from "crypto";
import path from "path";
var SECRET_DIR_SEGMENTS = [
// SSH / cloud / package-registry / signing credentials.
".ssh",
".aws",
".gnupg",
".config/gh",
".config/gcloud",
".npmrc",
".docker",
".kube",
".netrc",
".git-credentials",
".cargo/credentials",
".cargo/credentials.toml",
".pypirc",
// OS keychains + browser cookie stores (highest-value credential theft).
"Library/Keychains",
"Library/Application Support/Google/Chrome",
"Library/Application Support/Firefox",
"Library/Cookies",
// mcpm's own store (secrets.enc.json, pins, policy, the confine store itself).
".mcpm",
// Sibling MCP client configs (each can hold another server's plaintext secrets).
"Library/Application Support/Claude",
"Library/Application Support/Cursor",
"Library/Application Support/Code/User",
"Library/Application Support/Windsurf",
".cursor",
".vscode",
".codeium",
".claude.json",
// Claude Code user-global config (see src/config/paths.ts getConfigPath)
".claude",
// Claude Code also keeps state under ~/.claude/
".gemini"
// Gemini CLI user-global config (~/.gemini/settings.json)
];
var WRITE_ALLOW_HOME_SEGMENTS = [".npm", ".cache", "Library/Caches"];
var WRITE_ALLOW_STATIC = [
"/tmp",
"/private/tmp",
"/var/tmp",
"/var/folders",
"/private/var/folders",
"/dev"
];
var LAUNCHER_COMMANDS = /* @__PURE__ */ new Set([
"npx",
"npm",
"pnpm",
"yarn",
"bun",
"bunx",
"uv",
"uvx",
"pip",
"pip3",
"pipx",
"pipenv",
"poetry",
"docker"
]);
function commandBasename(command) {
const base = path.basename(command).toLowerCase();
const dot = base.indexOf(".");
return dot === -1 ? base : base.slice(0, dot);
}
function classifyNet(command) {
return LAUNCHER_COMMANDS.has(commandBasename(command)) ? "all" : "none";
}
function safeServerSegment(serverName) {
if (serverName.length === 0) throw new Error("confine: empty server name");
const cleaned = serverName.replace(/[^A-Za-z0-9._@-]/g, "_").replace(/\.{2,}/g, "_").replace(/^\.+/, "_");
const suffix = createHash("sha256").update(serverName).digest("hex").slice(0, 8);
return `${cleaned}-${suffix}`;
}
function deriveDefaultProfile(input) {
if (input.command.length === 0) throw new Error("confine: empty command");
const scratchDir = path.join(input.sandboxRoot, safeServerSegment(input.serverName));
const readDeny = SECRET_DIR_SEGMENTS.map((seg) => path.join(input.home, seg));
const writeAllow = [
scratchDir,
...WRITE_ALLOW_STATIC,
input.tmpDir,
...WRITE_ALLOW_HOME_SEGMENTS.map((seg) => path.join(input.home, seg))
];
return {
tier: "standard",
require_confine: input.requireConfine === true,
read_deny: dedupeSorted(readDeny),
write_allow: dedupeSorted(writeAllow),
net: input.net ?? classifyNet(input.command),
scratch_dir: scratchDir,
captured_at: input.capturedAt
};
}
function dedupeSorted(paths) {
return [...new Set(paths)].sort();
}
// src/guard/cli.ts
var CLIENT_LABELS = {
"claude-desktop": "Claude Desktop",
"claude-code": "Claude Code",
cursor: "Cursor",
vscode: "VS Code",
windsurf: "Windsurf",
"gemini-cli": "Gemini CLI"
};
function buildDeps(extra = {}) {
return {
detectClients: detectInstalledClients,
getAdapter,
getConfigPath,
wrapContext: defaultWrapContext(),
readUnguardedConsent,
recordUnguardedConsent: async (names) => {
const previous = await readUnguardedConsent();
await writeUnguardedConsent(mergeUnguarded(previous, names));
},
...extra
};
}
async function runEnableCommand(opts) {
const previousConsented = await readUnguardedConsent();
if (opts.dryRun === true) {
await printEnableDryRun(opts);
return;
}
if (opts.confine === "off") {
opts.write("OS confinement: skipped (--confine off).\n");
}
let confineMarkers;
if (opts.confine === "standard") {
try {
confineMarkers = await computeConfineMarkers({
client: opts.client,
server: opts.server,
write: opts.write
});
} catch (err) {
opts.write(
chalk.yellow(`
\u26A0 OS confinement aborted: ${sanitizeForTerminal(err.message)}`) + "\n"
);
return;
}
}
const deps = buildDeps({ allowUnguarded: opts.allowUnguarded, confineMarkers });
const summary = await enableGuardAcrossClients(deps, opts);
printEnableDisable(summary, opts);
printUnguardedWarning(summary, previousConsented, opts);
if (confineMarkers !== void 0) printConfineNotice(confineMarkers, opts);
printRestartReminder(opts);
}
async function printEnableDryRun(opts) {
const deps = buildDeps({ allowUnguarded: opts.allowUnguarded });
const status = await statusAcrossClients(deps);
const confineNote = opts.confine === "standard" ? " (with OS confinement)" : "";
opts.write(`Dry-run: planned wraps${confineNote}
`);
for (const c of status.clients) {
if (opts.client !== void 0 && c.clientId !== opts.client) continue;
const candidates = c.servers.filter((s) => {
if (opts.server !== void 0 && s.name !== opts.server) return false;
return !s.wrapped;
});
opts.write(` ${CLIENT_LABELS[c.clientId]}: would wrap ${candidates.length} server(s)
`);
for (const s of candidates) opts.write(` + ${s.name}
`);
}
}
async function collectConfineTargets(opts) {
const targets = /* @__PURE__ */ new Map();
let clients;
try {
clients = await detectInstalledClients();
} catch {
return targets;
}
if (opts.client !== void 0) clients = clients.filter((c) => c === opts.client);
for (const clientId of clients) {
let entries;
try {
entries = await getAdapter(clientId).read(getConfigPath(clientId));
} catch {
continue;
}
for (const [name, entry] of Object.entries(entries)) {
if (opts.server !== void 0 && name !== opts.server) continue;
if (!entry.command || isWrapped(entry)) continue;
if (!targets.has(name)) targets.set(name, { command: entry.command, args: entry.args });
}
}
return targets;
}
async function computeConfineMarkers(opts) {
const targets = await collectConfineTargets(opts);
if (targets.size === 0) return /* @__PURE__ */ new Map();
let store;
try {
store = await readConfineStore();
} catch (err) {
throw new Error(
`cannot read the confine store (~/.mcpm/guard-confine.yaml): ${err.message} Review it for unauthorized changes; if you edited it intentionally, restore or remove it. Refusing to enroll \u2014 the store was left untouched.`
);
}
const { markers, storeToWrite } = await resolveConfineMarkers(targets, store, opts);
if (storeToWrite !== null) await writeConfineStore(storeToWrite);
return markers;
}
async function resolveConfineMarkers(targets, store, opts) {
const markers = /* @__PURE__ */ new Map();
const home = os.homedir();
const sandboxRoot = await confineSandboxRoot();
const tmpDir = os.tmpdir();
const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
let next = store;
let added = 0;
for (const [name, target] of targets) {
const existing = Object.hasOwn(store.servers, name) ? store.servers[name] : void 0;
if (existing !== void 0) {
markers.set(name, {
profileHash: hashConfineProfile(existing),
required: existing.require_confine
});
continue;
}
const profile = deriveDefaultProfile({
serverName: name,
command: target.command,
args: target.args,
home,
sandboxRoot,
tmpDir,
capturedAt
});
next = withProfile(next, name, profile);
added += 1;
markers.set(name, {
profileHash: hashConfineProfile(profile),
required: profile.require_confine
});
}
return { markers, storeToWrite: added > 0 ? next : null };
}
function printConfineNotice(confineMarkers, opts) {
if (confineMarkers.size === 0) {
opts.write("\nOS confinement: no unwrapped stdio servers to enroll.\n");
return;
}
opts.write(
`
\u{1F512} ${confineMarkers.size} server(s) enrolled in OS confinement (standard tier). Run \`mcpm guard doctor-confine\` for status.
`
);
if (!isConfineBackendAvailable()) {
opts.write(
chalk.yellow(
"\u26A0 No OS sandbox backend on this platform \u2014 enrolled servers run UNCONFINED until a backend is present (they are NOT blocked; a require_confine server would fail closed)."
) + "\n"
);
}
}
async function runDoctorConfineCommand(opts) {
const backendAvailable = isConfineBackendAvailable();
let servers = [];
let storeError;
try {
const store = await readConfineStore();
servers = Object.entries(store.servers).map(([name, p]) => ({
name,
tier: p.tier,
net: p.net,
requireConfine: p.require_confine
}));
} catch (err) {
storeError = err.message;
}
opts.write(
opts.json === true ? renderDoctorConfineJson(backendAvailable, servers, storeError) : renderDoctorConfineText(backendAvailable, servers, storeError)
);
}
function renderDoctorConfineJson(backendAvailable, servers, storeError) {
return JSON.stringify(
{
platform: process.platform,
backendAvailable,
sandboxExecPath: process.platform === "darwin" ? SANDBOX_EXEC_PATH : null,
// sanitize: an OS error message can carry control chars / ANSI (parity
// with the text branch, which already sanitizes).
storeError: storeError !== void 0 ? sanitizeForTerminal(storeError) : null,
servers
},
null,
2
) + "\n";
}
function renderDoctorConfineText(backendAvailable, servers, storeError) {
const out = ["mcpm guard doctor-confine", ""];
out.push(` platform : ${process.platform}`);
let backendLine = ` sandbox backend : ${backendAvailable ? "available" : "UNAVAILABLE"}`;
if (process.platform === "darwin") backendLine += ` (${SANDBOX_EXEC_PATH})`;
out.push(backendLine);
if (!backendAvailable) {
out.push(
" \u2192 enrolled servers run UNCONFINED here (hybrid posture); a require_confine server fails closed."
);
}
out.push("");
if (storeError !== void 0) {
out.push(` \u26A0 could not read the confine store: ${sanitizeForTerminal(storeError)}`, "");
return out.join("\n");
}
if (servers.length === 0) {
out.push(" No servers enrolled in confinement. Enroll with `mcpm guard enable --confine`.", "");
return out.join("\n");
}
out.push(` Enrolled servers (${servers.length}):`);
for (const s of servers) {
out.push(` ${sanitizeForTerminal(s.name)} \u2014 tier=${s.tier} net=${s.net} require_confine=${s.requireConfine}`);
}
out.push("", " Run `mcpm guard status` for per-client wrap state.", "");
return out.join("\n");
}
function printUnguardedWarning(summary, previousConsented, opts) {
const current = [
...new Set(
summary.clients.flatMap(
(c) => c.servers.filter((s) => s.status === "unguarded").map((s) => s.name)
)
)
].sort();
if (current.length === 0) return;
if (isNewUnguarded(current, previousConsented)) {
const prev = new Set(previousConsented);
const newlyConsented = current.filter((n) => !prev.has(n));
const alreadyCount = current.length - newlyConsented.length;
opts.write(
chalk.yellow(
"\n\u26A0 UNGUARDED: the following server(s) run WITHOUT runtime inspection \u2014 the guard relay cannot wrap a non-stdio (URL/HTTP) transport:"
) + "\n"
);
for (const name of newlyConsented) opts.write(` \u26A0 ${sanitizeForTerminal(name)}
`);
if (alreadyCount > 0) {
opts.write(` (+${alreadyCount} previously consented)
`);
}
opts.write(
"This grants consent to run them UNGUARDED \u2014 it does NOT add protection. The only true fix is a streamable-HTTP relay (not yet implemented). Future `enable` runs stay quiet unless a NEW unguarded server appears.\n"
);
} else {
opts.write(
`
${current.length} server(s) running unguarded (previously consented): ${current.map((n) => sanitizeForTerminal(n)).join(", ")}
`
);
}
}
async function runDisableCommand(opts) {
const deps = buildDeps();
const summary = await disableGuardAcrossClients(deps, opts);
printEnableDisable(summary, opts);
printRestartReminder(opts);
await warnUnresolvablePlaceholders(opts);
}
async function warnUnresolvablePlaceholders(opts) {
let clients;
try {
clients = await detectInstalledClients();
} catch {
return;
}
const affected = [];
for (const clientId of clients) {
if (opts.client !== void 0 && clientId !== opts.client) continue;
let entries;
try {
entries = await getAdapter(clientId).read(getConfigPath(clientId));
} catch (err) {
if (err.code !== "ENOENT") {
opts.write(
` (could not read ${CLIENT_LABELS[clientId]} config: ${sanitizeForTerminal(err.message)})
`
);
}
continue;
}
for (const [name, entry] of Object.entries(entries)) {
if (opts.server !== void 0 && name !== opts.server) continue;
const keys = placeholderEnvKeys(entry.env);
if (keys.length > 0) affected.push({ client: clientId, server: name, keys });
}
}
if (affected.length === 0) return;
opts.write(
"\n\x1B[33mwarning: these servers reference encrypted secrets (mcpm:keychain:\u2026) that only resolve while mcpm guard is enabled. Without guard they receive the literal placeholder and will fail to start:\x1B[0m\n"
);
for (const a of affected) {
opts.write(
` - ${sanitizeForTerminal(a.server)} (${CLIENT_LABELS[a.client]}): ${formatAffectedKeys(a.keys)}
`
);
}
opts.write(
"Re-enable with `mcpm guard enable`, or replace those env values with plaintext.\n"
);
}
function formatAffectedKeys(keys) {
return keys.map((k) => sanitizeForTerminal(k)).join(", ");
}
async function runStatusCommand(opts) {
const deps = buildDeps();
const status = await statusAcrossClients(deps);
if (status.clients.length === 0) {
if (opts.helpFallback) {
opts.helpFallback();
return;
}
opts.write("No MCP clients detected.\n");
return;
}
if (status.totalWrapped === 0 && opts.helpFallback) {
opts.helpFallback();
return;
}
printStatus(status, opts);
}
function printEnableDisable(summary, opts) {
const verb = summary.action === "enable" ? "wrapped" : "unwrapped";
opts.write(`mcpm guard ${summary.action}: ${summary.totalChanged} ${verb}, `);
opts.write(`${summary.totalSkipped} skipped`);
if (summary.totalUnguarded > 0) {
opts.write(`, ${summary.totalUnguarded} unguarded`);
}
opts.write(`, ${summary.errors} error(s)
`);
for (const client of summary.clients) {
printClientReport(client, opts);
}
}
function printClientReport(report, opts) {
opts.write(` ${CLIENT_LABELS[report.clientId]}:
`);
if (report.error !== void 0) {
opts.write(` error: ${sanitizeForTerminal(report.error)}
`);
return;
}
if (report.servers.length === 0) {
opts.write(` (no servers)
`);
return;
}
for (const s of report.servers) {
const symbol = s.status === "wrapped" ? "+" : s.status === "unwrapped" ? "-" : s.status === "unguarded" ? "\u26A0" : "\xB7";
const reason = s.reason !== void 0 ? ` (${sanitizeForTerminal(s.reason)})` : "";
opts.write(` ${symbol} ${sanitizeForTerminal(s.name)}${reason}
`);
}
}
function printStatus(status, opts) {
opts.write(`mcpm guard status: ${status.totalWrapped} wrapped, ${status.totalUnwrapped} unwrapped
`);
for (const c of status.clients) {
opts.write(` ${CLIENT_LABELS[c.clientId]}: ${c.wrapped} wrapped / ${c.unwrapped} unwrapped
`);
if (c.error !== void 0) {
opts.write(` error: ${sanitizeForTerminal(c.error)}
`);
continue;
}
for (const s of c.servers) {
const marker = s.wrapped ? "+" : s.unguarded ? "\u26A0 UNGUARDED" : "\xB7";
opts.write(` ${marker} ${sanitizeForTerminal(s.name)}
`);
}
}
}
function printRestartReminder(opts) {
opts.write(
"\n\u2192 Restart your IDE (Claude Desktop / Cursor / VS Code / Windsurf) for changes to take effect.\n"
);
}
async function runCleanupCommand(opts) {
const deps = buildDeps();
const status = await statusAcrossClients(deps);
const installedServerNames = /* @__PURE__ */ new Set();
for (const c of status.clients) {
for (const s of c.servers) installedServerNames.add(s.name);
}
const { readPins, writePins, clearServerPins, PinsIntegrityError } = await import("./pins-ETT4XWEP.js");
let pins;
try {
pins = await readPins();
} catch (err) {
if (err instanceof PinsIntegrityError) {
opts.write(
`mcpm guard cleanup: cannot read ~/.mcpm/pins.json \u2014 integrity check failed.
${err.message}
Refusing to prune until this is resolved.
`
);
} else {
opts.write(
`mcpm guard cleanup: cannot read ~/.mcpm/pins.json \u2014 ${err.message}
Refusing to prune until this is resolved.
`
);
}
return;
}
const orphanPinned = [];
for (const serverName of Object.keys(pins.servers)) {
if (!installedServerNames.has(serverName)) orphanPinned.push(serverName);
}
if (orphanPinned.length === 0) {
opts.write("mcpm guard cleanup: nothing to prune (0 orphan pins, 0 orphan wraps).\n");
return;
}
opts.write(`mcpm guard cleanup: ${orphanPinned.length} orphan pin entr${orphanPinned.length === 1 ? "y" : "ies"} found:
`);
for (const s of orphanPinned) opts.write(` - ${sanitizeForTerminal(s)}
`);
if (!opts.apply) {
opts.write("\nDry run. Re-run with --yes to prune.\n");
return;
}
let next = pins;
for (const serverName of orphanPinned) next = clearServerPins(next, serverName);
await writePins(next);
opts.write(`
Pruned ${orphanPinned.length} orphan pin entr${orphanPinned.length === 1 ? "y" : "ies"} from ~/.mcpm/pins.json.
`);
}
export {
computeConfineMarkers,
formatAffectedKeys,
printUnguardedWarning,
runCleanupCommand,
runDisableCommand,
runDoctorConfineCommand,
runEnableCommand,
runStatusCommand
};
//# sourceMappingURL=cli-JBJQ3BY2.js.map

Sorry, the diff of this file is too big to display

#!/usr/bin/env node
import {
activeSecretBackend,
applyKeychainSecrets,
deleteSecret,
deriveKeychainId,
getSecret,
listAll,
migrateToKeychain,
parsePlaceholder,
placeholderEnvKeys,
resolveEnvPlaceholders,
setSecret,
setSecrets,
toPlaceholder
} from "./chunk-GZ3WCRLG.js";
import "./chunk-3X76P3FG.js";
export {
activeSecretBackend,
applyKeychainSecrets,
deleteSecret,
deriveKeychainId,
getSecret,
listAll,
migrateToKeychain,
parsePlaceholder,
placeholderEnvKeys,
resolveEnvPlaceholders,
setSecret,
setSecrets,
toPlaceholder
};
//# sourceMappingURL=keychain-FSY3TEBD.js.map
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
#!/usr/bin/env node
import {
buildDriftFinding,
buildHandshakeDriftFinding,
classifyDrift,
classifyHandshakeDrift,
inspectForDrift,
inspectHandshakeForDrift
} from "./chunk-5W5Z3VZG.js";
import {
PolicyIntegrityError,
expireStale,
readPolicy
} from "./chunk-CYYYMOUS.js";
import {
hasToolsList,
inspectFrame,
mergeInspect,
withReplyToOrigin
} from "./chunk-XLPT6EJQ.js";
import {
hashConfineProfile,
loadProfile
} from "./chunk-544DEV2D.js";
import {
OWASP_MCP_TOP_10
} from "./chunk-4ANBMGU5.js";
import {
fieldHashesOf,
handshakeCapabilityKeys,
handshakeFieldHashesOf,
hashHandshake,
hashToolDefinition,
lookupHandshake,
readPins,
writePins
} from "./chunk-DDCTUMSZ.js";
import {
hashOriginalEntry,
isConfineBackendAvailable,
wrapForConfinement
} from "./chunk-WYSMWP2R.js";
import "./chunk-OIFKZA4V.js";
import {
sanitizeForTerminal
} from "./chunk-FEXJHHDM.js";
import {
resolveEnvPlaceholders
} from "./chunk-GZ3WCRLG.js";
import {
getStorePath
} from "./chunk-3X76P3FG.js";
import {
ACTION_RANK,
defaultActionForFinding,
inspectMessage
} from "./chunk-WT6V33F2.js";
// src/guard/relay.ts
import { spawn } from "child_process";
import { ReadBuffer, serializeMessage } from "@modelcontextprotocol/sdk/shared/stdio.js";
var GUARD_BLOCK_ERROR_CODE = -32099;
function makeBlockResponse(blocked, result) {
if (!("id" in blocked) || blocked.id === void 0) return null;
const finding = result.findings[0];
return {
jsonrpc: "2.0",
id: blocked.id,
error: {
code: GUARD_BLOCK_ERROR_CODE,
message: "BLOCKED by mcpm-guard",
data: finding ? {
signature_id: finding.signature_id,
category: finding.category,
severity: finding.severity,
matched_text_excerpt: finding.matched_text_excerpt,
remediation: finding.remediation
} : void 0
}
};
}
var SAFE_ENV_PASSTHROUGH = /* @__PURE__ */ new Set([
"PATH",
"HOME",
"TMPDIR",
"TEMP",
"TMP",
"LANG",
"LC_ALL",
"USER",
"SHELL"
]);
function buildSafeEnv(source = process.env) {
const out = {};
for (const [k, v] of Object.entries(source)) {
if (SAFE_ENV_PASSTHROUGH.has(k) || k.startsWith("LC_")) out[k] = v;
}
return out;
}
var MAX_BUFFER_BYTES = 64 * 1024 * 1024;
function startRelay(opts) {
const env = opts.env ?? buildSafeEnv();
const child = opts.spawnChild ? opts.spawnChild(opts.command, opts.args, env) : spawn(opts.command, [...opts.args], {
env,
stdio: ["pipe", "pipe", "inherit"]
// stderr passthrough — preserves IDE diagnostics
});
const forwardSignal = (sig) => {
if (!child.killed) child.kill(sig);
};
let settled = false;
let resolveExit;
const exit = new Promise((resolve) => {
resolveExit = resolve;
});
child.on("error", (err) => {
if (settled) return;
settled = true;
process.off("SIGTERM", forwardSignal);
process.off("SIGINT", forwardSignal);
const code = err.code ?? "SPAWN-FAILED";
opts.onEvent?.({
ts: (/* @__PURE__ */ new Date()).toISOString(),
direction: "child->parent",
action: "block",
findings: [
{
signature_id: "spawn-failure",
category: "RELAY",
severity: "critical",
target: "tool_response",
matched_text_excerpt: `${code}: ${err.message}`,
remediation: "The wrapped MCP server binary failed to start. Verify the command exists and is executable."
}
]
});
process.stderr.write(`[mcpm-guard] SPAWN-FAILED ${opts.command}: ${code}
`);
child.stdout?.destroy();
child.stdin?.destroy();
resolveExit(1);
});
child.stdin?.on("error", (err) => {
const code = err.code;
if (code !== "EPIPE" && code !== "ERR_STREAM_DESTROYED") {
opts.onEvent?.({
ts: (/* @__PURE__ */ new Date()).toISOString(),
direction: "parent->child",
action: "warn",
findings: []
});
}
});
const writeToChild = (bytes) => {
if (child.stdin && !child.stdin.destroyed) child.stdin.write(bytes);
};
wireDirection({
source: opts.parentIn,
target: writeToChild,
targetEnd: () => child.stdin?.end(),
parentOut: opts.parentOut,
inspect: opts.inspectParentRequest,
direction: "parent->child",
onEvent: opts.onEvent,
// Symmetry only — a parent-INITIATED block replies to the client (parentOut),
// so this is unused for this direction (no replyToOrigin on parent requests).
replyToSource: (bytes) => opts.parentOut.write(bytes)
});
if (child.stdout) {
wireDirection({
source: child.stdout,
target: (bytes) => opts.parentOut.write(bytes),
targetEnd: () => void 0,
// never end parentOut on child exit
parentOut: opts.parentOut,
inspect: opts.inspectChildResponse,
direction: "child->parent",
onEvent: opts.onEvent,
// H7: a blocked server-INITIATED request (sampling/elicitation) errors
// back to the SERVER (child.stdin), not the client.
replyToSource: writeToChild
});
}
process.on("SIGTERM", forwardSignal);
process.on("SIGINT", forwardSignal);
child.on("exit", (code) => {
if (settled) return;
settled = true;
process.off("SIGTERM", forwardSignal);
process.off("SIGINT", forwardSignal);
resolveExit(code ?? 0);
});
return { child, exit };
}
function wireDirection(w) {
const buffer = new ReadBuffer();
let bufferedBytes = 0;
w.source.on("data", (chunk) => {
bufferedBytes += chunk.byteLength;
if (bufferedBytes > MAX_BUFFER_BYTES) {
w.onEvent?.({
ts: (/* @__PURE__ */ new Date()).toISOString(),
direction: w.direction,
action: "block",
findings: []
});
w.source.destroy();
return;
}
buffer.append(chunk);
let msg;
try {
msg = buffer.readMessage();
} catch {
w.onEvent?.(malformedFrameEvent(w.direction));
w.source.destroy();
return;
}
while (msg !== null) {
bufferedBytes = 0;
const decision = w.inspect?.(msg);
if (decision?.action === "block") {
logEvent(decision, w.direction, w.onEvent);
const errResp = makeBlockResponse(msg, decision);
if (errResp !== null) {
if (decision.replyToOrigin === true) w.replyToSource(serializeMessage(errResp));
else w.parentOut.write(serializeMessage(errResp));
}
} else {
logEvent(decision, w.direction, w.onEvent);
w.target(serializeMessage(msg));
}
try {
msg = buffer.readMessage();
} catch {
w.onEvent?.(malformedFrameEvent(w.direction));
w.source.destroy();
return;
}
}
});
w.source.on("end", () => {
w.targetEnd();
});
}
function malformedFrameEvent(direction) {
return {
ts: (/* @__PURE__ */ new Date()).toISOString(),
direction,
action: "block",
findings: [
{
signature_id: "malformed-frame",
category: "RELAY",
severity: "critical",
target: "tool_response",
matched_text_excerpt: "malformed JSON-RPC frame on stdio",
remediation: "The wrapped MCP server emitted a non-JSON-RPC line (e.g. a startup banner). It must write only JSON-RPC frames to stdout."
}
]
};
}
function logEvent(result, direction, onEvent) {
if (!result || result.findings.length === 0) return;
onEvent?.({
ts: (/* @__PURE__ */ new Date()).toISOString(),
direction,
action: result.action,
findings: result.findings
});
}
// src/guard/event-log.ts
import { appendFile, mkdir } from "fs/promises";
import path from "path";
var EVENT_LOG_FILENAME = "guard-events.jsonl";
var _warnedOnFailure = false;
async function eventLogPath() {
return path.join(await getStorePath(), EVENT_LOG_FILENAME);
}
function buildEventLogEntry(event, serverName) {
return {
ts: event.ts,
server_name: sanitizeForTerminal(serverName),
direction: event.direction,
action: event.action,
findings: event.findings.map((f) => ({
signature_id: f.signature_id,
category: f.category,
severity: f.severity,
target: f.target,
matched_text_excerpt: f.matched_text_excerpt
}))
};
}
async function appendEvent(event, serverName) {
try {
const filePath = await eventLogPath();
await mkdir(path.dirname(filePath), { recursive: true, mode: 448 });
const line = `${JSON.stringify(buildEventLogEntry(event, serverName))}
`;
await appendFile(filePath, line, { encoding: "utf-8", mode: 384 });
} catch (err) {
if (!_warnedOnFailure) {
_warnedOnFailure = true;
process.stderr.write(
`[mcpm-guard] event log write failed (logging will continue silently): ${err instanceof Error ? err.message : String(err)}
`
);
}
}
}
// src/guard/confine/decide.ts
function decideConfine(input) {
const { profile, markerHash, markerRequired, backendAvailable } = input;
const mustConfine = markerRequired || profile?.require_confine === true;
if (profile !== null) {
if (markerHash === null) {
return mustConfine ? { action: "fail-closed", reason: "confine marker stripped on a required server", event: "confine-marker-stripped" } : { action: "unconfined", reason: "confine marker stripped", event: "confine-marker-stripped" };
}
if (hashConfineProfile(profile) !== markerHash) {
return { action: "fail-closed", reason: "confine profile hash mismatch (tamper)", event: "confine-hash-mismatch" };
}
if (!backendAvailable) {
return mustConfine ? { action: "fail-closed", reason: "no confine backend on a required server", event: "confine-backend-missing" } : { action: "unconfined", reason: "no confine backend on this platform", event: "confine-backend-missing" };
}
return { action: "confine", reason: "confined", event: "confine-applied" };
}
if (markerRequired) {
return { action: "fail-closed", reason: "confine required but no stored profile (store missing?)", event: "confine-profile-missing" };
}
if (markerHash !== null) {
return { action: "unconfined", reason: "confine marker present but no stored profile", event: "confine-profile-missing" };
}
return { action: "unconfined", reason: "not confined" };
}
// src/guard/run-inner.ts
var SIGNATURE_LIST_VERSION = "owasp-mcp-top-10@v0.5.0";
function applyPolicy(result, policy) {
const overrides = policy.signature_overrides ?? [];
if (overrides.length === 0) return result;
const byId = new Map(overrides.map((o) => [o.id, o]));
let highest = "pass";
const kept = [];
for (const f of result.findings) {
const o = byId.get(f.signature_id);
let perFindingAction;
if (o === void 0) {
perFindingAction = defaultActionForFinding(f);
kept.push(f);
} else if (o.action === "ignore") {
continue;
} else if (o.action === "log_only") {
perFindingAction = "pass";
kept.push(f);
} else {
perFindingAction = o.action;
kept.push(f);
}
if (ACTION_RANK[perFindingAction] > ACTION_RANK[highest]) highest = perFindingAction;
}
return withReplyToOrigin({ action: highest, findings: kept }, result.replyToOrigin === true);
}
function confineGuardEvent(event, reason, action, severity) {
return {
ts: (/* @__PURE__ */ new Date()).toISOString(),
direction: "parent->child",
action,
findings: [
{
signature_id: event,
category: "CONFINE",
severity,
target: "tool_response",
matched_text_excerpt: reason,
remediation: "See docs/GUARD.md \u2014 `mcpm guard confine`."
}
]
};
}
async function runInner(parsed) {
const safeName = sanitizeForTerminal(parsed.serverName);
if (typeof parsed.origHash === "string" && parsed.origHash.length > 0) {
const recomputed = hashOriginalEntry(parsed.command, parsed.args, parsed.declaredEnvKeys);
if (recomputed !== parsed.origHash) {
process.stderr.write(
`[mcpm-guard] ORIG-HASH-MISMATCH ${safeName}: the wrapped command/args/declared-env no longer match the integrity hash embedded at \`mcpm guard enable\` time \u2014 the client config entry may have been edited or tampered with. Starting anyway (advisory); a future mcpm release will refuse to start on mismatch. Review ~/.mcpm/guard-events.jsonl, and if you changed the entry on purpose re-run \`mcpm guard enable\` to re-pin it.
`
);
void appendEvent(
{
ts: (/* @__PURE__ */ new Date()).toISOString(),
direction: "parent->child",
action: "warn",
findings: [
{
signature_id: "orig-hash-mismatch",
category: "RELAY",
severity: "high",
target: "tool_response",
matched_text_excerpt: "wrap-marker integrity: recomputed hash != embedded --orig-hash",
remediation: "Re-run `mcpm guard enable` to re-pin, or restore the original wrapped entry in the client config."
}
]
},
parsed.serverName
);
}
}
const logEvent2 = (event) => {
if (event.action === "block" || event.action === "warn") {
process.stderr.write(
`[mcpm-guard] ${event.action.toUpperCase()} ${safeName} ${event.findings.map((f) => f.signature_id).join(",")}
`
);
void appendEvent(event, parsed.serverName);
}
};
let pinsSnapshot;
try {
pinsSnapshot = await readPins();
} catch (err) {
process.stderr.write(
`[mcpm-guard] PINS-READ-ERROR: ${safeName} could not load ~/.mcpm/pins.json: ${err.message}
Refusing to start the relay \u2014 running with rug-pull (schema-drift) protection silently disabled is more dangerous than not starting. Review ~/.mcpm/guard-events.jsonl for unauthorized activity. If you intentionally changed pins.json, run \`mcpm guard reset-integrity\`.
`
);
process.exit(1);
}
const policy = expireStale(
await readPolicy().catch((err) => {
if (err instanceof PolicyIntegrityError) {
process.stderr.write(
`[mcpm-guard] POLICY-INTEGRITY-ERROR: ${safeName} ${err.message}
Falling back to full enforcement (ignoring guard-policy.yaml) for this session.
`
);
} else {
process.stderr.write(
`[mcpm-guard] POLICY-READ-ERROR: ${err.message}
`
);
}
return {};
})
);
const pausedUntilFuture = policy.paused_until !== void 0 && new Date(policy.paused_until) > /* @__PURE__ */ new Date();
const sessionState = {
firstHashes: /* @__PURE__ */ new Map(),
revalidationArmed: false,
handshakeSeenHash: null
};
const baselineForDrift = pinsSnapshot;
const inspectChild = (msg) => {
if (pausedUntilFuture) return { action: "pass", findings: [] };
if (isToolsListChangedNotification(msg)) {
sessionState.revalidationArmed = true;
return { action: "pass", findings: [] };
}
const statelessResult = inspectFrame(msg);
let driftResult = { action: "pass", findings: [] };
if (hasToolsList(msg)) {
driftResult = inspectForDriftSync(msg, parsed.serverName, baselineForDrift, sessionState);
void (async () => {
await inspectForDrift(msg, parsed.serverName, {
read: () => readPins().catch(() => pinsSnapshot),
write: writePins,
signatureListVersion: SIGNATURE_LIST_VERSION
});
pinsSnapshot = await readPins().catch(() => pinsSnapshot);
})();
} else if (isInitializeResult(msg)) {
driftResult = inspectHandshakeDriftSync(msg, parsed.serverName, baselineForDrift, sessionState);
void (async () => {
await inspectHandshakeForDrift(msg, parsed.serverName, {
read: () => readPins().catch(() => pinsSnapshot),
write: writePins,
signatureListVersion: SIGNATURE_LIST_VERSION
});
pinsSnapshot = await readPins().catch(() => pinsSnapshot);
})();
}
return applyPolicy(mergeInspect(statelessResult, driftResult), policy);
};
const inspectParent = (msg) => {
if (pausedUntilFuture) return { action: "pass", findings: [] };
return applyPolicy(inspectMessage(msg, OWASP_MCP_TOP_10), policy);
};
const baselineEnv = buildSafeEnv(process.env);
const childEnvSource = { ...baselineEnv };
for (const key of parsed.declaredEnvKeys) {
const value = process.env[key];
if (value !== void 0) childEnvSource[key] = value;
}
let childEnv;
try {
childEnv = await resolveEnvPlaceholders(childEnvSource);
} catch (err) {
process.stderr.write(
`[mcpm-guard] SECRET-MISSING ${safeName} ${err.message}
`
);
return 1;
}
if (parsed.confineProfileHash !== void 0 && !/^[0-9a-f]{64}$/.test(parsed.confineProfileHash)) {
process.stderr.write(
`[mcpm-guard] CONFINE-BLOCK ${safeName}: malformed --confine-profile-hash in the wrap marker (the client config entry may be tampered or corrupt). Refusing to start.
`
);
void appendEvent(
confineGuardEvent(
"confine-marker-malformed",
"malformed confine profile hash",
"block",
"critical"
),
parsed.serverName
);
process.exit(1);
}
let spawnCommand = parsed.command;
let spawnArgs = parsed.args;
let confineProfile = null;
try {
confineProfile = await loadProfile(parsed.serverName);
} catch (err) {
process.stderr.write(
`[mcpm-guard] CONFINE-STORE-ERROR ${safeName}: ${err.message}
`
);
}
const confineDecision = decideConfine({
profile: confineProfile,
markerHash: parsed.confineProfileHash ?? null,
markerRequired: parsed.confineRequired === true,
backendAvailable: isConfineBackendAvailable()
});
if (confineDecision.action === "fail-closed") {
process.stderr.write(
`[mcpm-guard] CONFINE-BLOCK ${safeName}: ${confineDecision.reason}. Refusing to start (this server is marked require-confine). Run \`mcpm guard doctor-confine\` to check the backend, and review ~/.mcpm/guard-events.jsonl.
`
);
if (confineDecision.event !== void 0) {
void appendEvent(
confineGuardEvent(confineDecision.event, confineDecision.reason, "block", "critical"),
parsed.serverName
);
}
process.exit(1);
}
if (confineDecision.action === "confine" && confineProfile !== null) {
const wrapped = wrapForConfinement(confineProfile, parsed.command, parsed.args);
if (wrapped !== null) {
spawnCommand = wrapped.command;
spawnArgs = wrapped.args;
void appendEvent(
confineGuardEvent(
confineDecision.event ?? "confine-applied",
confineDecision.reason,
"pass",
"low"
),
parsed.serverName
);
} else {
const required = parsed.confineRequired === true || confineProfile.require_confine;
if (required) {
process.stderr.write(
`[mcpm-guard] CONFINE-BLOCK ${safeName}: sandbox backend became unavailable at spawn (require-confine). Refusing to start.
`
);
void appendEvent(
confineGuardEvent(
"confine-backend-missing",
"backend unavailable at wrap",
"block",
"critical"
),
parsed.serverName
);
process.exit(1);
}
process.stderr.write(
`[mcpm-guard] CONFINE-UNCONFINED ${safeName}: sandbox backend unavailable at wrap \u2014 running unconfined.
`
);
void appendEvent(
confineGuardEvent("confine-backend-missing", "backend unavailable at wrap", "warn", "high"),
parsed.serverName
);
}
} else if (confineDecision.event !== void 0) {
process.stderr.write(
`[mcpm-guard] CONFINE-UNCONFINED ${safeName}: ${confineDecision.reason} \u2014 running unconfined.
`
);
void appendEvent(
confineGuardEvent(confineDecision.event, confineDecision.reason, "warn", "high"),
parsed.serverName
);
}
const handle = startRelay({
command: spawnCommand,
args: spawnArgs,
env: childEnv,
parentIn: process.stdin,
parentOut: process.stdout,
inspectChildResponse: inspectChild,
inspectParentRequest: inspectParent,
onEvent: logEvent2
});
return handle.exit;
}
function sanitizeLabel(s) {
return sanitizeForTerminal(s, 128);
}
function inspectForDriftSync(msg, serverName, baseline, state) {
const armed = state.revalidationArmed;
state.revalidationArmed = false;
const result = msg.result;
const tools = Array.isArray(result?.tools) ? result.tools : [];
const findings = [];
for (const rawTool of tools) {
const finding = inspectToolDrift(rawTool, serverName, baseline, state, armed);
if (finding !== null) findings.push(finding);
}
const action = findings.reduce((acc, f) => {
const a = defaultActionForFinding(f);
return ACTION_RANK[a] > ACTION_RANK[acc] ? a : acc;
}, "pass");
return { action, findings };
}
function inspectToolDrift(rawTool, serverName, baseline, state, armed) {
if (rawTool === null || typeof rawTool !== "object") return null;
const tool = rawTool;
const toolName = typeof tool.name === "string" ? tool.name : null;
if (toolName === null) return null;
const fields = {
description: typeof tool.description === "string" ? tool.description : null,
schema: tool.inputSchema ?? tool.schema,
annotations: tool.annotations
};
const liveWhole = hashToolDefinition(fields);
const liveFields = fieldHashesOf(fields);
const serverPins = Object.hasOwn(baseline.servers, serverName) ? baseline.servers[serverName] : void 0;
const pinned = serverPins && Object.hasOwn(serverPins, toolName) ? serverPins[toolName] : void 0;
const sessionKey = `${serverName}::${toolName}`;
const firstSeen = state.firstHashes.get(sessionKey);
if (!armed && firstSeen !== void 0 && firstSeen !== liveWhole) {
return inSessionDriftFinding(serverName, toolName, firstSeen, liveWhole);
}
if (firstSeen === void 0 || armed) state.firstHashes.set(sessionKey, liveWhole);
if (!pinned || pinned.current_hash === null) return null;
if (liveWhole === pinned.current_hash) return null;
const cls = classifyDrift(pinned, liveFields);
const newDescriptionExcerpt = typeof tool.description === "string" ? sanitizeForTerminal(tool.description, 80) : void 0;
return buildDriftFinding({
cls,
safeServer: sanitizeLabel(serverName),
safeTool: sanitizeLabel(toolName),
expected: pinned.current_hash,
actual: liveWhole,
newDescriptionExcerpt
});
}
function inSessionDriftFinding(serverName, toolName, firstSeen, liveWhole) {
return {
signature_id: "schema-drift-in-session",
category: "OWASP-MCP-1",
severity: "critical",
target: "tool_description",
matched_text_excerpt: `${sanitizeLabel(toolName)}: ${firstSeen.slice(7, 19)}\u2026 \u2192 ${liveWhole.slice(7, 19)}\u2026 (same session)`,
remediation: `Server "${sanitizeLabel(serverName)}" delivered two different schemas for tool "${sanitizeLabel(toolName)}" in the same session. This is a rug-pull attempt; restart the IDE and reinspect the server's source.`
};
}
function isToolsListChangedNotification(msg) {
if (!("method" in msg)) return false;
if (msg.method !== "notifications/tools/list_changed") return false;
return !("result" in msg);
}
function isInitializeResult(msg) {
if (!("result" in msg)) return false;
const result = msg.result;
return result !== null && typeof result === "object" && typeof result.protocolVersion === "string";
}
function inspectHandshakeDriftSync(msg, serverName, baseline, state) {
const result = msg.result;
if (result === null || typeof result !== "object") return { action: "pass", findings: [] };
const liveFields = handshakeFieldHashesOf(result);
const liveCapKeys = handshakeCapabilityKeys(result);
const liveWhole = hashHandshake(liveFields);
const seen = state.handshakeSeenHash;
if (seen !== null && seen !== liveWhole) {
return warnResult(handshakeInSessionFinding(serverName, seen, liveWhole));
}
if (seen === null) state.handshakeSeenHash = liveWhole;
const pinned = lookupHandshake(baseline, serverName);
if (pinned === void 0) return { action: "pass", findings: [] };
if (liveWhole === pinned.current_hash || pinned.previous_hashes.includes(liveWhole)) {
return { action: "pass", findings: [] };
}
const cls = classifyHandshakeDrift(pinned, liveFields, liveCapKeys);
const findings = buildHandshakeDriftFinding({
cls,
safeServer: sanitizeLabel(serverName)
});
const action = findings.reduce((acc, f) => {
const a = defaultActionForFinding(f);
return ACTION_RANK[a] > ACTION_RANK[acc] ? a : acc;
}, "pass");
return { action, findings };
}
function warnResult(finding) {
return { action: defaultActionForFinding(finding), findings: [finding] };
}
function handshakeInSessionFinding(serverName, firstSeen, liveWhole) {
return {
signature_id: "handshake-drift-in-session",
category: "OWASP-MCP-1",
severity: "high",
target: "initialize_instructions",
matched_text_excerpt: `${sanitizeLabel(serverName)}: ${firstSeen.slice(7, 19)}\u2026 \u2192 ${liveWhole.slice(7, 19)}\u2026 (same session)`,
remediation: `Server "${sanitizeLabel(serverName)}" delivered two different initialize handshakes in the same session \u2014 initialize should occur once. Inspect the wrapped command; this is a warn-only signal and does not block the session.`
};
}
export {
applyPolicy,
inspectForDriftSync,
inspectHandshakeDriftSync,
isInitializeResult,
isToolsListChangedNotification,
runInner
};
//# sourceMappingURL=run-inner-WH5ZSWBR.js.map

Sorry, the diff of this file is too big to display

#!/usr/bin/env node
import {
buildDoctorModel,
execCheckDefault,
formatMcpEntryCommand,
makeCheckConfigExists
} from "./chunk-RUSWATRY.js";
import {
resolveInstallEntry
} from "./chunk-7QRDQF55.js";
import {
readPins
} from "./chunk-DDCTUMSZ.js";
import "./chunk-E3T224S3.js";
import {
fetchNpmProvenance
} from "./chunk-RTI2GLYX.js";
import "./chunk-WYSMWP2R.js";
import "./chunk-OIFKZA4V.js";
import "./chunk-FEXJHHDM.js";
import "./chunk-F6CHEUGO.js";
import {
nativeTrustScore
} from "./chunk-LSNEZAFR.js";
import "./chunk-UNGY7RTE.js";
import "./chunk-W4IAFBUN.js";
import "./chunk-2PWW3Q5Q.js";
import {
fetchNpmIntegrity
} from "./chunk-7RJXJERN.js";
import "./chunk-K4U7EXLG.js";
import "./chunk-GZ3WCRLG.js";
import "./chunk-6R7TL5O2.js";
import {
CLIENT_IDS
} from "./chunk-R4R2VPDA.js";
import "./chunk-4QDJ3I7X.js";
import "./chunk-3X76P3FG.js";
import {
extractRegistryMeta
} from "./chunk-U7N6FRYF.js";
import "./chunk-WT6V33F2.js";
// src/server/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
// src/server/tools.ts
import { z } from "zod";
var serverName = z.string().min(1).max(256);
var clientId = z.enum(CLIENT_IDS);
var NoArgsInput = z.strictObject({});
var SearchInput = z.strictObject({
query: z.string().min(1).max(200),
limit: z.number().int().min(1).max(100).optional().default(20)
});
var InstallInput = z.strictObject({
name: serverName,
client: clientId.optional(),
minTrustScore: z.number().min(0).max(100).optional().default(50)
});
var InfoInput = z.strictObject({
name: serverName
});
var ListInput = z.strictObject({
client: clientId.optional()
});
var RemoveInput = z.strictObject({
name: serverName,
client: clientId.optional()
});
var SetupInput = z.strictObject({
description: z.string().min(1).max(1e3),
client: clientId.optional(),
minTrustScore: z.number().min(0).max(100).optional().default(50)
});
var UpInput = z.strictObject({
stackFile: z.string().optional().default("mcpm.yaml"),
profile: z.string().optional(),
dryRun: z.boolean().optional().default(false)
});
// src/server/handlers.ts
import path from "path";
var SERVER_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,126}\/[a-zA-Z0-9][a-zA-Z0-9._-]{0,126}$/;
function validateMcpServerName(name) {
if (typeof name !== "string" || name.length === 0 || name.length > 256) {
throw new Error(`Invalid server name: must be a non-empty string under 256 characters.`);
}
if (!SERVER_NAME_RE.test(name)) {
throw new Error(
`Invalid server name format: "${name}". Expected format: "namespace/server-name" (alphanumeric, dots, hyphens, underscores only).`
);
}
}
function computeTrust(entry, deps) {
const findings = deps.scanTier1(entry);
return deps.computeTrustScore({
findings,
healthCheckPassed: null,
hasExternalScanner: false,
registryMeta: extractRegistryMeta(entry)
});
}
async function resolveClients(requestedClient, deps) {
const detected = await deps.detectClients();
if (detected.length === 0) {
throw new Error("No supported AI clients found.");
}
if (requestedClient !== void 0) {
if (!CLIENT_IDS.includes(requestedClient)) {
throw new Error(
`Unknown client "${requestedClient}". Valid values: ${CLIENT_IDS.join(", ")}.`
);
}
const id = requestedClient;
if (!detected.includes(id)) {
throw new Error(`Client "${requestedClient}" is not installed.`);
}
return [id];
}
return detected;
}
async function handleSearch(args, deps) {
const entries = await deps.registrySearch(args.query, args.limit);
const servers = entries.map((entry) => {
const trust = computeTrust(entry, deps);
return {
name: entry.server.name,
description: entry.server.description ?? "",
version: entry.server.version,
trustScore: trust.score
};
});
return { servers };
}
var DEFAULT_MIN_TRUST_SCORE = 50;
var HARD_TRUST_FLOOR = 25;
function effectiveMinTrustScore(requested) {
return Math.max(requested ?? DEFAULT_MIN_TRUST_SCORE, HARD_TRUST_FLOOR);
}
async function handleInstall(args, deps, preResolved) {
validateMcpServerName(args.name);
const entry = preResolved?.entry ?? await deps.registryGetServer(args.name);
const trust = preResolved?.trust ?? computeTrust(entry, deps);
const minScore = effectiveMinTrustScore(args.minTrustScore);
const nativeTrust = nativeTrustScore(trust);
if (nativeTrust.score < minScore) {
throw new Error(
`Server "${args.name}" has trust score ${nativeTrust.score}/${nativeTrust.maxPossible} (level: ${trust.level}), which is below the minimum threshold of ${minScore}. ` + (nativeTrust.excludedExternalCredit > 0 ? `An external scanner's ${nativeTrust.excludedExternalCredit} points are excluded from this floor because mcpm cannot verify them. ` : "") + `Install rejected for safety. Use mcpm CLI with --yes to override after manual review.`
);
}
const clients = await resolveClients(args.client, deps);
const planned = clients.map((clientId2) => {
const mcpEntry = resolveInstallEntry(entry, clientId2);
if (mcpEntry.url !== void 0 && mcpEntry.command === void 0) {
throw new Error(
`Server "${args.name}" uses a URL/HTTP transport and runs UNGUARDED (the guard relay only wraps stdio servers). Installing it is not permitted via the MCP surface. Use the mcpm CLI with --allow-unguarded after manual review.`
);
}
return {
clientId: clientId2,
adapter: deps.getAdapter(clientId2),
configPath: deps.getConfigPath(clientId2),
mcpEntry
};
});
const done = [];
try {
for (const p of planned) {
await p.adapter.addServer(p.configPath, args.name, p.mcpEntry);
done.push(p);
}
await deps.addToStore({
name: args.name,
version: entry.server.version,
clients: done.map((p) => p.clientId),
installedAt: (/* @__PURE__ */ new Date()).toISOString()
});
} catch (err) {
const stranded = await rollbackInstall(done, args.name);
if (stranded.length > 0) {
throw new Error(
`${err instanceof Error ? err.message : String(err)}
Rollback incomplete: "${args.name}" is STILL INSTALLED in ${stranded.join(", ")}. Remove it with \`mcpm remove ${args.name}\` before retrying.`
);
}
throw err;
}
return {
installed: true,
name: args.name,
version: entry.server.version,
clients: done.map((p) => p.clientId),
trustScore: trust
};
}
async function rollbackInstall(done, name) {
const stranded = [];
for (const p of done) {
try {
await p.adapter.removeServer(p.configPath, name);
} catch {
stranded.push(p.clientId);
}
}
return stranded;
}
async function handleInfo(args, deps) {
validateMcpServerName(args.name);
const entry = await deps.registryGetServer(args.name);
const trust = computeTrust(entry, deps);
return {
name: entry.server.name,
description: entry.server.description ?? "",
version: entry.server.version,
packages: entry.server.packages.map((p) => ({
registryType: p.registryType,
identifier: p.identifier
})),
trustScore: trust
};
}
async function handleList(args, deps) {
const clients = await resolveClients(args.client, deps);
const servers = [];
for (const clientId2 of clients) {
const adapter = deps.getAdapter(clientId2);
const configPath = deps.getConfigPath(clientId2);
const installed = await adapter.read(configPath);
for (const [name, entry] of Object.entries(installed)) {
const command = formatMcpEntryCommand(entry, "unknown");
servers.push({ name, client: clientId2, command });
}
}
return { servers };
}
async function handleRemove(args, deps) {
validateMcpServerName(args.name);
const clients = await resolveClients(args.client, deps);
const removedClients = [];
for (const clientId2 of clients) {
const adapter = deps.getAdapter(clientId2);
const configPath = deps.getConfigPath(clientId2);
try {
await adapter.removeServer(configPath, args.name);
removedClients.push(clientId2);
} catch {
}
}
if (removedClients.length === 0) {
throw new Error(`Server "${args.name}" not found in any client config.`);
}
try {
await deps.removeFromStore(args.name);
} catch {
}
return { removed: true, name: args.name, clients: removedClients };
}
async function handleAudit(deps) {
const clients = await deps.detectClients();
const results = [];
for (const clientId2 of clients) {
const adapter = deps.getAdapter(clientId2);
const configPath = deps.getConfigPath(clientId2);
const installed = await adapter.read(configPath);
for (const name of Object.keys(installed)) {
try {
const entry = await deps.registryGetServer(name);
const trust = computeTrust(entry, deps);
results.push({ name, client: clientId2, trustScore: trust });
} catch {
results.push({
name,
client: clientId2,
trustScore: { score: 0, maxPossible: 80, level: "risky", breakdown: { healthCheck: 0, staticScan: 0, externalScan: 0, registryMeta: 0 } }
});
}
}
}
return { results };
}
async function handleDoctor(deps) {
return buildDoctorModel({
getAdapter: deps.getAdapter,
getConfigPath: deps.getConfigPath,
checkConfigExists: makeCheckConfigExists(deps.getConfigPath),
execCheck: execCheckDefault
});
}
async function handleSetup(args, deps) {
if (!args.description.trim()) {
throw new Error("Could not extract any keywords from empty description.");
}
const keywords = extractKeywords(args.description);
const minScore = Math.max(
effectiveMinTrustScore(args.minTrustScore),
DEFAULT_MIN_TRUST_SCORE
);
const installed = [];
const skipped = [];
const searchResults = await Promise.all(
keywords.map(
(kw) => deps.registrySearch(kw, 5).then((entries) => ({ ok: true, entries })).catch((err) => ({
ok: false,
error: err instanceof Error ? err.message : String(err)
}))
)
);
const seenNames = /* @__PURE__ */ new Set();
for (let i = 0; i < keywords.length; i++) {
const keyword = keywords[i];
const outcome = searchResults[i];
if (!outcome.ok) {
skipped.push({ name: keyword, reason: `Registry search failed: ${outcome.error}` });
continue;
}
const entries = outcome.entries;
if (entries.length === 0) {
skipped.push({ name: keyword, reason: `No servers found for "${keyword}"` });
continue;
}
let bestEntry = null;
let bestTrust = null;
for (const entry of entries) {
if (seenNames.has(entry.server.name)) continue;
const trust = computeTrust(entry, deps);
if (bestTrust === null || trust.score > bestTrust.score) {
bestEntry = entry;
bestTrust = trust;
}
}
if (bestEntry === null || bestTrust === null) {
skipped.push({ name: keyword, reason: "All results already installed or duplicated" });
continue;
}
const bestNative = nativeTrustScore(bestTrust);
if (bestNative.score < minScore) {
skipped.push({
name: bestEntry.server.name,
reason: `Trust score ${bestNative.score}/${bestNative.maxPossible} is below minimum ${minScore}` + (bestNative.excludedExternalCredit > 0 ? ` (an external scanner's ${bestNative.excludedExternalCredit} points are excluded \u2014 mcpm cannot verify them)` : "")
});
continue;
}
try {
await handleInstall(
{ name: bestEntry.server.name, client: args.client },
deps,
{ entry: bestEntry, trust: bestTrust }
);
seenNames.add(bestEntry.server.name);
installed.push({ name: bestEntry.server.name, trustScore: bestTrust });
} catch (err) {
skipped.push({
name: bestEntry.server.name,
reason: `Install failed: ${err.message}`
});
}
}
const note = installed.length > 0 ? "Restart your AI client to use the newly installed servers." : void 0;
return { installed, skipped, ...note ? { note } : {} };
}
async function handleMcpUp(args, deps) {
const stackFile = args.stackFile ?? "mcpm.yaml";
const resolved = path.resolve(process.cwd(), stackFile);
if (resolved !== process.cwd() && !resolved.startsWith(process.cwd() + path.sep)) {
throw new Error("stackFile must be within the working directory");
}
{
const { realpath } = await import("fs/promises");
try {
const [realStack, realCwd] = await Promise.all([
realpath(resolved),
realpath(process.cwd())
]);
if (realStack !== realCwd && !realStack.startsWith(realCwd + path.sep)) {
throw new Error("stackFile must be within the working directory");
}
} catch (err) {
const code = err.code ?? "";
if (!["ENOENT", "ELOOP", "ENOTDIR"].includes(code)) throw err;
}
}
const { handleUp } = await import("./up-3RX6WQ4D.js");
const { writeFile } = await import("fs/promises");
const { handleLock } = await import("./lock-3JKW72C5.js");
const { RegistryClient } = await import("./client-3RPMRFZL.js");
const { scanTier1: st1 } = await import("./tier1-OPUMS3NX.js");
const { checkScannerAvailable: csa, scanTier2: st2 } = await import("./tier2-PI43NCHZ.js");
const { computeTrustScore: cts } = await import("./trust-score-BAGF67DE.js");
const client = new RegistryClient();
const outputLines = [];
const records = [];
let thrownError;
try {
await handleUp(
{
stackFile,
profile: args.profile,
dryRun: args.dryRun,
ci: true,
yes: false,
// MCP surface lockdown (fixes C, D & H1): never auto-read ambient
// secrets from process.env OR the working-directory .env file, and never
// install URL servers (they bypass the registry trust gate). All three
// default to true on the CLI; the MCP (untrusted-caller) surface opts in
// to the locked-down behavior.
allowProcessEnv: false,
allowUrlServers: false,
allowEnvFile: false,
// M2: the batch `up` path must honor the same non-overridable trust floor
// the single-install MCP tool enforces (issue #24), so a low-trust server
// an agent could not install via mcpm_install can't slip in via mcpm_up.
minTrustFloor: HARD_TRUST_FLOOR
},
{
detectClients: deps.detectClients,
getAdapter: deps.getAdapter,
getPath: deps.getConfigPath,
getServer: (name, version) => client.getServer(name, version),
scanTier1: st1,
checkScannerAvailable: csa,
scanTier2: (name) => st2(name),
computeTrustScore: cts,
runLock: async (stackFile2) => {
await handleLock(
{ stackFile: stackFile2 },
{
getServerVersions: (name) => client.getServerVersions(name),
getServer: (name, v) => client.getServer(name, v),
scanTier1: st1,
checkScannerAvailable: csa,
scanTier2: (name) => st2(name),
computeTrustScore: cts,
writeLockFile: (path2, content) => writeFile(path2, content, { encoding: "utf-8", mode: 384 }),
fetchNpmIntegrity,
fetchNpmProvenance: (id, ver, sri) => fetchNpmProvenance(id, ver, { integritySri: sri }),
output: (text) => outputLines.push(text)
}
);
},
// Issue #22: never auto-confirm on the MCP (no-human-in-loop) surface.
// The previous `async () => true` blanket-approved every confirmation,
// including strict-mode *removals* of servers not in mcpm.yaml — a
// prompt-injected agent could silently mutate client configs. Refusing
// confirmation here means destructive prompts are declined; the trust
// policy still gates installs via checkTrustPolicy in handleUp.
confirm: async () => false,
promptEnvVar: async () => "",
output: (text) => outputLines.push(text),
fetchNpmIntegrity,
// F8/B3: wire the provenance re-check on the MCP surface too, or a
// policy.frozen: true stack run through mcpm_up would silently skip it.
fetchNpmProvenance: (id, v, o) => fetchNpmProvenance(id, v, o),
readPins,
recordResult: (r) => records.push(r)
}
);
} catch (err) {
thrownError = err instanceof Error ? err.message : String(err);
}
const installed = [];
const blocked = [];
const failed = [];
const skipped = [];
if (records.length > 0) {
for (const r of records) {
switch (r.status) {
case "installed":
installed.push(r.name);
break;
case "blocked":
blocked.push(r.name);
break;
case "failed":
failed.push(r.name);
break;
case "skipped":
case "removed":
skipped.push(r.name);
break;
}
}
} else {
for (const line of outputLines) {
if (line.includes("\u2713")) installed.push(line.trim());
else if (line.includes("\u2717") && line.includes("blocked")) blocked.push(line.trim());
else if (line.includes("\u2717")) failed.push(line.trim());
else if (line.includes("\u2022")) skipped.push(line.trim());
}
}
return {
installed,
blocked,
failed,
skipped,
...thrownError !== void 0 ? { error: thrownError } : {},
...installed.length > 0 ? { note: "Restart your AI client to use the newly installed servers." } : {}
};
}
var STOPWORDS = /\b(i need|set up|access|work with|connect to|a server that|a server for|to|the|a|an|my|for|and|with)\b/gi;
function extractKeywords(description) {
const cleaned = description.toLowerCase().replace(STOPWORDS, " ").replace(/[,&]/g, " ");
const tokens = cleaned.split(/\s+/).map((s) => s.trim()).filter((s) => s.length > 2);
if (tokens.length > 5) {
return [cleaned.replace(/\s+/g, " ").trim()];
}
return tokens.length > 0 ? tokens : [description.trim()];
}
// src/server/index.ts
async function createDeps() {
const { RegistryClient } = await import("./client-3RPMRFZL.js");
const { detectInstalledClients } = await import("./detector-ZI4OWRCJ.js");
const { getConfigPath } = await import("./paths-US27HRTP.js");
const { getAdapter } = await import("./config-XMU247VO.js");
const { scanTier1 } = await import("./tier1-OPUMS3NX.js");
const { computeTrustScore } = await import("./trust-score-BAGF67DE.js");
const { addInstalledServer, removeInstalledServer } = await import("./servers-IS6ZWSYC.js");
const client = new RegistryClient();
return {
registrySearch: async (query, limit) => {
const result = await client.searchServers(query, { limit });
return result.servers;
},
registryGetServer: (name) => client.getServer(name),
detectClients: detectInstalledClients,
getAdapter,
getConfigPath,
scanTier1,
computeTrustScore,
addToStore: addInstalledServer,
removeFromStore: removeInstalledServer
};
}
function registerTools(server, deps) {
server.registerTool("mcpm_search", {
description: "Search the MCP registry for servers with trust scores",
inputSchema: SearchInput,
annotations: { readOnlyHint: true }
}, async (args) => {
const result = await handleSearch(args, deps);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
});
server.registerTool("mcpm_install", {
description: "Install an MCP server with trust assessment",
inputSchema: InstallInput,
annotations: { destructiveHint: true }
}, async (args) => {
const result = await handleInstall(args, deps);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
});
server.registerTool("mcpm_info", {
description: "Show full details and trust score for an MCP server",
inputSchema: InfoInput,
annotations: { readOnlyHint: true }
}, async (args) => {
const result = await handleInfo(args, deps);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
});
server.registerTool("mcpm_list", {
description: "List installed MCP servers across AI clients",
inputSchema: ListInput,
annotations: { readOnlyHint: true }
}, async (args) => {
const result = await handleList(args, deps);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
});
server.registerTool("mcpm_remove", {
description: "Remove an MCP server from client configs",
inputSchema: RemoveInput,
annotations: { destructiveHint: true }
}, async (args) => {
const result = await handleRemove(args, deps);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
});
server.registerTool("mcpm_audit", {
inputSchema: NoArgsInput,
description: "Scan all installed servers and produce trust report",
annotations: { readOnlyHint: true }
}, async () => {
const result = await handleAudit(deps);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
});
server.registerTool("mcpm_doctor", {
inputSchema: NoArgsInput,
description: "Check MCP setup health",
annotations: { readOnlyHint: true }
}, async () => {
const result = await handleDoctor(deps);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
});
server.registerTool("mcpm_setup", {
description: "Install MCP servers from a natural language description",
inputSchema: SetupInput,
annotations: { destructiveHint: true }
}, async (args) => {
const result = await handleSetup(args, deps);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
});
server.registerTool("mcpm_up", {
description: "Install all servers from an mcpm.yaml stack file with trust verification. Equivalent to docker-compose up for MCP servers. Runs trust re-assessment and blocks servers that violate the trust policy. Pass profile to install only servers matching that profile, or dryRun to preview what would be installed without making changes.",
inputSchema: UpInput,
annotations: { destructiveHint: true }
}, async (args) => {
const result = await handleMcpUp(args, deps);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
});
}
async function startServer() {
const deps = await createDeps();
const server = new McpServer({
name: "mcpm",
// Issue #22: advertise the real package version (injected by tsup at build),
// not a hardcoded stale "0.1.0".
version: "0.29.0"
});
registerTools(server, deps);
const transport = new StdioServerTransport();
await server.connect(transport);
}
export {
registerTools,
startServer
};
//# sourceMappingURL=server-NHD3D4OL.js.map
{"version":3,"sources":["../src/server/index.ts","../src/server/tools.ts","../src/server/handlers.ts"],"sourcesContent":["/**\n * MCP server for mcpm — exposes search, install, audit, and setup as tools.\n *\n * Uses @modelcontextprotocol/sdk with stdio transport.\n * All logic delegates to handlers.ts which wraps existing mcpm functions.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport {\n NoArgsInput,\n SearchInput,\n InstallInput,\n InfoInput,\n ListInput,\n RemoveInput,\n SetupInput,\n UpInput,\n} from \"./tools.js\";\nimport {\n handleSearch,\n handleInstall,\n handleInfo,\n handleList,\n handleRemove,\n handleAudit,\n handleDoctor,\n handleSetup,\n handleMcpUp,\n} from \"./handlers.js\";\nimport type { ServerDeps } from \"./handlers.js\";\n\n// ---------------------------------------------------------------------------\n// Wire up real dependencies\n// ---------------------------------------------------------------------------\n\nasync function createDeps(): Promise<ServerDeps> {\n const { RegistryClient } = await import(\"../registry/client.js\");\n const { detectInstalledClients } = await import(\"../config/detector.js\");\n const { getConfigPath } = await import(\"../config/paths.js\");\n const { getAdapter } = await import(\"../config/index.js\");\n const { scanTier1 } = await import(\"../scanner/tier1.js\");\n const { computeTrustScore } = await import(\"../scanner/trust-score.js\");\n const { addInstalledServer, removeInstalledServer } = await import(\"../store/servers.js\");\n\n const client = new RegistryClient();\n\n return {\n registrySearch: async (query, limit) => {\n const result = await client.searchServers(query, { limit });\n return result.servers;\n },\n registryGetServer: (name) => client.getServer(name),\n detectClients: detectInstalledClients,\n getAdapter,\n getConfigPath,\n scanTier1,\n computeTrustScore,\n addToStore: addInstalledServer,\n removeFromStore: removeInstalledServer,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Server setup\n// ---------------------------------------------------------------------------\n\n/**\n * Register every mcpm tool on the server. Extracted from startServer so the\n * registration can be unit-tested (fix F.1): a test spies registerTool and\n * asserts every TOOL_DEFINITIONS name is registered exactly once, guarding\n * against future tool/registration divergence.\n *\n * `server` is typed loosely as `Pick<McpServer, \"registerTool\">` so tests can\n * pass a lightweight spy without constructing a full McpServer.\n */\nexport function registerTools(\n server: Pick<McpServer, \"registerTool\">,\n deps: ServerDeps\n): void {\n // Register tools using registerTool API\n server.registerTool(\"mcpm_search\", {\n description: \"Search the MCP registry for servers with trust scores\",\n inputSchema: SearchInput,\n annotations: { readOnlyHint: true },\n }, async (args) => {\n const result = await handleSearch(args, deps);\n return { content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }] };\n });\n\n server.registerTool(\"mcpm_install\", {\n description: \"Install an MCP server with trust assessment\",\n inputSchema: InstallInput,\n annotations: { destructiveHint: true },\n }, async (args) => {\n const result = await handleInstall(args, deps);\n return { content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }] };\n });\n\n server.registerTool(\"mcpm_info\", {\n description: \"Show full details and trust score for an MCP server\",\n inputSchema: InfoInput,\n annotations: { readOnlyHint: true },\n }, async (args) => {\n const result = await handleInfo(args, deps);\n return { content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }] };\n });\n\n server.registerTool(\"mcpm_list\", {\n description: \"List installed MCP servers across AI clients\",\n inputSchema: ListInput,\n annotations: { readOnlyHint: true },\n }, async (args) => {\n const result = await handleList(args, deps);\n return { content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }] };\n });\n\n server.registerTool(\"mcpm_remove\", {\n description: \"Remove an MCP server from client configs\",\n inputSchema: RemoveInput,\n annotations: { destructiveHint: true },\n }, async (args) => {\n const result = await handleRemove(args, deps);\n return { content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }] };\n });\n\n server.registerTool(\"mcpm_audit\", {\n inputSchema: NoArgsInput,\n description: \"Scan all installed servers and produce trust report\",\n annotations: { readOnlyHint: true },\n }, async () => {\n const result = await handleAudit(deps);\n return { content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }] };\n });\n\n server.registerTool(\"mcpm_doctor\", {\n inputSchema: NoArgsInput,\n description: \"Check MCP setup health\",\n annotations: { readOnlyHint: true },\n }, async () => {\n const result = await handleDoctor(deps);\n return { content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }] };\n });\n\n server.registerTool(\"mcpm_setup\", {\n description: \"Install MCP servers from a natural language description\",\n inputSchema: SetupInput,\n annotations: { destructiveHint: true },\n }, async (args) => {\n const result = await handleSetup(args, deps);\n return { content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }] };\n });\n\n server.registerTool(\"mcpm_up\", {\n description: \"Install all servers from an mcpm.yaml stack file with trust verification. Equivalent to docker-compose up for MCP servers. Runs trust re-assessment and blocks servers that violate the trust policy. Pass profile to install only servers matching that profile, or dryRun to preview what would be installed without making changes.\",\n inputSchema: UpInput,\n annotations: { destructiveHint: true },\n }, async (args) => {\n const result = await handleMcpUp(args, deps);\n return { content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }] };\n });\n}\n\nexport async function startServer(): Promise<void> {\n const deps = await createDeps();\n\n const server = new McpServer({\n name: \"mcpm\",\n // Issue #22: advertise the real package version (injected by tsup at build),\n // not a hardcoded stale \"0.1.0\".\n version: __PKG_VERSION__,\n });\n\n registerTools(server, deps);\n\n // Start stdio transport\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n","/**\n * MCP tool definitions for mcpm serve.\n *\n * Each tool has a name, description, and Zod input schema.\n * Handlers are in handlers.ts.\n */\n\nimport { z } from \"zod\";\nimport { CLIENT_IDS } from \"../config/paths.js\";\n\nexport const TOOL_DEFINITIONS = [\n {\n name: \"mcpm_search\",\n description: \"Search the MCP registry for servers. Returns results with trust scores.\",\n inputSchema: {\n type: \"object\" as const,\n properties: {\n query: { type: \"string\", description: \"Search query (substring match on server name)\" },\n limit: { type: \"number\", description: \"Max results to return (default 20)\" },\n },\n required: [\"query\"],\n },\n },\n {\n name: \"mcpm_install\",\n description: \"Install an MCP server from the registry into detected AI client configs. Runs trust assessment automatically. Rejects servers below the minimum trust score (default 50).\",\n inputSchema: {\n type: \"object\" as const,\n properties: {\n name: { type: \"string\", description: \"Server name (e.g. io.github.domdomegg/filesystem-mcp)\" },\n client: { type: \"string\", description: \"Install to specific client only (claude-desktop, cursor, vscode, windsurf)\" },\n minTrustScore: { type: \"number\", description: \"Minimum trust score to allow install (default 50, range 0-100)\" },\n },\n required: [\"name\"],\n },\n },\n {\n name: \"mcpm_info\",\n description: \"Show full details for an MCP server including trust score breakdown.\",\n inputSchema: {\n type: \"object\" as const,\n properties: {\n name: { type: \"string\", description: \"Server name\" },\n },\n required: [\"name\"],\n },\n },\n {\n name: \"mcpm_list\",\n description: \"List all installed MCP servers across detected AI clients.\",\n inputSchema: {\n type: \"object\" as const,\n properties: {\n client: { type: \"string\", description: \"Filter to specific client\" },\n },\n required: [],\n },\n },\n {\n name: \"mcpm_remove\",\n description: \"Remove an MCP server from AI client configs.\",\n inputSchema: {\n type: \"object\" as const,\n properties: {\n name: { type: \"string\", description: \"Server name to remove\" },\n client: { type: \"string\", description: \"Remove from specific client only\" },\n },\n required: [\"name\"],\n },\n },\n {\n name: \"mcpm_audit\",\n description: \"Scan all installed MCP servers and produce a trust report with scores.\",\n inputSchema: {\n type: \"object\" as const,\n properties: {},\n required: [],\n },\n },\n {\n name: \"mcpm_doctor\",\n description: \"Check MCP setup health: detected clients, available runtimes, configuration issues.\",\n inputSchema: {\n type: \"object\" as const,\n properties: {},\n required: [],\n },\n },\n {\n name: \"mcpm_setup\",\n description: \"Install MCP servers from a natural language description. Searches, evaluates trust, installs the best match for each keyword. Example: 'filesystem and GitHub' installs filesystem + GitHub servers.\",\n inputSchema: {\n type: \"object\" as const,\n properties: {\n description: { type: \"string\", description: \"What you need (e.g. 'filesystem access and GitHub integration')\" },\n client: { type: \"string\", description: \"Install to specific client only\" },\n minTrustScore: { type: \"number\", description: \"Minimum trust score to auto-install (default 50, range 0-100)\" },\n },\n required: [\"description\"],\n },\n },\n {\n name: \"mcpm_up\",\n description: \"Install all servers from an mcpm.yaml stack file with trust verification. Equivalent to docker-compose up for MCP servers. Runs trust re-assessment and blocks servers that violate the trust policy. Pass profile to install only servers matching that profile, or dryRun to preview what would be installed without making changes.\",\n inputSchema: {\n type: \"object\" as const,\n properties: {\n stackFile: { type: \"string\", description: \"Path to mcpm.yaml (default: mcpm.yaml in CWD)\" },\n profile: { type: \"string\", description: \"Install only servers matching this profile\" },\n dryRun: { type: \"boolean\", description: \"Show what would be installed without making changes\" },\n },\n required: [],\n },\n },\n] as const;\n\n// Shared field schemas (security #31): a bounded server-name string and a closed\n// client enum, so the Zod layer — not just the runtime `validateMcpServerName` /\n// `CLIENT_IDS.includes` checks in handlers.ts — is the declarative enforcement\n// point. The objects below are `strictObject` so unknown keys are rejected\n// instead of silently dropped.\n//\n// These are passed to `registerTool` WHOLE (not via `.shape`) — see\n// server/index.ts. That distinction is load-bearing: the SDK accepts either a\n// raw shape or a full schema, but a raw shape is rebuilt as a plain\n// `z.object(shape)`, which silently DROPS the object-level `strict` setting.\n// Per-field constraints (the length bound, the client enum) survive either way;\n// strictness does not.\n//\n// Passing the whole schema means the SDK rejects unknown keys with a JSON-RPC\n// -32602 `unrecognized_keys` error, AND advertises `additionalProperties: false`\n// in `tools/list` so a caller can see the contract before calling. Verified over\n// a real in-memory MCP transport in server-strict-schema.test.ts.\n//\n// The runtime guards in handlers.ts (`validateMcpServerName`, `CLIENT_IDS`)\n// remain as defence in depth.\nconst serverName = z.string().min(1).max(256);\nconst clientId = z.enum(CLIENT_IDS);\n\n/**\n * Zero-argument tools (`mcpm_audit`, `mcpm_doctor`) still declare a CLOSED\n * schema rather than omitting `inputSchema` entirely. Omitting it advertises no\n * `additionalProperties: false`, so any argument a caller passes is silently\n * ignored — for a tool that takes nothing, that means EVERY argument is\n * silently ignored. An empty strict object makes the contract explicit and\n * turns a mistaken call into a clear error.\n */\nexport const NoArgsInput = z.strictObject({});\n\nexport const SearchInput = z.strictObject({\n query: z.string().min(1).max(200),\n limit: z.number().int().min(1).max(100).optional().default(20),\n});\n\nexport const InstallInput = z.strictObject({\n name: serverName,\n client: clientId.optional(),\n minTrustScore: z.number().min(0).max(100).optional().default(50),\n});\n\nexport const InfoInput = z.strictObject({\n name: serverName,\n});\n\nexport const ListInput = z.strictObject({\n client: clientId.optional(),\n});\n\nexport const RemoveInput = z.strictObject({\n name: serverName,\n client: clientId.optional(),\n});\n\nexport const SetupInput = z.strictObject({\n description: z.string().min(1).max(1000),\n client: clientId.optional(),\n minTrustScore: z.number().min(0).max(100).optional().default(50),\n});\n\nexport const UpInput = z.strictObject({\n stackFile: z.string().optional().default(\"mcpm.yaml\"),\n profile: z.string().optional(),\n dryRun: z.boolean().optional().default(false),\n});\n","/**\n * MCP tool handlers for mcpm serve.\n *\n * Each handler wraps existing mcpm logic and returns structured JSON.\n * All dependencies are injectable for testability.\n */\n\nimport path from \"node:path\";\nimport type { ClientId } from \"../config/paths.js\";\nimport { CLIENT_IDS } from \"../config/paths.js\";\nimport type { ConfigAdapter, McpServerEntry } from \"../config/adapters/index.js\";\nimport type { ServerEntry } from \"../registry/types.js\";\nimport type { Finding } from \"../scanner/tier1.js\";\nimport type { TrustScore, TrustScoreInput } from \"../scanner/trust-score.js\";\nimport { nativeTrustScore } from \"../scanner/trust-score.js\";\nimport { extractRegistryMeta } from \"../utils/format-trust.js\";\nimport { formatMcpEntryCommand } from \"../utils/format-entry.js\";\nimport { resolveInstallEntry } from \"../commands/install.js\";\nimport { buildDoctorModel, makeCheckConfigExists, execCheckDefault } from \"../commands/doctor.js\";\nimport { fetchNpmIntegrity as _fetchNpmIntegrity } from \"../registry/npm-integrity.js\";\nimport { fetchNpmProvenance as _fetchNpmProvenance } from \"../registry/npm-provenance.js\";\nimport { readPins as _readPins } from \"../guard/pins.js\";\n\n// ---------------------------------------------------------------------------\n// Input validation for MCP server tool arguments\n// ---------------------------------------------------------------------------\n\n/**\n * Server name pattern for MCP registry names.\n * Format: \"namespace/server-name\" — alphanumeric with dots, hyphens, underscores.\n * Max length 256 to prevent abuse. Must not contain shell metacharacters,\n * path traversal sequences, or control characters.\n */\nconst SERVER_NAME_RE =\n /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,126}\\/[a-zA-Z0-9][a-zA-Z0-9._-]{0,126}$/;\n\n/**\n * Validate a server name received from an MCP tool call.\n * This is the trust boundary — AI agents provide these strings, and they\n * could be influenced by prompt injection or adversarial inputs.\n */\nfunction validateMcpServerName(name: string): void {\n if (typeof name !== \"string\" || name.length === 0 || name.length > 256) {\n throw new Error(`Invalid server name: must be a non-empty string under 256 characters.`);\n }\n if (!SERVER_NAME_RE.test(name)) {\n throw new Error(\n `Invalid server name format: \"${name}\". Expected format: \"namespace/server-name\" ` +\n `(alphanumeric, dots, hyphens, underscores only).`\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Dependency injection types\n// ---------------------------------------------------------------------------\n\nexport interface ServerDeps {\n registrySearch: (query: string, limit: number) => Promise<ServerEntry[]>;\n registryGetServer: (name: string) => Promise<ServerEntry>;\n detectClients: () => Promise<ClientId[]>;\n getAdapter: (clientId: ClientId) => ConfigAdapter;\n getConfigPath: (clientId: ClientId) => string;\n scanTier1: (server: ServerEntry) => Finding[];\n computeTrustScore: (input: TrustScoreInput) => TrustScore;\n addToStore: (server: { name: string; version: string; clients: ClientId[]; installedAt: string }) => Promise<void>;\n removeFromStore: (name: string) => Promise<void>;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * F4 scope note: this helper deliberately does NOT include the\n * release-cooldown finding (ServerDeps has no injectable clock; the F4 spec\n * file list excludes server/). Consequence: mcpm_install / mcpm_search score\n * a fresh (<24h) package up to 5 points higher than CLI install/why AND than\n * the sibling mcpm_up tool (which inherits the finding via up.ts\n * processServer), and HARD_TRUST_FLOOR evaluates that inflated score — do NOT\n * compensate by raising the floor. Fast-follow is mechanical:\n * ServerDeps += now?: () => number, then append\n * assessReleaseAge({...}).finding here; no schema changes.\n */\nfunction computeTrust(entry: ServerEntry, deps: ServerDeps): TrustScore {\n const findings = deps.scanTier1(entry);\n return deps.computeTrustScore({\n findings,\n healthCheckPassed: null,\n hasExternalScanner: false,\n registryMeta: extractRegistryMeta(entry),\n });\n}\n\nasync function resolveClients(\n requestedClient: string | undefined,\n deps: ServerDeps\n): Promise<ClientId[]> {\n const detected = await deps.detectClients();\n if (detected.length === 0) {\n throw new Error(\"No supported AI clients found.\");\n }\n if (requestedClient !== undefined) {\n if (!CLIENT_IDS.includes(requestedClient as ClientId)) {\n throw new Error(\n `Unknown client \"${requestedClient}\". Valid values: ${CLIENT_IDS.join(\", \")}.`\n );\n }\n const id = requestedClient as ClientId;\n if (!detected.includes(id)) {\n throw new Error(`Client \"${requestedClient}\" is not installed.`);\n }\n return [id];\n }\n return detected;\n}\n\n// ---------------------------------------------------------------------------\n// Handlers\n// ---------------------------------------------------------------------------\n\nexport async function handleSearch(\n args: { query: string; limit: number },\n deps: ServerDeps\n): Promise<object> {\n const entries = await deps.registrySearch(args.query, args.limit);\n const servers = entries.map((entry) => {\n const trust = computeTrust(entry, deps);\n return {\n name: entry.server.name,\n description: entry.server.description ?? \"\",\n version: entry.server.version,\n trustScore: trust.score,\n };\n });\n return { servers };\n}\n\n/** Default minimum trust score for MCP server tool installs (no human in the loop). */\nconst DEFAULT_MIN_TRUST_SCORE = 50;\n\n/**\n * Hard, non-overridable trust floor for the MCP server surface (issue #24).\n *\n * The MCP `minTrustScore` input accepts `0`, which a prompt-injected agent could\n * pass to disable the install gate entirely. We clamp the effective threshold to\n * `Math.max(userValue, HARD_TRUST_FLOOR)` so no caller-supplied value can lower\n * the gate below this floor. This protects the no-human-in-loop path; the CLI\n * (with a human confirmation prompt) is the only place to install below it.\n *\n * Lowering the gate is only half of it. A score can also be pushed UP to meet\n * the gate, and `MCPM_EXTERNAL_SCANNER` is caller-supplied too — so the floor is\n * evaluated against `nativeTrustScore`, which excludes the external bucket's\n * unverifiable credit (TODOS #33).\n */\nconst HARD_TRUST_FLOOR = 25;\n\n/** Clamp a requested minimum trust score so it can never sink below the floor. */\nfunction effectiveMinTrustScore(requested: number | undefined): number {\n return Math.max(requested ?? DEFAULT_MIN_TRUST_SCORE, HARD_TRUST_FLOOR);\n}\n\nexport async function handleInstall(\n args: { name: string; client?: string; minTrustScore?: number },\n deps: ServerDeps,\n preResolved?: { entry: ServerEntry; trust: TrustScore }\n): Promise<object> {\n validateMcpServerName(args.name);\n const entry = preResolved?.entry ?? await deps.registryGetServer(args.name);\n const trust = preResolved?.trust ?? computeTrust(entry, deps);\n\n // Security gate: reject servers below the minimum trust score.\n // Unlike the CLI path which has a human confirmation prompt, the MCP server\n // path is driven by AI agents with no human in the loop. A malicious prompt\n // could trick an agent into installing a dangerous server, so we enforce a\n // hard trust floor here. Issue #24: minTrustScore:0 must NOT disable the gate —\n // the effective threshold is clamped to HARD_TRUST_FLOOR.\n //\n // TODOS #33: compared against mcpm's OWN evidence. `computeTrust` above already\n // passes `hasExternalScanner: false`, so today this subtracts nothing — it is\n // here so that wiring a scanner into this path later cannot silently reopen the\n // floor, which is the failure mode #33 found on the sibling `mcpm_up` path.\n const minScore = effectiveMinTrustScore(args.minTrustScore);\n const nativeTrust = nativeTrustScore(trust);\n if (nativeTrust.score < minScore) {\n throw new Error(\n `Server \"${args.name}\" has trust score ${nativeTrust.score}/${nativeTrust.maxPossible} ` +\n `(level: ${trust.level}), which is below the minimum threshold of ${minScore}. ` +\n (nativeTrust.excludedExternalCredit > 0\n ? `An external scanner's ${nativeTrust.excludedExternalCredit} points are excluded from this floor because mcpm cannot verify them. `\n : \"\") +\n `Install rejected for safety. Use mcpm CLI with --yes to override after manual review.`\n );\n }\n\n const clients = await resolveClients(args.client, deps);\n\n // PRE-FLIGHT: resolve and validate EVERY client's entry before touching a single\n // config. resolveInstallEntry's URL rule is cursor-ONLY, so a server carrying both\n // an npm package and an http remote used to install cleanly on claude-desktop and\n // only THEN hit the H9 deny on cursor — the agent was told the install failed while\n // a live execution surface sat in Claude Desktop, with no store record of it.\n const planned = clients.map((clientId) => {\n const mcpEntry = resolveInstallEntry(entry, clientId);\n // H9 (fail-closed): a URL/HTTP-transport entry (url, no command) runs\n // UNGUARDED — the guard relay only wraps a stdio process. The MCP surface is\n // driven by an untrusted agent with no human in the loop and no\n // `--allow-unguarded` opt-in, so url-transport installs are HARD-DENIED here\n // (mirrors the batch `up` MCP wiring's allowUrlServers:false kill-switch).\n if (mcpEntry.url !== undefined && mcpEntry.command === undefined) {\n throw new Error(\n `Server \"${args.name}\" uses a URL/HTTP transport and runs UNGUARDED ` +\n `(the guard relay only wraps stdio servers). Installing it is not permitted ` +\n `via the MCP surface. Use the mcpm CLI with --allow-unguarded after manual review.`\n );\n }\n return {\n clientId,\n adapter: deps.getAdapter(clientId),\n configPath: deps.getConfigPath(clientId),\n mcpEntry,\n };\n });\n\n // APPLY as a unit. Any failure — a client write or the store write — unwinds every\n // write already made, so this tool never reports failure over a live install.\n // The store write is inside the transaction on purpose: configs written without a\n // store record are invisible to `mcpm list` / `audit` and survive `mcpm remove`.\n const done: PlannedInstall[] = [];\n try {\n for (const p of planned) {\n await p.adapter.addServer(p.configPath, args.name, p.mcpEntry);\n done.push(p);\n }\n await deps.addToStore({\n name: args.name,\n version: entry.server.version,\n clients: done.map((p) => p.clientId),\n installedAt: new Date().toISOString(),\n });\n } catch (err) {\n const stranded = await rollbackInstall(done, args.name);\n if (stranded.length > 0) {\n // Rollback is best-effort and can fail too. Swallowing that would report a\n // clean failure over a server that is still installed — name it instead.\n throw new Error(\n `${err instanceof Error ? err.message : String(err)}\\n\\n` +\n `Rollback incomplete: \"${args.name}\" is STILL INSTALLED in ${stranded.join(\", \")}. ` +\n `Remove it with \\`mcpm remove ${args.name}\\` before retrying.`\n );\n }\n throw err;\n }\n\n return {\n installed: true,\n name: args.name,\n version: entry.server.version,\n clients: done.map((p) => p.clientId),\n trustScore: trust,\n };\n}\n\n/** One client's fully-resolved install, validated and ready to write. */\ntype PlannedInstall = {\n clientId: ClientId;\n adapter: ConfigAdapter;\n configPath: string;\n mcpEntry: McpServerEntry;\n};\n\n/**\n * Undo the client-config writes already made by a failed install.\n * @returns the clients that could NOT be rolled back (still installed).\n */\nasync function rollbackInstall(done: PlannedInstall[], name: string): Promise<ClientId[]> {\n const stranded: ClientId[] = [];\n for (const p of done) {\n try {\n await p.adapter.removeServer(p.configPath, name);\n } catch {\n stranded.push(p.clientId);\n }\n }\n return stranded;\n}\n\nexport async function handleInfo(\n args: { name: string },\n deps: ServerDeps\n): Promise<object> {\n validateMcpServerName(args.name);\n const entry = await deps.registryGetServer(args.name);\n const trust = computeTrust(entry, deps);\n return {\n name: entry.server.name,\n description: entry.server.description ?? \"\",\n version: entry.server.version,\n packages: entry.server.packages.map((p) => ({\n registryType: p.registryType,\n identifier: p.identifier,\n })),\n trustScore: trust,\n };\n}\n\nexport async function handleList(\n args: { client?: string },\n deps: ServerDeps\n): Promise<object> {\n const clients = await resolveClients(args.client, deps);\n const servers: Array<{ name: string; client: string; command: string }> = [];\n\n for (const clientId of clients) {\n const adapter = deps.getAdapter(clientId);\n const configPath = deps.getConfigPath(clientId);\n const installed = await adapter.read(configPath);\n\n for (const [name, entry] of Object.entries(installed)) {\n const command = formatMcpEntryCommand(entry, \"unknown\");\n servers.push({ name, client: clientId, command });\n }\n }\n\n return { servers };\n}\n\nexport async function handleRemove(\n args: { name: string; client?: string },\n deps: ServerDeps\n): Promise<object> {\n validateMcpServerName(args.name);\n const clients = await resolveClients(args.client, deps);\n const removedClients: ClientId[] = [];\n\n for (const clientId of clients) {\n const adapter = deps.getAdapter(clientId);\n const configPath = deps.getConfigPath(clientId);\n try {\n await adapter.removeServer(configPath, args.name);\n removedClients.push(clientId);\n } catch {\n // Server not in this client, skip\n }\n }\n\n if (removedClients.length === 0) {\n throw new Error(`Server \"${args.name}\" not found in any client config.`);\n }\n\n try {\n await deps.removeFromStore(args.name);\n } catch {\n // Not in store, fine\n }\n\n return { removed: true, name: args.name, clients: removedClients };\n}\n\nexport async function handleAudit(deps: ServerDeps): Promise<object> {\n const clients = await deps.detectClients();\n const results: Array<{ name: string; client: string; trustScore: TrustScore }> = [];\n\n for (const clientId of clients) {\n const adapter = deps.getAdapter(clientId);\n const configPath = deps.getConfigPath(clientId);\n const installed = await adapter.read(configPath);\n\n for (const name of Object.keys(installed)) {\n try {\n const entry = await deps.registryGetServer(name);\n const trust = computeTrust(entry, deps);\n results.push({ name, client: clientId, trustScore: trust });\n } catch {\n results.push({\n name,\n client: clientId,\n trustScore: { score: 0, maxPossible: 80, level: \"risky\", breakdown: { healthCheck: 0, staticScan: 0, externalScan: 0, registryMeta: 0 } },\n });\n }\n }\n }\n\n return { results };\n}\n\nexport async function handleDoctor(deps: ServerDeps): Promise<object> {\n // Reuse the CLI's structured model so this tool reports real issues instead of\n // the formerly-hardcoded `issues: []` (D7). Honors the injected getConfigPath.\n return buildDoctorModel({\n getAdapter: deps.getAdapter,\n getConfigPath: deps.getConfigPath,\n checkConfigExists: makeCheckConfigExists(deps.getConfigPath),\n execCheck: execCheckDefault,\n });\n}\n\nexport async function handleSetup(\n args: { description: string; client?: string; minTrustScore: number },\n deps: ServerDeps\n): Promise<object> {\n if (!args.description.trim()) {\n throw new Error(\"Could not extract any keywords from empty description.\");\n }\n const keywords = extractKeywords(args.description);\n\n // Issue #24: clamp to the hard floor so minTrustScore:0 can't disable the gate\n // on the no-human-in-loop setup path either.\n //\n // Also clamp UP to handleInstall's own default. This pre-filter delegates to\n // handleInstall without forwarding minTrustScore, so the enforcing gate always\n // applies DEFAULT_MIN_TRUST_SCORE. With a requested 25-49 the two disagreed:\n // the pre-filter waved the server through and handleInstall then refused it,\n // reporting a trust rejection as \"Install failed\" and quoting a threshold the\n // caller never asked for. Taking the stricter of the two makes the pre-filter\n // report exactly what the enforcing gate will do. Deliberately NOT fixed by\n // forwarding minTrustScore instead -- that would let a caller-supplied 30\n // LOWER the gate this path enforces today.\n const minScore = Math.max(\n effectiveMinTrustScore(args.minTrustScore),\n DEFAULT_MIN_TRUST_SCORE,\n );\n\n const installed: Array<{ name: string; trustScore: TrustScore }> = [];\n const skipped: Array<{ name: string; reason: string }> = [];\n\n // Parallel search pass — all keywords searched concurrently. Capture the\n // thrown error per keyword so a registry outage is distinguishable from a\n // genuine empty result (both otherwise look like \"no servers\").\n type SearchOutcome =\n | { ok: true; entries: ServerEntry[] }\n | { ok: false; error: string };\n const searchResults: SearchOutcome[] = await Promise.all(\n keywords.map((kw) =>\n deps\n .registrySearch(kw, 5)\n .then((entries): SearchOutcome => ({ ok: true, entries }))\n .catch((err): SearchOutcome => ({\n ok: false,\n error: err instanceof Error ? err.message : String(err),\n }))\n )\n );\n\n const seenNames = new Set<string>();\n\n // Sequential evaluate/install pass (installs depend on previous state)\n for (let i = 0; i < keywords.length; i++) {\n const keyword = keywords[i];\n const outcome = searchResults[i];\n\n if (!outcome.ok) {\n skipped.push({ name: keyword, reason: `Registry search failed: ${outcome.error}` });\n continue;\n }\n\n const entries = outcome.entries;\n\n if (entries.length === 0) {\n skipped.push({ name: keyword, reason: `No servers found for \"${keyword}\"` });\n continue;\n }\n\n let bestEntry: ServerEntry | null = null;\n let bestTrust: TrustScore | null = null;\n\n for (const entry of entries) {\n if (seenNames.has(entry.server.name)) continue;\n const trust = computeTrust(entry, deps);\n if (bestTrust === null || trust.score > bestTrust.score) {\n bestEntry = entry;\n bestTrust = trust;\n }\n }\n\n if (bestEntry === null || bestTrust === null) {\n skipped.push({ name: keyword, reason: \"All results already installed or duplicated\" });\n continue;\n }\n\n // TODOS #33: the same native-evidence rule as the sibling gates. handleInstall\n // below re-checks and is the enforcing gate, so this pre-filter exists to\n // produce an accurate \"skipped\" reason rather than an \"Install failed\" one —\n // but it must agree with it, or a server rejected downstream gets reported\n // under the wrong heading with a score that was never compared.\n const bestNative = nativeTrustScore(bestTrust);\n if (bestNative.score < minScore) {\n skipped.push({\n name: bestEntry.server.name,\n reason:\n `Trust score ${bestNative.score}/${bestNative.maxPossible} is below minimum ${minScore}` +\n (bestNative.excludedExternalCredit > 0\n ? ` (an external scanner's ${bestNative.excludedExternalCredit} points are excluded — mcpm cannot verify them)`\n : \"\"),\n });\n continue;\n }\n\n try {\n await handleInstall(\n { name: bestEntry.server.name, client: args.client },\n deps,\n { entry: bestEntry, trust: bestTrust }\n );\n seenNames.add(bestEntry.server.name);\n installed.push({ name: bestEntry.server.name, trustScore: bestTrust });\n } catch (err) {\n skipped.push({\n name: bestEntry.server.name,\n reason: `Install failed: ${(err as Error).message}`,\n });\n }\n }\n\n const note = installed.length > 0\n ? \"Restart your AI client to use the newly installed servers.\"\n : undefined;\n\n return { installed, skipped, ...(note ? { note } : {}) };\n}\n\n// ---------------------------------------------------------------------------\n// mcpm_up — batch install from stack file\n// ---------------------------------------------------------------------------\n\nexport async function handleMcpUp(\n args: { stackFile?: string; profile?: string; dryRun?: boolean },\n deps: ServerDeps\n): Promise<{\n installed: string[];\n blocked: string[];\n failed: string[];\n skipped: string[];\n error?: string;\n note?: string;\n}> {\n // Validate stackFile path (AI agent trust boundary). Zod defaults stackFile to\n // \"mcpm.yaml\", so the old `if (args.stackFile !== undefined)` guard was dead.\n // Enforce real containment unconditionally via resolved paths: path.resolve\n // normalizes Windows backslashes and \"..\", so this catches traversal and\n // absolute escapes that string-only checks miss.\n const stackFile = args.stackFile ?? \"mcpm.yaml\";\n const resolved = path.resolve(process.cwd(), stackFile);\n if (\n resolved !== process.cwd() &&\n !resolved.startsWith(process.cwd() + path.sep)\n ) {\n throw new Error(\"stackFile must be within the working directory\");\n }\n // M3: the lexical check above catches \"../\" and absolute escapes, but NOT a\n // symlink that lives inside cwd yet points outside it — the file reader would\n // follow it (arbitrary out-of-tree read). Resolve the REAL path and re-check.\n // realpath throws ENOENT when the file does not exist yet; that's fine — handleUp\n // reports the missing file. A containment failure thrown inside the try is not\n // an ErrnoException, so the catch re-throws it.\n {\n const { realpath } = await import(\"node:fs/promises\");\n try {\n const [realStack, realCwd] = await Promise.all([\n realpath(resolved),\n realpath(process.cwd()),\n ]);\n if (realStack !== realCwd && !realStack.startsWith(realCwd + path.sep)) {\n throw new Error(\"stackFile must be within the working directory\");\n }\n } catch (err) {\n // ENOENT (no such file), ELOOP (circular symlink), and ENOTDIR (a path\n // component is a file) all mean \"no real path to contain\" — fall through and\n // let handleUp report the missing/invalid file. Re-throwing them would leak a\n // raw internal ErrnoException (with stack) to the untrusted caller. The\n // containment Error thrown just above has no `.code`, so it still propagates.\n const code = (err as NodeJS.ErrnoException).code ?? \"\";\n if (![\"ENOENT\", \"ELOOP\", \"ENOTDIR\"].includes(code)) throw err;\n }\n }\n\n const { handleUp } = await import(\"../commands/up.js\");\n const { writeFile } = await import(\"fs/promises\");\n const { handleLock } = await import(\"../commands/lock.js\");\n const { RegistryClient } = await import(\"../registry/client.js\");\n const { scanTier1: st1 } = await import(\"../scanner/tier1.js\");\n const { checkScannerAvailable: csa, scanTier2: st2 } = await import(\"../scanner/tier2.js\");\n const { computeTrustScore: cts } = await import(\"../scanner/trust-score.js\");\n\n const client = new RegistryClient();\n const outputLines: string[] = [];\n // Fix A/D: structured per-server results from handleUp. Authoritative source\n // for categorization — emoji-scraping cannot distinguish blocked from failed.\n const records: Array<{ name: string; status: string }> = [];\n let thrownError: string | undefined;\n\n try {\n await handleUp(\n {\n stackFile,\n profile: args.profile,\n dryRun: args.dryRun,\n ci: true,\n yes: false,\n // MCP surface lockdown (fixes C, D & H1): never auto-read ambient\n // secrets from process.env OR the working-directory .env file, and never\n // install URL servers (they bypass the registry trust gate). All three\n // default to true on the CLI; the MCP (untrusted-caller) surface opts in\n // to the locked-down behavior.\n allowProcessEnv: false,\n allowUrlServers: false,\n allowEnvFile: false,\n // M2: the batch `up` path must honor the same non-overridable trust floor\n // the single-install MCP tool enforces (issue #24), so a low-trust server\n // an agent could not install via mcpm_install can't slip in via mcpm_up.\n minTrustFloor: HARD_TRUST_FLOOR,\n },\n {\n detectClients: deps.detectClients,\n getAdapter: deps.getAdapter,\n getPath: deps.getConfigPath,\n getServer: (name, version?) => client.getServer(name, version),\n scanTier1: st1,\n checkScannerAvailable: csa,\n scanTier2: (name) => st2(name),\n computeTrustScore: cts,\n runLock: async (stackFile) => {\n await handleLock(\n { stackFile },\n {\n getServerVersions: (name) => client.getServerVersions(name),\n getServer: (name, v?) => client.getServer(name, v),\n scanTier1: st1,\n checkScannerAvailable: csa,\n scanTier2: (name) => st2(name),\n computeTrustScore: cts,\n writeLockFile: (path, content) =>\n writeFile(path, content, { encoding: \"utf-8\", mode: 0o600 }),\n fetchNpmIntegrity: _fetchNpmIntegrity,\n fetchNpmProvenance: (id, ver, sri) => _fetchNpmProvenance(id, ver, { integritySri: sri }),\n output: (text) => outputLines.push(text),\n }\n );\n },\n // Issue #22: never auto-confirm on the MCP (no-human-in-loop) surface.\n // The previous `async () => true` blanket-approved every confirmation,\n // including strict-mode *removals* of servers not in mcpm.yaml — a\n // prompt-injected agent could silently mutate client configs. Refusing\n // confirmation here means destructive prompts are declined; the trust\n // policy still gates installs via checkTrustPolicy in handleUp.\n confirm: async () => false,\n promptEnvVar: async () => \"\",\n output: (text) => outputLines.push(text),\n fetchNpmIntegrity: _fetchNpmIntegrity,\n // F8/B3: wire the provenance re-check on the MCP surface too, or a\n // policy.frozen: true stack run through mcpm_up would silently skip it.\n fetchNpmProvenance: (id, v, o) => _fetchNpmProvenance(id, v, o),\n readPins: _readPins,\n recordResult: (r) => records.push(r),\n }\n );\n } catch (err) {\n // Fix A: handleUp throws on early/whole-batch failures (no clients, lock-file\n // creation failure, missing required env in CI, the summary \"N could not be\n // installed\" throw, etc.). The previous bare catch swallowed these into a\n // clean-looking empty result. Capture the message so the caller can never\n // mistake a thrown failure for success.\n thrownError = err instanceof Error ? err.message : String(err);\n }\n\n const installed: string[] = [];\n const blocked: string[] = [];\n const failed: string[] = [];\n const skipped: string[] = [];\n\n if (records.length > 0) {\n // Authoritative path (fix D, F.3/F.5): categorize from handleUp's typed\n // per-server statuses. Unlike emoji-scraping, this reliably separates\n // \"blocked\" (policy/URL-lockdown) from \"failed\".\n for (const r of records) {\n switch (r.status) {\n case \"installed\": installed.push(r.name); break;\n case \"blocked\": blocked.push(r.name); break;\n case \"failed\": failed.push(r.name); break;\n case \"skipped\":\n case \"removed\": skipped.push(r.name); break;\n }\n }\n } else {\n // Fallback for the no-record path (e.g. a throw before any server is\n // processed): preserve the original output-line parsing.\n for (const line of outputLines) {\n if (line.includes(\"\\u2713\")) installed.push(line.trim());\n else if (line.includes(\"\\u2717\") && line.includes(\"blocked\")) blocked.push(line.trim());\n else if (line.includes(\"\\u2717\")) failed.push(line.trim());\n else if (line.includes(\"\\u2022\")) skipped.push(line.trim());\n }\n }\n\n // Fix A, refined for M1: a thrown handleUp failure MUST be signaled \\u2014 but only\n // via the top-level `error` field (set in the return below). The previous\n // version pushed the error *message* into `failed`, which is contracted to hold\n // server NAMES; a consumer iterating it as names got a stray sentence. `error`\n // is the authoritative batch-failure signal; `failed` stays names-only (genuine\n // per-server failures are already recorded into it above via `records`).\n\n return {\n installed,\n blocked,\n failed,\n skipped,\n ...(thrownError !== undefined ? { error: thrownError } : {}),\n ...(installed.length > 0\n ? { note: \"Restart your AI client to use the newly installed servers.\" }\n : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Keyword extraction\n// ---------------------------------------------------------------------------\n\nconst STOPWORDS = /\\b(i need|set up|access|work with|connect to|a server that|a server for|to|the|a|an|my|for|and|with)\\b/gi;\n\nexport function extractKeywords(description: string): string[] {\n const cleaned = description\n .toLowerCase()\n .replace(STOPWORDS, \" \")\n .replace(/[,&]/g, \" \");\n\n const tokens = cleaned\n .split(/\\s+/)\n .map((s) => s.trim())\n .filter((s) => s.length > 2);\n\n // If splitting produced too many tokens, use the full cleaned string\n if (tokens.length > 5) {\n return [cleaned.replace(/\\s+/g, \" \").trim()];\n }\n\n return tokens.length > 0 ? tokens : [description.trim()];\n}\n\n"],"mappings":"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","names":["clientId","stackFile","path"]}
#!/usr/bin/env node
import {
handleUp,
registerUpCommand
} from "./chunk-TX4RGSW7.js";
import "./chunk-C6CAHFQX.js";
import "./chunk-7QRDQF55.js";
import "./chunk-DDCTUMSZ.js";
import "./chunk-E3T224S3.js";
import "./chunk-RTI2GLYX.js";
import "./chunk-OIFKZA4V.js";
import "./chunk-FEXJHHDM.js";
import "./chunk-F6CHEUGO.js";
import "./chunk-LSNEZAFR.js";
import "./chunk-UNGY7RTE.js";
import "./chunk-W4IAFBUN.js";
import "./chunk-2PWW3Q5Q.js";
import "./chunk-MLVDFLDQ.js";
import "./chunk-7RJXJERN.js";
import "./chunk-V4AA4ZL5.js";
import "./chunk-32VRWVOF.js";
import "./chunk-K4U7EXLG.js";
import "./chunk-GZ3WCRLG.js";
import "./chunk-6R7TL5O2.js";
import "./chunk-R4R2VPDA.js";
import "./chunk-4QDJ3I7X.js";
import "./chunk-3X76P3FG.js";
import "./chunk-U7N6FRYF.js";
import "./chunk-WT6V33F2.js";
export {
handleUp,
registerUpCommand
};
//# sourceMappingURL=up-3RX6WQ4D.js.map
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}

Sorry, the diff of this file is too big to display