🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@getmcpm/cli

Package Overview
Dependencies
Maintainers
1
Versions
44
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.26.3
to
0.27.0
+455
dist/chunk-2MBO4SX3.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-GZ3WCRLG.js";
import {
CLIENT_IDS,
getConfigPath
} from "./chunk-R4R2VPDA.js";
import {
detectSecretLabels
} from "./chunk-MZCNQU2K.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.27.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-2MBO4SX3.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 {
OWASP_MCP_TOP_10
} from "./chunk-MXHNRCQI.js";
import {
ACTION_RANK,
defaultActionForFinding,
inspectMessage,
normalizeForMatch
} from "./chunk-62744DB3.js";
// src/guard/exfil-names.ts
var EXFIL_PARAM_DENY = [
/^_system_prompt_$/,
/^_conversation_history_$/,
/^_chat_history_$/,
/^_chain_of_thought_$/,
/^_reasoning_trace_$/,
/^_(?:full_)?context_window_$/,
/^_exfil(?:trate)?(?:_[a-z0-9]+)*_$/
];
function canonicalize(rawKey) {
const camelSplit = rawKey.replace(/([a-z0-9])([A-Z])/g, "$1_$2");
return normalizeForMatch(camelSplit).toLowerCase().replace(/[\s-]+/g, "_").replace(/_{2,}/g, "_");
}
function classifyParamName(rawKey) {
const canonical = canonicalize(rawKey);
return EXFIL_PARAM_DENY.some((re) => re.test(canonical)) ? "deny" : null;
}
// src/guard/exfil-params.ts
var EXFIL_PARAM_SIGNATURE_ID = "exfil-param-in-schema";
var MAX_EXCERPT = 200;
var PASS = { action: "pass", findings: [] };
var REMEDIATION = "A tool's input schema declares a parameter named like a context-exfiltration sigil (e.g. `_system_prompt_`) that the model would silently auto-fill from the conversation / system prompt \u2014 a zero-interaction prompt leak. No legitimate tool names a parameter this way. The server's ENTIRE tools/list was blocked before the agent saw it. This is a tripwire for the documented underscore-sigil convention \u2014 a renamed parameter evades it. If you trust this server, mute via `mcpm guard mute exfil-param-in-schema` (re-enables the whole server).";
function truncate(s) {
return s.length > MAX_EXCERPT ? `${s.slice(0, MAX_EXCERPT)}\u2026` : s;
}
function* exfilKeys(schema, depth) {
if (depth > 1 || schema === null || typeof schema !== "object") return;
const props = schema.properties;
if (props === null || typeof props !== "object" || Array.isArray(props)) return;
for (const key of Object.keys(props)) {
if (!Object.hasOwn(props, key)) continue;
if (classifyParamName(key) === "deny") yield key;
yield* exfilKeys(props[key], depth + 1);
}
}
function makeFinding(toolName, rawKey) {
return {
signature_id: EXFIL_PARAM_SIGNATURE_ID,
category: "OWASP-MCP-1",
severity: "critical",
target: "tool_description",
// block-capable carrier (NOT in WARN_ONLY_TARGETS)
matched_text_excerpt: truncate(`parameter "${rawKey}" in tool "${toolName}"`),
remediation: REMEDIATION
};
}
function detectExfilParams(msg) {
if (!("result" in msg)) return PASS;
const tools = msg.result?.tools;
if (!Array.isArray(tools)) return PASS;
const findings = [];
for (const tool of tools) {
if (tool === null || typeof tool !== "object") continue;
const rawName = tool.name;
const toolName = typeof rawName === "string" ? rawName : "<unnamed>";
for (const key of exfilKeys(tool.inputSchema, 0)) {
findings.push(makeFinding(toolName, key));
}
}
if (findings.length === 0) return PASS;
const action = findings.reduce((acc, f) => {
const a = defaultActionForFinding(f);
return ACTION_RANK[a] > ACTION_RANK[acc] ? a : acc;
}, "pass");
return { action, findings };
}
// src/guard/inspect-frame.ts
function withReplyToOrigin(result, replyToOrigin) {
if (replyToOrigin && result.action === "block") return { ...result, replyToOrigin: true };
return result;
}
function mergeInspect(a, b) {
const action = ACTION_RANK[a.action] >= ACTION_RANK[b.action] ? a.action : b.action;
return withReplyToOrigin(
{ action, findings: [...a.findings, ...b.findings] },
a.replyToOrigin === true || b.replyToOrigin === true
);
}
function hasToolsList(msg) {
if (!("result" in msg)) return false;
const result = msg.result;
return Array.isArray(result?.tools);
}
function isServerInitiatedMethod(msg) {
if (!("method" in msg)) return false;
const m = msg.method;
return m === "sampling/createMessage" || m === "elicitation/create";
}
function serverInitiatedContent(msg) {
const params = msg.params;
if (params === null || typeof params !== "object") return [];
const p = params;
const out = [];
if (typeof p.systemPrompt === "string") out.push(p.systemPrompt);
if (Array.isArray(p.messages)) {
for (const m of p.messages) {
if (m !== null && typeof m === "object" && "content" in m) out.push(m.content);
}
}
if (typeof p.message === "string") out.push(p.message);
if (p.requestedSchema !== null && typeof p.requestedSchema === "object") out.push(p.requestedSchema);
return out;
}
function inspectServerInitiated(msg) {
if (!isServerInitiatedMethod(msg)) return null;
const contentLeaves = serverInitiatedContent(msg);
if (contentLeaves.length === 0) return null;
const synthetic = {
jsonrpc: "2.0",
id: 0,
// dummy — the scan reads only the result subtree, never the id.
result: { messages: contentLeaves.map((c) => ({ role: "user", content: c })) }
};
const scan = inspectMessage(synthetic, OWASP_MCP_TOP_10);
if (scan.findings.length === 0) return null;
const findings = scan.findings.map((f) => ({ ...f, target: "sampling_prompt" }));
const action = findings.reduce((acc, f) => {
const a = defaultActionForFinding(f);
return ACTION_RANK[a] > ACTION_RANK[acc] ? a : acc;
}, "pass");
const hasId = "id" in msg && msg.id !== void 0;
return action === "block" && hasId ? { action, findings, replyToOrigin: true } : { action, findings };
}
function inspectFrame(msg) {
const serverInitiated = inspectServerInitiated(msg);
if (serverInitiated !== null) return serverInitiated;
return mergeInspect(inspectMessage(msg, OWASP_MCP_TOP_10), detectExfilParams(msg));
}
export {
withReplyToOrigin,
mergeInspect,
hasToolsList,
inspectFrame
};
//# sourceMappingURL=chunk-74BFQMZZ.js.map
{"version":3,"sources":["../src/guard/exfil-names.ts","../src/guard/exfil-params.ts","../src/guard/inspect-frame.ts"],"sourcesContent":["/**\n * F5 — exfil-param name classifier.\n *\n * Tool-poisoning attackers add an input-schema parameter the model silently\n * auto-fills from context — named with the documented underscore-sigil convention\n * (`_system_prompt_`, `_conversation_history_`, `_chain_of_thought_`) so the model\n * treats it as a magic slot and leaks the conversation/system prompt with zero user\n * interaction (HiddenLayer / CyberArk PoCs vs Claude 3.7). The guard's content\n * regex walks string VALUES (`stringLeaves` yields `Object.values`), so it\n * structurally cannot see a parameter KEY — this classifier fills that gap.\n *\n * DENY tier = ZERO-FP only. A match blocks the server's whole `tools/list` at\n * advertisement time, so a false positive bricks the entire server. We therefore\n * deny ONLY the underscore-WRAPPED sigil form (the attacker tell), and ONLY for\n * nouns no legitimate tool wraps:\n * - `_system_prompt_`, `_conversation_history_`, `_chat_history_`,\n * `_chain_of_thought_`, `_reasoning_trace_`, `_(full_)context_window_`,\n * `_exfil*` / `_exfiltrate*` verbs.\n * DELIBERATELY EXCLUDED (a legit tool/framework genuinely uses these, so they are\n * the deferred SUSPECT tier, never DENY):\n * - bare unwrapped `system_prompt` / `messages` / `reasoning` (real tool inputs);\n * - `_context_` and `_memory_` (agent frameworks — LangGraph `_context`,\n * mem0/letta `_memory` — inject these as runtime slots);\n * - `_thinking_` (reasoning-trace framework slot; `_chain_of_thought_` already\n * covers the malicious CoT intent).\n *\n * HONEST SCOPE: this is a tripwire for the documented underscore-sigil convention,\n * NOT a general context-exfil defense — a renamed parameter (`systemPrompt`,\n * `sys_prompt`, `context_dump`) evades it.\n */\n\nimport { normalizeForMatch } from \"./patterns.js\";\n\n// Match against the CANONICAL key (see canonicalize): homoglyph/zero-width folded,\n// camelCase split, lowercased, separator runs collapsed to a single `_`. So\n// `_systemPrompt_`, `__system__prompt__`, `_System-Prompt_` all reduce to\n// `_system_prompt_`. The leading/trailing `_` is the load-bearing FP gate — a bare\n// `system_prompt` (no wrap) never matches.\nconst EXFIL_PARAM_DENY: ReadonlyArray<RegExp> = [\n /^_system_prompt_$/,\n /^_conversation_history_$/,\n /^_chat_history_$/,\n /^_chain_of_thought_$/,\n /^_reasoning_trace_$/,\n /^_(?:full_)?context_window_$/,\n /^_exfil(?:trate)?(?:_[a-z0-9]+)*_$/,\n];\n\nfunction canonicalize(rawKey: string): string {\n // Split camelCase BEFORE folding so `_systemPrompt_` → `_system_Prompt_`.\n const camelSplit = rawKey.replace(/([a-z0-9])([A-Z])/g, \"$1_$2\");\n return normalizeForMatch(camelSplit)\n .toLowerCase()\n .replace(/[\\s-]+/g, \"_\") // hyphens / whitespace → underscore\n .replace(/_{2,}/g, \"_\"); // collapse runs (wrap stays a single `_`)\n}\n\n/** Returns \"deny\" if the parameter name matches the zero-FP exfil-sigil denylist. */\nexport function classifyParamName(rawKey: string): \"deny\" | null {\n const canonical = canonicalize(rawKey);\n return EXFIL_PARAM_DENY.some((re) => re.test(canonical)) ? \"deny\" : null;\n}\n","/**\n * F5 — structural exfil-param detector for the guard relay.\n *\n * Walks the KEYS of each tool's `inputSchema.properties` in a `tools/list` response\n * and blocks the frame when a parameter name matches the zero-FP exfil-sigil\n * denylist (see exfil-names.ts). Runs at advertisement time — BEFORE the model ever\n * sees the tool — so it closes the line-jumping window the content-regex pipeline\n * cannot (that pipeline only walks string values, never property keys).\n *\n * IMPORTANT (blast radius): a block on a `tools/list` frame replaces the WHOLE frame\n * with one JSON-RPC error, so the server's entire tool surface is disabled until the\n * finding is muted — not just the one poisoned tool. That is why the denylist is\n * strictly zero-FP. The finding reuses the block-capable `tool_description` target\n * (critical → block) so it needs no new SignatureTarget wiring.\n */\n\nimport type { JSONRPCMessage } from \"@modelcontextprotocol/sdk/types.js\";\nimport type { InspectFinding, InspectResult } from \"./types.js\";\nimport { ACTION_RANK, defaultActionForFinding } from \"./patterns.js\";\nimport { classifyParamName } from \"./exfil-names.js\";\n\nexport const EXFIL_PARAM_SIGNATURE_ID = \"exfil-param-in-schema\";\n\nconst MAX_EXCERPT = 200;\nconst PASS: InspectResult = { action: \"pass\", findings: [] };\n\nconst REMEDIATION =\n \"A tool's input schema declares a parameter named like a context-exfiltration sigil \" +\n \"(e.g. `_system_prompt_`) that the model would silently auto-fill from the conversation / \" +\n \"system prompt — a zero-interaction prompt leak. No legitimate tool names a parameter this \" +\n \"way. The server's ENTIRE tools/list was blocked before the agent saw it. This is a tripwire \" +\n \"for the documented underscore-sigil convention — a renamed parameter evades it. If you trust \" +\n \"this server, mute via `mcpm guard mute exfil-param-in-schema` (re-enables the whole server).\";\n\nfunction truncate(s: string): string {\n return s.length > MAX_EXCERPT ? `${s.slice(0, MAX_EXCERPT)}…` : s;\n}\n\n/**\n * Yield every property KEY (bounded to top-level + one nested `properties` level)\n * whose name matches the exfil denylist. Walks `.properties` keys ONLY — never enum\n * values (those live in `sub.enum`, an array we never key-walk), so a legitimate\n * string value like `enum: [\"_system_prompt_\"]` is not flagged. `Object.hasOwn`\n * guards against inherited keys. `$ref`/`allOf`/`anyOf` are not resolved in v1 (the\n * local key is still classified; the ref is not followed).\n */\nfunction* exfilKeys(schema: unknown, depth: number): Iterable<string> {\n if (depth > 1 || schema === null || typeof schema !== \"object\") return;\n const props = (schema as { properties?: unknown }).properties;\n if (props === null || typeof props !== \"object\" || Array.isArray(props)) return;\n for (const key of Object.keys(props)) {\n if (!Object.hasOwn(props, key)) continue;\n if (classifyParamName(key) === \"deny\") yield key;\n yield* exfilKeys((props as Record<string, unknown>)[key], depth + 1);\n }\n}\n\nfunction makeFinding(toolName: string, rawKey: string): InspectFinding {\n return {\n signature_id: EXFIL_PARAM_SIGNATURE_ID,\n category: \"OWASP-MCP-1\",\n severity: \"critical\",\n target: \"tool_description\", // block-capable carrier (NOT in WARN_ONLY_TARGETS)\n matched_text_excerpt: truncate(`parameter \"${rawKey}\" in tool \"${toolName}\"`),\n remediation: REMEDIATION,\n };\n}\n\n/**\n * Inspect a `tools/list` response for exfil-sigil parameter names. A no-op (pass)\n * on every non-tools/list frame. Returns block when any tool declares one.\n */\nexport function detectExfilParams(msg: JSONRPCMessage): InspectResult {\n if (!(\"result\" in msg)) return PASS;\n const tools = (msg as { result?: { tools?: unknown } }).result?.tools;\n if (!Array.isArray(tools)) return PASS;\n\n const findings: InspectFinding[] = [];\n for (const tool of tools) {\n if (tool === null || typeof tool !== \"object\") continue;\n const rawName = (tool as { name?: unknown }).name;\n const toolName = typeof rawName === \"string\" ? rawName : \"<unnamed>\";\n for (const key of exfilKeys((tool as { inputSchema?: unknown }).inputSchema, 0)) {\n findings.push(makeFinding(toolName, key));\n }\n }\n if (findings.length === 0) return PASS;\n\n const action = findings.reduce<InspectResult[\"action\"]>((acc, f) => {\n const a = defaultActionForFinding(f);\n return ACTION_RANK[a] > ACTION_RANK[acc] ? a : acc;\n }, \"pass\");\n return { action, findings };\n}\n","/**\n * The ONE stateless inspection composition — everything the guard can decide\n * about a single frame without relay state (no pins, no session, no policy).\n *\n * Why this module exists: the relay composed three detectors inline\n * (`inspectMessage` + `detectExfilParams` + `inspectServerInitiated`) while\n * `mcpm guard inspect` and the fixture release-gate each called `inspectMessage`\n * alone. So the PUBLIC scoring seam reported `pass` on frames the relay blocks\n * as critical, for 3 of the 12 catalog signatures — and because\n * `mcptox.test.ts` evaluated fixtures through the same incomplete pipeline, a\n * fixture for one of those signatures would have FAILED the release gate. The\n * corpus was shaped by the hole, and mcp-guardbench (which extracts from that\n * corpus) inherited it. One composition, three consumers, no drift.\n *\n * Deliberately excluded — these need relay state and stay in run-inner:\n * schema/handshake drift (pin store + per-session cache) and policy overrides.\n */\n\nimport type { JSONRPCMessage } from \"@modelcontextprotocol/sdk/types.js\";\nimport { inspectMessage, defaultActionForFinding, ACTION_RANK } from \"./patterns.js\";\nimport { detectExfilParams } from \"./exfil-params.js\";\nimport { OWASP_MCP_TOP_10 } from \"./signatures.js\";\nimport type { InspectFinding, InspectResult } from \"./types.js\";\n\n/**\n * H7: replyToOrigin is only meaningful on a block. A policy that downgrades\n * block→warn/pass must not leave a stranded reply-to-origin flag behind.\n */\nexport function withReplyToOrigin(result: InspectResult, replyToOrigin: boolean): InspectResult {\n if (replyToOrigin && result.action === \"block\") return { ...result, replyToOrigin: true };\n return result;\n}\n\nexport function mergeInspect(a: InspectResult, b: InspectResult): InspectResult {\n // Most-severe action wins; concat findings. Uses the shared ACTION_RANK scale\n // (pass < warn < block) instead of a local duplicate map.\n const action = ACTION_RANK[a.action] >= ACTION_RANK[b.action] ? a.action : b.action;\n // H7: carry replyToOrigin if EITHER side requested it (a server-initiated\n // sampling/elicitation block must not be stranded by merging with a benign\n // pattern/drift result). Only kept on a block action (see withReplyToOrigin).\n return withReplyToOrigin(\n { action, findings: [...a.findings, ...b.findings] },\n a.replyToOrigin === true || b.replyToOrigin === true,\n );\n}\n\nexport function hasToolsList(msg: JSONRPCMessage): boolean {\n if (!(\"result\" in msg)) return false;\n const result = (msg as { result?: { tools?: unknown } }).result;\n return Array.isArray(result?.tools);\n}\n\n/** H7: a server-INITIATED sampling/elicitation method frame (id OR no-id — used\n * for content SCANNING; block-to-origin eligibility separately requires an id). */\nfunction isServerInitiatedMethod(msg: JSONRPCMessage): boolean {\n if (!(\"method\" in msg)) return false;\n const m = (msg as { method?: unknown }).method;\n return m === \"sampling/createMessage\" || m === \"elicitation/create\";\n}\n\n/**\n * Extract the server-authored content leaves to scan from a sampling/elicitation\n * request: sampling → params.systemPrompt + params.messages[*].content;\n * elicitation → params.message plus the requestedSchema property descriptions.\n * Non-object/missing shapes yield an empty list (nothing to scan).\n */\nfunction serverInitiatedContent(msg: JSONRPCMessage): unknown[] {\n const params = (msg as { params?: unknown }).params;\n if (params === null || typeof params !== \"object\") return [];\n const p = params as {\n messages?: unknown;\n message?: unknown;\n requestedSchema?: unknown;\n systemPrompt?: unknown;\n };\n const out: unknown[] = [];\n // systemPrompt is server-authored model context (MCP CreateMessageRequestParams)\n // and the highest-leverage sampling injection surface — scan it (review: HIGH).\n if (typeof p.systemPrompt === \"string\") out.push(p.systemPrompt);\n if (Array.isArray(p.messages)) {\n for (const m of p.messages) {\n if (m !== null && typeof m === \"object\" && \"content\" in m) out.push((m as { content: unknown }).content);\n }\n }\n if (typeof p.message === \"string\") out.push(p.message);\n if (p.requestedSchema !== null && typeof p.requestedSchema === \"object\") out.push(p.requestedSchema);\n return out;\n}\n\n/**\n * H7: inspect a server-INITIATED sampling/elicitation request's server-authored\n * content for prompt-injection. Returns block (+ replyToOrigin when the frame can\n * be error-replied) on a detected injection, else null (benign / out of scope) →\n * caller forwards untouched. We gate the injection CONTENT, not the mechanism.\n *\n * The content is wrapped into a synthetic `prompts/get`-shaped frame so the\n * existing `prompt_content` array-content extraction (H1) scans it WITHOUT a new\n * targetSubtree case. But the findings are then RE-TAGGED to `sampling_prompt`:\n * - `prompt_content` is a WARN_ONLY carrier (retrieved prompts/get data), so\n * leaving the finding on it makes applyPolicy's defaultActionForFinding clamp\n * the block back to WARN whenever guard-policy.yaml has ANY signature_override\n * — silently forwarding the injection (CRITICAL, caught in review).\n * - `sampling_prompt` is NOT warn-only, so the action derives from the finding's\n * native severity (critical→block) and survives applyPolicy unclamped.\n * Content scanning covers BOTH id-bearing requests and no-id (notification-shaped)\n * frames; only an id-bearing block carries replyToOrigin (a no-id frame is still\n * dropped — makeBlockResponse returns null for it — but has no reply channel).\n */\nexport function inspectServerInitiated(msg: JSONRPCMessage): InspectResult | null {\n if (!isServerInitiatedMethod(msg)) return null;\n const contentLeaves = serverInitiatedContent(msg);\n if (contentLeaves.length === 0) return null;\n\n const synthetic = {\n jsonrpc: \"2.0\",\n id: 0, // dummy — the scan reads only the result subtree, never the id.\n result: { messages: contentLeaves.map((c) => ({ role: \"user\", content: c })) },\n } as JSONRPCMessage;\n\n const scan = inspectMessage(synthetic, OWASP_MCP_TOP_10);\n if (scan.findings.length === 0) return null;\n\n const findings: InspectFinding[] = scan.findings.map((f) => ({ ...f, target: \"sampling_prompt\" }));\n const action = findings.reduce<InspectResult[\"action\"]>((acc, f) => {\n const a = defaultActionForFinding(f);\n return ACTION_RANK[a] > ACTION_RANK[acc] ? a : acc;\n }, \"pass\");\n\n const hasId = \"id\" in msg && (msg as { id?: unknown }).id !== undefined;\n return action === \"block\" && hasId\n ? { action, findings, replyToOrigin: true }\n : { action, findings };\n}\n\n/**\n * Every stateless verdict the guard can reach for one frame.\n *\n * A server-initiated sampling/elicitation frame SHORT-CIRCUITS, matching the\n * relay: such a frame carries `method`, never `result`, so the pattern and\n * exfil passes would have nothing to inspect anyway.\n */\nexport function inspectFrame(msg: JSONRPCMessage): InspectResult {\n const serverInitiated = inspectServerInitiated(msg);\n if (serverInitiated !== null) return serverInitiated;\n // detectExfilParams self-guards on `result.tools`, so it is a no-op pass on\n // every non-tools/list frame — no caller-side gate needed.\n return mergeInspect(inspectMessage(msg, OWASP_MCP_TOP_10), detectExfilParams(msg));\n}\n"],"mappings":";;;;;;;;;;;;AAsCA,IAAM,mBAA0C;AAAA,EAC9C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,aAAa,QAAwB;AAE5C,QAAM,aAAa,OAAO,QAAQ,sBAAsB,OAAO;AAC/D,SAAO,kBAAkB,UAAU,EAChC,YAAY,EACZ,QAAQ,WAAW,GAAG,EACtB,QAAQ,UAAU,GAAG;AAC1B;AAGO,SAAS,kBAAkB,QAA+B;AAC/D,QAAM,YAAY,aAAa,MAAM;AACrC,SAAO,iBAAiB,KAAK,CAAC,OAAO,GAAG,KAAK,SAAS,CAAC,IAAI,SAAS;AACtE;;;ACxCO,IAAM,2BAA2B;AAExC,IAAM,cAAc;AACpB,IAAM,OAAsB,EAAE,QAAQ,QAAQ,UAAU,CAAC,EAAE;AAE3D,IAAM,cACJ;AAOF,SAAS,SAAS,GAAmB;AACnC,SAAO,EAAE,SAAS,cAAc,GAAG,EAAE,MAAM,GAAG,WAAW,CAAC,WAAM;AAClE;AAUA,UAAU,UAAU,QAAiB,OAAiC;AACpE,MAAI,QAAQ,KAAK,WAAW,QAAQ,OAAO,WAAW,SAAU;AAChE,QAAM,QAAS,OAAoC;AACnD,MAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,EAAG;AACzE,aAAW,OAAO,OAAO,KAAK,KAAK,GAAG;AACpC,QAAI,CAAC,OAAO,OAAO,OAAO,GAAG,EAAG;AAChC,QAAI,kBAAkB,GAAG,MAAM,OAAQ,OAAM;AAC7C,WAAO,UAAW,MAAkC,GAAG,GAAG,QAAQ,CAAC;AAAA,EACrE;AACF;AAEA,SAAS,YAAY,UAAkB,QAAgC;AACrE,SAAO;AAAA,IACL,cAAc;AAAA,IACd,UAAU;AAAA,IACV,UAAU;AAAA,IACV,QAAQ;AAAA;AAAA,IACR,sBAAsB,SAAS,cAAc,MAAM,cAAc,QAAQ,GAAG;AAAA,IAC5E,aAAa;AAAA,EACf;AACF;AAMO,SAAS,kBAAkB,KAAoC;AACpE,MAAI,EAAE,YAAY,KAAM,QAAO;AAC/B,QAAM,QAAS,IAAyC,QAAQ;AAChE,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO;AAElC,QAAM,WAA6B,CAAC;AACpC,aAAW,QAAQ,OAAO;AACxB,QAAI,SAAS,QAAQ,OAAO,SAAS,SAAU;AAC/C,UAAM,UAAW,KAA4B;AAC7C,UAAM,WAAW,OAAO,YAAY,WAAW,UAAU;AACzD,eAAW,OAAO,UAAW,KAAmC,aAAa,CAAC,GAAG;AAC/E,eAAS,KAAK,YAAY,UAAU,GAAG,CAAC;AAAA,IAC1C;AAAA,EACF;AACA,MAAI,SAAS,WAAW,EAAG,QAAO;AAElC,QAAM,SAAS,SAAS,OAAgC,CAAC,KAAK,MAAM;AAClE,UAAM,IAAI,wBAAwB,CAAC;AACnC,WAAO,YAAY,CAAC,IAAI,YAAY,GAAG,IAAI,IAAI;AAAA,EACjD,GAAG,MAAM;AACT,SAAO,EAAE,QAAQ,SAAS;AAC5B;;;ACjEO,SAAS,kBAAkB,QAAuB,eAAuC;AAC9F,MAAI,iBAAiB,OAAO,WAAW,QAAS,QAAO,EAAE,GAAG,QAAQ,eAAe,KAAK;AACxF,SAAO;AACT;AAEO,SAAS,aAAa,GAAkB,GAAiC;AAG9E,QAAM,SAAS,YAAY,EAAE,MAAM,KAAK,YAAY,EAAE,MAAM,IAAI,EAAE,SAAS,EAAE;AAI7E,SAAO;AAAA,IACL,EAAE,QAAQ,UAAU,CAAC,GAAG,EAAE,UAAU,GAAG,EAAE,QAAQ,EAAE;AAAA,IACnD,EAAE,kBAAkB,QAAQ,EAAE,kBAAkB;AAAA,EAClD;AACF;AAEO,SAAS,aAAa,KAA8B;AACzD,MAAI,EAAE,YAAY,KAAM,QAAO;AAC/B,QAAM,SAAU,IAAyC;AACzD,SAAO,MAAM,QAAQ,QAAQ,KAAK;AACpC;AAIA,SAAS,wBAAwB,KAA8B;AAC7D,MAAI,EAAE,YAAY,KAAM,QAAO;AAC/B,QAAM,IAAK,IAA6B;AACxC,SAAO,MAAM,4BAA4B,MAAM;AACjD;AAQA,SAAS,uBAAuB,KAAgC;AAC9D,QAAM,SAAU,IAA6B;AAC7C,MAAI,WAAW,QAAQ,OAAO,WAAW,SAAU,QAAO,CAAC;AAC3D,QAAM,IAAI;AAMV,QAAM,MAAiB,CAAC;AAGxB,MAAI,OAAO,EAAE,iBAAiB,SAAU,KAAI,KAAK,EAAE,YAAY;AAC/D,MAAI,MAAM,QAAQ,EAAE,QAAQ,GAAG;AAC7B,eAAW,KAAK,EAAE,UAAU;AAC1B,UAAI,MAAM,QAAQ,OAAO,MAAM,YAAY,aAAa,EAAG,KAAI,KAAM,EAA2B,OAAO;AAAA,IACzG;AAAA,EACF;AACA,MAAI,OAAO,EAAE,YAAY,SAAU,KAAI,KAAK,EAAE,OAAO;AACrD,MAAI,EAAE,oBAAoB,QAAQ,OAAO,EAAE,oBAAoB,SAAU,KAAI,KAAK,EAAE,eAAe;AACnG,SAAO;AACT;AAqBO,SAAS,uBAAuB,KAA2C;AAChF,MAAI,CAAC,wBAAwB,GAAG,EAAG,QAAO;AAC1C,QAAM,gBAAgB,uBAAuB,GAAG;AAChD,MAAI,cAAc,WAAW,EAAG,QAAO;AAEvC,QAAM,YAAY;AAAA,IAChB,SAAS;AAAA,IACT,IAAI;AAAA;AAAA,IACJ,QAAQ,EAAE,UAAU,cAAc,IAAI,CAAC,OAAO,EAAE,MAAM,QAAQ,SAAS,EAAE,EAAE,EAAE;AAAA,EAC/E;AAEA,QAAM,OAAO,eAAe,WAAW,gBAAgB;AACvD,MAAI,KAAK,SAAS,WAAW,EAAG,QAAO;AAEvC,QAAM,WAA6B,KAAK,SAAS,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,QAAQ,kBAAkB,EAAE;AACjG,QAAM,SAAS,SAAS,OAAgC,CAAC,KAAK,MAAM;AAClE,UAAM,IAAI,wBAAwB,CAAC;AACnC,WAAO,YAAY,CAAC,IAAI,YAAY,GAAG,IAAI,IAAI;AAAA,EACjD,GAAG,MAAM;AAET,QAAM,QAAQ,QAAQ,OAAQ,IAAyB,OAAO;AAC9D,SAAO,WAAW,WAAW,QACzB,EAAE,QAAQ,UAAU,eAAe,KAAK,IACxC,EAAE,QAAQ,SAAS;AACzB;AASO,SAAS,aAAa,KAAoC;AAC/D,QAAM,kBAAkB,uBAAuB,GAAG;AAClD,MAAI,oBAAoB,KAAM,QAAO;AAGrC,SAAO,aAAa,eAAe,KAAK,gBAAgB,GAAG,kBAAkB,GAAG,CAAC;AACnF;","names":[]}
#!/usr/bin/env node
import {
inspectFrame
} from "./chunk-74BFQMZZ.js";
import "./chunk-MXHNRCQI.js";
import {
sanitizeForTerminal
} from "./chunk-FEXJHHDM.js";
import "./chunk-62744DB3.js";
// src/guard/inspect-cli.ts
var ACTION_RANK = { pass: 0, warn: 1, block: 2 };
function parseFrames(rawSource) {
const source = rawSource.replace(/^\uFEFF/, "");
if (source.trim() === "") return [];
try {
return [asFrame(JSON.parse(source))];
} catch {
}
const frames = [];
for (const line of source.split("\n")) {
const trimmed = line.trim();
if (trimmed === "") continue;
try {
frames.push(asFrame(JSON.parse(trimmed)));
} catch (err) {
frames.push({ error: err instanceof Error ? err.message : String(err) });
}
}
return frames;
}
function asFrame(value) {
if (typeof value !== "object" || value === null) {
return { error: `expected a JSON-RPC object, got ${value === null ? "null" : typeof value}` };
}
if (Array.isArray(value)) {
return { error: "expected a single JSON-RPC object, got an array (send batch members as separate NDJSON lines)" };
}
return { frame: value };
}
function findingToJson(f) {
return {
signature_id: f.signature_id,
category: f.category,
severity: f.severity,
target: f.target,
matched_text_excerpt: f.matched_text_excerpt,
remediation: f.remediation,
...f.decoded === true ? { decoded: true } : {}
};
}
function plural(n, word) {
return `${n} ${word}${n === 1 ? "" : "s"}`;
}
function jsonLine(value) {
return JSON.stringify(value).replace(
/[\u007F-\u009F\u2028\u2029]/g,
(c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`
);
}
function runInspectCommand(opts) {
const parsed = parseFrames(opts.source);
const json = opts.json === true;
let worst = "pass";
let errors = 0;
const tally = { pass: 0, warn: 0, block: 0 };
const humanLines = [];
parsed.forEach((entry, i) => {
if ("error" in entry) {
errors += 1;
if (json) {
opts.write(`${jsonLine({ action: "error", error: entry.error })}
`);
} else {
humanLines.push(`frame ${i + 1} \u2014 error: ${sanitizeForTerminal(entry.error)}`);
}
return;
}
const result = inspectFrame(entry.frame);
tally[result.action] += 1;
if (ACTION_RANK[result.action] > ACTION_RANK[worst]) worst = result.action;
if (json) {
opts.write(`${jsonLine({ action: result.action, findings: result.findings.map(findingToJson) })}
`);
return;
}
humanLines.push(`frame ${i + 1} \u2014 ${result.action}`);
for (const f of result.findings) {
humanLines.push(` ${f.signature_id} \xB7 ${f.severity} \xB7 ${f.target}${f.decoded === true ? " \xB7 decoded" : ""}`);
humanLines.push(` excerpt: ${sanitizeForTerminal(f.matched_text_excerpt)}`);
humanLines.push(` fix: ${sanitizeForTerminal(f.remediation)}`);
}
});
if (!json) {
if (parsed.length === 0) {
opts.write("no frames on input\n");
} else {
opts.write(`${humanLines.join("\n")}
`);
const parts = [plural(parsed.length, "frame")];
for (const a of ["block", "warn", "pass"]) {
if (tally[a] > 0) parts.push(`${tally[a]} ${a}`);
}
if (errors > 0) parts.push(plural(errors, "error"));
opts.write(`${parts.join(" \xB7 ")}
`);
}
}
return { action: worst, errors, frames: parsed.length };
}
export {
runInspectCommand
};
//# sourceMappingURL=inspect-cli-ZJF55JOR.js.map
{"version":3,"sources":["../src/guard/inspect-cli.ts"],"sourcesContent":["/**\n * `mcpm guard inspect` — run the guard's signature catalog over MCP JSON-RPC\n * frame(s) offline, with no relay, no wrapped server, and no network.\n *\n * Why this exists as a PUBLIC command (not just an internal function): an\n * external harness — mcp-guardbench, a CI job, a researcher reproducing a\n * finding — needs to ask \"what does mcpm's guard say about this frame?\" without\n * importing `src/guard/*`. Before this command the benchmark's reference adapter\n * vendored an esbuild bundle of patterns+signatures, which (a) silently drifts\n * from the shipped engine and (b) gave mcpm a privileged in-process path that no\n * other guard being scored could have. This command is the level playing field:\n * every guard, mcpm included, is measured through its own published CLI.\n *\n * Contract (depended on by external adapters — treat as semi-stable):\n * - input is ONE JSON frame (pretty-printed is fine) or NDJSON, one per line\n * - `--json` writes exactly one verdict object per input frame, in INPUT\n * ORDER — positional correlation is what lets a harness zip verdicts back\n * to its own case ids without mcpm needing to know about them\n * - an unparseable frame yields `{\"action\":\"error\"}`, never a silent skip and\n * never a fabricated \"pass\" (a harness must be able to tell \"my guard said\n * this is safe\" apart from \"my guard fell over\")\n *\n * The verdict comes from `inspectFrame` — the SAME stateless composition the\n * relay enforces (signature patterns + the F5 exfil-param key walker + the H7\n * server-initiated content scan), including the warn-only carrier clamp, so a\n * `resources/read` injection reports `warn` here exactly as it would in-line.\n * v0.25.0 shipped this command calling `inspectMessage` alone, which silently\n * reported `pass` on frames the relay blocks for 3 of the 12 catalog\n * signatures; `inspect-relay-parity.test.ts` now pins the equivalence.\n *\n * Excluded by design, because they are not properties of the frame: schema and\n * handshake drift (needs the pin store and per-session state) and policy\n * overrides (mute/log_only). This command answers \"what do the signatures\n * see\", not \"what would this user's configured policy do\".\n */\n\nimport type { JSONRPCMessage } from \"@modelcontextprotocol/sdk/types.js\";\nimport { inspectFrame } from \"./inspect-frame.js\";\nimport { sanitizeForTerminal } from \"./sanitize.js\";\nimport type { InspectAction, InspectFinding } from \"./types.js\";\n\nexport interface InspectCliOpts {\n /** Raw input text: one JSON frame, or NDJSON with one frame per line. */\n readonly source: string;\n /** Emit NDJSON verdicts (one line per input frame) instead of human text. */\n readonly json?: boolean;\n readonly write: (s: string) => void;\n}\n\nexport interface InspectCliResult {\n /** Worst action across all frames — drives the process exit code. */\n readonly action: InspectAction;\n /** Frames that could not be parsed as a JSON-RPC object. */\n readonly errors: number;\n /** Frames actually inspected, including the unparseable ones. */\n readonly frames: number;\n}\n\nconst ACTION_RANK: Readonly<Record<InspectAction, number>> = { pass: 0, warn: 1, block: 2 };\n\ntype ParsedFrame = { readonly frame: JSONRPCMessage } | { readonly error: string };\n\n/**\n * Split input into frames. A whole-input parse is tried FIRST so a\n * pretty-printed single frame (the common hand-authored / captured case) works;\n * NDJSON falls through to per-line parsing.\n */\nfunction parseFrames(rawSource: string): readonly ParsedFrame[] {\n // A leading BOM is common in editor-saved captures and makes JSON.parse throw\n // on otherwise-valid input; stripping it avoids a baffling parse error.\n const source = rawSource.replace(/^\\uFEFF/, \"\");\n if (source.trim() === \"\") return [];\n\n try {\n return [asFrame(JSON.parse(source) as unknown)];\n } catch {\n // Not a single JSON document — treat as NDJSON.\n }\n\n const frames: ParsedFrame[] = [];\n for (const line of source.split(\"\\n\")) {\n const trimmed = line.trim();\n if (trimmed === \"\") continue; // blank lines are separators, not frames\n try {\n frames.push(asFrame(JSON.parse(trimmed) as unknown));\n } catch (err) {\n frames.push({ error: err instanceof Error ? err.message : String(err) });\n }\n }\n return frames;\n}\n\n/**\n * A JSON-RPC frame must be a plain object. Arrays (JSON-RPC batches) are\n * rejected rather than silently mis-inspected — `inspectMessage` takes a single\n * message, and quietly passing a batch would report a false \"pass\" on whatever\n * it contains. Send batch members as separate NDJSON lines.\n */\nfunction asFrame(value: unknown): ParsedFrame {\n if (typeof value !== \"object\" || value === null) {\n return { error: `expected a JSON-RPC object, got ${value === null ? \"null\" : typeof value}` };\n }\n if (Array.isArray(value)) {\n return { error: \"expected a single JSON-RPC object, got an array (send batch members as separate NDJSON lines)\" };\n }\n return { frame: value as JSONRPCMessage };\n}\n\nfunction findingToJson(f: InspectFinding): Record<string, unknown> {\n return {\n signature_id: f.signature_id,\n category: f.category,\n severity: f.severity,\n target: f.target,\n matched_text_excerpt: f.matched_text_excerpt,\n remediation: f.remediation,\n ...(f.decoded === true ? { decoded: true } : {}),\n };\n}\n\nfunction plural(n: number, word: string): string {\n return `${n} ${word}${n === 1 ? \"\" : \"s\"}`;\n}\n\n/**\n * Serialize one verdict as a single output line.\n *\n * `JSON.stringify` escapes C0 but leaves two families raw, and BOTH matter here\n * because the excerpt is attacker-controlled:\n *\n * - **U+2028 / U+2029** are line terminators to Node's `readline` (and to\n * ECMAScript), which is exactly how the documented consumer splits this\n * stream. One of them inside an excerpt splits a verdict across two \"lines\"\n * and permanently desyncs a consumer doing positional correlation —\n * reproduced forging a `pass` on a real attack and a `block` on a benign\n * case. That makes one-verdict-per-line a security property, not formatting.\n * - **C1 controls (U+0080–U+009F)** drive a terminal with no ESC byte at all\n * (8-bit CSI/OSC), so \"stringify escapes C0, therefore ESC sequences can't\n * survive\" was true but did not imply safety. `--json` gets piped into\n * terminals while triaging hostile captures.\n *\n * Escaping is LOSSLESS — the consumer's `JSON.parse` yields the identical\n * string — so byte-fidelity of the excerpt is preserved. DEL (U+007F) rides\n * along in the same class.\n */\nfunction jsonLine(value: unknown): string {\n return JSON.stringify(value).replace(\n /[\\u007F-\\u009F\\u2028\\u2029]/g,\n (c) => `\\\\u${c.charCodeAt(0).toString(16).padStart(4, \"0\")}`,\n );\n}\n\nexport function runInspectCommand(opts: InspectCliOpts): InspectCliResult {\n const parsed = parseFrames(opts.source);\n const json = opts.json === true;\n\n let worst: InspectAction = \"pass\";\n let errors = 0;\n const tally: Record<InspectAction, number> = { pass: 0, warn: 0, block: 0 };\n const humanLines: string[] = [];\n\n parsed.forEach((entry, i) => {\n if (\"error\" in entry) {\n errors += 1;\n if (json) {\n opts.write(`${jsonLine({ action: \"error\", error: entry.error })}\\n`);\n } else {\n humanLines.push(`frame ${i + 1} — error: ${sanitizeForTerminal(entry.error)}`);\n }\n return;\n }\n\n const result = inspectFrame(entry.frame);\n tally[result.action] += 1;\n if (ACTION_RANK[result.action] > ACTION_RANK[worst]) worst = result.action;\n\n if (json) {\n // Excerpts keep byte-fidelity (a harness needs to see what matched), but\n // are emitted through jsonLine so no character can break the one-line\n // framing or reach a terminal as a control sequence. See jsonLine.\n opts.write(`${jsonLine({ action: result.action, findings: result.findings.map(findingToJson) })}\\n`);\n return;\n }\n\n humanLines.push(`frame ${i + 1} — ${result.action}`);\n for (const f of result.findings) {\n humanLines.push(` ${f.signature_id} · ${f.severity} · ${f.target}${f.decoded === true ? \" · decoded\" : \"\"}`);\n // Excerpts are attacker-controlled. Sanitize before they reach a\n // terminal, or `guard inspect` becomes the ANSI/OSC injection vector the\n // guard itself detects.\n humanLines.push(` excerpt: ${sanitizeForTerminal(f.matched_text_excerpt)}`);\n humanLines.push(` fix: ${sanitizeForTerminal(f.remediation)}`);\n }\n });\n\n if (!json) {\n if (parsed.length === 0) {\n opts.write(\"no frames on input\\n\");\n } else {\n opts.write(`${humanLines.join(\"\\n\")}\\n\\n`);\n const parts = [plural(parsed.length, \"frame\")];\n for (const a of [\"block\", \"warn\", \"pass\"] as const) {\n if (tally[a] > 0) parts.push(`${tally[a]} ${a}`);\n }\n if (errors > 0) parts.push(plural(errors, \"error\"));\n opts.write(`${parts.join(\" · \")}\\n`);\n }\n }\n\n return { action: worst, errors, frames: parsed.length };\n}\n"],"mappings":"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","names":[]}
#!/usr/bin/env node
import {
buildDriftFinding,
buildHandshakeDriftFinding,
classifyDrift,
classifyHandshakeDrift,
inspectForDrift,
inspectHandshakeForDrift
} from "./chunk-QFYQJDKQ.js";
import {
PolicyIntegrityError,
expireStale,
readPolicy
} from "./chunk-CYYYMOUS.js";
import {
hasToolsList,
inspectFrame,
mergeInspect,
withReplyToOrigin
} from "./chunk-74BFQMZZ.js";
import {
hashConfineProfile,
loadProfile
} from "./chunk-544DEV2D.js";
import {
OWASP_MCP_TOP_10
} from "./chunk-MXHNRCQI.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-62744DB3.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-JQBYVKIO.js.map

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

#!/usr/bin/env node
import {
buildDoctorModel,
execCheckDefault,
formatMcpEntryCommand,
makeCheckConfigExists
} from "./chunk-2MBO4SX3.js";
import {
resolveInstallEntry
} from "./chunk-OVIPM4DT.js";
import {
readPins
} from "./chunk-DDCTUMSZ.js";
import "./chunk-E3T224S3.js";
import {
fetchNpmProvenance
} from "./chunk-QBEWWR7M.js";
import "./chunk-WYSMWP2R.js";
import "./chunk-OIFKZA4V.js";
import "./chunk-FEXJHHDM.js";
import "./chunk-SN3RQIVF.js";
import "./chunk-YU6C7OHM.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-2SYM6O5W.js";
import "./chunk-3X76P3FG.js";
import {
extractRegistryMeta
} from "./chunk-MZCNQU2K.js";
import "./chunk-62744DB3.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);
if (trust.score < minScore) {
throw new Error(
`Server "${args.name}" has trust score ${trust.score}/${trust.maxPossible} (level: ${trust.level}), which is below the minimum threshold of ${minScore}. Install rejected for safety. Use mcpm CLI with --yes to override after manual review.`
);
}
const clients = await resolveClients(args.client, deps);
const installedClients = [];
for (const clientId2 of clients) {
const adapter = deps.getAdapter(clientId2);
const configPath = deps.getConfigPath(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.`
);
}
await adapter.addServer(configPath, args.name, mcpEntry);
installedClients.push(clientId2);
}
await deps.addToStore({
name: args.name,
version: entry.server.version,
clients: [...installedClients],
installedAt: (/* @__PURE__ */ new Date()).toISOString()
});
return {
installed: true,
name: args.name,
version: entry.server.version,
clients: installedClients,
trustScore: trust
};
}
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 = effectiveMinTrustScore(args.minTrustScore);
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;
}
if (bestTrust.score < minScore) {
skipped.push({
name: bestEntry.server.name,
reason: `Trust score ${bestTrust.score}/${bestTrust.maxPossible} is below minimum ${minScore}`
});
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-VGICTIUI.js");
const { writeFile } = await import("fs/promises");
const { handleLock } = await import("./lock-O7O3VM6R.js");
const { RegistryClient } = await import("./client-3RPMRFZL.js");
const { scanTier1: st1 } = await import("./tier1-VFXYMODG.js");
const { checkScannerAvailable: csa, scanTier2: st2 } = await import("./tier2-DE35UF7V.js");
const { computeTrustScore: cts } = await import("./trust-score-IP4Y5SAY.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-VFXYMODG.js");
const { computeTrustScore } = await import("./trust-score-IP4Y5SAY.js");
const { addInstalledServer, removeInstalledServer } = await import("./servers-WFV3RC3Z.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.27.0"
});
registerTools(server, deps);
const transport = new StdioServerTransport();
await server.connect(transport);
}
export {
registerTools,
startServer
};
//# sourceMappingURL=server-T4II2WP6.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 } 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 { 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 */\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 const minScore = effectiveMinTrustScore(args.minTrustScore);\n if (trust.score < minScore) {\n throw new Error(\n `Server \"${args.name}\" has trust score ${trust.score}/${trust.maxPossible} ` +\n `(level: ${trust.level}), which is below the minimum threshold of ${minScore}. ` +\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 const installedClients: ClientId[] = [];\n for (const clientId of clients) {\n const adapter = deps.getAdapter(clientId);\n const configPath = deps.getConfigPath(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 await adapter.addServer(configPath, args.name, mcpEntry);\n installedClients.push(clientId);\n }\n\n await deps.addToStore({\n name: args.name,\n version: entry.server.version,\n clients: [...installedClients],\n installedAt: new Date().toISOString(),\n });\n\n return {\n installed: true,\n name: args.name,\n version: entry.server.version,\n clients: installedClients,\n trustScore: trust,\n };\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 const minScore = effectiveMinTrustScore(args.minTrustScore);\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 if (bestTrust.score < minScore) {\n skipped.push({\n name: bestEntry.server.name,\n reason: `Trust score ${bestTrust.score}/${bestTrust.maxPossible} is below minimum ${minScore}`,\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"]}
+1
-1
{
"name": "@getmcpm/cli",
"version": "0.26.3",
"version": "0.27.0",
"mcpName": "io.github.getmcpm/cli",

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

#!/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-MZCNQU2K.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.26.3",
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-AIAAC2ZX.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 {
OWASP_MCP_TOP_10
} from "./chunk-MXHNRCQI.js";
import {
sanitizeForTerminal
} from "./chunk-FEXJHHDM.js";
import {
inspectMessage
} from "./chunk-62744DB3.js";
// src/guard/inspect-cli.ts
var ACTION_RANK = { pass: 0, warn: 1, block: 2 };
function parseFrames(rawSource) {
const source = rawSource.replace(/^\uFEFF/, "");
if (source.trim() === "") return [];
try {
return [asFrame(JSON.parse(source))];
} catch {
}
const frames = [];
for (const line of source.split("\n")) {
const trimmed = line.trim();
if (trimmed === "") continue;
try {
frames.push(asFrame(JSON.parse(trimmed)));
} catch (err) {
frames.push({ error: err instanceof Error ? err.message : String(err) });
}
}
return frames;
}
function asFrame(value) {
if (typeof value !== "object" || value === null) {
return { error: `expected a JSON-RPC object, got ${value === null ? "null" : typeof value}` };
}
if (Array.isArray(value)) {
return { error: "expected a single JSON-RPC object, got an array (send batch members as separate NDJSON lines)" };
}
return { frame: value };
}
function findingToJson(f) {
return {
signature_id: f.signature_id,
category: f.category,
severity: f.severity,
target: f.target,
matched_text_excerpt: f.matched_text_excerpt,
remediation: f.remediation,
...f.decoded === true ? { decoded: true } : {}
};
}
function plural(n, word) {
return `${n} ${word}${n === 1 ? "" : "s"}`;
}
function jsonLine(value) {
return JSON.stringify(value).replace(
/[\u007F-\u009F\u2028\u2029]/g,
(c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`
);
}
function runInspectCommand(opts) {
const parsed = parseFrames(opts.source);
const json = opts.json === true;
let worst = "pass";
let errors = 0;
const tally = { pass: 0, warn: 0, block: 0 };
const humanLines = [];
parsed.forEach((entry, i) => {
if ("error" in entry) {
errors += 1;
if (json) {
opts.write(`${jsonLine({ action: "error", error: entry.error })}
`);
} else {
humanLines.push(`frame ${i + 1} \u2014 error: ${sanitizeForTerminal(entry.error)}`);
}
return;
}
const result = inspectMessage(entry.frame, OWASP_MCP_TOP_10);
tally[result.action] += 1;
if (ACTION_RANK[result.action] > ACTION_RANK[worst]) worst = result.action;
if (json) {
opts.write(`${jsonLine({ action: result.action, findings: result.findings.map(findingToJson) })}
`);
return;
}
humanLines.push(`frame ${i + 1} \u2014 ${result.action}`);
for (const f of result.findings) {
humanLines.push(` ${f.signature_id} \xB7 ${f.severity} \xB7 ${f.target}${f.decoded === true ? " \xB7 decoded" : ""}`);
humanLines.push(` excerpt: ${sanitizeForTerminal(f.matched_text_excerpt)}`);
humanLines.push(` fix: ${sanitizeForTerminal(f.remediation)}`);
}
});
if (!json) {
if (parsed.length === 0) {
opts.write("no frames on input\n");
} else {
opts.write(`${humanLines.join("\n")}
`);
const parts = [plural(parsed.length, "frame")];
for (const a of ["block", "warn", "pass"]) {
if (tally[a] > 0) parts.push(`${tally[a]} ${a}`);
}
if (errors > 0) parts.push(plural(errors, "error"));
opts.write(`${parts.join(" \xB7 ")}
`);
}
}
return { action: worst, errors, frames: parsed.length };
}
export {
runInspectCommand
};
//# sourceMappingURL=inspect-cli-GGGLUPES.js.map
{"version":3,"sources":["../src/guard/inspect-cli.ts"],"sourcesContent":["/**\n * `mcpm guard inspect` — run the guard's signature catalog over MCP JSON-RPC\n * frame(s) offline, with no relay, no wrapped server, and no network.\n *\n * Why this exists as a PUBLIC command (not just an internal function): an\n * external harness — mcp-guardbench, a CI job, a researcher reproducing a\n * finding — needs to ask \"what does mcpm's guard say about this frame?\" without\n * importing `src/guard/*`. Before this command the benchmark's reference adapter\n * vendored an esbuild bundle of patterns+signatures, which (a) silently drifts\n * from the shipped engine and (b) gave mcpm a privileged in-process path that no\n * other guard being scored could have. This command is the level playing field:\n * every guard, mcpm included, is measured through its own published CLI.\n *\n * Contract (depended on by external adapters — treat as semi-stable):\n * - input is ONE JSON frame (pretty-printed is fine) or NDJSON, one per line\n * - `--json` writes exactly one verdict object per input frame, in INPUT\n * ORDER — positional correlation is what lets a harness zip verdicts back\n * to its own case ids without mcpm needing to know about them\n * - an unparseable frame yields `{\"action\":\"error\"}`, never a silent skip and\n * never a fabricated \"pass\" (a harness must be able to tell \"my guard said\n * this is safe\" apart from \"my guard fell over\")\n *\n * The verdict is the same `inspectMessage` default action the relay uses,\n * including the warn-only carrier clamp — so a `resources/read` injection\n * reports `warn` here exactly as it would in-line. Policy overrides\n * (mute/log_only, `guard.policy.json`) are deliberately NOT applied: this\n * command answers \"what do the signatures see\", not \"what would this user's\n * configured policy do\".\n */\n\nimport type { JSONRPCMessage } from \"@modelcontextprotocol/sdk/types.js\";\nimport { inspectMessage } from \"./patterns.js\";\nimport { sanitizeForTerminal } from \"./sanitize.js\";\nimport { OWASP_MCP_TOP_10 } from \"./signatures.js\";\nimport type { InspectAction, InspectFinding } from \"./types.js\";\n\nexport interface InspectCliOpts {\n /** Raw input text: one JSON frame, or NDJSON with one frame per line. */\n readonly source: string;\n /** Emit NDJSON verdicts (one line per input frame) instead of human text. */\n readonly json?: boolean;\n readonly write: (s: string) => void;\n}\n\nexport interface InspectCliResult {\n /** Worst action across all frames — drives the process exit code. */\n readonly action: InspectAction;\n /** Frames that could not be parsed as a JSON-RPC object. */\n readonly errors: number;\n /** Frames actually inspected, including the unparseable ones. */\n readonly frames: number;\n}\n\nconst ACTION_RANK: Readonly<Record<InspectAction, number>> = { pass: 0, warn: 1, block: 2 };\n\ntype ParsedFrame = { readonly frame: JSONRPCMessage } | { readonly error: string };\n\n/**\n * Split input into frames. A whole-input parse is tried FIRST so a\n * pretty-printed single frame (the common hand-authored / captured case) works;\n * NDJSON falls through to per-line parsing.\n */\nfunction parseFrames(rawSource: string): readonly ParsedFrame[] {\n // A leading BOM is common in editor-saved captures and makes JSON.parse throw\n // on otherwise-valid input; stripping it avoids a baffling parse error.\n const source = rawSource.replace(/^\\uFEFF/, \"\");\n if (source.trim() === \"\") return [];\n\n try {\n return [asFrame(JSON.parse(source) as unknown)];\n } catch {\n // Not a single JSON document — treat as NDJSON.\n }\n\n const frames: ParsedFrame[] = [];\n for (const line of source.split(\"\\n\")) {\n const trimmed = line.trim();\n if (trimmed === \"\") continue; // blank lines are separators, not frames\n try {\n frames.push(asFrame(JSON.parse(trimmed) as unknown));\n } catch (err) {\n frames.push({ error: err instanceof Error ? err.message : String(err) });\n }\n }\n return frames;\n}\n\n/**\n * A JSON-RPC frame must be a plain object. Arrays (JSON-RPC batches) are\n * rejected rather than silently mis-inspected — `inspectMessage` takes a single\n * message, and quietly passing a batch would report a false \"pass\" on whatever\n * it contains. Send batch members as separate NDJSON lines.\n */\nfunction asFrame(value: unknown): ParsedFrame {\n if (typeof value !== \"object\" || value === null) {\n return { error: `expected a JSON-RPC object, got ${value === null ? \"null\" : typeof value}` };\n }\n if (Array.isArray(value)) {\n return { error: \"expected a single JSON-RPC object, got an array (send batch members as separate NDJSON lines)\" };\n }\n return { frame: value as JSONRPCMessage };\n}\n\nfunction findingToJson(f: InspectFinding): Record<string, unknown> {\n return {\n signature_id: f.signature_id,\n category: f.category,\n severity: f.severity,\n target: f.target,\n matched_text_excerpt: f.matched_text_excerpt,\n remediation: f.remediation,\n ...(f.decoded === true ? { decoded: true } : {}),\n };\n}\n\nfunction plural(n: number, word: string): string {\n return `${n} ${word}${n === 1 ? \"\" : \"s\"}`;\n}\n\n/**\n * Serialize one verdict as a single output line.\n *\n * `JSON.stringify` escapes C0 but leaves two families raw, and BOTH matter here\n * because the excerpt is attacker-controlled:\n *\n * - **U+2028 / U+2029** are line terminators to Node's `readline` (and to\n * ECMAScript), which is exactly how the documented consumer splits this\n * stream. One of them inside an excerpt splits a verdict across two \"lines\"\n * and permanently desyncs a consumer doing positional correlation —\n * reproduced forging a `pass` on a real attack and a `block` on a benign\n * case. That makes one-verdict-per-line a security property, not formatting.\n * - **C1 controls (U+0080–U+009F)** drive a terminal with no ESC byte at all\n * (8-bit CSI/OSC), so \"stringify escapes C0, therefore ESC sequences can't\n * survive\" was true but did not imply safety. `--json` gets piped into\n * terminals while triaging hostile captures.\n *\n * Escaping is LOSSLESS — the consumer's `JSON.parse` yields the identical\n * string — so byte-fidelity of the excerpt is preserved. DEL (U+007F) rides\n * along in the same class.\n */\nfunction jsonLine(value: unknown): string {\n return JSON.stringify(value).replace(\n /[\\u007F-\\u009F\\u2028\\u2029]/g,\n (c) => `\\\\u${c.charCodeAt(0).toString(16).padStart(4, \"0\")}`,\n );\n}\n\nexport function runInspectCommand(opts: InspectCliOpts): InspectCliResult {\n const parsed = parseFrames(opts.source);\n const json = opts.json === true;\n\n let worst: InspectAction = \"pass\";\n let errors = 0;\n const tally: Record<InspectAction, number> = { pass: 0, warn: 0, block: 0 };\n const humanLines: string[] = [];\n\n parsed.forEach((entry, i) => {\n if (\"error\" in entry) {\n errors += 1;\n if (json) {\n opts.write(`${jsonLine({ action: \"error\", error: entry.error })}\\n`);\n } else {\n humanLines.push(`frame ${i + 1} — error: ${sanitizeForTerminal(entry.error)}`);\n }\n return;\n }\n\n const result = inspectMessage(entry.frame, OWASP_MCP_TOP_10);\n tally[result.action] += 1;\n if (ACTION_RANK[result.action] > ACTION_RANK[worst]) worst = result.action;\n\n if (json) {\n // Excerpts keep byte-fidelity (a harness needs to see what matched), but\n // are emitted through jsonLine so no character can break the one-line\n // framing or reach a terminal as a control sequence. See jsonLine.\n opts.write(`${jsonLine({ action: result.action, findings: result.findings.map(findingToJson) })}\\n`);\n return;\n }\n\n humanLines.push(`frame ${i + 1} — ${result.action}`);\n for (const f of result.findings) {\n humanLines.push(` ${f.signature_id} · ${f.severity} · ${f.target}${f.decoded === true ? \" · decoded\" : \"\"}`);\n // Excerpts are attacker-controlled. Sanitize before they reach a\n // terminal, or `guard inspect` becomes the ANSI/OSC injection vector the\n // guard itself detects.\n humanLines.push(` excerpt: ${sanitizeForTerminal(f.matched_text_excerpt)}`);\n humanLines.push(` fix: ${sanitizeForTerminal(f.remediation)}`);\n }\n });\n\n if (!json) {\n if (parsed.length === 0) {\n opts.write(\"no frames on input\\n\");\n } else {\n opts.write(`${humanLines.join(\"\\n\")}\\n\\n`);\n const parts = [plural(parsed.length, \"frame\")];\n for (const a of [\"block\", \"warn\", \"pass\"] as const) {\n if (tally[a] > 0) parts.push(`${tally[a]} ${a}`);\n }\n if (errors > 0) parts.push(plural(errors, \"error\"));\n opts.write(`${parts.join(\" · \")}\\n`);\n }\n }\n\n return { action: worst, errors, frames: parsed.length };\n}\n"],"mappings":"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","names":[]}
#!/usr/bin/env node
import {
buildDriftFinding,
buildHandshakeDriftFinding,
classifyDrift,
classifyHandshakeDrift,
inspectForDrift,
inspectHandshakeForDrift
} from "./chunk-QFYQJDKQ.js";
import {
PolicyIntegrityError,
expireStale,
readPolicy
} from "./chunk-CYYYMOUS.js";
import {
hashConfineProfile,
loadProfile
} from "./chunk-544DEV2D.js";
import {
OWASP_MCP_TOP_10
} from "./chunk-MXHNRCQI.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,
normalizeForMatch
} from "./chunk-62744DB3.js";
// src/guard/exfil-names.ts
var EXFIL_PARAM_DENY = [
/^_system_prompt_$/,
/^_conversation_history_$/,
/^_chat_history_$/,
/^_chain_of_thought_$/,
/^_reasoning_trace_$/,
/^_(?:full_)?context_window_$/,
/^_exfil(?:trate)?(?:_[a-z0-9]+)*_$/
];
function canonicalize(rawKey) {
const camelSplit = rawKey.replace(/([a-z0-9])([A-Z])/g, "$1_$2");
return normalizeForMatch(camelSplit).toLowerCase().replace(/[\s-]+/g, "_").replace(/_{2,}/g, "_");
}
function classifyParamName(rawKey) {
const canonical = canonicalize(rawKey);
return EXFIL_PARAM_DENY.some((re) => re.test(canonical)) ? "deny" : null;
}
// src/guard/exfil-params.ts
var EXFIL_PARAM_SIGNATURE_ID = "exfil-param-in-schema";
var MAX_EXCERPT = 200;
var PASS = { action: "pass", findings: [] };
var REMEDIATION = "A tool's input schema declares a parameter named like a context-exfiltration sigil (e.g. `_system_prompt_`) that the model would silently auto-fill from the conversation / system prompt \u2014 a zero-interaction prompt leak. No legitimate tool names a parameter this way. The server's ENTIRE tools/list was blocked before the agent saw it. This is a tripwire for the documented underscore-sigil convention \u2014 a renamed parameter evades it. If you trust this server, mute via `mcpm guard mute exfil-param-in-schema` (re-enables the whole server).";
function truncate(s) {
return s.length > MAX_EXCERPT ? `${s.slice(0, MAX_EXCERPT)}\u2026` : s;
}
function* exfilKeys(schema, depth) {
if (depth > 1 || schema === null || typeof schema !== "object") return;
const props = schema.properties;
if (props === null || typeof props !== "object" || Array.isArray(props)) return;
for (const key of Object.keys(props)) {
if (!Object.hasOwn(props, key)) continue;
if (classifyParamName(key) === "deny") yield key;
yield* exfilKeys(props[key], depth + 1);
}
}
function makeFinding(toolName, rawKey) {
return {
signature_id: EXFIL_PARAM_SIGNATURE_ID,
category: "OWASP-MCP-1",
severity: "critical",
target: "tool_description",
// block-capable carrier (NOT in WARN_ONLY_TARGETS)
matched_text_excerpt: truncate(`parameter "${rawKey}" in tool "${toolName}"`),
remediation: REMEDIATION
};
}
function detectExfilParams(msg) {
if (!("result" in msg)) return PASS;
const tools = msg.result?.tools;
if (!Array.isArray(tools)) return PASS;
const findings = [];
for (const tool of tools) {
if (tool === null || typeof tool !== "object") continue;
const rawName = tool.name;
const toolName = typeof rawName === "string" ? rawName : "<unnamed>";
for (const key of exfilKeys(tool.inputSchema, 0)) {
findings.push(makeFinding(toolName, key));
}
}
if (findings.length === 0) return PASS;
const action = findings.reduce((acc, f) => {
const a = defaultActionForFinding(f);
return ACTION_RANK[a] > ACTION_RANK[acc] ? a : acc;
}, "pass");
return { action, findings };
}
// 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 mergeInspect(a, b) {
const action = ACTION_RANK[a.action] >= ACTION_RANK[b.action] ? a.action : b.action;
return withReplyToOrigin(
{ action, findings: [...a.findings, ...b.findings] },
a.replyToOrigin === true || b.replyToOrigin === true
);
}
function withReplyToOrigin(result, replyToOrigin) {
if (replyToOrigin && result.action === "block") return { ...result, replyToOrigin: true };
return result;
}
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 hasToolsList(msg) {
if (!("result" in msg)) return false;
const result = msg.result;
return Array.isArray(result?.tools);
}
function isServerInitiatedMethod(msg) {
if (!("method" in msg)) return false;
const m = msg.method;
return m === "sampling/createMessage" || m === "elicitation/create";
}
function inspectServerInitiated(msg) {
if (!isServerInitiatedMethod(msg)) return null;
const contentLeaves = serverInitiatedContent(msg);
if (contentLeaves.length === 0) return null;
const synthetic = {
jsonrpc: "2.0",
id: 0,
// dummy — the scan reads only the result subtree, never the id.
result: { messages: contentLeaves.map((c) => ({ role: "user", content: c })) }
};
const scan = inspectMessage(synthetic, OWASP_MCP_TOP_10);
if (scan.findings.length === 0) return null;
const findings = scan.findings.map((f) => ({ ...f, target: "sampling_prompt" }));
const action = findings.reduce((acc, f) => {
const a = defaultActionForFinding(f);
return ACTION_RANK[a] > ACTION_RANK[acc] ? a : acc;
}, "pass");
const hasId = "id" in msg && msg.id !== void 0;
return action === "block" && hasId ? { action, findings, replyToOrigin: true } : { action, findings };
}
function serverInitiatedContent(msg) {
const params = msg.params;
if (params === null || typeof params !== "object") return [];
const p = params;
const out = [];
if (typeof p.systemPrompt === "string") out.push(p.systemPrompt);
if (Array.isArray(p.messages)) {
for (const m of p.messages) {
if (m !== null && typeof m === "object" && "content" in m) out.push(m.content);
}
}
if (typeof p.message === "string") out.push(p.message);
if (p.requestedSchema !== null && typeof p.requestedSchema === "object") out.push(p.requestedSchema);
return out;
}
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 serverInitiated = inspectServerInitiated(msg);
if (serverInitiated !== null) return applyPolicy(serverInitiated, policy);
const patternResult = inspectMessage(msg, OWASP_MCP_TOP_10);
let driftResult = { action: "pass", findings: [] };
let exfilResult = { action: "pass", findings: [] };
if (hasToolsList(msg)) {
exfilResult = detectExfilParams(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(mergeInspect(patternResult, driftResult), exfilResult), 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,
inspectServerInitiated,
isInitializeResult,
isToolsListChangedNotification,
mergeInspect,
runInner
};
//# sourceMappingURL=run-inner-AEJPKZM5.js.map

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

#!/usr/bin/env node
import {
buildDoctorModel,
execCheckDefault,
formatMcpEntryCommand,
makeCheckConfigExists
} from "./chunk-AIAAC2ZX.js";
import {
resolveInstallEntry
} from "./chunk-OVIPM4DT.js";
import {
readPins
} from "./chunk-DDCTUMSZ.js";
import "./chunk-E3T224S3.js";
import {
fetchNpmProvenance
} from "./chunk-QBEWWR7M.js";
import "./chunk-WYSMWP2R.js";
import "./chunk-OIFKZA4V.js";
import "./chunk-FEXJHHDM.js";
import "./chunk-SN3RQIVF.js";
import "./chunk-YU6C7OHM.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-2SYM6O5W.js";
import "./chunk-3X76P3FG.js";
import {
extractRegistryMeta
} from "./chunk-MZCNQU2K.js";
import "./chunk-62744DB3.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);
if (trust.score < minScore) {
throw new Error(
`Server "${args.name}" has trust score ${trust.score}/${trust.maxPossible} (level: ${trust.level}), which is below the minimum threshold of ${minScore}. Install rejected for safety. Use mcpm CLI with --yes to override after manual review.`
);
}
const clients = await resolveClients(args.client, deps);
const installedClients = [];
for (const clientId2 of clients) {
const adapter = deps.getAdapter(clientId2);
const configPath = deps.getConfigPath(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.`
);
}
await adapter.addServer(configPath, args.name, mcpEntry);
installedClients.push(clientId2);
}
await deps.addToStore({
name: args.name,
version: entry.server.version,
clients: [...installedClients],
installedAt: (/* @__PURE__ */ new Date()).toISOString()
});
return {
installed: true,
name: args.name,
version: entry.server.version,
clients: installedClients,
trustScore: trust
};
}
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 = effectiveMinTrustScore(args.minTrustScore);
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;
}
if (bestTrust.score < minScore) {
skipped.push({
name: bestEntry.server.name,
reason: `Trust score ${bestTrust.score}/${bestTrust.maxPossible} is below minimum ${minScore}`
});
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-VGICTIUI.js");
const { writeFile } = await import("fs/promises");
const { handleLock } = await import("./lock-O7O3VM6R.js");
const { RegistryClient } = await import("./client-3RPMRFZL.js");
const { scanTier1: st1 } = await import("./tier1-VFXYMODG.js");
const { checkScannerAvailable: csa, scanTier2: st2 } = await import("./tier2-DE35UF7V.js");
const { computeTrustScore: cts } = await import("./trust-score-IP4Y5SAY.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-VFXYMODG.js");
const { computeTrustScore } = await import("./trust-score-IP4Y5SAY.js");
const { addInstalledServer, removeInstalledServer } = await import("./servers-WFV3RC3Z.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.26.3"
});
registerTools(server, deps);
const transport = new StdioServerTransport();
await server.connect(transport);
}
export {
registerTools,
startServer
};
//# sourceMappingURL=server-UCXWUSQF.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 } 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 { 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 */\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 const minScore = effectiveMinTrustScore(args.minTrustScore);\n if (trust.score < minScore) {\n throw new Error(\n `Server \"${args.name}\" has trust score ${trust.score}/${trust.maxPossible} ` +\n `(level: ${trust.level}), which is below the minimum threshold of ${minScore}. ` +\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 const installedClients: ClientId[] = [];\n for (const clientId of clients) {\n const adapter = deps.getAdapter(clientId);\n const configPath = deps.getConfigPath(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 await adapter.addServer(configPath, args.name, mcpEntry);\n installedClients.push(clientId);\n }\n\n await deps.addToStore({\n name: args.name,\n version: entry.server.version,\n clients: [...installedClients],\n installedAt: new Date().toISOString(),\n });\n\n return {\n installed: true,\n name: args.name,\n version: entry.server.version,\n clients: installedClients,\n trustScore: trust,\n };\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 const minScore = effectiveMinTrustScore(args.minTrustScore);\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 if (bestTrust.score < minScore) {\n skipped.push({\n name: bestEntry.server.name,\n reason: `Trust score ${bestTrust.score}/${bestTrust.maxPossible} is below minimum ${minScore}`,\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"]}

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