@yawlabs/mcph 0.35.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 +8 -0
- package/dist/index.js +104 -65
- package/package.json +1 -1
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.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
|
+
|
|
9
|
+
## 0.36.0 — 2026-04-18
|
|
10
|
+
|
|
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.
|
|
12
|
+
|
|
5
13
|
## 0.35.0 — 2026-04-18
|
|
6
14
|
|
|
7
15
|
- **Inline reliability warning in `mcp_connect_discover`** — Discover now annotates dormant (not currently loaded) servers with `reliability: P% success across N past calls` when persisted learning shows ≥3 dispatches and <80% success. Renders under the server card right after the live health warning, so the LLM sees the flaky history *before* it picks a server to activate — not only after `handleHealth` surfaces it post-hoc. Thresholds match the cross-session reliability block from v0.34.0 so the two views stay consistent. Suppressed for loaded servers (the live per-call warning already covers them with fresher data).
|
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.
|
|
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);
|
|
@@ -2355,6 +2434,8 @@ function pushToolCall(history, record, limit = HISTORY_LIMIT) {
|
|
|
2355
2434
|
// src/learning.ts
|
|
2356
2435
|
var LEARNING_MIN_OBSERVATIONS = 3;
|
|
2357
2436
|
var LEARNING_MAX_BOOST = 1.1;
|
|
2437
|
+
var LEARNING_MIN_BOOST = 0.9;
|
|
2438
|
+
var PENALTY_RATE_THRESHOLD = 0.8;
|
|
2358
2439
|
var SATURATION_AT = 10;
|
|
2359
2440
|
var LearningStore = class {
|
|
2360
2441
|
usage = /* @__PURE__ */ new Map();
|
|
@@ -2377,13 +2458,23 @@ var LearningStore = class {
|
|
|
2377
2458
|
get(namespace) {
|
|
2378
2459
|
return this.usage.get(namespace);
|
|
2379
2460
|
}
|
|
2380
|
-
// Boost factor in [
|
|
2381
|
-
//
|
|
2382
|
-
//
|
|
2383
|
-
//
|
|
2461
|
+
// Boost factor in [LEARNING_MIN_BOOST, LEARNING_MAX_BOOST]. Penalty
|
|
2462
|
+
// branch wins when a namespace has been dispatched enough times and
|
|
2463
|
+
// its success rate has fallen below 80%; otherwise the positive
|
|
2464
|
+
// branch grows the factor with successful observation count, saturating
|
|
2465
|
+
// at SATURATION_AT successes so a heavily-used server can't runaway-win
|
|
2466
|
+
// against legitimately better matches.
|
|
2384
2467
|
boostFactor(namespace) {
|
|
2385
2468
|
const u = this.usage.get(namespace);
|
|
2386
|
-
if (!u
|
|
2469
|
+
if (!u) return 1;
|
|
2470
|
+
if (u.dispatched >= LEARNING_MIN_OBSERVATIONS) {
|
|
2471
|
+
const rate = u.succeeded / u.dispatched;
|
|
2472
|
+
if (rate < PENALTY_RATE_THRESHOLD) {
|
|
2473
|
+
const distance = Math.min(1, (PENALTY_RATE_THRESHOLD - rate) / PENALTY_RATE_THRESHOLD);
|
|
2474
|
+
return 1 - distance * (1 - LEARNING_MIN_BOOST);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
if (u.succeeded < LEARNING_MIN_OBSERVATIONS) return 1;
|
|
2387
2478
|
const progress = Math.min(1, u.succeeded / SATURATION_AT);
|
|
2388
2479
|
return 1 + progress * (LEARNING_MAX_BOOST - 1);
|
|
2389
2480
|
}
|
|
@@ -3922,7 +4013,7 @@ function categorizeSpawnError(err) {
|
|
|
3922
4013
|
}
|
|
3923
4014
|
async function connectToUpstream(config, onDisconnect, onListChanged) {
|
|
3924
4015
|
const client = new Client(
|
|
3925
|
-
{ name: "mcph", version: true ? "0.
|
|
4016
|
+
{ name: "mcph", version: true ? "0.37.0" : "dev" },
|
|
3926
4017
|
{ capabilities: {} }
|
|
3927
4018
|
);
|
|
3928
4019
|
let transport;
|
|
@@ -4320,51 +4411,6 @@ async function reportTools(serverId, tools) {
|
|
|
4320
4411
|
}
|
|
4321
4412
|
}
|
|
4322
4413
|
|
|
4323
|
-
// src/usage-hints.ts
|
|
4324
|
-
var MAX_PEERS = 3;
|
|
4325
|
-
var MIN_SUCCESS_TO_SHOW = 1;
|
|
4326
|
-
var RELIABILITY_MIN_OBSERVATIONS = 3;
|
|
4327
|
-
var RELIABILITY_THRESHOLD = 0.8;
|
|
4328
|
-
function buildCoUsageMap(packs) {
|
|
4329
|
-
const result = /* @__PURE__ */ new Map();
|
|
4330
|
-
for (const pack of packs) {
|
|
4331
|
-
for (const ns of pack.namespaces) {
|
|
4332
|
-
const bucket = result.get(ns) ?? /* @__PURE__ */ new Set();
|
|
4333
|
-
for (const peer of pack.namespaces) {
|
|
4334
|
-
if (peer !== ns) bucket.add(peer);
|
|
4335
|
-
}
|
|
4336
|
-
result.set(ns, bucket);
|
|
4337
|
-
}
|
|
4338
|
-
}
|
|
4339
|
-
const sorted = /* @__PURE__ */ new Map();
|
|
4340
|
-
for (const [ns, peers] of result) {
|
|
4341
|
-
sorted.set(ns, Array.from(peers).sort());
|
|
4342
|
-
}
|
|
4343
|
-
return sorted;
|
|
4344
|
-
}
|
|
4345
|
-
function formatUsageHint(usage, coUsedWith) {
|
|
4346
|
-
const parts = [];
|
|
4347
|
-
if (usage && usage.succeeded >= MIN_SUCCESS_TO_SHOW) {
|
|
4348
|
-
parts.push(`used ${usage.succeeded}x`);
|
|
4349
|
-
}
|
|
4350
|
-
if (coUsedWith.length > 0) {
|
|
4351
|
-
const shown = coUsedWith.slice(0, MAX_PEERS);
|
|
4352
|
-
const more = coUsedWith.length - shown.length;
|
|
4353
|
-
const names = shown.map((n) => `"${n}"`).join(", ");
|
|
4354
|
-
const tail = more > 0 ? ` +${more} more` : "";
|
|
4355
|
-
parts.push(`often loaded with ${names}${tail}`);
|
|
4356
|
-
}
|
|
4357
|
-
if (parts.length === 0) return null;
|
|
4358
|
-
return `usage: ${parts.join("; ")}`;
|
|
4359
|
-
}
|
|
4360
|
-
function formatReliabilityWarning(usage) {
|
|
4361
|
-
if (!usage || usage.dispatched < RELIABILITY_MIN_OBSERVATIONS) return null;
|
|
4362
|
-
const rate = usage.succeeded / usage.dispatched;
|
|
4363
|
-
if (rate >= RELIABILITY_THRESHOLD) return null;
|
|
4364
|
-
const pct = Math.round(rate * 100);
|
|
4365
|
-
return `reliability: ${pct}% success across ${usage.dispatched} past calls`;
|
|
4366
|
-
}
|
|
4367
|
-
|
|
4368
4414
|
// src/server.ts
|
|
4369
4415
|
var DEFAULT_POLL_INTERVAL_MS = 6e4;
|
|
4370
4416
|
function resolvePollIntervalMs() {
|
|
@@ -4448,7 +4494,7 @@ var ConnectServer = class _ConnectServer {
|
|
|
4448
4494
|
this.apiUrl = apiUrl6;
|
|
4449
4495
|
this.token = token6;
|
|
4450
4496
|
this.server = new Server(
|
|
4451
|
-
{ name: "mcph", version: true ? "0.
|
|
4497
|
+
{ name: "mcph", version: true ? "0.37.0" : "dev" },
|
|
4452
4498
|
{
|
|
4453
4499
|
capabilities: {
|
|
4454
4500
|
tools: { listChanged: true },
|
|
@@ -6207,17 +6253,10 @@ Use mcp_connect_discover to see imported servers.`
|
|
|
6207
6253
|
}
|
|
6208
6254
|
}
|
|
6209
6255
|
const now = Date.now();
|
|
6210
|
-
const flaky =
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
}).sort((a, b) => {
|
|
6215
|
-
const aRate = a.usage.succeeded / a.usage.dispatched;
|
|
6216
|
-
const bRate = b.usage.succeeded / b.usage.dispatched;
|
|
6217
|
-
if (aRate !== bRate) return aRate - bRate;
|
|
6218
|
-
if (a.usage.dispatched !== b.usage.dispatched) return b.usage.dispatched - a.usage.dispatched;
|
|
6219
|
-
return a.namespace.localeCompare(b.namespace);
|
|
6220
|
-
}).slice(0, 5);
|
|
6256
|
+
const flaky = selectFlakyNamespaces(
|
|
6257
|
+
this.learning.entries().filter(({ namespace }) => !this.connections.has(namespace)),
|
|
6258
|
+
5
|
|
6259
|
+
);
|
|
6221
6260
|
if (flaky.length > 0) {
|
|
6222
6261
|
lines.push("\nCross-session reliability (dormant, <80% success):");
|
|
6223
6262
|
for (const { namespace, usage } of flaky) {
|
|
@@ -6553,7 +6592,7 @@ ${installBlock}
|
|
|
6553
6592
|
);
|
|
6554
6593
|
process.exit(0);
|
|
6555
6594
|
} else if (subcommand === "--version" || subcommand === "-V") {
|
|
6556
|
-
process.stdout.write(`mcph ${true ? "0.
|
|
6595
|
+
process.stdout.write(`mcph ${true ? "0.37.0" : "dev"}
|
|
6557
6596
|
`);
|
|
6558
6597
|
process.exit(0);
|
|
6559
6598
|
} else if (subcommand && !subcommand.startsWith("-")) {
|
package/package.json
CHANGED