@yawlabs/mcp 0.64.1 → 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 +654 -106
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1148,9 +1148,15 @@ async function runBundlesCommand(opts = {}) {
|
|
|
1148
1148
|
env: opts.env
|
|
1149
1149
|
});
|
|
1150
1150
|
if (!config.token) {
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1151
|
+
const msg = "no token resolved. Run `yaw-mcp install <client> --token mcp_pat_\u2026` or set YAW_MCP_TOKEN.";
|
|
1152
|
+
if (opts.json) {
|
|
1153
|
+
const jsonErr = JSON.stringify({ ok: false, error: msg });
|
|
1154
|
+
lines.push(jsonErr);
|
|
1155
|
+
writeErr(`${jsonErr}
|
|
1156
|
+
`);
|
|
1157
|
+
} else {
|
|
1158
|
+
printErr(`yaw-mcp bundles match: ${msg}`);
|
|
1159
|
+
}
|
|
1154
1160
|
return { exitCode: 1, lines };
|
|
1155
1161
|
}
|
|
1156
1162
|
const fetcher = opts.fetcher ?? fetchConfig;
|
|
@@ -1159,11 +1165,26 @@ async function runBundlesCommand(opts = {}) {
|
|
|
1159
1165
|
backend = await fetcher(config.apiBase, config.token);
|
|
1160
1166
|
} catch (err) {
|
|
1161
1167
|
const msg = err instanceof ConfigError || err instanceof Error ? err.message : String(err);
|
|
1162
|
-
|
|
1168
|
+
if (opts.json) {
|
|
1169
|
+
const jsonErr = JSON.stringify({ ok: false, error: msg });
|
|
1170
|
+
lines.push(jsonErr);
|
|
1171
|
+
writeErr(`${jsonErr}
|
|
1172
|
+
`);
|
|
1173
|
+
} else {
|
|
1174
|
+
printErr(`yaw-mcp bundles match: ${msg}`);
|
|
1175
|
+
}
|
|
1163
1176
|
return { exitCode: 2, lines };
|
|
1164
1177
|
}
|
|
1165
1178
|
if (!backend) {
|
|
1166
|
-
|
|
1179
|
+
const msg = "backend returned 304 without a conditional request.";
|
|
1180
|
+
if (opts.json) {
|
|
1181
|
+
const jsonErr = JSON.stringify({ ok: false, error: msg });
|
|
1182
|
+
lines.push(jsonErr);
|
|
1183
|
+
writeErr(`${jsonErr}
|
|
1184
|
+
`);
|
|
1185
|
+
} else {
|
|
1186
|
+
printErr(`yaw-mcp bundles match: ${msg}`);
|
|
1187
|
+
}
|
|
1167
1188
|
return { exitCode: 2, lines };
|
|
1168
1189
|
}
|
|
1169
1190
|
const installed = backend.servers.filter((s) => s.isActive).map((s) => s.namespace);
|
|
@@ -1380,18 +1401,27 @@ function renderScript(shell) {
|
|
|
1380
1401
|
return renderPowershell();
|
|
1381
1402
|
}
|
|
1382
1403
|
}
|
|
1404
|
+
function isPlaceholder(s) {
|
|
1405
|
+
return s.startsWith("<") && s.endsWith(">");
|
|
1406
|
+
}
|
|
1383
1407
|
function renderBash() {
|
|
1384
1408
|
const subcommandList = SUBCOMMAND_SPEC.map((s) => s.name).join(" ");
|
|
1385
1409
|
const topLevelFlags = "--help -h --version -V";
|
|
1386
1410
|
const cases = SUBCOMMAND_SPEC.map((spec) => {
|
|
1387
|
-
const
|
|
1388
|
-
|
|
1411
|
+
const indexedPositionals = (spec.positional ?? []).map((p, i) => ({ value: p, index: i })).filter(({ value }) => !isPlaceholder(value));
|
|
1412
|
+
const posClauses = indexedPositionals.map(
|
|
1413
|
+
({ value, index }) => ` if [[ $cword -eq $((${index} + 2)) ]]; then
|
|
1414
|
+
COMPREPLY=( $(compgen -W "${value}" -- "$cur") )
|
|
1389
1415
|
return 0
|
|
1390
|
-
fi`
|
|
1416
|
+
fi`
|
|
1417
|
+
);
|
|
1418
|
+
const parts = [
|
|
1419
|
+
...posClauses,
|
|
1420
|
+
` COMPREPLY=( $(compgen -W "${spec.flags.join(" ")}" -- "$cur") )`,
|
|
1421
|
+
" return 0"
|
|
1422
|
+
].filter((p) => p !== "");
|
|
1391
1423
|
return ` ${spec.name})
|
|
1392
|
-
${
|
|
1393
|
-
COMPREPLY=( $(compgen -W "${spec.flags.join(" ")}" -- "$cur") )
|
|
1394
|
-
return 0
|
|
1424
|
+
${parts.join("\n")}
|
|
1395
1425
|
;;`;
|
|
1396
1426
|
}).join("\n");
|
|
1397
1427
|
return `# bash completion for yaw-mcp \u2014 generated by \`yaw-mcp completion bash\`
|
|
@@ -1418,8 +1448,10 @@ function renderZsh() {
|
|
|
1418
1448
|
const subcommandList = SUBCOMMAND_SPEC.map((s) => ` '${s.name}:${s.description}'`).join("\n");
|
|
1419
1449
|
const argsCases = SUBCOMMAND_SPEC.map((spec) => {
|
|
1420
1450
|
const lines = [` ${spec.name})`];
|
|
1421
|
-
|
|
1422
|
-
|
|
1451
|
+
const indexedPositionals = (spec.positional ?? []).map((p, i) => ({ value: p, index: i })).filter(({ value }) => !isPlaceholder(value));
|
|
1452
|
+
if (indexedPositionals.length > 0) {
|
|
1453
|
+
const posArgs = indexedPositionals.map(({ value, index }) => `'${index + 1}: :(${value})'`).join(" ");
|
|
1454
|
+
lines.push(` _arguments ${posArgs} '*: :(${spec.flags.join(" ")})'`);
|
|
1423
1455
|
} else {
|
|
1424
1456
|
lines.push(` _arguments '*: :(${spec.flags.join(" ")})'`);
|
|
1425
1457
|
}
|
|
@@ -1463,9 +1495,13 @@ complete -c yaw-mcp -f`;
|
|
|
1463
1495
|
const flagLines = [];
|
|
1464
1496
|
for (const spec of SUBCOMMAND_SPEC) {
|
|
1465
1497
|
if (spec.positional) {
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1498
|
+
spec.positional.forEach((p, i) => {
|
|
1499
|
+
if (isPlaceholder(p)) return;
|
|
1500
|
+
const expectedCount = i + 2;
|
|
1501
|
+
positionalLines.push(
|
|
1502
|
+
`complete -c yaw-mcp -n "__fish_seen_subcommand_from ${spec.name}; and test (count (commandline -opc)) -eq ${expectedCount}" -a ${p}`
|
|
1503
|
+
);
|
|
1504
|
+
});
|
|
1469
1505
|
}
|
|
1470
1506
|
for (const f of spec.flags) {
|
|
1471
1507
|
if (!f.startsWith("--")) continue;
|
|
@@ -1478,12 +1514,13 @@ complete -c yaw-mcp -f`;
|
|
|
1478
1514
|
function renderPowershell() {
|
|
1479
1515
|
const subcommandNames = SUBCOMMAND_SPEC.map((s) => `'${s.name}'`).join(", ");
|
|
1480
1516
|
const caseBranches = SUBCOMMAND_SPEC.map((spec) => {
|
|
1481
|
-
const
|
|
1517
|
+
const indexedPositionals = (spec.positional ?? []).map((p, i) => ({ value: p, index: i })).filter(({ value }) => !isPlaceholder(value));
|
|
1482
1518
|
const flags = spec.flags.map((f) => `'${f}'`).join(", ");
|
|
1483
|
-
const
|
|
1519
|
+
const positionalLines = indexedPositionals.map(({ value, index }) => ` if ($tokens.Count -eq ${index + 2}) { $completions += @('${value}') }`).join("\n");
|
|
1520
|
+
const positionalBlock = positionalLines ? `${positionalLines}
|
|
1484
1521
|
` : "";
|
|
1485
1522
|
return ` '${spec.name}' {
|
|
1486
|
-
${
|
|
1523
|
+
${positionalBlock} $completions += @(${flags})
|
|
1487
1524
|
}`;
|
|
1488
1525
|
}).join("\n");
|
|
1489
1526
|
return `# PowerShell completion for yaw-mcp \u2014 generated by \`yaw-mcp completion powershell\`
|
|
@@ -1694,6 +1731,19 @@ var token = "";
|
|
|
1694
1731
|
var lastFailure = null;
|
|
1695
1732
|
var lastLoggedConnectStatus = null;
|
|
1696
1733
|
var lastLoggedDispatchStatus = null;
|
|
1734
|
+
var warnedInsecureBearerSkipConnect = false;
|
|
1735
|
+
var warnedInsecureBearerSkipDispatch = false;
|
|
1736
|
+
function shouldSendBearer(targetUrl) {
|
|
1737
|
+
let parsed;
|
|
1738
|
+
try {
|
|
1739
|
+
parsed = new URL(targetUrl);
|
|
1740
|
+
} catch {
|
|
1741
|
+
return false;
|
|
1742
|
+
}
|
|
1743
|
+
if (parsed.protocol === "https:") return true;
|
|
1744
|
+
if (parsed.protocol === "http:" && isLoopbackHost(parsed.hostname)) return true;
|
|
1745
|
+
return false;
|
|
1746
|
+
}
|
|
1697
1747
|
function getLastAnalyticsFailure() {
|
|
1698
1748
|
return lastFailure;
|
|
1699
1749
|
}
|
|
@@ -1746,12 +1796,20 @@ async function flush() {
|
|
|
1746
1796
|
const events = buffer.splice(0, FLUSH_SIZE);
|
|
1747
1797
|
const url = `${apiUrl.replace(/\/$/, "")}/api/connect/analytics`;
|
|
1748
1798
|
try {
|
|
1799
|
+
const headers = { "Content-Type": "application/json" };
|
|
1800
|
+
if (shouldSendBearer(url)) {
|
|
1801
|
+
headers.Authorization = `Bearer ${token}`;
|
|
1802
|
+
} else if (!warnedInsecureBearerSkipConnect) {
|
|
1803
|
+
log(
|
|
1804
|
+
"warn",
|
|
1805
|
+
"Analytics URL is not https and not loopback; sending without Authorization header to avoid leaking the bearer token",
|
|
1806
|
+
{ url }
|
|
1807
|
+
);
|
|
1808
|
+
warnedInsecureBearerSkipConnect = true;
|
|
1809
|
+
}
|
|
1749
1810
|
const res = await request3(url, {
|
|
1750
1811
|
method: "POST",
|
|
1751
|
-
headers
|
|
1752
|
-
Authorization: `Bearer ${token}`,
|
|
1753
|
-
"Content-Type": "application/json"
|
|
1754
|
-
},
|
|
1812
|
+
headers,
|
|
1755
1813
|
body: JSON.stringify({ events }),
|
|
1756
1814
|
headersTimeout: 1e4,
|
|
1757
1815
|
bodyTimeout: 1e4
|
|
@@ -1788,17 +1846,25 @@ async function flushDispatch() {
|
|
|
1788
1846
|
const events = dispatchBuffer.splice(0, FLUSH_SIZE);
|
|
1789
1847
|
const url = `${apiUrl.replace(/\/$/, "")}/api/connect/dispatch-events`;
|
|
1790
1848
|
try {
|
|
1849
|
+
const headers = { "Content-Type": "application/json" };
|
|
1850
|
+
if (shouldSendBearer(url)) {
|
|
1851
|
+
headers.Authorization = `Bearer ${token}`;
|
|
1852
|
+
} else if (!warnedInsecureBearerSkipDispatch) {
|
|
1853
|
+
log(
|
|
1854
|
+
"warn",
|
|
1855
|
+
"Analytics URL is not https and not loopback; sending without Authorization header to avoid leaking the bearer token",
|
|
1856
|
+
{ url }
|
|
1857
|
+
);
|
|
1858
|
+
warnedInsecureBearerSkipDispatch = true;
|
|
1859
|
+
}
|
|
1791
1860
|
const res = await request3(url, {
|
|
1792
1861
|
method: "POST",
|
|
1793
|
-
headers
|
|
1794
|
-
Authorization: `Bearer ${token}`,
|
|
1795
|
-
"Content-Type": "application/json"
|
|
1796
|
-
},
|
|
1862
|
+
headers,
|
|
1797
1863
|
body: JSON.stringify({ events }),
|
|
1798
1864
|
headersTimeout: 1e4,
|
|
1799
1865
|
bodyTimeout: 1e4
|
|
1800
1866
|
});
|
|
1801
|
-
if (res.statusCode >= 400
|
|
1867
|
+
if (res.statusCode >= 400) {
|
|
1802
1868
|
const retryable = res.statusCode >= 500 || res.statusCode === 408 || res.statusCode === 429;
|
|
1803
1869
|
if (retryable) {
|
|
1804
1870
|
const room = MAX_BUFFER - dispatchBuffer.length;
|
|
@@ -1812,7 +1878,7 @@ async function flushDispatch() {
|
|
|
1812
1878
|
lastLoggedDispatchStatus = res.statusCode;
|
|
1813
1879
|
}
|
|
1814
1880
|
lastFailure = { statusCode: res.statusCode, url, at: Date.now() };
|
|
1815
|
-
} else
|
|
1881
|
+
} else {
|
|
1816
1882
|
lastFailure = null;
|
|
1817
1883
|
lastLoggedDispatchStatus = null;
|
|
1818
1884
|
}
|
|
@@ -1830,6 +1896,8 @@ function initAnalytics(url, tok) {
|
|
|
1830
1896
|
token = tok;
|
|
1831
1897
|
lastLoggedConnectStatus = null;
|
|
1832
1898
|
lastLoggedDispatchStatus = null;
|
|
1899
|
+
warnedInsecureBearerSkipConnect = false;
|
|
1900
|
+
warnedInsecureBearerSkipDispatch = false;
|
|
1833
1901
|
teamAnalyticsDisabled = false;
|
|
1834
1902
|
flushTimer = setInterval(() => {
|
|
1835
1903
|
flush().catch(() => {
|
|
@@ -2296,9 +2364,12 @@ function errorMessage(err) {
|
|
|
2296
2364
|
import { request as request4 } from "undici";
|
|
2297
2365
|
var apiUrl2 = "";
|
|
2298
2366
|
var token2 = "";
|
|
2299
|
-
var
|
|
2300
|
-
function getLastReportFailure() {
|
|
2301
|
-
return
|
|
2367
|
+
var lastFailureByServer = /* @__PURE__ */ new Map();
|
|
2368
|
+
function getLastReportFailure(serverId) {
|
|
2369
|
+
if (serverId !== void 0) return lastFailureByServer.get(serverId) ?? null;
|
|
2370
|
+
const entries = [...lastFailureByServer.values()];
|
|
2371
|
+
if (entries.length === 0) return null;
|
|
2372
|
+
return entries.reduce((a, b) => a.at >= b.at ? a : b);
|
|
2302
2373
|
}
|
|
2303
2374
|
function initToolReport(url, tok) {
|
|
2304
2375
|
apiUrl2 = url;
|
|
@@ -2306,7 +2377,7 @@ function initToolReport(url, tok) {
|
|
|
2306
2377
|
}
|
|
2307
2378
|
async function reportTools(serverId, tools) {
|
|
2308
2379
|
if (!apiUrl2 || !token2 || !serverId) return;
|
|
2309
|
-
const url = `${apiUrl2.replace(/\/$/, "")}/api/connect/servers/${serverId}/tools`;
|
|
2380
|
+
const url = `${apiUrl2.replace(/\/$/, "")}/api/connect/servers/${encodeURIComponent(serverId)}/tools`;
|
|
2310
2381
|
try {
|
|
2311
2382
|
const res = await request4(url, {
|
|
2312
2383
|
method: "POST",
|
|
@@ -2322,12 +2393,13 @@ async function reportTools(serverId, tools) {
|
|
|
2322
2393
|
});
|
|
2323
2394
|
if (res.statusCode >= 400 && res.statusCode !== 404) {
|
|
2324
2395
|
log("warn", "Tool report failed", { serverId, status: res.statusCode });
|
|
2325
|
-
|
|
2326
|
-
} else
|
|
2327
|
-
|
|
2396
|
+
lastFailureByServer.set(serverId, { statusCode: res.statusCode, url, at: Date.now() });
|
|
2397
|
+
} else {
|
|
2398
|
+
lastFailureByServer.delete(serverId);
|
|
2328
2399
|
}
|
|
2329
2400
|
} catch (err) {
|
|
2330
2401
|
log("warn", "Tool report error", { serverId, error: err?.message });
|
|
2402
|
+
lastFailureByServer.set(serverId, { statusCode: 0, url, at: Date.now() });
|
|
2331
2403
|
}
|
|
2332
2404
|
}
|
|
2333
2405
|
|
|
@@ -2367,6 +2439,9 @@ function tokenizeCommand(cmd) {
|
|
|
2367
2439
|
has = true;
|
|
2368
2440
|
}
|
|
2369
2441
|
}
|
|
2442
|
+
if (quote !== null) {
|
|
2443
|
+
throw new Error(`Unbalanced quote in command: ${cmd}`);
|
|
2444
|
+
}
|
|
2370
2445
|
if (has) out.push(cur);
|
|
2371
2446
|
return out;
|
|
2372
2447
|
}
|
|
@@ -3912,7 +3987,7 @@ async function runUpgrade(opts = {}) {
|
|
|
3912
3987
|
return { exitCode: 3, lines };
|
|
3913
3988
|
}
|
|
3914
3989
|
function readCurrentVersion() {
|
|
3915
|
-
return true ? "0.
|
|
3990
|
+
return true ? "0.65.0" : "dev";
|
|
3916
3991
|
}
|
|
3917
3992
|
|
|
3918
3993
|
// src/usage-hints.ts
|
|
@@ -3974,7 +4049,7 @@ function selectFlakyNamespaces(entries, limit) {
|
|
|
3974
4049
|
}
|
|
3975
4050
|
|
|
3976
4051
|
// src/doctor-cmd.ts
|
|
3977
|
-
var VERSION = true ? "0.
|
|
4052
|
+
var VERSION = true ? "0.65.0" : "dev";
|
|
3978
4053
|
function isPersistenceDisabled(env) {
|
|
3979
4054
|
const raw = env.YAW_MCP_DISABLE_PERSISTENCE;
|
|
3980
4055
|
return raw !== void 0 && raw !== "" && (raw === "1" || raw.toLowerCase() === "true");
|
|
@@ -5588,13 +5663,85 @@ function isFileNotFound2(err) {
|
|
|
5588
5663
|
}
|
|
5589
5664
|
|
|
5590
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
|
|
5591
5670
|
import { existsSync as existsSync5 } from "fs";
|
|
5592
|
-
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
|
+
}
|
|
5593
5740
|
|
|
5594
5741
|
// src/secrets-vault.ts
|
|
5595
|
-
import { chmod as
|
|
5596
|
-
import { homedir as
|
|
5597
|
-
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";
|
|
5598
5745
|
|
|
5599
5746
|
// src/secrets-crypto.ts
|
|
5600
5747
|
import { createCipheriv, createDecipheriv, randomBytes, scrypt as scryptCb } from "crypto";
|
|
@@ -5653,8 +5800,8 @@ function decryptEntry(entry, key) {
|
|
|
5653
5800
|
var SECRETS_FILENAME = "secrets.json";
|
|
5654
5801
|
var SECRETS_SCHEMA_VERSION = 1;
|
|
5655
5802
|
var VAULT_CHECK_PLAINTEXT = "yaw-mcp-vault-v1";
|
|
5656
|
-
function vaultPath(home =
|
|
5657
|
-
return
|
|
5803
|
+
function vaultPath(home = homedir14()) {
|
|
5804
|
+
return join11(home, CONFIG_DIRNAME, SECRETS_FILENAME);
|
|
5658
5805
|
}
|
|
5659
5806
|
function emptyVault() {
|
|
5660
5807
|
return {
|
|
@@ -5666,7 +5813,7 @@ function emptyVault() {
|
|
|
5666
5813
|
async function loadVault(path5) {
|
|
5667
5814
|
let raw;
|
|
5668
5815
|
try {
|
|
5669
|
-
raw = await
|
|
5816
|
+
raw = await readFile10(path5, "utf8");
|
|
5670
5817
|
} catch (err) {
|
|
5671
5818
|
const code = err.code;
|
|
5672
5819
|
if (code === "ENOENT") return null;
|
|
@@ -5711,7 +5858,7 @@ async function saveVault(path5, vault) {
|
|
|
5711
5858
|
await mkdir4(dir, { recursive: true });
|
|
5712
5859
|
if (process.platform !== "win32") {
|
|
5713
5860
|
try {
|
|
5714
|
-
await
|
|
5861
|
+
await chmod5(dir, 448);
|
|
5715
5862
|
} catch {
|
|
5716
5863
|
}
|
|
5717
5864
|
}
|
|
@@ -5719,7 +5866,7 @@ async function saveVault(path5, vault) {
|
|
|
5719
5866
|
`, "utf8", 384, 448);
|
|
5720
5867
|
if (process.platform !== "win32") {
|
|
5721
5868
|
try {
|
|
5722
|
-
await
|
|
5869
|
+
await chmod5(path5, 384);
|
|
5723
5870
|
} catch {
|
|
5724
5871
|
}
|
|
5725
5872
|
}
|
|
@@ -5753,6 +5900,44 @@ function ensureCheck(vault, key) {
|
|
|
5753
5900
|
if (vault.check) return vault;
|
|
5754
5901
|
return { ...vault, check: encryptEntry(VAULT_CHECK_PLAINTEXT, key) };
|
|
5755
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
|
+
}
|
|
5756
5941
|
function listKeys(vault) {
|
|
5757
5942
|
return Object.keys(vault.entries).sort();
|
|
5758
5943
|
}
|
|
@@ -5844,6 +6029,19 @@ Actions:
|
|
|
5844
6029
|
\`yaw-mcp login\` first. Refuses when the local
|
|
5845
6030
|
vault has a different salt (different passphrase
|
|
5846
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.
|
|
5847
6045
|
|
|
5848
6046
|
Flags:
|
|
5849
6047
|
--json Machine-readable output (where applicable).
|
|
@@ -5855,12 +6053,18 @@ Flags:
|
|
|
5855
6053
|
--replace (push only) Overwrite even when the remote vault
|
|
5856
6054
|
salt differs from the local (different passphrase
|
|
5857
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.
|
|
5858
6060
|
|
|
5859
6061
|
Passphrase:
|
|
5860
6062
|
Set YAW_MCP_VAULT_PASSPHRASE in the env, or you will be prompted on
|
|
5861
6063
|
the controlling TTY. The passphrase derives the encryption key via
|
|
5862
6064
|
scrypt and is cached in memory for the lifetime of this yaw-mcp
|
|
5863
|
-
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).`;
|
|
5864
6068
|
function parseSecretsArgs(argv) {
|
|
5865
6069
|
const opts = {};
|
|
5866
6070
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -5882,6 +6086,10 @@ function parseSecretsArgs(argv) {
|
|
|
5882
6086
|
opts.replace = true;
|
|
5883
6087
|
continue;
|
|
5884
6088
|
}
|
|
6089
|
+
if (a === "--push") {
|
|
6090
|
+
opts.push = true;
|
|
6091
|
+
continue;
|
|
6092
|
+
}
|
|
5885
6093
|
if (a === "--value") {
|
|
5886
6094
|
const v = argv[++i];
|
|
5887
6095
|
if (v === void 0 || v.startsWith("-")) {
|
|
@@ -5895,13 +6103,31 @@ ${SECRETS_USAGE}`
|
|
|
5895
6103
|
opts.value = v;
|
|
5896
6104
|
continue;
|
|
5897
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
|
+
}
|
|
5898
6124
|
if (a.startsWith("-")) {
|
|
5899
6125
|
return { ok: false, error: `yaw-mcp secrets: unknown flag "${a}"
|
|
5900
6126
|
|
|
5901
6127
|
${SECRETS_USAGE}` };
|
|
5902
6128
|
}
|
|
5903
6129
|
if (!opts.action) {
|
|
5904
|
-
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") {
|
|
5905
6131
|
return { ok: false, error: `yaw-mcp secrets: unknown action "${a}"
|
|
5906
6132
|
|
|
5907
6133
|
${SECRETS_USAGE}` };
|
|
@@ -5965,6 +6191,35 @@ async function resolvePassphrase(opts) {
|
|
|
5965
6191
|
}
|
|
5966
6192
|
return null;
|
|
5967
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
|
+
}
|
|
5968
6223
|
var MAX_PASSPHRASE_PROMPTS = 3;
|
|
5969
6224
|
var MIN_PASSPHRASE_WARN_LEN = 12;
|
|
5970
6225
|
function readPassphraseFromTTY(stdin, stdout, prompt = "Vault passphrase: ") {
|
|
@@ -6033,7 +6288,7 @@ async function runSecrets(opts, io = {
|
|
|
6033
6288
|
out: (s) => process.stdout.write(s),
|
|
6034
6289
|
err: (s) => process.stderr.write(s)
|
|
6035
6290
|
}) {
|
|
6036
|
-
const home = opts.home ??
|
|
6291
|
+
const home = opts.home ?? homedir15();
|
|
6037
6292
|
const path5 = vaultPath(home);
|
|
6038
6293
|
if (opts.action === "lock") {
|
|
6039
6294
|
lock();
|
|
@@ -6048,12 +6303,18 @@ async function runSecrets(opts, io = {
|
|
|
6048
6303
|
if (opts.action === "pull") {
|
|
6049
6304
|
return await runSecretsPull(opts, io);
|
|
6050
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
|
+
}
|
|
6051
6312
|
if (opts.action === "list") {
|
|
6052
6313
|
const loaded = await safeLoadVault(path5, io, opts.json, "list");
|
|
6053
6314
|
if (!loaded.ok) return loaded.result;
|
|
6054
6315
|
const vault2 = loaded.vault;
|
|
6055
6316
|
const keys = vault2 ? listKeys(vault2) : [];
|
|
6056
|
-
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)}
|
|
6057
6318
|
`);
|
|
6058
6319
|
else if (!vault2) io.out(`No vault at ${path5}. Run \`yaw-mcp secrets set <name>\` to create one.
|
|
6059
6320
|
`);
|
|
@@ -6084,7 +6345,7 @@ async function runSecrets(opts, io = {
|
|
|
6084
6345
|
const loadedForMutate = await safeLoadVault(path5, io, opts.json, opts.action ?? "");
|
|
6085
6346
|
if (!loadedForMutate.ok) return loadedForMutate.result;
|
|
6086
6347
|
let vault = loadedForMutate.vault ?? newVault();
|
|
6087
|
-
const isFresh = !
|
|
6348
|
+
const isFresh = !existsSync6(path5);
|
|
6088
6349
|
const passphrase = await resolvePassphrase(opts);
|
|
6089
6350
|
if (passphrase === null) {
|
|
6090
6351
|
const msg = "Passphrase required. Set YAW_MCP_VAULT_PASSPHRASE or run from a TTY so we can prompt.";
|
|
@@ -6186,7 +6447,7 @@ async function runSecrets(opts, io = {
|
|
|
6186
6447
|
}
|
|
6187
6448
|
var MCP_SECRETS_RESOURCE = "mcp_secrets";
|
|
6188
6449
|
async function runSecretsPush(opts, io) {
|
|
6189
|
-
const home = opts.home ??
|
|
6450
|
+
const home = opts.home ?? homedir15();
|
|
6190
6451
|
const path5 = vaultPath(home);
|
|
6191
6452
|
const session = await getSession({ home, baseUrl: opts.baseUrl });
|
|
6192
6453
|
if (!session) {
|
|
@@ -6260,7 +6521,7 @@ async function runSecretsPush(opts, io) {
|
|
|
6260
6521
|
}
|
|
6261
6522
|
}
|
|
6262
6523
|
async function runSecretsPull(opts, io) {
|
|
6263
|
-
const home = opts.home ??
|
|
6524
|
+
const home = opts.home ?? homedir15();
|
|
6264
6525
|
const path5 = vaultPath(home);
|
|
6265
6526
|
const session = await getSession({ home, baseUrl: opts.baseUrl });
|
|
6266
6527
|
if (!session) {
|
|
@@ -6328,10 +6589,159 @@ async function runSecretsPull(opts, io) {
|
|
|
6328
6589
|
return { exitCode: 1 };
|
|
6329
6590
|
}
|
|
6330
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
|
+
}
|
|
6331
6741
|
|
|
6332
6742
|
// src/server.ts
|
|
6333
|
-
import { readFile as
|
|
6334
|
-
import { homedir as
|
|
6743
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
6744
|
+
import { homedir as homedir16 } from "os";
|
|
6335
6745
|
import { isAbsolute as isAbsolute2, relative, resolve as resolve6 } from "path";
|
|
6336
6746
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6337
6747
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -6447,7 +6857,7 @@ function defaultSpawn2(cmd, args) {
|
|
|
6447
6857
|
async function maybeAutoUpgrade(deps = {}) {
|
|
6448
6858
|
const optOut = process.env.YAW_MCP_AUTO_UPGRADE;
|
|
6449
6859
|
if (optOut === "0" || optOut?.toLowerCase() === "false") return;
|
|
6450
|
-
const current = deps.currentVersion ?? (true ? "0.
|
|
6860
|
+
const current = deps.currentVersion ?? (true ? "0.65.0" : "dev");
|
|
6451
6861
|
if (current === "dev") return;
|
|
6452
6862
|
const method = (deps.isSeaImpl ? await deps.isSeaImpl() : await detectSea()) ? "binary" : detectInstallMethod(deps.argvPath ?? process.argv[1]);
|
|
6453
6863
|
const latest = await (deps.fetchLatestImpl ?? fetchLatestVersion2)();
|
|
@@ -6953,22 +7363,22 @@ function closestNames(query, candidates, limit) {
|
|
|
6953
7363
|
}
|
|
6954
7364
|
|
|
6955
7365
|
// src/guide.ts
|
|
6956
|
-
import { readFile as
|
|
7366
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
6957
7367
|
var GUIDE_READ_TIMEOUT_MS = 1e3;
|
|
6958
7368
|
async function readGuide(path5, scope) {
|
|
6959
7369
|
let raw;
|
|
7370
|
+
const ac = new AbortController();
|
|
7371
|
+
const timer = setTimeout(() => ac.abort(new Error("guide read timeout")), GUIDE_READ_TIMEOUT_MS);
|
|
6960
7372
|
try {
|
|
6961
|
-
raw = await
|
|
6962
|
-
readFile10(path5, "utf8"),
|
|
6963
|
-
new Promise(
|
|
6964
|
-
(_, reject) => setTimeout(() => reject(new Error("guide read timeout")), GUIDE_READ_TIMEOUT_MS)
|
|
6965
|
-
)
|
|
6966
|
-
]);
|
|
7373
|
+
raw = await readFile11(path5, { encoding: "utf8", signal: ac.signal });
|
|
6967
7374
|
} catch (err) {
|
|
6968
|
-
|
|
7375
|
+
const isTimeout = err instanceof Error && err.code === "ABORT_ERR";
|
|
7376
|
+
if (isTimeout) {
|
|
6969
7377
|
log("warn", "Guide read timed out", { path: path5 });
|
|
6970
7378
|
}
|
|
6971
7379
|
return null;
|
|
7380
|
+
} finally {
|
|
7381
|
+
clearTimeout(timer);
|
|
6972
7382
|
}
|
|
6973
7383
|
const content = raw.trim();
|
|
6974
7384
|
if (content.length === 0) {
|
|
@@ -7080,7 +7490,7 @@ function initHeartbeat(url, tok) {
|
|
|
7080
7490
|
lastLoggedErrorMessage = null;
|
|
7081
7491
|
warnedInsecureBearerSkip = false;
|
|
7082
7492
|
}
|
|
7083
|
-
function
|
|
7493
|
+
function shouldSendBearer2(targetUrl) {
|
|
7084
7494
|
let parsed;
|
|
7085
7495
|
try {
|
|
7086
7496
|
parsed = new URL(targetUrl);
|
|
@@ -7106,7 +7516,7 @@ async function reportHeartbeat(clientName, clientVersion, isRefresh = false) {
|
|
|
7106
7516
|
const headers = {
|
|
7107
7517
|
"Content-Type": "application/json"
|
|
7108
7518
|
};
|
|
7109
|
-
if (
|
|
7519
|
+
if (shouldSendBearer2(fullUrl)) {
|
|
7110
7520
|
headers.Authorization = `Bearer ${token3}`;
|
|
7111
7521
|
}
|
|
7112
7522
|
const res = await request6(fullUrl, {
|
|
@@ -7572,6 +7982,26 @@ var META_TOOLS = {
|
|
|
7572
7982
|
openWorldHint: false
|
|
7573
7983
|
}
|
|
7574
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
|
+
},
|
|
7575
8005
|
exec: {
|
|
7576
8006
|
name: "mcp_connect_exec",
|
|
7577
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.",
|
|
@@ -7689,6 +8119,30 @@ function buildInstallPayload(args) {
|
|
|
7689
8119
|
}
|
|
7690
8120
|
return { ok: true, payload };
|
|
7691
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
|
+
}
|
|
7692
8146
|
var META_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
7693
8147
|
META_TOOLS.discover.name,
|
|
7694
8148
|
META_TOOLS.activate.name,
|
|
@@ -7700,7 +8154,8 @@ var META_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
|
7700
8154
|
META_TOOLS.read_tool.name,
|
|
7701
8155
|
META_TOOLS.suggest.name,
|
|
7702
8156
|
META_TOOLS.exec.name,
|
|
7703
|
-
META_TOOLS.bundles.name
|
|
8157
|
+
META_TOOLS.bundles.name,
|
|
8158
|
+
META_TOOLS.secrets.name
|
|
7704
8159
|
]);
|
|
7705
8160
|
|
|
7706
8161
|
// src/pack-detect.ts
|
|
@@ -8139,7 +8594,8 @@ function pruneJson(value) {
|
|
|
8139
8594
|
}
|
|
8140
8595
|
|
|
8141
8596
|
// src/read-tool.ts
|
|
8142
|
-
function normalizeToolName(namespace, raw) {
|
|
8597
|
+
function normalizeToolName(namespace, raw, tools) {
|
|
8598
|
+
if (tools?.some((t) => t.name === raw)) return raw;
|
|
8143
8599
|
const prefix = `${namespace}_`;
|
|
8144
8600
|
if (raw.startsWith(prefix) && raw.length > prefix.length) return raw.slice(prefix.length);
|
|
8145
8601
|
return raw;
|
|
@@ -9137,7 +9593,7 @@ async function resolveUvSpawn(command, args) {
|
|
|
9137
9593
|
}
|
|
9138
9594
|
|
|
9139
9595
|
// src/upstream.ts
|
|
9140
|
-
async function resolveServerEnv(env) {
|
|
9596
|
+
async function resolveServerEnv(env, namespace) {
|
|
9141
9597
|
if (!hasSecretRefs(env)) return env;
|
|
9142
9598
|
const refKeys = Object.entries(env).filter(([, v]) => typeof v === "string" && v.includes("${secret:")).map(([k]) => k);
|
|
9143
9599
|
const passphrase = process.env.YAW_MCP_VAULT_PASSPHRASE;
|
|
@@ -9159,8 +9615,36 @@ async function resolveServerEnv(env) {
|
|
|
9159
9615
|
if (missing.length > 0) {
|
|
9160
9616
|
throw new Error(`vault: missing or undecryptable secret refs: ${missing.join(", ")}`);
|
|
9161
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
|
+
}
|
|
9162
9626
|
return resolved;
|
|
9163
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
|
+
}
|
|
9164
9648
|
var DEFAULT_CONNECT_TIMEOUT = (() => {
|
|
9165
9649
|
const env = process.env.MCP_CONNECT_TIMEOUT;
|
|
9166
9650
|
if (!env) return 15e3;
|
|
@@ -9208,7 +9692,7 @@ function categorizeSpawnError(err) {
|
|
|
9208
9692
|
}
|
|
9209
9693
|
async function connectToUpstream(config, onDisconnect, onListChanged) {
|
|
9210
9694
|
const client = new Client(
|
|
9211
|
-
{ name: "yaw-mcp", version: true ? "0.
|
|
9695
|
+
{ name: "yaw-mcp", version: true ? "0.65.0" : "dev" },
|
|
9212
9696
|
{ capabilities: {} }
|
|
9213
9697
|
);
|
|
9214
9698
|
let transport;
|
|
@@ -9224,7 +9708,7 @@ async function connectToUpstream(config, onDisconnect, onListChanged) {
|
|
|
9224
9708
|
...parentEnv
|
|
9225
9709
|
} = process.env;
|
|
9226
9710
|
const resolved = await resolveUvSpawn(config.command, config.args ?? []);
|
|
9227
|
-
const serverEnv = await resolveServerEnv(config.env ?? {});
|
|
9711
|
+
const serverEnv = await resolveServerEnv(config.env ?? {}, config.namespace);
|
|
9228
9712
|
resolvedServerEnv = serverEnv;
|
|
9229
9713
|
const stdioTransport = new StdioClientTransport({
|
|
9230
9714
|
command: resolved.command,
|
|
@@ -9279,7 +9763,7 @@ async function connectToUpstream(config, onDisconnect, onListChanged) {
|
|
|
9279
9763
|
message = `Server "${config.namespace}" started but didn't complete the MCP handshake within ${connectTimeoutMs / 1e3}s.${trimmedStderr ? ` stderr tail: ${redactSecretsInOutput(trimmedStderr, resolvedServerEnv).slice(-500)}` : ""}`;
|
|
9280
9764
|
} else if (trimmedStderr.length > 0) {
|
|
9281
9765
|
category = "install_failure";
|
|
9282
|
-
const safe = redactSecretsInOutput(trimmedStderr,
|
|
9766
|
+
const safe = redactSecretsInOutput(trimmedStderr, resolvedServerEnv);
|
|
9283
9767
|
message = `Server "${config.namespace}" failed to start. stderr: ${safe.slice(-500)}`;
|
|
9284
9768
|
} else {
|
|
9285
9769
|
category = categorizeSpawnError(err);
|
|
@@ -9292,7 +9776,7 @@ async function connectToUpstream(config, onDisconnect, onListChanged) {
|
|
|
9292
9776
|
if (config.id) {
|
|
9293
9777
|
message = `${message} \u2192 Edit at https://yaw.sh/mcp/dashboard/connect#server-${config.id}`;
|
|
9294
9778
|
}
|
|
9295
|
-
const redactedTail = trimmedStderr ? redactSecretsInOutput(trimmedStderr,
|
|
9779
|
+
const redactedTail = trimmedStderr ? redactSecretsInOutput(trimmedStderr, resolvedServerEnv) : void 0;
|
|
9296
9780
|
throw new ActivationError(message, category, redactedTail, err);
|
|
9297
9781
|
}
|
|
9298
9782
|
log("info", "Connected to upstream", { name: config.name, namespace: config.namespace, type: config.type });
|
|
@@ -9540,7 +10024,7 @@ var ConnectServer = class _ConnectServer {
|
|
|
9540
10024
|
this.apiUrl = apiUrl5;
|
|
9541
10025
|
this.token = token5;
|
|
9542
10026
|
this.server = new Server(
|
|
9543
|
-
{ name: "yaw-mcp", version: true ? "0.
|
|
10027
|
+
{ name: "yaw-mcp", version: true ? "0.65.0" : "dev" },
|
|
9544
10028
|
{
|
|
9545
10029
|
capabilities: {
|
|
9546
10030
|
tools: { listChanged: true },
|
|
@@ -10122,6 +10606,17 @@ var ConnectServer = class _ConnectServer {
|
|
|
10122
10606
|
recordConnectEvent({ namespace: null, toolName: null, action: "bundles", latencyMs: null, success: true });
|
|
10123
10607
|
return this.attachGuideNudge(this.handleBundles(action));
|
|
10124
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
|
+
}
|
|
10125
10620
|
let routes = this.toolRoutes;
|
|
10126
10621
|
let route = routes.get(name);
|
|
10127
10622
|
if (route?.deferred) {
|
|
@@ -11217,7 +11712,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
|
|
|
11217
11712
|
}
|
|
11218
11713
|
const ALLOWED_FILENAMES = ["claude_desktop_config.json", "mcp.json", "settings.json", "mcp_config.json"];
|
|
11219
11714
|
try {
|
|
11220
|
-
const resolved = filepath.startsWith("~/") || filepath.startsWith("~\\") ? resolve6(
|
|
11715
|
+
const resolved = filepath.startsWith("~/") || filepath.startsWith("~\\") ? resolve6(homedir16(), filepath.slice(2)) : resolve6(filepath);
|
|
11221
11716
|
const resolvedBasename = resolved.split(/[/\\]/).pop() || "";
|
|
11222
11717
|
if (!ALLOWED_FILENAMES.includes(resolvedBasename)) {
|
|
11223
11718
|
return {
|
|
@@ -11234,7 +11729,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
|
|
|
11234
11729
|
const rel = relative(base, p);
|
|
11235
11730
|
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
11236
11731
|
};
|
|
11237
|
-
if (!isUnder(
|
|
11732
|
+
if (!isUnder(homedir16(), resolved) && !isUnder(process.cwd(), resolved)) {
|
|
11238
11733
|
return {
|
|
11239
11734
|
content: [
|
|
11240
11735
|
{ type: "text", text: "Import path must be under your home directory or the current working directory." }
|
|
@@ -11242,7 +11737,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
|
|
|
11242
11737
|
isError: true
|
|
11243
11738
|
};
|
|
11244
11739
|
}
|
|
11245
|
-
const raw = await
|
|
11740
|
+
const raw = await readFile12(resolved, "utf-8");
|
|
11246
11741
|
const parsed = JSON.parse(raw);
|
|
11247
11742
|
if (!parsed.mcpServers || typeof parsed.mcpServers !== "object" || Array.isArray(parsed.mcpServers)) {
|
|
11248
11743
|
return {
|
|
@@ -11471,9 +11966,9 @@ Use mcp_connect_discover to see imported servers.`
|
|
|
11471
11966
|
isError: true
|
|
11472
11967
|
};
|
|
11473
11968
|
}
|
|
11474
|
-
const toolName = normalizeToolName(serverArg, toolArg);
|
|
11475
11969
|
const existing = this.connections.get(serverArg);
|
|
11476
11970
|
if (existing && existing.status === "connected") {
|
|
11971
|
+
const toolName = normalizeToolName(serverArg, toolArg, existing.tools);
|
|
11477
11972
|
const tool = findTool(existing.tools, toolName);
|
|
11478
11973
|
if (!tool) {
|
|
11479
11974
|
return {
|
|
@@ -11509,6 +12004,7 @@ Use mcp_connect_discover to see imported servers.`
|
|
|
11509
12004
|
};
|
|
11510
12005
|
}
|
|
11511
12006
|
try {
|
|
12007
|
+
const toolName = normalizeToolName(serverArg, toolArg, transient.tools);
|
|
11512
12008
|
const tool = findTool(transient.tools, toolName);
|
|
11513
12009
|
if (!tool) {
|
|
11514
12010
|
return {
|
|
@@ -11533,6 +12029,43 @@ Use mcp_connect_discover to see imported servers.`
|
|
|
11533
12029
|
);
|
|
11534
12030
|
}
|
|
11535
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
|
+
}
|
|
11536
12069
|
handleHealth() {
|
|
11537
12070
|
const lines = [];
|
|
11538
12071
|
if (this.profile) {
|
|
@@ -11974,9 +12507,10 @@ async function runServersCommand(opts = {}) {
|
|
|
11974
12507
|
printErr("yaw-mcp servers: backend returned no data (unexpected 304).");
|
|
11975
12508
|
return { exitCode: 2, lines };
|
|
11976
12509
|
}
|
|
11977
|
-
const
|
|
12510
|
+
const filterStr = opts.filter;
|
|
12511
|
+
const filtered = filterStr !== void 0 ? {
|
|
11978
12512
|
...backend,
|
|
11979
|
-
servers: backend.servers.filter((s) => s.namespace.toLowerCase().includes(
|
|
12513
|
+
servers: backend.servers.filter((s) => s.namespace.toLowerCase().includes(filterStr.toLowerCase()))
|
|
11980
12514
|
} : backend;
|
|
11981
12515
|
const gradesReader = opts.gradesReader ?? readGradesCache;
|
|
11982
12516
|
const grades = await gradesReader(opts.home).catch(() => ({}));
|
|
@@ -11991,12 +12525,12 @@ async function runServersCommand(opts = {}) {
|
|
|
11991
12525
|
const payload = {
|
|
11992
12526
|
...merged,
|
|
11993
12527
|
filter: opts.filter ?? null,
|
|
11994
|
-
filterMatched: opts.filter ? merged.servers.length > 0 : null
|
|
12528
|
+
filterMatched: opts.filter !== void 0 ? merged.servers.length > 0 : null
|
|
11995
12529
|
};
|
|
11996
12530
|
print(JSON.stringify(payload, null, 2));
|
|
11997
12531
|
return { exitCode: 0, lines };
|
|
11998
12532
|
}
|
|
11999
|
-
if (opts.filter && filtered.servers.length === 0) {
|
|
12533
|
+
if (opts.filter !== void 0 && filtered.servers.length === 0) {
|
|
12000
12534
|
print(`No servers match "${opts.filter}". Run \`yaw-mcp servers\` to see the full list.`);
|
|
12001
12535
|
return { exitCode: 0, lines };
|
|
12002
12536
|
}
|
|
@@ -12044,21 +12578,21 @@ function truncateVersion(v) {
|
|
|
12044
12578
|
}
|
|
12045
12579
|
|
|
12046
12580
|
// src/set-active-cmd.ts
|
|
12047
|
-
import { homedir as
|
|
12581
|
+
import { homedir as homedir17 } from "os";
|
|
12048
12582
|
|
|
12049
12583
|
// src/sync-state.ts
|
|
12050
|
-
import { existsSync as
|
|
12051
|
-
import { mkdir as mkdir5, readFile as
|
|
12052
|
-
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";
|
|
12053
12587
|
var SYNC_STATE_FILENAME = "sync-state.json";
|
|
12054
12588
|
function syncStatePath(home) {
|
|
12055
|
-
return
|
|
12589
|
+
return join12(home, CONFIG_DIRNAME, SYNC_STATE_FILENAME);
|
|
12056
12590
|
}
|
|
12057
12591
|
async function readSyncState(home) {
|
|
12058
12592
|
const path5 = syncStatePath(home);
|
|
12059
|
-
if (!
|
|
12593
|
+
if (!existsSync7(path5)) return {};
|
|
12060
12594
|
try {
|
|
12061
|
-
const raw = await
|
|
12595
|
+
const raw = await readFile13(path5, "utf8");
|
|
12062
12596
|
const parsed = JSON.parse(raw);
|
|
12063
12597
|
if (!parsed || typeof parsed !== "object") return {};
|
|
12064
12598
|
return parsed;
|
|
@@ -12162,10 +12696,15 @@ async function runSetActive(opts, io = { out: (s) => process.stdout.write(s), er
|
|
|
12162
12696
|
base
|
|
12163
12697
|
);
|
|
12164
12698
|
if (typeof putRes.version === "number") {
|
|
12165
|
-
await deps.writeSyncState(opts.home ??
|
|
12699
|
+
await deps.writeSyncState(opts.home ?? homedir17(), {
|
|
12166
12700
|
mcp_bundles: { lastPulledVersion: putRes.version }
|
|
12167
12701
|
}).catch(() => {
|
|
12168
12702
|
});
|
|
12703
|
+
} else {
|
|
12704
|
+
io.err(
|
|
12705
|
+
`yaw-mcp set-active: putRes.version was not a number (got ${JSON.stringify(putRes.version)}); local sync-state not updated
|
|
12706
|
+
`
|
|
12707
|
+
);
|
|
12169
12708
|
}
|
|
12170
12709
|
return done(io, opts.json, namespace, active, true);
|
|
12171
12710
|
} catch (e) {
|
|
@@ -12203,7 +12742,7 @@ function fail(io, json, message, code) {
|
|
|
12203
12742
|
}
|
|
12204
12743
|
|
|
12205
12744
|
// src/stats-cmd.ts
|
|
12206
|
-
import { homedir as
|
|
12745
|
+
import { homedir as homedir18 } from "os";
|
|
12207
12746
|
var STATS_USAGE = `Usage: yaw-mcp stats [--json] [--limit N] [--days N]
|
|
12208
12747
|
|
|
12209
12748
|
Print a digest of recent AI tool calls recorded against your Yaw
|
|
@@ -12327,7 +12866,7 @@ async function runStats(opts, io = {
|
|
|
12327
12866
|
out: (s) => process.stdout.write(s),
|
|
12328
12867
|
err: (s) => process.stderr.write(s)
|
|
12329
12868
|
}) {
|
|
12330
|
-
const home = opts.home ??
|
|
12869
|
+
const home = opts.home ?? homedir18();
|
|
12331
12870
|
const session = await getSession({ home, baseUrl: opts.baseUrl });
|
|
12332
12871
|
if (!session) {
|
|
12333
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>";
|
|
@@ -12416,14 +12955,22 @@ function suggestSubcommand(input, limit = 3) {
|
|
|
12416
12955
|
}
|
|
12417
12956
|
function suggestFlag(input, limit = 2) {
|
|
12418
12957
|
if (input.length <= 2) return [];
|
|
12419
|
-
|
|
12958
|
+
const q = input.toLowerCase();
|
|
12959
|
+
const hits = [];
|
|
12960
|
+
for (const alias of FLAG_ALIASES) {
|
|
12961
|
+
if (alias.toLowerCase() === q) continue;
|
|
12962
|
+
const d = levenshtein(q, alias.toLowerCase());
|
|
12963
|
+
if (d <= 2) hits.push({ name: alias, d });
|
|
12964
|
+
}
|
|
12965
|
+
hits.sort((a, b) => a.d - b.d || a.name.localeCompare(b.name));
|
|
12966
|
+
return hits.slice(0, limit).map((h) => h.name);
|
|
12420
12967
|
}
|
|
12421
12968
|
|
|
12422
12969
|
// src/sync-cmd.ts
|
|
12423
|
-
import { existsSync as
|
|
12424
|
-
import { mkdir as mkdir6, readFile as
|
|
12425
|
-
import { homedir as
|
|
12426
|
-
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";
|
|
12427
12974
|
var SYNC_USAGE = `Usage: yaw-mcp sync <push|pull|status> [--json]
|
|
12428
12975
|
|
|
12429
12976
|
Replicate ~/.yaw-mcp/bundles.json across machines via your Yaw
|
|
@@ -12470,12 +13017,12 @@ ${SYNC_USAGE}` };
|
|
|
12470
13017
|
return { ok: true, options: opts };
|
|
12471
13018
|
}
|
|
12472
13019
|
function bundlesPath(home) {
|
|
12473
|
-
return
|
|
13020
|
+
return join13(home, CONFIG_DIRNAME, BUNDLES_FILENAME2);
|
|
12474
13021
|
}
|
|
12475
13022
|
async function readLocalBundles(home) {
|
|
12476
13023
|
const path5 = bundlesPath(home);
|
|
12477
|
-
if (!
|
|
12478
|
-
const raw = await
|
|
13024
|
+
if (!existsSync8(path5)) return { version: 1, servers: [] };
|
|
13025
|
+
const raw = await readFile14(path5, "utf8");
|
|
12479
13026
|
let parsed;
|
|
12480
13027
|
try {
|
|
12481
13028
|
parsed = JSON.parse(raw);
|
|
@@ -12526,7 +13073,7 @@ async function runSync(opts, io = {
|
|
|
12526
13073
|
out: (s) => process.stdout.write(s),
|
|
12527
13074
|
err: (s) => process.stderr.write(s)
|
|
12528
13075
|
}) {
|
|
12529
|
-
const home = opts.home ??
|
|
13076
|
+
const home = opts.home ?? homedir19();
|
|
12530
13077
|
const session = await getSession({ home, baseUrl: opts.baseUrl });
|
|
12531
13078
|
if (!session) {
|
|
12532
13079
|
const msg = "Not signed in. Run `yaw-mcp login --key <license-key>` first.";
|
|
@@ -12685,10 +13232,11 @@ function handleSyncError(err, opts, io) {
|
|
|
12685
13232
|
return { exitCode: 1 };
|
|
12686
13233
|
}
|
|
12687
13234
|
if (err instanceof TeamSyncAuthError) {
|
|
12688
|
-
|
|
12689
|
-
|
|
13235
|
+
const authMsg = "Session expired or revoked. Run `yaw-mcp login --key <license-key>` again.";
|
|
13236
|
+
if (opts.json) io.err(`${JSON.stringify({ ok: false, error: authMsg })}
|
|
13237
|
+
`);
|
|
13238
|
+
else io.err(`yaw-mcp sync: ${authMsg}
|
|
12690
13239
|
`);
|
|
12691
|
-
else io.err("yaw-mcp sync: session expired or revoked. Run `yaw-mcp login --key <license-key>` again.\n");
|
|
12692
13240
|
return { exitCode: 1 };
|
|
12693
13241
|
}
|
|
12694
13242
|
if (err instanceof TeamSyncForbiddenError) {
|
|
@@ -13100,7 +13648,7 @@ if (subcommand === "compliance") {
|
|
|
13100
13648
|
`);
|
|
13101
13649
|
process.exit(0);
|
|
13102
13650
|
} else if (subcommand === "--version" || subcommand === "-V") {
|
|
13103
|
-
process.stdout.write(`yaw-mcp ${true ? "0.
|
|
13651
|
+
process.stdout.write(`yaw-mcp ${true ? "0.65.0" : "dev"}
|
|
13104
13652
|
`);
|
|
13105
13653
|
process.exit(0);
|
|
13106
13654
|
} else if (subcommand && !subcommand.startsWith("-")) {
|