What you can safely automate against, and what may change without warning. mcpm is
pre-1.0 (0.x); this document is the promise we do keep in the 0.x line, and it
tightens (never loosens) at 1.0.
These are the contract CI and scripts should depend on. 0 = success, non-zero =
do-not-proceed.
| Command | 0 |
non-zero | Notes |
|---|---|---|---|
mcpm --version |
always | — | prints X.Y.Z |
mcpm up |
applied cleanly | 1 |
blocks the run when any server is blocked (trust floor, integrity, policy) or failed, regardless of --ci |
mcpm up --frozen |
lockfile verified, applied | 1 |
fail-closed pre-install verify: blocks on integrity drift, an unverifiable record, a format mismatch, or a missing stack/lock |
mcpm verify |
lockfile integrity verified | 1 |
repo-only, client-free CI gate: the same fail-closed integrity pass as up --frozen (drift / unverifiable / format mismatch / suspicious missing baseline), plus 1 when no lock file is found. --json emits the verify model |
mcpm up --ci |
applied, no prompts | 1 |
non-interactive; also non-zero on shadow collisions when combined with --check-shadowing |
mcpm sync --check |
all clients in sync and every config readable | 2 on drift/conflict, on an entry that failed shape validation, or on a config that could not be read at all; 1 on error |
2 is the drift signal — the value CI consumes. --json emits the drift model. Since 0.37.0 (#59), 2 also covers "could not verify": previously an unreadable entry or config exited 0, reporting in-sync over input never compared |
mcpm audit |
scan complete | 1 when overall trust level is risky; 2 when the invocation cannot be satisfied |
advisory findings (e.g. a delisted/deprecated server) lower the score but do not by themselves flip the exit. 2 is scoped to four invocations mcpm refuses outright: --min-trust above the highest score audit could produce for every scanned server, --fix --json without --yes, --min-trust without --fix, and --sarif with --fix. It is not a general "usage errors exit 2" promise — Commander's own argument-parse failures (e.g. --min-trust 150) still exit 1 |
mcpm audit --report-json |
bounded selected assessment complete or empty inventory | 1 for incomplete assessment or existing risky score; 2 for incompatible report flags |
CLI-only schema-version-1 coverage evidence; no host/installed-byte/health safety verdict. Existing audit formats remain unchanged; report contract |
mcpm scope <project-root> |
fixed candidates inventoried (present or absent) | 1 for an invalid root or incomplete fixed inventory |
read-only Claude Code by default; explicit --client vscode selects a distinct VS Code/Copilot inventory. --json emits a version-1 client-discriminated source/metadata/coverage model, not a health/activation gate. Unsupported profile/host coverage remains unknown even on exit 0; see scope contract |
mcpm review-artifacts <before.tar> <after.tar> --profile <profile> |
bounded byte inventory and supported declaration coverage complete | 1 for incomplete/unsupported input or declaration coverage |
advisory version-1 receipt, never an update approval/safety/activation gate; archive/profile contract |
mcpm doctor |
no blocking issues | 1 |
health check; the cross-client advisory section never changes the exit code |
mcpm install |
installed | 1 |
non-zero on a policy/trust block (--min-trust, --min-release-age, a registry-deleted server) or any failure |
mcpm guard enable / disable |
no Claude Code file-policy refusal | 1 on a denied or unknown Claude Code file-policy transformation, including enable --dry-run |
file-policy projection only; overall host policy stays unknown. Policy refusals count as errors and do not create config backups or confine profiles; unrelated servers may have succeeded |
mcpm guard run (relay) |
child exit 0 |
child's code; 1 |
propagates the wrapped child's exit; fails closed with 1 on a confine hash mismatch, a stripped required marker, or a pins-integrity error |
mcpm guard cleanup |
ran: pruned, a dry run, or nothing to prune | 1 when it refuses |
refuses when pins.json cannot be read or fails its integrity check, and when a pin is held by no readable client config while some detected client's config cannot be read (the servers there are unknown, so the pin cannot be proven orphaned). Changed in 0.43.0 (#236): every refusal exited 0, so mcpm guard cleanup --yes && <next step> read a refusal as success |
mcpm info / mcpm why |
found | 1 when the named server is not in the registry |
Changed in 0.42.0: both printed Server '<name>' not found and exited 0, so mcpm info X && <next step> ran the next step on a server that is not there. The message and its stream (stdout) are unchanged; only the code moved, to match install/remove and the general rule below |
Any command exits 1 on an unhandled error. New non-zero codes may be added for
new failure modes, but the meanings above will not be repurposed within 0.x.
mcpm.yamlcarries a top-levelversion: "1". New optional fields may be added; a breaking change bumps this and ships a documented migration.mcpm-lock.yamlcarrieslockfileVersion: 1. Theintegrityblock is additive/optional (older locks still parse); a breaking change bumps the number.
Registry locks distinguish the MCP publication version from the actual
packageVersion. Launches use npm identifier@version, PyPI identifier===version,
or an explicit OCI tag/digest. PyPI uses strict === equality so a locked public
version cannot select a different local-version suffix. OCI tags are mutable; they pin a launch coordinate,
not image bytes. A digest-bearing identifier is retained even when the registry
also supplies a descriptive release version.
up binds the retained publication metadata to the locked package type, identifier
and package version for every selected server/client before backups, secrets or
config writes, including dry-run. Cursor cannot switch a locked package to HTTP.
Old npm locks may infer package versions from agreeing integrity/provenance
snapshots; absent evidence requires re-locking. Missing/alias package versions in
a registry listing require fixing the listing. Contradictory lock snapshots refuse
in up --frozen and verify. Package-manager downloads are independent of mcpm's
published-record checks: these are not npm ci enforcement of downloaded bytes.
Since v0.46.0: mcpm_search and mcpm_info have versioned success contracts.
Both return schemaVersion: 1, advertise an MCP outputSchema, and return the
same object in structuredContent and the existing JSON text block. Existing
fields retain their types: search's trustScore is a number; info's trustScore
is the full score/breakdown object.
Since v0.49.0, mcpm_doctor exposes its existing schema-version-1 model through
the same outputSchema / structuredContent / JSON text contract. Remaining
tools are unstable in 0.x.
Within schema version 1, the documented fields and meanings below are stable. Consumers must accept additive fields and new finding/reason codes. A breaking change requires a new schema version and a documented migration; stop on an unknown version rather than assuming a pass.
| Field | Search | Info | Meaning |
|---|---|---|---|
schemaVersion |
top level | top level | 1 |
trustScore |
each servers[] row: number |
top level: score object | Existing assessment, not a probability of safety |
maxPossible / level |
each row | inside trustScore |
Score denominator / existing safe, caution, risky classification |
registryStatus |
each row | top level | {status: string | null, statusMessage: string | null, blocksInstall: boolean} |
assessment |
each row | top level | {maxAchievableScore: number, checks: object, findings: Finding[]} |
registryStatus.status is the registry's trimmed, lower-case lifecycle value;
missing/empty means null, and unrecognized values mean unknown status. Only
deleted sets blocksInstall: true. A false value says only that this lifecycle
gate does not block; it does not grant installation or override trust/policy.
The optional registry explanation is control-character-stripped and capped at
256 characters. Descriptions, explanations and finding messages are untrusted
data, never instructions or authorization.
assessment.checks currently reports staticScan: "completed" and
healthCheck, externalScan, releaseCooldown, packageIntegrity, and
provenance as "not_run". No package is executed by search/info. This scope
explains differences from CLI why (which adds cooldown and provenance evidence)
and verify (which checks locked records); do not compare their scores as if
they performed identical checks. findings carries severity, type, message,
location and optional source from the single tier-1 scan.
maxAchievableScore is the assessment-wide pre-health-check ceiling (currently
62/80), not a predicted score for this server. npm launchers incur a low finding
and currently top out at 60/80. Unrun checks are not passed checks; level and
score thresholds are unchanged.
On a handler failure, the MCP result has isError: true and no structured
success evidence; JSON-RPC argument/protocol failures may instead reject the
call. Diagnostic text is not a stable error code. An empty successful search
is {schemaVersion: 1, servers: []}. Neither error nor no match is a clean scan.
The consuming workflow is an agent answering “what is wrong with my MCP setup?”
using the existing read-only mcpm_doctor tool. Prefer structuredContent when
its output schema is advertised; otherwise parse the existing JSON text. Require
schemaVersion: 1 and stop on isError or an unknown version. No new tool input
or CLI JSON format is introduced; the CLI --json surface retains its separate
compatibility policy below.
| Field | Meaning |
|---|---|
schemaVersion |
1 |
clients[] |
id, label, exists, malformed, serverCount, guardedCount for every known client. Missing configs stay exists:false; zero servers is not evidence that the config was read. malformed also covers adapter/read failures; its message is not a precise errno classification. |
runtimes[] |
name and PATH available for npx, uvx, docker. Availability does not prove a server starts or a provider works. |
crossClient |
Advisory {consistent,clientCount,serverCount,drift[]}, or null when fewer than two configs were readable. null means no cross-client comparison, not consistency. |
crossClient.drift[] |
name, kind (conflict, absent, unreadable), present, absent, optional fields / malformed. Skipped entries retain their unreadable identity instead of becoming absent. |
secrets[] |
Advisory client, server, field (env or header), key, label; no detected value. This is not a comprehensive redaction of arbitrary config-controlled names or messages. |
issues[] |
Critical kind (malformed-config, missing-runtime) and diagnostic message. |
ok |
Exactly whether issues is empty. Missing configs, unavailable unused runtimes and advisory findings can coexist with ok:true. It does not certify safety or complete coverage. |
Inspect clients, issues, crossClient and secrets together; an agent must
not reduce the model to ok or an empty issue list. Root and nested output objects
allow additive fields; consumers must tolerate them and future reason codes.
Config-existence failures other than ENOENT/ENOTDIR now propagate. For example,
access denial can return isError:true without structured success evidence;
it is never silently converted to a missing config and clean health. If existence
was established but an adapter read fails, the existing model instead reports
malformed:true, a critical issue and ok:false. The shared helper also makes
uncertain existence checks fail the CLI invocation rather than claim missing.
Doctor validation separates synthetic SDK wire and packed-candidate proof from installed native-client or provider behavior.
Added in 0.42.0, both additive (#92):
mcpm_audit— every per-server row now carrieserror: string | null. It isnullon a row that scored, and a specific reason on a row that did not. The{score: 0, maxPossible: 80, level: "risky"}placeholder on a failed row is UNCHANGED, so a consumer that only reads the score keeps working; previously the reason existed nowhere on this surface and an agent could not tell a delisted server from a network blip from a genuinely risky one.mcpm_up— a newnotices: string[], always present ([]when empty), carrying the advisory lineshandleUpprinted, in order. These reached only the CLI's stdout before, which this surface does not have.
Changed outcome in 0.43.0 (backlog #116, PR #234): mcpm_install on a server the
registry marks deleted now refuses (the tool call errors; nothing is written) where
it installed it. mcpm_setup drops such a match before choosing a keyword's best
candidate and reports it in skipped with the same message instead of installing
it, so skipped can now hold a row for a deleted server alongside an install for
the same keyword. This aligns the MCP surface with the CLI's mcpm install /
mcpm up delisting gate; only an explicit deleted blocks, deprecated and an
absent status do not. The message names the status and carries the registry's
statusMessage, control-character-stripped and truncated to 256 characters.
--json is available on search, install, list, info, audit, update,
outdated, diff, sync, why, doctor, verify, guard list-signatures,
guard doctor-confine, guard inspect, and publish check. Treat these
shapes as unstable in 0.x — fields may be added or renamed — with these
exceptions:
mcpm sync --json(the drift model) is frozen because CI consumes it alongside the exit-2contract above. Changed in 0.37.0 (#59), deliberately and not additively:ServerDriftgainsmalformedandDriftModelgainsunreadableClients, but membership ofservers[]and thedrifted/inSynccounts also change — a server whose entry failed shape validation used to be absent from the model entirely, and a client whose config could not be parsed contributed nothing.sync --checktherefore now exits 2 where it previously exited 0 for a config mcpm could not read. The old exit0was the bug: the gate reported "in sync" over input it had never compared.
The separate opt-in CLI report has a stable versioned contract described in
coverage-aware audit. Read inventory, assessment completeness,
outcome, per-item check states and findings together. Accept additive fields and
reason codes; stop on unknown versions. Existing audit --json (including its
legacy empty-inventory text), SARIF, fix and mcpm_audit shapes/semantics are
unchanged. The new report is incompatible with their output/mutation flags.
Since v0.46.0: the existing VerifyModel has a stable versioned contract.
No result fields, gate decisions or exit codes change. Required fields:
| Field | Type | Meaning |
|---|---|---|
schemaVersion |
1 |
Contract version; accept additive fields, stop on unknown versions |
ok |
boolean | All applicable record checks and declared-stack coverage passed |
verified / checkedNpmCount |
number | Matched npm published records / npm records with baselines |
noBaselines |
boolean | npm entries exist but no integrity baseline can be checked |
blocked |
array | {name, reason, identifier?, npmVersion?}; reasons include drift, format, could-not-verify, missing-baseline |
unenforceable |
string array | Package types/URL servers with no integrity-check mechanism |
provenanceBlocked |
array | {name, identifier, npmVersion, reason, detail}; reasons include signer-changed, regression, unverifiable |
checkedProvenanceCount |
number | Previously crypto-verified baselines eligible for re-check |
uncovered |
string array | Declared servers absent from the lock |
vacuous |
boolean | Empty lock with no stack confirming it is intentional |
error |
optional string | Lock/stack load or operational failure; diagnostic text is not stable |
Exit 1 or ok: false means stop, including an operational error or an
unknown block reason. ok: true can coexist with unenforceable entries and
zero provenance checks: report those limits rather than claiming full coverage.
Zero checked npm records do not prove any package verified. Only crypto-verified
provenance baselines are re-checked; an empty provenanceBlocked array is not
proof that every publisher was verified. A matching published record does not
prove downloaded bytes or code safety. See agent workflows.
Additive (backlog #71), on the unstable shapes: guard inspect --json
findings, each guard-events.jsonl line's findings, and
guard list-signatures --json entries all gain an owasp field — the OWASP MCP
Top 10 pin (see docs/owasp-mcp-mapping.md). mcpm audit --sarif gains a
run.taxonomies entry for the same taxonomy, plus a relationships array
(kind subset, referencing the taxonomy by name) on any rule whose finding
type is pinned to a category. guard list-signatures' human output gains an
owasp line. Nothing existing is renamed, removed, or re-valued.
Since v0.47.0, coverage diagnostic: guard inspect --json and relay events use
the existing finding shape for guard-unsupported-input-request (COVERAGE,
high, sampling_prompt, default warn). It means an embedded request's method
or container/parameter shape was not covered; warn does not mean it was fully
inspected. Known sampling/elicitation siblings still run the detectors and may
block the enclosing result. This diagnostic is not a catalog regex signature;
finding ids remain open to additions. See protocol coverage.
The remaining --json shapes stabilize per-command as they are schema-typed and
documented; until then, pin to the exit codes, not the field names.
mcpm doctor --report is a redacted, human-pasteable text snapshot (not JSON):
OS/arch, mcpm + node versions, per-client server counts, runtime availability,
confine + secret-store backend, and issue counts. It carries no server names or
arguments by design (for pasting into public bug reports). Format is UNSTABLE.
mcpm audit --sarif emits SARIF 2.1.0 — the most CI-automated structured output
mcpm produces (uploaded to GitHub code-scanning). The outer shape is governed by the
SARIF spec; there is one rule per Finding type. The rule catalog and severities may
still change within 0.x. Its exit code follows audit (risky → 1).
These are implementation details, guarded by their own format_version fields and
in-place migrations — they are not part of the public contract:
mcpm guard run --inner …— the internal relay argv and its wrap-marker tokens (--orig-hash,--confine-profile-hash,--confine-required).- The
~/.mcpm/store files —pins.json(PINS_FORMAT_VERSION),guard-policy.yaml,guard-confine.yaml(CONFINE_FORMAT_VERSION), and their.integritysidecars. ~/.mcpm/guard-events.jsonl— append-only; fields may be added. A stable, documented event schema is planned (see the adoption roadmap's SIEM item); until then, parse defensively (unknown fields, best-effort writes).
- macOS, Linux — supported and CI-tested (Ubuntu matrix; macOS runs the confine dogfood).
- Windows — code paths exist (config paths, DPAPI keychain) but are not yet
CI-verified; treat as best-effort.
--confineis macOS-only.