@yawlabs/mcph 0.36.0 → 0.37.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,10 @@
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.37.0 — 2026-04-18
6
+
7
+ - **`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.
8
+
5
9
  ## 0.36.0 — 2026-04-18
6
10
 
7
11
  - **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.37.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);
@@ -3934,7 +4013,7 @@ function categorizeSpawnError(err) {
3934
4013
  }
3935
4014
  async function connectToUpstream(config, onDisconnect, onListChanged) {
3936
4015
  const client = new Client(
3937
- { name: "mcph", version: true ? "0.36.0" : "dev" },
4016
+ { name: "mcph", version: true ? "0.37.0" : "dev" },
3938
4017
  { capabilities: {} }
3939
4018
  );
3940
4019
  let transport;
@@ -4332,51 +4411,6 @@ async function reportTools(serverId, tools) {
4332
4411
  }
4333
4412
  }
4334
4413
 
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
4414
  // src/server.ts
4381
4415
  var DEFAULT_POLL_INTERVAL_MS = 6e4;
4382
4416
  function resolvePollIntervalMs() {
@@ -4460,7 +4494,7 @@ var ConnectServer = class _ConnectServer {
4460
4494
  this.apiUrl = apiUrl6;
4461
4495
  this.token = token6;
4462
4496
  this.server = new Server(
4463
- { name: "mcph", version: true ? "0.36.0" : "dev" },
4497
+ { name: "mcph", version: true ? "0.37.0" : "dev" },
4464
4498
  {
4465
4499
  capabilities: {
4466
4500
  tools: { listChanged: true },
@@ -6219,17 +6253,10 @@ Use mcp_connect_discover to see imported servers.`
6219
6253
  }
6220
6254
  }
6221
6255
  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);
6256
+ const flaky = selectFlakyNamespaces(
6257
+ this.learning.entries().filter(({ namespace }) => !this.connections.has(namespace)),
6258
+ 5
6259
+ );
6233
6260
  if (flaky.length > 0) {
6234
6261
  lines.push("\nCross-session reliability (dormant, <80% success):");
6235
6262
  for (const { namespace, usage } of flaky) {
@@ -6565,7 +6592,7 @@ ${installBlock}
6565
6592
  );
6566
6593
  process.exit(0);
6567
6594
  } else if (subcommand === "--version" || subcommand === "-V") {
6568
- process.stdout.write(`mcph ${true ? "0.36.0" : "dev"}
6595
+ process.stdout.write(`mcph ${true ? "0.37.0" : "dev"}
6569
6596
  `);
6570
6597
  process.exit(0);
6571
6598
  } 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.37.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)",