@yawlabs/mcph 0.32.0 → 0.34.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.34.0 — 2026-04-18
6
+
7
+ - **Cross-session reliability block in `mcp_connect_health`** — New section at the bottom of health output surfaces flaky *dormant* namespaces pulled from persisted learning: `<namespace> — N calls, P% success, last used <age> ago`. Threshold is deliberately high (≥3 dispatches, <80% success) so a one-off failure doesn't light up the panel; loaded namespaces are skipped (in-session block already covers them). Sorted worst-rate first, ties broken by most calls then alpha; capped at 5. Also fixes a gap where `handleHealth` returned early on an empty-connections session and never showed dormant history — now it falls through so operators can see which past servers were unreliable even before loading anything.
8
+
9
+ ## 0.33.0 — 2026-04-18
10
+
11
+ - **`mcph doctor` ENVIRONMENT section** — New block enumerating every behavior-modifier env var mcph actually reads (`MCPH_POLL_INTERVAL`, `MCPH_SERVER_CAP`, `MCPH_MIN_COMPLIANCE`, `MCPH_AUTO_LOAD`, `MCPH_PRUNE_RESPONSES`). Each shows its current value, or `(not set — <default>)` when unset. Closes a diagnostic gap where users reporting "my server cap isn't taking effect" or "compliance filter isn't blocking anything" had no doctor signal on whether the knob was even set. TOKEN / URL / DISABLE_PERSISTENCE still get their dedicated sections (richer context there).
12
+
5
13
  ## 0.32.0 — 2026-04-18
6
14
 
7
15
  - **Unknown CLI subcommand detection + typo suggestions** — `mcph <typo>` (e.g. `mcph instal`, `mcph docto`) now exits 2 with `unknown subcommand "X". Did you mean: install?` instead of silently falling through to MCP-server mode and erroring opaquely on the missing token. Bare flags (anything with a leading `-`) still fall through so server startup can parse them.
package/dist/index.js CHANGED
@@ -946,7 +946,7 @@ function errorMessage(err) {
946
946
  }
947
947
 
948
948
  // src/doctor-cmd.ts
949
- var VERSION = true ? "0.32.0" : "dev";
949
+ var VERSION = true ? "0.34.0" : "dev";
950
950
  async function runDoctor(opts = {}) {
951
951
  const lines = [];
952
952
  const write = opts.out ?? ((s) => process.stdout.write(s));
@@ -981,6 +981,7 @@ async function runDoctor(opts = {}) {
981
981
  print(` value: ${config.apiBase}`);
982
982
  print(` source: ${config.apiBaseSource}`);
983
983
  print("");
984
+ renderEnvSection({ env, print });
984
985
  await renderStateSection({ home, env, print });
985
986
  const clients = probeClients({ home, os, cwd });
986
987
  print("INSTALLED CLIENTS (probed config files)");
@@ -1034,6 +1035,24 @@ async function runDoctor(opts = {}) {
1034
1035
  }
1035
1036
  return { exitCode, lines, snapshot: { version: VERSION, config, clients } };
1036
1037
  }
1038
+ function renderEnvSection(opts) {
1039
+ const { env, print } = opts;
1040
+ const vars = [
1041
+ { name: "MCPH_POLL_INTERVAL", defaultHint: "default 60s" },
1042
+ { name: "MCPH_SERVER_CAP", defaultHint: "default 6" },
1043
+ { name: "MCPH_MIN_COMPLIANCE", defaultHint: "filter inactive" },
1044
+ { name: "MCPH_AUTO_LOAD", defaultHint: "auto-load inactive" },
1045
+ { name: "MCPH_PRUNE_RESPONSES", defaultHint: "pruning active" }
1046
+ ];
1047
+ const widest = vars.reduce((m, v) => Math.max(m, v.name.length), 0);
1048
+ print("ENVIRONMENT (behavior overrides)");
1049
+ for (const v of vars) {
1050
+ const raw = env[v.name];
1051
+ const value = raw === void 0 || raw === "" ? `(not set \u2014 ${v.defaultHint})` : raw;
1052
+ print(` ${v.name.padEnd(widest)} ${value}`);
1053
+ }
1054
+ print("");
1055
+ }
1037
1056
  async function renderStateSection(opts) {
1038
1057
  const { home, env, print } = opts;
1039
1058
  const raw = env.MCPH_DISABLE_PERSISTENCE;
@@ -2383,6 +2402,19 @@ var LearningStore = class {
2383
2402
  }
2384
2403
  return out;
2385
2404
  }
2405
+ // Iterate the current store as { namespace, usage } pairs. Used by
2406
+ // observability paths (e.g., mcp_connect_health's cross-session
2407
+ // reliability block) that need to walk every recorded namespace.
2408
+ entries() {
2409
+ const out = [];
2410
+ for (const [ns, u] of this.usage) {
2411
+ out.push({
2412
+ namespace: ns,
2413
+ usage: { dispatched: u.dispatched, succeeded: u.succeeded, lastUsedAt: u.lastUsedAt }
2414
+ });
2415
+ }
2416
+ return out;
2417
+ }
2386
2418
  // Replace in-memory state with the given snapshot. Used on startup
2387
2419
  // to restore persisted signal; silently overwrites anything already
2388
2420
  // in the store, so callers should only invoke this before recording.
@@ -3890,7 +3922,7 @@ function categorizeSpawnError(err) {
3890
3922
  }
3891
3923
  async function connectToUpstream(config, onDisconnect, onListChanged) {
3892
3924
  const client = new Client(
3893
- { name: "mcph", version: true ? "0.32.0" : "dev" },
3925
+ { name: "mcph", version: true ? "0.34.0" : "dev" },
3894
3926
  { capabilities: {} }
3895
3927
  );
3896
3928
  let transport;
@@ -4407,7 +4439,7 @@ var ConnectServer = class _ConnectServer {
4407
4439
  this.apiUrl = apiUrl6;
4408
4440
  this.token = token6;
4409
4441
  this.server = new Server(
4410
- { name: "mcph", version: true ? "0.32.0" : "dev" },
4442
+ { name: "mcph", version: true ? "0.34.0" : "dev" },
4411
4443
  {
4412
4444
  capabilities: {
4413
4445
  tools: { listChanged: true },
@@ -6142,23 +6174,43 @@ Use mcp_connect_discover to see imported servers.`
6142
6174
  }
6143
6175
  if (this.connections.size === 0) {
6144
6176
  lines.push("No servers loaded in this session yet.");
6145
- return { content: [{ type: "text", text: lines.join("\n") }] };
6146
- }
6147
- lines.push("Session health:\n");
6148
- for (const [namespace, conn] of this.connections) {
6149
- const h = conn.health;
6150
- const avgLatency = h.totalCalls > 0 ? Math.round(h.totalLatencyMs / h.totalCalls) : 0;
6151
- const errorRate = h.totalCalls > 0 ? Math.round(h.errorCount / h.totalCalls * 100) : 0;
6152
- const idleCount = this.idleCallCounts.get(namespace) ?? 0;
6153
- const idleLimit = adaptiveThreshold(namespace, this.recentToolCalls, _ConnectServer.IDLE_CALL_THRESHOLD);
6154
- const toolNames = conn.tools.map((t) => t.name).join(", ");
6155
- lines.push(` ${namespace} [${conn.status}] (${conn.config.type})`);
6156
- lines.push(` tools: ${conn.tools.length} \u2014 ${toolNames}`);
6157
- lines.push(` calls: ${h.totalCalls}, errors: ${h.errorCount} (${errorRate}%)`);
6158
- lines.push(` avg latency: ${avgLatency}ms`);
6159
- lines.push(` idle: ${idleCount}/${idleLimit} until auto-unload`);
6160
- if (h.lastErrorMessage) {
6161
- lines.push(` last error: ${h.lastErrorMessage} at ${h.lastErrorAt}`);
6177
+ } else {
6178
+ lines.push("Session health:\n");
6179
+ for (const [namespace, conn] of this.connections) {
6180
+ const h = conn.health;
6181
+ const avgLatency = h.totalCalls > 0 ? Math.round(h.totalLatencyMs / h.totalCalls) : 0;
6182
+ const errorRate = h.totalCalls > 0 ? Math.round(h.errorCount / h.totalCalls * 100) : 0;
6183
+ const idleCount = this.idleCallCounts.get(namespace) ?? 0;
6184
+ const idleLimit = adaptiveThreshold(namespace, this.recentToolCalls, _ConnectServer.IDLE_CALL_THRESHOLD);
6185
+ const toolNames = conn.tools.map((t) => t.name).join(", ");
6186
+ lines.push(` ${namespace} [${conn.status}] (${conn.config.type})`);
6187
+ lines.push(` tools: ${conn.tools.length} \u2014 ${toolNames}`);
6188
+ lines.push(` calls: ${h.totalCalls}, errors: ${h.errorCount} (${errorRate}%)`);
6189
+ lines.push(` avg latency: ${avgLatency}ms`);
6190
+ lines.push(` idle: ${idleCount}/${idleLimit} until auto-unload`);
6191
+ if (h.lastErrorMessage) {
6192
+ lines.push(` last error: ${h.lastErrorMessage} at ${h.lastErrorAt}`);
6193
+ }
6194
+ }
6195
+ }
6196
+ const now = Date.now();
6197
+ const flaky = this.learning.entries().filter(({ namespace, usage }) => {
6198
+ if (this.connections.has(namespace)) return false;
6199
+ if (usage.dispatched < 3) return false;
6200
+ return usage.succeeded / usage.dispatched < 0.8;
6201
+ }).sort((a, b) => {
6202
+ const aRate = a.usage.succeeded / a.usage.dispatched;
6203
+ const bRate = b.usage.succeeded / b.usage.dispatched;
6204
+ if (aRate !== bRate) return aRate - bRate;
6205
+ if (a.usage.dispatched !== b.usage.dispatched) return b.usage.dispatched - a.usage.dispatched;
6206
+ return a.namespace.localeCompare(b.namespace);
6207
+ }).slice(0, 5);
6208
+ if (flaky.length > 0) {
6209
+ lines.push("\nCross-session reliability (dormant, <80% success):");
6210
+ for (const { namespace, usage } of flaky) {
6211
+ const rate = Math.round(usage.succeeded / usage.dispatched * 100);
6212
+ const age = formatRelativeAge(now - usage.lastUsedAt);
6213
+ lines.push(` ${namespace} \u2014 ${usage.dispatched} calls, ${rate}% success, last used ${age} ago`);
6162
6214
  }
6163
6215
  }
6164
6216
  return { content: [{ type: "text", text: lines.join("\n") }] };
@@ -6488,7 +6540,7 @@ ${installBlock}
6488
6540
  );
6489
6541
  process.exit(0);
6490
6542
  } else if (subcommand === "--version" || subcommand === "-V") {
6491
- process.stdout.write(`mcph ${true ? "0.32.0" : "dev"}
6543
+ process.stdout.write(`mcph ${true ? "0.34.0" : "dev"}
6492
6544
  `);
6493
6545
  process.exit(0);
6494
6546
  } else if (subcommand && !subcommand.startsWith("-")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/mcph",
3
- "version": "0.32.0",
3
+ "version": "0.34.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)",