@yawlabs/mcph 0.36.0 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to `@yawlabs/mcph` are documented here. This project uses [semantic versioning](https://semver.org) and a CI-gated release flow: pushing a `vX.Y.Z` tag triggers `.github/workflows/release.yml`, which publishes to npm.
4
4
 
5
+ ## 0.38.0 — 2026-04-18
6
+
7
+ - **`mcph reset-learning` CLI subcommand** — Deletes `~/.mcph/state.json` so cross-session learning starts fresh; prints the entry counts that were cleared. Pairs with v0.37.0's doctor RELIABILITY section: once a namespace has been flagged flaky, the dispatch penalty branch (v0.36.0) keeps suppressing it until enough new successes pile up — but if the user has since fixed the underlying cause (rotated a token, swapped the upstream, re-authed), that history is stale and the penalty has overstayed its welcome. This gives them a direct CLI lever to clear it. Scope is all-or-nothing by design; a per-namespace flag is footgunny (user clears one, forgets the others, keeps getting silently mis-ranked). No-op with an explanatory message when `MCPH_DISABLE_PERSISTENCE` is set or the file doesn't exist, so `mcph reset-learning` never surprises. Exit 0 on success or no-op, exit 1 on I/O error (permissions, disk).
8
+
9
+ ## 0.37.0 — 2026-04-18
10
+
11
+ - **`mcph doctor` RELIABILITY section** — New block surfaces flaky dormant namespaces pulled directly from `~/.mcph/state.json`, using the same ≥3-dispatches / <80%-success definition as `mcp_connect_health`'s cross-session reliability block — so the CLI diagnostic and the LLM-facing health tool agree on what "flaky" means. Sorted worst-rate first, capped at 5. Silently omitted when no namespace qualifies, state.json doesn't exist yet, or `MCPH_DISABLE_PERSISTENCE` is set. Threshold constants + sort logic extracted into `selectFlakyNamespaces` so handleHealth and doctor can't drift apart.
12
+
5
13
  ## 0.36.0 — 2026-04-18
6
14
 
7
15
  - **Negative signal in dispatch ranking (`boostFactor` penalty branch)** — The learning store's `boostFactor` now drops *below* 1.0 for namespaces with flaky history, mirroring the existing upward boost. Threshold is the same ≥3 dispatches / <80% success gate used by discover's inline reliability warning (v0.35.0) and health's cross-session block (v0.34.0) — so a server flagged flaky in those views also loses rank points at dispatch time rather than quietly continuing to win routing. Floor is `-10%` (`LEARNING_MIN_BOOST = 0.9`), symmetric with the existing `+10%` ceiling. Rate-based signal trumps count-based: a namespace with 10 successes but a 50% overall rate is flaky, not useful, and the penalty branch beats the positive branch in that case.
package/dist/index.js CHANGED
@@ -945,8 +945,66 @@ function errorMessage(err) {
945
945
  return err instanceof Error ? err.message : String(err);
946
946
  }
947
947
 
948
+ // src/usage-hints.ts
949
+ var MAX_PEERS = 3;
950
+ var MIN_SUCCESS_TO_SHOW = 1;
951
+ var RELIABILITY_MIN_OBSERVATIONS = 3;
952
+ var RELIABILITY_THRESHOLD = 0.8;
953
+ function buildCoUsageMap(packs) {
954
+ const result = /* @__PURE__ */ new Map();
955
+ for (const pack of packs) {
956
+ for (const ns of pack.namespaces) {
957
+ const bucket = result.get(ns) ?? /* @__PURE__ */ new Set();
958
+ for (const peer of pack.namespaces) {
959
+ if (peer !== ns) bucket.add(peer);
960
+ }
961
+ result.set(ns, bucket);
962
+ }
963
+ }
964
+ const sorted = /* @__PURE__ */ new Map();
965
+ for (const [ns, peers] of result) {
966
+ sorted.set(ns, Array.from(peers).sort());
967
+ }
968
+ return sorted;
969
+ }
970
+ function formatUsageHint(usage, coUsedWith) {
971
+ const parts = [];
972
+ if (usage && usage.succeeded >= MIN_SUCCESS_TO_SHOW) {
973
+ parts.push(`used ${usage.succeeded}x`);
974
+ }
975
+ if (coUsedWith.length > 0) {
976
+ const shown = coUsedWith.slice(0, MAX_PEERS);
977
+ const more = coUsedWith.length - shown.length;
978
+ const names = shown.map((n) => `"${n}"`).join(", ");
979
+ const tail = more > 0 ? ` +${more} more` : "";
980
+ parts.push(`often loaded with ${names}${tail}`);
981
+ }
982
+ if (parts.length === 0) return null;
983
+ return `usage: ${parts.join("; ")}`;
984
+ }
985
+ function formatReliabilityWarning(usage) {
986
+ if (!usage || usage.dispatched < RELIABILITY_MIN_OBSERVATIONS) return null;
987
+ const rate = usage.succeeded / usage.dispatched;
988
+ if (rate >= RELIABILITY_THRESHOLD) return null;
989
+ const pct = Math.round(rate * 100);
990
+ return `reliability: ${pct}% success across ${usage.dispatched} past calls`;
991
+ }
992
+ function selectFlakyNamespaces(entries, limit) {
993
+ if (limit <= 0) return [];
994
+ return Array.from(entries).filter(({ usage }) => {
995
+ if (usage.dispatched < RELIABILITY_MIN_OBSERVATIONS) return false;
996
+ return usage.succeeded / usage.dispatched < RELIABILITY_THRESHOLD;
997
+ }).sort((a, b) => {
998
+ const aRate = a.usage.succeeded / a.usage.dispatched;
999
+ const bRate = b.usage.succeeded / b.usage.dispatched;
1000
+ if (aRate !== bRate) return aRate - bRate;
1001
+ if (a.usage.dispatched !== b.usage.dispatched) return b.usage.dispatched - a.usage.dispatched;
1002
+ return a.namespace.localeCompare(b.namespace);
1003
+ }).slice(0, limit);
1004
+ }
1005
+
948
1006
  // src/doctor-cmd.ts
949
- var VERSION = true ? "0.36.0" : "dev";
1007
+ var VERSION = true ? "0.38.0" : "dev";
950
1008
  async function runDoctor(opts = {}) {
951
1009
  const lines = [];
952
1010
  const write = opts.out ?? ((s) => process.stdout.write(s));
@@ -983,6 +1041,7 @@ async function runDoctor(opts = {}) {
983
1041
  print("");
984
1042
  renderEnvSection({ env, print });
985
1043
  await renderStateSection({ home, env, print });
1044
+ await renderReliabilitySection({ home, env, print });
986
1045
  const clients = probeClients({ home, os, cwd });
987
1046
  print("INSTALLED CLIENTS (probed config files)");
988
1047
  for (const c of clients) {
@@ -1075,6 +1134,26 @@ async function renderStateSection(opts) {
1075
1134
  }
1076
1135
  print("");
1077
1136
  }
1137
+ async function renderReliabilitySection(opts) {
1138
+ const { home, env, print } = opts;
1139
+ const raw = env.MCPH_DISABLE_PERSISTENCE;
1140
+ const disabled = raw !== void 0 && raw !== "" && (raw === "1" || raw.toLowerCase() === "true");
1141
+ if (disabled) return;
1142
+ const filePath = join4(userConfigDir(home), STATE_FILENAME);
1143
+ const persisted = await loadState(filePath);
1144
+ if (persisted.savedAt === 0) return;
1145
+ const entries = Object.entries(persisted.learning).map(([namespace, usage]) => ({ namespace, usage }));
1146
+ const flaky = selectFlakyNamespaces(entries, 5);
1147
+ if (flaky.length === 0) return;
1148
+ print("RELIABILITY (dormant, <80% success)");
1149
+ const now = Date.now();
1150
+ for (const { namespace, usage } of flaky) {
1151
+ const rate = Math.round(usage.succeeded / usage.dispatched * 100);
1152
+ const age = formatRelativeAge(now - usage.lastUsedAt);
1153
+ print(` ${namespace} \u2014 ${usage.dispatched} calls, ${rate}% success, last used ${age} ago`);
1154
+ }
1155
+ print("");
1156
+ }
1078
1157
  function formatRelativeAge(ms) {
1079
1158
  const clamped = Math.max(0, ms);
1080
1159
  const s = Math.floor(clamped / 1e3);
@@ -1712,9 +1791,61 @@ ${USAGE}` };
1712
1791
  }
1713
1792
  var INSTALL_USAGE = USAGE;
1714
1793
 
1794
+ // src/reset-learning-cmd.ts
1795
+ import { unlink } from "fs/promises";
1796
+ import { homedir as homedir6 } from "os";
1797
+ import { join as join6 } from "path";
1798
+ async function runResetLearning(opts = {}) {
1799
+ const home = opts.home ?? homedir6();
1800
+ const env = opts.env ?? process.env;
1801
+ const write = opts.out ?? ((s) => process.stdout.write(s));
1802
+ const writeErr = opts.err ?? ((s) => process.stderr.write(s));
1803
+ const lines = [];
1804
+ const print = (s = "") => {
1805
+ lines.push(s);
1806
+ write(`${s}
1807
+ `);
1808
+ };
1809
+ const printErr = (s) => {
1810
+ lines.push(s);
1811
+ writeErr(`${s}
1812
+ `);
1813
+ };
1814
+ const filePath = join6(userConfigDir(home), STATE_FILENAME);
1815
+ const raw = env.MCPH_DISABLE_PERSISTENCE;
1816
+ const disabled = raw !== void 0 && raw !== "" && (raw === "1" || raw.toLowerCase() === "true");
1817
+ if (disabled) {
1818
+ print("mcph reset-learning: persistence is disabled (MCPH_DISABLE_PERSISTENCE) \u2014 nothing to clear.");
1819
+ return { exitCode: 0, lines, removed: false, path: filePath };
1820
+ }
1821
+ const persisted = await loadState(filePath);
1822
+ const learningCount = Object.keys(persisted.learning).length;
1823
+ const packCount = persisted.packHistory.length;
1824
+ try {
1825
+ await unlink(filePath);
1826
+ } catch (err) {
1827
+ if (isFileNotFound2(err)) {
1828
+ print("mcph reset-learning: no persisted state to reset.");
1829
+ print(` path: ${filePath}`);
1830
+ return { exitCode: 0, lines, removed: false, path: filePath };
1831
+ }
1832
+ const msg = err instanceof Error ? err.message : String(err);
1833
+ printErr(`mcph reset-learning: failed to remove ${filePath}: ${msg}`);
1834
+ return { exitCode: 1, lines, removed: false, path: filePath };
1835
+ }
1836
+ print("mcph reset-learning: cleared persisted state.");
1837
+ print(` path: ${filePath}`);
1838
+ print(` learning entries removed: ${learningCount}`);
1839
+ print(` pack history entries removed: ${packCount}`);
1840
+ return { exitCode: 0, lines, removed: true, path: filePath };
1841
+ }
1842
+ function isFileNotFound2(err) {
1843
+ return !!err && typeof err === "object" && "code" in err && err.code === "ENOENT";
1844
+ }
1845
+
1715
1846
  // src/server.ts
1716
1847
  import { readFile as readFile6 } from "fs/promises";
1717
- import { homedir as homedir6 } from "os";
1848
+ import { homedir as homedir7 } from "os";
1718
1849
  import { isAbsolute, relative, resolve as resolve3 } from "path";
1719
1850
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1720
1851
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -3934,7 +4065,7 @@ function categorizeSpawnError(err) {
3934
4065
  }
3935
4066
  async function connectToUpstream(config, onDisconnect, onListChanged) {
3936
4067
  const client = new Client(
3937
- { name: "mcph", version: true ? "0.36.0" : "dev" },
4068
+ { name: "mcph", version: true ? "0.38.0" : "dev" },
3938
4069
  { capabilities: {} }
3939
4070
  );
3940
4071
  let transport;
@@ -4332,51 +4463,6 @@ async function reportTools(serverId, tools) {
4332
4463
  }
4333
4464
  }
4334
4465
 
4335
- // src/usage-hints.ts
4336
- var MAX_PEERS = 3;
4337
- var MIN_SUCCESS_TO_SHOW = 1;
4338
- var RELIABILITY_MIN_OBSERVATIONS = 3;
4339
- var RELIABILITY_THRESHOLD = 0.8;
4340
- function buildCoUsageMap(packs) {
4341
- const result = /* @__PURE__ */ new Map();
4342
- for (const pack of packs) {
4343
- for (const ns of pack.namespaces) {
4344
- const bucket = result.get(ns) ?? /* @__PURE__ */ new Set();
4345
- for (const peer of pack.namespaces) {
4346
- if (peer !== ns) bucket.add(peer);
4347
- }
4348
- result.set(ns, bucket);
4349
- }
4350
- }
4351
- const sorted = /* @__PURE__ */ new Map();
4352
- for (const [ns, peers] of result) {
4353
- sorted.set(ns, Array.from(peers).sort());
4354
- }
4355
- return sorted;
4356
- }
4357
- function formatUsageHint(usage, coUsedWith) {
4358
- const parts = [];
4359
- if (usage && usage.succeeded >= MIN_SUCCESS_TO_SHOW) {
4360
- parts.push(`used ${usage.succeeded}x`);
4361
- }
4362
- if (coUsedWith.length > 0) {
4363
- const shown = coUsedWith.slice(0, MAX_PEERS);
4364
- const more = coUsedWith.length - shown.length;
4365
- const names = shown.map((n) => `"${n}"`).join(", ");
4366
- const tail = more > 0 ? ` +${more} more` : "";
4367
- parts.push(`often loaded with ${names}${tail}`);
4368
- }
4369
- if (parts.length === 0) return null;
4370
- return `usage: ${parts.join("; ")}`;
4371
- }
4372
- function formatReliabilityWarning(usage) {
4373
- if (!usage || usage.dispatched < RELIABILITY_MIN_OBSERVATIONS) return null;
4374
- const rate = usage.succeeded / usage.dispatched;
4375
- if (rate >= RELIABILITY_THRESHOLD) return null;
4376
- const pct = Math.round(rate * 100);
4377
- return `reliability: ${pct}% success across ${usage.dispatched} past calls`;
4378
- }
4379
-
4380
4466
  // src/server.ts
4381
4467
  var DEFAULT_POLL_INTERVAL_MS = 6e4;
4382
4468
  function resolvePollIntervalMs() {
@@ -4460,7 +4546,7 @@ var ConnectServer = class _ConnectServer {
4460
4546
  this.apiUrl = apiUrl6;
4461
4547
  this.token = token6;
4462
4548
  this.server = new Server(
4463
- { name: "mcph", version: true ? "0.36.0" : "dev" },
4549
+ { name: "mcph", version: true ? "0.38.0" : "dev" },
4464
4550
  {
4465
4551
  capabilities: {
4466
4552
  tools: { listChanged: true },
@@ -5895,7 +5981,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
5895
5981
  }
5896
5982
  const ALLOWED_FILENAMES = ["claude_desktop_config.json", "mcp.json", "settings.json", "mcp_config.json"];
5897
5983
  try {
5898
- const resolved = filepath.startsWith("~/") || filepath.startsWith("~\\") ? resolve3(homedir6(), filepath.slice(2)) : resolve3(filepath);
5984
+ const resolved = filepath.startsWith("~/") || filepath.startsWith("~\\") ? resolve3(homedir7(), filepath.slice(2)) : resolve3(filepath);
5899
5985
  const resolvedBasename = resolved.split(/[/\\]/).pop() || "";
5900
5986
  if (!ALLOWED_FILENAMES.includes(resolvedBasename)) {
5901
5987
  return {
@@ -5912,7 +5998,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
5912
5998
  const rel = relative(base, p);
5913
5999
  return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
5914
6000
  };
5915
- if (!isUnder(homedir6(), resolved) && !isUnder(process.cwd(), resolved)) {
6001
+ if (!isUnder(homedir7(), resolved) && !isUnder(process.cwd(), resolved)) {
5916
6002
  return {
5917
6003
  content: [
5918
6004
  { type: "text", text: "Import path must be under your home directory or the current working directory." }
@@ -6219,17 +6305,10 @@ Use mcp_connect_discover to see imported servers.`
6219
6305
  }
6220
6306
  }
6221
6307
  const now = Date.now();
6222
- const flaky = this.learning.entries().filter(({ namespace, usage }) => {
6223
- if (this.connections.has(namespace)) return false;
6224
- if (usage.dispatched < 3) return false;
6225
- return usage.succeeded / usage.dispatched < 0.8;
6226
- }).sort((a, b) => {
6227
- const aRate = a.usage.succeeded / a.usage.dispatched;
6228
- const bRate = b.usage.succeeded / b.usage.dispatched;
6229
- if (aRate !== bRate) return aRate - bRate;
6230
- if (a.usage.dispatched !== b.usage.dispatched) return b.usage.dispatched - a.usage.dispatched;
6231
- return a.namespace.localeCompare(b.namespace);
6232
- }).slice(0, 5);
6308
+ const flaky = selectFlakyNamespaces(
6309
+ this.learning.entries().filter(({ namespace }) => !this.connections.has(namespace)),
6310
+ 5
6311
+ );
6233
6312
  if (flaky.length > 0) {
6234
6313
  lines.push("\nCross-session reliability (dormant, <80% success):");
6235
6314
  for (const { namespace, usage } of flaky) {
@@ -6520,7 +6599,17 @@ To load the top pack in one step, call \`mcp_connect_activate\` with namespaces=
6520
6599
  };
6521
6600
 
6522
6601
  // src/index.ts
6523
- var KNOWN_SUBCOMMANDS = ["compliance", "install", "doctor", "help", "--help", "-h", "--version", "-V"];
6602
+ var KNOWN_SUBCOMMANDS = [
6603
+ "compliance",
6604
+ "install",
6605
+ "doctor",
6606
+ "reset-learning",
6607
+ "help",
6608
+ "--help",
6609
+ "-h",
6610
+ "--version",
6611
+ "-V"
6612
+ ];
6524
6613
  var subcommand = process.argv[2];
6525
6614
  if (subcommand === "compliance") {
6526
6615
  runComplianceCommand(process.argv.slice(3)).then((code) => process.exit(code));
@@ -6534,6 +6623,8 @@ if (subcommand === "compliance") {
6534
6623
  runInstall(parsed.options).then((r) => process.exit(r.exitCode));
6535
6624
  } else if (subcommand === "doctor") {
6536
6625
  runDoctor().then((r) => process.exit(r.exitCode));
6626
+ } else if (subcommand === "reset-learning") {
6627
+ runResetLearning().then((r) => process.exit(r.exitCode));
6537
6628
  } else if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
6538
6629
  const installBlock = ` ${INSTALL_USAGE.replace(/^Usage: /, "").replace(/\n/g, "\n ")}`;
6539
6630
  process.stdout.write(
@@ -6545,6 +6636,7 @@ if (subcommand === "compliance") {
6545
6636
  mcph install <client> [flags] Auto-edit an MCP client's config to launch mcph
6546
6637
  mcph doctor Print loaded config + detected clients (support diagnostic)
6547
6638
  mcph compliance <target> [flags] Run the compliance suite against an MCP server
6639
+ mcph reset-learning Clear cross-session learning history (~/.mcph/state.json)
6548
6640
  mcph --version Print version
6549
6641
 
6550
6642
  Install:
@@ -6565,7 +6657,7 @@ ${installBlock}
6565
6657
  );
6566
6658
  process.exit(0);
6567
6659
  } else if (subcommand === "--version" || subcommand === "-V") {
6568
- process.stdout.write(`mcph ${true ? "0.36.0" : "dev"}
6660
+ process.stdout.write(`mcph ${true ? "0.38.0" : "dev"}
6569
6661
  `);
6570
6662
  process.exit(0);
6571
6663
  } else if (subcommand && !subcommand.startsWith("-")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/mcph",
3
- "version": "0.36.0",
3
+ "version": "0.38.0",
4
4
  "description": "mcp.hosting — one install, all your MCP servers, managed from the cloud",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Yaw Labs <contact@yaw.sh> (https://yaw.sh)",