@yawlabs/mcp 0.64.2 → 0.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +100 -0
  2. package/dist/index.js +506 -50
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -373,6 +373,8 @@ Rotate a credential in one place (the dashboard), every machine picks up the new
373
373
  | `YAW_MCP_DISABLE_PERSISTENCE` | No | Set to `1` or `true` to keep learning + pack-history scoped to the current process -- nothing loaded at start, nothing written on shutdown. Intended for ephemeral / shared environments (CI, containers). Default: cross-session persistence enabled at `~/.yaw-mcp/state.json`. |
374
374
  | `YAW_MCP_AUTO_LOAD` | No | Set to `1` or `true` to pre-activate the top recurring pack (from persisted pack-history) on startup -- no LLM round-trip required. Skips silently when history is empty or no pack's namespaces are all installed. Default: off. Requires persistence to be enabled. |
375
375
  | `YAW_MCP_MIN_COMPLIANCE` | No | Minimum compliance grade (`A`, `B`, `C`, `D`, or `F`, case-insensitive) an installed server must report before `mcp_connect_activate` will load it. Ungraded servers always pass (don't punish unknown). `discover()` annotates below-grade servers in place and shows a "Compliance filter active" header when set. Invalid values log a warning and disable the filter. Default: unset (no filter). |
376
+ | `YAW_MCP_VAULT_PASSPHRASE` | No | Passphrase for the local secret vault (`~/.yaw-mcp/secrets.json`). Required for spawn-time `${secret:NAME}` substitution and to avoid the interactive prompt on `yaw-mcp secrets`. Stripped from every spawned upstream server's env. |
377
+ | `YAW_MCP_VAULT_PASSPHRASE_NEW` | No | The NEW passphrase for `yaw-mcp secrets rotate`. When unset, rotate prompts (confirm-twice) on a TTY instead. |
376
378
  | `MCP_CONNECT_TIMEOUT` | No | Connection timeout in ms for upstream servers (default: `15000`) |
377
379
  | `MCP_CONNECT_IDLE_THRESHOLD` | No | Baseline for idle auto-unload (default: `10`). The per-namespace adaptive cap is `[5, 50]` -- bursty namespaces extend past the baseline, long-idle ones unload at it. |
378
380
 
@@ -388,6 +390,104 @@ The popular Python-based MCP servers (`sqlite`, `time`, `sentry`, and other uvx-
388
390
 
389
391
  `uvx ARGS` is always rewritten to `uv tool run ARGS` at spawn time -- so only `uv` needs to be reachable, not `uvx` separately. Fixes Windows setups where one was on PATH and the other wasn't.
390
392
 
393
+ ## Local secret vault (no account required)
394
+
395
+ There are two ways a secret reaches a server yaw-mcp spawns, and they have
396
+ different threat models:
397
+
398
+ 1. **Backend-credential path (account required).** Paste a token into a
399
+ server's `env` block in the yaw.sh/mcp dashboard. The value is encrypted
400
+ at rest on the backend and injected at spawn time. It syncs across every
401
+ machine signed in to the same account, and revoking the account token cuts
402
+ off access everywhere on the next poll. This is the path the
403
+ "Cross-machine config sync" and "Trust & security" sections describe.
404
+
405
+ 2. **Local secret vault (no account required).** Keep the value in an
406
+ encrypted file on your own machine at `~/.yaw-mcp/secrets.json` and
407
+ reference it from any server's `env` with a `${secret:NAME}` placeholder.
408
+ yaw-mcp substitutes the decrypted value into the child env at spawn time.
409
+ The value never leaves your machine, never goes to the backend, and works
410
+ with no login. This is the right path when you want a credential that is
411
+ strictly local to one machine, or you don't have an account at all.
412
+
413
+ ### How `${secret:NAME}` references work
414
+
415
+ Put a placeholder in a server's `env` value -- it can stand alone or be
416
+ composed inline:
417
+
418
+ ```jsonc
419
+ {
420
+ "namespace": "gh",
421
+ "command": "npx",
422
+ "args": ["-y", "@modelcontextprotocol/server-github"],
423
+ "env": {
424
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "${secret:gh}",
425
+ "AUTH_HEADER": "Bearer ${secret:gh}"
426
+ }
427
+ }
428
+ ```
429
+
430
+ At spawn time, if `YAW_MCP_VAULT_PASSPHRASE` is set in yaw-mcp's own
431
+ environment, yaw-mcp loads the vault, decrypts the referenced names, and
432
+ substitutes the values into the child's env. If the passphrase is absent, the
433
+ vault is missing, or a referenced name isn't stored, the literal
434
+ `${secret:NAME}` is passed through unchanged -- the child then surfaces its
435
+ own "missing/invalid credential" error, which is louder than silently passing
436
+ an empty string.
437
+
438
+ ### Managing the vault -- `yaw-mcp secrets`
439
+
440
+ ```bash
441
+ yaw-mcp secrets set <name> # store a value (stdin prompt, no echo; or --value/--stdin)
442
+ yaw-mcp secrets get <name> # decrypt and print one value
443
+ yaw-mcp secrets list # show entry NAMES (values stay encrypted)
444
+ yaw-mcp secrets remove <name> # delete an entry
445
+ yaw-mcp secrets lock # clear the in-process passphrase cache
446
+ yaw-mcp secrets rotate [--push] # re-encrypt the whole vault under a NEW passphrase
447
+ yaw-mcp secrets audit [--secret NAME] [--server NS] [--json] # who consumed which secret, when
448
+ yaw-mcp secrets push # upload the encrypted blob to your account (login required)
449
+ yaw-mcp secrets pull # download it back (login required)
450
+ ```
451
+
452
+ The passphrase derives the encryption key via scrypt and is cached in memory
453
+ for the lifetime of one yaw-mcp process; the on-disk file only ever holds
454
+ ciphertext (AES-256-GCM, per-entry IV + auth tag, one vault-level salt). Set
455
+ `YAW_MCP_VAULT_PASSPHRASE` in the env to avoid the prompt -- this is required
456
+ for spawn-time substitution, since a spawned MCP server runs non-interactively
457
+ and can't prompt on a TTY.
458
+
459
+ `rotate` re-encrypts every entry under a fresh salt + key derived from a NEW
460
+ passphrase (read from `YAW_MCP_VAULT_PASSPHRASE_NEW` or a confirm-twice
461
+ prompt). It decrypts every entry under the current passphrase FIRST and aborts
462
+ the whole operation -- leaving the on-disk vault untouched -- if any entry
463
+ fails to decrypt. **Rotate re-wraps the ENCRYPTION, not the underlying token
464
+ values:** a token that has leaked is still leaked after a rotate; rotate it at
465
+ its source. Rotate does NOT push to your account unless you pass `--push`.
466
+
467
+ `audit` reads an append-only NDJSON log at `~/.yaw-mcp/secrets-audit.log`
468
+ (mode `0600`, tail-capped) recording that a secret NAME was `injected` into
469
+ (or was `missing` for) a given server namespace, with a timestamp. **The log
470
+ never records a value** -- only names, namespaces, and times. Writes to it are
471
+ fail-open: a broken or unwritable log never blocks a server spawn.
472
+
473
+ The `mcp_connect_secrets` meta-tool gives the same picture to the model
474
+ without any decryption: per server it lists `injectedSecrets` (names the vault
475
+ has and the server references) and `missing` (referenced names the vault
476
+ lacks). It reads only the vault's key list and the servers' reference names --
477
+ it never decrypts or returns a value, and needs no passphrase.
478
+
479
+ ### Offline threat model
480
+
481
+ The vault protects the on-disk file against **offline brute-force after
482
+ exfiltration** -- a stolen laptop, a leaked backup, a synced dotfile repo. The
483
+ file is useless without the passphrase: scrypt key derivation makes guessing
484
+ expensive, and AES-256-GCM means a tampered ciphertext fails to decrypt rather
485
+ than yielding garbage plaintext. What the vault does **not** defend against: a
486
+ process running as you while the passphrase is cached in memory (it can ask
487
+ yaw-mcp to decrypt), a keylogger capturing the passphrase, or a value already
488
+ leaked at its source. For those, rotate the underlying token, not the vault
489
+ encryption.
490
+
391
491
  ## Trust & security
392
492
 
393
493
  MCP servers are third-party code that you choose to run, and yaw-mcp launches them on your machine or calls them over the network. We don't sandbox arbitrary code and we're not an antivirus -- that's your OS and network. What yaw-mcp gives you is **visibility and a gate**:
package/dist/index.js CHANGED
@@ -3987,7 +3987,7 @@ async function runUpgrade(opts = {}) {
3987
3987
  return { exitCode: 3, lines };
3988
3988
  }
3989
3989
  function readCurrentVersion() {
3990
- return true ? "0.64.2" : "dev";
3990
+ return true ? "0.65.0" : "dev";
3991
3991
  }
3992
3992
 
3993
3993
  // src/usage-hints.ts
@@ -4049,7 +4049,7 @@ function selectFlakyNamespaces(entries, limit) {
4049
4049
  }
4050
4050
 
4051
4051
  // src/doctor-cmd.ts
4052
- var VERSION = true ? "0.64.2" : "dev";
4052
+ var VERSION = true ? "0.65.0" : "dev";
4053
4053
  function isPersistenceDisabled(env) {
4054
4054
  const raw = env.YAW_MCP_DISABLE_PERSISTENCE;
4055
4055
  return raw !== void 0 && raw !== "" && (raw === "1" || raw.toLowerCase() === "true");
@@ -5663,13 +5663,85 @@ function isFileNotFound2(err) {
5663
5663
  }
5664
5664
 
5665
5665
  // src/secrets-cmd.ts
5666
+ import { existsSync as existsSync6 } from "fs";
5667
+ import { homedir as homedir15 } from "os";
5668
+
5669
+ // src/secrets-audit.ts
5666
5670
  import { existsSync as existsSync5 } from "fs";
5667
- import { homedir as homedir14 } from "os";
5671
+ import { appendFile as appendFile2, chmod as chmod4, readFile as readFile9, writeFile } from "fs/promises";
5672
+ import { homedir as homedir13 } from "os";
5673
+ import { join as join10 } from "path";
5674
+ var SECRETS_AUDIT_FILENAME = "secrets-audit.log";
5675
+ var AUDIT_TAIL_CAP = 5e3;
5676
+ function auditLogPath(home = homedir13()) {
5677
+ return join10(home, CONFIG_DIRNAME, SECRETS_AUDIT_FILENAME);
5678
+ }
5679
+ async function appendAuditEvent(input, home = homedir13()) {
5680
+ try {
5681
+ const event = {
5682
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5683
+ server: input.server,
5684
+ secret: input.secret,
5685
+ event: input.event
5686
+ };
5687
+ const path5 = auditLogPath(home);
5688
+ const line = `${JSON.stringify(event)}
5689
+ `;
5690
+ if (!existsSync5(path5)) {
5691
+ await atomicWriteFile(path5, line);
5692
+ } else {
5693
+ await appendFile2(path5, line, "utf8");
5694
+ }
5695
+ if (process.platform !== "win32") {
5696
+ await chmod4(path5, 384).catch(() => void 0);
5697
+ }
5698
+ await trimToTailCap(path5);
5699
+ } catch {
5700
+ }
5701
+ }
5702
+ async function trimToTailCap(path5) {
5703
+ const raw = await readFile9(path5, "utf8");
5704
+ const lines = raw.split("\n").filter((l) => l.length > 0);
5705
+ if (lines.length <= AUDIT_TAIL_CAP) return;
5706
+ const kept = lines.slice(lines.length - AUDIT_TAIL_CAP);
5707
+ await writeFile(path5, `${kept.join("\n")}
5708
+ `, "utf8");
5709
+ }
5710
+ async function readAuditLog(filter = {}, home = homedir13()) {
5711
+ const path5 = auditLogPath(home);
5712
+ if (!existsSync5(path5)) return [];
5713
+ let raw;
5714
+ try {
5715
+ raw = await readFile9(path5, "utf8");
5716
+ } catch {
5717
+ return [];
5718
+ }
5719
+ const out = [];
5720
+ for (const line of raw.split("\n")) {
5721
+ if (line.length === 0) continue;
5722
+ let parsed;
5723
+ try {
5724
+ parsed = JSON.parse(line);
5725
+ } catch {
5726
+ continue;
5727
+ }
5728
+ if (!isAuditEvent(parsed)) continue;
5729
+ if (filter.secret !== void 0 && parsed.secret !== filter.secret) continue;
5730
+ if (filter.server !== void 0 && parsed.server !== filter.server) continue;
5731
+ out.push(parsed);
5732
+ }
5733
+ return out;
5734
+ }
5735
+ function isAuditEvent(v) {
5736
+ if (!v || typeof v !== "object") return false;
5737
+ const e = v;
5738
+ return typeof e.ts === "string" && typeof e.server === "string" && typeof e.secret === "string" && (e.event === "injected" || e.event === "missing");
5739
+ }
5668
5740
 
5669
5741
  // src/secrets-vault.ts
5670
- import { chmod as chmod4, mkdir as mkdir4, readFile as readFile9 } from "fs/promises";
5671
- import { homedir as homedir13 } from "os";
5672
- import { dirname as dirname2, join as join10 } from "path";
5742
+ import { chmod as chmod5, mkdir as mkdir4, readFile as readFile10 } from "fs/promises";
5743
+ import { homedir as homedir14 } from "os";
5744
+ import { dirname as dirname2, join as join11 } from "path";
5673
5745
 
5674
5746
  // src/secrets-crypto.ts
5675
5747
  import { createCipheriv, createDecipheriv, randomBytes, scrypt as scryptCb } from "crypto";
@@ -5728,8 +5800,8 @@ function decryptEntry(entry, key) {
5728
5800
  var SECRETS_FILENAME = "secrets.json";
5729
5801
  var SECRETS_SCHEMA_VERSION = 1;
5730
5802
  var VAULT_CHECK_PLAINTEXT = "yaw-mcp-vault-v1";
5731
- function vaultPath(home = homedir13()) {
5732
- return join10(home, CONFIG_DIRNAME, SECRETS_FILENAME);
5803
+ function vaultPath(home = homedir14()) {
5804
+ return join11(home, CONFIG_DIRNAME, SECRETS_FILENAME);
5733
5805
  }
5734
5806
  function emptyVault() {
5735
5807
  return {
@@ -5741,7 +5813,7 @@ function emptyVault() {
5741
5813
  async function loadVault(path5) {
5742
5814
  let raw;
5743
5815
  try {
5744
- raw = await readFile9(path5, "utf8");
5816
+ raw = await readFile10(path5, "utf8");
5745
5817
  } catch (err) {
5746
5818
  const code = err.code;
5747
5819
  if (code === "ENOENT") return null;
@@ -5786,7 +5858,7 @@ async function saveVault(path5, vault) {
5786
5858
  await mkdir4(dir, { recursive: true });
5787
5859
  if (process.platform !== "win32") {
5788
5860
  try {
5789
- await chmod4(dir, 448);
5861
+ await chmod5(dir, 448);
5790
5862
  } catch {
5791
5863
  }
5792
5864
  }
@@ -5794,7 +5866,7 @@ async function saveVault(path5, vault) {
5794
5866
  `, "utf8", 384, 448);
5795
5867
  if (process.platform !== "win32") {
5796
5868
  try {
5797
- await chmod4(path5, 384);
5869
+ await chmod5(path5, 384);
5798
5870
  } catch {
5799
5871
  }
5800
5872
  }
@@ -5828,6 +5900,44 @@ function ensureCheck(vault, key) {
5828
5900
  if (vault.check) return vault;
5829
5901
  return { ...vault, check: encryptEntry(VAULT_CHECK_PLAINTEXT, key) };
5830
5902
  }
5903
+ async function rotateVault(vault, oldKey, newPassphrase) {
5904
+ if (vault.check) {
5905
+ try {
5906
+ const probe2 = decryptEntry(vault.check, oldKey);
5907
+ if (probe2 !== VAULT_CHECK_PLAINTEXT) {
5908
+ throw new Error("vault check marker did not match expected plaintext");
5909
+ }
5910
+ } catch {
5911
+ throw new Error("rotate aborted: current passphrase is wrong (vault check failed to decrypt)");
5912
+ }
5913
+ }
5914
+ const plaintext = /* @__PURE__ */ new Map();
5915
+ for (const [name, entry] of Object.entries(vault.entries)) {
5916
+ try {
5917
+ plaintext.set(name, decryptEntry(entry, oldKey));
5918
+ } catch {
5919
+ plaintext.clear();
5920
+ throw new Error(`rotate aborted: entry "${name}" failed to decrypt under the current passphrase`);
5921
+ }
5922
+ }
5923
+ const newSalt = generateSalt();
5924
+ const newKey = await deriveKey(newPassphrase, newSalt);
5925
+ try {
5926
+ const entries = {};
5927
+ for (const [name, value] of plaintext) {
5928
+ entries[name] = encryptEntry(value, newKey);
5929
+ }
5930
+ return {
5931
+ version: SECRETS_SCHEMA_VERSION,
5932
+ salt: newSalt.toString("base64"),
5933
+ entries,
5934
+ check: encryptEntry(VAULT_CHECK_PLAINTEXT, newKey)
5935
+ };
5936
+ } finally {
5937
+ plaintext.clear();
5938
+ newKey.fill(0);
5939
+ }
5940
+ }
5831
5941
  function listKeys(vault) {
5832
5942
  return Object.keys(vault.entries).sort();
5833
5943
  }
@@ -5919,6 +6029,19 @@ Actions:
5919
6029
  \`yaw-mcp login\` first. Refuses when the local
5920
6030
  vault has a different salt (different passphrase
5921
6031
  lineage) unless --force is passed.
6032
+ rotate Re-encrypt every entry under a NEW passphrase
6033
+ (fresh salt + derived key). Re-wraps the
6034
+ ENCRYPTION, NOT the underlying token values -- a
6035
+ leaked token is still leaked; rotate it at its
6036
+ source. Reads the current passphrase, then the
6037
+ new one (env YAW_MCP_VAULT_PASSPHRASE_NEW or a
6038
+ confirm-twice TTY prompt). Pass --push to also
6039
+ upload the re-encrypted blob to mcp_secrets.
6040
+ audit [--secret NAME] [--server NS]
6041
+ Show the local secret-resolution audit trail
6042
+ (~/.yaw-mcp/secrets-audit.log): which secret
6043
+ NAMES were injected into (or missing for) which
6044
+ server, and when. Never shows a value.
5922
6045
 
5923
6046
  Flags:
5924
6047
  --json Machine-readable output (where applicable).
@@ -5930,12 +6053,18 @@ Flags:
5930
6053
  --replace (push only) Overwrite even when the remote vault
5931
6054
  salt differs from the local (different passphrase
5932
6055
  lineage). Coordinate with your team first.
6056
+ --push (rotate only) After re-encrypting, push the new
6057
+ blob to mcp_secrets (requires a login session).
6058
+ --secret <name> (audit only) Filter to one secret name.
6059
+ --server <ns> (audit only) Filter to one server namespace.
5933
6060
 
5934
6061
  Passphrase:
5935
6062
  Set YAW_MCP_VAULT_PASSPHRASE in the env, or you will be prompted on
5936
6063
  the controlling TTY. The passphrase derives the encryption key via
5937
6064
  scrypt and is cached in memory for the lifetime of this yaw-mcp
5938
- process; the on-disk vault only ever holds ciphertext.`;
6065
+ process; the on-disk vault only ever holds ciphertext. For rotate, the
6066
+ NEW passphrase comes from YAW_MCP_VAULT_PASSPHRASE_NEW (or a TTY
6067
+ confirm-twice prompt).`;
5939
6068
  function parseSecretsArgs(argv) {
5940
6069
  const opts = {};
5941
6070
  for (let i = 0; i < argv.length; i++) {
@@ -5957,6 +6086,10 @@ function parseSecretsArgs(argv) {
5957
6086
  opts.replace = true;
5958
6087
  continue;
5959
6088
  }
6089
+ if (a === "--push") {
6090
+ opts.push = true;
6091
+ continue;
6092
+ }
5960
6093
  if (a === "--value") {
5961
6094
  const v = argv[++i];
5962
6095
  if (v === void 0 || v.startsWith("-")) {
@@ -5970,13 +6103,31 @@ ${SECRETS_USAGE}`
5970
6103
  opts.value = v;
5971
6104
  continue;
5972
6105
  }
6106
+ if (a === "--secret") {
6107
+ const v = argv[++i];
6108
+ if (v === void 0)
6109
+ return { ok: false, error: `yaw-mcp secrets: --secret requires a value
6110
+
6111
+ ${SECRETS_USAGE}` };
6112
+ opts.secretFilter = v;
6113
+ continue;
6114
+ }
6115
+ if (a === "--server") {
6116
+ const v = argv[++i];
6117
+ if (v === void 0)
6118
+ return { ok: false, error: `yaw-mcp secrets: --server requires a value
6119
+
6120
+ ${SECRETS_USAGE}` };
6121
+ opts.serverFilter = v;
6122
+ continue;
6123
+ }
5973
6124
  if (a.startsWith("-")) {
5974
6125
  return { ok: false, error: `yaw-mcp secrets: unknown flag "${a}"
5975
6126
 
5976
6127
  ${SECRETS_USAGE}` };
5977
6128
  }
5978
6129
  if (!opts.action) {
5979
- if (a !== "set" && a !== "get" && a !== "list" && a !== "remove" && a !== "lock" && a !== "push" && a !== "pull") {
6130
+ if (a !== "set" && a !== "get" && a !== "list" && a !== "remove" && a !== "lock" && a !== "push" && a !== "pull" && a !== "rotate" && a !== "audit") {
5980
6131
  return { ok: false, error: `yaw-mcp secrets: unknown action "${a}"
5981
6132
 
5982
6133
  ${SECRETS_USAGE}` };
@@ -6040,6 +6191,35 @@ async function resolvePassphrase(opts) {
6040
6191
  }
6041
6192
  return null;
6042
6193
  }
6194
+ async function resolveNewPassphrase(opts) {
6195
+ if (opts.newPassphrase !== void 0) return opts.newPassphrase.length > 0 ? opts.newPassphrase : null;
6196
+ const fromEnv = process.env.YAW_MCP_VAULT_PASSPHRASE_NEW;
6197
+ if (typeof fromEnv === "string" && fromEnv.length > 0) {
6198
+ if (fromEnv.length < MIN_PASSPHRASE_WARN_LEN) {
6199
+ const stderr = opts.io?.stderr ?? process.stderr;
6200
+ stderr.write(
6201
+ `yaw-mcp secrets: warning -- the new passphrase is shorter than ${MIN_PASSPHRASE_WARN_LEN} characters; consider a longer passphrase.
6202
+ `
6203
+ );
6204
+ }
6205
+ return fromEnv;
6206
+ }
6207
+ const stdin = opts.io?.stdin ?? process.stdin;
6208
+ const stdout = opts.io?.stdout ?? process.stdout;
6209
+ const isTTY = stdin.isTTY === true && stdout.isTTY === true;
6210
+ if (!isTTY) return null;
6211
+ for (let attempt = 0; attempt < MAX_PASSPHRASE_PROMPTS; attempt++) {
6212
+ const first = await readPassphraseFromTTY(stdin, stdout, "New vault passphrase: ");
6213
+ if (first.length === 0) {
6214
+ stdout.write("Passphrase cannot be empty.\n");
6215
+ continue;
6216
+ }
6217
+ const second = await readPassphraseFromTTY(stdin, stdout, "Confirm new passphrase: ");
6218
+ if (first === second) return first;
6219
+ stdout.write("Passphrases did not match. Try again.\n");
6220
+ }
6221
+ return null;
6222
+ }
6043
6223
  var MAX_PASSPHRASE_PROMPTS = 3;
6044
6224
  var MIN_PASSPHRASE_WARN_LEN = 12;
6045
6225
  function readPassphraseFromTTY(stdin, stdout, prompt = "Vault passphrase: ") {
@@ -6108,7 +6288,7 @@ async function runSecrets(opts, io = {
6108
6288
  out: (s) => process.stdout.write(s),
6109
6289
  err: (s) => process.stderr.write(s)
6110
6290
  }) {
6111
- const home = opts.home ?? homedir14();
6291
+ const home = opts.home ?? homedir15();
6112
6292
  const path5 = vaultPath(home);
6113
6293
  if (opts.action === "lock") {
6114
6294
  lock();
@@ -6123,12 +6303,18 @@ async function runSecrets(opts, io = {
6123
6303
  if (opts.action === "pull") {
6124
6304
  return await runSecretsPull(opts, io);
6125
6305
  }
6306
+ if (opts.action === "rotate") {
6307
+ return await runSecretsRotate(opts, io);
6308
+ }
6309
+ if (opts.action === "audit") {
6310
+ return await runSecretsAudit(opts, io);
6311
+ }
6126
6312
  if (opts.action === "list") {
6127
6313
  const loaded = await safeLoadVault(path5, io, opts.json, "list");
6128
6314
  if (!loaded.ok) return loaded.result;
6129
6315
  const vault2 = loaded.vault;
6130
6316
  const keys = vault2 ? listKeys(vault2) : [];
6131
- if (opts.json) io.out(`${JSON.stringify({ ok: true, vault: existsSync5(path5), keys }, null, 2)}
6317
+ if (opts.json) io.out(`${JSON.stringify({ ok: true, vault: existsSync6(path5), keys }, null, 2)}
6132
6318
  `);
6133
6319
  else if (!vault2) io.out(`No vault at ${path5}. Run \`yaw-mcp secrets set <name>\` to create one.
6134
6320
  `);
@@ -6159,7 +6345,7 @@ async function runSecrets(opts, io = {
6159
6345
  const loadedForMutate = await safeLoadVault(path5, io, opts.json, opts.action ?? "");
6160
6346
  if (!loadedForMutate.ok) return loadedForMutate.result;
6161
6347
  let vault = loadedForMutate.vault ?? newVault();
6162
- const isFresh = !existsSync5(path5);
6348
+ const isFresh = !existsSync6(path5);
6163
6349
  const passphrase = await resolvePassphrase(opts);
6164
6350
  if (passphrase === null) {
6165
6351
  const msg = "Passphrase required. Set YAW_MCP_VAULT_PASSPHRASE or run from a TTY so we can prompt.";
@@ -6261,7 +6447,7 @@ async function runSecrets(opts, io = {
6261
6447
  }
6262
6448
  var MCP_SECRETS_RESOURCE = "mcp_secrets";
6263
6449
  async function runSecretsPush(opts, io) {
6264
- const home = opts.home ?? homedir14();
6450
+ const home = opts.home ?? homedir15();
6265
6451
  const path5 = vaultPath(home);
6266
6452
  const session = await getSession({ home, baseUrl: opts.baseUrl });
6267
6453
  if (!session) {
@@ -6335,7 +6521,7 @@ async function runSecretsPush(opts, io) {
6335
6521
  }
6336
6522
  }
6337
6523
  async function runSecretsPull(opts, io) {
6338
- const home = opts.home ?? homedir14();
6524
+ const home = opts.home ?? homedir15();
6339
6525
  const path5 = vaultPath(home);
6340
6526
  const session = await getSession({ home, baseUrl: opts.baseUrl });
6341
6527
  if (!session) {
@@ -6403,10 +6589,159 @@ async function runSecretsPull(opts, io) {
6403
6589
  return { exitCode: 1 };
6404
6590
  }
6405
6591
  }
6592
+ async function runSecretsRotate(opts, io) {
6593
+ const home = opts.home ?? homedir15();
6594
+ const path5 = vaultPath(home);
6595
+ const vault = await loadVault(path5);
6596
+ if (!vault) {
6597
+ const msg = `No vault at ${path5} to rotate. Run \`yaw-mcp secrets set <name>\` first.`;
6598
+ if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
6599
+ `);
6600
+ else io.err(`yaw-mcp secrets rotate: ${msg}
6601
+ `);
6602
+ return { exitCode: 1 };
6603
+ }
6604
+ const currentPassphrase = await resolvePassphrase(opts);
6605
+ if (currentPassphrase === null) {
6606
+ const msg = "Current passphrase required. Set YAW_MCP_VAULT_PASSPHRASE or run from a TTY so we can prompt.";
6607
+ if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
6608
+ `);
6609
+ else io.err(`yaw-mcp secrets rotate: ${msg}
6610
+ `);
6611
+ return { exitCode: 1 };
6612
+ }
6613
+ let oldKey;
6614
+ try {
6615
+ oldKey = await unlock(vault, currentPassphrase);
6616
+ } catch (err) {
6617
+ const msg = err instanceof Error ? err.message : String(err);
6618
+ if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
6619
+ `);
6620
+ else io.err(`yaw-mcp secrets rotate: ${msg}
6621
+ `);
6622
+ return { exitCode: 1 };
6623
+ }
6624
+ const newPassphrase = await resolveNewPassphrase(opts);
6625
+ if (newPassphrase === null) {
6626
+ const msg = "New passphrase required (and must be confirmed). Set YAW_MCP_VAULT_PASSPHRASE_NEW or run from a TTY so we can prompt.";
6627
+ if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
6628
+ `);
6629
+ else io.err(`yaw-mcp secrets rotate: ${msg}
6630
+ `);
6631
+ return { exitCode: 1 };
6632
+ }
6633
+ let rotated;
6634
+ try {
6635
+ rotated = await rotateVault(vault, oldKey, newPassphrase);
6636
+ } catch (err) {
6637
+ const msg = err instanceof Error ? err.message : String(err);
6638
+ if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
6639
+ `);
6640
+ else io.err(`yaw-mcp secrets rotate: ${msg}
6641
+ `);
6642
+ lock();
6643
+ return { exitCode: 1 };
6644
+ }
6645
+ await saveVault(path5, rotated);
6646
+ lock();
6647
+ const count = Object.keys(rotated.entries).length;
6648
+ let pushedVersion = null;
6649
+ if (opts.push) {
6650
+ const session = await getSession({ home, baseUrl: opts.baseUrl });
6651
+ if (!session) {
6652
+ const msg = "Rotated locally, but --push needs a session. Run `yaw-mcp login --key <license-key>` then push.";
6653
+ if (opts.json) io.err(`${JSON.stringify({ ok: true, rotated: true, pushed: false, note: msg })}
6654
+ `);
6655
+ else io.err(`yaw-mcp secrets rotate: ${msg}
6656
+ `);
6657
+ return { exitCode: 0 };
6658
+ }
6659
+ try {
6660
+ const remote = await getResource(MCP_SECRETS_RESOURCE, { home, baseUrl: opts.baseUrl });
6661
+ const result = await putResource(MCP_SECRETS_RESOURCE, remote.version, rotated, {
6662
+ home,
6663
+ baseUrl: opts.baseUrl
6664
+ });
6665
+ pushedVersion = result.version;
6666
+ } catch (err) {
6667
+ if (err instanceof TeamSyncStaleVersionError) {
6668
+ const hint = `Rotated locally. Push skipped -- remote is at v${err.currentVersion}; pull and reconcile, then push.`;
6669
+ if (opts.json) io.err(`${JSON.stringify({ ok: true, rotated: true, pushed: false, note: hint })}
6670
+ `);
6671
+ else io.err(`yaw-mcp secrets rotate: ${hint}
6672
+ `);
6673
+ return { exitCode: 0 };
6674
+ }
6675
+ if (err instanceof TeamSyncAuthError) {
6676
+ const hint = "Rotated locally. Push skipped -- session expired. Run `yaw-mcp login` again, then push.";
6677
+ if (opts.json) io.err(`${JSON.stringify({ ok: true, rotated: true, pushed: false, note: hint })}
6678
+ `);
6679
+ else io.err(`yaw-mcp secrets rotate: ${hint}
6680
+ `);
6681
+ return { exitCode: 0 };
6682
+ }
6683
+ const message = err instanceof Error ? err.message : String(err);
6684
+ if (opts.json) io.err(`${JSON.stringify({ ok: true, rotated: true, pushed: false, error: message })}
6685
+ `);
6686
+ else io.err(`yaw-mcp secrets rotate: rotated locally but push failed: ${message}
6687
+ `);
6688
+ return { exitCode: 0 };
6689
+ }
6690
+ }
6691
+ if (opts.json) {
6692
+ io.out(
6693
+ `${JSON.stringify({ ok: true, rotated: true, secret_count: count, pushed: pushedVersion !== null, ...pushedVersion !== null ? { new_version: pushedVersion } : {} })}
6694
+ `
6695
+ );
6696
+ } else {
6697
+ io.out(
6698
+ `Rotated ${count} secret${count === 1 ? "" : "s"} under a new passphrase (encryption re-wrapped, token values unchanged).
6699
+ `
6700
+ );
6701
+ if (pushedVersion !== null) io.out(`Pushed the re-encrypted vault -> mcp_secrets v${pushedVersion}.
6702
+ `);
6703
+ io.out("Vault locked -- the next secrets command will prompt for the new passphrase.\n");
6704
+ }
6705
+ return { exitCode: 0 };
6706
+ }
6707
+ async function runSecretsAudit(opts, io) {
6708
+ const home = opts.home ?? homedir15();
6709
+ let events;
6710
+ try {
6711
+ events = await readAuditLog(
6712
+ {
6713
+ ...opts.secretFilter !== void 0 ? { secret: opts.secretFilter } : {},
6714
+ ...opts.serverFilter !== void 0 ? { server: opts.serverFilter } : {}
6715
+ },
6716
+ home
6717
+ );
6718
+ } catch (err) {
6719
+ const msg = err instanceof Error ? err.message : String(err);
6720
+ if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
6721
+ `);
6722
+ else io.err(`yaw-mcp secrets audit: ${msg}
6723
+ `);
6724
+ return { exitCode: 1 };
6725
+ }
6726
+ if (opts.json) {
6727
+ io.out(`${JSON.stringify({ ok: true, count: events.length, events }, null, 2)}
6728
+ `);
6729
+ return { exitCode: 0 };
6730
+ }
6731
+ if (events.length === 0) {
6732
+ io.out("No secret-resolution audit events recorded yet.\n");
6733
+ return { exitCode: 0 };
6734
+ }
6735
+ for (const e of events) {
6736
+ io.out(`${e.ts} ${e.event === "injected" ? "injected" : "missing "} ${e.server} ${e.secret}
6737
+ `);
6738
+ }
6739
+ return { exitCode: 0 };
6740
+ }
6406
6741
 
6407
6742
  // src/server.ts
6408
- import { readFile as readFile11 } from "fs/promises";
6409
- import { homedir as homedir15 } from "os";
6743
+ import { readFile as readFile12 } from "fs/promises";
6744
+ import { homedir as homedir16 } from "os";
6410
6745
  import { isAbsolute as isAbsolute2, relative, resolve as resolve6 } from "path";
6411
6746
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6412
6747
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -6522,7 +6857,7 @@ function defaultSpawn2(cmd, args) {
6522
6857
  async function maybeAutoUpgrade(deps = {}) {
6523
6858
  const optOut = process.env.YAW_MCP_AUTO_UPGRADE;
6524
6859
  if (optOut === "0" || optOut?.toLowerCase() === "false") return;
6525
- const current = deps.currentVersion ?? (true ? "0.64.2" : "dev");
6860
+ const current = deps.currentVersion ?? (true ? "0.65.0" : "dev");
6526
6861
  if (current === "dev") return;
6527
6862
  const method = (deps.isSeaImpl ? await deps.isSeaImpl() : await detectSea()) ? "binary" : detectInstallMethod(deps.argvPath ?? process.argv[1]);
6528
6863
  const latest = await (deps.fetchLatestImpl ?? fetchLatestVersion2)();
@@ -7028,14 +7363,14 @@ function closestNames(query, candidates, limit) {
7028
7363
  }
7029
7364
 
7030
7365
  // src/guide.ts
7031
- import { readFile as readFile10 } from "fs/promises";
7366
+ import { readFile as readFile11 } from "fs/promises";
7032
7367
  var GUIDE_READ_TIMEOUT_MS = 1e3;
7033
7368
  async function readGuide(path5, scope) {
7034
7369
  let raw;
7035
7370
  const ac = new AbortController();
7036
7371
  const timer = setTimeout(() => ac.abort(new Error("guide read timeout")), GUIDE_READ_TIMEOUT_MS);
7037
7372
  try {
7038
- raw = await readFile10(path5, { encoding: "utf8", signal: ac.signal });
7373
+ raw = await readFile11(path5, { encoding: "utf8", signal: ac.signal });
7039
7374
  } catch (err) {
7040
7375
  const isTimeout = err instanceof Error && err.code === "ABORT_ERR";
7041
7376
  if (isTimeout) {
@@ -7647,6 +7982,26 @@ var META_TOOLS = {
7647
7982
  openWorldHint: false
7648
7983
  }
7649
7984
  },
7985
+ secrets: {
7986
+ name: "mcp_connect_secrets",
7987
+ description: "List, per installed server, which local-vault secrets its `${secret:NAME}` env references resolve to -- by NAME only, never a value. Use this to confirm a server will get the credentials it needs before activating it, or to spot a typo'd / un-set secret reference. `injectedSecrets` are the names the local vault HAS and the server references; `missing` are names the server references but the vault LACKS (set them via `yaw-mcp secrets set <name>`). This is a values-free preview: it reads the vault's KEY LIST and the server's env-reference NAMES, and never decrypts or returns any secret value. Servers with no `${secret:...}` references are omitted. Requires no passphrase (no decryption happens).",
7988
+ inputSchema: {
7989
+ type: "object",
7990
+ properties: {
7991
+ server: {
7992
+ type: "string",
7993
+ description: 'Optional: restrict the report to a single server namespace (e.g. "gh"). Omit to report every installed server that references a vault secret.'
7994
+ }
7995
+ }
7996
+ },
7997
+ annotations: {
7998
+ title: "Inspect Vault Secret Resolution",
7999
+ readOnlyHint: true,
8000
+ destructiveHint: false,
8001
+ idempotentHint: true,
8002
+ openWorldHint: false
8003
+ }
8004
+ },
7650
8005
  exec: {
7651
8006
  name: "mcp_connect_exec",
7652
8007
  description: "Run a short DECLARATIVE pipeline of upstream tool calls in a single round-trip. Use this when you already know the exact 2-4 tool calls to make and one call's output feeds another's args \u2014 e.g. `a = gh_list_prs(); b = gh_get_pr(a[0].number); return b`. NOT a code sandbox: there is no expression language, no loops, no branching, no arithmetic. The only control flow is sequential step execution; the only data-flow primitive is `{\"$ref\": \"<stepId>[.path.to.value]\"}` which substitutes a prior step's output (or a nested field of it) into the next step's args. Paths support dot keys and `[N]` / `.N` array indexing. Each step's `tool` must be a namespaced, already-loaded tool name (the exec does not auto-activate \u2014 call `mcp_connect_activate` first). Max 16 steps per exec. If any step fails, the whole pipeline fails and returns `{ ok: false, failedStep, error, partial: { ...completed outputs } }`. On success returns `{ ok: true, result: <return-step output>, steps: { ...all outputs } }`. Prefer this over back-to-back tool calls when the chain is deterministic \u2014 it saves prompt-token replay and client round-trips.",
@@ -7764,6 +8119,30 @@ function buildInstallPayload(args) {
7764
8119
  }
7765
8120
  return { ok: true, payload };
7766
8121
  }
8122
+ var SECRETS_REPORT_REF_RE = /\$\{secret:([a-zA-Z0-9_.-]+)\}/g;
8123
+ function computeSecretsReport(servers, vaultKeys) {
8124
+ const rows = [];
8125
+ for (const server of servers) {
8126
+ const referenced = /* @__PURE__ */ new Set();
8127
+ for (const v of Object.values(server.env ?? {})) {
8128
+ if (typeof v !== "string") continue;
8129
+ for (const m of v.matchAll(SECRETS_REPORT_REF_RE)) referenced.add(m[1]);
8130
+ }
8131
+ if (referenced.size === 0) continue;
8132
+ const injectedSecrets = [];
8133
+ const missing = [];
8134
+ for (const name of referenced) {
8135
+ if (vaultKeys.has(name)) injectedSecrets.push(name);
8136
+ else missing.push(name);
8137
+ }
8138
+ rows.push({
8139
+ server: server.namespace,
8140
+ injectedSecrets: injectedSecrets.sort(),
8141
+ missing: missing.sort()
8142
+ });
8143
+ }
8144
+ return rows;
8145
+ }
7767
8146
  var META_TOOL_NAMES = /* @__PURE__ */ new Set([
7768
8147
  META_TOOLS.discover.name,
7769
8148
  META_TOOLS.activate.name,
@@ -7775,7 +8154,8 @@ var META_TOOL_NAMES = /* @__PURE__ */ new Set([
7775
8154
  META_TOOLS.read_tool.name,
7776
8155
  META_TOOLS.suggest.name,
7777
8156
  META_TOOLS.exec.name,
7778
- META_TOOLS.bundles.name
8157
+ META_TOOLS.bundles.name,
8158
+ META_TOOLS.secrets.name
7779
8159
  ]);
7780
8160
 
7781
8161
  // src/pack-detect.ts
@@ -9213,7 +9593,7 @@ async function resolveUvSpawn(command, args) {
9213
9593
  }
9214
9594
 
9215
9595
  // src/upstream.ts
9216
- async function resolveServerEnv(env) {
9596
+ async function resolveServerEnv(env, namespace) {
9217
9597
  if (!hasSecretRefs(env)) return env;
9218
9598
  const refKeys = Object.entries(env).filter(([, v]) => typeof v === "string" && v.includes("${secret:")).map(([k]) => k);
9219
9599
  const passphrase = process.env.YAW_MCP_VAULT_PASSPHRASE;
@@ -9235,8 +9615,36 @@ async function resolveServerEnv(env) {
9235
9615
  if (missing.length > 0) {
9236
9616
  throw new Error(`vault: missing or undecryptable secret refs: ${missing.join(", ")}`);
9237
9617
  }
9618
+ try {
9619
+ await recordResolveAudit(namespace, env, missing);
9620
+ } catch (auditErr) {
9621
+ log("warn", "Failed to record secret-resolve audit (non-fatal)", {
9622
+ namespace,
9623
+ error: auditErr instanceof Error ? auditErr.message : String(auditErr)
9624
+ });
9625
+ }
9238
9626
  return resolved;
9239
9627
  }
9628
+ async function recordResolveAudit(namespace, env, missing) {
9629
+ const missingSet = new Set(missing);
9630
+ const referenced = collectSecretNames(env);
9631
+ for (const name of referenced) {
9632
+ if (missingSet.has(name)) continue;
9633
+ await appendAuditEvent({ server: namespace, secret: name, event: "injected" });
9634
+ }
9635
+ for (const name of missingSet) {
9636
+ await appendAuditEvent({ server: namespace, secret: name, event: "missing" });
9637
+ }
9638
+ }
9639
+ function collectSecretNames(env) {
9640
+ const names = /* @__PURE__ */ new Set();
9641
+ const re = /\$\{secret:([a-zA-Z0-9_.-]+)\}/g;
9642
+ for (const v of Object.values(env)) {
9643
+ if (typeof v !== "string") continue;
9644
+ for (const m of v.matchAll(re)) names.add(m[1]);
9645
+ }
9646
+ return [...names];
9647
+ }
9240
9648
  var DEFAULT_CONNECT_TIMEOUT = (() => {
9241
9649
  const env = process.env.MCP_CONNECT_TIMEOUT;
9242
9650
  if (!env) return 15e3;
@@ -9284,7 +9692,7 @@ function categorizeSpawnError(err) {
9284
9692
  }
9285
9693
  async function connectToUpstream(config, onDisconnect, onListChanged) {
9286
9694
  const client = new Client(
9287
- { name: "yaw-mcp", version: true ? "0.64.2" : "dev" },
9695
+ { name: "yaw-mcp", version: true ? "0.65.0" : "dev" },
9288
9696
  { capabilities: {} }
9289
9697
  );
9290
9698
  let transport;
@@ -9300,7 +9708,7 @@ async function connectToUpstream(config, onDisconnect, onListChanged) {
9300
9708
  ...parentEnv
9301
9709
  } = process.env;
9302
9710
  const resolved = await resolveUvSpawn(config.command, config.args ?? []);
9303
- const serverEnv = await resolveServerEnv(config.env ?? {});
9711
+ const serverEnv = await resolveServerEnv(config.env ?? {}, config.namespace);
9304
9712
  resolvedServerEnv = serverEnv;
9305
9713
  const stdioTransport = new StdioClientTransport({
9306
9714
  command: resolved.command,
@@ -9616,7 +10024,7 @@ var ConnectServer = class _ConnectServer {
9616
10024
  this.apiUrl = apiUrl5;
9617
10025
  this.token = token5;
9618
10026
  this.server = new Server(
9619
- { name: "yaw-mcp", version: true ? "0.64.2" : "dev" },
10027
+ { name: "yaw-mcp", version: true ? "0.65.0" : "dev" },
9620
10028
  {
9621
10029
  capabilities: {
9622
10030
  tools: { listChanged: true },
@@ -10198,6 +10606,17 @@ var ConnectServer = class _ConnectServer {
10198
10606
  recordConnectEvent({ namespace: null, toolName: null, action: "bundles", latencyMs: null, success: true });
10199
10607
  return this.attachGuideNudge(this.handleBundles(action));
10200
10608
  }
10609
+ if (name === META_TOOLS.secrets.name) {
10610
+ const serverArg = typeof args.server === "string" ? args.server : void 0;
10611
+ recordConnectEvent({
10612
+ namespace: serverArg ?? null,
10613
+ toolName: null,
10614
+ action: "secrets",
10615
+ latencyMs: null,
10616
+ success: true
10617
+ });
10618
+ return this.attachGuideNudge(await this.handleSecretsReport(serverArg));
10619
+ }
10201
10620
  let routes = this.toolRoutes;
10202
10621
  let route = routes.get(name);
10203
10622
  if (route?.deferred) {
@@ -11293,7 +11712,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
11293
11712
  }
11294
11713
  const ALLOWED_FILENAMES = ["claude_desktop_config.json", "mcp.json", "settings.json", "mcp_config.json"];
11295
11714
  try {
11296
- const resolved = filepath.startsWith("~/") || filepath.startsWith("~\\") ? resolve6(homedir15(), filepath.slice(2)) : resolve6(filepath);
11715
+ const resolved = filepath.startsWith("~/") || filepath.startsWith("~\\") ? resolve6(homedir16(), filepath.slice(2)) : resolve6(filepath);
11297
11716
  const resolvedBasename = resolved.split(/[/\\]/).pop() || "";
11298
11717
  if (!ALLOWED_FILENAMES.includes(resolvedBasename)) {
11299
11718
  return {
@@ -11310,7 +11729,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
11310
11729
  const rel = relative(base, p);
11311
11730
  return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
11312
11731
  };
11313
- if (!isUnder(homedir15(), resolved) && !isUnder(process.cwd(), resolved)) {
11732
+ if (!isUnder(homedir16(), resolved) && !isUnder(process.cwd(), resolved)) {
11314
11733
  return {
11315
11734
  content: [
11316
11735
  { type: "text", text: "Import path must be under your home directory or the current working directory." }
@@ -11318,7 +11737,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
11318
11737
  isError: true
11319
11738
  };
11320
11739
  }
11321
- const raw = await readFile11(resolved, "utf-8");
11740
+ const raw = await readFile12(resolved, "utf-8");
11322
11741
  const parsed = JSON.parse(raw);
11323
11742
  if (!parsed.mcpServers || typeof parsed.mcpServers !== "object" || Array.isArray(parsed.mcpServers)) {
11324
11743
  return {
@@ -11610,6 +12029,43 @@ Use mcp_connect_discover to see imported servers.`
11610
12029
  );
11611
12030
  }
11612
12031
  }
12032
+ // Values-free preview of which local-vault secrets each installed
12033
+ // server's `${secret:NAME}` env refs resolve to. NAMES ONLY -- this
12034
+ // reads the vault's KEY LIST (listKeys, no unlock, no passphrase) and
12035
+ // the servers' env-reference names, and NEVER calls getSecret /
12036
+ // decryptEntry. Servers with no refs are omitted.
12037
+ async handleSecretsReport(serverArg) {
12038
+ const vault = await loadVault(vaultPath()).catch(() => null);
12039
+ const vaultKeys = new Set(vault ? listKeys(vault) : []);
12040
+ let servers = this.getProfiledActiveServers().map((s) => ({ namespace: s.namespace, env: s.env }));
12041
+ if (serverArg) servers = servers.filter((s) => s.namespace === serverArg);
12042
+ const rows = computeSecretsReport(servers, vaultKeys);
12043
+ if (serverArg && servers.length === 0) {
12044
+ return {
12045
+ content: [
12046
+ {
12047
+ type: "text",
12048
+ text: `No installed server with namespace "${serverArg}". Call mcp_connect_discover to list installed servers.`
12049
+ }
12050
+ ],
12051
+ isError: true
12052
+ };
12053
+ }
12054
+ if (rows.length === 0) {
12055
+ const scope = serverArg ? `Server "${serverArg}"` : "No installed server";
12056
+ return {
12057
+ content: [
12058
+ {
12059
+ type: "text",
12060
+ text: `${scope} references any \${secret:NAME} vault values. Add a reference in a server's env (e.g. GITHUB_TOKEN=\${secret:gh}) and store the value with \`yaw-mcp secrets set <name>\`.`
12061
+ }
12062
+ ]
12063
+ };
12064
+ }
12065
+ return {
12066
+ content: [{ type: "text", text: JSON.stringify(rows, null, 2) }]
12067
+ };
12068
+ }
11613
12069
  handleHealth() {
11614
12070
  const lines = [];
11615
12071
  if (this.profile) {
@@ -12122,21 +12578,21 @@ function truncateVersion(v) {
12122
12578
  }
12123
12579
 
12124
12580
  // src/set-active-cmd.ts
12125
- import { homedir as homedir16 } from "os";
12581
+ import { homedir as homedir17 } from "os";
12126
12582
 
12127
12583
  // src/sync-state.ts
12128
- import { existsSync as existsSync6 } from "fs";
12129
- import { mkdir as mkdir5, readFile as readFile12 } from "fs/promises";
12130
- import { dirname as dirname4, join as join11 } from "path";
12584
+ import { existsSync as existsSync7 } from "fs";
12585
+ import { mkdir as mkdir5, readFile as readFile13 } from "fs/promises";
12586
+ import { dirname as dirname4, join as join12 } from "path";
12131
12587
  var SYNC_STATE_FILENAME = "sync-state.json";
12132
12588
  function syncStatePath(home) {
12133
- return join11(home, CONFIG_DIRNAME, SYNC_STATE_FILENAME);
12589
+ return join12(home, CONFIG_DIRNAME, SYNC_STATE_FILENAME);
12134
12590
  }
12135
12591
  async function readSyncState(home) {
12136
12592
  const path5 = syncStatePath(home);
12137
- if (!existsSync6(path5)) return {};
12593
+ if (!existsSync7(path5)) return {};
12138
12594
  try {
12139
- const raw = await readFile12(path5, "utf8");
12595
+ const raw = await readFile13(path5, "utf8");
12140
12596
  const parsed = JSON.parse(raw);
12141
12597
  if (!parsed || typeof parsed !== "object") return {};
12142
12598
  return parsed;
@@ -12240,7 +12696,7 @@ async function runSetActive(opts, io = { out: (s) => process.stdout.write(s), er
12240
12696
  base
12241
12697
  );
12242
12698
  if (typeof putRes.version === "number") {
12243
- await deps.writeSyncState(opts.home ?? homedir16(), {
12699
+ await deps.writeSyncState(opts.home ?? homedir17(), {
12244
12700
  mcp_bundles: { lastPulledVersion: putRes.version }
12245
12701
  }).catch(() => {
12246
12702
  });
@@ -12286,7 +12742,7 @@ function fail(io, json, message, code) {
12286
12742
  }
12287
12743
 
12288
12744
  // src/stats-cmd.ts
12289
- import { homedir as homedir17 } from "os";
12745
+ import { homedir as homedir18 } from "os";
12290
12746
  var STATS_USAGE = `Usage: yaw-mcp stats [--json] [--limit N] [--days N]
12291
12747
 
12292
12748
  Print a digest of recent AI tool calls recorded against your Yaw
@@ -12410,7 +12866,7 @@ async function runStats(opts, io = {
12410
12866
  out: (s) => process.stdout.write(s),
12411
12867
  err: (s) => process.stderr.write(s)
12412
12868
  }) {
12413
- const home = opts.home ?? homedir17();
12869
+ const home = opts.home ?? homedir18();
12414
12870
  const session = await getSession({ home, baseUrl: opts.baseUrl });
12415
12871
  if (!session) {
12416
12872
  const msg = "Not signed in. Yaw MCP analytics requires a Yaw Team account.\n - Yaw Team: $15/seat/mo or $150/seat/yr -- https://yaw.sh/mcp\nSign in with: yaw-mcp login --key <license-key>";
@@ -12511,10 +12967,10 @@ function suggestFlag(input, limit = 2) {
12511
12967
  }
12512
12968
 
12513
12969
  // src/sync-cmd.ts
12514
- import { existsSync as existsSync7 } from "fs";
12515
- import { mkdir as mkdir6, readFile as readFile13 } from "fs/promises";
12516
- import { homedir as homedir18 } from "os";
12517
- import { dirname as dirname5, join as join12 } from "path";
12970
+ import { existsSync as existsSync8 } from "fs";
12971
+ import { mkdir as mkdir6, readFile as readFile14 } from "fs/promises";
12972
+ import { homedir as homedir19 } from "os";
12973
+ import { dirname as dirname5, join as join13 } from "path";
12518
12974
  var SYNC_USAGE = `Usage: yaw-mcp sync <push|pull|status> [--json]
12519
12975
 
12520
12976
  Replicate ~/.yaw-mcp/bundles.json across machines via your Yaw
@@ -12561,12 +13017,12 @@ ${SYNC_USAGE}` };
12561
13017
  return { ok: true, options: opts };
12562
13018
  }
12563
13019
  function bundlesPath(home) {
12564
- return join12(home, CONFIG_DIRNAME, BUNDLES_FILENAME2);
13020
+ return join13(home, CONFIG_DIRNAME, BUNDLES_FILENAME2);
12565
13021
  }
12566
13022
  async function readLocalBundles(home) {
12567
13023
  const path5 = bundlesPath(home);
12568
- if (!existsSync7(path5)) return { version: 1, servers: [] };
12569
- const raw = await readFile13(path5, "utf8");
13024
+ if (!existsSync8(path5)) return { version: 1, servers: [] };
13025
+ const raw = await readFile14(path5, "utf8");
12570
13026
  let parsed;
12571
13027
  try {
12572
13028
  parsed = JSON.parse(raw);
@@ -12617,7 +13073,7 @@ async function runSync(opts, io = {
12617
13073
  out: (s) => process.stdout.write(s),
12618
13074
  err: (s) => process.stderr.write(s)
12619
13075
  }) {
12620
- const home = opts.home ?? homedir18();
13076
+ const home = opts.home ?? homedir19();
12621
13077
  const session = await getSession({ home, baseUrl: opts.baseUrl });
12622
13078
  if (!session) {
12623
13079
  const msg = "Not signed in. Run `yaw-mcp login --key <license-key>` first.";
@@ -13192,7 +13648,7 @@ if (subcommand === "compliance") {
13192
13648
  `);
13193
13649
  process.exit(0);
13194
13650
  } else if (subcommand === "--version" || subcommand === "-V") {
13195
- process.stdout.write(`yaw-mcp ${true ? "0.64.2" : "dev"}
13651
+ process.stdout.write(`yaw-mcp ${true ? "0.65.0" : "dev"}
13196
13652
  `);
13197
13653
  process.exit(0);
13198
13654
  } else if (subcommand && !subcommand.startsWith("-")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/mcp",
3
- "version": "0.64.2",
3
+ "version": "0.65.0",
4
4
  "mcpName": "io.github.YawLabs/mcp",
5
5
  "description": "Yaw MCP -- MCP servers, managed. Free to run locally; Yaw Team adds cross-machine sync.",
6
6
  "license": "UNLICENSED",