@yawlabs/mcph 0.47.1 → 0.47.3

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/dist/index.js CHANGED
@@ -125,7 +125,8 @@ function stripJsoncComments(src) {
125
125
  return out;
126
126
  }
127
127
  function parseJsonc(src) {
128
- const stripped = stripJsoncComments(src);
128
+ const debommed = src.charCodeAt(0) === 65279 ? src.slice(1) : src;
129
+ const stripped = stripJsoncComments(debommed);
129
130
  return JSON.parse(stripped);
130
131
  }
131
132
 
@@ -191,9 +192,9 @@ var LEGACY_PROJECT_FILENAME = ".mcph.json";
191
192
  var LEGACY_LOCAL_FILENAME = ".mcph.local.json";
192
193
  var NEW_CONFIG_FILENAME = "config.json";
193
194
  var NEW_LOCAL_FILENAME = "config.local.json";
194
- async function exists(path4) {
195
+ async function exists(path5) {
195
196
  try {
196
- await stat(path4);
197
+ await stat(path5);
197
198
  return true;
198
199
  } catch {
199
200
  return false;
@@ -244,7 +245,7 @@ async function migrateLegacyConfigPaths(opts) {
244
245
  }
245
246
  }
246
247
  async function findLegacyProjectRoot(cwd, home) {
247
- const { resolve: resolve4, dirname: dirname3 } = await import("path");
248
+ const { resolve: resolve4, dirname: dirname2 } = await import("path");
248
249
  const homeResolved = resolve4(home);
249
250
  let dir = resolve4(cwd);
250
251
  let prev = "";
@@ -254,7 +255,7 @@ async function findLegacyProjectRoot(cwd, home) {
254
255
  const legacyLocal = join(dir, LEGACY_LOCAL_FILENAME);
255
256
  if (await exists(legacyProject) || await exists(legacyLocal)) return dir;
256
257
  prev = dir;
257
- dir = dirname3(dir);
258
+ dir = dirname2(dir);
258
259
  }
259
260
  return null;
260
261
  }
@@ -264,10 +265,10 @@ var CONFIG_FILENAME = "config.json";
264
265
  var LOCAL_CONFIG_FILENAME = "config.local.json";
265
266
  var CURRENT_SCHEMA_VERSION = 1;
266
267
  var DEFAULT_API_BASE = "https://mcp.hosting";
267
- async function readConfigAt(path4, scope, warnings) {
268
+ async function readConfigAt(path5, scope, warnings) {
268
269
  let raw;
269
270
  try {
270
- raw = await readFile(path4, "utf8");
271
+ raw = await readFile(path5, "utf8");
271
272
  } catch {
272
273
  return null;
273
274
  }
@@ -276,43 +277,43 @@ async function readConfigAt(path4, scope, warnings) {
276
277
  parsed = parseJsonc(raw);
277
278
  } catch (err) {
278
279
  const msg = err instanceof Error ? err.message : String(err);
279
- warnings.push(`${path4}: invalid JSON (${msg}) \u2014 file ignored`);
280
- log("warn", "Config file is not valid JSON; ignoring", { path: path4, error: msg });
280
+ warnings.push(`${path5}: invalid JSON (${msg}) \u2014 file ignored`);
281
+ log("warn", "Config file is not valid JSON; ignoring", { path: path5, error: msg });
281
282
  return null;
282
283
  }
283
284
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
284
- warnings.push(`${path4}: root must be a JSON object \u2014 file ignored`);
285
+ warnings.push(`${path5}: root must be a JSON object \u2014 file ignored`);
285
286
  return null;
286
287
  }
287
288
  const obj = parsed;
288
289
  const version = typeof obj.version === "number" ? obj.version : void 0;
289
290
  if (version !== void 0 && version > CURRENT_SCHEMA_VERSION) {
290
291
  warnings.push(
291
- `${path4}: schema version ${version} is newer than this mcph (${CURRENT_SCHEMA_VERSION}); upgrade with \`npm i -g @yawlabs/mcph@latest\`. Loading best-effort.`
292
+ `${path5}: schema version ${version} is newer than this mcph (${CURRENT_SCHEMA_VERSION}); upgrade with \`npm i -g @yawlabs/mcph@latest\`. Loading best-effort.`
292
293
  );
293
294
  }
294
- const token6 = typeof obj.token === "string" && obj.token.length > 0 ? obj.token : void 0;
295
+ const token5 = typeof obj.token === "string" && obj.token.length > 0 ? obj.token : void 0;
295
296
  const apiBase = typeof obj.apiBase === "string" && obj.apiBase.length > 0 ? obj.apiBase : void 0;
296
297
  const servers = Array.isArray(obj.servers) ? obj.servers.filter((v) => typeof v === "string") : void 0;
297
298
  const blocked = Array.isArray(obj.blocked) ? obj.blocked.filter((v) => typeof v === "string") : void 0;
298
- if (token6) {
299
+ if (token5) {
299
300
  if (scope === "project") {
300
301
  warnings.push(
301
- `${path4}: 'token' should not appear in a project-shared file. Move it to ${CONFIG_DIRNAME}/${LOCAL_CONFIG_FILENAME} (gitignored) or ~/${CONFIG_DIRNAME}/${CONFIG_FILENAME}.`
302
+ `${path5}: 'token' should not appear in a project-shared file. Move it to ${CONFIG_DIRNAME}/${LOCAL_CONFIG_FILENAME} (gitignored) or ~/${CONFIG_DIRNAME}/${CONFIG_FILENAME}.`
302
303
  );
303
304
  }
304
- await checkPermissions(path4, warnings);
305
+ await checkPermissions(path5, warnings);
305
306
  }
306
- return { path: path4, scope, version, token: token6, apiBase, servers, blocked };
307
+ return { path: path5, scope, version, token: token5, apiBase, servers, blocked };
307
308
  }
308
- async function checkPermissions(path4, warnings) {
309
+ async function checkPermissions(path5, warnings) {
309
310
  if (process.platform === "win32") return;
310
311
  try {
311
- const st = await stat2(path4);
312
+ const st = await stat2(path5);
312
313
  const mode = st.mode & 511;
313
314
  if ((mode & 63) !== 0) {
314
315
  warnings.push(
315
- `${path4}: contains a token but is readable by group/other (mode ${mode.toString(8)}). Run \`chmod 600 ${path4}\` to restrict.`
316
+ `${path5}: contains a token but is readable by group/other (mode ${mode.toString(8)}). Run \`chmod 600 ${path5}\` to restrict.`
316
317
  );
317
318
  }
318
319
  } catch {
@@ -360,16 +361,16 @@ async function loadMcphConfig(opts = {}) {
360
361
  if (project) loadedFiles.push(project);
361
362
  const global = await readConfigAt(globalPath, "global", warnings);
362
363
  if (global) loadedFiles.push(global);
363
- let token6 = null;
364
+ let token5 = null;
364
365
  let tokenSource = "missing";
365
366
  if (typeof env.MCPH_TOKEN === "string" && env.MCPH_TOKEN.length > 0) {
366
- token6 = env.MCPH_TOKEN;
367
+ token5 = env.MCPH_TOKEN;
367
368
  tokenSource = "env";
368
369
  } else if (local?.token) {
369
- token6 = local.token;
370
+ token5 = local.token;
370
371
  tokenSource = "local";
371
372
  } else if (global?.token) {
372
- token6 = global.token;
373
+ token5 = global.token;
373
374
  tokenSource = "global";
374
375
  }
375
376
  let apiBase = DEFAULT_API_BASE;
@@ -388,7 +389,7 @@ async function loadMcphConfig(opts = {}) {
388
389
  apiBaseSource = "global";
389
390
  }
390
391
  return {
391
- token: token6,
392
+ token: token5,
392
393
  tokenSource,
393
394
  apiBase,
394
395
  apiBaseSource,
@@ -399,10 +400,10 @@ async function loadMcphConfig(opts = {}) {
399
400
  warnings
400
401
  };
401
402
  }
402
- function tokenFingerprint(token6) {
403
- if (!token6) return "(none)";
404
- if (token6.length <= 8) return `***${token6.slice(-2)}`;
405
- return `${token6.slice(0, 8)}\u2026${token6.slice(-4)}`;
403
+ function tokenFingerprint(token5) {
404
+ if (!token5) return "(none)";
405
+ if (token5.length <= 8) return `***${token5.slice(-2)}`;
406
+ return `${token5.slice(0, 8)}\u2026${token5.slice(-4)}`;
406
407
  }
407
408
  function toProfile(config) {
408
409
  if (config.servers === void 0 && config.blocked === void 0) return null;
@@ -441,10 +442,10 @@ function profileAllows(profile, namespace) {
441
442
 
442
443
  // src/config.ts
443
444
  import { request } from "undici";
444
- async function fetchConfig(apiUrl6, token6, currentVersion) {
445
- const url = `${apiUrl6.replace(/\/$/, "")}/api/connect/config`;
445
+ async function fetchConfig(apiUrl5, token5, currentVersion) {
446
+ const url = `${apiUrl5.replace(/\/$/, "")}/api/connect/config`;
446
447
  const headers = {
447
- Authorization: `Bearer ${token6}`,
448
+ Authorization: `Bearer ${token5}`,
448
449
  Accept: "application/json"
449
450
  };
450
451
  if (currentVersion) {
@@ -465,7 +466,7 @@ async function fetchConfig(apiUrl6, token6, currentVersion) {
465
466
  await res.body.text().catch(() => {
466
467
  });
467
468
  throw new ConfigError(
468
- `Token rejected (HTTP 401) \u2014 the token ${tokenFingerprint(token6)} is invalid or revoked.
469
+ `Token rejected (HTTP 401) \u2014 the token ${tokenFingerprint(token5)} is invalid or revoked.
469
470
  Generate a new token at https://mcp.hosting/dashboard/settings/tokens,
470
471
  then re-run \`mcph install <client> --token mcp_pat_...\` or set MCPH_TOKEN.`,
471
472
  true
@@ -475,7 +476,7 @@ async function fetchConfig(apiUrl6, token6, currentVersion) {
475
476
  await res.body.text().catch(() => {
476
477
  });
477
478
  throw new ConfigError(
478
- `Access denied (HTTP 403) \u2014 the token ${tokenFingerprint(token6)} was accepted but lacks permission to read this account's servers.
479
+ `Access denied (HTTP 403) \u2014 the token ${tokenFingerprint(token5)} was accepted but lacks permission to read this account's servers.
479
480
  The account may be suspended or the token scope reduced \u2014 check
480
481
  https://mcp.hosting/dashboard/settings/tokens, or reach support@mcp.hosting.`,
481
482
  true
@@ -899,12 +900,12 @@ async function runComplianceCommand(argv) {
899
900
  );
900
901
  return 1;
901
902
  }
902
- const apiUrl6 = process.env.MCPH_URL ?? "https://mcp.hosting";
903
+ const apiUrl5 = process.env.MCPH_URL ?? "https://mcp.hosting";
903
904
  const report = await runTest(args);
904
905
  if (!report) return 1;
905
906
  printSummary(report);
906
907
  if (publish) {
907
- const result = await publishReport(apiUrl6, report);
908
+ const result = await publishReport(apiUrl5, report);
908
909
  if (!result) return 1;
909
910
  process.stdout.write(`
910
911
  Published: ${result.reportUrl}
@@ -964,9 +965,9 @@ Target: ${url}
964
965
  `
965
966
  );
966
967
  }
967
- async function publishReport(apiUrl6, report) {
968
+ async function publishReport(apiUrl5, report) {
968
969
  try {
969
- const res = await request2(`${apiUrl6.replace(/\/$/, "")}/api/compliance/ext`, {
970
+ const res = await request2(`${apiUrl5.replace(/\/$/, "")}/api/compliance/ext`, {
970
971
  method: "POST",
971
972
  headers: { "Content-Type": "application/json" },
972
973
  body: JSON.stringify(report)
@@ -994,6 +995,124 @@ import { readFile as readFile3 } from "fs/promises";
994
995
  import { homedir as homedir4 } from "os";
995
996
  import { join as join4 } from "path";
996
997
 
998
+ // src/analytics.ts
999
+ import { request as request3 } from "undici";
1000
+ var FLUSH_INTERVAL = 3e4;
1001
+ var FLUSH_SIZE = 50;
1002
+ var MAX_BUFFER = 5e3;
1003
+ var buffer = [];
1004
+ var dispatchBuffer = [];
1005
+ var flushTimer = null;
1006
+ var apiUrl = "";
1007
+ var token = "";
1008
+ var lastFailure = null;
1009
+ function getLastAnalyticsFailure() {
1010
+ return lastFailure;
1011
+ }
1012
+ function recordConnectEvent(event) {
1013
+ if (buffer.length >= MAX_BUFFER) return;
1014
+ buffer.push({ ...event, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
1015
+ if (buffer.length >= FLUSH_SIZE) {
1016
+ flush().catch(() => {
1017
+ });
1018
+ }
1019
+ }
1020
+ function recordDispatchEvent(event) {
1021
+ if (dispatchBuffer.length >= MAX_BUFFER) return;
1022
+ dispatchBuffer.push(event);
1023
+ if (dispatchBuffer.length >= FLUSH_SIZE) {
1024
+ flushDispatch().catch(() => {
1025
+ });
1026
+ }
1027
+ }
1028
+ async function flush() {
1029
+ if (buffer.length === 0 || !apiUrl || !token) return;
1030
+ const events = buffer.splice(0, FLUSH_SIZE);
1031
+ const url = `${apiUrl.replace(/\/$/, "")}/api/connect/analytics`;
1032
+ try {
1033
+ const res = await request3(url, {
1034
+ method: "POST",
1035
+ headers: {
1036
+ Authorization: `Bearer ${token}`,
1037
+ "Content-Type": "application/json"
1038
+ },
1039
+ body: JSON.stringify({ events }),
1040
+ headersTimeout: 1e4,
1041
+ bodyTimeout: 1e4
1042
+ });
1043
+ if (res.statusCode >= 400) {
1044
+ const room = MAX_BUFFER - buffer.length;
1045
+ if (room > 0) buffer.push(...events.slice(0, room));
1046
+ log("warn", "Analytics flush failed", { status: res.statusCode });
1047
+ lastFailure = { statusCode: res.statusCode, url, at: Date.now() };
1048
+ } else {
1049
+ lastFailure = null;
1050
+ }
1051
+ await res.body.text().catch(() => {
1052
+ });
1053
+ } catch (err) {
1054
+ const room = MAX_BUFFER - buffer.length;
1055
+ if (room > 0) buffer.push(...events.slice(0, room));
1056
+ log("warn", "Analytics flush error", { error: err.message });
1057
+ }
1058
+ }
1059
+ async function flushDispatch() {
1060
+ if (dispatchBuffer.length === 0 || !apiUrl || !token) return;
1061
+ const events = dispatchBuffer.splice(0, FLUSH_SIZE);
1062
+ const url = `${apiUrl.replace(/\/$/, "")}/api/connect/dispatch-events`;
1063
+ try {
1064
+ const res = await request3(url, {
1065
+ method: "POST",
1066
+ headers: {
1067
+ Authorization: `Bearer ${token}`,
1068
+ "Content-Type": "application/json"
1069
+ },
1070
+ body: JSON.stringify({ events }),
1071
+ headersTimeout: 1e4,
1072
+ bodyTimeout: 1e4
1073
+ });
1074
+ if (res.statusCode >= 400 && res.statusCode !== 204) {
1075
+ const room = MAX_BUFFER - dispatchBuffer.length;
1076
+ if (room > 0) dispatchBuffer.push(...events.slice(0, room));
1077
+ log("warn", "Dispatch-events flush failed", { status: res.statusCode });
1078
+ lastFailure = { statusCode: res.statusCode, url, at: Date.now() };
1079
+ } else if (res.statusCode < 400) {
1080
+ lastFailure = null;
1081
+ }
1082
+ await res.body.text().catch(() => {
1083
+ });
1084
+ } catch (err) {
1085
+ const room = MAX_BUFFER - dispatchBuffer.length;
1086
+ if (room > 0) dispatchBuffer.push(...events.slice(0, room));
1087
+ log("warn", "Dispatch-events flush error", { error: err.message });
1088
+ }
1089
+ }
1090
+ function initAnalytics(url, tok) {
1091
+ apiUrl = url;
1092
+ token = tok;
1093
+ flushTimer = setInterval(() => {
1094
+ flush().catch(() => {
1095
+ });
1096
+ flushDispatch().catch(() => {
1097
+ });
1098
+ }, FLUSH_INTERVAL);
1099
+ if (flushTimer.unref) flushTimer.unref();
1100
+ }
1101
+ async function shutdownAnalytics() {
1102
+ if (flushTimer) {
1103
+ clearInterval(flushTimer);
1104
+ flushTimer = null;
1105
+ }
1106
+ for (let i = 0; i < 3 && buffer.length > 0; i++) {
1107
+ await flush();
1108
+ }
1109
+ for (let i = 0; i < 3 && dispatchBuffer.length > 0; i++) {
1110
+ await flushDispatch();
1111
+ }
1112
+ buffer.length = 0;
1113
+ dispatchBuffer.length = 0;
1114
+ }
1115
+
997
1116
  // src/cli-shadows.ts
998
1117
  var EMPTY = [];
999
1118
  var NAMESPACE_REGISTRY = {
@@ -1229,7 +1348,7 @@ var INSTALL_TARGETS = [
1229
1348
  function resolveInstallPath(opts) {
1230
1349
  const home = opts.home ?? homedir3();
1231
1350
  const appData = opts.appData ?? process.env.APPDATA ?? join3(home, "AppData", "Roaming");
1232
- const { clientId, scope, os, projectDir } = opts;
1351
+ const { clientId, scope, os, projectDir, claudeConfigDir } = opts;
1233
1352
  const target = INSTALL_TARGETS.find((t) => t.clientId === clientId);
1234
1353
  if (!target) throw new Error(`Unknown client: ${clientId}`);
1235
1354
  const scopeSpec = target.scopes.find((s) => s.scope === scope);
@@ -1240,15 +1359,24 @@ function resolveInstallPath(opts) {
1240
1359
  if (scopeSpec.requiresProjectDir && !projectDir) {
1241
1360
  throw new Error(`Scope ${scope} for ${clientId} requires a project directory`);
1242
1361
  }
1243
- const p = pathFor(clientId, scope, os, { home, appData, projectDir: projectDir ?? "" });
1362
+ const p = pathFor(clientId, scope, os, {
1363
+ home,
1364
+ appData,
1365
+ projectDir: projectDir ?? "",
1366
+ claudeConfigDir: claudeConfigDir && claudeConfigDir.length > 0 ? claudeConfigDir : void 0
1367
+ });
1244
1368
  return p;
1245
1369
  }
1246
1370
  function pathFor(client, scope, os, base) {
1247
- const { home, appData, projectDir } = base;
1371
+ const { home, appData, projectDir, claudeConfigDir } = base;
1248
1372
  const sep = os === "windows" ? "\\" : "/";
1249
1373
  const joinPath = (...parts) => parts.join(sep);
1250
1374
  if (client === "claude-code") {
1251
1375
  if (scope === "user") {
1376
+ if (claudeConfigDir) {
1377
+ const absolute = join3(claudeConfigDir, ".claude.json");
1378
+ return { absolute, display: absolute, containerPath: ["mcpServers"] };
1379
+ }
1252
1380
  const display = os === "windows" ? "%USERPROFILE%\\.claude.json" : "~/.claude.json";
1253
1381
  return { absolute: join3(home, ".claude.json"), display, containerPath: ["mcpServers"] };
1254
1382
  }
@@ -1259,6 +1387,10 @@ function pathFor(client, scope, os, base) {
1259
1387
  containerPath: ["mcpServers"]
1260
1388
  };
1261
1389
  }
1390
+ if (claudeConfigDir) {
1391
+ const absolute = join3(claudeConfigDir, ".claude.json");
1392
+ return { absolute, display: absolute, containerPath: ["projects", projectDir, "mcpServers"] };
1393
+ }
1262
1394
  return {
1263
1395
  absolute: join3(home, ".claude.json"),
1264
1396
  display: os === "windows" ? "%USERPROFILE%\\.claude.json" : "~/.claude.json",
@@ -1311,20 +1443,39 @@ function buildLaunchEntry(opts) {
1311
1443
  var ENTRY_NAME = "mcp.hosting";
1312
1444
  var CLAUDE_CODE_ALLOW_PATTERN = "mcp__mcp_hosting__*";
1313
1445
  function resolveClaudeCodeSettingsPath(scope, opts) {
1314
- const { home, projectDir } = opts;
1315
- if (scope === "user") return join3(home, ".claude", "settings.json");
1446
+ const { home, projectDir, claudeConfigDir } = opts;
1447
+ const cfgDir = claudeConfigDir && claudeConfigDir.length > 0 ? claudeConfigDir : null;
1448
+ if (scope === "user") return cfgDir ? join3(cfgDir, "settings.json") : join3(home, ".claude", "settings.json");
1316
1449
  if (scope === "project" && projectDir) return join3(projectDir, ".claude", "settings.json");
1317
1450
  if (scope === "local" && projectDir) return join3(projectDir, ".claude", "settings.local.json");
1318
1451
  return null;
1319
1452
  }
1320
1453
 
1321
1454
  // src/persistence.ts
1322
- import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile } from "fs/promises";
1455
+ import { readFile as readFile2 } from "fs/promises";
1456
+ import path3 from "path";
1457
+
1458
+ // src/atomic-write.ts
1459
+ import { mkdir as mkdir2, rename as rename2, unlink, writeFile } from "fs/promises";
1323
1460
  import path2 from "path";
1461
+ async function atomicWriteFile(filePath, contents, encoding = "utf8") {
1462
+ const dir = path2.dirname(filePath);
1463
+ const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
1464
+ await mkdir2(dir, { recursive: true });
1465
+ try {
1466
+ await writeFile(tmp, contents, encoding);
1467
+ await rename2(tmp, filePath);
1468
+ } catch (err) {
1469
+ await unlink(tmp).catch(() => void 0);
1470
+ throw err;
1471
+ }
1472
+ }
1473
+
1474
+ // src/persistence.ts
1324
1475
  var STATE_SCHEMA_VERSION = 1;
1325
1476
  var STATE_FILENAME = "state.json";
1326
1477
  function statePath(configDir = userConfigDir()) {
1327
- return path2.join(configDir, STATE_FILENAME);
1478
+ return path3.join(configDir, STATE_FILENAME);
1328
1479
  }
1329
1480
  function emptyState() {
1330
1481
  return { version: STATE_SCHEMA_VERSION, savedAt: 0, learning: {}, packHistory: [] };
@@ -1348,19 +1499,21 @@ async function loadState(filePath = statePath()) {
1348
1499
  return emptyState();
1349
1500
  }
1350
1501
  }
1351
- async function saveState(state, filePath = statePath()) {
1502
+ var saveChain = Promise.resolve();
1503
+ function saveState(state, filePath = statePath()) {
1504
+ const next = saveChain.then(() => doSaveState(state, filePath));
1505
+ saveChain = next.catch(() => void 0);
1506
+ return next;
1507
+ }
1508
+ async function doSaveState(state, filePath) {
1352
1509
  const payload = {
1353
1510
  version: STATE_SCHEMA_VERSION,
1354
1511
  savedAt: Date.now(),
1355
1512
  learning: state.learning,
1356
1513
  packHistory: state.packHistory
1357
1514
  };
1358
- const dir = path2.dirname(filePath);
1359
- const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
1360
1515
  try {
1361
- await mkdir2(dir, { recursive: true });
1362
- await writeFile(tmp, JSON.stringify(payload, null, 2), "utf8");
1363
- await rename2(tmp, filePath);
1516
+ await atomicWriteFile(filePath, JSON.stringify(payload, null, 2));
1364
1517
  } catch (err) {
1365
1518
  log("warn", "Failed to save mcph state", { error: errorMessage(err) });
1366
1519
  }
@@ -1399,6 +1552,45 @@ function errorMessage(err) {
1399
1552
  return err instanceof Error ? err.message : String(err);
1400
1553
  }
1401
1554
 
1555
+ // src/tool-report.ts
1556
+ import { request as request4 } from "undici";
1557
+ var apiUrl2 = "";
1558
+ var token2 = "";
1559
+ var lastFailure2 = null;
1560
+ function getLastReportFailure() {
1561
+ return lastFailure2;
1562
+ }
1563
+ function initToolReport(url, tok) {
1564
+ apiUrl2 = url;
1565
+ token2 = tok;
1566
+ }
1567
+ async function reportTools(serverId, tools) {
1568
+ if (!apiUrl2 || !token2 || !serverId) return;
1569
+ const url = `${apiUrl2.replace(/\/$/, "")}/api/connect/servers/${serverId}/tools`;
1570
+ try {
1571
+ const res = await request4(url, {
1572
+ method: "POST",
1573
+ headers: {
1574
+ Authorization: `Bearer ${token2}`,
1575
+ "Content-Type": "application/json"
1576
+ },
1577
+ body: JSON.stringify({ tools }),
1578
+ headersTimeout: 1e4,
1579
+ bodyTimeout: 1e4
1580
+ });
1581
+ await res.body.text().catch(() => {
1582
+ });
1583
+ if (res.statusCode >= 400 && res.statusCode !== 404) {
1584
+ log("warn", "Tool report failed", { serverId, status: res.statusCode });
1585
+ lastFailure2 = { statusCode: res.statusCode, url, at: Date.now() };
1586
+ } else if (res.statusCode < 400) {
1587
+ lastFailure2 = null;
1588
+ }
1589
+ } catch (err) {
1590
+ log("warn", "Tool report error", { serverId, error: err?.message });
1591
+ }
1592
+ }
1593
+
1402
1594
  // src/usage-hints.ts
1403
1595
  var MAX_PEERS = 3;
1404
1596
  var MIN_SUCCESS_TO_SHOW = 1;
@@ -1458,7 +1650,7 @@ function selectFlakyNamespaces(entries, limit) {
1458
1650
  }
1459
1651
 
1460
1652
  // src/doctor-cmd.ts
1461
- var VERSION = true ? "0.47.1" : "dev";
1653
+ var VERSION = true ? "0.47.3" : "dev";
1462
1654
  async function runDoctor(opts = {}) {
1463
1655
  if (opts.json) return runDoctorJson(opts);
1464
1656
  const lines = [];
@@ -1497,7 +1689,9 @@ async function runDoctor(opts = {}) {
1497
1689
  renderEnvSection({ env, print });
1498
1690
  await renderStateSection({ home, env, print });
1499
1691
  await renderReliabilitySection({ home, env, print });
1500
- const clients = probeClients({ home, os, cwd });
1692
+ renderBackgroundPostersSection({ print });
1693
+ const claudeConfigDir = env.CLAUDE_CONFIG_DIR && env.CLAUDE_CONFIG_DIR.length > 0 ? env.CLAUDE_CONFIG_DIR : void 0;
1694
+ const clients = probeClients({ home, os, cwd, claudeConfigDir });
1501
1695
  print("INSTALLED CLIENTS (probed config files)");
1502
1696
  for (const c of clients) {
1503
1697
  const status = c.unavailable ? "unavailable on this OS" : c.malformed ? "exists but JSON is malformed \u2014 fix or rerun `mcph install`" : c.hasMcphEntry ? `OK \u2014 has "${ENTRY_NAME}" entry` : c.exists ? `present, no "${ENTRY_NAME}" entry \u2014 run \`mcph install ${c.clientId}${c.scope === "user" ? "" : ` --scope ${c.scope}`}\`` : `not configured \u2014 run \`mcph install ${c.clientId}${c.scope === "user" ? "" : ` --scope ${c.scope}`}\``;
@@ -1557,7 +1751,8 @@ async function runDoctorJson(opts) {
1557
1751
  const env = opts.env ?? process.env;
1558
1752
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1559
1753
  const config = await loadMcphConfig({ cwd, home, env });
1560
- const clients = probeClients({ home, os, cwd });
1754
+ const claudeConfigDir = env.CLAUDE_CONFIG_DIR && env.CLAUDE_CONFIG_DIR.length > 0 ? env.CLAUDE_CONFIG_DIR : void 0;
1755
+ const clients = probeClients({ home, os, cwd, claudeConfigDir });
1561
1756
  const envVarNames = [
1562
1757
  "MCPH_POLL_INTERVAL",
1563
1758
  "MCPH_SERVER_CAP",
@@ -1675,6 +1870,25 @@ async function renderStateSection(opts) {
1675
1870
  }
1676
1871
  const filePath = join4(userConfigDir(home), STATE_FILENAME);
1677
1872
  print(` path: ${filePath}`);
1873
+ const peek = await peekStateFile(filePath);
1874
+ if (peek.kind === "malformed") {
1875
+ print(" status: corrupt -- file exists but JSON is unparseable");
1876
+ print(` fix: \`mcph reset-learning\` to clear, or open ${filePath} and fix by hand`);
1877
+ print(` detail: ${peek.message}`);
1878
+ print("");
1879
+ return;
1880
+ }
1881
+ if (peek.kind === "stale-version") {
1882
+ print(` status: schema mismatch (file is v${peek.version ?? "?"}, this mcph reads v${peek.expected})`);
1883
+ print(" fix: `mcph reset-learning` to drop the old file -- learning will rebuild on use");
1884
+ print("");
1885
+ return;
1886
+ }
1887
+ if (peek.kind === "unreadable") {
1888
+ print(` status: unreadable (${peek.message})`);
1889
+ print("");
1890
+ return;
1891
+ }
1678
1892
  const persisted = await loadState(filePath);
1679
1893
  if (persisted.savedAt === 0) {
1680
1894
  print(" (no persisted state yet \u2014 will be created on the first tool call)");
@@ -1685,6 +1899,29 @@ async function renderStateSection(opts) {
1685
1899
  }
1686
1900
  print("");
1687
1901
  }
1902
+ async function peekStateFile(filePath) {
1903
+ let raw;
1904
+ try {
1905
+ raw = await readFile3(filePath, "utf8");
1906
+ } catch (err) {
1907
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
1908
+ return { kind: "missing" };
1909
+ }
1910
+ return { kind: "unreadable", message: err instanceof Error ? err.message : String(err) };
1911
+ }
1912
+ let parsed;
1913
+ try {
1914
+ parsed = JSON.parse(raw);
1915
+ } catch (err) {
1916
+ return { kind: "malformed", message: err instanceof Error ? err.message : String(err) };
1917
+ }
1918
+ if (!parsed || typeof parsed !== "object") return { kind: "malformed", message: "top-level value is not an object" };
1919
+ const version = parsed.version;
1920
+ if (version !== STATE_SCHEMA_VERSION) {
1921
+ return { kind: "stale-version", version, expected: STATE_SCHEMA_VERSION };
1922
+ }
1923
+ return { kind: "ok" };
1924
+ }
1688
1925
  async function renderReliabilitySection(opts) {
1689
1926
  const { home, env, print } = opts;
1690
1927
  const raw = env.MCPH_DISABLE_PERSISTENCE;
@@ -1705,6 +1942,18 @@ async function renderReliabilitySection(opts) {
1705
1942
  }
1706
1943
  print("");
1707
1944
  }
1945
+ function renderBackgroundPostersSection(opts) {
1946
+ const { print } = opts;
1947
+ const analyticsFailure = getLastAnalyticsFailure();
1948
+ const reportFailure = getLastReportFailure();
1949
+ if (!analyticsFailure && !reportFailure) return;
1950
+ const now = Date.now();
1951
+ const fmt = (f) => `HTTP ${f.statusCode} from ${f.url}, ${formatRelativeAge(now - f.at)} ago`;
1952
+ print("BACKGROUND POSTERS (recent failures)");
1953
+ print(` analytics: ${analyticsFailure ? fmt(analyticsFailure) : "(no recent failure)"}`);
1954
+ print(` tool-report: ${reportFailure ? fmt(reportFailure) : "(no recent failure)"}`);
1955
+ print("");
1956
+ }
1708
1957
  function formatRelativeAge(ms) {
1709
1958
  const clamped = Math.max(0, ms);
1710
1959
  const s = Math.floor(clamped / 1e3);
@@ -1746,7 +1995,8 @@ function probeClients(opts) {
1746
1995
  scope: scope.scope,
1747
1996
  os: opts.os,
1748
1997
  home: opts.home,
1749
- projectDir: scope.requiresProjectDir ? opts.cwd : void 0
1998
+ projectDir: scope.requiresProjectDir ? opts.cwd : void 0,
1999
+ claudeConfigDir: opts.claudeConfigDir
1750
2000
  });
1751
2001
  } catch {
1752
2002
  continue;
@@ -1784,9 +2034,9 @@ function probeClients(opts) {
1784
2034
  }
1785
2035
  return out;
1786
2036
  }
1787
- function walkContainer(root, path4) {
2037
+ function walkContainer(root, path5) {
1788
2038
  let cur = root;
1789
- for (const key of path4) {
2039
+ for (const key of path5) {
1790
2040
  if (typeof cur !== "object" || cur === null || Array.isArray(cur)) return null;
1791
2041
  cur = cur[key];
1792
2042
  }
@@ -1815,7 +2065,8 @@ async function probeClientsAsync(opts) {
1815
2065
  scope: scope.scope,
1816
2066
  os: opts.os,
1817
2067
  home: opts.home,
1818
- projectDir: scope.requiresProjectDir ? opts.cwd : void 0
2068
+ projectDir: scope.requiresProjectDir ? opts.cwd : void 0,
2069
+ claudeConfigDir: opts.claudeConfigDir
1819
2070
  });
1820
2071
  const exists3 = existsSync(resolved.absolute);
1821
2072
  let hasMcphEntry = false;
@@ -1922,9 +2173,9 @@ function shellHistorySources(opts) {
1922
2173
  }
1923
2174
  return sources;
1924
2175
  }
1925
- function readTailLines(path4, n) {
2176
+ function readTailLines(path5, n) {
1926
2177
  try {
1927
- const raw = readFileSync(path4, "utf8");
2178
+ const raw = readFileSync(path5, "utf8");
1928
2179
  const all = raw.split(/\r?\n/);
1929
2180
  return all.length <= n ? all : all.slice(all.length - n);
1930
2181
  } catch {
@@ -2025,9 +2276,8 @@ function closestNames(query, candidates, limit) {
2025
2276
 
2026
2277
  // src/install-cmd.ts
2027
2278
  import { existsSync as existsSync2 } from "fs";
2028
- import { chmod, mkdir as mkdir3, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
2279
+ import { chmod, readFile as readFile4 } from "fs/promises";
2029
2280
  import { homedir as homedir5 } from "os";
2030
- import { dirname as dirname2 } from "path";
2031
2281
  import { join as join5, resolve as resolve2 } from "path";
2032
2282
  import { createInterface } from "readline/promises";
2033
2283
  var USAGE = "Usage: mcph 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-mcph-config]\n mcph install --list (detect clients; no writes)\n mcph install --all [--token <mcp_pat_\u2026>] (install into every detected client)";
@@ -2089,7 +2339,8 @@ ${USAGE}`);
2089
2339
  scope,
2090
2340
  os,
2091
2341
  home: opts.home,
2092
- projectDir
2342
+ projectDir,
2343
+ claudeConfigDir: opts.claudeConfigDir
2093
2344
  });
2094
2345
  } catch (e) {
2095
2346
  err(`mcph install: ${e.message}`);
@@ -2097,12 +2348,12 @@ ${USAGE}`);
2097
2348
  }
2098
2349
  log2(`Target: ${target.label} (${scope})`);
2099
2350
  log2(`File: ${resolved.absolute}`);
2100
- let token6 = opts.token ?? null;
2101
- if (!token6) {
2351
+ let token5 = opts.token ?? null;
2352
+ if (!token5) {
2102
2353
  const cfg = await loadMcphConfig({ home: opts.home, cwd: process.cwd(), env: {} });
2103
- token6 = cfg.token;
2354
+ token5 = cfg.token;
2104
2355
  }
2105
- if (!token6) {
2356
+ if (!token5) {
2106
2357
  err(
2107
2358
  "\nmcph install: no token available.\n Pass one with --token mcp_pat_\u2026, or run `mcph install` with --token once to seed ~/.mcph/config.json,\n or create the token at https://mcp.hosting \u2192 Settings \u2192 API Tokens."
2108
2359
  );
@@ -2172,40 +2423,39 @@ ${USAGE}`);
2172
2423
  const writeMcphConfig = !opts.skipMcphConfig;
2173
2424
  const home = opts.home ?? homedir5();
2174
2425
  const mcphConfigPath = join5(home, CONFIG_DIRNAME, CONFIG_FILENAME);
2175
- const mcphConfigJson = await composeMcphConfig(mcphConfigPath, token6);
2426
+ const mcphConfigComposed = await composeMcphConfig(mcphConfigPath, token5);
2427
+ if (mcphConfigComposed.backupPath) {
2428
+ log2(
2429
+ `mcph install: existing ${mcphConfigPath} was malformed; original bytes backed up to ${mcphConfigComposed.backupPath} before overwriting.`
2430
+ );
2431
+ }
2432
+ const mcphConfigJson = mcphConfigComposed.json;
2176
2433
  const settingsPatch = opts.clientId === "claude-code" ? await prepareClaudeCodeSettingsPatch({
2177
2434
  scope,
2178
2435
  home,
2179
2436
  projectDir,
2180
- os
2437
+ os,
2438
+ claudeConfigDir: opts.claudeConfigDir
2181
2439
  }) : null;
2182
2440
  if (opts.dryRun) {
2183
2441
  log2("\n--- dry run: would write the following ---");
2442
+ if (writeMcphConfig) log2(`# ${mcphConfigPath}
2443
+ ${mcphConfigJson}`);
2184
2444
  log2(`
2185
2445
  # ${resolved.absolute}
2186
2446
  ${clientJson}`);
2187
- if (writeMcphConfig) log2(`# ${mcphConfigPath}
2188
- ${mcphConfigJson}`);
2189
2447
  if (settingsPatch?.changed) log2(`# ${settingsPatch.path}
2190
2448
  ${settingsPatch.nextJson}`);
2191
- const wouldWrite = [resolved.absolute];
2449
+ const wouldWrite = [];
2192
2450
  if (writeMcphConfig) wouldWrite.push(mcphConfigPath);
2451
+ wouldWrite.push(resolved.absolute);
2193
2452
  if (settingsPatch?.changed) wouldWrite.push(settingsPatch.path);
2194
2453
  return { written: [], wouldWrite, messages, exitCode: 0 };
2195
2454
  }
2196
- try {
2197
- await mkdir3(dirname2(resolved.absolute), { recursive: true });
2198
- await writeFile2(resolved.absolute, clientJson, "utf8");
2199
- } catch (e) {
2200
- err(`mcph install: failed to write ${resolved.absolute}: ${e.message}`);
2201
- return { written: [], wouldWrite: [], messages, exitCode: 1 };
2202
- }
2203
- log2(`Wrote ${resolved.absolute}`);
2204
- const written = [resolved.absolute];
2455
+ const written = [];
2205
2456
  if (writeMcphConfig) {
2206
2457
  try {
2207
- await mkdir3(dirname2(mcphConfigPath), { recursive: true });
2208
- await writeFile2(mcphConfigPath, mcphConfigJson, "utf8");
2458
+ await atomicWriteFile(mcphConfigPath, mcphConfigJson);
2209
2459
  if (process.platform !== "win32") {
2210
2460
  try {
2211
2461
  await chmod(mcphConfigPath, 384);
@@ -2214,15 +2464,22 @@ ${settingsPatch.nextJson}`);
2214
2464
  }
2215
2465
  } catch (e) {
2216
2466
  err(`mcph install: failed to write ${mcphConfigPath}: ${e.message}`);
2217
- return { written, wouldWrite: [], messages, exitCode: 1 };
2467
+ return { written: [], wouldWrite: [], messages, exitCode: 1 };
2218
2468
  }
2219
2469
  log2(`Wrote ${mcphConfigPath}`);
2220
2470
  written.push(mcphConfigPath);
2221
2471
  }
2472
+ try {
2473
+ await atomicWriteFile(resolved.absolute, clientJson);
2474
+ } catch (e) {
2475
+ err(`mcph install: failed to write ${resolved.absolute}: ${e.message}`);
2476
+ return { written, wouldWrite: [], messages, exitCode: 1 };
2477
+ }
2478
+ log2(`Wrote ${resolved.absolute}`);
2479
+ written.push(resolved.absolute);
2222
2480
  if (settingsPatch?.changed) {
2223
2481
  try {
2224
- await mkdir3(dirname2(settingsPatch.path), { recursive: true });
2225
- await writeFile2(settingsPatch.path, settingsPatch.nextJson, "utf8");
2482
+ await atomicWriteFile(settingsPatch.path, settingsPatch.nextJson);
2226
2483
  log2(`Wrote ${settingsPatch.path} (added ${CLAUDE_CODE_ALLOW_PATTERN} to permissions.allow)`);
2227
2484
  written.push(settingsPatch.path);
2228
2485
  } catch (e) {
@@ -2237,33 +2494,34 @@ ${settingsPatch.nextJson}`);
2237
2494
  return { written, wouldWrite: [], messages, exitCode: 0 };
2238
2495
  }
2239
2496
  async function prepareClaudeCodeSettingsPatch(opts) {
2240
- const path4 = resolveClaudeCodeSettingsPath(opts.scope, {
2497
+ const path5 = resolveClaudeCodeSettingsPath(opts.scope, {
2241
2498
  home: opts.home,
2242
2499
  projectDir: opts.projectDir,
2243
- os: opts.os
2500
+ os: opts.os,
2501
+ claudeConfigDir: opts.claudeConfigDir
2244
2502
  });
2245
- if (!path4) return null;
2503
+ if (!path5) return null;
2246
2504
  let existing = {};
2247
- if (existsSync2(path4)) {
2505
+ if (existsSync2(path5)) {
2248
2506
  try {
2249
- const raw = await readFile4(path4, "utf8");
2507
+ const raw = await readFile4(path5, "utf8");
2250
2508
  if (raw.trim().length > 0) {
2251
2509
  const parsed = parseJsonc(raw);
2252
2510
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
2253
2511
  existing = parsed;
2254
2512
  } else {
2255
- return { path: path4, nextJson: "", changed: false };
2513
+ return { path: path5, nextJson: "", changed: false };
2256
2514
  }
2257
2515
  }
2258
2516
  } catch {
2259
- return { path: path4, nextJson: "", changed: false };
2517
+ return { path: path5, nextJson: "", changed: false };
2260
2518
  }
2261
2519
  }
2262
2520
  const merged = mergePermissionsAllow(existing, [CLAUDE_CODE_ALLOW_PATTERN]);
2263
2521
  const before = JSON.stringify(existing);
2264
2522
  const after = JSON.stringify(merged);
2265
- if (before === after) return { path: path4, nextJson: "", changed: false };
2266
- return { path: path4, nextJson: `${JSON.stringify(merged, null, 2)}
2523
+ if (before === after) return { path: path5, nextJson: "", changed: false };
2524
+ return { path: path5, nextJson: `${JSON.stringify(merged, null, 2)}
2267
2525
  `, changed: true };
2268
2526
  }
2269
2527
  function mergePermissionsAllow(existing, patterns) {
@@ -2279,13 +2537,13 @@ function mergePermissionsAllow(existing, patterns) {
2279
2537
  out.permissions = perms;
2280
2538
  return out;
2281
2539
  }
2282
- async function promptCollision(path4, io) {
2540
+ async function promptCollision(path5, io) {
2283
2541
  const stdin = io?.stdin ?? process.stdin;
2284
2542
  const stdout = io?.stdout ?? process.stdout;
2285
2543
  const rl = createInterface({ input: stdin, output: stdout });
2286
2544
  try {
2287
2545
  const answer = (await rl.question(
2288
- `${path4} already has an "${ENTRY_NAME}" entry.
2546
+ `${path5} already has an "${ENTRY_NAME}" entry.
2289
2547
  [o]verwrite, [s]kip, or [a]bort? (default: skip) `
2290
2548
  )).trim().toLowerCase();
2291
2549
  if (answer.startsWith("o")) return "overwrite";
@@ -2321,23 +2579,37 @@ function mergeClientConfig(existing, containerPath, entry) {
2321
2579
  parent[leafKey] = container;
2322
2580
  return out;
2323
2581
  }
2324
- async function composeMcphConfig(path4, token6) {
2582
+ async function composeMcphConfig(path5, token5) {
2325
2583
  let existing = {};
2326
- if (existsSync2(path4)) {
2584
+ let backupPath;
2585
+ if (existsSync2(path5)) {
2586
+ let raw = "";
2327
2587
  try {
2328
- const raw = await readFile4(path4, "utf8");
2329
- const parsed = parseJsonc(raw);
2330
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
2331
- existing = parsed;
2332
- }
2588
+ raw = await readFile4(path5, "utf8");
2333
2589
  } catch {
2590
+ raw = "";
2591
+ }
2592
+ if (raw) {
2593
+ try {
2594
+ const parsed = parseJsonc(raw);
2595
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
2596
+ existing = parsed;
2597
+ }
2598
+ } catch {
2599
+ const candidate = `${path5}.bak-${Date.now()}`;
2600
+ try {
2601
+ await atomicWriteFile(candidate, raw);
2602
+ backupPath = candidate;
2603
+ } catch {
2604
+ }
2605
+ }
2334
2606
  }
2335
2607
  }
2336
2608
  const next = { version: CURRENT_SCHEMA_VERSION, ...existing };
2337
- next.token = token6;
2609
+ next.token = token5;
2338
2610
  if (typeof next.version !== "number") next.version = CURRENT_SCHEMA_VERSION;
2339
- return `${JSON.stringify(next, null, 2)}
2340
- `;
2611
+ return { json: `${JSON.stringify(next, null, 2)}
2612
+ `, backupPath };
2341
2613
  }
2342
2614
  function parseInstallArgs(argv) {
2343
2615
  if (argv.length === 0) return { ok: false, error: USAGE };
@@ -2427,7 +2699,7 @@ async function runInstallList(opts, log2) {
2427
2699
  const home = opts.home ?? homedir5();
2428
2700
  const cwd = opts.cwd ?? process.cwd();
2429
2701
  const os = opts.os ?? CURRENT_OS;
2430
- const probes = await probeClientsAsync({ home, os, cwd });
2702
+ const probes = await probeClientsAsync({ home, os, cwd, claudeConfigDir: opts.claudeConfigDir });
2431
2703
  const rows = probes.map((p) => ({
2432
2704
  client: INSTALL_TARGETS.find((t) => t.clientId === p.clientId)?.label ?? p.clientId,
2433
2705
  scope: p.scope,
@@ -2546,9 +2818,29 @@ async function runInstallAll(opts, log2, err) {
2546
2818
  }
2547
2819
 
2548
2820
  // src/reset-learning-cmd.ts
2549
- import { unlink } from "fs/promises";
2821
+ import { unlink as unlink2 } from "fs/promises";
2550
2822
  import { homedir as homedir6 } from "os";
2551
2823
  import { join as join6 } from "path";
2824
+ var RESET_LEARNING_USAGE = `Usage: mcph reset-learning
2825
+
2826
+ Delete ~/.mcph/state.json so cross-session learning starts fresh.
2827
+ Use this after fixing the root cause of a flaky upstream (token
2828
+ rotated, account swapped, server replaced) so the routing penalty
2829
+ doesn't keep suppressing it.
2830
+
2831
+ -h, --help Show this help.`;
2832
+ function parseResetLearningArgs(argv) {
2833
+ for (const arg of argv) {
2834
+ if (arg === "-h" || arg === "--help") return { kind: "help" };
2835
+ return {
2836
+ kind: "error",
2837
+ error: `mcph reset-learning: unknown argument "${arg}"
2838
+
2839
+ ${RESET_LEARNING_USAGE}`
2840
+ };
2841
+ }
2842
+ return { kind: "ok", options: {} };
2843
+ }
2552
2844
  async function runResetLearning(opts = {}) {
2553
2845
  const home = opts.home ?? homedir6();
2554
2846
  const env = opts.env ?? process.env;
@@ -2576,7 +2868,7 @@ async function runResetLearning(opts = {}) {
2576
2868
  const learningCount = Object.keys(persisted.learning).length;
2577
2869
  const packCount = persisted.packHistory.length;
2578
2870
  try {
2579
- await unlink(filePath);
2871
+ await unlink2(filePath);
2580
2872
  } catch (err) {
2581
2873
  if (isFileNotFound2(err)) {
2582
2874
  print("mcph reset-learning: no persisted state to reset.");
@@ -2611,113 +2903,7 @@ import {
2611
2903
  ListToolsRequestSchema,
2612
2904
  ReadResourceRequestSchema
2613
2905
  } from "@modelcontextprotocol/sdk/types.js";
2614
- import { request as request9 } from "undici";
2615
-
2616
- // src/analytics.ts
2617
- import { request as request3 } from "undici";
2618
- var FLUSH_INTERVAL = 3e4;
2619
- var FLUSH_SIZE = 50;
2620
- var MAX_BUFFER = 5e3;
2621
- var buffer = [];
2622
- var dispatchBuffer = [];
2623
- var flushTimer = null;
2624
- var apiUrl = "";
2625
- var token = "";
2626
- function recordConnectEvent(event) {
2627
- if (buffer.length >= MAX_BUFFER) return;
2628
- buffer.push({ ...event, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
2629
- if (buffer.length >= FLUSH_SIZE) {
2630
- flush().catch(() => {
2631
- });
2632
- }
2633
- }
2634
- function recordDispatchEvent(event) {
2635
- if (dispatchBuffer.length >= MAX_BUFFER) return;
2636
- dispatchBuffer.push(event);
2637
- if (dispatchBuffer.length >= FLUSH_SIZE) {
2638
- flushDispatch().catch(() => {
2639
- });
2640
- }
2641
- }
2642
- async function flush() {
2643
- if (buffer.length === 0 || !apiUrl || !token) return;
2644
- const events = buffer.splice(0, FLUSH_SIZE);
2645
- try {
2646
- const res = await request3(`${apiUrl.replace(/\/$/, "")}/api/connect/analytics`, {
2647
- method: "POST",
2648
- headers: {
2649
- Authorization: `Bearer ${token}`,
2650
- "Content-Type": "application/json"
2651
- },
2652
- body: JSON.stringify({ events }),
2653
- headersTimeout: 1e4,
2654
- bodyTimeout: 1e4
2655
- });
2656
- if (res.statusCode >= 400) {
2657
- const room = MAX_BUFFER - buffer.length;
2658
- if (room > 0) buffer.push(...events.slice(0, room));
2659
- log("warn", "Analytics flush failed", { status: res.statusCode });
2660
- }
2661
- await res.body.text().catch(() => {
2662
- });
2663
- } catch (err) {
2664
- const room = MAX_BUFFER - buffer.length;
2665
- if (room > 0) buffer.push(...events.slice(0, room));
2666
- log("warn", "Analytics flush error", { error: err.message });
2667
- }
2668
- }
2669
- async function flushDispatch() {
2670
- if (dispatchBuffer.length === 0 || !apiUrl || !token) return;
2671
- const events = dispatchBuffer.splice(0, FLUSH_SIZE);
2672
- try {
2673
- const res = await request3(`${apiUrl.replace(/\/$/, "")}/api/connect/dispatch-events`, {
2674
- method: "POST",
2675
- headers: {
2676
- Authorization: `Bearer ${token}`,
2677
- "Content-Type": "application/json"
2678
- },
2679
- body: JSON.stringify({ events }),
2680
- headersTimeout: 1e4,
2681
- bodyTimeout: 1e4
2682
- });
2683
- if (res.statusCode >= 400 && res.statusCode !== 204) {
2684
- const room = MAX_BUFFER - dispatchBuffer.length;
2685
- if (room > 0) dispatchBuffer.push(...events.slice(0, room));
2686
- log("warn", "Dispatch-events flush failed", { status: res.statusCode });
2687
- }
2688
- await res.body.text().catch(() => {
2689
- });
2690
- } catch (err) {
2691
- const room = MAX_BUFFER - dispatchBuffer.length;
2692
- if (room > 0) dispatchBuffer.push(...events.slice(0, room));
2693
- log("warn", "Dispatch-events flush error", { error: err.message });
2694
- }
2695
- }
2696
- function initAnalytics(url, tok) {
2697
- apiUrl = url;
2698
- token = tok;
2699
- flushTimer = setInterval(() => {
2700
- flush().catch(() => {
2701
- });
2702
- flushDispatch().catch(() => {
2703
- });
2704
- }, FLUSH_INTERVAL);
2705
- if (flushTimer.unref) flushTimer.unref();
2706
- }
2707
- async function shutdownAnalytics() {
2708
- if (flushTimer) {
2709
- clearInterval(flushTimer);
2710
- flushTimer = null;
2711
- }
2712
- for (let i = 0; i < 3 && buffer.length > 0; i++) {
2713
- await flush();
2714
- }
2715
- for (let i = 0; i < 3 && dispatchBuffer.length > 0; i++) {
2716
- await flushDispatch();
2717
- }
2718
- buffer.length = 0;
2719
- dispatchBuffer.length = 0;
2720
- }
2906
+ import { request as request8 } from "undici";
2721
2907
 
2722
2908
  // src/compliance.ts
2723
2909
  var GRADE_ORDER = {
@@ -2727,13 +2913,20 @@ var GRADE_ORDER = {
2727
2913
  D: 1,
2728
2914
  F: 0
2729
2915
  };
2916
+ function classifyGrade(grade) {
2917
+ if (grade === void 0 || grade === null) return { kind: "ungraded" };
2918
+ const trimmed = grade.trim();
2919
+ if (trimmed === "") return { kind: "ungraded" };
2920
+ const up = trimmed.toUpperCase();
2921
+ if (up in GRADE_ORDER) return { kind: "graded", rank: GRADE_ORDER[up] };
2922
+ return { kind: "unrecognized", raw: grade };
2923
+ }
2730
2924
  function gradeRank(grade) {
2731
- if (!grade) return -1;
2732
- const up = grade.toUpperCase();
2733
- if (up in GRADE_ORDER) return GRADE_ORDER[up];
2734
- return -1;
2925
+ const c = classifyGrade(grade);
2926
+ return c.kind === "graded" ? c.rank : -1;
2735
2927
  }
2736
2928
  var invalidWarned = false;
2929
+ var unrecognizedServerWarned = /* @__PURE__ */ new Set();
2737
2930
  function parseMinCompliance(raw) {
2738
2931
  if (raw === void 0) return null;
2739
2932
  const trimmed = raw.trim();
@@ -2750,9 +2943,19 @@ function parseMinCompliance(raw) {
2750
2943
  }
2751
2944
  function passesMinCompliance(serverGrade, min) {
2752
2945
  if (min === null) return true;
2753
- const serverRank = gradeRank(serverGrade);
2754
- if (serverRank < 0) return true;
2755
- return serverRank >= gradeRank(min);
2946
+ const c = classifyGrade(serverGrade);
2947
+ if (c.kind === "ungraded") return true;
2948
+ if (c.kind === "unrecognized") {
2949
+ if (!unrecognizedServerWarned.has(c.raw)) {
2950
+ unrecognizedServerWarned.add(c.raw);
2951
+ log("warn", "Unrecognized server compliance grade; failing closed under MCPH_MIN_COMPLIANCE", {
2952
+ grade: c.raw,
2953
+ min
2954
+ });
2955
+ }
2956
+ return false;
2957
+ }
2958
+ return c.rank >= gradeRank(min);
2756
2959
  }
2757
2960
 
2758
2961
  // src/cost-estimate.ts
@@ -3023,18 +3226,18 @@ function stepBindingKey(step, index) {
3023
3226
  // src/guide.ts
3024
3227
  import { readFile as readFile5 } from "fs/promises";
3025
3228
  var GUIDE_READ_TIMEOUT_MS = 1e3;
3026
- async function readGuide(path4, scope) {
3229
+ async function readGuide(path5, scope) {
3027
3230
  let raw;
3028
3231
  try {
3029
3232
  raw = await Promise.race([
3030
- readFile5(path4, "utf8"),
3233
+ readFile5(path5, "utf8"),
3031
3234
  new Promise(
3032
3235
  (_, reject) => setTimeout(() => reject(new Error("guide read timeout")), GUIDE_READ_TIMEOUT_MS)
3033
3236
  )
3034
3237
  ]);
3035
3238
  } catch (err) {
3036
3239
  if (err instanceof Error && err.message === "guide read timeout") {
3037
- log("warn", "Guide read timed out", { path: path4 });
3240
+ log("warn", "Guide read timed out", { path: path5 });
3038
3241
  }
3039
3242
  return null;
3040
3243
  }
@@ -3042,7 +3245,7 @@ async function readGuide(path4, scope) {
3042
3245
  if (content.length === 0) {
3043
3246
  return null;
3044
3247
  }
3045
- return { scope, path: path4, content };
3248
+ return { scope, path: path5, content };
3046
3249
  }
3047
3250
  async function loadUserGuide(home) {
3048
3251
  const p = guidePath(userConfigDir(home));
@@ -3741,9 +3944,9 @@ var PackDetector = class {
3741
3944
 
3742
3945
  // src/progress.ts
3743
3946
  function createProgressReporter(extra) {
3744
- const token6 = extra?._meta?.progressToken;
3947
+ const token5 = extra?._meta?.progressToken;
3745
3948
  const send = extra?.sendNotification;
3746
- if (token6 === void 0 || token6 === null || !send) {
3949
+ if (token5 === void 0 || token5 === null || !send) {
3747
3950
  return () => {
3748
3951
  };
3749
3952
  }
@@ -3751,7 +3954,7 @@ function createProgressReporter(extra) {
3751
3954
  return (message, progress, total) => {
3752
3955
  step += 1;
3753
3956
  const params = {
3754
- progressToken: token6,
3957
+ progressToken: token5,
3755
3958
  progress: progress ?? step,
3756
3959
  message
3757
3960
  };
@@ -3822,6 +4025,14 @@ function buildToolRoutes(activeConnections, inactiveWithCache = []) {
3822
4025
  const routes = /* @__PURE__ */ new Map();
3823
4026
  for (const conn of activeConnections.values()) {
3824
4027
  for (const tool of conn.tools) {
4028
+ const existing = routes.get(tool.namespacedName);
4029
+ if (existing && existing.namespace !== conn.config.namespace) {
4030
+ log("warn", "Tool route collision; later upstream shadows earlier", {
4031
+ tool: tool.namespacedName,
4032
+ shadowedNamespace: existing.namespace,
4033
+ winningNamespace: conn.config.namespace
4034
+ });
4035
+ }
3825
4036
  routes.set(tool.namespacedName, {
3826
4037
  namespace: conn.config.namespace,
3827
4038
  originalName: tool.name
@@ -3969,9 +4180,12 @@ async function routeToolCall(toolName, args, toolRoutes, activeConnections) {
3969
4180
  });
3970
4181
  return result;
3971
4182
  } catch (err) {
3972
- log("error", "Tool call failed", { tool: toolName, namespace: route.namespace, error: err.message });
4183
+ const message = err instanceof Error ? err.message : String(err);
4184
+ const code = err && typeof err === "object" && "code" in err && typeof err.code === "number" ? err.code : void 0;
4185
+ log("error", "Tool call failed", { tool: toolName, namespace: route.namespace, error: message, code });
4186
+ const codeTag = code !== void 0 ? ` [code=${code}]` : "";
3973
4187
  return {
3974
- content: [{ type: "text", text: `Error calling ${toolName}: ${err.message}` }],
4188
+ content: [{ type: "text", text: `Error calling ${toolName}${codeTag}: ${message}` }],
3975
4189
  isError: true
3976
4190
  };
3977
4191
  }
@@ -4194,26 +4408,26 @@ function rankServers(context, servers) {
4194
4408
  }
4195
4409
 
4196
4410
  // src/rerank.ts
4197
- import { request as request4 } from "undici";
4198
- var apiUrl2 = "";
4199
- var token2 = "";
4411
+ import { request as request5 } from "undici";
4412
+ var apiUrl3 = "";
4413
+ var token3 = "";
4200
4414
  var RERANK_TIMEOUT_MS = 2e3;
4201
4415
  function initRerank(url, tok) {
4202
- apiUrl2 = url;
4203
- token2 = tok;
4416
+ apiUrl3 = url;
4417
+ token3 = tok;
4204
4418
  }
4205
4419
  async function rerank(intent, candidateIds, limit) {
4206
- if (!apiUrl2 || !token2) return null;
4420
+ if (!apiUrl3 || !token3) return null;
4207
4421
  if (!intent?.trim()) return null;
4208
4422
  if (candidateIds !== void 0 && candidateIds.length === 0) return null;
4209
4423
  const payload = { intent: intent.trim() };
4210
4424
  if (candidateIds && candidateIds.length > 0) payload.candidateIds = candidateIds;
4211
4425
  if (typeof limit === "number" && limit > 0) payload.limit = limit;
4212
4426
  try {
4213
- const res = await request4(`${apiUrl2.replace(/\/$/, "")}/api/connect/rerank`, {
4427
+ const res = await request5(`${apiUrl3.replace(/\/$/, "")}/api/connect/rerank`, {
4214
4428
  method: "POST",
4215
4429
  headers: {
4216
- Authorization: `Bearer ${token2}`,
4430
+ Authorization: `Bearer ${token3}`,
4217
4431
  "Content-Type": "application/json"
4218
4432
  },
4219
4433
  body: JSON.stringify(payload),
@@ -4243,14 +4457,14 @@ async function rerank(intent, candidateIds, limit) {
4243
4457
 
4244
4458
  // src/runtime-detect.ts
4245
4459
  import { spawn as spawn2 } from "child_process";
4246
- import { request as request5 } from "undici";
4460
+ import { request as request6 } from "undici";
4247
4461
  var PROBE_TIMEOUT_MS = 3e3;
4248
4462
  var RUNTIME_REPORT_PATH = "/api/connect/runtimes";
4249
- var apiUrl3 = "";
4250
- var token3 = "";
4463
+ var apiUrl4 = "";
4464
+ var token4 = "";
4251
4465
  function initRuntimeDetect(url, tok) {
4252
- apiUrl3 = url;
4253
- token3 = tok;
4466
+ apiUrl4 = url;
4467
+ token4 = tok;
4254
4468
  }
4255
4469
  var PROBES = {
4256
4470
  node: {
@@ -4362,7 +4576,7 @@ async function detectRuntimes() {
4362
4576
  return out;
4363
4577
  }
4364
4578
  async function reportRuntimes() {
4365
- if (!apiUrl3 || !token3) return;
4579
+ if (!apiUrl4 || !token4) return;
4366
4580
  let runtimes;
4367
4581
  try {
4368
4582
  runtimes = await detectRuntimes();
@@ -4371,10 +4585,10 @@ async function reportRuntimes() {
4371
4585
  return;
4372
4586
  }
4373
4587
  try {
4374
- const res = await request5(`${apiUrl3.replace(/\/$/, "")}${RUNTIME_REPORT_PATH}`, {
4588
+ const res = await request6(`${apiUrl4.replace(/\/$/, "")}${RUNTIME_REPORT_PATH}`, {
4375
4589
  method: "POST",
4376
4590
  headers: {
4377
- Authorization: `Bearer ${token3}`,
4591
+ Authorization: `Bearer ${token4}`,
4378
4592
  "Content-Type": "application/json"
4379
4593
  },
4380
4594
  body: JSON.stringify({ runtimes }),
@@ -4419,15 +4633,23 @@ function buildTiebreakPrompt(intent, candidates) {
4419
4633
  ].join("\n");
4420
4634
  }
4421
4635
  function parseTiebreakResponse(response, candidates) {
4422
- const namespaces = new Set(candidates.map((c) => c.namespace));
4636
+ const namespaces = candidates.map((c) => c.namespace);
4637
+ const namespaceSet = new Set(namespaces);
4423
4638
  for (const rawLine of response.split(/\r?\n/)) {
4424
4639
  const line = rawLine.trim().replace(/^[`"'*>\-\s]+|[`"'*\s]+$/g, "");
4425
4640
  if (!line) continue;
4426
- if (namespaces.has(line)) return line;
4641
+ if (namespaceSet.has(line)) return line;
4642
+ let bestNs = null;
4643
+ let bestPos = Number.POSITIVE_INFINITY;
4427
4644
  for (const ns of namespaces) {
4428
4645
  const re = new RegExp(`\\b${escapeRegex(ns)}\\b`);
4429
- if (re.test(line)) return ns;
4646
+ const match = re.exec(line);
4647
+ if (match && match.index < bestPos) {
4648
+ bestPos = match.index;
4649
+ bestNs = ns;
4650
+ }
4430
4651
  }
4652
+ if (bestNs) return bestNs;
4431
4653
  }
4432
4654
  return null;
4433
4655
  }
@@ -4505,9 +4727,6 @@ function evaluateServerCap(namespace, loaded, cap) {
4505
4727
  };
4506
4728
  }
4507
4729
 
4508
- // src/test-runner.ts
4509
- import { request as request7 } from "undici";
4510
-
4511
4730
  // src/upstream.ts
4512
4731
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4513
4732
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
@@ -4524,9 +4743,9 @@ import { spawn as spawn3 } from "child_process";
4524
4743
  import { createHash } from "crypto";
4525
4744
  import { createWriteStream } from "fs";
4526
4745
  import fs from "fs/promises";
4527
- import path3 from "path";
4746
+ import path4 from "path";
4528
4747
  import { pipeline } from "stream/promises";
4529
- import { request as request6 } from "undici";
4748
+ import { request as request7 } from "undici";
4530
4749
  var UV_VERSION = "0.11.7";
4531
4750
  var RELEASE_BASE = `https://github.com/astral-sh/uv/releases/download/${UV_VERSION}`;
4532
4751
  function uvTarget() {
@@ -4603,7 +4822,7 @@ async function onPath(cmd) {
4603
4822
  async function fetchWithRedirects(url, maxHops = 5) {
4604
4823
  let current = url;
4605
4824
  for (let i = 0; i < maxHops; i++) {
4606
- const res = await request6(current, { method: "GET" });
4825
+ const res = await request7(current, { method: "GET" });
4607
4826
  if (res.statusCode >= 300 && res.statusCode < 400) {
4608
4827
  const loc = res.headers.location;
4609
4828
  if (!loc) throw new Error(`Redirect without Location header from ${current}`);
@@ -4653,7 +4872,7 @@ function runCommand(cmd, args) {
4653
4872
  async function findBinary(root, name) {
4654
4873
  const entries = await fs.readdir(root, { withFileTypes: true });
4655
4874
  for (const e of entries) {
4656
- const full = path3.join(root, e.name);
4875
+ const full = path4.join(root, e.name);
4657
4876
  if (e.isFile() && e.name === name) return full;
4658
4877
  if (e.isDirectory()) {
4659
4878
  const found = await findBinary(full, name);
@@ -4675,8 +4894,8 @@ async function resolveUv() {
4675
4894
  `No prebuilt uv binary for ${process.platform}/${process.arch}. Install uv manually: https://docs.astral.sh/uv/`
4676
4895
  );
4677
4896
  }
4678
- const installDir = path3.join(cacheDir(), "uv", UV_VERSION);
4679
- const finalBin = path3.join(installDir, binName());
4897
+ const installDir = path4.join(cacheDir(), "uv", UV_VERSION);
4898
+ const finalBin = path4.join(installDir, binName());
4680
4899
  if (await exists2(finalBin)) return finalBin;
4681
4900
  await fs.mkdir(installDir, { recursive: true });
4682
4901
  log("info", "Bootstrapping uv", { version: UV_VERSION, target, cache: installDir });
@@ -4689,11 +4908,11 @@ async function resolveUv() {
4689
4908
  if (!expected || expected.toLowerCase() !== actual.toLowerCase()) {
4690
4909
  throw new Error(`uv archive checksum mismatch (expected ${expected}, got ${actual})`);
4691
4910
  }
4692
- const archivePath = path3.join(installDir, archiveName);
4911
+ const archivePath = path4.join(installDir, archiveName);
4693
4912
  await pipeline(async function* () {
4694
4913
  yield archiveBuf;
4695
4914
  }, createWriteStream(archivePath));
4696
- const extractDir = path3.join(installDir, "extract");
4915
+ const extractDir = path4.join(installDir, "extract");
4697
4916
  await fs.rm(extractDir, { recursive: true, force: true });
4698
4917
  await extractArchive(archivePath, extractDir);
4699
4918
  const extracted = await findBinary(extractDir, binName());
@@ -4722,6 +4941,12 @@ var CONNECT_TIMEOUT = (() => {
4722
4941
  const n = Number.parseInt(env, 10);
4723
4942
  return Number.isFinite(n) && n > 0 ? n : 15e3;
4724
4943
  })();
4944
+ var LIST_TIMEOUT = (() => {
4945
+ const env = process.env.MCP_LIST_TIMEOUT;
4946
+ if (!env) return 15e3;
4947
+ const n = Number.parseInt(env, 10);
4948
+ return Number.isFinite(n) && n > 0 ? n : 15e3;
4949
+ })();
4725
4950
  var STDERR_RING_CAP = 8 * 1024;
4726
4951
  var MAX_TOOLS_PER_SERVER = 1e3;
4727
4952
  var MAX_RESOURCES_PER_SERVER = 1e3;
@@ -4746,7 +4971,7 @@ function categorizeSpawnError(err) {
4746
4971
  }
4747
4972
  async function connectToUpstream(config, onDisconnect, onListChanged) {
4748
4973
  const client = new Client(
4749
- { name: "mcph", version: true ? "0.47.1" : "dev" },
4974
+ { name: "mcph", version: true ? "0.47.3" : "dev" },
4750
4975
  { capabilities: {} }
4751
4976
  );
4752
4977
  let transport;
@@ -4911,7 +5136,7 @@ async function disconnectFromUpstream(connection) {
4911
5136
  }
4912
5137
  async function fetchResourcesFromUpstream(client, namespace) {
4913
5138
  try {
4914
- const result = await client.listResources();
5139
+ const result = await client.listResources({}, { timeout: LIST_TIMEOUT });
4915
5140
  const raw = result.resources ?? [];
4916
5141
  if (raw.length > MAX_RESOURCES_PER_SERVER) {
4917
5142
  log("warn", "Upstream returned more resources than cap; truncating", {
@@ -4933,7 +5158,7 @@ async function fetchResourcesFromUpstream(client, namespace) {
4933
5158
  }
4934
5159
  async function fetchPromptsFromUpstream(client, namespace) {
4935
5160
  try {
4936
- const result = await client.listPrompts();
5161
+ const result = await client.listPrompts({}, { timeout: LIST_TIMEOUT });
4937
5162
  const raw = result.prompts ?? [];
4938
5163
  if (raw.length > MAX_PROMPTS_PER_SERVER) {
4939
5164
  log("warn", "Upstream returned more prompts than cap; truncating", {
@@ -4953,7 +5178,7 @@ async function fetchPromptsFromUpstream(client, namespace) {
4953
5178
  }
4954
5179
  }
4955
5180
  async function fetchToolsFromUpstream(client, namespace) {
4956
- const result = await client.listTools();
5181
+ const result = await client.listTools({}, { timeout: LIST_TIMEOUT });
4957
5182
  const raw = result.tools ?? [];
4958
5183
  if (raw.length > MAX_TOOLS_PER_SERVER) {
4959
5184
  log("warn", "Upstream returned more tools than cap; truncating", {
@@ -4971,179 +5196,6 @@ async function fetchToolsFromUpstream(client, namespace) {
4971
5196
  }));
4972
5197
  }
4973
5198
 
4974
- // src/test-runner.ts
4975
- var POLL_INTERVAL_MS = 3e4;
4976
- var REQUEST_TIMEOUT_MS = 1e4;
4977
- var apiUrl4 = "";
4978
- var token4 = "";
4979
- var pollTimer = null;
4980
- var running = false;
4981
- var configRef = () => null;
4982
- function initTestRunner(url, tok, getConfig) {
4983
- apiUrl4 = url;
4984
- token4 = tok;
4985
- configRef = getConfig;
4986
- }
4987
- function startTestRunner() {
4988
- if (running) return;
4989
- running = true;
4990
- schedule();
4991
- }
4992
- function stopTestRunner() {
4993
- running = false;
4994
- if (pollTimer) {
4995
- clearTimeout(pollTimer);
4996
- pollTimer = null;
4997
- }
4998
- }
4999
- function schedule() {
5000
- pollTimer = setTimeout(async () => {
5001
- try {
5002
- await pollOnce();
5003
- } catch (err) {
5004
- log("warn", "Test runner poll failed", { error: err?.message });
5005
- }
5006
- if (running) schedule();
5007
- }, POLL_INTERVAL_MS);
5008
- pollTimer.unref?.();
5009
- }
5010
- async function pollOnce() {
5011
- if (!apiUrl4 || !token4) return;
5012
- const list = await fetchPending();
5013
- if (list.length === 0) return;
5014
- for (const pending2 of list) {
5015
- if (!running) return;
5016
- await runOne(pending2).catch((err) => {
5017
- log("warn", "Test execution failed", { requestId: pending2.requestId, error: err?.message });
5018
- });
5019
- }
5020
- }
5021
- async function fetchPending() {
5022
- try {
5023
- const res = await request7(`${apiUrl4.replace(/\/$/, "")}/api/connect/test-requests`, {
5024
- method: "GET",
5025
- headers: { Authorization: `Bearer ${token4}` },
5026
- headersTimeout: REQUEST_TIMEOUT_MS,
5027
- bodyTimeout: REQUEST_TIMEOUT_MS
5028
- });
5029
- if (res.statusCode === 404) {
5030
- await res.body.text().catch(() => {
5031
- });
5032
- stopTestRunner();
5033
- return [];
5034
- }
5035
- if (res.statusCode !== 200) {
5036
- await res.body.text().catch(() => {
5037
- });
5038
- return [];
5039
- }
5040
- const body = await res.body.json();
5041
- return Array.isArray(body?.requests) ? body.requests : [];
5042
- } catch {
5043
- return [];
5044
- }
5045
- }
5046
- async function runOne(pending2) {
5047
- const config = configRef();
5048
- const serverConfig = config?.servers.find((s) => s.id === pending2.serverId);
5049
- if (!serverConfig) {
5050
- await postResult(pending2.requestId, {
5051
- status: "failed",
5052
- message: `Server "${pending2.serverId}" is not in this mcph's current config \u2014 restart mcph or refresh the dashboard.`,
5053
- errorCategory: "not_in_config"
5054
- });
5055
- return;
5056
- }
5057
- if (!serverConfig.isActive) {
5058
- await postResult(pending2.requestId, {
5059
- status: "failed",
5060
- message: `Server "${serverConfig.namespace}" is disabled in the dashboard \u2014 re-enable it before testing.`,
5061
- errorCategory: "disabled"
5062
- });
5063
- return;
5064
- }
5065
- let connection = null;
5066
- try {
5067
- connection = await connectToUpstream(serverConfig);
5068
- await postResult(pending2.requestId, {
5069
- status: "passed",
5070
- toolCount: connection.tools.length,
5071
- message: `Connected \u2014 ${connection.tools.length} tool${connection.tools.length === 1 ? "" : "s"} available.`
5072
- });
5073
- } catch (err) {
5074
- if (err instanceof ActivationError) {
5075
- await postResult(pending2.requestId, {
5076
- status: "failed",
5077
- message: err.message,
5078
- errorCategory: err.category
5079
- });
5080
- } else {
5081
- await postResult(pending2.requestId, {
5082
- status: "failed",
5083
- message: err instanceof Error ? err.message : String(err),
5084
- errorCategory: "unknown"
5085
- });
5086
- }
5087
- } finally {
5088
- if (connection) {
5089
- await disconnectFromUpstream(connection).catch(() => {
5090
- });
5091
- }
5092
- }
5093
- }
5094
- async function postResult(requestId, result) {
5095
- try {
5096
- const res = await request7(
5097
- `${apiUrl4.replace(/\/$/, "")}/api/connect/test-requests/${encodeURIComponent(requestId)}/result`,
5098
- {
5099
- method: "POST",
5100
- headers: {
5101
- Authorization: `Bearer ${token4}`,
5102
- "Content-Type": "application/json"
5103
- },
5104
- body: JSON.stringify(result),
5105
- headersTimeout: REQUEST_TIMEOUT_MS,
5106
- bodyTimeout: REQUEST_TIMEOUT_MS
5107
- }
5108
- );
5109
- await res.body.text().catch(() => {
5110
- });
5111
- } catch (err) {
5112
- log("warn", "Posting test result failed", { requestId, error: err?.message });
5113
- }
5114
- }
5115
-
5116
- // src/tool-report.ts
5117
- import { request as request8 } from "undici";
5118
- var apiUrl5 = "";
5119
- var token5 = "";
5120
- function initToolReport(url, tok) {
5121
- apiUrl5 = url;
5122
- token5 = tok;
5123
- }
5124
- async function reportTools(serverId, tools) {
5125
- if (!apiUrl5 || !token5 || !serverId) return;
5126
- try {
5127
- const res = await request8(`${apiUrl5.replace(/\/$/, "")}/api/connect/servers/${serverId}/tools`, {
5128
- method: "POST",
5129
- headers: {
5130
- Authorization: `Bearer ${token5}`,
5131
- "Content-Type": "application/json"
5132
- },
5133
- body: JSON.stringify({ tools }),
5134
- headersTimeout: 1e4,
5135
- bodyTimeout: 1e4
5136
- });
5137
- await res.body.text().catch(() => {
5138
- });
5139
- if (res.statusCode >= 400 && res.statusCode !== 404) {
5140
- log("warn", "Tool report failed", { serverId, status: res.statusCode });
5141
- }
5142
- } catch (err) {
5143
- log("warn", "Tool report error", { serverId, error: err?.message });
5144
- }
5145
- }
5146
-
5147
5199
  // src/server.ts
5148
5200
  var DEFAULT_POLL_INTERVAL_MS = 6e4;
5149
5201
  function resolvePollIntervalMs() {
@@ -5223,11 +5275,11 @@ function computeToolOverlaps(connections) {
5223
5275
  return overlaps;
5224
5276
  }
5225
5277
  var ConnectServer = class _ConnectServer {
5226
- constructor(apiUrl6, token6) {
5227
- this.apiUrl = apiUrl6;
5228
- this.token = token6;
5278
+ constructor(apiUrl5, token5) {
5279
+ this.apiUrl = apiUrl5;
5280
+ this.token = token5;
5229
5281
  this.server = new Server(
5230
- { name: "mcph", version: true ? "0.47.1" : "dev" },
5282
+ { name: "mcph", version: true ? "0.47.3" : "dev" },
5231
5283
  {
5232
5284
  capabilities: {
5233
5285
  tools: { listChanged: true },
@@ -5368,23 +5420,23 @@ var ConnectServer = class _ConnectServer {
5368
5420
  this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
5369
5421
  tools: buildToolList(this.connections, this.getDeferredServers(), this.toolFilters)
5370
5422
  }));
5371
- this.server.setRequestHandler(CallToolRequestSchema, async (request10, extra) => {
5372
- const { name, arguments: args } = request10.params;
5423
+ this.server.setRequestHandler(CallToolRequestSchema, async (request9, extra) => {
5424
+ const { name, arguments: args } = request9.params;
5373
5425
  return this.handleToolCall(name, args ?? {}, extra);
5374
5426
  });
5375
5427
  this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
5376
5428
  resources: buildResourceList(this.connections, this.getBuiltinResources())
5377
5429
  }));
5378
- this.server.setRequestHandler(ReadResourceRequestSchema, async (request10) => {
5379
- return routeResourceRead(request10.params.uri, this.resourceRoutes, this.connections, this.getBuiltinResourceMap());
5430
+ this.server.setRequestHandler(ReadResourceRequestSchema, async (request9) => {
5431
+ return routeResourceRead(request9.params.uri, this.resourceRoutes, this.connections, this.getBuiltinResourceMap());
5380
5432
  });
5381
5433
  this.server.setRequestHandler(ListPromptsRequestSchema, async () => ({
5382
5434
  prompts: buildPromptList(this.connections)
5383
5435
  }));
5384
- this.server.setRequestHandler(GetPromptRequestSchema, async (request10) => {
5436
+ this.server.setRequestHandler(GetPromptRequestSchema, async (request9) => {
5385
5437
  return routePromptGet(
5386
- request10.params.name,
5387
- request10.params.arguments,
5438
+ request9.params.name,
5439
+ request9.params.arguments,
5388
5440
  this.promptRoutes,
5389
5441
  this.connections
5390
5442
  );
@@ -5489,12 +5541,10 @@ var ConnectServer = class _ConnectServer {
5489
5541
  initToolReport(this.apiUrl, this.token);
5490
5542
  initRerank(this.apiUrl, this.token);
5491
5543
  initRuntimeDetect(this.apiUrl, this.token);
5492
- initTestRunner(this.apiUrl, this.token, () => this.config);
5493
5544
  reportRuntimes().catch((err) => log("warn", "reportRuntimes failed", { error: err?.message }));
5494
5545
  if (this.config?.servers.some((s) => s.command === "uv" || s.command === "uvx")) {
5495
5546
  ensureUv().catch((err) => log("warn", "uv prewarm failed", { error: err?.message }));
5496
5547
  }
5497
- startTestRunner();
5498
5548
  const transport = new StdioServerTransport();
5499
5549
  await this.server.connect(transport);
5500
5550
  this.startPolling();
@@ -5740,7 +5790,10 @@ var ConnectServer = class _ConnectServer {
5740
5790
  if (serverConfig) {
5741
5791
  let reconnected = false;
5742
5792
  let lastErr;
5743
- for (let attempt = 0; attempt < 2; attempt++) {
5793
+ const RECONNECT_ATTEMPTS = 2;
5794
+ const RECONNECT_DELAY_MS = 1e3;
5795
+ for (let attempt = 0; attempt < RECONNECT_ATTEMPTS; attempt++) {
5796
+ if (attempt > 0) await new Promise((r) => setTimeout(r, RECONNECT_DELAY_MS));
5744
5797
  try {
5745
5798
  await disconnectFromUpstream(conn);
5746
5799
  const newConn = await connectToUpstream(
@@ -5756,23 +5809,23 @@ var ConnectServer = class _ConnectServer {
5756
5809
  break;
5757
5810
  } catch (err) {
5758
5811
  lastErr = err;
5759
- if (attempt === 0) {
5812
+ if (attempt < RECONNECT_ATTEMPTS - 1) {
5760
5813
  log("warn", "Auto-reconnect attempt failed, retrying", {
5761
5814
  namespace: route.namespace,
5762
- error: err.message
5815
+ error: err instanceof Error ? err.message : String(err)
5763
5816
  });
5764
- await new Promise((r) => setTimeout(r, 1e3 * 2 ** attempt));
5765
5817
  }
5766
5818
  }
5767
5819
  }
5768
5820
  if (!reconnected) {
5769
5821
  conn.status = "error";
5770
- log("error", "Auto-reconnect failed", { namespace: route.namespace, error: lastErr.message });
5822
+ const lastErrMsg = lastErr instanceof Error ? lastErr.message : String(lastErr);
5823
+ log("error", "Auto-reconnect failed", { namespace: route.namespace, error: lastErrMsg });
5771
5824
  return {
5772
5825
  content: [
5773
5826
  {
5774
5827
  type: "text",
5775
- text: `Server "${route.namespace}" disconnected and auto-reconnect failed: ${lastErr.message}. Use mcp_connect_activate with server "${route.namespace}" to reload it manually.`
5828
+ text: `Server "${route.namespace}" disconnected and auto-reconnect failed: ${lastErrMsg}. Use mcp_connect_activate with server "${route.namespace}" to reload it manually.`
5776
5829
  }
5777
5830
  ],
5778
5831
  isError: true
@@ -5800,8 +5853,7 @@ var ConnectServer = class _ConnectServer {
5800
5853
  toolName: route.originalName,
5801
5854
  action: "tool_call",
5802
5855
  latencyMs,
5803
- success: !result.isError,
5804
- error: result.isError ? result.content[0]?.text : void 0
5856
+ success: !result.isError
5805
5857
  });
5806
5858
  if (!result.isError && Array.isArray(result.content)) {
5807
5859
  try {
@@ -5848,9 +5900,11 @@ var ConnectServer = class _ConnectServer {
5848
5900
  } catch {
5849
5901
  }
5850
5902
  }
5903
+ this.learning.recordDispatch(route.namespace);
5904
+ if (!result.isError) this.learning.recordSuccess(route.namespace);
5905
+ this.scheduleStateSave();
5851
5906
  if (!result.isError) {
5852
5907
  this.packDetector.recordCall(route.namespace, route.originalName, Date.now());
5853
- this.scheduleStateSave();
5854
5908
  }
5855
5909
  await this.trackUsageAndAutoDeactivate(route.namespace);
5856
5910
  }
@@ -5909,9 +5963,9 @@ var ConnectServer = class _ConnectServer {
5909
5963
  const shortlist = bm25.slice(0, _ConnectServer.BM25_TOP_K);
5910
5964
  const idByNamespace = new Map(servers.map((s) => [s.namespace, s.id]));
5911
5965
  const candidateIds = shortlist.map((r) => idByNamespace.get(r.namespace)).filter((id) => typeof id === "string" && id.length > 0);
5912
- if (candidateIds.length === 0) return shortlist;
5966
+ if (candidateIds.length === 0) return shortlist.map((r) => ({ ...r, hasRerank: false }));
5913
5967
  const rerankResults = await rerank(context, candidateIds);
5914
- if (!rerankResults) return shortlist;
5968
+ if (!rerankResults) return shortlist.map((r) => ({ ...r, hasRerank: false }));
5915
5969
  const namespaceById = new Map(servers.map((s) => [s.id, s.namespace]));
5916
5970
  const rerankScoreByNamespace = /* @__PURE__ */ new Map();
5917
5971
  for (const r of rerankResults) {
@@ -5931,7 +5985,7 @@ var ConnectServer = class _ConnectServer {
5931
5985
  if (a.hasRerank !== b.hasRerank) return a.hasRerank ? -1 : 1;
5932
5986
  return b.score - a.score;
5933
5987
  });
5934
- return reordered.map((r) => ({ namespace: r.namespace, score: r.score }));
5988
+ return reordered;
5935
5989
  }
5936
5990
  // Auto-warm confidence gate — applied to discover(context) so a single
5937
5991
  // clearly-winning server gets activated without the LLM needing to
@@ -5942,10 +5996,15 @@ var ConnectServer = class _ConnectServer {
5942
5996
  return raw === void 0 || raw === "" || raw === "1" || raw.toLowerCase() === "true";
5943
5997
  })();
5944
5998
  // Top score must clear this floor AND the gap over the runner-up must
5945
- // be convincing before we auto-activate. Values tuned by intuition;
5946
- // when we have real usage data we can re-pick them.
5947
- static AUTO_ACTIVATE_MIN_SCORE = 1;
5948
- static AUTO_ACTIVATE_MARGIN = 1.3;
5999
+ // be convincing before we auto-activate. Values are scale-dependent --
6000
+ // BM25 scores are unbounded positive numbers, rerank cosines are in
6001
+ // [0, 1] -- so we keep separate thresholds and pick based on whether
6002
+ // the top entry was reranked. Tuned by intuition; revisit when we
6003
+ // have real usage data.
6004
+ static AUTO_ACTIVATE_MIN_SCORE_BM25 = 1;
6005
+ static AUTO_ACTIVATE_MARGIN_BM25 = 1.3;
6006
+ static AUTO_ACTIVATE_MIN_SCORE_COSINE = 0.5;
6007
+ static AUTO_ACTIVATE_MARGIN_COSINE = 1.25;
5949
6008
  // Below this installed-server count, discover() appends a one-line
5950
6009
  // marketplace pointer so sparse-config users see where to add more.
5951
6010
  // At or above the threshold we stay silent — power users already know
@@ -5966,7 +6025,9 @@ var ConnectServer = class _ConnectServer {
5966
6025
  if (ranked.length === 0) return this.handleDiscover(context);
5967
6026
  const top = ranked[0];
5968
6027
  const second = ranked[1];
5969
- const topWinsDecisively = top !== void 0 && top.score >= _ConnectServer.AUTO_ACTIVATE_MIN_SCORE && (second === void 0 || top.score / (second.score || 1e-6) >= _ConnectServer.AUTO_ACTIVATE_MARGIN);
6028
+ const minScore = top?.hasRerank ? _ConnectServer.AUTO_ACTIVATE_MIN_SCORE_COSINE : _ConnectServer.AUTO_ACTIVATE_MIN_SCORE_BM25;
6029
+ const margin = top?.hasRerank ? _ConnectServer.AUTO_ACTIVATE_MARGIN_COSINE : _ConnectServer.AUTO_ACTIVATE_MARGIN_BM25;
6030
+ const topWinsDecisively = top !== void 0 && top.score >= minScore && (second === void 0 || top.score / (second.score || 1e-6) >= margin);
5970
6031
  if (!topWinsDecisively || !top) return this.handleDiscover(context);
5971
6032
  const existing = this.connections.get(top.namespace);
5972
6033
  if (existing && existing.status === "connected") return this.handleDiscover(context);
@@ -6487,9 +6548,6 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
6487
6548
  results.push(`${winner.namespace} (score ${winner.score.toFixed(2)}): ${r.message}`);
6488
6549
  if (r.isChanged) anyChanged = true;
6489
6550
  if (!r.ok) anyError = true;
6490
- this.learning.recordDispatch(winner.namespace);
6491
- if (r.ok) this.learning.recordSuccess(winner.namespace);
6492
- this.scheduleStateSave();
6493
6551
  }
6494
6552
  if (anyChanged) {
6495
6553
  this.rebuildRoutes();
@@ -6723,7 +6781,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
6723
6781
  nsToKeys.set(s.namespace, existing);
6724
6782
  }
6725
6783
  const collisions = [...nsToKeys.entries()].filter(([, keys]) => keys.length > 1);
6726
- const res = await request9(`${this.apiUrl.replace(/\/$/, "")}/api/connect/import`, {
6784
+ const res = await request8(`${this.apiUrl.replace(/\/$/, "")}/api/connect/import`, {
6727
6785
  method: "POST",
6728
6786
  headers: {
6729
6787
  Authorization: `Bearer ${this.token}`,
@@ -6784,7 +6842,7 @@ Use mcp_connect_discover to see imported servers.`
6784
6842
  }
6785
6843
  const payload = built.payload;
6786
6844
  try {
6787
- const res = await request9(`${this.apiUrl.replace(/\/$/, "")}/api/connect/servers`, {
6845
+ const res = await request8(`${this.apiUrl.replace(/\/$/, "")}/api/connect/servers`, {
6788
6846
  method: "POST",
6789
6847
  headers: {
6790
6848
  Authorization: `Bearer ${this.token}`,
@@ -7248,7 +7306,6 @@ To load the top pack in one step, call \`mcp_connect_activate\` with namespaces=
7248
7306
  if (this.persistenceReady) {
7249
7307
  await this.flushStateSave();
7250
7308
  }
7251
- stopTestRunner();
7252
7309
  await shutdownAnalytics();
7253
7310
  const disconnects = Array.from(this.connections.values()).map((conn) => disconnectFromUpstream(conn));
7254
7311
  await Promise.allSettled(disconnects);
@@ -7551,13 +7608,24 @@ async function runUpgrade(opts = {}) {
7551
7608
  print("No upgrade command available for this install method.");
7552
7609
  return { exitCode: 0, lines };
7553
7610
  }
7611
+ const autoRunnable = method === "global-npm";
7554
7612
  if (!opts.run) {
7555
- print(`Run:
7613
+ if (autoRunnable) {
7614
+ print(`Run:
7615
+ ${plan.command}
7616
+
7617
+ Or re-run with --run to upgrade in place.`);
7618
+ } else {
7619
+ print(`Suggested command (run it yourself; --run only works for global-npm installs):
7556
7620
  ${plan.command}`);
7621
+ }
7557
7622
  return { exitCode: 1, lines };
7558
7623
  }
7559
- if (method !== "global-npm") {
7560
- printErr(`mcph upgrade --run: refusing to auto-run upgrade for method "${method}". Run manually: ${plan.command}`);
7624
+ if (!autoRunnable) {
7625
+ printErr(
7626
+ `mcph upgrade --run: install method "${method}" can't be upgraded automatically. Run manually:
7627
+ ${plan.command}`
7628
+ );
7561
7629
  return { exitCode: 2, lines };
7562
7630
  }
7563
7631
  const runner = opts.spawnImpl ?? defaultSpawn;
@@ -7572,7 +7640,7 @@ async function runUpgrade(opts = {}) {
7572
7640
  return { exitCode: 3, lines };
7573
7641
  }
7574
7642
  function readCurrentVersion() {
7575
- return true ? "0.47.1" : "dev";
7643
+ return true ? "0.47.3" : "dev";
7576
7644
  }
7577
7645
 
7578
7646
  // src/index.ts
@@ -7601,7 +7669,8 @@ if (subcommand === "compliance") {
7601
7669
  `);
7602
7670
  process.exit(2);
7603
7671
  }
7604
- runInstall(parsed.options).then((r) => process.exit(r.exitCode));
7672
+ const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR && process.env.CLAUDE_CONFIG_DIR.length > 0 ? process.env.CLAUDE_CONFIG_DIR : void 0;
7673
+ runInstall({ ...parsed.options, claudeConfigDir }).then((r) => process.exit(r.exitCode));
7605
7674
  } else if (subcommand === "doctor") {
7606
7675
  const doctorArgs = process.argv.slice(3);
7607
7676
  const doctorJson = doctorArgs.includes("--json");
@@ -7619,6 +7688,17 @@ if (subcommand === "compliance") {
7619
7688
  }
7620
7689
  runDoctor({ json: doctorJson }).then((r) => process.exit(r.exitCode));
7621
7690
  } else if (subcommand === "reset-learning") {
7691
+ const parsed = parseResetLearningArgs(process.argv.slice(3));
7692
+ if (parsed.kind === "help") {
7693
+ process.stdout.write(`${RESET_LEARNING_USAGE}
7694
+ `);
7695
+ process.exit(0);
7696
+ }
7697
+ if (parsed.kind === "error") {
7698
+ process.stderr.write(`${parsed.error}
7699
+ `);
7700
+ process.exit(2);
7701
+ }
7622
7702
  runResetLearning().then((r) => process.exit(r.exitCode));
7623
7703
  } else if (subcommand === "servers") {
7624
7704
  const parsed = parseServersArgs(process.argv.slice(3));
@@ -7728,7 +7808,7 @@ if (subcommand === "compliance") {
7728
7808
  `);
7729
7809
  process.exit(0);
7730
7810
  } else if (subcommand === "--version" || subcommand === "-V") {
7731
- process.stdout.write(`mcph ${true ? "0.47.1" : "dev"}
7811
+ process.stdout.write(`mcph ${true ? "0.47.3" : "dev"}
7732
7812
  `);
7733
7813
  process.exit(0);
7734
7814
  } else if (subcommand && !subcommand.startsWith("-")) {