@yawlabs/mcph 0.47.0 → 0.47.2

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,19 +277,19 @@ 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
295
  const token6 = typeof obj.token === "string" && obj.token.length > 0 ? obj.token : void 0;
@@ -298,21 +299,21 @@ async function readConfigAt(path4, scope, warnings) {
298
299
  if (token6) {
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: token6, 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 {
@@ -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.0" : "dev";
1653
+ var VERSION = true ? "0.47.2" : "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}`);
@@ -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, token6);
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, token6) {
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
2609
  next.token = token6;
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.");
@@ -2613,112 +2905,6 @@ import {
2613
2905
  } from "@modelcontextprotocol/sdk/types.js";
2614
2906
  import { request as request9 } from "undici";
2615
2907
 
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
- }
2721
-
2722
2908
  // src/compliance.ts
2723
2909
  var GRADE_ORDER = {
2724
2910
  A: 4,
@@ -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));
@@ -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
  }
@@ -4506,7 +4728,7 @@ function evaluateServerCap(namespace, loaded, cap) {
4506
4728
  }
4507
4729
 
4508
4730
  // src/test-runner.ts
4509
- import { request as request7 } from "undici";
4731
+ import { request as request8 } from "undici";
4510
4732
 
4511
4733
  // src/upstream.ts
4512
4734
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
@@ -4524,9 +4746,9 @@ import { spawn as spawn3 } from "child_process";
4524
4746
  import { createHash } from "crypto";
4525
4747
  import { createWriteStream } from "fs";
4526
4748
  import fs from "fs/promises";
4527
- import path3 from "path";
4749
+ import path4 from "path";
4528
4750
  import { pipeline } from "stream/promises";
4529
- import { request as request6 } from "undici";
4751
+ import { request as request7 } from "undici";
4530
4752
  var UV_VERSION = "0.11.7";
4531
4753
  var RELEASE_BASE = `https://github.com/astral-sh/uv/releases/download/${UV_VERSION}`;
4532
4754
  function uvTarget() {
@@ -4603,7 +4825,7 @@ async function onPath(cmd) {
4603
4825
  async function fetchWithRedirects(url, maxHops = 5) {
4604
4826
  let current = url;
4605
4827
  for (let i = 0; i < maxHops; i++) {
4606
- const res = await request6(current, { method: "GET" });
4828
+ const res = await request7(current, { method: "GET" });
4607
4829
  if (res.statusCode >= 300 && res.statusCode < 400) {
4608
4830
  const loc = res.headers.location;
4609
4831
  if (!loc) throw new Error(`Redirect without Location header from ${current}`);
@@ -4653,7 +4875,7 @@ function runCommand(cmd, args) {
4653
4875
  async function findBinary(root, name) {
4654
4876
  const entries = await fs.readdir(root, { withFileTypes: true });
4655
4877
  for (const e of entries) {
4656
- const full = path3.join(root, e.name);
4878
+ const full = path4.join(root, e.name);
4657
4879
  if (e.isFile() && e.name === name) return full;
4658
4880
  if (e.isDirectory()) {
4659
4881
  const found = await findBinary(full, name);
@@ -4675,8 +4897,8 @@ async function resolveUv() {
4675
4897
  `No prebuilt uv binary for ${process.platform}/${process.arch}. Install uv manually: https://docs.astral.sh/uv/`
4676
4898
  );
4677
4899
  }
4678
- const installDir = path3.join(cacheDir(), "uv", UV_VERSION);
4679
- const finalBin = path3.join(installDir, binName());
4900
+ const installDir = path4.join(cacheDir(), "uv", UV_VERSION);
4901
+ const finalBin = path4.join(installDir, binName());
4680
4902
  if (await exists2(finalBin)) return finalBin;
4681
4903
  await fs.mkdir(installDir, { recursive: true });
4682
4904
  log("info", "Bootstrapping uv", { version: UV_VERSION, target, cache: installDir });
@@ -4689,11 +4911,11 @@ async function resolveUv() {
4689
4911
  if (!expected || expected.toLowerCase() !== actual.toLowerCase()) {
4690
4912
  throw new Error(`uv archive checksum mismatch (expected ${expected}, got ${actual})`);
4691
4913
  }
4692
- const archivePath = path3.join(installDir, archiveName);
4914
+ const archivePath = path4.join(installDir, archiveName);
4693
4915
  await pipeline(async function* () {
4694
4916
  yield archiveBuf;
4695
4917
  }, createWriteStream(archivePath));
4696
- const extractDir = path3.join(installDir, "extract");
4918
+ const extractDir = path4.join(installDir, "extract");
4697
4919
  await fs.rm(extractDir, { recursive: true, force: true });
4698
4920
  await extractArchive(archivePath, extractDir);
4699
4921
  const extracted = await findBinary(extractDir, binName());
@@ -4722,6 +4944,12 @@ var CONNECT_TIMEOUT = (() => {
4722
4944
  const n = Number.parseInt(env, 10);
4723
4945
  return Number.isFinite(n) && n > 0 ? n : 15e3;
4724
4946
  })();
4947
+ var LIST_TIMEOUT = (() => {
4948
+ const env = process.env.MCP_LIST_TIMEOUT;
4949
+ if (!env) return 15e3;
4950
+ const n = Number.parseInt(env, 10);
4951
+ return Number.isFinite(n) && n > 0 ? n : 15e3;
4952
+ })();
4725
4953
  var STDERR_RING_CAP = 8 * 1024;
4726
4954
  var MAX_TOOLS_PER_SERVER = 1e3;
4727
4955
  var MAX_RESOURCES_PER_SERVER = 1e3;
@@ -4746,7 +4974,7 @@ function categorizeSpawnError(err) {
4746
4974
  }
4747
4975
  async function connectToUpstream(config, onDisconnect, onListChanged) {
4748
4976
  const client = new Client(
4749
- { name: "mcph", version: true ? "0.47.0" : "dev" },
4977
+ { name: "mcph", version: true ? "0.47.2" : "dev" },
4750
4978
  { capabilities: {} }
4751
4979
  );
4752
4980
  let transport;
@@ -4911,7 +5139,7 @@ async function disconnectFromUpstream(connection) {
4911
5139
  }
4912
5140
  async function fetchResourcesFromUpstream(client, namespace) {
4913
5141
  try {
4914
- const result = await client.listResources();
5142
+ const result = await client.listResources({}, { timeout: LIST_TIMEOUT });
4915
5143
  const raw = result.resources ?? [];
4916
5144
  if (raw.length > MAX_RESOURCES_PER_SERVER) {
4917
5145
  log("warn", "Upstream returned more resources than cap; truncating", {
@@ -4933,7 +5161,7 @@ async function fetchResourcesFromUpstream(client, namespace) {
4933
5161
  }
4934
5162
  async function fetchPromptsFromUpstream(client, namespace) {
4935
5163
  try {
4936
- const result = await client.listPrompts();
5164
+ const result = await client.listPrompts({}, { timeout: LIST_TIMEOUT });
4937
5165
  const raw = result.prompts ?? [];
4938
5166
  if (raw.length > MAX_PROMPTS_PER_SERVER) {
4939
5167
  log("warn", "Upstream returned more prompts than cap; truncating", {
@@ -4953,7 +5181,7 @@ async function fetchPromptsFromUpstream(client, namespace) {
4953
5181
  }
4954
5182
  }
4955
5183
  async function fetchToolsFromUpstream(client, namespace) {
4956
- const result = await client.listTools();
5184
+ const result = await client.listTools({}, { timeout: LIST_TIMEOUT });
4957
5185
  const raw = result.tools ?? [];
4958
5186
  if (raw.length > MAX_TOOLS_PER_SERVER) {
4959
5187
  log("warn", "Upstream returned more tools than cap; truncating", {
@@ -4974,23 +5202,27 @@ async function fetchToolsFromUpstream(client, namespace) {
4974
5202
  // src/test-runner.ts
4975
5203
  var POLL_INTERVAL_MS = 3e4;
4976
5204
  var REQUEST_TIMEOUT_MS = 1e4;
4977
- var apiUrl4 = "";
4978
- var token4 = "";
5205
+ var CONSECUTIVE_404_LIMIT = 10;
5206
+ var apiUrl5 = "";
5207
+ var token5 = "";
4979
5208
  var pollTimer = null;
4980
5209
  var running = false;
5210
+ var consecutive404 = 0;
4981
5211
  var configRef = () => null;
4982
5212
  function initTestRunner(url, tok, getConfig) {
4983
- apiUrl4 = url;
4984
- token4 = tok;
5213
+ apiUrl5 = url;
5214
+ token5 = tok;
4985
5215
  configRef = getConfig;
4986
5216
  }
4987
5217
  function startTestRunner() {
4988
5218
  if (running) return;
4989
5219
  running = true;
5220
+ consecutive404 = 0;
4990
5221
  schedule();
4991
5222
  }
4992
5223
  function stopTestRunner() {
4993
5224
  running = false;
5225
+ consecutive404 = 0;
4994
5226
  if (pollTimer) {
4995
5227
  clearTimeout(pollTimer);
4996
5228
  pollTimer = null;
@@ -5008,7 +5240,7 @@ function schedule() {
5008
5240
  pollTimer.unref?.();
5009
5241
  }
5010
5242
  async function pollOnce() {
5011
- if (!apiUrl4 || !token4) return;
5243
+ if (!apiUrl5 || !token5) return;
5012
5244
  const list = await fetchPending();
5013
5245
  if (list.length === 0) return;
5014
5246
  for (const pending2 of list) {
@@ -5020,23 +5252,34 @@ async function pollOnce() {
5020
5252
  }
5021
5253
  async function fetchPending() {
5022
5254
  try {
5023
- const res = await request7(`${apiUrl4.replace(/\/$/, "")}/api/connect/test-requests`, {
5255
+ const res = await request8(`${apiUrl5.replace(/\/$/, "")}/api/connect/test-requests`, {
5024
5256
  method: "GET",
5025
- headers: { Authorization: `Bearer ${token4}` },
5257
+ headers: { Authorization: `Bearer ${token5}` },
5026
5258
  headersTimeout: REQUEST_TIMEOUT_MS,
5027
5259
  bodyTimeout: REQUEST_TIMEOUT_MS
5028
5260
  });
5029
5261
  if (res.statusCode === 404) {
5030
5262
  await res.body.text().catch(() => {
5031
5263
  });
5032
- stopTestRunner();
5264
+ consecutive404++;
5265
+ if (consecutive404 === 1) {
5266
+ log("warn", "Test runner endpoint returned 404; will retry", { url: `${apiUrl5}/api/connect/test-requests` });
5267
+ }
5268
+ if (consecutive404 >= CONSECUTIVE_404_LIMIT) {
5269
+ log("warn", "Test runner endpoint persistently 404; stopping poller", {
5270
+ consecutive: consecutive404
5271
+ });
5272
+ stopTestRunner();
5273
+ }
5033
5274
  return [];
5034
5275
  }
5035
5276
  if (res.statusCode !== 200) {
5036
5277
  await res.body.text().catch(() => {
5037
5278
  });
5279
+ consecutive404 = 0;
5038
5280
  return [];
5039
5281
  }
5282
+ consecutive404 = 0;
5040
5283
  const body = await res.body.json();
5041
5284
  return Array.isArray(body?.requests) ? body.requests : [];
5042
5285
  } catch {
@@ -5093,12 +5336,12 @@ async function runOne(pending2) {
5093
5336
  }
5094
5337
  async function postResult(requestId, result) {
5095
5338
  try {
5096
- const res = await request7(
5097
- `${apiUrl4.replace(/\/$/, "")}/api/connect/test-requests/${encodeURIComponent(requestId)}/result`,
5339
+ const res = await request8(
5340
+ `${apiUrl5.replace(/\/$/, "")}/api/connect/test-requests/${encodeURIComponent(requestId)}/result`,
5098
5341
  {
5099
5342
  method: "POST",
5100
5343
  headers: {
5101
- Authorization: `Bearer ${token4}`,
5344
+ Authorization: `Bearer ${token5}`,
5102
5345
  "Content-Type": "application/json"
5103
5346
  },
5104
5347
  body: JSON.stringify(result),
@@ -5113,37 +5356,6 @@ async function postResult(requestId, result) {
5113
5356
  }
5114
5357
  }
5115
5358
 
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
5359
  // src/server.ts
5148
5360
  var DEFAULT_POLL_INTERVAL_MS = 6e4;
5149
5361
  function resolvePollIntervalMs() {
@@ -5227,7 +5439,7 @@ var ConnectServer = class _ConnectServer {
5227
5439
  this.apiUrl = apiUrl6;
5228
5440
  this.token = token6;
5229
5441
  this.server = new Server(
5230
- { name: "mcph", version: true ? "0.47.0" : "dev" },
5442
+ { name: "mcph", version: true ? "0.47.2" : "dev" },
5231
5443
  {
5232
5444
  capabilities: {
5233
5445
  tools: { listChanged: true },
@@ -5740,7 +5952,10 @@ var ConnectServer = class _ConnectServer {
5740
5952
  if (serverConfig) {
5741
5953
  let reconnected = false;
5742
5954
  let lastErr;
5743
- for (let attempt = 0; attempt < 2; attempt++) {
5955
+ const RECONNECT_ATTEMPTS = 2;
5956
+ const RECONNECT_DELAY_MS = 1e3;
5957
+ for (let attempt = 0; attempt < RECONNECT_ATTEMPTS; attempt++) {
5958
+ if (attempt > 0) await new Promise((r) => setTimeout(r, RECONNECT_DELAY_MS));
5744
5959
  try {
5745
5960
  await disconnectFromUpstream(conn);
5746
5961
  const newConn = await connectToUpstream(
@@ -5756,23 +5971,23 @@ var ConnectServer = class _ConnectServer {
5756
5971
  break;
5757
5972
  } catch (err) {
5758
5973
  lastErr = err;
5759
- if (attempt === 0) {
5974
+ if (attempt < RECONNECT_ATTEMPTS - 1) {
5760
5975
  log("warn", "Auto-reconnect attempt failed, retrying", {
5761
5976
  namespace: route.namespace,
5762
- error: err.message
5977
+ error: err instanceof Error ? err.message : String(err)
5763
5978
  });
5764
- await new Promise((r) => setTimeout(r, 1e3 * 2 ** attempt));
5765
5979
  }
5766
5980
  }
5767
5981
  }
5768
5982
  if (!reconnected) {
5769
5983
  conn.status = "error";
5770
- log("error", "Auto-reconnect failed", { namespace: route.namespace, error: lastErr.message });
5984
+ const lastErrMsg = lastErr instanceof Error ? lastErr.message : String(lastErr);
5985
+ log("error", "Auto-reconnect failed", { namespace: route.namespace, error: lastErrMsg });
5771
5986
  return {
5772
5987
  content: [
5773
5988
  {
5774
5989
  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.`
5990
+ text: `Server "${route.namespace}" disconnected and auto-reconnect failed: ${lastErrMsg}. Use mcp_connect_activate with server "${route.namespace}" to reload it manually.`
5776
5991
  }
5777
5992
  ],
5778
5993
  isError: true
@@ -5800,8 +6015,7 @@ var ConnectServer = class _ConnectServer {
5800
6015
  toolName: route.originalName,
5801
6016
  action: "tool_call",
5802
6017
  latencyMs,
5803
- success: !result.isError,
5804
- error: result.isError ? result.content[0]?.text : void 0
6018
+ success: !result.isError
5805
6019
  });
5806
6020
  if (!result.isError && Array.isArray(result.content)) {
5807
6021
  try {
@@ -5848,9 +6062,11 @@ var ConnectServer = class _ConnectServer {
5848
6062
  } catch {
5849
6063
  }
5850
6064
  }
6065
+ this.learning.recordDispatch(route.namespace);
6066
+ if (!result.isError) this.learning.recordSuccess(route.namespace);
6067
+ this.scheduleStateSave();
5851
6068
  if (!result.isError) {
5852
6069
  this.packDetector.recordCall(route.namespace, route.originalName, Date.now());
5853
- this.scheduleStateSave();
5854
6070
  }
5855
6071
  await this.trackUsageAndAutoDeactivate(route.namespace);
5856
6072
  }
@@ -5909,9 +6125,9 @@ var ConnectServer = class _ConnectServer {
5909
6125
  const shortlist = bm25.slice(0, _ConnectServer.BM25_TOP_K);
5910
6126
  const idByNamespace = new Map(servers.map((s) => [s.namespace, s.id]));
5911
6127
  const candidateIds = shortlist.map((r) => idByNamespace.get(r.namespace)).filter((id) => typeof id === "string" && id.length > 0);
5912
- if (candidateIds.length === 0) return shortlist;
6128
+ if (candidateIds.length === 0) return shortlist.map((r) => ({ ...r, hasRerank: false }));
5913
6129
  const rerankResults = await rerank(context, candidateIds);
5914
- if (!rerankResults) return shortlist;
6130
+ if (!rerankResults) return shortlist.map((r) => ({ ...r, hasRerank: false }));
5915
6131
  const namespaceById = new Map(servers.map((s) => [s.id, s.namespace]));
5916
6132
  const rerankScoreByNamespace = /* @__PURE__ */ new Map();
5917
6133
  for (const r of rerankResults) {
@@ -5931,7 +6147,7 @@ var ConnectServer = class _ConnectServer {
5931
6147
  if (a.hasRerank !== b.hasRerank) return a.hasRerank ? -1 : 1;
5932
6148
  return b.score - a.score;
5933
6149
  });
5934
- return reordered.map((r) => ({ namespace: r.namespace, score: r.score }));
6150
+ return reordered;
5935
6151
  }
5936
6152
  // Auto-warm confidence gate — applied to discover(context) so a single
5937
6153
  // clearly-winning server gets activated without the LLM needing to
@@ -5942,10 +6158,15 @@ var ConnectServer = class _ConnectServer {
5942
6158
  return raw === void 0 || raw === "" || raw === "1" || raw.toLowerCase() === "true";
5943
6159
  })();
5944
6160
  // 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;
6161
+ // be convincing before we auto-activate. Values are scale-dependent --
6162
+ // BM25 scores are unbounded positive numbers, rerank cosines are in
6163
+ // [0, 1] -- so we keep separate thresholds and pick based on whether
6164
+ // the top entry was reranked. Tuned by intuition; revisit when we
6165
+ // have real usage data.
6166
+ static AUTO_ACTIVATE_MIN_SCORE_BM25 = 1;
6167
+ static AUTO_ACTIVATE_MARGIN_BM25 = 1.3;
6168
+ static AUTO_ACTIVATE_MIN_SCORE_COSINE = 0.5;
6169
+ static AUTO_ACTIVATE_MARGIN_COSINE = 1.25;
5949
6170
  // Below this installed-server count, discover() appends a one-line
5950
6171
  // marketplace pointer so sparse-config users see where to add more.
5951
6172
  // At or above the threshold we stay silent — power users already know
@@ -5966,7 +6187,9 @@ var ConnectServer = class _ConnectServer {
5966
6187
  if (ranked.length === 0) return this.handleDiscover(context);
5967
6188
  const top = ranked[0];
5968
6189
  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);
6190
+ const minScore = top?.hasRerank ? _ConnectServer.AUTO_ACTIVATE_MIN_SCORE_COSINE : _ConnectServer.AUTO_ACTIVATE_MIN_SCORE_BM25;
6191
+ const margin = top?.hasRerank ? _ConnectServer.AUTO_ACTIVATE_MARGIN_COSINE : _ConnectServer.AUTO_ACTIVATE_MARGIN_BM25;
6192
+ const topWinsDecisively = top !== void 0 && top.score >= minScore && (second === void 0 || top.score / (second.score || 1e-6) >= margin);
5970
6193
  if (!topWinsDecisively || !top) return this.handleDiscover(context);
5971
6194
  const existing = this.connections.get(top.namespace);
5972
6195
  if (existing && existing.status === "connected") return this.handleDiscover(context);
@@ -6487,9 +6710,6 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
6487
6710
  results.push(`${winner.namespace} (score ${winner.score.toFixed(2)}): ${r.message}`);
6488
6711
  if (r.isChanged) anyChanged = true;
6489
6712
  if (!r.ok) anyError = true;
6490
- this.learning.recordDispatch(winner.namespace);
6491
- if (r.ok) this.learning.recordSuccess(winner.namespace);
6492
- this.scheduleStateSave();
6493
6713
  }
6494
6714
  if (anyChanged) {
6495
6715
  this.rebuildRoutes();
@@ -7551,13 +7771,24 @@ async function runUpgrade(opts = {}) {
7551
7771
  print("No upgrade command available for this install method.");
7552
7772
  return { exitCode: 0, lines };
7553
7773
  }
7774
+ const autoRunnable = method === "global-npm";
7554
7775
  if (!opts.run) {
7555
- print(`Run:
7776
+ if (autoRunnable) {
7777
+ print(`Run:
7778
+ ${plan.command}
7779
+
7780
+ Or re-run with --run to upgrade in place.`);
7781
+ } else {
7782
+ print(`Suggested command (run it yourself; --run only works for global-npm installs):
7556
7783
  ${plan.command}`);
7784
+ }
7557
7785
  return { exitCode: 1, lines };
7558
7786
  }
7559
- if (method !== "global-npm") {
7560
- printErr(`mcph upgrade --run: refusing to auto-run upgrade for method "${method}". Run manually: ${plan.command}`);
7787
+ if (!autoRunnable) {
7788
+ printErr(
7789
+ `mcph upgrade --run: install method "${method}" can't be upgraded automatically. Run manually:
7790
+ ${plan.command}`
7791
+ );
7561
7792
  return { exitCode: 2, lines };
7562
7793
  }
7563
7794
  const runner = opts.spawnImpl ?? defaultSpawn;
@@ -7572,7 +7803,7 @@ async function runUpgrade(opts = {}) {
7572
7803
  return { exitCode: 3, lines };
7573
7804
  }
7574
7805
  function readCurrentVersion() {
7575
- return true ? "0.47.0" : "dev";
7806
+ return true ? "0.47.2" : "dev";
7576
7807
  }
7577
7808
 
7578
7809
  // src/index.ts
@@ -7601,7 +7832,8 @@ if (subcommand === "compliance") {
7601
7832
  `);
7602
7833
  process.exit(2);
7603
7834
  }
7604
- runInstall(parsed.options).then((r) => process.exit(r.exitCode));
7835
+ const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR && process.env.CLAUDE_CONFIG_DIR.length > 0 ? process.env.CLAUDE_CONFIG_DIR : void 0;
7836
+ runInstall({ ...parsed.options, claudeConfigDir }).then((r) => process.exit(r.exitCode));
7605
7837
  } else if (subcommand === "doctor") {
7606
7838
  const doctorArgs = process.argv.slice(3);
7607
7839
  const doctorJson = doctorArgs.includes("--json");
@@ -7619,6 +7851,17 @@ if (subcommand === "compliance") {
7619
7851
  }
7620
7852
  runDoctor({ json: doctorJson }).then((r) => process.exit(r.exitCode));
7621
7853
  } else if (subcommand === "reset-learning") {
7854
+ const parsed = parseResetLearningArgs(process.argv.slice(3));
7855
+ if (parsed.kind === "help") {
7856
+ process.stdout.write(`${RESET_LEARNING_USAGE}
7857
+ `);
7858
+ process.exit(0);
7859
+ }
7860
+ if (parsed.kind === "error") {
7861
+ process.stderr.write(`${parsed.error}
7862
+ `);
7863
+ process.exit(2);
7864
+ }
7622
7865
  runResetLearning().then((r) => process.exit(r.exitCode));
7623
7866
  } else if (subcommand === "servers") {
7624
7867
  const parsed = parseServersArgs(process.argv.slice(3));
@@ -7728,7 +7971,7 @@ if (subcommand === "compliance") {
7728
7971
  `);
7729
7972
  process.exit(0);
7730
7973
  } else if (subcommand === "--version" || subcommand === "-V") {
7731
- process.stdout.write(`mcph ${true ? "0.47.0" : "dev"}
7974
+ process.stdout.write(`mcph ${true ? "0.47.2" : "dev"}
7732
7975
  `);
7733
7976
  process.exit(0);
7734
7977
  } else if (subcommand && !subcommand.startsWith("-")) {