@yawlabs/mcp 0.60.6 → 0.62.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/CHANGELOG.md +23 -0
- package/README.md +2 -2
- package/dist/{chunk-I5625HJE.js → chunk-WORQOSXT.js} +12 -7
- package/dist/index.js +2106 -637
- package/dist/{team-sync-5356FJP6.js → team-sync-B4R6FLKR.js} +3 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
signIn,
|
|
18
18
|
signOut,
|
|
19
19
|
userConfigDir
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-WORQOSXT.js";
|
|
21
21
|
|
|
22
22
|
// src/audit-cmd.ts
|
|
23
23
|
import { homedir as homedir3 } from "os";
|
|
@@ -73,9 +73,49 @@ function stripJsoncComments(src) {
|
|
|
73
73
|
}
|
|
74
74
|
return out;
|
|
75
75
|
}
|
|
76
|
+
function stripTrailingCommas(src) {
|
|
77
|
+
let out = "";
|
|
78
|
+
let i = 0;
|
|
79
|
+
const len = src.length;
|
|
80
|
+
let inString = false;
|
|
81
|
+
let stringChar = "";
|
|
82
|
+
while (i < len) {
|
|
83
|
+
const c = src[i];
|
|
84
|
+
if (inString) {
|
|
85
|
+
out += c;
|
|
86
|
+
if (c === "\\" && i + 1 < len) {
|
|
87
|
+
out += src[i + 1];
|
|
88
|
+
i += 2;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (c === stringChar) inString = false;
|
|
92
|
+
i++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (c === '"' || c === "'") {
|
|
96
|
+
inString = true;
|
|
97
|
+
stringChar = c;
|
|
98
|
+
out += c;
|
|
99
|
+
i++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (c === ",") {
|
|
103
|
+
let j = i + 1;
|
|
104
|
+
while (j < len && (src[j] === " " || src[j] === " " || src[j] === "\r" || src[j] === "\n")) j++;
|
|
105
|
+
if (j < len && (src[j] === "]" || src[j] === "}")) {
|
|
106
|
+
out += " ";
|
|
107
|
+
i++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
out += c;
|
|
112
|
+
i++;
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
76
116
|
function parseJsonc(src) {
|
|
77
117
|
const debommed = src.charCodeAt(0) === 65279 ? src.slice(1) : src;
|
|
78
|
-
const stripped = stripJsoncComments(debommed);
|
|
118
|
+
const stripped = stripTrailingCommas(stripJsoncComments(debommed));
|
|
79
119
|
return JSON.parse(stripped);
|
|
80
120
|
}
|
|
81
121
|
|
|
@@ -97,10 +137,10 @@ function validateEntry(entry) {
|
|
|
97
137
|
return { grade, score, gradedAt };
|
|
98
138
|
}
|
|
99
139
|
async function readGradesCache(home = homedir()) {
|
|
100
|
-
const
|
|
140
|
+
const path5 = gradesCachePath(home);
|
|
101
141
|
let raw;
|
|
102
142
|
try {
|
|
103
|
-
raw = await readFile(
|
|
143
|
+
raw = await readFile(path5, "utf8");
|
|
104
144
|
} catch {
|
|
105
145
|
return {};
|
|
106
146
|
}
|
|
@@ -109,7 +149,7 @@ async function readGradesCache(home = homedir()) {
|
|
|
109
149
|
parsed = parseJsonc(raw);
|
|
110
150
|
} catch (err) {
|
|
111
151
|
log("warn", "grades.json is not valid JSON; ignoring", {
|
|
112
|
-
path:
|
|
152
|
+
path: path5,
|
|
113
153
|
error: err instanceof Error ? err.message : String(err)
|
|
114
154
|
});
|
|
115
155
|
return {};
|
|
@@ -123,12 +163,12 @@ async function readGradesCache(home = homedir()) {
|
|
|
123
163
|
return out;
|
|
124
164
|
}
|
|
125
165
|
async function writeGrade(namespace, grade, home = homedir()) {
|
|
126
|
-
const
|
|
166
|
+
const path5 = gradesCachePath(home);
|
|
127
167
|
const cache = await readGradesCache(home);
|
|
128
168
|
cache[namespace] = grade;
|
|
129
|
-
await atomicWriteFile(
|
|
169
|
+
await atomicWriteFile(path5, `${JSON.stringify(cache, null, 2)}
|
|
130
170
|
`);
|
|
131
|
-
return
|
|
171
|
+
return path5;
|
|
132
172
|
}
|
|
133
173
|
|
|
134
174
|
// src/local-bundles.ts
|
|
@@ -180,36 +220,43 @@ function validateEntry2(entry, warnings) {
|
|
|
180
220
|
description
|
|
181
221
|
};
|
|
182
222
|
}
|
|
183
|
-
async function readBundlesAt(
|
|
223
|
+
async function readBundlesAt(path5, warnings) {
|
|
184
224
|
let raw;
|
|
185
225
|
try {
|
|
186
|
-
raw = await readFile2(
|
|
187
|
-
} catch {
|
|
188
|
-
|
|
226
|
+
raw = await readFile2(path5, "utf8");
|
|
227
|
+
} catch (err) {
|
|
228
|
+
const code = err.code;
|
|
229
|
+
if (code === "ENOENT" || code === "EISDIR") {
|
|
230
|
+
return { exists: false, file: null };
|
|
231
|
+
}
|
|
232
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
233
|
+
warnings.push(`${path5}: could not read file (${msg}) -- skipping`);
|
|
234
|
+
log("warn", "Could not read bundles.json", { path: path5, error: msg, code });
|
|
235
|
+
return { exists: true, file: null };
|
|
189
236
|
}
|
|
190
237
|
let parsed;
|
|
191
238
|
try {
|
|
192
239
|
parsed = parseJsonc(raw);
|
|
193
240
|
} catch (err) {
|
|
194
241
|
const msg = err instanceof Error ? err.message : String(err);
|
|
195
|
-
warnings.push(`${
|
|
196
|
-
log("warn", "bundles.json is not valid JSON; ignoring", { path:
|
|
242
|
+
warnings.push(`${path5}: invalid JSON (${msg}) -- file ignored`);
|
|
243
|
+
log("warn", "bundles.json is not valid JSON; ignoring", { path: path5, error: msg });
|
|
197
244
|
return { exists: true, file: null };
|
|
198
245
|
}
|
|
199
246
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
200
|
-
warnings.push(`${
|
|
247
|
+
warnings.push(`${path5}: root must be a JSON object -- file ignored`);
|
|
201
248
|
return { exists: true, file: null };
|
|
202
249
|
}
|
|
203
250
|
const obj = parsed;
|
|
204
251
|
const version = typeof obj.version === "number" ? obj.version : void 0;
|
|
205
252
|
if (version !== void 0 && version > CURRENT_BUNDLES_SCHEMA_VERSION) {
|
|
206
253
|
warnings.push(
|
|
207
|
-
`${
|
|
254
|
+
`${path5}: schema version ${version} is newer than this yaw-mcp (${CURRENT_BUNDLES_SCHEMA_VERSION}); upgrade with \`npm i -g @yawlabs/mcp@latest\`. Loading best-effort.`
|
|
208
255
|
);
|
|
209
256
|
}
|
|
210
257
|
const rawServers = obj.servers;
|
|
211
258
|
if (!Array.isArray(rawServers)) {
|
|
212
|
-
warnings.push(`${
|
|
259
|
+
warnings.push(`${path5}: 'servers' must be an array -- file ignored`);
|
|
213
260
|
return { exists: true, file: null };
|
|
214
261
|
}
|
|
215
262
|
return {
|
|
@@ -257,6 +304,7 @@ async function loadLocalBundles(opts = {}) {
|
|
|
257
304
|
warnings
|
|
258
305
|
};
|
|
259
306
|
}
|
|
307
|
+
var bundleWriteChain = Promise.resolve();
|
|
260
308
|
function deriveNamespace(name) {
|
|
261
309
|
let ns = name.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
262
310
|
if (ns.length === 0) return "server";
|
|
@@ -265,21 +313,34 @@ function deriveNamespace(name) {
|
|
|
265
313
|
return ns;
|
|
266
314
|
}
|
|
267
315
|
async function readRawUserBundles(home) {
|
|
268
|
-
const
|
|
269
|
-
if (!existsSync(
|
|
316
|
+
const path5 = localBundlesPath(userConfigDir(home));
|
|
317
|
+
if (!existsSync(path5)) {
|
|
270
318
|
return { version: CURRENT_BUNDLES_SCHEMA_VERSION, servers: [] };
|
|
271
319
|
}
|
|
272
320
|
const warnings = [];
|
|
273
|
-
const r = await readBundlesAt(
|
|
321
|
+
const r = await readBundlesAt(path5, warnings);
|
|
274
322
|
if (!r.file) {
|
|
275
|
-
const
|
|
276
|
-
|
|
323
|
+
const warningText = warnings.join("; ");
|
|
324
|
+
const isReadError = /EPERM|EACCES|could not read/i.test(warningText);
|
|
325
|
+
if (isReadError) {
|
|
326
|
+
throw new Error(`${path5} could not be read (${warningText}) -- check file permissions before adding servers.`);
|
|
327
|
+
}
|
|
328
|
+
const detail = warnings.length > 0 ? ` (${warningText})` : "";
|
|
329
|
+
throw new Error(`${path5} could not be parsed -- fix the JSON${detail} before adding servers.`);
|
|
277
330
|
}
|
|
278
331
|
return { version: r.file.version ?? CURRENT_BUNDLES_SCHEMA_VERSION, servers: r.file.servers };
|
|
279
332
|
}
|
|
280
|
-
|
|
333
|
+
function upsertUserBundle(entry, opts = {}) {
|
|
334
|
+
const result = bundleWriteChain.then(() => doUpsertUserBundle(entry, opts));
|
|
335
|
+
bundleWriteChain = result.then(
|
|
336
|
+
() => void 0,
|
|
337
|
+
() => void 0
|
|
338
|
+
);
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
async function doUpsertUserBundle(entry, opts) {
|
|
281
342
|
const home = opts.home ?? homedir2();
|
|
282
|
-
const
|
|
343
|
+
const path5 = localBundlesPath(userConfigDir(home));
|
|
283
344
|
const file = await readRawUserBundles(home);
|
|
284
345
|
const idx = file.servers.findIndex(
|
|
285
346
|
(s) => s?.namespace === entry.namespace || entry.name != null && s?.name === entry.name
|
|
@@ -288,22 +349,30 @@ async function upsertUserBundle(entry, opts = {}) {
|
|
|
288
349
|
if (replaced) file.servers[idx] = entry;
|
|
289
350
|
else file.servers.push(entry);
|
|
290
351
|
file.version = file.version ?? CURRENT_BUNDLES_SCHEMA_VERSION;
|
|
291
|
-
await atomicWriteFile(
|
|
352
|
+
await atomicWriteFile(path5, `${JSON.stringify(file, null, 2)}
|
|
292
353
|
`);
|
|
293
|
-
return { path:
|
|
354
|
+
return { path: path5, replaced };
|
|
355
|
+
}
|
|
356
|
+
function removeUserBundle(namespace, opts = {}) {
|
|
357
|
+
const result = bundleWriteChain.then(() => doRemoveUserBundle(namespace, opts));
|
|
358
|
+
bundleWriteChain = result.then(
|
|
359
|
+
() => void 0,
|
|
360
|
+
() => void 0
|
|
361
|
+
);
|
|
362
|
+
return result;
|
|
294
363
|
}
|
|
295
|
-
async function
|
|
364
|
+
async function doRemoveUserBundle(namespace, opts) {
|
|
296
365
|
const home = opts.home ?? homedir2();
|
|
297
|
-
const
|
|
298
|
-
if (!existsSync(
|
|
366
|
+
const path5 = localBundlesPath(userConfigDir(home));
|
|
367
|
+
if (!existsSync(path5)) return { path: path5, removed: false };
|
|
299
368
|
const file = await readRawUserBundles(home);
|
|
300
369
|
const before = file.servers.length;
|
|
301
370
|
file.servers = file.servers.filter((s) => s?.namespace !== namespace);
|
|
302
|
-
if (file.servers.length === before) return { path:
|
|
371
|
+
if (file.servers.length === before) return { path: path5, removed: false };
|
|
303
372
|
file.version = file.version ?? CURRENT_BUNDLES_SCHEMA_VERSION;
|
|
304
|
-
await atomicWriteFile(
|
|
373
|
+
await atomicWriteFile(path5, `${JSON.stringify(file, null, 2)}
|
|
305
374
|
`);
|
|
306
|
-
return { path:
|
|
375
|
+
return { path: path5, removed: true };
|
|
307
376
|
}
|
|
308
377
|
async function findShadowingProjectBundles(cwd, home = homedir2()) {
|
|
309
378
|
const projectDir = await findProjectConfigDir(cwd, home).catch(() => null);
|
|
@@ -385,11 +454,11 @@ async function runAudit(opts = {}) {
|
|
|
385
454
|
return { exitCode: 1, lines };
|
|
386
455
|
}
|
|
387
456
|
const home = opts.home ?? homedir3();
|
|
388
|
-
const { config, path:
|
|
457
|
+
const { config, path: path5 } = await loadLocalBundles({ cwd: opts.cwd, home });
|
|
389
458
|
const servers = config?.servers ?? [];
|
|
390
459
|
const server = findServer(servers, namespace);
|
|
391
460
|
if (!server) {
|
|
392
|
-
const where =
|
|
461
|
+
const where = path5 ? ` (${path5})` : "";
|
|
393
462
|
printErr(
|
|
394
463
|
`yaw-mcp audit: no server named "${namespace}" in bundles.json${where}. Run \`yaw-mcp list\` to see configured servers.`
|
|
395
464
|
);
|
|
@@ -495,7 +564,7 @@ function matchBundles(installedNamespaces) {
|
|
|
495
564
|
return { ready, partial };
|
|
496
565
|
}
|
|
497
566
|
function bundleActivateHint(bundle) {
|
|
498
|
-
return `mcp_connect_activate({
|
|
567
|
+
return `mcp_connect_activate({ servers: ${JSON.stringify(bundle.namespaces)} })`;
|
|
499
568
|
}
|
|
500
569
|
function topPartialBundles(installedNamespaces, limit) {
|
|
501
570
|
if (limit <= 0) return [];
|
|
@@ -510,19 +579,19 @@ function topPartialBundles(installedNamespaces, limit) {
|
|
|
510
579
|
// src/config-loader.ts
|
|
511
580
|
import { readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
512
581
|
import { homedir as homedir4 } from "os";
|
|
513
|
-
import { join as join4, resolve } from "path";
|
|
582
|
+
import { join as join4, resolve as resolve2 } from "path";
|
|
514
583
|
|
|
515
584
|
// src/migrate.ts
|
|
516
585
|
import { mkdir, rename, stat } from "fs/promises";
|
|
517
|
-
import { dirname, join as join3 } from "path";
|
|
586
|
+
import { dirname, join as join3, resolve } from "path";
|
|
518
587
|
var LEGACY_GLOBAL_FILENAME = ".yaw-mcp.json";
|
|
519
588
|
var LEGACY_PROJECT_FILENAME = ".yaw-mcp.json";
|
|
520
589
|
var LEGACY_LOCAL_FILENAME = ".yaw-mcp.local.json";
|
|
521
590
|
var NEW_CONFIG_FILENAME = "config.json";
|
|
522
591
|
var NEW_LOCAL_FILENAME = "config.local.json";
|
|
523
|
-
async function exists(
|
|
592
|
+
async function exists(path5) {
|
|
524
593
|
try {
|
|
525
|
-
await stat(
|
|
594
|
+
await stat(path5);
|
|
526
595
|
return true;
|
|
527
596
|
} catch {
|
|
528
597
|
return false;
|
|
@@ -573,9 +642,8 @@ async function migrateLegacyConfigPaths(opts) {
|
|
|
573
642
|
}
|
|
574
643
|
}
|
|
575
644
|
async function findLegacyProjectRoot(cwd, home) {
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
let dir = resolve5(cwd);
|
|
645
|
+
const homeResolved = resolve(home);
|
|
646
|
+
let dir = resolve(cwd);
|
|
579
647
|
let prev = "";
|
|
580
648
|
while (dir !== prev) {
|
|
581
649
|
if (dir === homeResolved) return null;
|
|
@@ -583,7 +651,7 @@ async function findLegacyProjectRoot(cwd, home) {
|
|
|
583
651
|
const legacyLocal = join3(dir, LEGACY_LOCAL_FILENAME);
|
|
584
652
|
if (await exists(legacyProject) || await exists(legacyLocal)) return dir;
|
|
585
653
|
prev = dir;
|
|
586
|
-
dir =
|
|
654
|
+
dir = dirname(dir);
|
|
587
655
|
}
|
|
588
656
|
return null;
|
|
589
657
|
}
|
|
@@ -593,10 +661,10 @@ var CONFIG_FILENAME = "config.json";
|
|
|
593
661
|
var LOCAL_CONFIG_FILENAME = "config.local.json";
|
|
594
662
|
var CURRENT_SCHEMA_VERSION = 1;
|
|
595
663
|
var DEFAULT_API_BASE = "https://yaw.sh/mcp";
|
|
596
|
-
async function readConfigAt(
|
|
664
|
+
async function readConfigAt(path5, scope, warnings) {
|
|
597
665
|
let raw;
|
|
598
666
|
try {
|
|
599
|
-
raw = await readFile3(
|
|
667
|
+
raw = await readFile3(path5, "utf8");
|
|
600
668
|
} catch {
|
|
601
669
|
return null;
|
|
602
670
|
}
|
|
@@ -605,19 +673,19 @@ async function readConfigAt(path3, scope, warnings) {
|
|
|
605
673
|
parsed = parseJsonc(raw);
|
|
606
674
|
} catch (err) {
|
|
607
675
|
const msg = err instanceof Error ? err.message : String(err);
|
|
608
|
-
warnings.push(`${
|
|
609
|
-
log("warn", "Config file is not valid JSON; ignoring", { path:
|
|
676
|
+
warnings.push(`${path5}: invalid JSON (${msg}) \u2014 file ignored`);
|
|
677
|
+
log("warn", "Config file is not valid JSON; ignoring", { path: path5, error: msg });
|
|
610
678
|
return null;
|
|
611
679
|
}
|
|
612
680
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
613
|
-
warnings.push(`${
|
|
681
|
+
warnings.push(`${path5}: root must be a JSON object \u2014 file ignored`);
|
|
614
682
|
return null;
|
|
615
683
|
}
|
|
616
684
|
const obj = parsed;
|
|
617
685
|
const version = typeof obj.version === "number" ? obj.version : void 0;
|
|
618
686
|
if (version !== void 0 && version > CURRENT_SCHEMA_VERSION) {
|
|
619
687
|
warnings.push(
|
|
620
|
-
`${
|
|
688
|
+
`${path5}: schema version ${version} is newer than this yaw-mcp (${CURRENT_SCHEMA_VERSION}); upgrade with \`npm i -g @yawlabs/mcp@latest\`. Loading best-effort.`
|
|
621
689
|
);
|
|
622
690
|
}
|
|
623
691
|
const token5 = typeof obj.token === "string" && obj.token.length > 0 ? obj.token : void 0;
|
|
@@ -627,21 +695,21 @@ async function readConfigAt(path3, scope, warnings) {
|
|
|
627
695
|
if (token5) {
|
|
628
696
|
if (scope === "project") {
|
|
629
697
|
warnings.push(
|
|
630
|
-
`${
|
|
698
|
+
`${path5}: 'token' found in a project-shared config file is IGNORED -- yaw-mcp never reads a token from this scope to avoid committing credentials. Move it to ${CONFIG_DIRNAME}/${LOCAL_CONFIG_FILENAME} (machine-local, gitignore by convention) or ~/${CONFIG_DIRNAME}/${CONFIG_FILENAME} (user-global).`
|
|
631
699
|
);
|
|
632
700
|
}
|
|
633
|
-
await checkPermissions(
|
|
701
|
+
await checkPermissions(path5, warnings);
|
|
634
702
|
}
|
|
635
|
-
return { path:
|
|
703
|
+
return { path: path5, scope, version, token: token5, apiBase, servers, blocked };
|
|
636
704
|
}
|
|
637
|
-
async function checkPermissions(
|
|
705
|
+
async function checkPermissions(path5, warnings) {
|
|
638
706
|
if (process.platform === "win32") return;
|
|
639
707
|
try {
|
|
640
|
-
const st = await stat2(
|
|
708
|
+
const st = await stat2(path5);
|
|
641
709
|
const mode = st.mode & 511;
|
|
642
710
|
if ((mode & 63) !== 0) {
|
|
643
711
|
warnings.push(
|
|
644
|
-
`${
|
|
712
|
+
`${path5}: contains a token but is readable by group/other (mode ${mode.toString(8)}). Run \`chmod 600 ${path5}\` to restrict.`
|
|
645
713
|
);
|
|
646
714
|
}
|
|
647
715
|
} catch {
|
|
@@ -666,8 +734,8 @@ function unionBlocked(files) {
|
|
|
666
734
|
return touched ? [...set] : void 0;
|
|
667
735
|
}
|
|
668
736
|
async function loadYawMcpConfig(opts = {}) {
|
|
669
|
-
const cwd =
|
|
670
|
-
const home =
|
|
737
|
+
const cwd = resolve2(opts.cwd ?? process.cwd());
|
|
738
|
+
const home = resolve2(opts.home ?? homedir4());
|
|
671
739
|
const env = opts.env ?? process.env;
|
|
672
740
|
const warnings = [];
|
|
673
741
|
const loadedFiles = [];
|
|
@@ -684,7 +752,11 @@ async function loadYawMcpConfig(opts = {}) {
|
|
|
684
752
|
const globalPath = join4(globalDir, CONFIG_FILENAME);
|
|
685
753
|
const local = localPath ? await readConfigAt(localPath, "local", warnings) : null;
|
|
686
754
|
if (local) loadedFiles.push(local);
|
|
687
|
-
const
|
|
755
|
+
const normalizeDir = (d) => {
|
|
756
|
+
const r = resolve2(d);
|
|
757
|
+
return process.platform === "win32" ? r.toLowerCase() : r;
|
|
758
|
+
};
|
|
759
|
+
const projectIsGlobal = projectConfigDir !== null && normalizeDir(projectConfigDir) === normalizeDir(globalDir);
|
|
688
760
|
const project = projectIsGlobal || !projectPath ? null : await readConfigAt(projectPath, "project", warnings);
|
|
689
761
|
if (project) loadedFiles.push(project);
|
|
690
762
|
const global = await readConfigAt(globalPath, "global", warnings);
|
|
@@ -804,9 +876,9 @@ async function fetchConfig(apiUrl5, token5, currentVersion) {
|
|
|
804
876
|
await res.body.text().catch(() => {
|
|
805
877
|
});
|
|
806
878
|
throw new ConfigError(
|
|
807
|
-
`Access denied (HTTP 403)
|
|
808
|
-
The account may be suspended or the token scope reduced
|
|
809
|
-
https://yaw.sh/mcp/dashboard/settings/tokens, or reach support@
|
|
879
|
+
`Access denied (HTTP 403) -- the token ${tokenFingerprint(token5)} was accepted but lacks permission to read this account's servers.
|
|
880
|
+
The account may be suspended or the token scope reduced -- check
|
|
881
|
+
https://yaw.sh/mcp/dashboard/settings/tokens, or reach support@yaw.sh.`,
|
|
810
882
|
true
|
|
811
883
|
);
|
|
812
884
|
}
|
|
@@ -825,9 +897,8 @@ async function fetchConfig(apiUrl5, token5, currentVersion) {
|
|
|
825
897
|
}
|
|
826
898
|
return true;
|
|
827
899
|
});
|
|
828
|
-
const NAMESPACE_RE3 = /^[a-z][a-z0-9_]{0,29}$/;
|
|
829
900
|
data.servers = data.servers.filter((s) => {
|
|
830
|
-
if (!s.namespace || !
|
|
901
|
+
if (!s.namespace || !NAMESPACE_RE.test(s.namespace)) {
|
|
831
902
|
log("warn", "Skipping server with invalid namespace", { namespace: s.namespace, name: s.name });
|
|
832
903
|
return false;
|
|
833
904
|
}
|
|
@@ -862,7 +933,7 @@ function parseBundlesArgs(argv) {
|
|
|
862
933
|
if (a === "--json") {
|
|
863
934
|
json = true;
|
|
864
935
|
} else if (a === "--help" || a === "-h") {
|
|
865
|
-
return { ok: false, error: BUNDLES_USAGE };
|
|
936
|
+
return { ok: false, error: BUNDLES_USAGE, help: true };
|
|
866
937
|
} else if (a === "list" || a === "match") {
|
|
867
938
|
if (actionSet) {
|
|
868
939
|
return {
|
|
@@ -926,7 +997,7 @@ async function runBundlesCommand(opts = {}) {
|
|
|
926
997
|
return { exitCode: 2, lines };
|
|
927
998
|
}
|
|
928
999
|
if (!backend) {
|
|
929
|
-
printErr("yaw-mcp bundles match: backend returned
|
|
1000
|
+
printErr("yaw-mcp bundles match: backend returned 304 without a conditional request.");
|
|
930
1001
|
return { exitCode: 2, lines };
|
|
931
1002
|
}
|
|
932
1003
|
const installed = backend.servers.filter((s) => s.isActive).map((s) => s.namespace);
|
|
@@ -1065,20 +1136,27 @@ var SUBCOMMAND_SPEC = [
|
|
|
1065
1136
|
positional: ["push", "pull", "status"],
|
|
1066
1137
|
flags: ["--key", "--json", "--help"]
|
|
1067
1138
|
},
|
|
1068
|
-
{ name: "stats", description: "Show usage statistics", flags: ["--
|
|
1139
|
+
{ name: "stats", description: "Show usage statistics", flags: ["--limit", "--days", "--json", "--help"] },
|
|
1069
1140
|
{
|
|
1070
1141
|
name: "secrets",
|
|
1071
1142
|
description: "Manage stored secrets",
|
|
1072
1143
|
positional: ["set", "get", "list", "remove", "lock", "push", "pull"],
|
|
1073
1144
|
flags: ["--key", "--value", "--stdin", "--json", "--help"]
|
|
1074
1145
|
},
|
|
1146
|
+
{
|
|
1147
|
+
name: "set-active",
|
|
1148
|
+
description: "Enable/disable a team server (authoritative)",
|
|
1149
|
+
positional: ["<namespace>", "on", "off"],
|
|
1150
|
+
flags: ["--json", "--help"]
|
|
1151
|
+
},
|
|
1075
1152
|
// Other.
|
|
1153
|
+
{ name: "audit", description: "Run a full-pass audit of loaded servers", flags: ["--json", "--help"] },
|
|
1076
1154
|
{ name: "compliance", description: "Run the compliance suite against a server", flags: ["--publish", "--help"] },
|
|
1077
1155
|
{ name: "help", description: "Show usage", flags: [] }
|
|
1078
1156
|
];
|
|
1079
1157
|
function parseCompletionArgs(argv) {
|
|
1080
1158
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
1081
|
-
return { ok: false, error: COMPLETION_USAGE };
|
|
1159
|
+
return { ok: false, error: COMPLETION_USAGE, help: true };
|
|
1082
1160
|
}
|
|
1083
1161
|
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
1084
1162
|
if (positional.length === 0) {
|
|
@@ -1275,10 +1353,16 @@ async function runComplianceCommand(argv) {
|
|
|
1275
1353
|
if (publish) {
|
|
1276
1354
|
const result = await publishReport(apiUrl5, report);
|
|
1277
1355
|
if (!result) return 1;
|
|
1356
|
+
const reportUrl = typeof result.reportUrl === "string" ? result.reportUrl.trim() : "";
|
|
1357
|
+
const badgeUrl = typeof result.badgeUrl === "string" ? result.badgeUrl.trim() : "";
|
|
1358
|
+
if (!reportUrl || !badgeUrl) {
|
|
1359
|
+
process.stderr.write("\nPublish failed: server returned 200 but no report/badge URL.\n");
|
|
1360
|
+
return 1;
|
|
1361
|
+
}
|
|
1278
1362
|
process.stdout.write(`
|
|
1279
|
-
Published: ${
|
|
1363
|
+
Published: ${reportUrl}
|
|
1280
1364
|
`);
|
|
1281
|
-
process.stdout.write(`Badge: ${
|
|
1365
|
+
process.stdout.write(`Badge: ${badgeUrl}
|
|
1282
1366
|
`);
|
|
1283
1367
|
if (result.deleteToken) {
|
|
1284
1368
|
process.stdout.write(`
|
|
@@ -1289,7 +1373,7 @@ Delete token (save this): ${result.deleteToken}
|
|
|
1289
1373
|
return 0;
|
|
1290
1374
|
}
|
|
1291
1375
|
function runTest(args) {
|
|
1292
|
-
return new Promise((
|
|
1376
|
+
return new Promise((resolve7) => {
|
|
1293
1377
|
const child = spawn("npx", ["-y", "@yawlabs/mcp-compliance", "test", "--format", "json", ...args], {
|
|
1294
1378
|
stdio: ["ignore", "pipe", "inherit"],
|
|
1295
1379
|
shell: process.platform === "win32"
|
|
@@ -1302,7 +1386,7 @@ function runTest(args) {
|
|
|
1302
1386
|
process.stderr.write(`
|
|
1303
1387
|
Failed to launch mcp-compliance: ${err.message}
|
|
1304
1388
|
`);
|
|
1305
|
-
|
|
1389
|
+
resolve7(null);
|
|
1306
1390
|
});
|
|
1307
1391
|
child.on("close", (code) => {
|
|
1308
1392
|
try {
|
|
@@ -1311,15 +1395,15 @@ Failed to launch mcp-compliance: ${err.message}
|
|
|
1311
1395
|
process.stderr.write(`
|
|
1312
1396
|
mcp-compliance returned unexpected JSON (exit ${code}).
|
|
1313
1397
|
`);
|
|
1314
|
-
|
|
1398
|
+
resolve7(null);
|
|
1315
1399
|
return;
|
|
1316
1400
|
}
|
|
1317
|
-
|
|
1401
|
+
resolve7(parsed);
|
|
1318
1402
|
} catch {
|
|
1319
1403
|
process.stderr.write(`
|
|
1320
1404
|
mcp-compliance exited ${code} without valid JSON output.
|
|
1321
1405
|
`);
|
|
1322
|
-
|
|
1406
|
+
resolve7(null);
|
|
1323
1407
|
}
|
|
1324
1408
|
});
|
|
1325
1409
|
});
|
|
@@ -1328,7 +1412,7 @@ function printSummary(report) {
|
|
|
1328
1412
|
const { grade, score, summary, url } = report;
|
|
1329
1413
|
process.stdout.write(
|
|
1330
1414
|
`
|
|
1331
|
-
Compliance: ${grade} (${score.toFixed(1)}%)
|
|
1415
|
+
Compliance: ${grade} (${score.toFixed(1)}%) -- ${summary.passed}/${summary.total} passed, ${summary.requiredPassed}/${summary.required} required
|
|
1332
1416
|
Target: ${url}
|
|
1333
1417
|
`
|
|
1334
1418
|
);
|
|
@@ -1343,15 +1427,16 @@ async function publishReport(apiUrl5, report) {
|
|
|
1343
1427
|
if (res.statusCode !== 200) {
|
|
1344
1428
|
const body = await res.body.text().catch(() => "");
|
|
1345
1429
|
process.stderr.write(`
|
|
1346
|
-
Publish failed: HTTP ${res.statusCode}${body ? `
|
|
1430
|
+
Publish failed: HTTP ${res.statusCode}${body ? ` -- ${body}` : ""}
|
|
1347
1431
|
`);
|
|
1348
1432
|
return null;
|
|
1349
1433
|
}
|
|
1350
1434
|
const parsed = await res.body.json();
|
|
1351
1435
|
return parsed;
|
|
1352
1436
|
} catch (err) {
|
|
1437
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1353
1438
|
process.stderr.write(`
|
|
1354
|
-
Publish failed: ${
|
|
1439
|
+
Publish failed: ${message}
|
|
1355
1440
|
`);
|
|
1356
1441
|
return null;
|
|
1357
1442
|
}
|
|
@@ -1359,7 +1444,7 @@ Publish failed: ${err?.message ?? String(err)}
|
|
|
1359
1444
|
|
|
1360
1445
|
// src/doctor-cmd.ts
|
|
1361
1446
|
import { existsSync as existsSync4, readFileSync, statSync } from "fs";
|
|
1362
|
-
import { readFile as readFile7 } from "fs/promises";
|
|
1447
|
+
import { readFile as readFile7, stat as stat3 } from "fs/promises";
|
|
1363
1448
|
import { homedir as homedir8 } from "os";
|
|
1364
1449
|
import { join as join8 } from "path";
|
|
1365
1450
|
|
|
@@ -1494,6 +1579,7 @@ function initAnalytics(url, tok) {
|
|
|
1494
1579
|
token = tok;
|
|
1495
1580
|
lastLoggedConnectStatus = null;
|
|
1496
1581
|
lastLoggedDispatchStatus = null;
|
|
1582
|
+
teamAnalyticsDisabled = false;
|
|
1497
1583
|
flushTimer = setInterval(() => {
|
|
1498
1584
|
flush().catch(() => {
|
|
1499
1585
|
});
|
|
@@ -1669,7 +1755,7 @@ function formatShadowLine(server) {
|
|
|
1669
1755
|
|
|
1670
1756
|
// src/install-targets.ts
|
|
1671
1757
|
import { homedir as homedir5 } from "os";
|
|
1672
|
-
import { join as join5 } from "path";
|
|
1758
|
+
import { isAbsolute, join as join5, resolve as resolve3 } from "path";
|
|
1673
1759
|
var CURRENT_OS = process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux";
|
|
1674
1760
|
var INSTALL_TARGETS = [
|
|
1675
1761
|
{
|
|
@@ -1763,10 +1849,11 @@ function resolveInstallPath(opts) {
|
|
|
1763
1849
|
if (scopeSpec.requiresProjectDir && !projectDir) {
|
|
1764
1850
|
throw new Error(`Scope ${scope} for ${clientId} requires a project directory`);
|
|
1765
1851
|
}
|
|
1852
|
+
const absoluteProjectDir = projectDir && !isAbsolute(projectDir) ? resolve3(projectDir) : projectDir;
|
|
1766
1853
|
const p = pathFor(clientId, scope, os, {
|
|
1767
1854
|
home,
|
|
1768
1855
|
appData,
|
|
1769
|
-
projectDir:
|
|
1856
|
+
projectDir: absoluteProjectDir ?? "",
|
|
1770
1857
|
claudeConfigDir: claudeConfigDir && claudeConfigDir.length > 0 ? claudeConfigDir : void 0
|
|
1771
1858
|
});
|
|
1772
1859
|
return p;
|
|
@@ -1929,7 +2016,8 @@ function sanitizeLearning(input) {
|
|
|
1929
2016
|
if (typeof u.dispatched !== "number" || !Number.isFinite(u.dispatched) || u.dispatched < 0) continue;
|
|
1930
2017
|
if (typeof u.succeeded !== "number" || !Number.isFinite(u.succeeded) || u.succeeded < 0) continue;
|
|
1931
2018
|
if (typeof u.lastUsedAt !== "number" || !Number.isFinite(u.lastUsedAt) || u.lastUsedAt < 0) continue;
|
|
1932
|
-
|
|
2019
|
+
const succeeded = Math.min(u.succeeded, u.dispatched);
|
|
2020
|
+
out[k] = { dispatched: u.dispatched, succeeded, lastUsedAt: u.lastUsedAt };
|
|
1933
2021
|
}
|
|
1934
2022
|
return out;
|
|
1935
2023
|
}
|
|
@@ -1997,7 +2085,7 @@ import { createHash as createHash2 } from "crypto";
|
|
|
1997
2085
|
import { existsSync as existsSync3 } from "fs";
|
|
1998
2086
|
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile6, readdir, unlink } from "fs/promises";
|
|
1999
2087
|
import { homedir as homedir7, hostname, userInfo } from "os";
|
|
2000
|
-
import { join as join7, resolve as
|
|
2088
|
+
import { join as join7, resolve as resolve5 } from "path";
|
|
2001
2089
|
import { request as request5 } from "undici";
|
|
2002
2090
|
|
|
2003
2091
|
// src/catalog.ts
|
|
@@ -2099,7 +2187,7 @@ async function resolveCatalogSlug(slug, opts = {}) {
|
|
|
2099
2187
|
import { existsSync as existsSync2 } from "fs";
|
|
2100
2188
|
import { chmod, readFile as readFile5 } from "fs/promises";
|
|
2101
2189
|
import { homedir as homedir6 } from "os";
|
|
2102
|
-
import { join as join6, resolve as
|
|
2190
|
+
import { join as join6, resolve as resolve4 } from "path";
|
|
2103
2191
|
import { createInterface } from "readline/promises";
|
|
2104
2192
|
var USAGE = "Usage: yaw-mcp install <claude-code|claude-desktop|cursor|vscode> [--scope user|project|local]\n [--token <mcp_pat_\u2026>] [--project-dir <path>] [--os macos|linux|windows]\n [--force | --skip] [--dry-run] [--no-yaw-mcp-config]\n yaw-mcp install --list (detect clients; no writes)\n yaw-mcp install --all [--token <mcp_pat_\u2026>] (install into every detected client)";
|
|
2105
2193
|
async function runInstall(opts) {
|
|
@@ -2152,7 +2240,7 @@ ${USAGE}`);
|
|
|
2152
2240
|
);
|
|
2153
2241
|
return { written: [], wouldWrite: [], messages, exitCode: 2 };
|
|
2154
2242
|
}
|
|
2155
|
-
const projectDir = scopeSpec.requiresProjectDir ?
|
|
2243
|
+
const projectDir = scopeSpec.requiresProjectDir ? resolve4(opts.projectDir ?? process.cwd()) : void 0;
|
|
2156
2244
|
let resolved;
|
|
2157
2245
|
try {
|
|
2158
2246
|
resolved = resolveInstallPath({
|
|
@@ -2221,7 +2309,7 @@ ${USAGE}`);
|
|
|
2221
2309
|
if (opts.force) decision = "overwrite";
|
|
2222
2310
|
else if (opts.skip) decision = "skip";
|
|
2223
2311
|
else if (opts.promptAnswer) decision = opts.promptAnswer;
|
|
2224
|
-
else if (opts.io?.isTTY ?? process.stdout.isTTY) {
|
|
2312
|
+
else if (opts.io?.isTTY ?? (Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY))) {
|
|
2225
2313
|
decision = await promptCollision(resolved.absolute, opts.io);
|
|
2226
2314
|
} else {
|
|
2227
2315
|
err(
|
|
@@ -2323,38 +2411,38 @@ ${settingsPatch.nextJson}`);
|
|
|
2323
2411
|
);
|
|
2324
2412
|
}
|
|
2325
2413
|
log2(`
|
|
2326
|
-
|
|
2414
|
+
Done: ${target.label} is configured. Restart it to pick up the new MCP server.`);
|
|
2327
2415
|
return { written, wouldWrite: [], messages, exitCode: 0 };
|
|
2328
2416
|
}
|
|
2329
2417
|
async function prepareClaudeCodeSettingsPatch(opts) {
|
|
2330
|
-
const
|
|
2418
|
+
const path5 = resolveClaudeCodeSettingsPath(opts.scope, {
|
|
2331
2419
|
home: opts.home,
|
|
2332
2420
|
projectDir: opts.projectDir,
|
|
2333
2421
|
os: opts.os,
|
|
2334
2422
|
claudeConfigDir: opts.claudeConfigDir
|
|
2335
2423
|
});
|
|
2336
|
-
if (!
|
|
2424
|
+
if (!path5) return null;
|
|
2337
2425
|
let existing = {};
|
|
2338
|
-
if (existsSync2(
|
|
2426
|
+
if (existsSync2(path5)) {
|
|
2339
2427
|
try {
|
|
2340
|
-
const raw = await readFile5(
|
|
2428
|
+
const raw = await readFile5(path5, "utf8");
|
|
2341
2429
|
if (raw.trim().length > 0) {
|
|
2342
2430
|
const parsed = parseJsonc(raw);
|
|
2343
2431
|
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
2344
2432
|
existing = parsed;
|
|
2345
2433
|
} else {
|
|
2346
|
-
return { path:
|
|
2434
|
+
return { path: path5, nextJson: "", changed: false };
|
|
2347
2435
|
}
|
|
2348
2436
|
}
|
|
2349
2437
|
} catch {
|
|
2350
|
-
return { path:
|
|
2438
|
+
return { path: path5, nextJson: "", changed: false };
|
|
2351
2439
|
}
|
|
2352
2440
|
}
|
|
2353
2441
|
const merged = mergePermissionsAllow(existing, [CLAUDE_CODE_ALLOW_PATTERN]);
|
|
2354
2442
|
const before = JSON.stringify(existing);
|
|
2355
2443
|
const after = JSON.stringify(merged);
|
|
2356
|
-
if (before === after) return { path:
|
|
2357
|
-
return { path:
|
|
2444
|
+
if (before === after) return { path: path5, nextJson: "", changed: false };
|
|
2445
|
+
return { path: path5, nextJson: `${JSON.stringify(merged, null, 2)}
|
|
2358
2446
|
`, changed: true };
|
|
2359
2447
|
}
|
|
2360
2448
|
var LEGACY_CLAUDE_CODE_ALLOW_PATTERNS = ["mcp__mcp_hosting__*", "mcp__yaw_mcp__*"];
|
|
@@ -2373,13 +2461,13 @@ function mergePermissionsAllow(existing, patterns) {
|
|
|
2373
2461
|
out.permissions = perms;
|
|
2374
2462
|
return out;
|
|
2375
2463
|
}
|
|
2376
|
-
async function promptCollision(
|
|
2464
|
+
async function promptCollision(path5, io) {
|
|
2377
2465
|
const stdin = io?.stdin ?? process.stdin;
|
|
2378
2466
|
const stdout = io?.stdout ?? process.stdout;
|
|
2379
2467
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
2380
2468
|
try {
|
|
2381
2469
|
const answer = (await rl.question(
|
|
2382
|
-
`${
|
|
2470
|
+
`${path5} already has an "${ENTRY_NAME}" entry.
|
|
2383
2471
|
[o]verwrite, [s]kip, or [a]bort? (default: skip) `
|
|
2384
2472
|
)).trim().toLowerCase();
|
|
2385
2473
|
if (answer.startsWith("o")) return "overwrite";
|
|
@@ -2439,13 +2527,13 @@ function removeFromClientConfig(existing, containerPath, entryName) {
|
|
|
2439
2527
|
parent[leafKey] = container;
|
|
2440
2528
|
return out;
|
|
2441
2529
|
}
|
|
2442
|
-
async function composeYawMcpConfig(
|
|
2530
|
+
async function composeYawMcpConfig(path5, token5) {
|
|
2443
2531
|
let existing = {};
|
|
2444
2532
|
let backupPath;
|
|
2445
|
-
if (existsSync2(
|
|
2533
|
+
if (existsSync2(path5)) {
|
|
2446
2534
|
let raw = "";
|
|
2447
2535
|
try {
|
|
2448
|
-
raw = await readFile5(
|
|
2536
|
+
raw = await readFile5(path5, "utf8");
|
|
2449
2537
|
} catch {
|
|
2450
2538
|
raw = "";
|
|
2451
2539
|
}
|
|
@@ -2454,9 +2542,16 @@ async function composeYawMcpConfig(path3, token5) {
|
|
|
2454
2542
|
const parsed = parseJsonc(raw);
|
|
2455
2543
|
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
2456
2544
|
existing = parsed;
|
|
2545
|
+
} else {
|
|
2546
|
+
const candidate = `${path5}.bak-${Date.now()}`;
|
|
2547
|
+
try {
|
|
2548
|
+
await atomicWriteFile(candidate, raw);
|
|
2549
|
+
backupPath = candidate;
|
|
2550
|
+
} catch {
|
|
2551
|
+
}
|
|
2457
2552
|
}
|
|
2458
2553
|
} catch {
|
|
2459
|
-
const candidate = `${
|
|
2554
|
+
const candidate = `${path5}.bak-${Date.now()}`;
|
|
2460
2555
|
try {
|
|
2461
2556
|
await atomicWriteFile(candidate, raw);
|
|
2462
2557
|
backupPath = candidate;
|
|
@@ -2525,7 +2620,7 @@ function parseInstallArgs(argv) {
|
|
|
2525
2620
|
break;
|
|
2526
2621
|
case "-h":
|
|
2527
2622
|
case "--help":
|
|
2528
|
-
return { ok: false, error: USAGE };
|
|
2623
|
+
return { ok: false, error: USAGE, help: true };
|
|
2529
2624
|
default:
|
|
2530
2625
|
if (a.startsWith("--")) return { ok: false, error: `Unknown flag: ${a}
|
|
2531
2626
|
${USAGE}` };
|
|
@@ -2660,7 +2755,7 @@ async function runInstallAll(opts, log2, err) {
|
|
|
2660
2755
|
}
|
|
2661
2756
|
const totalPlanned = plans.length;
|
|
2662
2757
|
if (failed === 0) {
|
|
2663
|
-
log2(
|
|
2758
|
+
log2(`Done: ${succeeded}/${totalPlanned} clients installed successfully.`);
|
|
2664
2759
|
return {
|
|
2665
2760
|
written: aggregateWritten,
|
|
2666
2761
|
wouldWrite: aggregateWouldWrite,
|
|
@@ -2755,7 +2850,7 @@ function parseTryArgs(argv) {
|
|
|
2755
2850
|
}
|
|
2756
2851
|
case "-h":
|
|
2757
2852
|
case "--help":
|
|
2758
|
-
return { ok: false, error: TRY_USAGE };
|
|
2853
|
+
return { ok: false, error: TRY_USAGE, help: true };
|
|
2759
2854
|
default:
|
|
2760
2855
|
if (a.startsWith("--")) return { ok: false, error: `Unknown flag: ${a}
|
|
2761
2856
|
${TRY_USAGE}` };
|
|
@@ -2776,7 +2871,7 @@ function parseTryCleanupArgs(argv) {
|
|
|
2776
2871
|
const positional = [];
|
|
2777
2872
|
for (let i = 0; i < argv.length; i++) {
|
|
2778
2873
|
const a = argv[i];
|
|
2779
|
-
if (a === "-h" || a === "--help") return { ok: false, error: TRY_CLEANUP_USAGE };
|
|
2874
|
+
if (a === "-h" || a === "--help") return { ok: false, error: TRY_CLEANUP_USAGE, help: true };
|
|
2780
2875
|
if (a === "--base") {
|
|
2781
2876
|
const v = argv[++i];
|
|
2782
2877
|
if (!v) return { ok: false, error: "--base requires a URL" };
|
|
@@ -2822,10 +2917,10 @@ function computeAnonId() {
|
|
|
2822
2917
|
return h.digest("hex").slice(0, 16);
|
|
2823
2918
|
}
|
|
2824
2919
|
async function loadOrCreateAnonId(home = homedir7()) {
|
|
2825
|
-
const
|
|
2826
|
-
if (existsSync3(
|
|
2920
|
+
const path5 = anonIdPath(home);
|
|
2921
|
+
if (existsSync3(path5)) {
|
|
2827
2922
|
try {
|
|
2828
|
-
const raw = (await readFile6(
|
|
2923
|
+
const raw = (await readFile6(path5, "utf8")).trim();
|
|
2829
2924
|
if (/^[0-9a-f]{16}$/.test(raw)) return raw;
|
|
2830
2925
|
} catch {
|
|
2831
2926
|
}
|
|
@@ -2833,11 +2928,11 @@ async function loadOrCreateAnonId(home = homedir7()) {
|
|
|
2833
2928
|
const id = computeAnonId();
|
|
2834
2929
|
try {
|
|
2835
2930
|
await mkdir2(trialsDir(home), { recursive: true });
|
|
2836
|
-
await atomicWriteFile(
|
|
2931
|
+
await atomicWriteFile(path5, `${id}
|
|
2837
2932
|
`);
|
|
2838
2933
|
if (process.platform !== "win32") {
|
|
2839
2934
|
try {
|
|
2840
|
-
await chmod2(
|
|
2935
|
+
await chmod2(path5, 384);
|
|
2841
2936
|
} catch {
|
|
2842
2937
|
}
|
|
2843
2938
|
}
|
|
@@ -2922,7 +3017,7 @@ async function runTry(opts) {
|
|
|
2922
3017
|
}
|
|
2923
3018
|
const clientId = opts.clientId ?? await autoDetectClient({ home, os, cwd, claudeConfigDir });
|
|
2924
3019
|
const scope = clientId === "vscode" ? "project" : "user";
|
|
2925
|
-
const projectDir = scope === "project" ?
|
|
3020
|
+
const projectDir = scope === "project" ? resolve5(cwd) : void 0;
|
|
2926
3021
|
let resolved;
|
|
2927
3022
|
try {
|
|
2928
3023
|
resolved = resolveInstallPath({ clientId, scope, os, home, projectDir, claudeConfigDir });
|
|
@@ -2931,7 +3026,7 @@ async function runTry(opts) {
|
|
|
2931
3026
|
return { exitCode: 1, written: [] };
|
|
2932
3027
|
}
|
|
2933
3028
|
const supplied = { ...env, ...opts.envOverrides ?? {} };
|
|
2934
|
-
const missing = (server.requiredEnvVars ?? []).filter((k) =>
|
|
3029
|
+
const missing = (server.requiredEnvVars ?? []).filter((k) => (supplied[k] ?? "").trim() === "");
|
|
2935
3030
|
if (missing.length > 0) {
|
|
2936
3031
|
printErr(`yaw-mcp try: ${server.name} needs the following env var(s) before it can run:`);
|
|
2937
3032
|
for (const k of missing) printErr(` - ${k}`);
|
|
@@ -2944,7 +3039,7 @@ async function runTry(opts) {
|
|
|
2944
3039
|
}
|
|
2945
3040
|
const trialEnv = {};
|
|
2946
3041
|
for (const k of server.requiredEnvVars ?? []) {
|
|
2947
|
-
const v = supplied[k];
|
|
3042
|
+
const v = (supplied[k] ?? "").trim();
|
|
2948
3043
|
if (v) trialEnv[k] = v;
|
|
2949
3044
|
}
|
|
2950
3045
|
for (const [k, v] of Object.entries(opts.envOverrides ?? {})) {
|
|
@@ -3065,6 +3160,7 @@ async function runTryCleanup(opts) {
|
|
|
3065
3160
|
printErr(`yaw-mcp try-cleanup: marker at ${markerPath} is unreadable (${e.message}).`);
|
|
3066
3161
|
return { exitCode: 1, written: [] };
|
|
3067
3162
|
}
|
|
3163
|
+
const written = [];
|
|
3068
3164
|
if (existsSync3(marker.clientPath)) {
|
|
3069
3165
|
try {
|
|
3070
3166
|
const raw = await readFile6(marker.clientPath, "utf8");
|
|
@@ -3079,13 +3175,14 @@ async function runTryCleanup(opts) {
|
|
|
3079
3175
|
if (stripped !== parsed) {
|
|
3080
3176
|
await atomicWriteFile(marker.clientPath, `${JSON.stringify(stripped, null, 2)}
|
|
3081
3177
|
`);
|
|
3178
|
+
written.push(marker.clientPath);
|
|
3082
3179
|
print(`Removed ${marker.entryName} from ${marker.clientPath}`);
|
|
3083
3180
|
}
|
|
3084
3181
|
}
|
|
3085
3182
|
}
|
|
3086
3183
|
} catch (e) {
|
|
3087
3184
|
printErr(
|
|
3088
|
-
`yaw-mcp try-cleanup: warning
|
|
3185
|
+
`yaw-mcp try-cleanup: warning -- couldn't strip ${marker.entryName} from ${marker.clientPath} (${e.message}).`
|
|
3089
3186
|
);
|
|
3090
3187
|
}
|
|
3091
3188
|
}
|
|
@@ -3099,7 +3196,7 @@ async function runTryCleanup(opts) {
|
|
|
3099
3196
|
const postEvent = opts.postEvent ?? defaultPostEvent;
|
|
3100
3197
|
postEvent(baseUrl, { slug, action: "cleanup", anonId }).catch(() => void 0);
|
|
3101
3198
|
print(`Trial for "${slug}" cleaned up.`);
|
|
3102
|
-
return { exitCode: 0, written
|
|
3199
|
+
return { exitCode: 0, written };
|
|
3103
3200
|
}
|
|
3104
3201
|
function formatTtl(ms) {
|
|
3105
3202
|
const clamped = Math.max(0, ms);
|
|
@@ -3122,12 +3219,12 @@ async function scanTrials(opts = {}) {
|
|
|
3122
3219
|
}
|
|
3123
3220
|
for (const filename of entries) {
|
|
3124
3221
|
if (!filename.endsWith(".json")) continue;
|
|
3125
|
-
const
|
|
3222
|
+
const path5 = join7(dir, filename);
|
|
3126
3223
|
try {
|
|
3127
|
-
const raw = await readFile6(
|
|
3224
|
+
const raw = await readFile6(path5, "utf8");
|
|
3128
3225
|
const parsed = JSON.parse(raw);
|
|
3129
3226
|
if (!parsed || typeof parsed !== "object" || typeof parsed.slug !== "string" || typeof parsed.expiresAt !== "number" || typeof parsed.clientPath !== "string" || !Array.isArray(parsed.containerPath) || typeof parsed.entryName !== "string") {
|
|
3130
|
-
result.malformed.push(
|
|
3227
|
+
result.malformed.push(path5);
|
|
3131
3228
|
continue;
|
|
3132
3229
|
}
|
|
3133
3230
|
const msUntilExpiry = parsed.expiresAt - now;
|
|
@@ -3136,7 +3233,7 @@ async function scanTrials(opts = {}) {
|
|
|
3136
3233
|
if (expired) result.expired.push(entry);
|
|
3137
3234
|
else result.live.push(entry);
|
|
3138
3235
|
} catch {
|
|
3139
|
-
result.malformed.push(
|
|
3236
|
+
result.malformed.push(path5);
|
|
3140
3237
|
}
|
|
3141
3238
|
}
|
|
3142
3239
|
return result;
|
|
@@ -3189,16 +3286,19 @@ var UPGRADE_USAGE = `Usage: yaw-mcp upgrade [--run] [--json]
|
|
|
3189
3286
|
|
|
3190
3287
|
Show (or execute) the command to upgrade @yawlabs/mcp to the latest version.
|
|
3191
3288
|
|
|
3192
|
-
--run Run the upgrade in place (global and local npm
|
|
3193
|
-
No-op for npx installs
|
|
3289
|
+
--run Run the upgrade in place (global npm, pnpm, bun, and local npm
|
|
3290
|
+
installs). No-op for npx installs -- they always fetch the latest.
|
|
3194
3291
|
--json Emit a machine-readable snapshot ({ current, latest, stale,
|
|
3195
|
-
method, command }) instead of prose
|
|
3292
|
+
method, command }) instead of prose.
|
|
3293
|
+
NOTE: --json is a report-only snapshot; it never spawns an upgrade
|
|
3294
|
+
even when combined with --run. Use --run without --json to
|
|
3295
|
+
actually perform the upgrade.`;
|
|
3196
3296
|
function parseUpgradeArgs(argv) {
|
|
3197
3297
|
const opts = {};
|
|
3198
3298
|
for (const a of argv) {
|
|
3199
3299
|
if (a === "--run") opts.run = true;
|
|
3200
3300
|
else if (a === "--json") opts.json = true;
|
|
3201
|
-
else if (a === "--help" || a === "-h") return { ok: false, error: UPGRADE_USAGE };
|
|
3301
|
+
else if (a === "--help" || a === "-h") return { ok: false, error: UPGRADE_USAGE, help: true };
|
|
3202
3302
|
else return { ok: false, error: `yaw-mcp upgrade: unknown argument "${a}"
|
|
3203
3303
|
|
|
3204
3304
|
${UPGRADE_USAGE}` };
|
|
@@ -3227,7 +3327,7 @@ function localInstallRoot(argvPath) {
|
|
|
3227
3327
|
}
|
|
3228
3328
|
async function defaultNpmPrefix() {
|
|
3229
3329
|
if (process.env.VITEST) return null;
|
|
3230
|
-
return new Promise((
|
|
3330
|
+
return new Promise((resolve7) => {
|
|
3231
3331
|
const child = spawn2("npm", ["prefix", "-g"], {
|
|
3232
3332
|
shell: process.platform === "win32",
|
|
3233
3333
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -3235,18 +3335,18 @@ async function defaultNpmPrefix() {
|
|
|
3235
3335
|
let out = "";
|
|
3236
3336
|
const timer = setTimeout(() => {
|
|
3237
3337
|
child.kill();
|
|
3238
|
-
|
|
3338
|
+
resolve7(null);
|
|
3239
3339
|
}, 3e3);
|
|
3240
3340
|
child.stdout?.on("data", (d) => {
|
|
3241
3341
|
out += String(d);
|
|
3242
3342
|
});
|
|
3243
3343
|
child.on("close", (code) => {
|
|
3244
3344
|
clearTimeout(timer);
|
|
3245
|
-
|
|
3345
|
+
resolve7(code === 0 && out.trim() ? out.trim() : null);
|
|
3246
3346
|
});
|
|
3247
3347
|
child.on("error", () => {
|
|
3248
3348
|
clearTimeout(timer);
|
|
3249
|
-
|
|
3349
|
+
resolve7(null);
|
|
3250
3350
|
});
|
|
3251
3351
|
});
|
|
3252
3352
|
}
|
|
@@ -3339,10 +3439,10 @@ async function defaultFetchLatest() {
|
|
|
3339
3439
|
}
|
|
3340
3440
|
}
|
|
3341
3441
|
async function defaultSpawn(cmd, args, cwd) {
|
|
3342
|
-
return new Promise((
|
|
3442
|
+
return new Promise((resolve7) => {
|
|
3343
3443
|
const child = spawn2(cmd, args, { stdio: "inherit", shell: process.platform === "win32", cwd });
|
|
3344
|
-
child.on("close", (code) =>
|
|
3345
|
-
child.on("error", () =>
|
|
3444
|
+
child.on("close", (code) => resolve7(typeof code === "number" ? code : 1));
|
|
3445
|
+
child.on("error", () => resolve7(1));
|
|
3346
3446
|
});
|
|
3347
3447
|
}
|
|
3348
3448
|
async function detectSea() {
|
|
@@ -3407,7 +3507,7 @@ async function runUpgrade(opts = {}) {
|
|
|
3407
3507
|
print(`Install: ${method}`);
|
|
3408
3508
|
if (!plan.stale) {
|
|
3409
3509
|
print("");
|
|
3410
|
-
print("
|
|
3510
|
+
print("OK: You're on the latest version -- nothing to do.");
|
|
3411
3511
|
return { exitCode: 0, lines };
|
|
3412
3512
|
}
|
|
3413
3513
|
print("");
|
|
@@ -3440,6 +3540,9 @@ async function runUpgrade(opts = {}) {
|
|
|
3440
3540
|
print("Run it yourself (--run can't safely automate this install method):");
|
|
3441
3541
|
}
|
|
3442
3542
|
print("");
|
|
3543
|
+
if (installRoot) {
|
|
3544
|
+
print(`in ${installRoot}:`);
|
|
3545
|
+
}
|
|
3443
3546
|
print(` ${plan.command}`);
|
|
3444
3547
|
return { exitCode: 1, lines };
|
|
3445
3548
|
}
|
|
@@ -3460,7 +3563,7 @@ async function runUpgrade(opts = {}) {
|
|
|
3460
3563
|
const code = await runner(runSpec.cmd, runSpec.args, runSpec.cwd);
|
|
3461
3564
|
if (code === 0) {
|
|
3462
3565
|
print("");
|
|
3463
|
-
print(
|
|
3566
|
+
print(`OK: Upgraded @yawlabs/mcp to ${latest}`);
|
|
3464
3567
|
return { exitCode: 0, lines };
|
|
3465
3568
|
}
|
|
3466
3569
|
printErr(`yaw-mcp upgrade: ${runSpec.cmd} exited ${code}. Try running the command yourself:`);
|
|
@@ -3469,7 +3572,7 @@ async function runUpgrade(opts = {}) {
|
|
|
3469
3572
|
return { exitCode: 3, lines };
|
|
3470
3573
|
}
|
|
3471
3574
|
function readCurrentVersion() {
|
|
3472
|
-
return true ? "0.
|
|
3575
|
+
return true ? "0.62.0" : "dev";
|
|
3473
3576
|
}
|
|
3474
3577
|
|
|
3475
3578
|
// src/usage-hints.ts
|
|
@@ -3497,7 +3600,7 @@ function buildCoUsageMap(packs) {
|
|
|
3497
3600
|
function formatUsageHint(usage, coUsedWith) {
|
|
3498
3601
|
const parts = [];
|
|
3499
3602
|
if (usage && usage.succeeded >= MIN_SUCCESS_TO_SHOW) {
|
|
3500
|
-
parts.push(`used ${usage.succeeded}x`);
|
|
3603
|
+
parts.push(`used ${Math.round(usage.succeeded)}x`);
|
|
3501
3604
|
}
|
|
3502
3605
|
if (coUsedWith.length > 0) {
|
|
3503
3606
|
const shown = coUsedWith.slice(0, MAX_PEERS);
|
|
@@ -3531,7 +3634,11 @@ function selectFlakyNamespaces(entries, limit) {
|
|
|
3531
3634
|
}
|
|
3532
3635
|
|
|
3533
3636
|
// src/doctor-cmd.ts
|
|
3534
|
-
var VERSION = true ? "0.
|
|
3637
|
+
var VERSION = true ? "0.62.0" : "dev";
|
|
3638
|
+
function isPersistenceDisabled(env) {
|
|
3639
|
+
const raw = env.YAW_MCP_DISABLE_PERSISTENCE;
|
|
3640
|
+
return raw !== void 0 && raw !== "" && (raw === "1" || raw.toLowerCase() === "true");
|
|
3641
|
+
}
|
|
3535
3642
|
async function runDoctor(opts = {}) {
|
|
3536
3643
|
if (opts.json) return runDoctorJson(opts);
|
|
3537
3644
|
const lines = [];
|
|
@@ -3568,8 +3675,16 @@ async function runDoctor(opts = {}) {
|
|
|
3568
3675
|
print(` source: ${config.apiBaseSource}`);
|
|
3569
3676
|
print("");
|
|
3570
3677
|
renderEnvSection({ env, print });
|
|
3571
|
-
|
|
3572
|
-
|
|
3678
|
+
const persistenceDisabled = isPersistenceDisabled(env);
|
|
3679
|
+
const stateFilePath = join8(userConfigDir(home), STATE_FILENAME);
|
|
3680
|
+
const persistedState = persistenceDisabled ? null : await loadState(stateFilePath);
|
|
3681
|
+
await renderStateSection({
|
|
3682
|
+
filePath: stateFilePath,
|
|
3683
|
+
disabled: persistenceDisabled,
|
|
3684
|
+
persisted: persistedState,
|
|
3685
|
+
print
|
|
3686
|
+
});
|
|
3687
|
+
renderReliabilitySection({ disabled: persistenceDisabled, persisted: persistedState, print });
|
|
3573
3688
|
await renderTrialsSection({ home, env, print, postEvent: opts.postTryEvent, now: opts.now });
|
|
3574
3689
|
renderBackgroundPostersSection({ print });
|
|
3575
3690
|
const claudeConfigDir = env.CLAUDE_CONFIG_DIR && env.CLAUDE_CONFIG_DIR.length > 0 ? env.CLAUDE_CONFIG_DIR : void 0;
|
|
@@ -3599,29 +3714,31 @@ async function runDoctor(opts = {}) {
|
|
|
3599
3714
|
}
|
|
3600
3715
|
print("");
|
|
3601
3716
|
}
|
|
3602
|
-
const skipCheck = opts.skipRegistryCheck === true || Boolean(process.env.VITEST);
|
|
3717
|
+
const skipCheck = (opts.skipRegistryCheck === true || Boolean(process.env.VITEST)) && !opts.registryFetch;
|
|
3603
3718
|
const latest = skipCheck ? null : await fetchLatestVersion(opts.registryFetch);
|
|
3604
|
-
const
|
|
3719
|
+
const effectiveVersion = opts.currentVersion ?? VERSION;
|
|
3720
|
+
const staleHint = latest && effectiveVersion !== "dev" && compareSemver(effectiveVersion, latest) < 0 ? latest : null;
|
|
3605
3721
|
if (staleHint) {
|
|
3606
|
-
const
|
|
3722
|
+
const effectiveArgvPath = opts.argvPath ?? process.argv[1];
|
|
3723
|
+
const method = await detectSea() ? "binary" : await refineInstallMethod(detectInstallMethod(effectiveArgvPath), effectiveArgvPath);
|
|
3607
3724
|
print("UPGRADE AVAILABLE");
|
|
3608
3725
|
if (method === "bundled-app") {
|
|
3609
|
-
print(` Running ${
|
|
3726
|
+
print(` Running ${effectiveVersion}; npm latest is ${staleHint}. This copy ships inside`);
|
|
3610
3727
|
print(" Yaw Terminal and updates with the app \u2014 update Yaw Terminal to get it.");
|
|
3611
3728
|
} else if (method === "npx") {
|
|
3612
|
-
print(` Running ${
|
|
3729
|
+
print(` Running ${effectiveVersion}; npm latest is ${staleHint}. npx fetches the latest`);
|
|
3613
3730
|
print(" on each spawn \u2014 restart your MCP client to pick it up.");
|
|
3614
3731
|
} else if (method === "binary") {
|
|
3615
|
-
print(` Running ${
|
|
3732
|
+
print(` Running ${effectiveVersion}; npm latest is ${staleHint}. This is a standalone`);
|
|
3616
3733
|
print(" binary \u2014 download the latest build and replace the executable:");
|
|
3617
3734
|
print(` ${BINARY_DOWNLOAD_URL}`);
|
|
3618
3735
|
} else if (method === "global-npm" || method === "pnpm-global" || method === "bun-global" || method === "local-node-modules") {
|
|
3619
|
-
print(` Running ${
|
|
3736
|
+
print(` Running ${effectiveVersion}; npm latest is ${staleHint}. To upgrade in place:`);
|
|
3620
3737
|
print("");
|
|
3621
3738
|
print(" yaw-mcp upgrade --run");
|
|
3622
3739
|
} else {
|
|
3623
|
-
const plan = buildUpgradePlan({ current:
|
|
3624
|
-
print(` Running ${
|
|
3740
|
+
const plan = buildUpgradePlan({ current: effectiveVersion, latest: staleHint, method });
|
|
3741
|
+
print(` Running ${effectiveVersion}; npm latest is ${staleHint}. To upgrade:`);
|
|
3625
3742
|
print("");
|
|
3626
3743
|
print(` ${plan.command ?? "npm install -g @yawlabs/mcp@latest"}`);
|
|
3627
3744
|
}
|
|
@@ -3668,24 +3785,21 @@ async function runDoctorJson(opts) {
|
|
|
3668
3785
|
const raw = env[name];
|
|
3669
3786
|
envOverrides[name] = raw === void 0 || raw === "" ? null : raw;
|
|
3670
3787
|
}
|
|
3671
|
-
const
|
|
3672
|
-
const
|
|
3673
|
-
const
|
|
3674
|
-
|
|
3675
|
-
const persisted = await loadState(filePath);
|
|
3788
|
+
const persistDisabled = isPersistenceDisabled(env);
|
|
3789
|
+
const stateFilePath = join8(userConfigDir(home), STATE_FILENAME);
|
|
3790
|
+
const persisted = persistDisabled ? null : await loadState(stateFilePath);
|
|
3791
|
+
const state = persistDisabled || !persisted ? { disabled: true, path: null, savedAt: null, learningEntries: null, packHistoryEntries: null } : (() => {
|
|
3676
3792
|
const fresh = persisted.savedAt === 0;
|
|
3677
3793
|
return {
|
|
3678
3794
|
disabled: false,
|
|
3679
|
-
path:
|
|
3795
|
+
path: stateFilePath,
|
|
3680
3796
|
savedAt: fresh ? null : new Date(persisted.savedAt).toISOString(),
|
|
3681
3797
|
learningEntries: fresh ? 0 : Object.keys(persisted.learning).length,
|
|
3682
3798
|
packHistoryEntries: fresh ? 0 : persisted.packHistory.length
|
|
3683
3799
|
};
|
|
3684
3800
|
})();
|
|
3685
3801
|
const reliability = [];
|
|
3686
|
-
if (!persistDisabled) {
|
|
3687
|
-
const filePath = join8(userConfigDir(home), STATE_FILENAME);
|
|
3688
|
-
const persisted = await loadState(filePath);
|
|
3802
|
+
if (!persistDisabled && persisted) {
|
|
3689
3803
|
if (persisted.savedAt !== 0) {
|
|
3690
3804
|
const entries = Object.entries(persisted.learning).map(([namespace, usage]) => ({ namespace, usage }));
|
|
3691
3805
|
for (const { namespace, usage } of selectFlakyNamespaces(entries, 5)) {
|
|
@@ -3700,9 +3814,10 @@ async function runDoctorJson(opts) {
|
|
|
3700
3814
|
}
|
|
3701
3815
|
}
|
|
3702
3816
|
const shellShadows = scanShellHistoryForShadows({ home, env });
|
|
3703
|
-
const skipCheck = opts.skipRegistryCheck === true || Boolean(process.env.VITEST);
|
|
3817
|
+
const skipCheck = (opts.skipRegistryCheck === true || Boolean(process.env.VITEST)) && !opts.registryFetch;
|
|
3704
3818
|
const latest = skipCheck ? null : await fetchLatestVersion(opts.registryFetch);
|
|
3705
|
-
const
|
|
3819
|
+
const effectiveVersion = opts.currentVersion ?? VERSION;
|
|
3820
|
+
const stale = latest !== null && effectiveVersion !== "dev" && compareSemver(effectiveVersion, latest) < 0;
|
|
3706
3821
|
let exitCode = 0;
|
|
3707
3822
|
let summary;
|
|
3708
3823
|
if (config.token === null) {
|
|
@@ -3731,7 +3846,7 @@ async function runDoctorJson(opts) {
|
|
|
3731
3846
|
reliability,
|
|
3732
3847
|
clients,
|
|
3733
3848
|
shellShadows,
|
|
3734
|
-
upgrade: { current:
|
|
3849
|
+
upgrade: { current: effectiveVersion, latest, stale },
|
|
3735
3850
|
diagnosis: { exitCode, summary }
|
|
3736
3851
|
};
|
|
3737
3852
|
const blob = JSON.stringify(snapshotJson, null, 2);
|
|
@@ -3760,16 +3875,13 @@ function renderEnvSection(opts) {
|
|
|
3760
3875
|
print("");
|
|
3761
3876
|
}
|
|
3762
3877
|
async function renderStateSection(opts) {
|
|
3763
|
-
const {
|
|
3764
|
-
const raw = env.YAW_MCP_DISABLE_PERSISTENCE;
|
|
3765
|
-
const disabled = raw !== void 0 && raw !== "" && (raw === "1" || raw.toLowerCase() === "true");
|
|
3878
|
+
const { filePath, disabled, persisted, print } = opts;
|
|
3766
3879
|
print("STATE");
|
|
3767
3880
|
if (disabled) {
|
|
3768
3881
|
print(" status: disabled via YAW_MCP_DISABLE_PERSISTENCE");
|
|
3769
3882
|
print("");
|
|
3770
3883
|
return;
|
|
3771
3884
|
}
|
|
3772
|
-
const filePath = join8(userConfigDir(home), STATE_FILENAME);
|
|
3773
3885
|
print(` path: ${filePath}`);
|
|
3774
3886
|
const peek = await peekStateFile(filePath);
|
|
3775
3887
|
if (peek.kind === "malformed") {
|
|
@@ -3790,8 +3902,7 @@ async function renderStateSection(opts) {
|
|
|
3790
3902
|
print("");
|
|
3791
3903
|
return;
|
|
3792
3904
|
}
|
|
3793
|
-
|
|
3794
|
-
if (persisted.savedAt === 0) {
|
|
3905
|
+
if (!persisted || persisted.savedAt === 0) {
|
|
3795
3906
|
print(" (no persisted state yet \u2014 will be created on the first tool call)");
|
|
3796
3907
|
} else {
|
|
3797
3908
|
print(` last saved: ${formatRelativeAge(Date.now() - persisted.savedAt)} ago`);
|
|
@@ -3823,13 +3934,9 @@ async function peekStateFile(filePath) {
|
|
|
3823
3934
|
}
|
|
3824
3935
|
return { kind: "ok" };
|
|
3825
3936
|
}
|
|
3826
|
-
|
|
3827
|
-
const {
|
|
3828
|
-
|
|
3829
|
-
const disabled = raw !== void 0 && raw !== "" && (raw === "1" || raw.toLowerCase() === "true");
|
|
3830
|
-
if (disabled) return;
|
|
3831
|
-
const filePath = join8(userConfigDir(home), STATE_FILENAME);
|
|
3832
|
-
const persisted = await loadState(filePath);
|
|
3937
|
+
function renderReliabilitySection(opts) {
|
|
3938
|
+
const { disabled, persisted, print } = opts;
|
|
3939
|
+
if (disabled || !persisted) return;
|
|
3833
3940
|
if (persisted.savedAt === 0) return;
|
|
3834
3941
|
const entries = Object.entries(persisted.learning).map(([namespace, usage]) => ({ namespace, usage }));
|
|
3835
3942
|
const flaky = selectFlakyNamespaces(entries, 5);
|
|
@@ -3855,8 +3962,8 @@ async function renderTrialsSection(opts) {
|
|
|
3855
3962
|
for (const { marker, msUntilExpiry } of scan.live) {
|
|
3856
3963
|
print(` ${marker.slug} -> ${marker.clientName} (${marker.clientPath}) \u2014 expires in ${formatTtl(msUntilExpiry)}`);
|
|
3857
3964
|
}
|
|
3858
|
-
for (const
|
|
3859
|
-
print(` ! malformed marker at ${
|
|
3965
|
+
for (const path5 of scan.malformed) {
|
|
3966
|
+
print(` ! malformed marker at ${path5} (delete by hand)`);
|
|
3860
3967
|
}
|
|
3861
3968
|
print("");
|
|
3862
3969
|
}
|
|
@@ -3975,9 +4082,9 @@ function probeClients(opts) {
|
|
|
3975
4082
|
}
|
|
3976
4083
|
return out;
|
|
3977
4084
|
}
|
|
3978
|
-
function walkContainer(root,
|
|
4085
|
+
function walkContainer(root, path5) {
|
|
3979
4086
|
let cur = root;
|
|
3980
|
-
for (const key of
|
|
4087
|
+
for (const key of path5) {
|
|
3981
4088
|
if (typeof cur !== "object" || cur === null || Array.isArray(cur)) return null;
|
|
3982
4089
|
cur = cur[key];
|
|
3983
4090
|
}
|
|
@@ -4018,6 +4125,7 @@ async function probeClientsAsync(opts) {
|
|
|
4018
4125
|
let malformed = false;
|
|
4019
4126
|
if (exists3) {
|
|
4020
4127
|
try {
|
|
4128
|
+
await stat3(resolved.absolute);
|
|
4021
4129
|
const raw = await readFile7(resolved.absolute, "utf8");
|
|
4022
4130
|
if (raw.trim().length > 0) {
|
|
4023
4131
|
const parsed = parseJsonc(raw);
|
|
@@ -4124,9 +4232,9 @@ function shellHistorySources(opts) {
|
|
|
4124
4232
|
}
|
|
4125
4233
|
return sources;
|
|
4126
4234
|
}
|
|
4127
|
-
function readTailLines(
|
|
4235
|
+
function readTailLines(path5, n) {
|
|
4128
4236
|
try {
|
|
4129
|
-
const raw = readFileSync(
|
|
4237
|
+
const raw = readFileSync(path5, "utf8");
|
|
4130
4238
|
const all = raw.split(/\r?\n/);
|
|
4131
4239
|
return all.length <= n ? all : all.slice(all.length - n);
|
|
4132
4240
|
} catch {
|
|
@@ -4171,97 +4279,494 @@ function compareSemver(a, b) {
|
|
|
4171
4279
|
return 0;
|
|
4172
4280
|
}
|
|
4173
4281
|
|
|
4174
|
-
// src/
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4282
|
+
// src/foundry-cmd.ts
|
|
4283
|
+
import { mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
4284
|
+
import { homedir as homedir10 } from "os";
|
|
4285
|
+
import path3 from "path";
|
|
4286
|
+
|
|
4287
|
+
// src/foundry-corpus.ts
|
|
4288
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
4289
|
+
|
|
4290
|
+
// src/relevance.ts
|
|
4291
|
+
var K1 = 1.2;
|
|
4292
|
+
var B = 0.75;
|
|
4293
|
+
var FIELD_WEIGHTS = {
|
|
4294
|
+
name: 3,
|
|
4295
|
+
namespace: 2,
|
|
4296
|
+
description: 1.5,
|
|
4297
|
+
toolName: 2,
|
|
4298
|
+
toolDescription: 1
|
|
4299
|
+
};
|
|
4300
|
+
var MIN_TOKEN_LEN = 3;
|
|
4301
|
+
function tokenize(text) {
|
|
4302
|
+
if (!text) return [];
|
|
4303
|
+
return text.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= MIN_TOKEN_LEN);
|
|
4304
|
+
}
|
|
4305
|
+
function buildDocFields(server) {
|
|
4306
|
+
const toolNameTokens = [];
|
|
4307
|
+
const toolDescriptionTokens = [];
|
|
4308
|
+
for (const tool of server.tools) {
|
|
4309
|
+
toolNameTokens.push(...tokenize(tool.name));
|
|
4310
|
+
toolDescriptionTokens.push(...tokenize(tool.description));
|
|
4311
|
+
}
|
|
4312
|
+
return {
|
|
4313
|
+
namespace: tokenize(server.namespace),
|
|
4314
|
+
name: tokenize(server.name),
|
|
4315
|
+
description: tokenize(server.description),
|
|
4316
|
+
toolName: toolNameTokens,
|
|
4317
|
+
toolDescription: toolDescriptionTokens
|
|
4318
|
+
};
|
|
4319
|
+
}
|
|
4320
|
+
function termFreq(tokens, term) {
|
|
4321
|
+
let count = 0;
|
|
4322
|
+
for (const t of tokens) {
|
|
4323
|
+
if (t === term) count++;
|
|
4324
|
+
}
|
|
4325
|
+
return count;
|
|
4326
|
+
}
|
|
4327
|
+
function bm25Score(queryTerms, fields, avgFieldLen, idfValues) {
|
|
4328
|
+
let score = 0;
|
|
4329
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4330
|
+
for (const term of queryTerms) {
|
|
4331
|
+
if (seen.has(term)) continue;
|
|
4332
|
+
seen.add(term);
|
|
4333
|
+
const termIdf = idfValues.get(term);
|
|
4334
|
+
if (termIdf === void 0 || termIdf <= 0) continue;
|
|
4335
|
+
for (const [fieldName, weight] of Object.entries(FIELD_WEIGHTS)) {
|
|
4336
|
+
const fieldTokens = fields[fieldName];
|
|
4337
|
+
if (fieldTokens.length === 0) continue;
|
|
4338
|
+
const tf = termFreq(fieldTokens, term);
|
|
4339
|
+
if (tf === 0) continue;
|
|
4340
|
+
const avg = avgFieldLen[fieldName] || 1;
|
|
4341
|
+
const normLen = 1 - B + B * (fieldTokens.length / avg);
|
|
4342
|
+
const numerator = tf * (K1 + 1);
|
|
4343
|
+
const denominator = tf + K1 * normLen;
|
|
4344
|
+
score += weight * termIdf * (numerator / denominator);
|
|
4196
4345
|
}
|
|
4197
|
-
[prev, curr] = [curr, prev];
|
|
4198
4346
|
}
|
|
4199
|
-
return
|
|
4347
|
+
return score;
|
|
4200
4348
|
}
|
|
4201
|
-
function
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
const
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
if (d <= 2) score = 2 + d;
|
|
4349
|
+
function rankServers(context, servers) {
|
|
4350
|
+
const queryTerms = tokenize(context);
|
|
4351
|
+
if (queryTerms.length === 0 || servers.length === 0) return [];
|
|
4352
|
+
const docsWithFields = servers.map((s) => ({ server: s, fields: buildDocFields(s) }));
|
|
4353
|
+
const N = docsWithFields.length;
|
|
4354
|
+
const df = /* @__PURE__ */ new Map();
|
|
4355
|
+
for (const { fields } of docsWithFields) {
|
|
4356
|
+
const bag = /* @__PURE__ */ new Set([
|
|
4357
|
+
...fields.namespace,
|
|
4358
|
+
...fields.name,
|
|
4359
|
+
...fields.description,
|
|
4360
|
+
...fields.toolName,
|
|
4361
|
+
...fields.toolDescription
|
|
4362
|
+
]);
|
|
4363
|
+
for (const term of bag) {
|
|
4364
|
+
df.set(term, (df.get(term) ?? 0) + 1);
|
|
4218
4365
|
}
|
|
4219
|
-
if (score !== null) scored.push({ name: c, score });
|
|
4220
4366
|
}
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4367
|
+
const idfValues = /* @__PURE__ */ new Map();
|
|
4368
|
+
for (const [term, d] of df) {
|
|
4369
|
+
idfValues.set(term, Math.log((N - d + 0.5) / (d + 0.5) + 1));
|
|
4370
|
+
}
|
|
4371
|
+
const totalLen = {
|
|
4372
|
+
namespace: 0,
|
|
4373
|
+
name: 0,
|
|
4374
|
+
description: 0,
|
|
4375
|
+
toolName: 0,
|
|
4376
|
+
toolDescription: 0
|
|
4377
|
+
};
|
|
4378
|
+
for (const { fields } of docsWithFields) {
|
|
4379
|
+
totalLen.namespace += fields.namespace.length;
|
|
4380
|
+
totalLen.name += fields.name.length;
|
|
4381
|
+
totalLen.description += fields.description.length;
|
|
4382
|
+
totalLen.toolName += fields.toolName.length;
|
|
4383
|
+
totalLen.toolDescription += fields.toolDescription.length;
|
|
4384
|
+
}
|
|
4385
|
+
const denom = Math.max(N, 1);
|
|
4386
|
+
const avgFieldLen = {
|
|
4387
|
+
namespace: totalLen.namespace / denom,
|
|
4388
|
+
name: totalLen.name / denom,
|
|
4389
|
+
description: totalLen.description / denom,
|
|
4390
|
+
toolName: totalLen.toolName / denom,
|
|
4391
|
+
toolDescription: totalLen.toolDescription / denom
|
|
4392
|
+
};
|
|
4393
|
+
const results = [];
|
|
4394
|
+
for (const { server, fields } of docsWithFields) {
|
|
4395
|
+
const score = bm25Score(queryTerms, fields, avgFieldLen, idfValues);
|
|
4396
|
+
if (score > 0) {
|
|
4397
|
+
results.push({ namespace: server.namespace, score });
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
results.sort((a, b) => {
|
|
4401
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
4402
|
+
return a.namespace < b.namespace ? -1 : 1;
|
|
4224
4403
|
});
|
|
4225
|
-
return
|
|
4404
|
+
return results;
|
|
4226
4405
|
}
|
|
4227
4406
|
|
|
4228
|
-
// src/
|
|
4229
|
-
|
|
4230
|
-
var
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
return
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4407
|
+
// src/foundry-corpus.ts
|
|
4408
|
+
var FOUNDRY_CORPUS_VERSION = 1;
|
|
4409
|
+
var DEFAULT_CORPUS_CAP = 500;
|
|
4410
|
+
function parseTraceLines(text) {
|
|
4411
|
+
const out = [];
|
|
4412
|
+
for (const line of text.split("\n")) {
|
|
4413
|
+
const trimmed = line.trim();
|
|
4414
|
+
if (!trimmed) continue;
|
|
4415
|
+
try {
|
|
4416
|
+
const obj = JSON.parse(trimmed);
|
|
4417
|
+
if (obj && Array.isArray(obj.tokens) && typeof obj.chosen === "string") {
|
|
4418
|
+
out.push(obj);
|
|
4419
|
+
}
|
|
4420
|
+
} catch {
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
4423
|
+
return out;
|
|
4424
|
+
}
|
|
4425
|
+
function entryKey(tokens, chosen) {
|
|
4426
|
+
return `${tokens.join(" ")}::${chosen}`;
|
|
4427
|
+
}
|
|
4428
|
+
function capStratified(entries, cap) {
|
|
4429
|
+
if (entries.length <= cap) return entries;
|
|
4430
|
+
const byChosen = /* @__PURE__ */ new Map();
|
|
4431
|
+
for (const e of entries) {
|
|
4432
|
+
const g = byChosen.get(e.chosen);
|
|
4433
|
+
if (g) g.push(e);
|
|
4434
|
+
else byChosen.set(e.chosen, [e]);
|
|
4435
|
+
}
|
|
4436
|
+
const groups = [...byChosen.entries()].sort((a, b) => a[0] < b[0] ? -1 : 1).map(([, g]) => g.sort((x, y) => y.weight - x.weight));
|
|
4437
|
+
const out = [];
|
|
4438
|
+
let i = 0;
|
|
4439
|
+
while (out.length < cap) {
|
|
4440
|
+
let took = false;
|
|
4441
|
+
for (const g of groups) {
|
|
4442
|
+
if (i < g.length) {
|
|
4443
|
+
out.push(g[i]);
|
|
4444
|
+
took = true;
|
|
4445
|
+
if (out.length >= cap) break;
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4448
|
+
if (!took) break;
|
|
4449
|
+
i++;
|
|
4450
|
+
}
|
|
4451
|
+
return out;
|
|
4452
|
+
}
|
|
4453
|
+
function buildCorpusFromTraces(traces, servers, opts = {}) {
|
|
4454
|
+
const known = new Set(servers.map((s) => s.namespace));
|
|
4455
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
4456
|
+
for (const t of traces) {
|
|
4457
|
+
if (!t || typeof t.chosen !== "string" || !known.has(t.chosen)) continue;
|
|
4458
|
+
if (!Array.isArray(t.tokens) || t.tokens.length === 0) continue;
|
|
4459
|
+
const tokens = [...t.tokens].filter((x) => typeof x === "string").sort();
|
|
4460
|
+
if (tokens.length === 0) continue;
|
|
4461
|
+
const key = entryKey(tokens, t.chosen);
|
|
4462
|
+
const prev = byKey.get(key);
|
|
4463
|
+
if (prev) prev.weight += 1;
|
|
4464
|
+
else byKey.set(key, { tokens, chosen: t.chosen, weight: 1 });
|
|
4465
|
+
}
|
|
4466
|
+
const entries = capStratified([...byKey.values()], opts.cap ?? DEFAULT_CORPUS_CAP);
|
|
4467
|
+
return { version: FOUNDRY_CORPUS_VERSION, servers, entries };
|
|
4468
|
+
}
|
|
4469
|
+
function scoreCorpus(corpus) {
|
|
4470
|
+
let totalWeight = 0;
|
|
4471
|
+
let top1Weight = 0;
|
|
4472
|
+
let top3Weight = 0;
|
|
4473
|
+
for (const e of corpus.entries) {
|
|
4474
|
+
totalWeight += e.weight;
|
|
4475
|
+
const top3 = rankServers(e.tokens.join(" "), corpus.servers).slice(0, 3).map((r) => r.namespace);
|
|
4476
|
+
if (top3[0] === e.chosen) top1Weight += e.weight;
|
|
4477
|
+
if (top3.includes(e.chosen)) top3Weight += e.weight;
|
|
4478
|
+
}
|
|
4479
|
+
return {
|
|
4480
|
+
totalWeight,
|
|
4481
|
+
top1Weight,
|
|
4482
|
+
top3Weight,
|
|
4483
|
+
top1: totalWeight > 0 ? top1Weight / totalWeight : 0,
|
|
4484
|
+
top3: totalWeight > 0 ? top3Weight / totalWeight : 0
|
|
4485
|
+
};
|
|
4486
|
+
}
|
|
4487
|
+
|
|
4488
|
+
// src/foundry.ts
|
|
4489
|
+
import { appendFile, mkdir as mkdir3, stat as stat4 } from "fs/promises";
|
|
4490
|
+
import { homedir as homedir9 } from "os";
|
|
4491
|
+
import path2 from "path";
|
|
4492
|
+
var SECRET_PREFIXES = ["sk_", "sk-", "tok_", "ghp_", "gho_", "xox", "pk_", "akia"];
|
|
4493
|
+
function looksSensitive(token5) {
|
|
4494
|
+
for (const prefix of SECRET_PREFIXES) {
|
|
4495
|
+
if (token5.startsWith(prefix)) return true;
|
|
4496
|
+
}
|
|
4497
|
+
if (token5.length >= 16 && /^[0-9a-f]+$/.test(token5)) return true;
|
|
4498
|
+
if (token5.length >= 12 && /[a-z]/.test(token5) && /[0-9]/.test(token5)) return true;
|
|
4499
|
+
if (token5.length >= 16 && /^[a-z]+$/.test(token5)) return true;
|
|
4500
|
+
return false;
|
|
4501
|
+
}
|
|
4502
|
+
function redactIntent(intent) {
|
|
4503
|
+
const all = tokenize(intent);
|
|
4504
|
+
const tokens = [];
|
|
4505
|
+
let redactedCount = 0;
|
|
4506
|
+
for (const token5 of all) {
|
|
4507
|
+
if (looksSensitive(token5)) {
|
|
4508
|
+
redactedCount++;
|
|
4509
|
+
} else {
|
|
4510
|
+
tokens.push(token5);
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
tokens.sort();
|
|
4514
|
+
return { tokens, redactedCount };
|
|
4515
|
+
}
|
|
4516
|
+
function isFoundryEnabled() {
|
|
4517
|
+
const raw = process.env.YAW_MCP_FOUNDRY;
|
|
4518
|
+
if (!raw) return false;
|
|
4519
|
+
const v = raw.trim().toLowerCase();
|
|
4520
|
+
return v === "1" || v === "true";
|
|
4521
|
+
}
|
|
4522
|
+
var MAX_FOUNDRY_BYTES = 5 * 1024 * 1024;
|
|
4523
|
+
var FOUNDRY_FILENAME = "foundry.jsonl";
|
|
4524
|
+
async function appendFoundryTrace(trace, home = homedir9()) {
|
|
4525
|
+
try {
|
|
4526
|
+
if (!isFoundryEnabled()) return;
|
|
4527
|
+
const dir = userConfigDir(home);
|
|
4528
|
+
const file = path2.join(dir, FOUNDRY_FILENAME);
|
|
4529
|
+
try {
|
|
4530
|
+
const info = await stat4(file);
|
|
4531
|
+
if (info.size >= MAX_FOUNDRY_BYTES) return;
|
|
4532
|
+
} catch {
|
|
4533
|
+
}
|
|
4534
|
+
const line = `${JSON.stringify({
|
|
4535
|
+
tokens: trace.tokens,
|
|
4536
|
+
candidates: trace.candidates,
|
|
4537
|
+
chosen: trace.chosen,
|
|
4538
|
+
redactedCount: trace.redactedCount
|
|
4539
|
+
})}
|
|
4540
|
+
`;
|
|
4541
|
+
await mkdir3(dir, { recursive: true });
|
|
4542
|
+
await appendFile(file, line, "utf8");
|
|
4543
|
+
} catch {
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
|
|
4547
|
+
// src/foundry-cmd.ts
|
|
4548
|
+
var DEFAULT_OUT = path3.join("src", "tests", "fixtures", "foundry-corpus.json");
|
|
4549
|
+
var FOUNDRY_USAGE = `Usage: yaw-mcp foundry export [--out <path>] [--cap <n>] [--json]
|
|
4550
|
+
|
|
4551
|
+
Fold the opt-in dispatch harvest (~/.yaw-mcp/foundry.jsonl) into a routing
|
|
4552
|
+
regression corpus consumed by the foundry-routing test gate. Maintainer
|
|
4553
|
+
command: requires a local bundles.json for the server-catalog snapshot.
|
|
4554
|
+
|
|
4555
|
+
--out <path> Where to write the corpus (default: ${DEFAULT_OUT}).
|
|
4556
|
+
--cap <n> Max entries, stratified by chosen server (default: ${DEFAULT_CORPUS_CAP}).
|
|
4557
|
+
--json Emit a machine-readable summary instead of text.`;
|
|
4558
|
+
function parseFoundryArgs(argv) {
|
|
4559
|
+
let action;
|
|
4560
|
+
let out = DEFAULT_OUT;
|
|
4561
|
+
let cap = DEFAULT_CORPUS_CAP;
|
|
4562
|
+
let json = false;
|
|
4563
|
+
for (let i = 0; i < argv.length; i++) {
|
|
4564
|
+
const a = argv[i];
|
|
4565
|
+
if (a === "--help" || a === "-h") return { ok: false, error: FOUNDRY_USAGE };
|
|
4566
|
+
if (a === "--json") {
|
|
4567
|
+
json = true;
|
|
4568
|
+
} else if (a === "--out") {
|
|
4569
|
+
const v = argv[++i];
|
|
4570
|
+
if (!v) return { ok: false, error: `yaw-mcp foundry: --out needs a path
|
|
4571
|
+
|
|
4572
|
+
${FOUNDRY_USAGE}` };
|
|
4573
|
+
out = v;
|
|
4574
|
+
} else if (a === "--cap") {
|
|
4575
|
+
const v = Number(argv[++i]);
|
|
4576
|
+
if (!Number.isFinite(v) || v <= 0)
|
|
4577
|
+
return { ok: false, error: `yaw-mcp foundry: --cap needs a positive number
|
|
4578
|
+
|
|
4579
|
+
${FOUNDRY_USAGE}` };
|
|
4580
|
+
cap = Math.floor(v);
|
|
4581
|
+
} else if (a.startsWith("-")) {
|
|
4582
|
+
return { ok: false, error: `yaw-mcp foundry: unknown argument "${a}"
|
|
4583
|
+
|
|
4584
|
+
${FOUNDRY_USAGE}` };
|
|
4585
|
+
} else if (action === void 0) {
|
|
4586
|
+
if (a !== "export")
|
|
4587
|
+
return { ok: false, error: `yaw-mcp foundry: unknown action "${a}" (only "export")
|
|
4588
|
+
|
|
4589
|
+
${FOUNDRY_USAGE}` };
|
|
4590
|
+
action = a;
|
|
4591
|
+
} else {
|
|
4592
|
+
return { ok: false, error: `yaw-mcp foundry: unexpected extra argument "${a}"
|
|
4593
|
+
|
|
4594
|
+
${FOUNDRY_USAGE}` };
|
|
4595
|
+
}
|
|
4596
|
+
}
|
|
4597
|
+
if (action === void 0) return { ok: false, error: `yaw-mcp foundry: missing action.
|
|
4598
|
+
|
|
4599
|
+
${FOUNDRY_USAGE}` };
|
|
4600
|
+
return { ok: true, options: { action, out, cap, json } };
|
|
4601
|
+
}
|
|
4602
|
+
async function defaultLoadServers(cwd, home) {
|
|
4603
|
+
const { config } = await loadLocalBundles({ cwd, home });
|
|
4604
|
+
return (config?.servers ?? []).map((s) => ({
|
|
4605
|
+
namespace: s.namespace,
|
|
4606
|
+
name: s.name,
|
|
4607
|
+
description: s.description,
|
|
4608
|
+
tools: s.toolCache ?? []
|
|
4609
|
+
}));
|
|
4610
|
+
}
|
|
4611
|
+
async function runFoundryExport(opts) {
|
|
4612
|
+
const write = opts.write ?? ((s) => process.stdout.write(s));
|
|
4613
|
+
const writeErr = opts.writeErr ?? ((s) => process.stderr.write(s));
|
|
4614
|
+
const lines = [];
|
|
4615
|
+
const print = (s = "") => {
|
|
4616
|
+
lines.push(s);
|
|
4617
|
+
write(`${s}
|
|
4618
|
+
`);
|
|
4619
|
+
};
|
|
4620
|
+
const printErr = (s) => {
|
|
4621
|
+
lines.push(s);
|
|
4622
|
+
writeErr(`${s}
|
|
4623
|
+
`);
|
|
4624
|
+
};
|
|
4625
|
+
const home = opts.home ?? homedir10();
|
|
4626
|
+
const harvestPath = path3.join(userConfigDir(home), FOUNDRY_FILENAME);
|
|
4627
|
+
const blob = opts.readTraces ? opts.readTraces() : (() => {
|
|
4628
|
+
try {
|
|
4629
|
+
return readFileSync3(harvestPath, "utf8");
|
|
4630
|
+
} catch {
|
|
4631
|
+
return null;
|
|
4632
|
+
}
|
|
4633
|
+
})();
|
|
4634
|
+
if (blob === null) {
|
|
4635
|
+
printErr(`yaw-mcp foundry: no harvest at ${harvestPath}. Set YAW_MCP_FOUNDRY=1 and dispatch first.`);
|
|
4636
|
+
return { exitCode: 1, lines };
|
|
4637
|
+
}
|
|
4638
|
+
const traces = parseTraceLines(blob);
|
|
4639
|
+
if (traces.length === 0) {
|
|
4640
|
+
printErr(`yaw-mcp foundry: ${harvestPath} has no parseable traces.`);
|
|
4641
|
+
return { exitCode: 1, lines };
|
|
4642
|
+
}
|
|
4643
|
+
const servers = opts.loadServers ? await opts.loadServers() : await defaultLoadServers(opts.cwd, home);
|
|
4644
|
+
const corpus = buildCorpusFromTraces(traces, servers, { cap: opts.cap });
|
|
4645
|
+
if (corpus.entries.length === 0) {
|
|
4646
|
+
printErr(
|
|
4647
|
+
`yaw-mcp foundry: ${traces.length} traces but 0 usable entries -- none of the chosen servers are in the local catalog (${servers.length} servers).`
|
|
4648
|
+
);
|
|
4649
|
+
return { exitCode: 2, lines };
|
|
4650
|
+
}
|
|
4651
|
+
mkdirSync(path3.dirname(path3.resolve(opts.out)), { recursive: true });
|
|
4652
|
+
writeFileSync(opts.out, `${JSON.stringify(corpus, null, 2)}
|
|
4653
|
+
`, "utf8");
|
|
4654
|
+
const score = scoreCorpus(corpus);
|
|
4655
|
+
if (opts.json) {
|
|
4656
|
+
print(
|
|
4657
|
+
JSON.stringify(
|
|
4658
|
+
{
|
|
4659
|
+
out: opts.out,
|
|
4660
|
+
entries: corpus.entries.length,
|
|
4661
|
+
servers: corpus.servers.length,
|
|
4662
|
+
fromTraces: traces.length,
|
|
4663
|
+
top1: score.top1,
|
|
4664
|
+
top3: score.top3
|
|
4665
|
+
},
|
|
4666
|
+
null,
|
|
4667
|
+
2
|
|
4668
|
+
)
|
|
4669
|
+
);
|
|
4670
|
+
return { exitCode: 0, lines };
|
|
4671
|
+
}
|
|
4672
|
+
print(`Wrote ${corpus.entries.length} entries (from ${traces.length} traces) to ${opts.out}`);
|
|
4673
|
+
print(
|
|
4674
|
+
`BM25-floor accuracy on this corpus: top-1 ${(score.top1 * 100).toFixed(1)}%, top-3 ${(score.top3 * 100).toFixed(1)}%`
|
|
4675
|
+
);
|
|
4676
|
+
return { exitCode: 0, lines };
|
|
4677
|
+
}
|
|
4678
|
+
|
|
4679
|
+
// src/fuzzy.ts
|
|
4680
|
+
function levenshtein(a, b) {
|
|
4681
|
+
if (a === b) return 0;
|
|
4682
|
+
const aLen = a.length;
|
|
4683
|
+
const bLen = b.length;
|
|
4684
|
+
if (aLen === 0) return bLen;
|
|
4685
|
+
if (bLen === 0) return aLen;
|
|
4686
|
+
let prev = new Array(bLen + 1);
|
|
4687
|
+
let curr = new Array(bLen + 1);
|
|
4688
|
+
for (let j = 0; j <= bLen; j++) prev[j] = j;
|
|
4689
|
+
for (let i = 1; i <= aLen; i++) {
|
|
4690
|
+
curr[0] = i;
|
|
4691
|
+
for (let j = 1; j <= bLen; j++) {
|
|
4692
|
+
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
4693
|
+
curr[j] = Math.min(
|
|
4694
|
+
curr[j - 1] + 1,
|
|
4695
|
+
// insertion
|
|
4696
|
+
prev[j] + 1,
|
|
4697
|
+
// deletion
|
|
4698
|
+
prev[j - 1] + cost
|
|
4699
|
+
// substitution
|
|
4700
|
+
);
|
|
4701
|
+
}
|
|
4702
|
+
[prev, curr] = [curr, prev];
|
|
4703
|
+
}
|
|
4704
|
+
return prev[bLen];
|
|
4705
|
+
}
|
|
4706
|
+
function closestNames(query, candidates, limit) {
|
|
4707
|
+
if (limit <= 0) return [];
|
|
4708
|
+
const q = query.toLowerCase();
|
|
4709
|
+
const scored = [];
|
|
4710
|
+
for (const c of candidates) {
|
|
4711
|
+
if (c === query) continue;
|
|
4712
|
+
const lc = c.toLowerCase();
|
|
4713
|
+
let score = null;
|
|
4714
|
+
if (lc === q) {
|
|
4715
|
+
score = 0;
|
|
4716
|
+
} else if (lc.startsWith(q) || q.startsWith(lc)) {
|
|
4717
|
+
score = 1;
|
|
4718
|
+
} else if (lc.includes(q) || q.includes(lc)) {
|
|
4719
|
+
score = 2;
|
|
4720
|
+
} else {
|
|
4721
|
+
const d = levenshtein(q, lc);
|
|
4722
|
+
if (d <= 2) score = 2 + d;
|
|
4723
|
+
}
|
|
4724
|
+
if (score !== null) scored.push({ name: c, score });
|
|
4725
|
+
}
|
|
4726
|
+
scored.sort((a, b) => {
|
|
4727
|
+
if (a.score !== b.score) return a.score - b.score;
|
|
4728
|
+
return a.name.localeCompare(b.name);
|
|
4729
|
+
});
|
|
4730
|
+
return scored.slice(0, limit).map((s) => s.name);
|
|
4731
|
+
}
|
|
4732
|
+
|
|
4733
|
+
// src/local-add-cmd.ts
|
|
4734
|
+
import { homedir as homedir11 } from "os";
|
|
4735
|
+
var SLUG_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
4736
|
+
var ADD_USAGE = `Usage: yaw-mcp add <slug> [flags]
|
|
4737
|
+
|
|
4738
|
+
Resolve <slug> from the yaw.sh/mcp catalog and add it to your local
|
|
4739
|
+
~/.yaw-mcp/bundles.json so yaw-mcp loads it (no account needed).
|
|
4740
|
+
|
|
4741
|
+
This is NOT the same as \`yaw-mcp install\` -- install wires the yaw-mcp
|
|
4742
|
+
aggregator into an AI client; add adds an MCP server to yaw-mcp itself.
|
|
4743
|
+
|
|
4744
|
+
--env KEY=value Provide a required env var's value. Repeatable. Required
|
|
4745
|
+
vars not given here AND not in your shell block the add.
|
|
4746
|
+
--dry-run Print what would be written without writing.
|
|
4747
|
+
--json Emit the written entry as JSON (implies success on stdout).
|
|
4748
|
+
--catalog <url> Override the catalog URL (default the public catalog).`;
|
|
4749
|
+
function parseEnvFlag(v, bag) {
|
|
4750
|
+
if (!v || !v.includes("=")) return "--env requires KEY=value";
|
|
4751
|
+
const eq = v.indexOf("=");
|
|
4752
|
+
const key = v.slice(0, eq);
|
|
4753
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) return `--env: invalid KEY "${key}"`;
|
|
4754
|
+
bag[key] = v.slice(eq + 1);
|
|
4755
|
+
return null;
|
|
4756
|
+
}
|
|
4757
|
+
function parseAddArgs(argv) {
|
|
4758
|
+
if (argv.length === 0) return { ok: false, error: ADD_USAGE };
|
|
4759
|
+
const positional = [];
|
|
4760
|
+
const opts = {};
|
|
4761
|
+
const env = {};
|
|
4762
|
+
for (let i = 0; i < argv.length; i++) {
|
|
4763
|
+
const a = argv[i];
|
|
4764
|
+
const next = () => argv[++i];
|
|
4765
|
+
switch (a) {
|
|
4766
|
+
case "--env": {
|
|
4767
|
+
const e = parseEnvFlag(next(), env);
|
|
4768
|
+
if (e) return { ok: false, error: e };
|
|
4769
|
+
break;
|
|
4265
4770
|
}
|
|
4266
4771
|
case "--dry-run":
|
|
4267
4772
|
opts.dryRun = true;
|
|
@@ -4277,7 +4782,7 @@ function parseAddArgs(argv) {
|
|
|
4277
4782
|
}
|
|
4278
4783
|
case "-h":
|
|
4279
4784
|
case "--help":
|
|
4280
|
-
return { ok: false, error: ADD_USAGE };
|
|
4785
|
+
return { ok: false, error: ADD_USAGE, help: true };
|
|
4281
4786
|
default:
|
|
4282
4787
|
if (a.startsWith("--")) return { ok: false, error: `Unknown flag: ${a}
|
|
4283
4788
|
${ADD_USAGE}` };
|
|
@@ -4309,7 +4814,7 @@ async function runAdd(opts) {
|
|
|
4309
4814
|
return { exitCode: 2, written: [] };
|
|
4310
4815
|
}
|
|
4311
4816
|
const env = opts.env ?? process.env;
|
|
4312
|
-
const home = opts.home ??
|
|
4817
|
+
const home = opts.home ?? homedir11();
|
|
4313
4818
|
const cwd = opts.cwd ?? process.cwd();
|
|
4314
4819
|
let server;
|
|
4315
4820
|
try {
|
|
@@ -4336,6 +4841,10 @@ async function runAdd(opts) {
|
|
|
4336
4841
|
const entryEnv = {};
|
|
4337
4842
|
for (const k of server.requiredEnvKeys) entryEnv[k] = "";
|
|
4338
4843
|
for (const [k, v] of Object.entries(opts.envOverrides ?? {})) entryEnv[k] = v;
|
|
4844
|
+
const overrides = opts.envOverrides ?? {};
|
|
4845
|
+
const ambientOnlyRequired = server.requiredEnvKeys.filter(
|
|
4846
|
+
(k) => (!overrides[k] || overrides[k] === "") && env[k] != null && env[k] !== ""
|
|
4847
|
+
);
|
|
4339
4848
|
const entry = {
|
|
4340
4849
|
id: `local-${namespace}`,
|
|
4341
4850
|
name: server.name,
|
|
@@ -4371,6 +4880,11 @@ async function runAdd(opts) {
|
|
|
4371
4880
|
print(`${res.replaced ? "Updated" : "Added"} ${server.name} (namespace "${namespace}") in ${res.path}`);
|
|
4372
4881
|
print("Restart your MCP client (or yaw-mcp) to pick it up.");
|
|
4373
4882
|
}
|
|
4883
|
+
if (ambientOnlyRequired.length > 0) {
|
|
4884
|
+
printErr(
|
|
4885
|
+
`Note: ${ambientOnlyRequired.join(", ")} ${ambientOnlyRequired.length === 1 ? "was" : "were"} read from your shell env and NOT persisted; the server depends on ${ambientOnlyRequired.length === 1 ? "that var" : "those vars"} being present wherever yaw-mcp launches. Pass --env ${ambientOnlyRequired[0]}=... to persist a value.`
|
|
4886
|
+
);
|
|
4887
|
+
}
|
|
4374
4888
|
const shadow = await findShadowingProjectBundles(cwd, home).catch(() => null);
|
|
4375
4889
|
if (shadow) {
|
|
4376
4890
|
printErr(
|
|
@@ -4389,7 +4903,7 @@ function parseRemoveArgs(argv) {
|
|
|
4389
4903
|
if (argv.length === 0) return { ok: false, error: REMOVE_USAGE };
|
|
4390
4904
|
const positional = [];
|
|
4391
4905
|
for (const a of argv) {
|
|
4392
|
-
if (a === "-h" || a === "--help") return { ok: false, error: REMOVE_USAGE };
|
|
4906
|
+
if (a === "-h" || a === "--help") return { ok: false, error: REMOVE_USAGE, help: true };
|
|
4393
4907
|
if (a.startsWith("--")) return { ok: false, error: `Unknown flag: ${a}
|
|
4394
4908
|
${REMOVE_USAGE}` };
|
|
4395
4909
|
positional.push(a);
|
|
@@ -4415,7 +4929,7 @@ async function runRemove(opts) {
|
|
|
4415
4929
|
printErr(`yaw-mcp remove: "${opts.target}" isn't a valid slug or namespace.`);
|
|
4416
4930
|
return { exitCode: 2, written: [] };
|
|
4417
4931
|
}
|
|
4418
|
-
const home = opts.home ??
|
|
4932
|
+
const home = opts.home ?? homedir11();
|
|
4419
4933
|
const cwd = opts.cwd ?? process.cwd();
|
|
4420
4934
|
const derived = deriveNamespace(opts.target);
|
|
4421
4935
|
const candidates = derived === opts.target ? [opts.target] : [opts.target, derived];
|
|
@@ -4459,7 +4973,7 @@ var LIST_USAGE = `Usage: yaw-mcp list [--json]
|
|
|
4459
4973
|
function parseListArgs(argv) {
|
|
4460
4974
|
const opts = {};
|
|
4461
4975
|
for (const a of argv) {
|
|
4462
|
-
if (a === "-h" || a === "--help") return { ok: false, error: LIST_USAGE };
|
|
4976
|
+
if (a === "-h" || a === "--help") return { ok: false, error: LIST_USAGE, help: true };
|
|
4463
4977
|
if (a === "--json") {
|
|
4464
4978
|
opts.json = true;
|
|
4465
4979
|
continue;
|
|
@@ -4471,14 +4985,18 @@ ${LIST_USAGE}` };
|
|
|
4471
4985
|
}
|
|
4472
4986
|
async function runList(opts) {
|
|
4473
4987
|
const out = opts.out ?? ((s) => process.stdout.write(s));
|
|
4988
|
+
const err = opts.err ?? ((s) => process.stderr.write(s));
|
|
4474
4989
|
const print = (s = "") => out(`${s}
|
|
4475
4990
|
`);
|
|
4476
|
-
const
|
|
4991
|
+
const printErr = (s) => err(`${s}
|
|
4992
|
+
`);
|
|
4993
|
+
const home = opts.home ?? homedir11();
|
|
4477
4994
|
const cwd = opts.cwd ?? process.cwd();
|
|
4478
4995
|
const loaded = await loadLocalBundles({ home, cwd });
|
|
4479
4996
|
const servers = loaded.config?.servers ?? [];
|
|
4997
|
+
for (const w of loaded.warnings) printErr(`warning: ${w}`);
|
|
4480
4998
|
if (opts.json) {
|
|
4481
|
-
print(JSON.stringify({ path: loaded.path, servers }, null, 2));
|
|
4999
|
+
print(JSON.stringify({ path: loaded.path, servers, warnings: loaded.warnings }, null, 2));
|
|
4482
5000
|
return { exitCode: 0, written: [] };
|
|
4483
5001
|
}
|
|
4484
5002
|
if (servers.length === 0) {
|
|
@@ -4519,12 +5037,14 @@ function parseLoginArgs(argv) {
|
|
|
4519
5037
|
const a = argv[i];
|
|
4520
5038
|
if (a === "--key") {
|
|
4521
5039
|
const v = argv[++i];
|
|
4522
|
-
if (!v) return { ok: false, error:
|
|
5040
|
+
if (!v) return { ok: false, error: `yaw-mcp login: --key requires a value
|
|
5041
|
+
|
|
5042
|
+
${LOGIN_USAGE}` };
|
|
4523
5043
|
opts.key = v;
|
|
4524
5044
|
} else if (a === "--json") {
|
|
4525
5045
|
opts.json = true;
|
|
4526
5046
|
} else if (a === "--help" || a === "-h") {
|
|
4527
|
-
return { ok: false, error: LOGIN_USAGE };
|
|
5047
|
+
return { ok: false, error: LOGIN_USAGE, help: true };
|
|
4528
5048
|
} else {
|
|
4529
5049
|
return { ok: false, error: `yaw-mcp login: unknown argument "${a}"
|
|
4530
5050
|
|
|
@@ -4532,7 +5052,9 @@ ${LOGIN_USAGE}` };
|
|
|
4532
5052
|
}
|
|
4533
5053
|
}
|
|
4534
5054
|
if (!opts.key) {
|
|
4535
|
-
return { ok: false, error:
|
|
5055
|
+
return { ok: false, error: `yaw-mcp login: --key is required
|
|
5056
|
+
|
|
5057
|
+
${LOGIN_USAGE}` };
|
|
4536
5058
|
}
|
|
4537
5059
|
return { ok: true, options: opts };
|
|
4538
5060
|
}
|
|
@@ -4574,7 +5096,7 @@ async function runLogin(opts, io = {
|
|
|
4574
5096
|
io.err(`yaw-mcp login: ${message}
|
|
4575
5097
|
`);
|
|
4576
5098
|
}
|
|
4577
|
-
return { exitCode: err instanceof TeamSyncAuthError ? 1 :
|
|
5099
|
+
return { exitCode: err instanceof TeamSyncAuthError ? 1 : 2 };
|
|
4578
5100
|
}
|
|
4579
5101
|
}
|
|
4580
5102
|
|
|
@@ -4590,7 +5112,7 @@ function parseLogoutArgs(argv) {
|
|
|
4590
5112
|
const opts = {};
|
|
4591
5113
|
for (const a of argv) {
|
|
4592
5114
|
if (a === "--json") opts.json = true;
|
|
4593
|
-
else if (a === "--help" || a === "-h") return { ok: false, error: LOGOUT_USAGE };
|
|
5115
|
+
else if (a === "--help" || a === "-h") return { ok: false, error: LOGOUT_USAGE, help: true };
|
|
4594
5116
|
else return { ok: false, error: `yaw-mcp logout: unknown argument "${a}"
|
|
4595
5117
|
|
|
4596
5118
|
${LOGOUT_USAGE}` };
|
|
@@ -4621,7 +5143,7 @@ async function runLogout(opts = {}, io = {
|
|
|
4621
5143
|
|
|
4622
5144
|
// src/reset-learning-cmd.ts
|
|
4623
5145
|
import { unlink as unlink2 } from "fs/promises";
|
|
4624
|
-
import { homedir as
|
|
5146
|
+
import { homedir as homedir12 } from "os";
|
|
4625
5147
|
import { join as join9 } from "path";
|
|
4626
5148
|
var RESET_LEARNING_USAGE = `Usage: yaw-mcp reset-learning
|
|
4627
5149
|
|
|
@@ -4644,7 +5166,7 @@ ${RESET_LEARNING_USAGE}`
|
|
|
4644
5166
|
return { kind: "ok", options: {} };
|
|
4645
5167
|
}
|
|
4646
5168
|
async function runResetLearning(opts = {}) {
|
|
4647
|
-
const home = opts.home ??
|
|
5169
|
+
const home = opts.home ?? homedir12();
|
|
4648
5170
|
const env = opts.env ?? process.env;
|
|
4649
5171
|
const write = opts.out ?? ((s) => process.stdout.write(s));
|
|
4650
5172
|
const writeErr = opts.err ?? ((s) => process.stderr.write(s));
|
|
@@ -4693,15 +5215,13 @@ function isFileNotFound2(err) {
|
|
|
4693
5215
|
|
|
4694
5216
|
// src/secrets-cmd.ts
|
|
4695
5217
|
import { existsSync as existsSync6 } from "fs";
|
|
4696
|
-
import {
|
|
4697
|
-
import { homedir as homedir12 } from "os";
|
|
4698
|
-
import { dirname as dirname3 } from "path";
|
|
5218
|
+
import { homedir as homedir14 } from "os";
|
|
4699
5219
|
|
|
4700
5220
|
// src/secrets-vault.ts
|
|
4701
5221
|
import { existsSync as existsSync5 } from "fs";
|
|
4702
5222
|
import { chmod as chmod3, readFile as readFile8 } from "fs/promises";
|
|
4703
|
-
import { homedir as
|
|
4704
|
-
import {
|
|
5223
|
+
import { homedir as homedir13 } from "os";
|
|
5224
|
+
import { join as join10 } from "path";
|
|
4705
5225
|
|
|
4706
5226
|
// src/secrets-crypto.ts
|
|
4707
5227
|
import { createCipheriv, createDecipheriv, randomBytes, scrypt as scryptCb } from "crypto";
|
|
@@ -4721,13 +5241,13 @@ async function deriveKey(passphrase, salt) {
|
|
|
4721
5241
|
return scryptCallWithMaxmem(passphrase, salt, KEY_LEN);
|
|
4722
5242
|
}
|
|
4723
5243
|
async function scryptCallWithMaxmem(password, salt, keylen) {
|
|
4724
|
-
return new Promise((
|
|
5244
|
+
return new Promise((resolve7, reject) => {
|
|
4725
5245
|
scryptCb(
|
|
4726
5246
|
password,
|
|
4727
5247
|
salt,
|
|
4728
5248
|
keylen,
|
|
4729
5249
|
{ N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P, maxmem: SCRYPT_MAXMEM },
|
|
4730
|
-
(err, key) => err ? reject(err) :
|
|
5250
|
+
(err, key) => err ? reject(err) : resolve7(key)
|
|
4731
5251
|
);
|
|
4732
5252
|
});
|
|
4733
5253
|
}
|
|
@@ -4759,7 +5279,8 @@ function decryptEntry(entry, key) {
|
|
|
4759
5279
|
// src/secrets-vault.ts
|
|
4760
5280
|
var SECRETS_FILENAME = "secrets.json";
|
|
4761
5281
|
var SECRETS_SCHEMA_VERSION = 1;
|
|
4762
|
-
|
|
5282
|
+
var VAULT_CHECK_PLAINTEXT = "yaw-mcp-vault-v1";
|
|
5283
|
+
function vaultPath(home = homedir13()) {
|
|
4763
5284
|
return join10(home, CONFIG_DIRNAME, SECRETS_FILENAME);
|
|
4764
5285
|
}
|
|
4765
5286
|
function emptyVault() {
|
|
@@ -4769,39 +5290,50 @@ function emptyVault() {
|
|
|
4769
5290
|
entries: {}
|
|
4770
5291
|
};
|
|
4771
5292
|
}
|
|
4772
|
-
async function loadVault(
|
|
4773
|
-
if (!existsSync5(
|
|
5293
|
+
async function loadVault(path5) {
|
|
5294
|
+
if (!existsSync5(path5)) return null;
|
|
4774
5295
|
let raw;
|
|
4775
5296
|
try {
|
|
4776
|
-
raw = await readFile8(
|
|
5297
|
+
raw = await readFile8(path5, "utf8");
|
|
4777
5298
|
} catch (err) {
|
|
4778
|
-
log("warn", "Failed to read vault", { path:
|
|
5299
|
+
log("warn", "Failed to read vault", { path: path5, error: err instanceof Error ? err.message : String(err) });
|
|
4779
5300
|
return null;
|
|
4780
5301
|
}
|
|
4781
5302
|
let parsed;
|
|
4782
5303
|
try {
|
|
4783
5304
|
parsed = JSON.parse(raw);
|
|
4784
5305
|
} catch (err) {
|
|
4785
|
-
log("warn", "Vault file is not valid JSON", { path:
|
|
5306
|
+
log("warn", "Vault file is not valid JSON", { path: path5, error: err instanceof Error ? err.message : String(err) });
|
|
4786
5307
|
return null;
|
|
4787
5308
|
}
|
|
4788
5309
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
4789
5310
|
const obj = parsed;
|
|
4790
5311
|
if (typeof obj.salt !== "string" || !obj.entries || typeof obj.entries !== "object") return null;
|
|
5312
|
+
const entries = obj.entries;
|
|
5313
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
5314
|
+
if (!isEncryptedEntry(entry)) {
|
|
5315
|
+
throw new Error(`vault corrupt at entry ${name}`);
|
|
5316
|
+
}
|
|
5317
|
+
}
|
|
5318
|
+
const check = isEncryptedEntry(obj.check) ? obj.check : void 0;
|
|
4791
5319
|
return {
|
|
4792
5320
|
version: typeof obj.version === "number" ? obj.version : SECRETS_SCHEMA_VERSION,
|
|
4793
5321
|
salt: obj.salt,
|
|
4794
|
-
entries: obj.entries
|
|
5322
|
+
entries: obj.entries,
|
|
5323
|
+
...check ? { check } : {}
|
|
4795
5324
|
};
|
|
4796
5325
|
}
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
5326
|
+
function isEncryptedEntry(v) {
|
|
5327
|
+
if (!v || typeof v !== "object") return false;
|
|
5328
|
+
const e = v;
|
|
5329
|
+
return typeof e.iv === "string" && typeof e.ciphertext === "string" && typeof e.authTag === "string";
|
|
5330
|
+
}
|
|
5331
|
+
async function saveVault(path5, vault) {
|
|
5332
|
+
await atomicWriteFile(path5, `${JSON.stringify(vault, null, 2)}
|
|
4801
5333
|
`);
|
|
4802
5334
|
if (process.platform !== "win32") {
|
|
4803
5335
|
try {
|
|
4804
|
-
await chmod3(
|
|
5336
|
+
await chmod3(path5, 384);
|
|
4805
5337
|
} catch {
|
|
4806
5338
|
}
|
|
4807
5339
|
}
|
|
@@ -4817,22 +5349,39 @@ async function unlock(vault, passphrase) {
|
|
|
4817
5349
|
if (cachedKey && cachedSalt === vault.salt) return cachedKey;
|
|
4818
5350
|
const salt = Buffer.from(vault.salt, "base64");
|
|
4819
5351
|
const key = await deriveKey(passphrase, salt);
|
|
5352
|
+
verifyKey(vault, key);
|
|
4820
5353
|
cachedKey = key;
|
|
4821
5354
|
cachedSalt = vault.salt;
|
|
4822
5355
|
return key;
|
|
4823
5356
|
}
|
|
5357
|
+
function verifyKey(vault, key) {
|
|
5358
|
+
const canary = vault.check ?? Object.values(vault.entries)[0];
|
|
5359
|
+
if (!canary) return;
|
|
5360
|
+
try {
|
|
5361
|
+
decryptEntry(canary, key);
|
|
5362
|
+
} catch {
|
|
5363
|
+
throw new Error("wrong passphrase for this vault (decryption failed)");
|
|
5364
|
+
}
|
|
5365
|
+
}
|
|
5366
|
+
function ensureCheck(vault, key) {
|
|
5367
|
+
if (vault.check) return vault;
|
|
5368
|
+
return { ...vault, check: encryptEntry(VAULT_CHECK_PLAINTEXT, key) };
|
|
5369
|
+
}
|
|
4824
5370
|
function listKeys(vault) {
|
|
4825
5371
|
return Object.keys(vault.entries).sort();
|
|
4826
5372
|
}
|
|
4827
5373
|
function setSecret(vault, key, name, value) {
|
|
4828
5374
|
if (!name) throw new Error("secret name is required");
|
|
4829
|
-
return
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
5375
|
+
return ensureCheck(
|
|
5376
|
+
{
|
|
5377
|
+
...vault,
|
|
5378
|
+
entries: {
|
|
5379
|
+
...vault.entries,
|
|
5380
|
+
[name]: encryptEntry(value, key)
|
|
5381
|
+
}
|
|
5382
|
+
},
|
|
5383
|
+
key
|
|
5384
|
+
);
|
|
4836
5385
|
}
|
|
4837
5386
|
function removeSecret(vault, name) {
|
|
4838
5387
|
if (!(name in vault.entries)) return vault;
|
|
@@ -4902,13 +5451,17 @@ Actions:
|
|
|
4902
5451
|
Requires \`yaw-mcp login\` first.
|
|
4903
5452
|
pull Download the vault from mcp_secrets and write
|
|
4904
5453
|
it locally. Overwrites local vault. Requires
|
|
4905
|
-
\`yaw-mcp login\` first.
|
|
5454
|
+
\`yaw-mcp login\` first. Refuses when the local
|
|
5455
|
+
vault has a different salt (different passphrase
|
|
5456
|
+
lineage) unless --force is passed.
|
|
4906
5457
|
|
|
4907
5458
|
Flags:
|
|
4908
5459
|
--json Machine-readable output (where applicable).
|
|
4909
5460
|
--value <v> Inline secret value (set only). Beware shell
|
|
4910
5461
|
history -- prefer the default stdin prompt.
|
|
4911
5462
|
--stdin Read the secret from raw stdin (set only).
|
|
5463
|
+
--force (pull only) Overwrite even when the local vault
|
|
5464
|
+
salt differs from the remote. Back up first.
|
|
4912
5465
|
|
|
4913
5466
|
Passphrase:
|
|
4914
5467
|
Set YAW_MCP_VAULT_PASSPHRASE in the env, or you will be prompted on
|
|
@@ -4919,7 +5472,7 @@ function parseSecretsArgs(argv) {
|
|
|
4919
5472
|
const opts = {};
|
|
4920
5473
|
for (let i = 0; i < argv.length; i++) {
|
|
4921
5474
|
const a = argv[i];
|
|
4922
|
-
if (a === "--help" || a === "-h") return { ok: false, error: SECRETS_USAGE };
|
|
5475
|
+
if (a === "--help" || a === "-h") return { ok: false, error: SECRETS_USAGE, help: true };
|
|
4923
5476
|
if (a === "--json") {
|
|
4924
5477
|
opts.json = true;
|
|
4925
5478
|
continue;
|
|
@@ -4928,9 +5481,15 @@ function parseSecretsArgs(argv) {
|
|
|
4928
5481
|
opts.fromStdin = true;
|
|
4929
5482
|
continue;
|
|
4930
5483
|
}
|
|
5484
|
+
if (a === "--force") {
|
|
5485
|
+
opts.force = true;
|
|
5486
|
+
continue;
|
|
5487
|
+
}
|
|
4931
5488
|
if (a === "--value") {
|
|
4932
5489
|
const v = argv[++i];
|
|
4933
|
-
if (v === void 0) return { ok: false, error:
|
|
5490
|
+
if (v === void 0) return { ok: false, error: `yaw-mcp secrets: --value requires a value
|
|
5491
|
+
|
|
5492
|
+
${SECRETS_USAGE}` };
|
|
4934
5493
|
opts.value = v;
|
|
4935
5494
|
continue;
|
|
4936
5495
|
}
|
|
@@ -4956,7 +5515,9 @@ ${SECRETS_USAGE}` };
|
|
|
4956
5515
|
|
|
4957
5516
|
${SECRETS_USAGE}` };
|
|
4958
5517
|
}
|
|
4959
|
-
if (!opts.action) return { ok: false, error:
|
|
5518
|
+
if (!opts.action) return { ok: false, error: `yaw-mcp secrets: missing action
|
|
5519
|
+
|
|
5520
|
+
${SECRETS_USAGE}` };
|
|
4960
5521
|
if ((opts.action === "set" || opts.action === "get" || opts.action === "remove") && !opts.name) {
|
|
4961
5522
|
return { ok: false, error: `yaw-mcp secrets ${opts.action}: <name> is required
|
|
4962
5523
|
|
|
@@ -4965,18 +5526,34 @@ ${SECRETS_USAGE}` };
|
|
|
4965
5526
|
return { ok: true, options: opts };
|
|
4966
5527
|
}
|
|
4967
5528
|
async function resolvePassphrase(opts) {
|
|
4968
|
-
if (opts.passphrase !== void 0) return opts.passphrase;
|
|
5529
|
+
if (opts.passphrase !== void 0) return opts.passphrase.length > 0 ? opts.passphrase : null;
|
|
4969
5530
|
const fromEnv = process.env.YAW_MCP_VAULT_PASSPHRASE;
|
|
4970
|
-
if (typeof fromEnv === "string" && fromEnv.length > 0)
|
|
5531
|
+
if (typeof fromEnv === "string" && fromEnv.length > 0) {
|
|
5532
|
+
if (fromEnv.length < MIN_PASSPHRASE_WARN_LEN) {
|
|
5533
|
+
const stderr = opts.io?.stderr ?? process.stderr;
|
|
5534
|
+
stderr.write(
|
|
5535
|
+
`yaw-mcp secrets: warning -- YAW_MCP_VAULT_PASSPHRASE is shorter than ${MIN_PASSPHRASE_WARN_LEN} characters; consider a longer passphrase.
|
|
5536
|
+
`
|
|
5537
|
+
);
|
|
5538
|
+
}
|
|
5539
|
+
return fromEnv;
|
|
5540
|
+
}
|
|
4971
5541
|
const stdin = opts.io?.stdin ?? process.stdin;
|
|
4972
5542
|
const stdout = opts.io?.stdout ?? process.stdout;
|
|
4973
5543
|
const isTTY = stdin.isTTY === true && stdout.isTTY === true;
|
|
4974
5544
|
if (!isTTY) return null;
|
|
4975
|
-
|
|
5545
|
+
for (let attempt = 0; attempt < MAX_PASSPHRASE_PROMPTS; attempt++) {
|
|
5546
|
+
const entered = await readPassphraseFromTTY(stdin, stdout);
|
|
5547
|
+
if (entered.length > 0) return entered;
|
|
5548
|
+
stdout.write("Passphrase cannot be empty.\n");
|
|
5549
|
+
}
|
|
5550
|
+
return null;
|
|
4976
5551
|
}
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
5552
|
+
var MAX_PASSPHRASE_PROMPTS = 3;
|
|
5553
|
+
var MIN_PASSPHRASE_WARN_LEN = 12;
|
|
5554
|
+
function readPassphraseFromTTY(stdin, stdout, prompt = "Vault passphrase: ") {
|
|
5555
|
+
stdout.write(prompt);
|
|
5556
|
+
return new Promise((resolve7) => {
|
|
4980
5557
|
const chunks = [];
|
|
4981
5558
|
const wasRaw = stdin.isRaw === true;
|
|
4982
5559
|
try {
|
|
@@ -4995,7 +5572,7 @@ function readPassphraseFromTTY(stdin, stdout) {
|
|
|
4995
5572
|
} catch {
|
|
4996
5573
|
}
|
|
4997
5574
|
stdin.pause();
|
|
4998
|
-
|
|
5575
|
+
resolve7(chunks.join(""));
|
|
4999
5576
|
return;
|
|
5000
5577
|
}
|
|
5001
5578
|
if (ch === "") {
|
|
@@ -5025,16 +5602,12 @@ async function readStdinValue(io) {
|
|
|
5025
5602
|
for await (const chunk of stdin) chunks.push(chunk);
|
|
5026
5603
|
return chunks.join("").replace(/\r?\n$/, "");
|
|
5027
5604
|
}
|
|
5028
|
-
async function ensureVaultDir(path3) {
|
|
5029
|
-
const dir = dirname3(path3);
|
|
5030
|
-
if (!existsSync6(dir)) await mkdir3(dir, { recursive: true });
|
|
5031
|
-
}
|
|
5032
5605
|
async function runSecrets(opts, io = {
|
|
5033
5606
|
out: (s) => process.stdout.write(s),
|
|
5034
5607
|
err: (s) => process.stderr.write(s)
|
|
5035
5608
|
}) {
|
|
5036
|
-
const home = opts.home ??
|
|
5037
|
-
const
|
|
5609
|
+
const home = opts.home ?? homedir14();
|
|
5610
|
+
const path5 = vaultPath(home);
|
|
5038
5611
|
if (opts.action === "lock") {
|
|
5039
5612
|
lock();
|
|
5040
5613
|
if (opts.json) io.out(`${JSON.stringify({ ok: true, locked: true })}
|
|
@@ -5049,24 +5622,24 @@ async function runSecrets(opts, io = {
|
|
|
5049
5622
|
return await runSecretsPull(opts, io);
|
|
5050
5623
|
}
|
|
5051
5624
|
if (opts.action === "list") {
|
|
5052
|
-
const vault2 = await loadVault(
|
|
5625
|
+
const vault2 = await loadVault(path5);
|
|
5053
5626
|
const keys = vault2 ? listKeys(vault2) : [];
|
|
5054
|
-
if (opts.json) io.out(`${JSON.stringify({ ok: true, vault: existsSync6(
|
|
5627
|
+
if (opts.json) io.out(`${JSON.stringify({ ok: true, vault: existsSync6(path5), keys }, null, 2)}
|
|
5055
5628
|
`);
|
|
5056
|
-
else if (!vault2) io.out(`No vault at ${
|
|
5629
|
+
else if (!vault2) io.out(`No vault at ${path5}. Run \`yaw-mcp secrets set <name>\` to create one.
|
|
5057
5630
|
`);
|
|
5058
|
-
else if (keys.length === 0) io.out(`Vault at ${
|
|
5631
|
+
else if (keys.length === 0) io.out(`Vault at ${path5} is empty.
|
|
5059
5632
|
`);
|
|
5060
5633
|
else {
|
|
5061
|
-
io.out(`Vault at ${
|
|
5634
|
+
io.out(`Vault at ${path5}
|
|
5062
5635
|
`);
|
|
5063
5636
|
for (const k of keys) io.out(` ${k}
|
|
5064
5637
|
`);
|
|
5065
5638
|
}
|
|
5066
5639
|
return { exitCode: 0 };
|
|
5067
5640
|
}
|
|
5068
|
-
let vault = await loadVault(
|
|
5069
|
-
const isFresh = !existsSync6(
|
|
5641
|
+
let vault = await loadVault(path5) ?? newVault();
|
|
5642
|
+
const isFresh = !existsSync6(path5);
|
|
5070
5643
|
const passphrase = await resolvePassphrase(opts);
|
|
5071
5644
|
if (passphrase === null) {
|
|
5072
5645
|
const msg = "Passphrase required. Set YAW_MCP_VAULT_PASSPHRASE or run from a TTY so we can prompt.";
|
|
@@ -5101,8 +5674,7 @@ async function runSecrets(opts, io = {
|
|
|
5101
5674
|
return { exitCode: 1 };
|
|
5102
5675
|
}
|
|
5103
5676
|
vault = setSecret(vault, key, name, value);
|
|
5104
|
-
await
|
|
5105
|
-
await saveVault(path3, vault);
|
|
5677
|
+
await saveVault(path5, vault);
|
|
5106
5678
|
if (opts.json) io.out(`${JSON.stringify({ ok: true, name, fresh_vault: isFresh })}
|
|
5107
5679
|
`);
|
|
5108
5680
|
else io.out(`${isFresh ? "Created vault and " : ""}Stored secret "${name}".
|
|
@@ -5148,7 +5720,7 @@ async function runSecrets(opts, io = {
|
|
|
5148
5720
|
return { exitCode: 1 };
|
|
5149
5721
|
}
|
|
5150
5722
|
vault = removeSecret(vault, name);
|
|
5151
|
-
await saveVault(
|
|
5723
|
+
await saveVault(path5, vault);
|
|
5152
5724
|
if (opts.json) io.out(`${JSON.stringify({ ok: true, removed: name })}
|
|
5153
5725
|
`);
|
|
5154
5726
|
else io.out(`Removed "${name}".
|
|
@@ -5161,8 +5733,8 @@ async function runSecrets(opts, io = {
|
|
|
5161
5733
|
}
|
|
5162
5734
|
var MCP_SECRETS_RESOURCE = "mcp_secrets";
|
|
5163
5735
|
async function runSecretsPush(opts, io) {
|
|
5164
|
-
const home = opts.home ??
|
|
5165
|
-
const
|
|
5736
|
+
const home = opts.home ?? homedir14();
|
|
5737
|
+
const path5 = vaultPath(home);
|
|
5166
5738
|
const session = await getSession({ home, baseUrl: opts.baseUrl });
|
|
5167
5739
|
if (!session) {
|
|
5168
5740
|
const msg = "Not signed in. Run `yaw-mcp login --key <license-key>` first.";
|
|
@@ -5172,9 +5744,9 @@ async function runSecretsPush(opts, io) {
|
|
|
5172
5744
|
`);
|
|
5173
5745
|
return { exitCode: 1 };
|
|
5174
5746
|
}
|
|
5175
|
-
const vault = await loadVault(
|
|
5747
|
+
const vault = await loadVault(path5);
|
|
5176
5748
|
if (!vault) {
|
|
5177
|
-
const msg = `No local vault at ${
|
|
5749
|
+
const msg = `No local vault at ${path5} to push. Run \`yaw-mcp secrets set <name>\` first.`;
|
|
5178
5750
|
if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
|
|
5179
5751
|
`);
|
|
5180
5752
|
else io.err(`yaw-mcp secrets push: ${msg}
|
|
@@ -5224,8 +5796,8 @@ async function runSecretsPush(opts, io) {
|
|
|
5224
5796
|
}
|
|
5225
5797
|
}
|
|
5226
5798
|
async function runSecretsPull(opts, io) {
|
|
5227
|
-
const home = opts.home ??
|
|
5228
|
-
const
|
|
5799
|
+
const home = opts.home ?? homedir14();
|
|
5800
|
+
const path5 = vaultPath(home);
|
|
5229
5801
|
const session = await getSession({ home, baseUrl: opts.baseUrl });
|
|
5230
5802
|
if (!session) {
|
|
5231
5803
|
const msg = "Not signed in. Run `yaw-mcp login --key <license-key>` first.";
|
|
@@ -5237,7 +5809,9 @@ async function runSecretsPull(opts, io) {
|
|
|
5237
5809
|
}
|
|
5238
5810
|
try {
|
|
5239
5811
|
const remote = await getResource(MCP_SECRETS_RESOURCE, { home, baseUrl: opts.baseUrl });
|
|
5240
|
-
|
|
5812
|
+
const remoteEntries = remote.data?.entries;
|
|
5813
|
+
const remoteHasEntries = remoteEntries !== void 0 && remoteEntries !== null && typeof remoteEntries === "object" && Object.keys(remoteEntries).length > 0;
|
|
5814
|
+
if (!remote.data || !remote.data.salt || !remoteHasEntries) {
|
|
5241
5815
|
const msg = "Remote mcp_secrets is empty. Push from this machine to seed it.";
|
|
5242
5816
|
if (opts.json) io.out(`${JSON.stringify({ ok: true, empty: true })}
|
|
5243
5817
|
`);
|
|
@@ -5245,18 +5819,29 @@ async function runSecretsPull(opts, io) {
|
|
|
5245
5819
|
`);
|
|
5246
5820
|
return { exitCode: 0 };
|
|
5247
5821
|
}
|
|
5248
|
-
await
|
|
5249
|
-
|
|
5822
|
+
const localVault = await loadVault(path5);
|
|
5823
|
+
const localHasEntries = localVault !== null && Object.keys(localVault.entries).length > 0;
|
|
5824
|
+
if (localHasEntries && localVault.salt !== remote.data.salt && !opts.force) {
|
|
5825
|
+
const msg = `Local vault at ${path5} has a different salt than the remote (different passphrase lineage). Back up ${path5} first, then re-run with --force to overwrite.`;
|
|
5826
|
+
if (opts.json) io.err(`${JSON.stringify({ ok: false, error: msg })}
|
|
5827
|
+
`);
|
|
5828
|
+
else io.err(`yaw-mcp secrets pull: ${msg}
|
|
5829
|
+
`);
|
|
5830
|
+
return { exitCode: 1 };
|
|
5831
|
+
}
|
|
5832
|
+
await saveVault(path5, remote.data);
|
|
5250
5833
|
lock();
|
|
5251
5834
|
const count = Object.keys(remote.data.entries).length;
|
|
5252
5835
|
if (opts.json) {
|
|
5253
5836
|
io.out(
|
|
5254
|
-
`${JSON.stringify({ ok: true, secret_count: count, remote_version: remote.version, written:
|
|
5837
|
+
`${JSON.stringify({ ok: true, secret_count: count, remote_version: remote.version, written: path5 }, null, 2)}
|
|
5255
5838
|
`
|
|
5256
5839
|
);
|
|
5257
5840
|
} else {
|
|
5258
|
-
io.out(
|
|
5259
|
-
`)
|
|
5841
|
+
io.out(
|
|
5842
|
+
`Local vault replaced with remote copy: ${count} secret${count === 1 ? "" : "s"} (encrypted) -> ${path5}
|
|
5843
|
+
`
|
|
5844
|
+
);
|
|
5260
5845
|
io.out("Vault locked -- next secrets command will prompt for the passphrase.\n");
|
|
5261
5846
|
}
|
|
5262
5847
|
return { exitCode: 0 };
|
|
@@ -5280,8 +5865,8 @@ async function runSecretsPull(opts, io) {
|
|
|
5280
5865
|
|
|
5281
5866
|
// src/server.ts
|
|
5282
5867
|
import { readFile as readFile10 } from "fs/promises";
|
|
5283
|
-
import { homedir as
|
|
5284
|
-
import { isAbsolute, relative, resolve as
|
|
5868
|
+
import { homedir as homedir15 } from "os";
|
|
5869
|
+
import { isAbsolute as isAbsolute2, relative, resolve as resolve6 } from "path";
|
|
5285
5870
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5286
5871
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5287
5872
|
import {
|
|
@@ -5314,33 +5899,38 @@ async function fetchLatestVersion2() {
|
|
|
5314
5899
|
}
|
|
5315
5900
|
}
|
|
5316
5901
|
function defaultSpawn2(cmd, args) {
|
|
5902
|
+
let errorFired = false;
|
|
5903
|
+
const correctiveCmd = cmd === "npm" ? "npm install -g @yawlabs/mcp@latest" : cmd === "pnpm" ? "pnpm add -g @yawlabs/mcp@latest" : "bun add -g @yawlabs/mcp@latest";
|
|
5317
5904
|
const child = spawn3(cmd, args, {
|
|
5318
5905
|
stdio: "ignore",
|
|
5319
5906
|
// Stay a child of this process (not detached) so it dies with yaw-mcp
|
|
5320
|
-
// if yaw-mcp exits mid-install -- a half-finished
|
|
5321
|
-
// (npm
|
|
5907
|
+
// if yaw-mcp exits mid-install -- a half-finished install is fine
|
|
5908
|
+
// (npm/pnpm/bun are atomic per package) and a re-run next startup completes it.
|
|
5322
5909
|
detached: false,
|
|
5323
5910
|
shell: process.platform === "win32"
|
|
5324
5911
|
});
|
|
5325
5912
|
child.on("close", (code) => {
|
|
5913
|
+
if (errorFired) return;
|
|
5326
5914
|
if (code === 0) {
|
|
5327
5915
|
log("info", "yaw-mcp self-upgrade complete; the next client restart will run the new version");
|
|
5328
5916
|
} else {
|
|
5917
|
+
const hint = cmd === "npm" ? " (often EACCES on a sudo-installed global -- run with the right permissions)" : "";
|
|
5329
5918
|
log(
|
|
5330
5919
|
"warn",
|
|
5331
|
-
|
|
5920
|
+
`yaw-mcp self-upgrade: ${cmd} exited non-zero${hint}. Run \`${correctiveCmd}\` manually, or set YAW_MCP_AUTO_UPGRADE=0 to silence this.`,
|
|
5332
5921
|
{ code }
|
|
5333
5922
|
);
|
|
5334
5923
|
}
|
|
5335
5924
|
});
|
|
5336
5925
|
child.on("error", (err) => {
|
|
5337
|
-
|
|
5926
|
+
errorFired = true;
|
|
5927
|
+
log("warn", `yaw-mcp self-upgrade: ${cmd} spawn failed`, { error: err?.message });
|
|
5338
5928
|
});
|
|
5339
5929
|
}
|
|
5340
5930
|
async function maybeAutoUpgrade(deps = {}) {
|
|
5341
5931
|
const optOut = process.env.YAW_MCP_AUTO_UPGRADE;
|
|
5342
5932
|
if (optOut === "0" || optOut?.toLowerCase() === "false") return;
|
|
5343
|
-
const current = deps.currentVersion ?? (true ? "0.
|
|
5933
|
+
const current = deps.currentVersion ?? (true ? "0.62.0" : "dev");
|
|
5344
5934
|
if (current === "dev") return;
|
|
5345
5935
|
const method = (deps.isSeaImpl ? await deps.isSeaImpl() : await detectSea()) ? "binary" : detectInstallMethod(deps.argvPath ?? process.argv[1]);
|
|
5346
5936
|
const latest = await (deps.fetchLatestImpl ?? fetchLatestVersion2)();
|
|
@@ -5365,11 +5955,23 @@ async function maybeAutoUpgrade(deps = {}) {
|
|
|
5365
5955
|
log("info", "yaw-mcp (standalone binary) is behind npm; download the latest build to update", { current, latest });
|
|
5366
5956
|
return;
|
|
5367
5957
|
}
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5958
|
+
if (method === "npx") {
|
|
5959
|
+
log("info", "yaw-mcp is out of date; restart your MCP client to pick up the latest version", {
|
|
5960
|
+
current,
|
|
5961
|
+
latest,
|
|
5962
|
+
method
|
|
5963
|
+
});
|
|
5964
|
+
} else {
|
|
5965
|
+
log(
|
|
5966
|
+
"info",
|
|
5967
|
+
"yaw-mcp is out of date; run `yaw-mcp upgrade --run` to update this install (a restart won't refresh a stale global)",
|
|
5968
|
+
{
|
|
5969
|
+
current,
|
|
5970
|
+
latest,
|
|
5971
|
+
method
|
|
5972
|
+
}
|
|
5973
|
+
);
|
|
5974
|
+
}
|
|
5373
5975
|
}
|
|
5374
5976
|
|
|
5375
5977
|
// src/compliance.ts
|
|
@@ -5671,6 +6273,28 @@ function resolveArgs(args, bindings) {
|
|
|
5671
6273
|
}
|
|
5672
6274
|
return args;
|
|
5673
6275
|
}
|
|
6276
|
+
function collectRefDeps(args) {
|
|
6277
|
+
const deps = /* @__PURE__ */ new Set();
|
|
6278
|
+
const walk = (node) => {
|
|
6279
|
+
if (isRefNode(node)) {
|
|
6280
|
+
const tokens = parseRefPath(node.$ref);
|
|
6281
|
+
if (tokens && tokens.length > 0 && typeof tokens[0] === "string") {
|
|
6282
|
+
deps.add(tokens[0]);
|
|
6283
|
+
}
|
|
6284
|
+
return;
|
|
6285
|
+
}
|
|
6286
|
+
if (Array.isArray(node)) {
|
|
6287
|
+
for (const v of node) walk(v);
|
|
6288
|
+
return;
|
|
6289
|
+
}
|
|
6290
|
+
if (node !== null && typeof node === "object") {
|
|
6291
|
+
for (const v of Object.values(node)) walk(v);
|
|
6292
|
+
return;
|
|
6293
|
+
}
|
|
6294
|
+
};
|
|
6295
|
+
walk(args);
|
|
6296
|
+
return Array.from(deps);
|
|
6297
|
+
}
|
|
5674
6298
|
var MAX_EXEC_STEPS = 16;
|
|
5675
6299
|
function validateExecRequest(req) {
|
|
5676
6300
|
if (req === null || typeof req !== "object" || Array.isArray(req)) {
|
|
@@ -5687,6 +6311,7 @@ function validateExecRequest(req) {
|
|
|
5687
6311
|
return { ok: false, message: `too many steps (${steps.length}); max is ${MAX_EXEC_STEPS}` };
|
|
5688
6312
|
}
|
|
5689
6313
|
const seenIds = /* @__PURE__ */ new Set();
|
|
6314
|
+
const positionalSlots = /* @__PURE__ */ new Set();
|
|
5690
6315
|
for (let i = 0; i < steps.length; i++) {
|
|
5691
6316
|
const step = steps[i];
|
|
5692
6317
|
if (step === null || typeof step !== "object" || Array.isArray(step)) {
|
|
@@ -5704,16 +6329,33 @@ function validateExecRequest(req) {
|
|
|
5704
6329
|
return { ok: false, message: `step ${i}: duplicate id "${s.id}"` };
|
|
5705
6330
|
}
|
|
5706
6331
|
seenIds.add(s.id);
|
|
6332
|
+
} else {
|
|
6333
|
+
positionalSlots.add(String(i));
|
|
5707
6334
|
}
|
|
5708
6335
|
if (s.args !== void 0 && (s.args === null || typeof s.args !== "object" || Array.isArray(s.args))) {
|
|
5709
6336
|
return { ok: false, message: `step ${i}: \`args\` must be an object if provided` };
|
|
5710
6337
|
}
|
|
5711
6338
|
}
|
|
6339
|
+
for (const id of seenIds) {
|
|
6340
|
+
if (positionalSlots.has(id)) {
|
|
6341
|
+
return {
|
|
6342
|
+
ok: false,
|
|
6343
|
+
message: `step id "${id}" collides with the positional binding key of an unnamed step; rename it`
|
|
6344
|
+
};
|
|
6345
|
+
}
|
|
6346
|
+
}
|
|
5712
6347
|
if (ret !== void 0) {
|
|
5713
6348
|
if (typeof ret !== "string" || ret.length === 0) {
|
|
5714
6349
|
return { ok: false, message: "`return` must be a non-empty step id string" };
|
|
5715
6350
|
}
|
|
5716
|
-
|
|
6351
|
+
const allBindingKeys = new Set(seenIds);
|
|
6352
|
+
for (let i = 0; i < steps.length; i++) {
|
|
6353
|
+
const s = steps[i];
|
|
6354
|
+
if (typeof s.id !== "string" || s.id.length === 0) {
|
|
6355
|
+
allBindingKeys.add(String(i));
|
|
6356
|
+
}
|
|
6357
|
+
}
|
|
6358
|
+
if (!allBindingKeys.has(ret)) {
|
|
5717
6359
|
return { ok: false, message: `\`return\` references unknown step id "${ret}"` };
|
|
5718
6360
|
}
|
|
5719
6361
|
}
|
|
@@ -5726,18 +6368,18 @@ function stepBindingKey(step, index) {
|
|
|
5726
6368
|
// src/guide.ts
|
|
5727
6369
|
import { readFile as readFile9 } from "fs/promises";
|
|
5728
6370
|
var GUIDE_READ_TIMEOUT_MS = 1e3;
|
|
5729
|
-
async function readGuide(
|
|
6371
|
+
async function readGuide(path5, scope) {
|
|
5730
6372
|
let raw;
|
|
5731
6373
|
try {
|
|
5732
6374
|
raw = await Promise.race([
|
|
5733
|
-
readFile9(
|
|
6375
|
+
readFile9(path5, "utf8"),
|
|
5734
6376
|
new Promise(
|
|
5735
6377
|
(_, reject) => setTimeout(() => reject(new Error("guide read timeout")), GUIDE_READ_TIMEOUT_MS)
|
|
5736
6378
|
)
|
|
5737
6379
|
]);
|
|
5738
6380
|
} catch (err) {
|
|
5739
6381
|
if (err instanceof Error && err.message === "guide read timeout") {
|
|
5740
|
-
log("warn", "Guide read timed out", { path:
|
|
6382
|
+
log("warn", "Guide read timed out", { path: path5 });
|
|
5741
6383
|
}
|
|
5742
6384
|
return null;
|
|
5743
6385
|
}
|
|
@@ -5745,7 +6387,7 @@ async function readGuide(path3, scope) {
|
|
|
5745
6387
|
if (content.length === 0) {
|
|
5746
6388
|
return null;
|
|
5747
6389
|
}
|
|
5748
|
-
return { scope, path:
|
|
6390
|
+
return { scope, path: path5, content };
|
|
5749
6391
|
}
|
|
5750
6392
|
async function loadUserGuide(home) {
|
|
5751
6393
|
const p = guidePath(userConfigDir(home));
|
|
@@ -5933,6 +6575,11 @@ var LEARNING_MIN_BOOST = 0.9;
|
|
|
5933
6575
|
var PENALTY_RATE_THRESHOLD = 0.8;
|
|
5934
6576
|
var SATURATION_AT = 10;
|
|
5935
6577
|
var LearningStore = class {
|
|
6578
|
+
// Growth bound: the map keys on namespace, and recordDispatch is only
|
|
6579
|
+
// reached with validated/bounded namespaces from the dispatch path, so
|
|
6580
|
+
// cardinality is capped by the number of distinct configured namespaces.
|
|
6581
|
+
// No explicit cap is needed unless untrusted strings can reach
|
|
6582
|
+
// recordDispatch (they cannot today).
|
|
5936
6583
|
usage = /* @__PURE__ */ new Map();
|
|
5937
6584
|
recordDispatch(namespace) {
|
|
5938
6585
|
const prev = this.usage.get(namespace);
|
|
@@ -5950,6 +6597,50 @@ var LearningStore = class {
|
|
|
5950
6597
|
lastUsedAt: Date.now()
|
|
5951
6598
|
});
|
|
5952
6599
|
}
|
|
6600
|
+
// Graded outcome in [0,1] for a single dispatched proxy call. This is
|
|
6601
|
+
// the SOUND replacement for the binary recordDispatch + recordSuccess
|
|
6602
|
+
// pair on the proxy path: a 200/empty/error-shaped reply no longer banks
|
|
6603
|
+
// full credit (the reward is computed by reward.ts/computeOutcomeReward).
|
|
6604
|
+
// One call records BOTH the dispatch (denominator) and the graded credit
|
|
6605
|
+
// (numerator), so callers no longer pair recordDispatch with a
|
|
6606
|
+
// conditional recordSuccess. reward is clamped to [0,1].
|
|
6607
|
+
recordOutcome(namespace, reward) {
|
|
6608
|
+
const r = Number.isFinite(reward) ? Math.min(1, Math.max(0, reward)) : 0;
|
|
6609
|
+
const prev = this.usage.get(namespace);
|
|
6610
|
+
this.usage.set(namespace, {
|
|
6611
|
+
dispatched: (prev?.dispatched ?? 0) + 1,
|
|
6612
|
+
succeeded: (prev?.succeeded ?? 0) + r,
|
|
6613
|
+
lastUsedAt: Date.now()
|
|
6614
|
+
});
|
|
6615
|
+
}
|
|
6616
|
+
// Synthetic failed observation with NO success credit. Used by the
|
|
6617
|
+
// re-dispatch routing-miss signal (redispatch.ts): when the model
|
|
6618
|
+
// abandons server A and re-routes a similar intent to B, A's earlier
|
|
6619
|
+
// "clean" reply was useless in hindsight, so we depress A's success rate
|
|
6620
|
+
// by one failed observation. Denominator-only — same shape as a
|
|
6621
|
+
// recordDispatch that never sees a matching success.
|
|
6622
|
+
recordMiss(namespace) {
|
|
6623
|
+
const prev = this.usage.get(namespace);
|
|
6624
|
+
this.usage.set(namespace, {
|
|
6625
|
+
dispatched: (prev?.dispatched ?? 0) + 1,
|
|
6626
|
+
succeeded: prev?.succeeded ?? 0,
|
|
6627
|
+
lastUsedAt: Date.now()
|
|
6628
|
+
});
|
|
6629
|
+
}
|
|
6630
|
+
// Apply a DELTA to a namespace's success credit WITHOUT touching the
|
|
6631
|
+
// dispatched count. Used by the optional LLM reward grader (reward-grader.ts)
|
|
6632
|
+
// to revise a previously-recorded heuristic reward in the background: the
|
|
6633
|
+
// caller records the heuristic via recordOutcome immediately, then later
|
|
6634
|
+
// adjusts by (graded - heuristic) once the LLM verdict lands. Adding a delta
|
|
6635
|
+
// (rather than setting an absolute) stays correct under concurrent
|
|
6636
|
+
// recordOutcome calls on the same namespace. No-op for an unknown namespace
|
|
6637
|
+
// (nothing to revise); succeeded is clamped to [0, dispatched].
|
|
6638
|
+
adjustSucceeded(namespace, delta) {
|
|
6639
|
+
const u = this.usage.get(namespace);
|
|
6640
|
+
if (!u || !Number.isFinite(delta)) return;
|
|
6641
|
+
u.succeeded = Math.min(u.dispatched, Math.max(0, u.succeeded + delta));
|
|
6642
|
+
u.lastUsedAt = Date.now();
|
|
6643
|
+
}
|
|
5953
6644
|
get(namespace) {
|
|
5954
6645
|
return this.usage.get(namespace);
|
|
5955
6646
|
}
|
|
@@ -5962,8 +6653,9 @@ var LearningStore = class {
|
|
|
5962
6653
|
boostFactor(namespace) {
|
|
5963
6654
|
const u = this.usage.get(namespace);
|
|
5964
6655
|
if (!u) return 1;
|
|
5965
|
-
|
|
5966
|
-
|
|
6656
|
+
const dispatched = Math.max(u.dispatched, u.succeeded);
|
|
6657
|
+
if (dispatched >= LEARNING_MIN_OBSERVATIONS) {
|
|
6658
|
+
const rate = u.succeeded / dispatched;
|
|
5967
6659
|
if (rate < PENALTY_RATE_THRESHOLD) {
|
|
5968
6660
|
const distance = Math.min(1, (PENALTY_RATE_THRESHOLD - rate) / PENALTY_RATE_THRESHOLD);
|
|
5969
6661
|
return 1 - distance * (1 - LEARNING_MIN_BOOST);
|
|
@@ -6007,7 +6699,11 @@ var LearningStore = class {
|
|
|
6007
6699
|
loadSnapshot(snapshot) {
|
|
6008
6700
|
this.usage.clear();
|
|
6009
6701
|
for (const [ns, usage] of Object.entries(snapshot)) {
|
|
6010
|
-
|
|
6702
|
+
const dispatched = Number.isFinite(usage.dispatched) ? Math.max(0, usage.dispatched) : 0;
|
|
6703
|
+
const succeededRaw = Number.isFinite(usage.succeeded) ? Math.max(0, usage.succeeded) : 0;
|
|
6704
|
+
const succeeded = Math.min(succeededRaw, dispatched);
|
|
6705
|
+
const lastUsedAt = Number.isFinite(usage.lastUsedAt) ? usage.lastUsedAt : 0;
|
|
6706
|
+
this.usage.set(ns, { dispatched, succeeded, lastUsedAt });
|
|
6011
6707
|
}
|
|
6012
6708
|
}
|
|
6013
6709
|
};
|
|
@@ -6137,6 +6833,7 @@ var META_TOOLS = {
|
|
|
6137
6833
|
},
|
|
6138
6834
|
budget: {
|
|
6139
6835
|
type: "number",
|
|
6836
|
+
default: 1,
|
|
6140
6837
|
description: "How many top-ranked servers to load into the session. Defaults to 1. Cap is 10. Raise only when one task genuinely spans multiple servers."
|
|
6141
6838
|
}
|
|
6142
6839
|
},
|
|
@@ -6684,8 +7381,9 @@ async function routeResourceRead(uri, resourceRoutes, activeConnections, builtin
|
|
|
6684
7381
|
const result = await connection.client.readResource({ uri: route.originalUri });
|
|
6685
7382
|
return result;
|
|
6686
7383
|
} catch (err) {
|
|
6687
|
-
|
|
6688
|
-
|
|
7384
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7385
|
+
log("error", "Resource read failed", { uri, namespace: route.namespace, error: message });
|
|
7386
|
+
return { contents: [{ uri, text: `Error: ${message}` }] };
|
|
6689
7387
|
}
|
|
6690
7388
|
}
|
|
6691
7389
|
async function routePromptGet(name, args, promptRoutes, activeConnections) {
|
|
@@ -6703,8 +7401,9 @@ async function routePromptGet(name, args, promptRoutes, activeConnections) {
|
|
|
6703
7401
|
const result = await connection.client.getPrompt({ name: route.originalName, arguments: args });
|
|
6704
7402
|
return result;
|
|
6705
7403
|
} catch (err) {
|
|
6706
|
-
|
|
6707
|
-
|
|
7404
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7405
|
+
log("error", "Prompt get failed", { name, namespace: route.namespace, error: message });
|
|
7406
|
+
return { messages: [{ role: "user", content: { type: "text", text: `Error: ${message}` } }] };
|
|
6708
7407
|
}
|
|
6709
7408
|
}
|
|
6710
7409
|
async function routeToolCall(toolName, args, toolRoutes, activeConnections) {
|
|
@@ -6791,12 +7490,14 @@ function pruneWhitespace(text) {
|
|
|
6791
7490
|
function pruneJson(value) {
|
|
6792
7491
|
if (value === null || value === void 0) return void 0;
|
|
6793
7492
|
if (Array.isArray(value)) {
|
|
6794
|
-
|
|
6795
|
-
|
|
7493
|
+
if (value.length === 0) return void 0;
|
|
7494
|
+
const cleaned = value.map((el) => {
|
|
6796
7495
|
const pv = pruneJson(el);
|
|
6797
|
-
if (pv !== void 0)
|
|
6798
|
-
|
|
6799
|
-
|
|
7496
|
+
if (pv !== void 0) return pv;
|
|
7497
|
+
if (el !== null && typeof el === "object" && !Array.isArray(el)) return {};
|
|
7498
|
+
return null;
|
|
7499
|
+
});
|
|
7500
|
+
return cleaned;
|
|
6800
7501
|
}
|
|
6801
7502
|
if (typeof value === "object") {
|
|
6802
7503
|
const out = {};
|
|
@@ -6849,122 +7550,87 @@ function formatToolNotFound(server, toolName, availableTools) {
|
|
|
6849
7550
|
return `"${toolName}" not found on "${server.namespace}". Available tools: ${names}`;
|
|
6850
7551
|
}
|
|
6851
7552
|
|
|
6852
|
-
// src/
|
|
6853
|
-
var
|
|
6854
|
-
var
|
|
6855
|
-
var
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
}
|
|
6887
|
-
return count;
|
|
6888
|
-
}
|
|
6889
|
-
function bm25Score(queryTerms, fields, idf, avgFieldLen, idfValues) {
|
|
6890
|
-
let score = 0;
|
|
6891
|
-
const seen = /* @__PURE__ */ new Set();
|
|
6892
|
-
for (const term of queryTerms) {
|
|
6893
|
-
if (seen.has(term)) continue;
|
|
6894
|
-
seen.add(term);
|
|
6895
|
-
const termIdf = idfValues.get(term);
|
|
6896
|
-
if (termIdf === void 0 || termIdf <= 0) continue;
|
|
6897
|
-
for (const [fieldName, weight] of Object.entries(FIELD_WEIGHTS)) {
|
|
6898
|
-
const fieldTokens = fields[fieldName];
|
|
6899
|
-
if (fieldTokens.length === 0) continue;
|
|
6900
|
-
const tf = termFreq(fieldTokens, term);
|
|
6901
|
-
if (tf === 0) continue;
|
|
6902
|
-
const avg = avgFieldLen[fieldName] || 1;
|
|
6903
|
-
const normLen = 1 - B + B * (fieldTokens.length / avg);
|
|
6904
|
-
const numerator = tf * (K1 + 1);
|
|
6905
|
-
const denominator = tf + K1 * normLen;
|
|
6906
|
-
score += weight * termIdf * (numerator / denominator);
|
|
7553
|
+
// src/redispatch.ts
|
|
7554
|
+
var RING_CAP = 8;
|
|
7555
|
+
var WINDOW_MS = 12e4;
|
|
7556
|
+
var JACCARD_THRESHOLD = 0.4;
|
|
7557
|
+
var MIN_SHARED_TOKENS = 3;
|
|
7558
|
+
function jaccard(a, b) {
|
|
7559
|
+
if (a.size === 0 || b.size === 0) return { score: 0, shared: 0 };
|
|
7560
|
+
let shared = 0;
|
|
7561
|
+
for (const t of a) {
|
|
7562
|
+
if (b.has(t)) shared++;
|
|
7563
|
+
}
|
|
7564
|
+
const union = a.size + b.size - shared;
|
|
7565
|
+
return { score: union === 0 ? 0 : shared / union, shared };
|
|
7566
|
+
}
|
|
7567
|
+
var RedispatchTracker = class {
|
|
7568
|
+
// Newest record is at the END of the array. Capped at RING_CAP by shifting
|
|
7569
|
+
// the oldest off the front.
|
|
7570
|
+
ring = [];
|
|
7571
|
+
// Record a new dispatch decision. intentTokens = tokenize(intent).
|
|
7572
|
+
push(namespace, intentTokens, now) {
|
|
7573
|
+
for (const rec of this.ring) {
|
|
7574
|
+
if (rec.namespace === namespace && !rec.consumed) rec.furtherUse = true;
|
|
7575
|
+
}
|
|
7576
|
+
this.ring.push({
|
|
7577
|
+
namespace,
|
|
7578
|
+
tokens: new Set(intentTokens),
|
|
7579
|
+
time: now,
|
|
7580
|
+
replied: false,
|
|
7581
|
+
cleanReply: false,
|
|
7582
|
+
furtherUse: false,
|
|
7583
|
+
consumed: false
|
|
7584
|
+
});
|
|
7585
|
+
while (this.ring.length > RING_CAP) {
|
|
7586
|
+
this.ring.shift();
|
|
6907
7587
|
}
|
|
6908
7588
|
}
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
df.set(term, (df.get(term) ?? 0) + 1);
|
|
7589
|
+
// Called from the proxy path when the dispatched server replied. We only
|
|
7590
|
+
// care about the MOST-RECENT record for `namespace` (that's the dispatch
|
|
7591
|
+
// the reply belongs to).
|
|
7592
|
+
//
|
|
7593
|
+
// First clean reply for that record sets cleanReply=true. Any SUBSEQUENT
|
|
7594
|
+
// call for that same record sets furtherUse=true (the server kept getting
|
|
7595
|
+
// used -> NOT abandoned, so it can never be a loser).
|
|
7596
|
+
markReply(namespace, clean) {
|
|
7597
|
+
for (let i = this.ring.length - 1; i >= 0; i--) {
|
|
7598
|
+
const rec = this.ring[i];
|
|
7599
|
+
if (rec.namespace !== namespace) continue;
|
|
7600
|
+
if (!rec.replied) {
|
|
7601
|
+
rec.replied = true;
|
|
7602
|
+
if (clean) rec.cleanReply = true;
|
|
7603
|
+
} else {
|
|
7604
|
+
rec.furtherUse = true;
|
|
7605
|
+
}
|
|
7606
|
+
return;
|
|
6928
7607
|
}
|
|
6929
7608
|
}
|
|
6930
|
-
|
|
6931
|
-
for
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
description: totalLen.description / N,
|
|
6952
|
-
toolName: totalLen.toolName / N,
|
|
6953
|
-
toolDescription: totalLen.toolDescription / N
|
|
6954
|
-
};
|
|
6955
|
-
const results = [];
|
|
6956
|
-
for (const { server, fields } of docsWithFields) {
|
|
6957
|
-
const score = bm25Score(queryTerms, fields, /* @__PURE__ */ new Map(), avgFieldLen, idfValues);
|
|
6958
|
-
if (score > 0) {
|
|
6959
|
-
results.push({ namespace: server.namespace, score });
|
|
7609
|
+
// When a new dispatch (newNamespace) lands, look back over the recent ring
|
|
7610
|
+
// for an ABANDONED record (cleanReply && !furtherUse) on a DIFFERENT
|
|
7611
|
+
// namespace whose intent is SIMILAR to newTokens and within the time
|
|
7612
|
+
// window. If found, that earlier server was the wrong route -> return
|
|
7613
|
+
// { loser }. isExcluded(a, b) returns true when a->b is a known legitimate
|
|
7614
|
+
// multi-server chain (curated bundle / detected pack) and must NOT be
|
|
7615
|
+
// treated as a miss. Returns null when no miss.
|
|
7616
|
+
detectMiss(newNamespace, newTokens, now, isExcluded) {
|
|
7617
|
+
const newSet = new Set(newTokens);
|
|
7618
|
+
for (let i = this.ring.length - 1; i >= 0; i--) {
|
|
7619
|
+
const rec = this.ring[i];
|
|
7620
|
+
if (rec.consumed) continue;
|
|
7621
|
+
if (rec.namespace === newNamespace) continue;
|
|
7622
|
+
if (!rec.cleanReply || rec.furtherUse) continue;
|
|
7623
|
+
if (now - rec.time > WINDOW_MS) continue;
|
|
7624
|
+
if (isExcluded(rec.namespace, newNamespace)) continue;
|
|
7625
|
+
const { score, shared } = jaccard(rec.tokens, newSet);
|
|
7626
|
+
if (shared < MIN_SHARED_TOKENS) continue;
|
|
7627
|
+
if (score < JACCARD_THRESHOLD) continue;
|
|
7628
|
+
rec.consumed = true;
|
|
7629
|
+
return { loser: rec.namespace };
|
|
6960
7630
|
}
|
|
7631
|
+
return null;
|
|
6961
7632
|
}
|
|
6962
|
-
|
|
6963
|
-
if (b.score !== a.score) return b.score - a.score;
|
|
6964
|
-
return a.namespace < b.namespace ? -1 : 1;
|
|
6965
|
-
});
|
|
6966
|
-
return results;
|
|
6967
|
-
}
|
|
7633
|
+
};
|
|
6968
7634
|
|
|
6969
7635
|
// src/rerank.ts
|
|
6970
7636
|
import { request as request7 } from "undici";
|
|
@@ -7064,18 +7730,150 @@ async function callLegacyRerank(payload) {
|
|
|
7064
7730
|
return null;
|
|
7065
7731
|
}
|
|
7066
7732
|
}
|
|
7067
|
-
async function readTeamCookie() {
|
|
7068
|
-
const teamSync = await import("./team-sync-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7733
|
+
async function readTeamCookie() {
|
|
7734
|
+
const teamSync = await import("./team-sync-B4R6FLKR.js");
|
|
7735
|
+
return teamSync.getCachedCookie();
|
|
7736
|
+
}
|
|
7737
|
+
|
|
7738
|
+
// src/reward-grader.ts
|
|
7739
|
+
function isRewardGraderEnabled() {
|
|
7740
|
+
const raw = process.env.YAW_MCP_REWARD_GRADER;
|
|
7741
|
+
if (!raw) return false;
|
|
7742
|
+
const v = raw.trim().toLowerCase();
|
|
7743
|
+
return v === "1" || v === "true";
|
|
7744
|
+
}
|
|
7745
|
+
function isUncertainReward(heuristic) {
|
|
7746
|
+
return heuristic >= 0.2 && heuristic <= 0.3;
|
|
7747
|
+
}
|
|
7748
|
+
var GRADER_MAX_TOKENS = 8;
|
|
7749
|
+
var GRADER_TIMEOUT_MS = 4e3;
|
|
7750
|
+
var RESULT_SNIPPET_LEN = 600;
|
|
7751
|
+
function firstResultText(result) {
|
|
7752
|
+
const content = result.content;
|
|
7753
|
+
if (Array.isArray(content)) {
|
|
7754
|
+
for (const block of content) {
|
|
7755
|
+
if (typeof block.text === "string" && block.text.trim().length > 0) {
|
|
7756
|
+
const t = block.text.trim();
|
|
7757
|
+
return t.length > RESULT_SNIPPET_LEN ? `${t.slice(0, RESULT_SNIPPET_LEN)}...` : t;
|
|
7758
|
+
}
|
|
7759
|
+
}
|
|
7760
|
+
}
|
|
7761
|
+
return "(empty result)";
|
|
7762
|
+
}
|
|
7763
|
+
function buildGraderPrompt(ctx) {
|
|
7764
|
+
const lines = ["You are grading whether an MCP tool call accomplished its goal."];
|
|
7765
|
+
if (ctx.intent && ctx.intent.trim().length > 0) {
|
|
7766
|
+
lines.push("", `Goal: ${ctx.intent.trim()}`);
|
|
7767
|
+
}
|
|
7768
|
+
lines.push(
|
|
7769
|
+
"",
|
|
7770
|
+
`Tool called: ${ctx.toolName}`,
|
|
7771
|
+
`Result (truncated): ${ctx.resultText}`,
|
|
7772
|
+
"",
|
|
7773
|
+
"Did the tool call accomplish the goal / return a useful, on-task result?",
|
|
7774
|
+
"Reply with ONLY one word: YES, PARTIAL, or NO."
|
|
7775
|
+
);
|
|
7776
|
+
return lines.join("\n");
|
|
7777
|
+
}
|
|
7778
|
+
function parseGrade(text) {
|
|
7779
|
+
const m = /\b(yes|partial|no)\b/i.exec(text);
|
|
7780
|
+
if (!m) return null;
|
|
7781
|
+
switch (m[1].toLowerCase()) {
|
|
7782
|
+
case "yes":
|
|
7783
|
+
return 1;
|
|
7784
|
+
case "partial":
|
|
7785
|
+
return 0.5;
|
|
7786
|
+
default:
|
|
7787
|
+
return 0;
|
|
7788
|
+
}
|
|
7789
|
+
}
|
|
7790
|
+
async function gradeOutcomeViaSampling(server, ctx) {
|
|
7791
|
+
const caps = server.getClientCapabilities();
|
|
7792
|
+
if (!caps?.sampling) return null;
|
|
7793
|
+
const prompt = buildGraderPrompt(ctx);
|
|
7794
|
+
try {
|
|
7795
|
+
const result = await withTimeout(
|
|
7796
|
+
server.createMessage({
|
|
7797
|
+
messages: [{ role: "user", content: { type: "text", text: prompt } }],
|
|
7798
|
+
maxTokens: GRADER_MAX_TOKENS,
|
|
7799
|
+
includeContext: "none"
|
|
7800
|
+
}),
|
|
7801
|
+
GRADER_TIMEOUT_MS
|
|
7802
|
+
);
|
|
7803
|
+
if (!result || typeof result !== "object" || !("content" in result) || !result.content) return null;
|
|
7804
|
+
const text = extractText(result.content);
|
|
7805
|
+
if (!text) return null;
|
|
7806
|
+
return parseGrade(text);
|
|
7807
|
+
} catch (err) {
|
|
7808
|
+
log("warn", "Reward grader sampling failed", { error: err instanceof Error ? err.message : String(err) });
|
|
7809
|
+
return null;
|
|
7810
|
+
}
|
|
7811
|
+
}
|
|
7812
|
+
function withTimeout(p, ms) {
|
|
7813
|
+
return new Promise((resolve7) => {
|
|
7814
|
+
const timer = setTimeout(() => resolve7(null), ms);
|
|
7815
|
+
if (typeof timer === "object" && timer && "unref" in timer) timer.unref();
|
|
7816
|
+
p.then(
|
|
7817
|
+
(v) => {
|
|
7818
|
+
clearTimeout(timer);
|
|
7819
|
+
resolve7(v);
|
|
7820
|
+
},
|
|
7821
|
+
() => {
|
|
7822
|
+
clearTimeout(timer);
|
|
7823
|
+
resolve7(null);
|
|
7824
|
+
}
|
|
7825
|
+
);
|
|
7826
|
+
});
|
|
7827
|
+
}
|
|
7828
|
+
function extractText(content) {
|
|
7829
|
+
if (Array.isArray(content)) {
|
|
7830
|
+
return content.map((c) => c && typeof c === "object" && "type" in c && c.type === "text" && "text" in c ? String(c.text) : "").filter(Boolean).join("\n");
|
|
7831
|
+
}
|
|
7832
|
+
if (content && typeof content === "object" && "type" in content) {
|
|
7833
|
+
const block = content;
|
|
7834
|
+
if (block.type === "text" && typeof block.text === "string") return block.text;
|
|
7835
|
+
}
|
|
7836
|
+
return "";
|
|
7837
|
+
}
|
|
7838
|
+
|
|
7839
|
+
// src/reward.ts
|
|
7840
|
+
var ERROR_SHAPED_CATEGORIES = /* @__PURE__ */ new Set([
|
|
7841
|
+
"validation_error",
|
|
7842
|
+
"timeout",
|
|
7843
|
+
"unauthorized",
|
|
7844
|
+
"unknown_tool",
|
|
7845
|
+
"connection_lost",
|
|
7846
|
+
"rate_limited",
|
|
7847
|
+
"not_found"
|
|
7848
|
+
]);
|
|
7849
|
+
function firstTextBlock(result) {
|
|
7850
|
+
const content = result.content;
|
|
7851
|
+
if (!content || content.length === 0) return void 0;
|
|
7852
|
+
for (const block of content) {
|
|
7853
|
+
if (typeof block.text === "string" && block.text.trim().length > 0) return block.text;
|
|
7854
|
+
}
|
|
7855
|
+
return void 0;
|
|
7856
|
+
}
|
|
7857
|
+
function isEmptyBody(result) {
|
|
7858
|
+
const content = result.content;
|
|
7859
|
+
if (!content || content.length === 0) return true;
|
|
7860
|
+
for (const block of content) {
|
|
7861
|
+
if (typeof block.text === "string" && block.text.trim().length > 0) {
|
|
7862
|
+
return false;
|
|
7863
|
+
}
|
|
7864
|
+
}
|
|
7865
|
+
return true;
|
|
7866
|
+
}
|
|
7867
|
+
function computeOutcomeReward(result) {
|
|
7868
|
+
if (result.isError === true) return 0;
|
|
7869
|
+
const text = firstTextBlock(result);
|
|
7870
|
+
if (text !== void 0) {
|
|
7871
|
+
if (ERROR_SHAPED_CATEGORIES.has(classifyError(text))) {
|
|
7872
|
+
return 0.2;
|
|
7873
|
+
}
|
|
7078
7874
|
}
|
|
7875
|
+
if (isEmptyBody(result)) return 0.3;
|
|
7876
|
+
return 1;
|
|
7079
7877
|
}
|
|
7080
7878
|
|
|
7081
7879
|
// src/runtime-detect.ts
|
|
@@ -7089,55 +7887,58 @@ function initRuntimeDetect(url, tok) {
|
|
|
7089
7887
|
apiUrl4 = url;
|
|
7090
7888
|
token4 = tok;
|
|
7091
7889
|
}
|
|
7890
|
+
var PYTHON_CANDIDATES = process.platform === "win32" ? [
|
|
7891
|
+
{ bin: "py", args: ["-3", "--version"] },
|
|
7892
|
+
{ bin: "python", args: ["--version"] },
|
|
7893
|
+
{ bin: "python3", args: ["--version"] }
|
|
7894
|
+
] : [
|
|
7895
|
+
{ bin: "python3", args: ["--version"] },
|
|
7896
|
+
{ bin: "python", args: ["--version"] }
|
|
7897
|
+
];
|
|
7092
7898
|
var PROBES = {
|
|
7093
7899
|
node: {
|
|
7094
|
-
bin: "node",
|
|
7095
|
-
args: ["--version"],
|
|
7900
|
+
candidates: [{ bin: "node", args: ["--version"] }],
|
|
7096
7901
|
parse: (out) => out.trim().replace(/^v/, "") || true
|
|
7097
7902
|
},
|
|
7098
7903
|
npx: {
|
|
7099
|
-
bin: "npx",
|
|
7100
|
-
args: ["--version"],
|
|
7904
|
+
candidates: [{ bin: "npx", args: ["--version"] }],
|
|
7101
7905
|
parse: (out) => out.trim() || true
|
|
7102
7906
|
},
|
|
7103
7907
|
python: {
|
|
7104
|
-
|
|
7105
|
-
args: ["--version"],
|
|
7908
|
+
candidates: PYTHON_CANDIDATES,
|
|
7106
7909
|
parse: (out) => {
|
|
7107
7910
|
const m = out.match(/Python\s+(\d+\.\d+\.\d+)/);
|
|
7108
7911
|
return m ? m[1] : true;
|
|
7109
7912
|
}
|
|
7110
7913
|
},
|
|
7111
7914
|
uvx: {
|
|
7112
|
-
bin: "uvx",
|
|
7113
|
-
args: ["--version"],
|
|
7915
|
+
candidates: [{ bin: "uvx", args: ["--version"] }],
|
|
7114
7916
|
parse: (out) => {
|
|
7115
7917
|
const m = out.match(/(\d+\.\d+\.\d+)/);
|
|
7116
7918
|
return m ? m[1] : true;
|
|
7117
7919
|
}
|
|
7118
7920
|
},
|
|
7119
7921
|
docker: {
|
|
7120
|
-
bin: "docker",
|
|
7121
|
-
args: ["--version"],
|
|
7922
|
+
candidates: [{ bin: "docker", args: ["--version"] }],
|
|
7122
7923
|
parse: (out) => {
|
|
7123
7924
|
const m = out.match(/Docker version (\d+\.\d+\.\d+)/);
|
|
7124
7925
|
return m ? m[1] : true;
|
|
7125
7926
|
}
|
|
7126
7927
|
}
|
|
7127
7928
|
};
|
|
7128
|
-
async function
|
|
7129
|
-
return new Promise((
|
|
7929
|
+
async function probeCandidate(c, parse) {
|
|
7930
|
+
return new Promise((resolve7) => {
|
|
7130
7931
|
let settled = false;
|
|
7131
7932
|
const settle = (v) => {
|
|
7132
7933
|
if (settled) return;
|
|
7133
7934
|
settled = true;
|
|
7134
|
-
|
|
7935
|
+
resolve7(v);
|
|
7135
7936
|
};
|
|
7136
7937
|
let stdout = "";
|
|
7137
7938
|
let stderr = "";
|
|
7138
7939
|
let child;
|
|
7139
7940
|
try {
|
|
7140
|
-
child = spawn4(
|
|
7941
|
+
child = spawn4(c.bin, c.args, {
|
|
7141
7942
|
stdio: ["ignore", "pipe", "pipe"],
|
|
7142
7943
|
// Windows needs a shell for PATH lookup of .cmd/.bat shims —
|
|
7143
7944
|
// node/npx/uvx arrive as `npx.cmd` in PATH, and native spawn
|
|
@@ -7180,16 +7981,22 @@ async function probe(name, p) {
|
|
|
7180
7981
|
return;
|
|
7181
7982
|
}
|
|
7182
7983
|
const text = stdout || stderr;
|
|
7183
|
-
if (
|
|
7184
|
-
const parsed =
|
|
7984
|
+
if (parse) {
|
|
7985
|
+
const parsed = parse(text);
|
|
7185
7986
|
settle(parsed);
|
|
7186
7987
|
} else {
|
|
7187
7988
|
settle(true);
|
|
7188
7989
|
}
|
|
7189
7990
|
});
|
|
7190
|
-
void name;
|
|
7191
7991
|
});
|
|
7192
7992
|
}
|
|
7993
|
+
async function probe(_name, p) {
|
|
7994
|
+
for (const c of p.candidates) {
|
|
7995
|
+
const result = await probeCandidate(c, p.parse);
|
|
7996
|
+
if (result !== false) return result;
|
|
7997
|
+
}
|
|
7998
|
+
return false;
|
|
7999
|
+
}
|
|
7193
8000
|
async function detectRuntimes() {
|
|
7194
8001
|
const entries = await Promise.all(
|
|
7195
8002
|
Object.entries(PROBES).map(async ([name, p]) => [name, await probe(name, p)])
|
|
@@ -7233,6 +8040,9 @@ async function reportRuntimes() {
|
|
|
7233
8040
|
// src/sampling-rank.ts
|
|
7234
8041
|
var SAMPLING_TIEBREAK_RATIO = 0.9;
|
|
7235
8042
|
var SAMPLING_MAX_TOKENS = 120;
|
|
8043
|
+
var MAX_SAMPLES = 5;
|
|
8044
|
+
var SAMPLING_TIMEOUT_MS = 2e3;
|
|
8045
|
+
var AGGRESSIVE_AMBIGUITY_THRESHOLD = 0.6;
|
|
7236
8046
|
function shouldTiebreak(ranked, ratio = SAMPLING_TIEBREAK_RATIO) {
|
|
7237
8047
|
if (ranked.length < 2) return false;
|
|
7238
8048
|
const [top, second] = ranked;
|
|
@@ -7242,7 +8052,7 @@ function shouldTiebreak(ranked, ratio = SAMPLING_TIEBREAK_RATIO) {
|
|
|
7242
8052
|
function buildTiebreakPrompt(intent, candidates) {
|
|
7243
8053
|
const blocks = candidates.map((c, i) => {
|
|
7244
8054
|
const toolLine = c.tools.length > 0 ? c.tools.slice(0, 8).map((t) => t.name).join(", ") : "(no tool metadata yet)";
|
|
7245
|
-
return `${i + 1}. ${c.namespace}${c.description ? `
|
|
8055
|
+
return `${i + 1}. ${c.namespace}${c.description ? ` -- ${c.description}` : ""}
|
|
7246
8056
|
tools: ${toolLine}`;
|
|
7247
8057
|
});
|
|
7248
8058
|
return [
|
|
@@ -7279,27 +8089,7 @@ function parseTiebreakResponse(response, candidates) {
|
|
|
7279
8089
|
function escapeRegex(s) {
|
|
7280
8090
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7281
8091
|
}
|
|
7282
|
-
|
|
7283
|
-
const caps = server.getClientCapabilities();
|
|
7284
|
-
if (!caps?.sampling) return null;
|
|
7285
|
-
if (candidates.length < 2) return null;
|
|
7286
|
-
const prompt = buildTiebreakPrompt(intent, candidates);
|
|
7287
|
-
try {
|
|
7288
|
-
const result = await server.createMessage({
|
|
7289
|
-
messages: [{ role: "user", content: { type: "text", text: prompt } }],
|
|
7290
|
-
maxTokens: SAMPLING_MAX_TOKENS,
|
|
7291
|
-
// Hint that we want a cheap, fast response.
|
|
7292
|
-
includeContext: "none"
|
|
7293
|
-
});
|
|
7294
|
-
const text = result && typeof result === "object" && "content" in result && result.content ? extractText(result.content) : "";
|
|
7295
|
-
if (!text) return null;
|
|
7296
|
-
return parseTiebreakResponse(text, candidates);
|
|
7297
|
-
} catch (err) {
|
|
7298
|
-
log("warn", "Sampling tiebreak failed", { error: err instanceof Error ? err.message : String(err) });
|
|
7299
|
-
return null;
|
|
7300
|
-
}
|
|
7301
|
-
}
|
|
7302
|
-
function extractText(content) {
|
|
8092
|
+
function extractText2(content) {
|
|
7303
8093
|
if (!content) return "";
|
|
7304
8094
|
if (Array.isArray(content)) {
|
|
7305
8095
|
return content.map((c) => c && typeof c === "object" && "type" in c && c.type === "text" && "text" in c ? String(c.text) : "").filter(Boolean).join("\n");
|
|
@@ -7325,6 +8115,109 @@ function buildCandidates(topRanked, serversByNamespace, toolsByNamespace) {
|
|
|
7325
8115
|
}
|
|
7326
8116
|
return out;
|
|
7327
8117
|
}
|
|
8118
|
+
function parseRouteEffort(raw) {
|
|
8119
|
+
if (raw === void 0) return "auto";
|
|
8120
|
+
switch (raw.trim().toLowerCase()) {
|
|
8121
|
+
case "off":
|
|
8122
|
+
return "off";
|
|
8123
|
+
case "aggressive":
|
|
8124
|
+
return "aggressive";
|
|
8125
|
+
default:
|
|
8126
|
+
return "auto";
|
|
8127
|
+
}
|
|
8128
|
+
}
|
|
8129
|
+
function computeAmbiguity(ranked, k = 3) {
|
|
8130
|
+
if (ranked.length < 2) return 0;
|
|
8131
|
+
const top = ranked[0];
|
|
8132
|
+
if (!top || top.score <= 0) return 0;
|
|
8133
|
+
const topK = ranked.slice(0, Math.max(2, k));
|
|
8134
|
+
const second = topK[1];
|
|
8135
|
+
const secondScore = second ? second.score : 0;
|
|
8136
|
+
const inverseMargin = Math.min(1, Math.max(0, secondScore / top.score));
|
|
8137
|
+
const weights = topK.map((c) => Math.max(0, c.score));
|
|
8138
|
+
const total = weights.reduce((a, b) => a + b, 0);
|
|
8139
|
+
let entropy = 0;
|
|
8140
|
+
if (total > 0 && topK.length >= 2) {
|
|
8141
|
+
let h = 0;
|
|
8142
|
+
for (const w of weights) {
|
|
8143
|
+
if (w <= 0) continue;
|
|
8144
|
+
const p = w / total;
|
|
8145
|
+
h -= p * Math.log(p);
|
|
8146
|
+
}
|
|
8147
|
+
const maxH = Math.log(topK.length);
|
|
8148
|
+
entropy = maxH > 0 ? h / maxH : 0;
|
|
8149
|
+
}
|
|
8150
|
+
return Math.max(inverseMargin, entropy);
|
|
8151
|
+
}
|
|
8152
|
+
function shouldSample(ranked, effort) {
|
|
8153
|
+
if (effort === "off") return false;
|
|
8154
|
+
if (effort === "auto") return shouldTiebreak(ranked);
|
|
8155
|
+
return computeAmbiguity(ranked) >= AGGRESSIVE_AMBIGUITY_THRESHOLD;
|
|
8156
|
+
}
|
|
8157
|
+
function sampleCountForEffort(effort) {
|
|
8158
|
+
switch (effort) {
|
|
8159
|
+
case "off":
|
|
8160
|
+
return 0;
|
|
8161
|
+
case "aggressive":
|
|
8162
|
+
return 3;
|
|
8163
|
+
default:
|
|
8164
|
+
return 1;
|
|
8165
|
+
}
|
|
8166
|
+
}
|
|
8167
|
+
async function bestOfNViaSampling(server, intent, candidates, n) {
|
|
8168
|
+
const caps = server.getClientCapabilities();
|
|
8169
|
+
if (!caps?.sampling) return null;
|
|
8170
|
+
if (candidates.length < 2) return null;
|
|
8171
|
+
const samples = Math.min(MAX_SAMPLES, Math.max(1, Math.floor(n)));
|
|
8172
|
+
const prompt = buildTiebreakPrompt(intent, candidates);
|
|
8173
|
+
const sampleOnce = async () => {
|
|
8174
|
+
try {
|
|
8175
|
+
const result = await server.createMessage({
|
|
8176
|
+
messages: [{ role: "user", content: { type: "text", text: prompt } }],
|
|
8177
|
+
maxTokens: SAMPLING_MAX_TOKENS,
|
|
8178
|
+
includeContext: "none"
|
|
8179
|
+
});
|
|
8180
|
+
const text = result && typeof result === "object" && "content" in result && result.content ? extractText2(result.content) : "";
|
|
8181
|
+
if (!text) return null;
|
|
8182
|
+
return parseTiebreakResponse(text, candidates);
|
|
8183
|
+
} catch (err) {
|
|
8184
|
+
log("warn", "Best-of-N sample failed", { error: err instanceof Error ? err.message : String(err) });
|
|
8185
|
+
return null;
|
|
8186
|
+
}
|
|
8187
|
+
};
|
|
8188
|
+
const aggregate2 = (async () => {
|
|
8189
|
+
const results = await Promise.all(Array.from({ length: samples }, () => sampleOnce()));
|
|
8190
|
+
const votes = /* @__PURE__ */ new Map();
|
|
8191
|
+
for (const ns of results) {
|
|
8192
|
+
if (!ns) continue;
|
|
8193
|
+
votes.set(ns, (votes.get(ns) ?? 0) + 1);
|
|
8194
|
+
}
|
|
8195
|
+
if (votes.size === 0) return null;
|
|
8196
|
+
const order = /* @__PURE__ */ new Map();
|
|
8197
|
+
candidates.forEach((c, i) => order.set(c.namespace, i));
|
|
8198
|
+
let winner = null;
|
|
8199
|
+
let bestVotes = -1;
|
|
8200
|
+
let bestRank = Number.POSITIVE_INFINITY;
|
|
8201
|
+
for (const [ns, count] of votes) {
|
|
8202
|
+
const rank = order.get(ns) ?? Number.POSITIVE_INFINITY;
|
|
8203
|
+
if (count > bestVotes || count === bestVotes && rank < bestRank) {
|
|
8204
|
+
winner = ns;
|
|
8205
|
+
bestVotes = count;
|
|
8206
|
+
bestRank = rank;
|
|
8207
|
+
}
|
|
8208
|
+
}
|
|
8209
|
+
return winner;
|
|
8210
|
+
})();
|
|
8211
|
+
let timer;
|
|
8212
|
+
const timeout = new Promise((resolve7) => {
|
|
8213
|
+
timer = setTimeout(() => resolve7(null), SAMPLING_TIMEOUT_MS);
|
|
8214
|
+
});
|
|
8215
|
+
try {
|
|
8216
|
+
return await Promise.race([aggregate2, timeout]);
|
|
8217
|
+
} finally {
|
|
8218
|
+
if (timer) clearTimeout(timer);
|
|
8219
|
+
}
|
|
8220
|
+
}
|
|
7328
8221
|
|
|
7329
8222
|
// src/server-cap.ts
|
|
7330
8223
|
var DEFAULT_SERVER_CAP = 6;
|
|
@@ -7366,7 +8259,7 @@ import { spawn as spawn5 } from "child_process";
|
|
|
7366
8259
|
import { createHash as createHash3 } from "crypto";
|
|
7367
8260
|
import { createWriteStream } from "fs";
|
|
7368
8261
|
import fs from "fs/promises";
|
|
7369
|
-
import
|
|
8262
|
+
import path4 from "path";
|
|
7370
8263
|
import { pipeline } from "stream/promises";
|
|
7371
8264
|
import { request as request9 } from "undici";
|
|
7372
8265
|
var UV_VERSION = "0.11.7";
|
|
@@ -7406,18 +8299,21 @@ async function exists2(p) {
|
|
|
7406
8299
|
}
|
|
7407
8300
|
}
|
|
7408
8301
|
async function onPath(cmd) {
|
|
7409
|
-
return new Promise((
|
|
8302
|
+
return new Promise((resolve7) => {
|
|
7410
8303
|
let settled = false;
|
|
7411
8304
|
const settle = (v) => {
|
|
7412
8305
|
if (settled) return;
|
|
7413
8306
|
settled = true;
|
|
7414
|
-
|
|
8307
|
+
resolve7(v);
|
|
7415
8308
|
};
|
|
7416
8309
|
let child;
|
|
7417
8310
|
try {
|
|
7418
8311
|
child = spawn5(cmd, ["--version"], {
|
|
7419
8312
|
stdio: "ignore",
|
|
7420
|
-
shell
|
|
8313
|
+
// Windows needs a shell for PATHEXT shim resolution (.cmd/.exe)
|
|
8314
|
+
// so `uv --version` finds uv.cmd without an ENOENT false-negative.
|
|
8315
|
+
// Mirrors the PROBES spawn options in runtime-detect.ts.
|
|
8316
|
+
shell: process.platform === "win32",
|
|
7421
8317
|
windowsHide: process.platform === "win32"
|
|
7422
8318
|
});
|
|
7423
8319
|
} catch {
|
|
@@ -7442,10 +8338,24 @@ async function onPath(cmd) {
|
|
|
7442
8338
|
});
|
|
7443
8339
|
});
|
|
7444
8340
|
}
|
|
8341
|
+
var UV_FETCH_TIMEOUT_MS = 3e4;
|
|
7445
8342
|
async function fetchWithRedirects(url, maxHops = 5) {
|
|
7446
8343
|
let current = url;
|
|
7447
8344
|
for (let i = 0; i < maxHops; i++) {
|
|
7448
|
-
|
|
8345
|
+
let res;
|
|
8346
|
+
try {
|
|
8347
|
+
res = await request9(current, {
|
|
8348
|
+
method: "GET",
|
|
8349
|
+
headersTimeout: UV_FETCH_TIMEOUT_MS,
|
|
8350
|
+
bodyTimeout: UV_FETCH_TIMEOUT_MS
|
|
8351
|
+
});
|
|
8352
|
+
} catch (e) {
|
|
8353
|
+
const code = e.code;
|
|
8354
|
+
if (code === "UND_ERR_HEADERS_TIMEOUT" || code === "UND_ERR_BODY_TIMEOUT") {
|
|
8355
|
+
throw new Error(`uv download timed out after ${UV_FETCH_TIMEOUT_MS}ms (${current})`);
|
|
8356
|
+
}
|
|
8357
|
+
throw e;
|
|
8358
|
+
}
|
|
7449
8359
|
if (res.statusCode >= 300 && res.statusCode < 400) {
|
|
7450
8360
|
const loc = res.headers.location;
|
|
7451
8361
|
if (!loc) throw new Error(`Redirect without Location header from ${current}`);
|
|
@@ -7457,13 +8367,29 @@ async function fetchWithRedirects(url, maxHops = 5) {
|
|
|
7457
8367
|
await res.body.dump();
|
|
7458
8368
|
throw new Error(`GET ${current} failed: HTTP ${res.statusCode}`);
|
|
7459
8369
|
}
|
|
7460
|
-
|
|
8370
|
+
try {
|
|
8371
|
+
return Buffer.from(await res.body.arrayBuffer());
|
|
8372
|
+
} catch (e) {
|
|
8373
|
+
const code = e.code;
|
|
8374
|
+
if (code === "UND_ERR_HEADERS_TIMEOUT" || code === "UND_ERR_BODY_TIMEOUT") {
|
|
8375
|
+
throw new Error(`uv download timed out after ${UV_FETCH_TIMEOUT_MS}ms (${current})`);
|
|
8376
|
+
}
|
|
8377
|
+
throw e;
|
|
8378
|
+
}
|
|
7461
8379
|
}
|
|
7462
8380
|
throw new Error(`Too many redirects starting at ${url}`);
|
|
7463
8381
|
}
|
|
7464
8382
|
async function extractArchive(archivePath, destDir) {
|
|
7465
8383
|
await fs.mkdir(destDir, { recursive: true });
|
|
7466
8384
|
if (process.platform === "win32") {
|
|
8385
|
+
for (const [label, p] of [
|
|
8386
|
+
["archivePath", archivePath],
|
|
8387
|
+
["destDir", destDir]
|
|
8388
|
+
]) {
|
|
8389
|
+
if (/['\r\n]/.test(p)) {
|
|
8390
|
+
throw new Error(`Refusing to extract uv archive: ${label} contains a quote or newline (${JSON.stringify(p)})`);
|
|
8391
|
+
}
|
|
8392
|
+
}
|
|
7467
8393
|
await runCommand("powershell.exe", [
|
|
7468
8394
|
"-NoProfile",
|
|
7469
8395
|
"-NonInteractive",
|
|
@@ -7475,7 +8401,7 @@ async function extractArchive(archivePath, destDir) {
|
|
|
7475
8401
|
}
|
|
7476
8402
|
}
|
|
7477
8403
|
function runCommand(cmd, args) {
|
|
7478
|
-
return new Promise((
|
|
8404
|
+
return new Promise((resolve7, reject) => {
|
|
7479
8405
|
const child = spawn5(cmd, args, {
|
|
7480
8406
|
stdio: ["ignore", "pipe", "pipe"],
|
|
7481
8407
|
shell: false,
|
|
@@ -7487,7 +8413,7 @@ function runCommand(cmd, args) {
|
|
|
7487
8413
|
});
|
|
7488
8414
|
child.on("error", reject);
|
|
7489
8415
|
child.on("close", (code) => {
|
|
7490
|
-
if (code === 0)
|
|
8416
|
+
if (code === 0) resolve7();
|
|
7491
8417
|
else reject(new Error(`${cmd} exited ${code}: ${stderr.trim()}`));
|
|
7492
8418
|
});
|
|
7493
8419
|
});
|
|
@@ -7495,7 +8421,7 @@ function runCommand(cmd, args) {
|
|
|
7495
8421
|
async function findBinary(root, name) {
|
|
7496
8422
|
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
7497
8423
|
for (const e of entries) {
|
|
7498
|
-
const full =
|
|
8424
|
+
const full = path4.join(root, e.name);
|
|
7499
8425
|
if (e.isFile() && e.name === name) return full;
|
|
7500
8426
|
if (e.isDirectory()) {
|
|
7501
8427
|
const found = await findBinary(full, name);
|
|
@@ -7506,7 +8432,12 @@ async function findBinary(root, name) {
|
|
|
7506
8432
|
}
|
|
7507
8433
|
var pending = null;
|
|
7508
8434
|
function ensureUv() {
|
|
7509
|
-
|
|
8435
|
+
if (!pending) {
|
|
8436
|
+
pending = resolveUv().catch((err) => {
|
|
8437
|
+
pending = null;
|
|
8438
|
+
return Promise.reject(err);
|
|
8439
|
+
});
|
|
8440
|
+
}
|
|
7510
8441
|
return pending;
|
|
7511
8442
|
}
|
|
7512
8443
|
async function resolveUv() {
|
|
@@ -7517,8 +8448,8 @@ async function resolveUv() {
|
|
|
7517
8448
|
`No prebuilt uv binary for ${process.platform}/${process.arch}. Install uv manually: https://docs.astral.sh/uv/`
|
|
7518
8449
|
);
|
|
7519
8450
|
}
|
|
7520
|
-
const installDir =
|
|
7521
|
-
const finalBin =
|
|
8451
|
+
const installDir = path4.join(cacheDir(), "uv", UV_VERSION);
|
|
8452
|
+
const finalBin = path4.join(installDir, binName());
|
|
7522
8453
|
if (await exists2(finalBin)) return finalBin;
|
|
7523
8454
|
await fs.mkdir(installDir, { recursive: true });
|
|
7524
8455
|
log("info", "Bootstrapping uv", { version: UV_VERSION, target, cache: installDir });
|
|
@@ -7531,20 +8462,24 @@ async function resolveUv() {
|
|
|
7531
8462
|
if (!expected || expected.toLowerCase() !== actual.toLowerCase()) {
|
|
7532
8463
|
throw new Error(`uv archive checksum mismatch (expected ${expected}, got ${actual})`);
|
|
7533
8464
|
}
|
|
7534
|
-
const archivePath =
|
|
8465
|
+
const archivePath = path4.join(installDir, archiveName);
|
|
7535
8466
|
await pipeline(async function* () {
|
|
7536
8467
|
yield archiveBuf;
|
|
7537
8468
|
}, createWriteStream(archivePath));
|
|
7538
|
-
const extractDir =
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
8469
|
+
const extractDir = path4.join(installDir, `extract-${process.pid}-${Date.now()}`);
|
|
8470
|
+
try {
|
|
8471
|
+
await extractArchive(archivePath, extractDir);
|
|
8472
|
+
const extracted = await findBinary(extractDir, binName());
|
|
8473
|
+
if (!extracted) throw new Error(`uv binary not found inside ${archiveName}`);
|
|
8474
|
+
await fs.rename(extracted, finalBin);
|
|
8475
|
+
await fs.chmod(finalBin, 493).catch(() => {
|
|
8476
|
+
});
|
|
8477
|
+
} finally {
|
|
8478
|
+
await fs.rm(extractDir, { recursive: true, force: true }).catch(() => {
|
|
8479
|
+
});
|
|
8480
|
+
await fs.rm(archivePath, { force: true }).catch(() => {
|
|
8481
|
+
});
|
|
8482
|
+
}
|
|
7548
8483
|
log("info", "uv bootstrap complete", { bin: finalBin });
|
|
7549
8484
|
return finalBin;
|
|
7550
8485
|
}
|
|
@@ -7622,7 +8557,7 @@ function categorizeSpawnError(err) {
|
|
|
7622
8557
|
}
|
|
7623
8558
|
async function connectToUpstream(config, onDisconnect, onListChanged) {
|
|
7624
8559
|
const client = new Client(
|
|
7625
|
-
{ name: "yaw-mcp", version: true ? "0.
|
|
8560
|
+
{ name: "yaw-mcp", version: true ? "0.62.0" : "dev" },
|
|
7626
8561
|
{ capabilities: {} }
|
|
7627
8562
|
);
|
|
7628
8563
|
let transport;
|
|
@@ -7631,7 +8566,11 @@ async function connectToUpstream(config, onDisconnect, onListChanged) {
|
|
|
7631
8566
|
if (!config.command) {
|
|
7632
8567
|
throw new Error("command is required for local servers");
|
|
7633
8568
|
}
|
|
7634
|
-
const {
|
|
8569
|
+
const {
|
|
8570
|
+
YAW_MCP_TOKEN: _excludedToken,
|
|
8571
|
+
YAW_MCP_VAULT_PASSPHRASE: _excludedVaultPassphrase,
|
|
8572
|
+
...parentEnv
|
|
8573
|
+
} = process.env;
|
|
7635
8574
|
const resolved = await resolveUvSpawn(config.command, config.args ?? []);
|
|
7636
8575
|
const serverEnv = await resolveServerEnv(config.env ?? {});
|
|
7637
8576
|
const stdioTransport = new StdioClientTransport({
|
|
@@ -7664,7 +8603,10 @@ async function connectToUpstream(config, onDisconnect, onListChanged) {
|
|
|
7664
8603
|
}, CONNECT_TIMEOUT);
|
|
7665
8604
|
});
|
|
7666
8605
|
try {
|
|
7667
|
-
|
|
8606
|
+
const connectP = client.connect(transport);
|
|
8607
|
+
connectP.catch(() => {
|
|
8608
|
+
});
|
|
8609
|
+
await Promise.race([connectP, timeoutPromise]);
|
|
7668
8610
|
clearTimeout(timer);
|
|
7669
8611
|
} catch (err) {
|
|
7670
8612
|
clearTimeout(timer);
|
|
@@ -7830,7 +8772,18 @@ async function fetchPromptsFromUpstream(client, namespace) {
|
|
|
7830
8772
|
}
|
|
7831
8773
|
}
|
|
7832
8774
|
async function fetchToolsFromUpstream(client, namespace) {
|
|
7833
|
-
|
|
8775
|
+
let result;
|
|
8776
|
+
try {
|
|
8777
|
+
result = await client.listTools({}, { timeout: LIST_TIMEOUT });
|
|
8778
|
+
} catch (err) {
|
|
8779
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
8780
|
+
throw new ActivationError(
|
|
8781
|
+
`"${namespace}" returned an error on tools/list: ${message}`,
|
|
8782
|
+
"protocol_error",
|
|
8783
|
+
void 0,
|
|
8784
|
+
err
|
|
8785
|
+
);
|
|
8786
|
+
}
|
|
7834
8787
|
const raw = result.tools ?? [];
|
|
7835
8788
|
if (raw.length > MAX_TOOLS_PER_SERVER) {
|
|
7836
8789
|
log("warn", "Upstream returned more tools than cap; truncating", {
|
|
@@ -7860,7 +8813,7 @@ function resolvePollIntervalMs() {
|
|
|
7860
8813
|
}
|
|
7861
8814
|
return seconds * 1e3;
|
|
7862
8815
|
}
|
|
7863
|
-
function
|
|
8816
|
+
function isPersistenceDisabled2() {
|
|
7864
8817
|
const raw = process.env.YAW_MCP_DISABLE_PERSISTENCE;
|
|
7865
8818
|
if (raw === void 0 || raw === "") return false;
|
|
7866
8819
|
return raw === "1" || raw.toLowerCase() === "true";
|
|
@@ -7931,7 +8884,7 @@ var ConnectServer = class _ConnectServer {
|
|
|
7931
8884
|
this.apiUrl = apiUrl5;
|
|
7932
8885
|
this.token = token5;
|
|
7933
8886
|
this.server = new Server(
|
|
7934
|
-
{ name: "yaw-mcp", version: true ? "0.
|
|
8887
|
+
{ name: "yaw-mcp", version: true ? "0.62.0" : "dev" },
|
|
7935
8888
|
{
|
|
7936
8889
|
capabilities: {
|
|
7937
8890
|
tools: { listChanged: true },
|
|
@@ -8006,6 +8959,17 @@ var ConnectServer = class _ConnectServer {
|
|
|
8006
8959
|
// same promise as the first; the entry is cleared when the promise
|
|
8007
8960
|
// settles (success or failure).
|
|
8008
8961
|
activationInflight = /* @__PURE__ */ new Map();
|
|
8962
|
+
// Tracks namespaces whose current activationInflight was initiated by
|
|
8963
|
+
// prewarmDormantServers. An explicit mcp_connect_activate clears the
|
|
8964
|
+
// namespace from this set, which prevents prewarm from disconnecting a
|
|
8965
|
+
// connection the user just claimed. Without this, the prewarm race is:
|
|
8966
|
+
// 1. prewarm activateOne("foo") -> inflight P1
|
|
8967
|
+
// 2. user activateOne("foo") -> joins P1 (same promise)
|
|
8968
|
+
// 3. P1 resolves ok=true for both callers
|
|
8969
|
+
// 4. prewarm disconnects "foo" — user's next tool call fails
|
|
8970
|
+
// With this set: prewarm only disconnects when the namespace was NOT
|
|
8971
|
+
// claimed by an explicit activate while P1 was in flight.
|
|
8972
|
+
prewarmNamespaces = /* @__PURE__ */ new Set();
|
|
8009
8973
|
// Usage learning — nudges dispatch toward namespaces that have been
|
|
8010
8974
|
// genuinely useful. Counts persist across yaw-mcp restarts via state.json
|
|
8011
8975
|
// (see persistence.ts). YAW_MCP_DISABLE_PERSISTENCE=1 makes it session
|
|
@@ -8016,6 +8980,16 @@ var ConnectServer = class _ConnectServer {
|
|
|
8016
8980
|
// "packs". Observation-only; never activates anything. Meta-tool calls
|
|
8017
8981
|
// are deliberately excluded because they aren't user workflow.
|
|
8018
8982
|
packDetector = new PackDetector();
|
|
8983
|
+
// Session-scoped re-dispatch tracking — watches for the model abandoning
|
|
8984
|
+
// one server and re-routing a similar intent to another, which is
|
|
8985
|
+
// evidence the first route was wrong. Feeds a negative learning signal
|
|
8986
|
+
// (LearningStore.recordMiss). Not persisted: a re-dispatch window that
|
|
8987
|
+
// spans a restart is meaningless. See redispatch.ts.
|
|
8988
|
+
redispatch = new RedispatchTracker();
|
|
8989
|
+
// Last dispatch intent per namespace (session-scoped, not persisted). Lets
|
|
8990
|
+
// the optional reward grader (reward-grader.ts) judge a tool call against the
|
|
8991
|
+
// goal the server was routed for. Bounded by the number of namespaces.
|
|
8992
|
+
lastIntentByNamespace = /* @__PURE__ */ new Map();
|
|
8019
8993
|
// Short-TTL dedup cache for discover output. Agents often call
|
|
8020
8994
|
// discover twice in quick succession (e.g. once to list, again after
|
|
8021
8995
|
// a failed activate) — the second call returns the same text if
|
|
@@ -8166,7 +9140,7 @@ var ConnectServer = class _ConnectServer {
|
|
|
8166
9140
|
});
|
|
8167
9141
|
}
|
|
8168
9142
|
async start() {
|
|
8169
|
-
if (
|
|
9143
|
+
if (isPersistenceDisabled2()) {
|
|
8170
9144
|
log("info", "Cross-session persistence disabled via YAW_MCP_DISABLE_PERSISTENCE");
|
|
8171
9145
|
} else {
|
|
8172
9146
|
const persisted = await loadState();
|
|
@@ -8203,6 +9177,15 @@ var ConnectServer = class _ConnectServer {
|
|
|
8203
9177
|
});
|
|
8204
9178
|
for (const w of result.warnings) log("warn", "bundles.json warning", { warning: w });
|
|
8205
9179
|
this.config = result.config ?? { servers: [], configVersion: "" };
|
|
9180
|
+
const seenNs = /* @__PURE__ */ new Set();
|
|
9181
|
+
this.config.servers = this.config.servers.filter((s) => {
|
|
9182
|
+
if (seenNs.has(s.namespace)) {
|
|
9183
|
+
log("warn", "Duplicate namespace in bundles.json, skipping", { namespace: s.namespace });
|
|
9184
|
+
return false;
|
|
9185
|
+
}
|
|
9186
|
+
seenNs.add(s.namespace);
|
|
9187
|
+
return true;
|
|
9188
|
+
});
|
|
8206
9189
|
this.configVersion = this.config.configVersion;
|
|
8207
9190
|
log("info", "Local mode: loaded bundles", {
|
|
8208
9191
|
path: result.path,
|
|
@@ -8314,8 +9297,21 @@ var ConnectServer = class _ConnectServer {
|
|
|
8314
9297
|
await Promise.all(
|
|
8315
9298
|
batch.map(async (server) => {
|
|
8316
9299
|
try {
|
|
8317
|
-
const result = await this.activateOne(
|
|
9300
|
+
const result = await this.activateOne(
|
|
9301
|
+
server.namespace,
|
|
9302
|
+
void 0,
|
|
9303
|
+
/* fromPrewarm */
|
|
9304
|
+
true
|
|
9305
|
+
);
|
|
8318
9306
|
if (!result.ok) return;
|
|
9307
|
+
if (!this.prewarmNamespaces.has(server.namespace)) {
|
|
9308
|
+
log("info", "Pre-warm skipping disconnect \u2014 namespace claimed by explicit activate", {
|
|
9309
|
+
namespace: server.namespace
|
|
9310
|
+
});
|
|
9311
|
+
anyPopulated = true;
|
|
9312
|
+
return;
|
|
9313
|
+
}
|
|
9314
|
+
this.prewarmNamespaces.delete(server.namespace);
|
|
8319
9315
|
const conn = this.connections.get(server.namespace);
|
|
8320
9316
|
if (conn) {
|
|
8321
9317
|
await disconnectFromUpstream(conn).catch(() => {
|
|
@@ -8361,7 +9357,7 @@ var ConnectServer = class _ConnectServer {
|
|
|
8361
9357
|
}
|
|
8362
9358
|
return result;
|
|
8363
9359
|
}
|
|
8364
|
-
async handleToolCall(name, args, extra) {
|
|
9360
|
+
async handleToolCall(name, args, extra, opts) {
|
|
8365
9361
|
const progress = createProgressReporter(extra);
|
|
8366
9362
|
if (name === META_TOOLS.discover.name) {
|
|
8367
9363
|
recordConnectEvent({ namespace: null, toolName: null, action: "discover", latencyMs: null, success: true });
|
|
@@ -8370,8 +9366,9 @@ var ConnectServer = class _ConnectServer {
|
|
|
8370
9366
|
if (name === META_TOOLS.dispatch.name) {
|
|
8371
9367
|
const intent = typeof args.intent === "string" ? args.intent : "";
|
|
8372
9368
|
const budget = typeof args.budget === "number" && Number.isFinite(args.budget) ? args.budget : 1;
|
|
9369
|
+
const routeEffort = typeof args.routeEffort === "string" ? args.routeEffort : void 0;
|
|
8373
9370
|
recordConnectEvent({ namespace: null, toolName: null, action: "activate", latencyMs: null, success: true });
|
|
8374
|
-
return this.attachGuideNudge(await this.handleDispatch(intent, budget, progress));
|
|
9371
|
+
return this.attachGuideNudge(await this.handleDispatch(intent, budget, progress, routeEffort));
|
|
8375
9372
|
}
|
|
8376
9373
|
if (name === META_TOOLS.activate.name) {
|
|
8377
9374
|
const namespaces = resolveNamespaces(args);
|
|
@@ -8496,7 +9493,7 @@ var ConnectServer = class _ConnectServer {
|
|
|
8496
9493
|
content: [
|
|
8497
9494
|
{
|
|
8498
9495
|
type: "text",
|
|
8499
|
-
text: `Tool "${name}" is no longer available after loading "${activation.serverId ? activation.serverId : name}" \u2014 the upstream's tool set changed. Call mcp_connect_discover to
|
|
9496
|
+
text: `Tool "${name}" is no longer available after loading "${activation.serverId ? activation.serverId : name}" \u2014 the upstream's tool set changed. Call mcp_connect_discover to list the current tools for that namespace.`
|
|
8500
9497
|
}
|
|
8501
9498
|
],
|
|
8502
9499
|
isError: true
|
|
@@ -8504,9 +9501,10 @@ var ConnectServer = class _ConnectServer {
|
|
|
8504
9501
|
}
|
|
8505
9502
|
}
|
|
8506
9503
|
if (route) {
|
|
8507
|
-
const
|
|
9504
|
+
const ns = route.namespace;
|
|
9505
|
+
const conn = this.connections.get(ns);
|
|
8508
9506
|
if (conn && conn.status === "error") {
|
|
8509
|
-
const serverConfig = this.config?.servers.find((s) => s.namespace ===
|
|
9507
|
+
const serverConfig = this.config?.servers.find((s) => s.namespace === ns);
|
|
8510
9508
|
if (serverConfig) {
|
|
8511
9509
|
let reconnected = false;
|
|
8512
9510
|
let lastErr;
|
|
@@ -8521,17 +9519,30 @@ var ConnectServer = class _ConnectServer {
|
|
|
8521
9519
|
this.onUpstreamDisconnect,
|
|
8522
9520
|
this.onUpstreamListChanged
|
|
8523
9521
|
);
|
|
8524
|
-
this.connections.set(
|
|
9522
|
+
this.connections.set(ns, newConn);
|
|
8525
9523
|
this.rebuildRoutes();
|
|
8526
9524
|
await this.notifyAllListsChanged();
|
|
8527
|
-
log("info", "Auto-reconnected to upstream", { namespace:
|
|
9525
|
+
log("info", "Auto-reconnected to upstream", { namespace: ns });
|
|
8528
9526
|
reconnected = true;
|
|
9527
|
+
routes = this.toolRoutes;
|
|
9528
|
+
route = routes.get(name);
|
|
9529
|
+
if (!route || route.deferred) {
|
|
9530
|
+
return {
|
|
9531
|
+
content: [
|
|
9532
|
+
{
|
|
9533
|
+
type: "text",
|
|
9534
|
+
text: `Tool "${name}" is no longer available after reconnecting "${serverConfig.namespace}" \u2014 the upstream's tool set changed. Call mcp_connect_discover to list the current tools for that namespace.`
|
|
9535
|
+
}
|
|
9536
|
+
],
|
|
9537
|
+
isError: true
|
|
9538
|
+
};
|
|
9539
|
+
}
|
|
8529
9540
|
break;
|
|
8530
9541
|
} catch (err) {
|
|
8531
9542
|
lastErr = err;
|
|
8532
9543
|
if (attempt < RECONNECT_ATTEMPTS - 1) {
|
|
8533
9544
|
log("warn", "Auto-reconnect attempt failed, retrying", {
|
|
8534
|
-
namespace:
|
|
9545
|
+
namespace: ns,
|
|
8535
9546
|
error: err instanceof Error ? err.message : String(err)
|
|
8536
9547
|
});
|
|
8537
9548
|
}
|
|
@@ -8540,12 +9551,12 @@ var ConnectServer = class _ConnectServer {
|
|
|
8540
9551
|
if (!reconnected) {
|
|
8541
9552
|
conn.status = "error";
|
|
8542
9553
|
const lastErrMsg = lastErr instanceof Error ? lastErr.message : String(lastErr);
|
|
8543
|
-
log("error", "Auto-reconnect failed", { namespace:
|
|
9554
|
+
log("error", "Auto-reconnect failed", { namespace: ns, error: lastErrMsg });
|
|
8544
9555
|
return {
|
|
8545
9556
|
content: [
|
|
8546
9557
|
{
|
|
8547
9558
|
type: "text",
|
|
8548
|
-
text: `Server "${
|
|
9559
|
+
text: `Server "${ns}" disconnected and auto-reconnect failed: ${lastErrMsg}. Use mcp_connect_activate with server "${ns}" to reload it manually.`
|
|
8549
9560
|
}
|
|
8550
9561
|
],
|
|
8551
9562
|
isError: true
|
|
@@ -8621,9 +9632,19 @@ var ConnectServer = class _ConnectServer {
|
|
|
8621
9632
|
} catch {
|
|
8622
9633
|
}
|
|
8623
9634
|
}
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
9635
|
+
if (!opts?.deferLearning) {
|
|
9636
|
+
const reward = computeOutcomeReward(result);
|
|
9637
|
+
this.learning.recordOutcome(route.namespace, reward);
|
|
9638
|
+
this.scheduleStateSave();
|
|
9639
|
+
if (isRewardGraderEnabled() && isUncertainReward(reward)) {
|
|
9640
|
+
void this.refineRewardInBackground(route.namespace, reward, {
|
|
9641
|
+
intent: this.lastIntentByNamespace.get(route.namespace),
|
|
9642
|
+
toolName: route.originalName,
|
|
9643
|
+
resultText: firstResultText(result)
|
|
9644
|
+
});
|
|
9645
|
+
}
|
|
9646
|
+
this.redispatch.markReply(route.namespace, !result.isError);
|
|
9647
|
+
}
|
|
8627
9648
|
if (!result.isError) {
|
|
8628
9649
|
this.packDetector.recordCall(route.namespace, route.originalName, Date.now());
|
|
8629
9650
|
}
|
|
@@ -8952,15 +9973,28 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
|
|
|
8952
9973
|
// child process; the second set() would win and the first would leak
|
|
8953
9974
|
// until its transport noticed. See activationInflight.
|
|
8954
9975
|
//
|
|
9976
|
+
// `fromPrewarm` marks the inflight as prewarm-initiated so that
|
|
9977
|
+
// prewarmDormantServers can safely disconnect when it is the sole
|
|
9978
|
+
// caller, but skip the disconnect when an explicit activate has also
|
|
9979
|
+
// joined the inflight promise. An explicit call (fromPrewarm=false)
|
|
9980
|
+
// removes the namespace from prewarmNamespaces so prewarm's teardown
|
|
9981
|
+
// code sees it as "claimed" and leaves the connection alive.
|
|
9982
|
+
//
|
|
8955
9983
|
// Returns:
|
|
8956
9984
|
// { ok: true, message } — already connected or newly connected
|
|
8957
9985
|
// { ok: false, message, isChanged: false } — failed or not in config
|
|
8958
|
-
activateOne(namespace, progress) {
|
|
9986
|
+
activateOne(namespace, progress, fromPrewarm = false) {
|
|
9987
|
+
if (!fromPrewarm) {
|
|
9988
|
+
this.prewarmNamespaces.delete(namespace);
|
|
9989
|
+
}
|
|
8959
9990
|
const inflight = this.activationInflight.get(namespace);
|
|
8960
9991
|
if (inflight) {
|
|
8961
9992
|
progress?.(`"${namespace}" load already in flight \u2014 awaiting existing attempt`);
|
|
8962
9993
|
return inflight;
|
|
8963
9994
|
}
|
|
9995
|
+
if (fromPrewarm) {
|
|
9996
|
+
this.prewarmNamespaces.add(namespace);
|
|
9997
|
+
}
|
|
8964
9998
|
const promise = this.runActivateOne(namespace, progress).finally(() => {
|
|
8965
9999
|
if (this.activationInflight.get(namespace) === promise) {
|
|
8966
10000
|
this.activationInflight.delete(namespace);
|
|
@@ -9010,7 +10044,12 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
|
|
|
9010
10044
|
}
|
|
9011
10045
|
const capDecision = evaluateServerCap(namespace, loadedSlots, this.serverCap);
|
|
9012
10046
|
if (!capDecision.allow) {
|
|
9013
|
-
return {
|
|
10047
|
+
return {
|
|
10048
|
+
ok: false,
|
|
10049
|
+
isChanged: false,
|
|
10050
|
+
capped: true,
|
|
10051
|
+
message: capDecision.message ?? "Concurrent server cap reached."
|
|
10052
|
+
};
|
|
9014
10053
|
}
|
|
9015
10054
|
const elicited = this.elicitedEnv.get(namespace);
|
|
9016
10055
|
const effectiveConfig = elicited ? { ...serverConfig, env: { ...serverConfig.env, ...elicited } } : serverConfig;
|
|
@@ -9160,6 +10199,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
|
|
|
9160
10199
|
const results = [];
|
|
9161
10200
|
let anyChanged = false;
|
|
9162
10201
|
let anyError = false;
|
|
10202
|
+
let anyCapped = false;
|
|
9163
10203
|
const minCompliance = resolveMinCompliance();
|
|
9164
10204
|
const total = namespaces.length;
|
|
9165
10205
|
let i = 0;
|
|
@@ -9179,7 +10219,10 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
|
|
|
9179
10219
|
const r = await this.activateOne(namespace, progress);
|
|
9180
10220
|
results.push(r.message);
|
|
9181
10221
|
if (r.isChanged) anyChanged = true;
|
|
9182
|
-
if (!r.ok)
|
|
10222
|
+
if (!r.ok) {
|
|
10223
|
+
if (r.capped) anyCapped = true;
|
|
10224
|
+
else anyError = true;
|
|
10225
|
+
}
|
|
9183
10226
|
}
|
|
9184
10227
|
if (anyChanged) {
|
|
9185
10228
|
this.rebuildRoutes();
|
|
@@ -9189,7 +10232,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
|
|
|
9189
10232
|
}
|
|
9190
10233
|
return {
|
|
9191
10234
|
content: [{ type: "text", text: results.join("\n") }],
|
|
9192
|
-
isError: anyError && !anyChanged ? true : void 0
|
|
10235
|
+
isError: anyError || anyCapped && !anyChanged ? true : void 0
|
|
9193
10236
|
};
|
|
9194
10237
|
}
|
|
9195
10238
|
// Smart-routing meta-tool. The LLM describes the task in plain English
|
|
@@ -9197,7 +10240,33 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
|
|
|
9197
10240
|
// with BM25 and activates the top N, then lets the LLM call the now-
|
|
9198
10241
|
// exposed tools normally. Default budget is 1 because over-activating
|
|
9199
10242
|
// pollutes the tool list in the LLM's context with noise.
|
|
9200
|
-
|
|
10243
|
+
// Is A -> B a designed multi-server flow rather than a routing miss? True
|
|
10244
|
+
// when both namespaces co-occur in a curated bundle or a detected usage
|
|
10245
|
+
// pack — those A-then-B sequences are intentional, so re-dispatch from A
|
|
10246
|
+
// to B must NOT penalize A. Used as detectMiss's exclusion predicate.
|
|
10247
|
+
// Background refinement of a just-recorded heuristic reward via the optional
|
|
10248
|
+
// LLM grader. Fire-and-forget: the tool result has already returned. If the
|
|
10249
|
+
// grader returns a verdict different from the heuristic, revise the credit by
|
|
10250
|
+
// the delta (recordOutcome already counted the dispatch). Never throws.
|
|
10251
|
+
async refineRewardInBackground(namespace, heuristic, ctx) {
|
|
10252
|
+
try {
|
|
10253
|
+
const graded = await gradeOutcomeViaSampling(this.server, ctx);
|
|
10254
|
+
if (graded === null || graded === heuristic) return;
|
|
10255
|
+
this.learning.adjustSucceeded(namespace, graded - heuristic);
|
|
10256
|
+
this.scheduleStateSave();
|
|
10257
|
+
} catch {
|
|
10258
|
+
}
|
|
10259
|
+
}
|
|
10260
|
+
isLegitChain(a, b) {
|
|
10261
|
+
for (const bundle of CURATED_BUNDLES) {
|
|
10262
|
+
if (bundle.namespaces.includes(a) && bundle.namespaces.includes(b)) return true;
|
|
10263
|
+
}
|
|
10264
|
+
for (const pack of this.packDetector.detectChains()) {
|
|
10265
|
+
if (pack.namespaces.includes(a) && pack.namespaces.includes(b)) return true;
|
|
10266
|
+
}
|
|
10267
|
+
return false;
|
|
10268
|
+
}
|
|
10269
|
+
async handleDispatch(intent, budget, progress, routeEffortOverride) {
|
|
9201
10270
|
const trimmed = intent?.trim?.() ?? "";
|
|
9202
10271
|
if (trimmed.length === 0) {
|
|
9203
10272
|
return {
|
|
@@ -9241,11 +10310,13 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
|
|
|
9241
10310
|
isError: true
|
|
9242
10311
|
};
|
|
9243
10312
|
}
|
|
9244
|
-
|
|
10313
|
+
const effort = parseRouteEffort(routeEffortOverride ?? process.env.YAW_MCP_ROUTE_EFFORT);
|
|
10314
|
+
if (budget === 1 && shouldSample(ranked, effort)) {
|
|
9245
10315
|
progress?.("Top candidates close \u2014 asking LLM to pick\u2026");
|
|
9246
10316
|
const serversByNamespace = new Map(activeServers.map((s) => [s.namespace, s]));
|
|
9247
10317
|
const candidates = buildCandidates(ranked.slice(0, 3), serversByNamespace, this.toolCache);
|
|
9248
|
-
const
|
|
10318
|
+
const samples = sampleCountForEffort(effort);
|
|
10319
|
+
const picked = await bestOfNViaSampling(this.server, trimmed, candidates, samples);
|
|
9249
10320
|
if (picked) {
|
|
9250
10321
|
const winner = ranked.find((r) => r.namespace === picked);
|
|
9251
10322
|
if (winner) {
|
|
@@ -9258,9 +10329,31 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
|
|
|
9258
10329
|
}
|
|
9259
10330
|
const safeBudget = Math.max(1, Math.min(10, Math.floor(budget)));
|
|
9260
10331
|
const winners = ranked.slice(0, safeBudget);
|
|
10332
|
+
const primary = winners[0]?.namespace;
|
|
10333
|
+
if (primary) {
|
|
10334
|
+
for (const w of winners) this.lastIntentByNamespace.set(w.namespace, trimmed);
|
|
10335
|
+
const intentTokens = tokenize(trimmed);
|
|
10336
|
+
const now = Date.now();
|
|
10337
|
+
const miss = this.redispatch.detectMiss(primary, intentTokens, now, (a, b) => this.isLegitChain(a, b));
|
|
10338
|
+
if (miss) {
|
|
10339
|
+
this.learning.recordMiss(miss.loser);
|
|
10340
|
+
this.scheduleStateSave();
|
|
10341
|
+
}
|
|
10342
|
+
this.redispatch.push(primary, intentTokens, now);
|
|
10343
|
+
if (isFoundryEnabled()) {
|
|
10344
|
+
const redacted = redactIntent(trimmed);
|
|
10345
|
+
void appendFoundryTrace({
|
|
10346
|
+
tokens: redacted.tokens,
|
|
10347
|
+
redactedCount: redacted.redactedCount,
|
|
10348
|
+
candidates: ranked.slice(0, 5).map((r) => ({ ns: r.namespace, score: r.score })),
|
|
10349
|
+
chosen: primary
|
|
10350
|
+
});
|
|
10351
|
+
}
|
|
10352
|
+
}
|
|
9261
10353
|
const results = [];
|
|
9262
10354
|
let anyChanged = false;
|
|
9263
10355
|
let anyError = false;
|
|
10356
|
+
let anyCapped = false;
|
|
9264
10357
|
let i = 0;
|
|
9265
10358
|
for (const winner of winners) {
|
|
9266
10359
|
i += 1;
|
|
@@ -9268,7 +10361,10 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
|
|
|
9268
10361
|
const r = await this.activateOne(winner.namespace, progress);
|
|
9269
10362
|
results.push(`${winner.namespace} (score ${winner.score.toFixed(2)}): ${r.message}`);
|
|
9270
10363
|
if (r.isChanged) anyChanged = true;
|
|
9271
|
-
if (!r.ok)
|
|
10364
|
+
if (!r.ok) {
|
|
10365
|
+
if (r.capped) anyCapped = true;
|
|
10366
|
+
else anyError = true;
|
|
10367
|
+
}
|
|
9272
10368
|
}
|
|
9273
10369
|
if (anyChanged) {
|
|
9274
10370
|
this.rebuildRoutes();
|
|
@@ -9278,7 +10374,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
|
|
|
9278
10374
|
`;
|
|
9279
10375
|
return {
|
|
9280
10376
|
content: [{ type: "text", text: header + results.join("\n") }],
|
|
9281
|
-
isError: anyError && !anyChanged ? true : void 0
|
|
10377
|
+
isError: anyError || anyCapped && !anyChanged ? true : void 0
|
|
9282
10378
|
};
|
|
9283
10379
|
}
|
|
9284
10380
|
async handleDeactivate(namespaces) {
|
|
@@ -9309,8 +10405,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
|
|
|
9309
10405
|
await this.notifyAllListsChanged();
|
|
9310
10406
|
}
|
|
9311
10407
|
return {
|
|
9312
|
-
content: [{ type: "text", text: results.join("\n") }]
|
|
9313
|
-
isError: !anyChanged ? true : void 0
|
|
10408
|
+
content: [{ type: "text", text: results.join("\n") }]
|
|
9314
10409
|
};
|
|
9315
10410
|
}
|
|
9316
10411
|
async trackUsageAndAutoDeactivate(calledNamespace) {
|
|
@@ -9400,7 +10495,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
|
|
|
9400
10495
|
continue;
|
|
9401
10496
|
}
|
|
9402
10497
|
const oldConfig = connection.config;
|
|
9403
|
-
if (oldConfig.command !== newServerConfig.command || !argsEqual(oldConfig.args, newServerConfig.args) || oldConfig.url !== newServerConfig.url || !envEqual(oldConfig.env, newServerConfig.env)) {
|
|
10498
|
+
if (oldConfig.command !== newServerConfig.command || !argsEqual(oldConfig.args, newServerConfig.args) || oldConfig.url !== newServerConfig.url || !envEqual(oldConfig.env, newServerConfig.env) || oldConfig.type !== newServerConfig.type || oldConfig.connectTimeoutMs !== newServerConfig.connectTimeoutMs) {
|
|
9404
10499
|
log("info", "Server config changed, deactivating stale connection", { namespace });
|
|
9405
10500
|
await disconnectFromUpstream(connection);
|
|
9406
10501
|
this.connections.delete(namespace);
|
|
@@ -9462,7 +10557,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
|
|
|
9462
10557
|
}
|
|
9463
10558
|
const ALLOWED_FILENAMES = ["claude_desktop_config.json", "mcp.json", "settings.json", "mcp_config.json"];
|
|
9464
10559
|
try {
|
|
9465
|
-
const resolved = filepath.startsWith("~/") || filepath.startsWith("~\\") ?
|
|
10560
|
+
const resolved = filepath.startsWith("~/") || filepath.startsWith("~\\") ? resolve6(homedir15(), filepath.slice(2)) : resolve6(filepath);
|
|
9466
10561
|
const resolvedBasename = resolved.split(/[/\\]/).pop() || "";
|
|
9467
10562
|
if (!ALLOWED_FILENAMES.includes(resolvedBasename)) {
|
|
9468
10563
|
return {
|
|
@@ -9477,9 +10572,9 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
|
|
|
9477
10572
|
}
|
|
9478
10573
|
const isUnder = (base, p) => {
|
|
9479
10574
|
const rel = relative(base, p);
|
|
9480
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
10575
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
9481
10576
|
};
|
|
9482
|
-
if (!isUnder(
|
|
10577
|
+
if (!isUnder(homedir15(), resolved) && !isUnder(process.cwd(), resolved)) {
|
|
9483
10578
|
return {
|
|
9484
10579
|
content: [
|
|
9485
10580
|
{ type: "text", text: "Import path must be under your home directory or the current working directory." }
|
|
@@ -9566,7 +10661,12 @@ Use mcp_connect_discover to see imported servers.`
|
|
|
9566
10661
|
]
|
|
9567
10662
|
};
|
|
9568
10663
|
} catch (err) {
|
|
9569
|
-
|
|
10664
|
+
const { code, text } = this.mapNetworkError(err, "Import");
|
|
10665
|
+
log("warn", "handleImport error", {
|
|
10666
|
+
error: err instanceof Error ? err.message : String(err),
|
|
10667
|
+
code
|
|
10668
|
+
});
|
|
10669
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
9570
10670
|
}
|
|
9571
10671
|
}
|
|
9572
10672
|
// Install a new MCP server on the user's Yaw MCP account. Validates
|
|
@@ -9657,15 +10757,7 @@ Use mcp_connect_discover to see imported servers.`
|
|
|
9657
10757
|
]
|
|
9658
10758
|
};
|
|
9659
10759
|
} catch (err) {
|
|
9660
|
-
const code =
|
|
9661
|
-
let text;
|
|
9662
|
-
if (code === "UND_ERR_HEADERS_TIMEOUT" || code === "UND_ERR_BODY_TIMEOUT" || code === "UND_ERR_CONNECT_TIMEOUT") {
|
|
9663
|
-
text = "Install timed out talking to yaw.sh/mcp. Retry in a moment.";
|
|
9664
|
-
} else if (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "EAI_AGAIN" || code === "UND_ERR_SOCKET") {
|
|
9665
|
-
text = "Couldn't reach yaw.sh/mcp (network unreachable or DNS failure). Check your connection and retry.";
|
|
9666
|
-
} else {
|
|
9667
|
-
text = "Install failed unexpectedly. Check yaw-mcp logs on this machine for the underlying error.";
|
|
9668
|
-
}
|
|
10760
|
+
const { code, text } = this.mapNetworkError(err, "Install");
|
|
9669
10761
|
log("warn", "handleInstall error", {
|
|
9670
10762
|
error: err instanceof Error ? err.message : String(err),
|
|
9671
10763
|
code
|
|
@@ -9673,6 +10765,23 @@ Use mcp_connect_discover to see imported servers.`
|
|
|
9673
10765
|
return { content: [{ type: "text", text }], isError: true };
|
|
9674
10766
|
}
|
|
9675
10767
|
}
|
|
10768
|
+
// Map a raw undici/network error to a user-facing string instead of
|
|
10769
|
+
// leaking `err.message` (e.g. "getaddrinfo ENOTFOUND yaw.sh") verbatim to
|
|
10770
|
+
// the model. Returns the extracted node error code (for ops logging) and a
|
|
10771
|
+
// clean message keyed by the failing operation verb ("Install"/"Import").
|
|
10772
|
+
// Callers keep the raw error in the log; the LLM/user only sees `text`.
|
|
10773
|
+
mapNetworkError(err, op) {
|
|
10774
|
+
const code = typeof err === "object" && err !== null ? err.code || err.cause?.code : void 0;
|
|
10775
|
+
let text;
|
|
10776
|
+
if (code === "UND_ERR_HEADERS_TIMEOUT" || code === "UND_ERR_BODY_TIMEOUT" || code === "UND_ERR_CONNECT_TIMEOUT") {
|
|
10777
|
+
text = `${op} timed out talking to yaw.sh/mcp. Retry in a moment.`;
|
|
10778
|
+
} else if (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "EAI_AGAIN" || code === "UND_ERR_SOCKET") {
|
|
10779
|
+
text = `Couldn't reach yaw.sh/mcp (network unreachable or DNS failure). Check your connection and retry.`;
|
|
10780
|
+
} else {
|
|
10781
|
+
text = `${op} failed unexpectedly. Check yaw-mcp logs on this machine for the underlying error.`;
|
|
10782
|
+
}
|
|
10783
|
+
return { code, text };
|
|
10784
|
+
}
|
|
9676
10785
|
// Signature-on-demand: return one tool's full input schema without
|
|
9677
10786
|
// persistently activating its server. When the server is already
|
|
9678
10787
|
// loaded we read from the in-memory connection. When it isn't, we
|
|
@@ -9861,7 +10970,7 @@ To load the top pack in one step, call \`mcp_connect_activate\` with namespaces=
|
|
|
9861
10970
|
`];
|
|
9862
10971
|
for (const bundle of CURATED_BUNDLES) {
|
|
9863
10972
|
lines2.push(` ${bundle.id} \u2014 ${bundle.description}`);
|
|
9864
|
-
lines2.push(`
|
|
10973
|
+
lines2.push(` servers: ${JSON.stringify(bundle.namespaces)}`);
|
|
9865
10974
|
lines2.push(` activate: ${bundleActivateHint(bundle)}`);
|
|
9866
10975
|
}
|
|
9867
10976
|
lines2.push("");
|
|
@@ -9887,7 +10996,7 @@ To load the top pack in one step, call \`mcp_connect_activate\` with namespaces=
|
|
|
9887
10996
|
lines.push("Bundles ready to activate now:");
|
|
9888
10997
|
for (const bundle of ready) {
|
|
9889
10998
|
lines.push(` ${bundle.id} \u2014 ${bundle.description}`);
|
|
9890
|
-
lines.push(`
|
|
10999
|
+
lines.push(` servers: ${JSON.stringify(bundle.namespaces)}`);
|
|
9891
11000
|
lines.push(` activate: ${bundleActivateHint(bundle)}`);
|
|
9892
11001
|
}
|
|
9893
11002
|
}
|
|
@@ -9903,6 +11012,30 @@ To load the top pack in one step, call \`mcp_connect_activate\` with namespaces=
|
|
|
9903
11012
|
}
|
|
9904
11013
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
9905
11014
|
}
|
|
11015
|
+
// Extract the semantic payload from a successful MCP tool result for use
|
|
11016
|
+
// as a step binding in exec pipelines. The MCP wire format wraps every
|
|
11017
|
+
// result in `{ content: [{type, text}, ...], isError? }`, but the exec
|
|
11018
|
+
// tool description promises `$ref` targets that behave like the tool's
|
|
11019
|
+
// actual output -- e.g. `a = gh_list_prs(); b = gh_get_pr(a[0].number)`.
|
|
11020
|
+
//
|
|
11021
|
+
// Rules, in order:
|
|
11022
|
+
// 1. Single text item whose text is valid JSON -> the parsed JSON value.
|
|
11023
|
+
// 2. Single text item (non-JSON) -> the raw text string.
|
|
11024
|
+
// 3. Everything else (multi-item, non-text, empty) -> the content array.
|
|
11025
|
+
//
|
|
11026
|
+
// This is intentionally simple and loss-free: callers can still reach
|
|
11027
|
+
// the full wire payload via the `partial` / `steps` objects if needed.
|
|
11028
|
+
static parseStepPayload(result) {
|
|
11029
|
+
const content = result.content;
|
|
11030
|
+
if (!Array.isArray(content) || content.length !== 1) return content ?? [];
|
|
11031
|
+
const item = content[0];
|
|
11032
|
+
if (item.type !== "text" || typeof item.text !== "string") return content;
|
|
11033
|
+
try {
|
|
11034
|
+
return JSON.parse(item.text);
|
|
11035
|
+
} catch {
|
|
11036
|
+
return item.text;
|
|
11037
|
+
}
|
|
11038
|
+
}
|
|
9906
11039
|
// Declarative pipeline executor. Runs N tool calls in order, binding
|
|
9907
11040
|
// each output under the step's id (or positional index), and lets
|
|
9908
11041
|
// later steps splice those outputs into their args via
|
|
@@ -9934,6 +11067,7 @@ To load the top pack in one step, call \`mcp_connect_activate\` with namespaces=
|
|
|
9934
11067
|
const explicitReturn = typeof args.return === "string" ? args.return : void 0;
|
|
9935
11068
|
const bindings = {};
|
|
9936
11069
|
const stepKeys = [];
|
|
11070
|
+
const stepNamespaces = /* @__PURE__ */ new Map();
|
|
9937
11071
|
for (let i = 0; i < steps.length; i++) {
|
|
9938
11072
|
const step = steps[i];
|
|
9939
11073
|
const key = stepBindingKey(step, i);
|
|
@@ -10003,9 +11137,26 @@ To load the top pack in one step, call \`mcp_connect_activate\` with namespaces=
|
|
|
10003
11137
|
isError: true
|
|
10004
11138
|
};
|
|
10005
11139
|
}
|
|
10006
|
-
const
|
|
11140
|
+
const stepNs = this.toolRoutes.get(step.tool)?.namespace;
|
|
11141
|
+
if (stepNs) stepNamespaces.set(key, stepNs);
|
|
11142
|
+
const stepResult = await this.handleToolCall(step.tool, resolvedArgs, void 0, { deferLearning: true });
|
|
10007
11143
|
if (stepResult.isError) {
|
|
10008
11144
|
const errText = stepResult.content?.[0]?.text ?? "unknown error";
|
|
11145
|
+
const routingFault = errText.includes("no longer available") || errText.includes("no longer connected") || errText.includes("auto-reconnect failed") || errText.includes("Unknown tool:");
|
|
11146
|
+
if (stepNs && !routingFault) {
|
|
11147
|
+
const inputShaped = errText.includes("[code=-32602]") || classifyError(errText) === "validation_error";
|
|
11148
|
+
const deps = collectRefDeps(step.args);
|
|
11149
|
+
if (inputShaped && deps.length > 0) {
|
|
11150
|
+
this.learning.recordOutcome(stepNs, 0.5);
|
|
11151
|
+
for (const dep of deps) {
|
|
11152
|
+
const depNs = stepNamespaces.get(dep);
|
|
11153
|
+
if (depNs) this.learning.recordOutcome(depNs, 0.5);
|
|
11154
|
+
}
|
|
11155
|
+
} else {
|
|
11156
|
+
this.learning.recordOutcome(stepNs, 0);
|
|
11157
|
+
}
|
|
11158
|
+
this.scheduleStateSave();
|
|
11159
|
+
}
|
|
10009
11160
|
return {
|
|
10010
11161
|
content: [
|
|
10011
11162
|
{
|
|
@@ -10025,7 +11176,11 @@ To load the top pack in one step, call \`mcp_connect_activate\` with namespaces=
|
|
|
10025
11176
|
isError: true
|
|
10026
11177
|
};
|
|
10027
11178
|
}
|
|
10028
|
-
|
|
11179
|
+
if (stepNs) {
|
|
11180
|
+
this.learning.recordOutcome(stepNs, computeOutcomeReward(stepResult));
|
|
11181
|
+
this.scheduleStateSave();
|
|
11182
|
+
}
|
|
11183
|
+
bindings[key] = _ConnectServer.parseStepPayload(stepResult);
|
|
10029
11184
|
}
|
|
10030
11185
|
const returnKey = explicitReturn ?? stepKeys[stepKeys.length - 1];
|
|
10031
11186
|
const finalResult = bindings[returnKey];
|
|
@@ -10097,7 +11252,7 @@ function parseServersArgs(argv) {
|
|
|
10097
11252
|
if (a === "--json") {
|
|
10098
11253
|
json = true;
|
|
10099
11254
|
} else if (a === "--help" || a === "-h") {
|
|
10100
|
-
return { ok: false, error: SERVERS_USAGE };
|
|
11255
|
+
return { ok: false, error: SERVERS_USAGE, help: true };
|
|
10101
11256
|
} else if (a.startsWith("-")) {
|
|
10102
11257
|
return { ok: false, error: `yaw-mcp servers: unknown argument "${a}"
|
|
10103
11258
|
|
|
@@ -10171,7 +11326,12 @@ async function runServersCommand(opts = {}) {
|
|
|
10171
11326
|
})
|
|
10172
11327
|
};
|
|
10173
11328
|
if (opts.json) {
|
|
10174
|
-
|
|
11329
|
+
const payload = {
|
|
11330
|
+
...merged,
|
|
11331
|
+
filter: opts.filter ?? null,
|
|
11332
|
+
filterMatched: opts.filter ? merged.servers.length > 0 : null
|
|
11333
|
+
};
|
|
11334
|
+
print(JSON.stringify(payload, null, 2));
|
|
10175
11335
|
return { exitCode: 0, lines };
|
|
10176
11336
|
}
|
|
10177
11337
|
if (opts.filter && filtered.servers.length === 0) {
|
|
@@ -10221,8 +11381,165 @@ function truncateVersion(v) {
|
|
|
10221
11381
|
return v.length > 8 ? v.slice(0, 8) : v;
|
|
10222
11382
|
}
|
|
10223
11383
|
|
|
11384
|
+
// src/set-active-cmd.ts
|
|
11385
|
+
import { homedir as homedir16 } from "os";
|
|
11386
|
+
|
|
11387
|
+
// src/sync-state.ts
|
|
11388
|
+
import { existsSync as existsSync7 } from "fs";
|
|
11389
|
+
import { mkdir as mkdir4, readFile as readFile11 } from "fs/promises";
|
|
11390
|
+
import { dirname as dirname2, join as join11 } from "path";
|
|
11391
|
+
var SYNC_STATE_FILENAME = "sync-state.json";
|
|
11392
|
+
function syncStatePath(home) {
|
|
11393
|
+
return join11(home, CONFIG_DIRNAME, SYNC_STATE_FILENAME);
|
|
11394
|
+
}
|
|
11395
|
+
async function readSyncState(home) {
|
|
11396
|
+
const path5 = syncStatePath(home);
|
|
11397
|
+
if (!existsSync7(path5)) return {};
|
|
11398
|
+
try {
|
|
11399
|
+
const raw = await readFile11(path5, "utf8");
|
|
11400
|
+
const parsed = JSON.parse(raw);
|
|
11401
|
+
if (!parsed || typeof parsed !== "object") return {};
|
|
11402
|
+
return parsed;
|
|
11403
|
+
} catch {
|
|
11404
|
+
return {};
|
|
11405
|
+
}
|
|
11406
|
+
}
|
|
11407
|
+
async function writeSyncState(home, state) {
|
|
11408
|
+
const path5 = syncStatePath(home);
|
|
11409
|
+
await mkdir4(dirname2(path5), { recursive: true });
|
|
11410
|
+
await atomicWriteFile(path5, `${JSON.stringify(state, null, 2)}
|
|
11411
|
+
`);
|
|
11412
|
+
}
|
|
11413
|
+
|
|
11414
|
+
// src/set-active-cmd.ts
|
|
11415
|
+
var SET_ACTIVE_RESOURCE = "mcp_bundles";
|
|
11416
|
+
var SET_ACTIVE_USAGE = `Usage: yaw-mcp set-active <namespace> <on|off>
|
|
11417
|
+
|
|
11418
|
+
Enable or disable a server in your shared Yaw Team config. The change is
|
|
11419
|
+
authoritative -- it applies to every member's config on their next
|
|
11420
|
+
sync/connect.
|
|
11421
|
+
|
|
11422
|
+
<namespace> The server's namespace (see \`yaw-mcp sync status\` or the dashboard).
|
|
11423
|
+
on|off Whether the server should be active.
|
|
11424
|
+
--json Emit machine-readable JSON instead of prose.
|
|
11425
|
+
|
|
11426
|
+
Sign in first with \`yaw-mcp login --key <license-key>\`. To hide a server only
|
|
11427
|
+
on THIS machine, edit the \`blocked\` list in ~/.yaw-mcp/config.json instead.`;
|
|
11428
|
+
function parseState(s) {
|
|
11429
|
+
const v = s.toLowerCase();
|
|
11430
|
+
if (v === "on" || v === "true" || v === "enable" || v === "enabled") return true;
|
|
11431
|
+
if (v === "off" || v === "false" || v === "disable" || v === "disabled") return false;
|
|
11432
|
+
return null;
|
|
11433
|
+
}
|
|
11434
|
+
function parseSetActiveArgs(argv) {
|
|
11435
|
+
const opts = {};
|
|
11436
|
+
const positionals = [];
|
|
11437
|
+
for (const a of argv) {
|
|
11438
|
+
if (a === "--json") opts.json = true;
|
|
11439
|
+
else if (a === "--help" || a === "-h") return { ok: false, error: SET_ACTIVE_USAGE, help: true };
|
|
11440
|
+
else if (a.startsWith("-"))
|
|
11441
|
+
return { ok: false, error: `yaw-mcp set-active: unknown flag "${a}"
|
|
11442
|
+
|
|
11443
|
+
${SET_ACTIVE_USAGE}` };
|
|
11444
|
+
else positionals.push(a);
|
|
11445
|
+
}
|
|
11446
|
+
if (positionals.length > 2)
|
|
11447
|
+
return { ok: false, error: `yaw-mcp set-active: too many arguments
|
|
11448
|
+
|
|
11449
|
+
${SET_ACTIVE_USAGE}` };
|
|
11450
|
+
const [ns, state] = positionals;
|
|
11451
|
+
if (!ns || !state)
|
|
11452
|
+
return { ok: false, error: `yaw-mcp set-active: <namespace> and <on|off> are required
|
|
11453
|
+
|
|
11454
|
+
${SET_ACTIVE_USAGE}` };
|
|
11455
|
+
if (!NAMESPACE_RE.test(ns))
|
|
11456
|
+
return { ok: false, error: `yaw-mcp set-active: invalid namespace "${ns}"
|
|
11457
|
+
|
|
11458
|
+
${SET_ACTIVE_USAGE}` };
|
|
11459
|
+
const active = parseState(state);
|
|
11460
|
+
if (active === null)
|
|
11461
|
+
return { ok: false, error: `yaw-mcp set-active: state must be on|off (got "${state}")
|
|
11462
|
+
|
|
11463
|
+
${SET_ACTIVE_USAGE}` };
|
|
11464
|
+
opts.namespace = ns;
|
|
11465
|
+
opts.active = active;
|
|
11466
|
+
return { ok: true, options: opts };
|
|
11467
|
+
}
|
|
11468
|
+
async function runSetActive(opts, io = { out: (s) => process.stdout.write(s), err: (s) => process.stderr.write(s) }, deps = { getResource, putResource, writeSyncState }) {
|
|
11469
|
+
const { namespace, active } = opts;
|
|
11470
|
+
if (!namespace || active === void 0) {
|
|
11471
|
+
io.err("yaw-mcp set-active: <namespace> and <on|off> are required\n");
|
|
11472
|
+
return { exitCode: 2 };
|
|
11473
|
+
}
|
|
11474
|
+
const base = { home: opts.home, baseUrl: opts.baseUrl };
|
|
11475
|
+
try {
|
|
11476
|
+
for (let attempt = 0; ; attempt++) {
|
|
11477
|
+
const res = await deps.getResource(SET_ACTIVE_RESOURCE, base);
|
|
11478
|
+
const data = res.data ?? { servers: [] };
|
|
11479
|
+
const servers = Array.isArray(data.servers) ? data.servers : [];
|
|
11480
|
+
const idx = servers.findIndex((s) => s?.namespace === namespace);
|
|
11481
|
+
if (idx < 0) {
|
|
11482
|
+
return fail(
|
|
11483
|
+
io,
|
|
11484
|
+
opts.json,
|
|
11485
|
+
`No team server with namespace "${namespace}". Run \`yaw-mcp sync status\` to list them.`,
|
|
11486
|
+
1
|
|
11487
|
+
);
|
|
11488
|
+
}
|
|
11489
|
+
if (servers[idx].isActive !== false === active) {
|
|
11490
|
+
return done(io, opts.json, namespace, active, false);
|
|
11491
|
+
}
|
|
11492
|
+
const nextServers = servers.map((s, i) => i === idx ? { ...s, isActive: active } : s);
|
|
11493
|
+
try {
|
|
11494
|
+
const putRes = await deps.putResource(
|
|
11495
|
+
SET_ACTIVE_RESOURCE,
|
|
11496
|
+
res.version,
|
|
11497
|
+
{ ...data, version: 1, servers: nextServers },
|
|
11498
|
+
base
|
|
11499
|
+
);
|
|
11500
|
+
if (typeof putRes.version === "number") {
|
|
11501
|
+
await deps.writeSyncState(opts.home ?? homedir16(), {
|
|
11502
|
+
mcp_bundles: { lastPulledVersion: putRes.version }
|
|
11503
|
+
}).catch(() => {
|
|
11504
|
+
});
|
|
11505
|
+
}
|
|
11506
|
+
return done(io, opts.json, namespace, active, true);
|
|
11507
|
+
} catch (e) {
|
|
11508
|
+
if (e instanceof TeamSyncStaleVersionError && attempt < 1) continue;
|
|
11509
|
+
throw e;
|
|
11510
|
+
}
|
|
11511
|
+
}
|
|
11512
|
+
} catch (e) {
|
|
11513
|
+
if (e instanceof TeamSyncAuthError)
|
|
11514
|
+
return fail(io, opts.json, "Not signed in. Run `yaw-mcp login --key <license-key>` first.", 1);
|
|
11515
|
+
if (e instanceof TeamSyncForbiddenError)
|
|
11516
|
+
return fail(io, opts.json, "You do not have permission to edit the team's servers.", 1);
|
|
11517
|
+
return fail(io, opts.json, e instanceof Error ? e.message : String(e), 1);
|
|
11518
|
+
}
|
|
11519
|
+
}
|
|
11520
|
+
function done(io, json, namespace, active, changed) {
|
|
11521
|
+
if (json) {
|
|
11522
|
+
io.out(`${JSON.stringify({ ok: true, namespace, isActive: active, changed })}
|
|
11523
|
+
`);
|
|
11524
|
+
} else if (changed) {
|
|
11525
|
+
io.out(`${namespace} is now ${active ? "active" : "inactive"} for the team.
|
|
11526
|
+
`);
|
|
11527
|
+
} else {
|
|
11528
|
+
io.out(`${namespace} is already ${active ? "active" : "inactive"}.
|
|
11529
|
+
`);
|
|
11530
|
+
}
|
|
11531
|
+
return { exitCode: 0 };
|
|
11532
|
+
}
|
|
11533
|
+
function fail(io, json, message, code) {
|
|
11534
|
+
if (json) io.err(`${JSON.stringify({ ok: false, error: message })}
|
|
11535
|
+
`);
|
|
11536
|
+
else io.err(`yaw-mcp set-active: ${message}
|
|
11537
|
+
`);
|
|
11538
|
+
return { exitCode: code };
|
|
11539
|
+
}
|
|
11540
|
+
|
|
10224
11541
|
// src/stats-cmd.ts
|
|
10225
|
-
import { homedir as
|
|
11542
|
+
import { homedir as homedir17 } from "os";
|
|
10226
11543
|
var STATS_USAGE = `Usage: yaw-mcp stats [--json] [--limit N] [--days N]
|
|
10227
11544
|
|
|
10228
11545
|
Print a digest of recent AI tool calls recorded against your Yaw
|
|
@@ -10242,18 +11559,22 @@ function parseStatsArgs(argv) {
|
|
|
10242
11559
|
const v = argv[++i];
|
|
10243
11560
|
const n = Number.parseInt(v ?? "", 10);
|
|
10244
11561
|
if (!Number.isFinite(n) || n <= 0 || n > 1e3)
|
|
10245
|
-
return { ok: false, error:
|
|
11562
|
+
return { ok: false, error: `yaw-mcp stats: --limit must be a positive integer up to 1000
|
|
11563
|
+
|
|
11564
|
+
${STATS_USAGE}` };
|
|
10246
11565
|
opts.limit = n;
|
|
10247
11566
|
} else if (a === "--days") {
|
|
10248
11567
|
const v = argv[++i];
|
|
10249
11568
|
const n = Number.parseInt(v ?? "", 10);
|
|
10250
11569
|
if (!Number.isFinite(n) || n <= 0 || n > 365)
|
|
10251
|
-
return { ok: false, error:
|
|
11570
|
+
return { ok: false, error: `yaw-mcp stats: --days must be a positive integer up to 365
|
|
11571
|
+
|
|
11572
|
+
${STATS_USAGE}` };
|
|
10252
11573
|
opts.days = n;
|
|
10253
11574
|
} else if (a === "--json") {
|
|
10254
11575
|
opts.json = true;
|
|
10255
11576
|
} else if (a === "--help" || a === "-h") {
|
|
10256
|
-
return { ok: false, error: STATS_USAGE };
|
|
11577
|
+
return { ok: false, error: STATS_USAGE, help: true };
|
|
10257
11578
|
} else {
|
|
10258
11579
|
return { ok: false, error: `yaw-mcp stats: unknown argument "${a}"
|
|
10259
11580
|
|
|
@@ -10294,8 +11615,9 @@ function aggregate(events) {
|
|
|
10294
11615
|
function formatPlain(events, opts, orderId, total) {
|
|
10295
11616
|
const lines = [];
|
|
10296
11617
|
lines.push(`Signed in to order ${orderId}.`);
|
|
11618
|
+
const renderedCount = Math.min(events.length, opts.limit ?? 50);
|
|
10297
11619
|
lines.push(
|
|
10298
|
-
`Showing ${
|
|
11620
|
+
`Showing ${renderedCount} of ${total} event${total === 1 ? "" : "s"} from the last ${opts.days ?? 7} day${opts.days === 1 ? "" : "s"}.`
|
|
10299
11621
|
);
|
|
10300
11622
|
if (events.length === 0) {
|
|
10301
11623
|
lines.push("");
|
|
@@ -10306,7 +11628,7 @@ function formatPlain(events, opts, orderId, total) {
|
|
|
10306
11628
|
}
|
|
10307
11629
|
const agg = aggregate(events);
|
|
10308
11630
|
lines.push("");
|
|
10309
|
-
lines.push("By server:");
|
|
11631
|
+
lines.push("By server (full window):");
|
|
10310
11632
|
const nsCol = Math.max(...agg.byNamespace.map((n) => n.namespace.length), 6);
|
|
10311
11633
|
for (const n of agg.byNamespace) {
|
|
10312
11634
|
const success = n.success.toString().padStart(5);
|
|
@@ -10337,7 +11659,7 @@ async function runStats(opts, io = {
|
|
|
10337
11659
|
out: (s) => process.stdout.write(s),
|
|
10338
11660
|
err: (s) => process.stderr.write(s)
|
|
10339
11661
|
}) {
|
|
10340
|
-
const home = opts.home ??
|
|
11662
|
+
const home = opts.home ?? homedir17();
|
|
10341
11663
|
const session = await getSession({ home, baseUrl: opts.baseUrl });
|
|
10342
11664
|
if (!session) {
|
|
10343
11665
|
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>";
|
|
@@ -10351,7 +11673,7 @@ async function runStats(opts, io = {
|
|
|
10351
11673
|
const result = await listAnalyticsEvents({ home, baseUrl: opts.baseUrl });
|
|
10352
11674
|
const days = opts.days ?? 7;
|
|
10353
11675
|
const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
10354
|
-
const filtered = result.events.filter((e) => e.ts >= cutoff);
|
|
11676
|
+
const filtered = result.events.filter((e) => e.ts >= cutoff).sort((a, b) => a.ts - b.ts);
|
|
10355
11677
|
if (opts.json) {
|
|
10356
11678
|
io.out(
|
|
10357
11679
|
`${JSON.stringify(
|
|
@@ -10394,10 +11716,10 @@ async function runStats(opts, io = {
|
|
|
10394
11716
|
}
|
|
10395
11717
|
|
|
10396
11718
|
// src/sync-cmd.ts
|
|
10397
|
-
import { existsSync as
|
|
10398
|
-
import { mkdir as
|
|
10399
|
-
import { homedir as
|
|
10400
|
-
import { dirname as
|
|
11719
|
+
import { existsSync as existsSync8 } from "fs";
|
|
11720
|
+
import { mkdir as mkdir5, readFile as readFile12 } from "fs/promises";
|
|
11721
|
+
import { homedir as homedir18 } from "os";
|
|
11722
|
+
import { dirname as dirname3, join as join12 } from "path";
|
|
10401
11723
|
var SYNC_USAGE = `Usage: yaw-mcp sync <push|pull|status> [--json]
|
|
10402
11724
|
|
|
10403
11725
|
Replicate ~/.yaw-mcp/bundles.json across machines via your Yaw
|
|
@@ -10410,7 +11732,9 @@ var SYNC_USAGE = `Usage: yaw-mcp sync <push|pull|status> [--json]
|
|
|
10410
11732
|
status Show sign-in state, last-pulled version, and a coarse
|
|
10411
11733
|
local-vs-remote diff.
|
|
10412
11734
|
|
|
10413
|
-
--json
|
|
11735
|
+
--json Emit machine-readable JSON.
|
|
11736
|
+
--dry-run (pull only) Preview the merge without writing the local file
|
|
11737
|
+
or advancing the sync version.
|
|
10414
11738
|
|
|
10415
11739
|
Sign in first with \`yaw-mcp login --key <license-key>\`.`;
|
|
10416
11740
|
var BUNDLES_FILENAME2 = "bundles.json";
|
|
@@ -10426,8 +11750,10 @@ ${SYNC_USAGE}` };
|
|
|
10426
11750
|
opts.action = a;
|
|
10427
11751
|
} else if (a === "--json") {
|
|
10428
11752
|
opts.json = true;
|
|
11753
|
+
} else if (a === "--dry-run") {
|
|
11754
|
+
opts.dryRun = true;
|
|
10429
11755
|
} else if (a === "--help" || a === "-h") {
|
|
10430
|
-
return { ok: false, error: SYNC_USAGE };
|
|
11756
|
+
return { ok: false, error: SYNC_USAGE, help: true };
|
|
10431
11757
|
} else {
|
|
10432
11758
|
return { ok: false, error: `yaw-mcp sync: unknown argument "${a}"
|
|
10433
11759
|
|
|
@@ -10440,24 +11766,30 @@ ${SYNC_USAGE}` };
|
|
|
10440
11766
|
return { ok: true, options: opts };
|
|
10441
11767
|
}
|
|
10442
11768
|
function bundlesPath(home) {
|
|
10443
|
-
return
|
|
11769
|
+
return join12(home, CONFIG_DIRNAME, BUNDLES_FILENAME2);
|
|
10444
11770
|
}
|
|
10445
11771
|
async function readLocalBundles(home) {
|
|
10446
|
-
const
|
|
10447
|
-
if (!
|
|
10448
|
-
const raw = await
|
|
10449
|
-
|
|
11772
|
+
const path5 = bundlesPath(home);
|
|
11773
|
+
if (!existsSync8(path5)) return { version: 1, servers: [] };
|
|
11774
|
+
const raw = await readFile12(path5, "utf8");
|
|
11775
|
+
let parsed;
|
|
11776
|
+
try {
|
|
11777
|
+
parsed = JSON.parse(raw);
|
|
11778
|
+
} catch (err) {
|
|
11779
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11780
|
+
throw new Error(`${path5}: invalid JSON -- ${msg}. Fix the file before running sync.`);
|
|
11781
|
+
}
|
|
10450
11782
|
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.servers)) {
|
|
10451
|
-
throw new Error(`${
|
|
11783
|
+
throw new Error(`${path5}: malformed -- expected { servers: [...] }`);
|
|
10452
11784
|
}
|
|
10453
11785
|
return parsed;
|
|
10454
11786
|
}
|
|
10455
11787
|
async function writeLocalBundles(home, file) {
|
|
10456
|
-
const
|
|
10457
|
-
await
|
|
10458
|
-
await atomicWriteFile(
|
|
11788
|
+
const path5 = bundlesPath(home);
|
|
11789
|
+
await mkdir5(dirname3(path5), { recursive: true });
|
|
11790
|
+
await atomicWriteFile(path5, `${JSON.stringify(file, null, 2)}
|
|
10459
11791
|
`);
|
|
10460
|
-
return
|
|
11792
|
+
return path5;
|
|
10461
11793
|
}
|
|
10462
11794
|
function stripEnvValues(server) {
|
|
10463
11795
|
if (!server.env || typeof server.env !== "object") return server;
|
|
@@ -10478,11 +11810,19 @@ function mergeLocalEnv(incoming, local) {
|
|
|
10478
11810
|
return { ...srv, env: merged };
|
|
10479
11811
|
});
|
|
10480
11812
|
}
|
|
11813
|
+
function mergeRemoteActive(localStripped, remoteServers) {
|
|
11814
|
+
const remoteByNs = new Map(remoteServers.filter((s) => s.namespace).map((s) => [s.namespace, s]));
|
|
11815
|
+
return localStripped.map((srv) => {
|
|
11816
|
+
const matched = srv.namespace ? remoteByNs.get(srv.namespace) : void 0;
|
|
11817
|
+
if (!matched || matched.isActive === void 0) return srv;
|
|
11818
|
+
return { ...srv, isActive: matched.isActive };
|
|
11819
|
+
});
|
|
11820
|
+
}
|
|
10481
11821
|
async function runSync(opts, io = {
|
|
10482
11822
|
out: (s) => process.stdout.write(s),
|
|
10483
11823
|
err: (s) => process.stderr.write(s)
|
|
10484
11824
|
}) {
|
|
10485
|
-
const home = opts.home ??
|
|
11825
|
+
const home = opts.home ?? homedir18();
|
|
10486
11826
|
const session = await getSession({ home, baseUrl: opts.baseUrl });
|
|
10487
11827
|
if (!session) {
|
|
10488
11828
|
const msg = "Not signed in. Run `yaw-mcp login --key <license-key>` first.";
|
|
@@ -10508,7 +11848,9 @@ async function syncStatus(session, opts, io, home) {
|
|
|
10508
11848
|
home: opts.home,
|
|
10509
11849
|
baseUrl: opts.baseUrl
|
|
10510
11850
|
});
|
|
10511
|
-
const local = await readLocalBundles(home)
|
|
11851
|
+
const local = await readLocalBundles(home);
|
|
11852
|
+
const syncState = await readSyncState(home);
|
|
11853
|
+
const lastPulledVersion = syncState.mcp_bundles?.lastPulledVersion ?? null;
|
|
10512
11854
|
const localNs = new Set(local.servers.map((s) => s.namespace).filter((n) => typeof n === "string"));
|
|
10513
11855
|
const remoteNs = new Set(
|
|
10514
11856
|
(remote.data?.servers ?? []).map((s) => s.namespace).filter((n) => typeof n === "string")
|
|
@@ -10523,6 +11865,7 @@ async function syncStatus(session, opts, io, home) {
|
|
|
10523
11865
|
signedInAs: session.email,
|
|
10524
11866
|
role: session.role,
|
|
10525
11867
|
remoteVersion: remote.version,
|
|
11868
|
+
lastPulledVersion,
|
|
10526
11869
|
localOnly,
|
|
10527
11870
|
remoteOnly,
|
|
10528
11871
|
updatedAt: remote.updated_at,
|
|
@@ -10539,6 +11882,8 @@ async function syncStatus(session, opts, io, home) {
|
|
|
10539
11882
|
io.out(`Remote mcp_bundles: version ${remote.version}`);
|
|
10540
11883
|
if (remote.updated_at) io.out(`, updated ${remote.updated_at} by ${remote.updated_by ?? "unknown"}`);
|
|
10541
11884
|
io.out("\n");
|
|
11885
|
+
io.out(lastPulledVersion === null ? "Last pulled: never pulled.\n" : `Last pulled: v${lastPulledVersion}.
|
|
11886
|
+
`);
|
|
10542
11887
|
if (localOnly.length > 0) io.out(`Local-only servers: ${localOnly.join(", ")}
|
|
10543
11888
|
`);
|
|
10544
11889
|
if (remoteOnly.length > 0) io.out(`Remote-only servers: ${remoteOnly.join(", ")}
|
|
@@ -10553,9 +11898,26 @@ async function syncPull(opts, io, home) {
|
|
|
10553
11898
|
baseUrl: opts.baseUrl
|
|
10554
11899
|
});
|
|
10555
11900
|
const remoteServers = remote.data?.servers ?? [];
|
|
10556
|
-
const local = await readLocalBundles(home)
|
|
11901
|
+
const local = await readLocalBundles(home);
|
|
10557
11902
|
const merged = mergeLocalEnv(remoteServers, local.servers);
|
|
11903
|
+
if (opts.dryRun) {
|
|
11904
|
+
const target = bundlesPath(home);
|
|
11905
|
+
if (opts.json) {
|
|
11906
|
+
io.out(
|
|
11907
|
+
`${JSON.stringify({ ok: true, dryRun: true, wouldWrite: target, serverCount: merged.length, remoteVersion: remote.version }, null, 2)}
|
|
11908
|
+
`
|
|
11909
|
+
);
|
|
11910
|
+
} else {
|
|
11911
|
+
io.out(
|
|
11912
|
+
`[dry-run] would pull ${merged.length} server${merged.length === 1 ? "" : "s"} -> ${target} (remote v${remote.version}); nothing written.
|
|
11913
|
+
`
|
|
11914
|
+
);
|
|
11915
|
+
if (remote.version === 0) io.out("Remote mcp_bundles is empty (version 0). Push from this machine to seed it.\n");
|
|
11916
|
+
}
|
|
11917
|
+
return { exitCode: 0 };
|
|
11918
|
+
}
|
|
10558
11919
|
const written = await writeLocalBundles(home, { version: 1, servers: merged });
|
|
11920
|
+
await writeSyncState(home, { mcp_bundles: { lastPulledVersion: remote.version } });
|
|
10559
11921
|
if (opts.json) {
|
|
10560
11922
|
io.out(
|
|
10561
11923
|
`${JSON.stringify({ ok: true, written, serverCount: merged.length, remoteVersion: remote.version }, null, 2)}
|
|
@@ -10574,19 +11936,24 @@ async function syncPush(opts, io, home) {
|
|
|
10574
11936
|
home: opts.home,
|
|
10575
11937
|
baseUrl: opts.baseUrl
|
|
10576
11938
|
});
|
|
10577
|
-
const
|
|
11939
|
+
const remoteServers = remote.data?.servers ?? [];
|
|
11940
|
+
const stripped = mergeRemoteActive(local.servers.map(stripEnvValues), remoteServers);
|
|
10578
11941
|
const payload = { version: 1, servers: stripped };
|
|
10579
|
-
const
|
|
11942
|
+
const syncState = await readSyncState(home);
|
|
11943
|
+
const lastPulled = syncState.mcp_bundles?.lastPulledVersion;
|
|
11944
|
+
const pushVersion = lastPulled ?? remote.version;
|
|
11945
|
+
const res = await putResource(MCP_BUNDLES_RESOURCE, pushVersion, payload, {
|
|
10580
11946
|
home: opts.home,
|
|
10581
11947
|
baseUrl: opts.baseUrl
|
|
10582
11948
|
});
|
|
11949
|
+
await writeSyncState(home, { mcp_bundles: { lastPulledVersion: res.version } });
|
|
10583
11950
|
if (opts.json) {
|
|
10584
11951
|
io.out(`${JSON.stringify({ ok: true, serverCount: stripped.length, newVersion: res.version }, null, 2)}
|
|
10585
11952
|
`);
|
|
10586
11953
|
} else {
|
|
10587
11954
|
io.out(`Pushed ${stripped.length} server${stripped.length === 1 ? "" : "s"} -> mcp_bundles v${res.version}.
|
|
10588
11955
|
`);
|
|
10589
|
-
io.out("Env values stripped before upload;
|
|
11956
|
+
io.out("Env values stripped before upload; use `yaw-mcp secrets push` to sync secrets across machines.\n");
|
|
10590
11957
|
}
|
|
10591
11958
|
return { exitCode: 0 };
|
|
10592
11959
|
}
|
|
@@ -10625,6 +11992,7 @@ function handleSyncError(err, opts, io) {
|
|
|
10625
11992
|
var KNOWN_SUBCOMMANDS = [
|
|
10626
11993
|
"compliance",
|
|
10627
11994
|
"audit",
|
|
11995
|
+
"foundry",
|
|
10628
11996
|
"install",
|
|
10629
11997
|
"add",
|
|
10630
11998
|
"remove",
|
|
@@ -10642,6 +12010,7 @@ var KNOWN_SUBCOMMANDS = [
|
|
|
10642
12010
|
"sync",
|
|
10643
12011
|
"stats",
|
|
10644
12012
|
"secrets",
|
|
12013
|
+
"set-active",
|
|
10645
12014
|
"help",
|
|
10646
12015
|
"--help",
|
|
10647
12016
|
"-h",
|
|
@@ -10659,9 +12028,23 @@ if (subcommand === "compliance") {
|
|
|
10659
12028
|
process.exit(2);
|
|
10660
12029
|
}
|
|
10661
12030
|
runAudit(parsed.options).then((r) => process.exit(r.exitCode));
|
|
12031
|
+
} else if (subcommand === "foundry") {
|
|
12032
|
+
const parsed = parseFoundryArgs(process.argv.slice(3));
|
|
12033
|
+
if (!parsed.ok) {
|
|
12034
|
+
const isHelp = parsed.error === FOUNDRY_USAGE;
|
|
12035
|
+
(isHelp ? process.stdout : process.stderr).write(`${parsed.error}
|
|
12036
|
+
`);
|
|
12037
|
+
process.exit(isHelp ? 0 : 2);
|
|
12038
|
+
}
|
|
12039
|
+
runFoundryExport(parsed.options).then((r) => process.exit(r.exitCode));
|
|
10662
12040
|
} else if (subcommand === "install") {
|
|
10663
12041
|
const parsed = parseInstallArgs(process.argv.slice(3));
|
|
10664
12042
|
if (!parsed.ok) {
|
|
12043
|
+
if (parsed.help) {
|
|
12044
|
+
process.stdout.write(`${parsed.error}
|
|
12045
|
+
`);
|
|
12046
|
+
process.exit(0);
|
|
12047
|
+
}
|
|
10665
12048
|
process.stderr.write(`${parsed.error}
|
|
10666
12049
|
`);
|
|
10667
12050
|
process.exit(2);
|
|
@@ -10700,6 +12083,11 @@ if (subcommand === "compliance") {
|
|
|
10700
12083
|
} else if (subcommand === "servers") {
|
|
10701
12084
|
const parsed = parseServersArgs(process.argv.slice(3));
|
|
10702
12085
|
if (!parsed.ok) {
|
|
12086
|
+
if (parsed.help) {
|
|
12087
|
+
process.stdout.write(`${parsed.error}
|
|
12088
|
+
`);
|
|
12089
|
+
process.exit(0);
|
|
12090
|
+
}
|
|
10703
12091
|
process.stderr.write(`${parsed.error}
|
|
10704
12092
|
`);
|
|
10705
12093
|
process.exit(2);
|
|
@@ -10708,6 +12096,11 @@ if (subcommand === "compliance") {
|
|
|
10708
12096
|
} else if (subcommand === "bundles") {
|
|
10709
12097
|
const parsed = parseBundlesArgs(process.argv.slice(3));
|
|
10710
12098
|
if (!parsed.ok) {
|
|
12099
|
+
if (parsed.help) {
|
|
12100
|
+
process.stdout.write(`${parsed.error}
|
|
12101
|
+
`);
|
|
12102
|
+
process.exit(0);
|
|
12103
|
+
}
|
|
10711
12104
|
process.stderr.write(`${parsed.error}
|
|
10712
12105
|
`);
|
|
10713
12106
|
process.exit(2);
|
|
@@ -10716,6 +12109,11 @@ if (subcommand === "compliance") {
|
|
|
10716
12109
|
} else if (subcommand === "completion") {
|
|
10717
12110
|
const parsed = parseCompletionArgs(process.argv.slice(3));
|
|
10718
12111
|
if (!parsed.ok) {
|
|
12112
|
+
if (parsed.help) {
|
|
12113
|
+
process.stdout.write(`${parsed.error}
|
|
12114
|
+
`);
|
|
12115
|
+
process.exit(0);
|
|
12116
|
+
}
|
|
10719
12117
|
process.stderr.write(`${parsed.error}
|
|
10720
12118
|
`);
|
|
10721
12119
|
process.exit(2);
|
|
@@ -10724,6 +12122,11 @@ if (subcommand === "compliance") {
|
|
|
10724
12122
|
} else if (subcommand === "upgrade") {
|
|
10725
12123
|
const parsed = parseUpgradeArgs(process.argv.slice(3));
|
|
10726
12124
|
if (!parsed.ok) {
|
|
12125
|
+
if (parsed.help) {
|
|
12126
|
+
process.stdout.write(`${parsed.error}
|
|
12127
|
+
`);
|
|
12128
|
+
process.exit(0);
|
|
12129
|
+
}
|
|
10727
12130
|
process.stderr.write(`${parsed.error}
|
|
10728
12131
|
`);
|
|
10729
12132
|
process.exit(2);
|
|
@@ -10732,6 +12135,11 @@ if (subcommand === "compliance") {
|
|
|
10732
12135
|
} else if (subcommand === "try") {
|
|
10733
12136
|
const parsed = parseTryArgs(process.argv.slice(3));
|
|
10734
12137
|
if (!parsed.ok) {
|
|
12138
|
+
if (parsed.help) {
|
|
12139
|
+
process.stdout.write(`${parsed.error}
|
|
12140
|
+
`);
|
|
12141
|
+
process.exit(0);
|
|
12142
|
+
}
|
|
10735
12143
|
process.stderr.write(`${parsed.error}
|
|
10736
12144
|
`);
|
|
10737
12145
|
process.exit(2);
|
|
@@ -10740,6 +12148,11 @@ if (subcommand === "compliance") {
|
|
|
10740
12148
|
} else if (subcommand === "try-cleanup") {
|
|
10741
12149
|
const parsed = parseTryCleanupArgs(process.argv.slice(3));
|
|
10742
12150
|
if (!parsed.ok) {
|
|
12151
|
+
if (parsed.help) {
|
|
12152
|
+
process.stdout.write(`${parsed.error}
|
|
12153
|
+
`);
|
|
12154
|
+
process.exit(0);
|
|
12155
|
+
}
|
|
10743
12156
|
process.stderr.write(`${parsed.error}
|
|
10744
12157
|
`);
|
|
10745
12158
|
process.exit(2);
|
|
@@ -10748,6 +12161,11 @@ if (subcommand === "compliance") {
|
|
|
10748
12161
|
} else if (subcommand === "add") {
|
|
10749
12162
|
const parsed = parseAddArgs(process.argv.slice(3));
|
|
10750
12163
|
if (!parsed.ok) {
|
|
12164
|
+
if (parsed.help) {
|
|
12165
|
+
process.stdout.write(`${parsed.error}
|
|
12166
|
+
`);
|
|
12167
|
+
process.exit(0);
|
|
12168
|
+
}
|
|
10751
12169
|
process.stderr.write(`${parsed.error}
|
|
10752
12170
|
`);
|
|
10753
12171
|
process.exit(2);
|
|
@@ -10756,6 +12174,11 @@ if (subcommand === "compliance") {
|
|
|
10756
12174
|
} else if (subcommand === "remove") {
|
|
10757
12175
|
const parsed = parseRemoveArgs(process.argv.slice(3));
|
|
10758
12176
|
if (!parsed.ok) {
|
|
12177
|
+
if (parsed.help) {
|
|
12178
|
+
process.stdout.write(`${parsed.error}
|
|
12179
|
+
`);
|
|
12180
|
+
process.exit(0);
|
|
12181
|
+
}
|
|
10759
12182
|
process.stderr.write(`${parsed.error}
|
|
10760
12183
|
`);
|
|
10761
12184
|
process.exit(2);
|
|
@@ -10764,6 +12187,11 @@ if (subcommand === "compliance") {
|
|
|
10764
12187
|
} else if (subcommand === "list") {
|
|
10765
12188
|
const parsed = parseListArgs(process.argv.slice(3));
|
|
10766
12189
|
if (!parsed.ok) {
|
|
12190
|
+
if (parsed.help) {
|
|
12191
|
+
process.stdout.write(`${parsed.error}
|
|
12192
|
+
`);
|
|
12193
|
+
process.exit(0);
|
|
12194
|
+
}
|
|
10767
12195
|
process.stderr.write(`${parsed.error}
|
|
10768
12196
|
`);
|
|
10769
12197
|
process.exit(2);
|
|
@@ -10772,6 +12200,11 @@ if (subcommand === "compliance") {
|
|
|
10772
12200
|
} else if (subcommand === "login") {
|
|
10773
12201
|
const parsed = parseLoginArgs(process.argv.slice(3));
|
|
10774
12202
|
if (!parsed.ok) {
|
|
12203
|
+
if (parsed.help) {
|
|
12204
|
+
process.stdout.write(`${parsed.error}
|
|
12205
|
+
`);
|
|
12206
|
+
process.exit(0);
|
|
12207
|
+
}
|
|
10775
12208
|
process.stderr.write(`${parsed.error}
|
|
10776
12209
|
`);
|
|
10777
12210
|
process.exit(2);
|
|
@@ -10780,6 +12213,11 @@ if (subcommand === "compliance") {
|
|
|
10780
12213
|
} else if (subcommand === "logout") {
|
|
10781
12214
|
const parsed = parseLogoutArgs(process.argv.slice(3));
|
|
10782
12215
|
if (!parsed.ok) {
|
|
12216
|
+
if (parsed.help) {
|
|
12217
|
+
process.stdout.write(`${parsed.error}
|
|
12218
|
+
`);
|
|
12219
|
+
process.exit(0);
|
|
12220
|
+
}
|
|
10783
12221
|
process.stderr.write(`${parsed.error}
|
|
10784
12222
|
`);
|
|
10785
12223
|
process.exit(2);
|
|
@@ -10788,6 +12226,11 @@ if (subcommand === "compliance") {
|
|
|
10788
12226
|
} else if (subcommand === "sync") {
|
|
10789
12227
|
const parsed = parseSyncArgs(process.argv.slice(3));
|
|
10790
12228
|
if (!parsed.ok) {
|
|
12229
|
+
if (parsed.help) {
|
|
12230
|
+
process.stdout.write(`${parsed.error}
|
|
12231
|
+
`);
|
|
12232
|
+
process.exit(0);
|
|
12233
|
+
}
|
|
10791
12234
|
process.stderr.write(`${parsed.error}
|
|
10792
12235
|
`);
|
|
10793
12236
|
process.exit(2);
|
|
@@ -10796,6 +12239,11 @@ if (subcommand === "compliance") {
|
|
|
10796
12239
|
} else if (subcommand === "stats") {
|
|
10797
12240
|
const parsed = parseStatsArgs(process.argv.slice(3));
|
|
10798
12241
|
if (!parsed.ok) {
|
|
12242
|
+
if (parsed.help) {
|
|
12243
|
+
process.stdout.write(`${parsed.error}
|
|
12244
|
+
`);
|
|
12245
|
+
process.exit(0);
|
|
12246
|
+
}
|
|
10799
12247
|
process.stderr.write(`${parsed.error}
|
|
10800
12248
|
`);
|
|
10801
12249
|
process.exit(2);
|
|
@@ -10804,19 +12252,38 @@ if (subcommand === "compliance") {
|
|
|
10804
12252
|
} else if (subcommand === "secrets") {
|
|
10805
12253
|
const parsed = parseSecretsArgs(process.argv.slice(3));
|
|
10806
12254
|
if (!parsed.ok) {
|
|
12255
|
+
if (parsed.help) {
|
|
12256
|
+
process.stdout.write(`${parsed.error}
|
|
12257
|
+
`);
|
|
12258
|
+
process.exit(0);
|
|
12259
|
+
}
|
|
10807
12260
|
process.stderr.write(`${parsed.error}
|
|
10808
12261
|
`);
|
|
10809
12262
|
process.exit(2);
|
|
10810
12263
|
}
|
|
10811
12264
|
runSecrets(parsed.options).then((r) => process.exit(r.exitCode));
|
|
12265
|
+
} else if (subcommand === "set-active") {
|
|
12266
|
+
const parsed = parseSetActiveArgs(process.argv.slice(3));
|
|
12267
|
+
if (!parsed.ok) {
|
|
12268
|
+
if (parsed.help) {
|
|
12269
|
+
process.stdout.write(`${parsed.error}
|
|
12270
|
+
`);
|
|
12271
|
+
process.exit(0);
|
|
12272
|
+
}
|
|
12273
|
+
process.stderr.write(`${parsed.error}
|
|
12274
|
+
`);
|
|
12275
|
+
process.exit(2);
|
|
12276
|
+
}
|
|
12277
|
+
runSetActive(parsed.options).then((r) => process.exit(r.exitCode));
|
|
10812
12278
|
} else if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
|
|
10813
12279
|
process.stdout.write(`
|
|
10814
12280
|
yaw-mcp \u2014 one install, every MCP server, managed from the cloud.
|
|
10815
12281
|
|
|
10816
12282
|
Quickstart:
|
|
10817
|
-
1.
|
|
10818
|
-
2.
|
|
10819
|
-
3.
|
|
12283
|
+
1. Install yaw-mcp yaw-mcp install claude-code
|
|
12284
|
+
2. Verify setup yaw-mcp doctor
|
|
12285
|
+
3. Yaw Team (optional) yaw-mcp login --key <license-key>
|
|
12286
|
+
https://yaw.sh/mcp/dashboard/settings/tokens
|
|
10820
12287
|
|
|
10821
12288
|
Setup (connect a client to yaw-mcp):
|
|
10822
12289
|
install <client> Connect one MCP client to yaw-mcp. This wires the
|
|
@@ -10919,12 +12386,12 @@ if (subcommand === "compliance") {
|
|
|
10919
12386
|
(or kill yaw-mcp; the client will respawn it) after editing any config.
|
|
10920
12387
|
|
|
10921
12388
|
Docs: https://yaw.sh/mcp
|
|
10922
|
-
Source: https://github.com/YawLabs/
|
|
12389
|
+
Source: https://github.com/YawLabs/mcp
|
|
10923
12390
|
|
|
10924
12391
|
`);
|
|
10925
12392
|
process.exit(0);
|
|
10926
12393
|
} else if (subcommand === "--version" || subcommand === "-V") {
|
|
10927
|
-
process.stdout.write(`yaw-mcp ${true ? "0.
|
|
12394
|
+
process.stdout.write(`yaw-mcp ${true ? "0.62.0" : "dev"}
|
|
10928
12395
|
`);
|
|
10929
12396
|
process.exit(0);
|
|
10930
12397
|
} else if (subcommand && !subcommand.startsWith("-")) {
|
|
@@ -10966,6 +12433,8 @@ async function runServer() {
|
|
|
10966
12433
|
};
|
|
10967
12434
|
process.on("SIGTERM", shutdown);
|
|
10968
12435
|
process.on("SIGINT", shutdown);
|
|
12436
|
+
process.on("unhandledRejection", (e) => log("error", "unhandledRejection", { error: String(e) }));
|
|
12437
|
+
process.on("uncaughtException", (e) => log("error", "uncaughtException", { error: String(e) }));
|
|
10969
12438
|
server.start().catch((err) => {
|
|
10970
12439
|
if (err instanceof ConfigError && err.fatal) {
|
|
10971
12440
|
const msg2 = err instanceof Error ? err.message : String(err);
|