@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 +4 -0
- package/dist/index.js +87 -60
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
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.
|
|
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