Selected implementation paths are shown below; this is not an exhaustive file inventory.
mcpm/
├── src/
│ ├── index.ts — CLI entry point (Commander)
│ ├── artifacts/ — bounded USTAR snapshot reading + skill/plugin review
│ ├── commands/
│ │ ├── index.ts — command registration
│ │ ├── search.ts — search the MCP registry
│ │ ├── install.ts — install a server to client configs
│ │ ├── info.ts — show server details
│ │ ├── list.ts — list installed servers
│ │ ├── remove.ts — remove a server from configs
│ │ ├── audit.ts — trust-scan installed servers
│ │ ├── update.ts — update installed servers
│ │ ├── doctor.ts — check MCP setup health
│ │ ├── init.ts — scaffold an empty mcpm.yaml stack
│ │ ├── scope.ts — read-only Claude Code / VS Code source inventory
│ │ ├── review-artifacts.ts — local skill/plugin archive comparison
│ │ ├── import.ts — import servers from client config
│ │ ├── serve.ts — start mcpm as an MCP server
│ │ ├── disable.ts — disable a server (thin wrapper over toggle)
│ │ ├── enable.ts — re-enable a disabled server
│ │ ├── toggle.ts — shared disable/enable logic
│ │ ├── completions.ts — shell completion scripts (bash/zsh/fish)
│ │ ├── alias.ts — short aliases for server names
│ │ ├── export.ts — export installed servers as mcpm.yaml
│ │ ├── lock.ts — resolve versions + trust → mcpm-lock.yaml
│ │ ├── up.ts — batch install from mcpm.yaml with trust policy
│ │ └── diff.ts — compare installed vs declared state
│ ├── server/
│ │ ├── index.ts — MCP server setup (registerTool, stdio transport)
│ │ ├── tools.ts — Zod input schemas for each tool
│ │ └── handlers.ts — tool handlers (wraps existing CLI logic)
│ ├── registry/
│ │ ├── index.ts — public API barrel export
│ │ ├── client.ts — RegistryClient (HTTP, injectable fetch)
│ │ ├── publish-client.ts — `mcpm publish`: GitHub token/OIDC → registry JWT exchange, then POST /v0.1/publish
│ │ ├── http-utils.ts — shared capped-body reader (decompression-bomb cap) + the deadline-covered body read and the one bounded retry
│ │ ├── schemas.ts — Zod schemas for API responses
│ │ ├── types.ts — TypeScript types inferred from schemas.ts
│ │ ├── argument-tokens.ts — shared extractor of scannable string tokens from a runtime Argument
│ │ ├── npm-integrity.ts — npm `dist.integrity` tripwire (H11)
│ │ ├── npm-provenance.ts — parse-only Sigstore attestation identity + the lazy crypto verdict (F8)
│ │ ├── sigstore-verify.ts — offline Sigstore verification; the ONLY module that may conclude "verified"
│ │ ├── sigstore-trusted-root.json — vendored sigstore/root-signing trusted root
│ │ └── errors.ts — RegistryError, NotFoundError, NetworkError, ValidationError + describeRegistryError (the shared failure classifier)
│ ├── config/
│ │ ├── paths.ts — OS-aware config file paths
│ │ ├── detector.ts — detect installed AI clients
│ │ └── adapters/
│ │ ├── base.ts — shared adapter logic
│ │ ├── claude-code.ts
│ │ ├── claude-desktop.ts
│ │ ├── cursor.ts
│ │ ├── gemini-cli.ts
│ │ ├── vscode.ts
│ │ ├── windsurf.ts
│ │ ├── parked-servers.ts — shared Cursor/Gemini disabled-definition storage
│ │ └── factory.ts — adapter factory by client ID
│ ├── scanner/
│ │ ├── trust-score.ts — 0-100 composite trust score
│ │ ├── tier1.ts — metadata-based checks (registry meta)
│ │ ├── tier2.ts — opt-in external scanner (MCPM_EXTERNAL_SCANNER)
│ │ └── patterns.ts — regex patterns for secrets, injection, typosquatting
│ ├── store/
│ │ ├── index.ts — local JSON state manager (~/.mcpm/)
│ │ ├── servers.ts — installed server registry (~/.mcpm/servers.json)
│ │ ├── aliases.ts — server name aliases (~/.mcpm/aliases.json)
│ │ ├── atomic.ts — symlink-safe atomic writes + locked read-modify-write
│ │ ├── keychain.ts — AES-GCM encrypted secret store (~/.mcpm/secrets.enc.json)
│ │ └── os-keychain.ts — zero-native-dep master-key access (macOS `security` / libsecret / DPAPI)
│ ├── stack/
│ │ ├── schema.ts — Zod schemas for mcpm.yaml + mcpm-lock.yaml
│ │ ├── resolve.ts — semver range resolution
│ │ ├── policy.ts — trust policy enforcement (normalized-percentage comparisons)
│ │ ├── paths.ts — stack→lock path derivation, shared by lock/up/verify/diff
│ │ ├── frozen-verify.ts — shared lockfile INTEGRITY verification (F3 / D2)
│ │ ├── frozen-provenance.ts — shared lockfile PROVENANCE re-verification (F8 / "B3")
│ │ ├── env.ts — .env file parser
│ │ └── index.ts — public API surface
│ ├── guard/ — v0.5.0 runtime defense (mcpm-guard)
│ │ ├── types.ts — Severity, Signature, InspectResult types (including discovery_metadata)
│ │ ├── patterns.ts — pattern engine (NFKC + leaf walk + regex + decode-and-rescan)
│ │ ├── signatures.ts — the shipped signature catalog
│ │ ├── owasp.ts — OWASP MCP Top 10 (beta) pin carried on every emitted finding
│ │ ├── inspect-frame.ts — the ONE stateless composition every consumer shares (relay, `guard inspect`, release gate)
│ │ ├── relay.ts — production stdio MITM (subprocess + in-process variants)
│ │ ├── wrap.ts — entry transformation (wrapEntry/unwrapEntry/isWrapped)
│ │ ├── orchestrator.ts — two-phase commit across detected clients
│ │ ├── pins.ts — schema-pin storage + integrity sidecar
│ │ ├── drift.ts — schema/handshake drift detection + accept-drift application
│ │ ├── policy.ts — guard-policy.yaml (mute/pause overrides)
│ │ ├── run-inner.ts — `mcpm guard run --inner` entry, wires the relay
│ │ ├── inspect-cli.ts — `mcpm guard inspect`: offline verdicts over captured frames (the public scoring seam)
│ │ ├── event-log.ts — append-only JSONL writer for guard-events.jsonl
│ │ ├── sanitize.ts — shared ANSI/control-char terminal sanitizer
│ │ ├── store-integrity.ts — shared fileSha / assertNotSymlink / writeFileAtomic (pins + policy + confine)
│ │ ├── unguarded.ts — H9 deny-by-default consent store for un-wrappable (HTTP/SSE) transports
│ │ ├── shadow.ts — F2 cross-server tool-name-collision detector
│ │ ├── key-canon.ts — shared identifier-KEY canonicalization (homoglyph fold + camelCase split)
│ │ ├── exfil-names.ts — F5 exfil-param name classifier (underscore-sigil denylist)
│ │ ├── exfil-params.ts — F5 structural exfil-param detector over `tools/list` inputSchema keys
│ │ ├── tool-call-args-walk.ts — shared `tools/call` argument-tree walker for the key+value detectors
│ │ ├── shell-metachar-args.ts — shell-metacharacter detector (CVE-2025-53818 / CVE-2026-25546 shape)
│ │ ├── query-control-args.ts — query-control-syntax detector (CVE-2026-33980 shape)
│ │ ├── cli-flag-injection-args.ts — embedded CLI-flag detector (CVE-2026-39884 shape)
│ │ ├── tool-name-confusable.ts — tool-NAME inspection: confusable duplicates + deceptive characters
│ │ ├── cli.ts — Commander glue for enable/disable/status/cleanup
│ │ ├── confine/ — OS confinement (F1, macOS-only, opt-in via --confine)
│ │ │ ├── profile.ts — platform-neutral ConfineProfile shape
│ │ │ ├── derive.ts — derive a standard-tier profile for a server (pure, no I/O)
│ │ │ ├── backend-macos.ts — macOS Seatbelt backend: renders SBPL + the sandbox-exec argv
│ │ │ ├── apply.ts — wrap the child spawn argv with the backend
│ │ │ ├── store.ts — ~/.mcpm/guard-confine.yaml enrollment store (+ integrity)
│ │ │ └── decide.ts — spawn-time confine/fail-closed/hybrid-warn decision
│ │ └── demo/ — synthetic echo-bot + runner for `mcpm guard demo`
│ └── utils/
│ ├── output.ts — leveled output helpers
│ ├── confirm.ts — confirmation prompts
│ ├── format-entry.ts — format MCP server config entries
│ ├── format-trust.ts — format trust score display
│ └── fs.ts — shared filesystem helpers (isEnoent)
├── src/__tests__/
│ ├── commands/ — CLI command tests
│ ├── config/ — adapter + detector + paths + drift tests
│ ├── output/ — SARIF mapper tests
│ ├── registry/ — registry-client tests
│ ├── server/ — MCP tool-surface tests
│ ├── smoke/ — built-binary output-contract smoke matrix
│ ├── stack/ — stack/lock schema + policy + frozen-gate tests
│ ├── store/ — servers + aliases + atomic + keychain tests
│ ├── engines-invariant.test.ts — `engines.node` vs the real dependency tree
│ └── test-isolation.test.ts — guards the suite against touching the real $HOME
├── scripts/
│ ├── demo.sh — asciinema demo recording script
│ ├── dogfood-confine.sh — hermetic end-to-end `--confine` dogfood (real macOS Seatbelt enforcement)
│ ├── dogfood-release.sh — pack → clean-install → smoke-run the real binary; gates the supported tag-release workflow (local prepublishOnly builds only)
│ ├── cursor-client/run.mjs — isolated real Cursor CLI config/lifecycle/discovery harness
│ ├── gemini-client/run.mjs — isolated real Gemini CLI lifecycle/connection harness
│ └── launch-checklist.md — pre/post-launch checklist
├── .github/workflows/
│ ├── ci.yml — build + test on push/PR (Node 22, 24, 26) + the `confine-macos` Seatbelt dogfood leg
│ ├── dogfood.yml — on-demand smoke of an ALREADY-PUBLISHED version on GitHub's machines
│ ├── publish.yml — packed-artifact gate (Node 22, 24, 26) then npm publish + GitHub Release, on v* tags; then a `registry` job listing the version on the official MCP registry via OIDC (also reachable alone via `workflow_dispatch`)
│ └── scorecard.yml — OpenSSF Scorecard supply-chain posture (weekly + on push to main)
├── .github/actions/mcpm-verify/ — composite Action wrapping `mcpm verify --json` for downstream CI
├── package.json — @getmcpm/cli, bin: mcpm
├── tsconfig.json
├── tsup.config.ts — bundler config
└── vitest.config.ts — test config with coverage thresholds
| Module | Purpose |
|---|---|
commands/ |
Commander command registration and actions, including the guard and publish subcommand groups |
artifacts/ |
Bounded local USTAR snapshot inventory and skill/plugin declaration comparison; profiles and limits |
server/ |
MCP server (stdio): 9 tools wrapping CLI logic via injectable handlers |
stack/ |
Stack file schemas (mcpm.yaml/mcpm-lock.yaml), semver resolution, trust policy, .env parsing |
guard/ |
v0.5.0 runtime defense. Stdio MITM relay, OWASP MCP Top 10 pattern engine, schema pinning + drift detection, policy file editor, integrity sidecars, event log. Plus guard/confine/ (F1, released v0.16.0, macOS-only, opt-in via --confine): OS process enforcement — the relay optionally wraps the child spawn in an OS sandbox (macOS sandbox-exec) to deny selected secret-path reads and restrict writes/network according to its profile, complementing byte-level detection. Configured secrets are still delivered to the child; network-enabled launchers can exfiltrate accessible data. See confinement limits. |
registry/ |
Typed HTTP client for the official MCP Registry API (v0.1 at registry.modelcontextprotocol.io) |
config/ |
OS-aware config paths, client detection, and per-client config adapters with atomic writes |
scanner/ |
Trust scoring engine: tier 1 (built-in static pattern analysis over registry metadata — secrets, prompt injection, typosquatting, exfil-shaped args), tier 2 (the opt-in external scanner named by MCPM_EXTERNAL_SCANNER, off by default), composite score |
store/ |
Local state in ~/.mcpm/ — installed server registry, server name aliases, the AES-GCM encrypted secret store (secrets.enc.json, master key in the OS keychain), guard pins + policy + confine + events. There is no HTTP cache: every registry read is a live fetch |
utils/ |
Output formatting, confirmation prompts, trust display helpers |
Use the maintained CLI command index for public commands and
mcpm <command> --help for options. mcpm guard run --inner is the internal
relay entry point invoked by wrapped configurations and is semver-exempt.
flowchart TD
Start["user: mcpm install <name>"]
Start --> GetServer["Fetch from Registry<br/>(registry/client.ts)"]
GetServer --> Tier1["Tier-1 Scanner<br/>(scanner/tier1.ts)<br/>Pattern detection:<br/>secrets, injection,<br/>typosquatting,<br/>exfil-args"]
Tier1 --> CheckExt{"External scanner<br/>available?"}
CheckExt -->|No| Trust["Compute Trust Score<br/>(scanner/trust-score.ts)<br/>static + registry meta<br/>health check not run"]
CheckExt -->|Yes| Tier2["Tier-2 Scanner<br/>(scanner/tier2.ts)<br/>Run the installed<br/>MCPM_EXTERNAL_SCANNER"]
Tier2 --> Trust
Trust --> CheckMinTrust{"Score ≥<br/>minTrust?"}
CheckMinTrust -->|No| Fail["Throw Error<br/>min_trust gate"]
Fail --> End1["Exit 1"]
CheckMinTrust -->|Yes| Prompt["Display Trust<br/>Breakdown + Prompt<br/>User Confirm"]
Prompt --> UserReject{"User<br/>accepts?"}
UserReject -->|No| End2["Return<br/>(cancelled)"]
UserReject -->|Yes| Detect["Detect Clients<br/>Claude Desktop,<br/>Claude Code,<br/>Cursor, VS Code,<br/>Windsurf, Gemini CLI"]
Detect --> CheckExisting{"Server already<br/>installed<br/>(unless --force)?"}
CheckExisting -->|Yes| FailExisting["Throw Error<br/>already installed"]
FailExisting --> End3["Exit 1"]
CheckExisting -->|No| PromptEnv["Prompt Env Vars<br/>(required/secret)"]
PromptEnv --> ApplyKeychain["Apply Keychain<br/>Mode"]
ApplyKeychain --> ResolveEntries["Resolve Entries<br/>npm/pypi/oci<br/>or HTTP remote"]
ResolveEntries --> WriteConfig["Write Config<br/>(config/adapters/base.ts)<br/>atomic: O_EXCL,<br/>mode 0o600"]
WriteConfig --> Store["Record in Store<br/>(~/.mcpm/servers.json<br/>with trust score)"]
Store --> Output["Output Result<br/>JSON or human"]
Output --> End4["Success"]
style Fail fill:#ffcccc
style End1 fill:#ffcccc
style FailExisting fill:#ffcccc
style End3 fill:#ffcccc
style Trust fill:#ccffcc
style WriteConfig fill:#ccffee
style Store fill:#ccffee
With --secrets keychain, mcpm encrypts environment values marked secret and writes
placeholders for those values into client configuration. Other environment values
pass through; plaintext mode remains the default. Guard resolves placeholders in
memory before spawning the child. This does not prevent the child from writing or
transmitting the decrypted values, or remove earlier plaintext copies/backups.
Without guard, placeholders remain literal strings.
flowchart TD
A["User runs<br/>mcpm install<br/>with --secrets keychain"] -->|prompts for API key| B["Secret value entered"]
B -->|applyKeychainSecrets| C["Encrypt via AES-GCM<br/>HKDF-derived key from<br/>OS keychain master key<br/>or PBKDF2-derived<br/>machine key fallback"]
C -->|deriveKeychainId<br/>sanitizes + SHA256| D["Store in ~/.mcpm/secrets.enc.json<br/>Key: derived-id/KEY<br/>Value: encrypted ciphertext"]
C -->|create placeholder| E["Client config file<br/>env: API_KEY =<br/>mcpm:keychain:derived-id/KEY"]
E -->|secret-marked values| F["Placeholders replace plaintext<br/>in the new config entry"]
G["User runs<br/>mcpm guard run --inner<br/>wraps server"] -->|at startup| H["resolveEnvPlaceholders<br/>reads declaredEnvKeys"]
H -->|parse & match| I["Find mcpm:keychain:derived-id/KEY<br/>in child env"]
I -->|lookup in store| J["Read + decrypt<br/>from secrets.enc.json<br/>Route on k1: prefix<br/>keychain or machine scheme"]
J -->|inject into memory| K["Child process env<br/>API_KEY = decrypted_value<br/>in-memory only"]
K -->|pass to child| L["startRelay<br/>spawns guarded server<br/>with plaintext in memory"]
L --> M["Child can use decrypted values;<br/>later persistence or egress<br/>is outside secret storage"]
O["Without guard<br/>placeholder remains<br/>literal string"]
E -.->|if guard disabled| O
style D fill:#e1f5ff
style K fill:#c8e6c9
style F fill:#fff9c4
style M fill:#fff9c4
style O fill:#ffccbc
See the canonical guard message flow. Each direction
is inspected independently: block substitutes an error; warn logs and forwards
the original frame. log_only contributes pass while retaining its finding.
Tool pins are first-observation TOFU, not install-time approval.
The relay optionally applies a macOS Seatbelt profile to the spawned server via
guard/confine/. Profile derivation, backend rendering and spawn decisions are
separate modules; the enrollment store and wrapper marker bind the selected
profile. Enrollment alone does not prove a confined launch.
Use the canonical confinement reference for profile permissions, enrollment, marker tokens and hybrid/fail-closed conditions. Configured credentials still reach the child, and network-enabled launchers can send accessible data. The CI section describes the macOS enforcement job.
~/.mcpm/
├── servers.json — installed server registry (name, version, clients, install date)
├── aliases.json — short aliases for server names
├── secrets.enc.json — AES-GCM encrypted credential store (master key in the OS keychain)
├── pins.json — guard schema pins (v0.5.0)
├── pins.json.integrity — sha256 sidecar over pins.json
├── guard-policy.yaml — user overrides (mute/pause)
├── guard-policy.yaml.integrity — sha256 sidecar over guard-policy.yaml
├── guard-confine.yaml — confine enrollment store (F1; source of truth for "server X is confined")
├── guard-confine.yaml.integrity — sha256 sidecar over guard-confine.yaml (fails closed on mismatch)
├── sandbox/<server>/ — per-server confine scratch dir (read+write inside the sandbox)
├── guard-unguarded.json — H9 consent record for un-wrappable (HTTP/SSE) servers the user allowed
└── guard-events.jsonl — append-only event log (parse with jq)
Plus, when mcpm guard enable runs, each touched client config gets a
<config>.guard-{enable,disable}.bak pre-batch backup alongside the per-write
.bak that BaseAdapter.writeAtomic already produces.
| Client | Config path |
|---|---|
| Claude Desktop | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Claude Code | ~/.claude.json |
| Cursor | ~/.cursor/mcp.json |
| VS Code | ~/Library/Application Support/Code/User/mcp.json |
| Windsurf | ~/.codeium/windsurf/mcp_config.json |
| Gemini CLI | ~/.gemini/settings.json |
Linux and Windows paths are also supported. Config key: mcpServers (Claude Desktop, Claude Code, Cursor, Windsurf, Gemini CLI) or servers (VS Code).
Cursor and Gemini park disabled definitions in _mcpmDisabledServers outside
the active map and retain logical inventory for mcpm. Enable restores the managed
user-global definition. This does not stop running peers or override native
approval/exclusion state or independently supplied project definitions. Gemini's
older disabled:true entries need mcpm disable again to move them into parked
storage. See Cursor validation and
Gemini validation for measured scope and limits.
All config writes use atomic file operations (write to .tmp, then fs.rename). Files are written with mode: 0o600 and directories with mode: 0o700 to restrict access.
- Framework: vitest with
@vitest/coverage-v8 - Coverage thresholds: lines 80%, branches 75%
- Test locations:
src/__tests__/(commands, config, output, registry, server, smoke, stack, store) + colocated*.test.ts(registry, scanner, stack, guard —src/guard/__tests__/) - Approach: injectable
fetchImplfor registry tests (no network calls), temp directories for config adapter tests
Run tests:
pnpm test # run all tests
pnpm test:coverage # run with coverage report
pnpm test:watch # watch modeRuns on push to main and pull requests. Matrix: Node 22, 24, 26. All GitHub Actions are SHA-pinned.
Steps: pnpm install --frozen-lockfile → typecheck → build → native harness
self-checks (no VM/accounts) → test:coverage
→ scripts/probe-coverage-thresholds.sh (the coverage threshold must still fail a build)
→ typecheck again against @types/node for that leg's own Node major (the pinned
@types/node tracks the engines floor, so the first typecheck only describes Node 22).
A second job, confine-macos, runs on macos-latest and executes
pnpm dogfood:confine (scripts/dogfood-confine.sh) — a hermetic end-to-end run of the
F1 --confine sandbox against real macOS Seatbelt enforcement under a throwaway $HOME.
The ubuntu matrix structurally cannot exercise sandbox-exec, so this leg is what keeps
the flagship enforcement path CI-verified.
The protocol-sandbox job separately runs the pinned published stdio
compatibility matrix and a fresh packed candidate matrix. Its launcher checks
and synthetic evidence uploads are defined in ci.yml; a baseline pass alone
does not validate candidate code. See protocol validation
and candidate serve validation for coverage.
Runs on v* tag push. Builds, tests, and publishes to npm as @getmcpm/cli with
provenance. A separate registry job then runs mcpm publish --github-oidc to
(re-)list the version on the official MCP registry, authenticating via a minted
GitHub Actions OIDC token — no npm publish and no --registry secret involved. The npm publish itself is ALSO OIDC (npm Trusted Publishing, tokens disallowed on the package since 2026-09-20): the publish job holds no NPM_TOKEN and id-token: write is its only credential. On the
tag-push path that registry job first POLLS npm for up to 5 minutes (30 attempts, 10 s
apart) until npm's read path actually serves the new version, and FAILS CLOSED naming the
workflow_dispatch recovery if it never does — the MCP registry validates the npm
coordinate before listing it, and v0.40.0's first-ever run lost that race.
The workflow also accepts a workflow_dispatch trigger (a version string input) that
runs ONLY the registry job, verifying the given version is already live on npm before
re-listing it — for recovering a registry listing independently of a new npm release
(e.g. when the registry job was skipped by a publish-job failure that happened
after pnpm publish itself had already succeeded).