Skip to content

feat: wire mcpm secrets command + keychain env-placeholder resolver#8

Merged
m1ngshum merged 1 commit into
mainfrom
feat/secrets-command
Jun 1, 2026
Merged

feat: wire mcpm secrets command + keychain env-placeholder resolver#8
m1ngshum merged 1 commit into
mainfrom
feat/secrets-command

Conversation

@m1ngshum

@m1ngshum m1ngshum commented Jun 1, 2026

Copy link
Copy Markdown
Member

Summary

Makes the documented-but-unregistered mcpm secrets command real, and connects the existing AES-GCM keychain (store/keychain.ts — fully built and tested, but until now with zero non-test importers) to the guard launch path.

Today mcpm install writes API keys as plaintext into client config files and prints a chmod 600 warning (install.ts:492-498). This PR lays the foundation to fix that: secrets can be stored encrypted and referenced from a server's env as a mcpm:keychain:server/KEY placeholder, which mcpm guard resolves to the real value at launch — so the plaintext never touches disk.

This is Phase 1: the read side + the CLI. It deliberately ships the resolver before any change to how install/up write config, so we never write a placeholder that nothing can resolve.

Key design point

mcpm only sits in a server's launch path when guard wraps it. The IDE launches mcpm guard run --inner … with the config's env as real environment variables, and run-inner.ts forwards them to the real child. That forward point is the one clean place to swap placeholder → decrypted secret. So encrypted-secret resolution is a property of guarded servers (coherent with guard being the flagship). A guard-independent launch shim is noted as a possible V2.

Changes

  • store/keychain.ts — add resolveEnvPlaceholders(env) (decrypts mcpm:keychain:… values, passes everything else through, throws an actionable error on a missing secret) and listAll() (key names grouped by server — never values). Additive; existing exports untouched.
  • guard/run-inner.ts — resolve placeholders into the wrapped server's child env before startRelay(). Missing secret → [mcpm-guard] SECRET-MISSING on stderr + non-zero exit. (One-line env: swap + guard clause.)
  • commands/secrets.ts (new)set / list / get / rm, built on injected SecretsDeps for testability. get requires --reveal; list/set never print secret values.
  • commands/index.ts — register + barrel-export.

Security properties

  • Decrypted secret values exist only in the wrapped child's in-memory env, never on disk.
  • secrets list / secrets set output is asserted (in tests) to never contain secret values.
  • secrets get refuses to print without an explicit --reveal.
  • On-disk store stays AES-GCM ciphertext (secrets.enc.json, mode 0600).

Test plan

  • pnpm test1073 pass (20 new: resolver, listAll, all four handlers, command registration)
  • pnpm lint (tsc) — clean
  • pnpm build (tsup) — clean
  • pnpm test:coverage80.85% lines / 87.33% branches (above repo thresholds)
  • Binary smoke (isolated HOME): secrets appears in help; list empty-state; get refuses without --reveal; rm --yes runs end-to-end through bundled dist
  • Interactive set masked prompt — same @inquirer password() already used by mcpm install; not driveable in CI without a TTY (handler logic covered by unit tests with injected prompt)

Follow-ups (not in this PR)

  • Phase 2: switch install/up to write placeholders for isSecret env vars, gated on guard being enabled (else keep plaintext + a tip) — no regression.
  • guard disable must re-materialize or warn on placeholder env values, so disabling guard doesn't brick a server that relies on resolution.

Register the documented-but-unregistered `mcpm secrets` command and
connect the existing (built, tested, orphaned) AES-GCM keychain to the
guard launch path, so credentials can live encrypted instead of as
plaintext in client config files.

- store/keychain.ts: add resolveEnvPlaceholders() — decrypts
  `mcpm:keychain:server/KEY` env values, passes everything else through,
  throws an actionable error on a missing secret; add listAll() — key
  names grouped by server, never values.
- guard/run-inner.ts: resolve placeholders into the wrapped server's
  child env before startRelay(). Secrets stay encrypted on disk and
  exist only in-memory at launch; a missing secret writes a
  [mcpm-guard] SECRET-MISSING line to stderr and exits non-zero.
- commands/secrets.ts: new command (set/list/get/rm) with injected deps
  for testability. `get` requires --reveal; list/set never print values.
- commands/index.ts: register + barrel-export.

Read-side (resolver) ships before any write-side change, so no config is
ever written with a placeholder nothing can resolve.

Tests: 20 new (resolver, listAll, handlers, registration). 1073 pass;
coverage 80.85% lines / 87.33% branches. typecheck + build clean.
@m1ngshum m1ngshum merged commit 297180a into main Jun 1, 2026
7 checks passed
@m1ngshum m1ngshum deleted the feat/secrets-command branch June 1, 2026 04:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant