@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.
- package/README.md +100 -0
- package/dist/index.js +506 -50
- 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.
|
|
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.
|
|
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 {
|
|
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
|
|
5671
|
-
import { homedir as
|
|
5672
|
-
import { dirname as dirname2, join as
|
|
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 =
|
|
5732
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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 ??
|
|
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:
|
|
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 = !
|
|
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 ??
|
|
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 ??
|
|
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
|
|
6409
|
-
import { homedir as
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
12581
|
+
import { homedir as homedir17 } from "os";
|
|
12126
12582
|
|
|
12127
12583
|
// src/sync-state.ts
|
|
12128
|
-
import { existsSync as
|
|
12129
|
-
import { mkdir as mkdir5, readFile as
|
|
12130
|
-
import { dirname as dirname4, join as
|
|
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
|
|
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 (!
|
|
12593
|
+
if (!existsSync7(path5)) return {};
|
|
12138
12594
|
try {
|
|
12139
|
-
const raw = await
|
|
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 ??
|
|
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
|
|
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 ??
|
|
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
|
|
12515
|
-
import { mkdir as mkdir6, readFile as
|
|
12516
|
-
import { homedir as
|
|
12517
|
-
import { dirname as dirname5, join as
|
|
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
|
|
13020
|
+
return join13(home, CONFIG_DIRNAME, BUNDLES_FILENAME2);
|
|
12565
13021
|
}
|
|
12566
13022
|
async function readLocalBundles(home) {
|
|
12567
13023
|
const path5 = bundlesPath(home);
|
|
12568
|
-
if (!
|
|
12569
|
-
const raw = await
|
|
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 ??
|
|
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.
|
|
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