@yawlabs/mcph 0.28.1 → 0.30.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.30.0 — 2026-04-18
6
+
7
+ - **Inline bundle completions in `discover()`** — When a curated bundle has some installed servers but is missing one or two, `mcp_connect_discover` surfaces a "Bundle completions" block with the partial bundle id, what's already installed, and what to add. Top 3 entries, ranked by fewest-missing first (cheapest to complete), tie-broken by most-momentum then id. Same data source as `mcp_connect_bundles action="match"`, but inline so the model can act on the nudge without the extra round-trip. Suppressed when no curated bundle has any overlap with the installed set.
8
+
9
+ ## 0.29.0 — 2026-04-18
10
+
11
+ - **Compliance-aware routing (`MCPH_MIN_COMPLIANCE`)** — Phase 3 item. Set the env var to `A`, `B`, `C`, `D`, or `F` and `mcp_connect_activate` refuses to load any installed server whose reported `complianceGrade` is below the floor, with an error that names the grade and the env var to unset. `mcp_connect_discover` annotates below-grade servers in place (so the model knows they exist and why they won't auto-activate) and emits a "Compliance filter active" header. Forward-compatible schema: the optional `complianceGrade` field on `UpstreamServerConfig` rides the existing `/api/connect/config` response — the feature kicks in automatically once the backend starts populating grades. Ungraded servers always pass (don't punish unknown).
12
+
5
13
  ## 0.28.1 — 2026-04-18
6
14
 
7
15
  Docs-only release.
package/README.md CHANGED
@@ -286,6 +286,7 @@ Rotate a credential in one place (the dashboard), every machine picks up the new
286
286
  | `MCPH_PRUNE_RESPONSES` | No | Conservative response pruning (redact large file blobs etc. before returning to the client). Set to `0` or `false` to disable. Default: enabled. |
287
287
  | `MCPH_DISABLE_PERSISTENCE` | No | Set to `1` or `true` to keep learning + pack-history scoped to the current process — nothing loaded at start, nothing written on shutdown. Intended for ephemeral / shared environments (CI, containers). Default: cross-session persistence enabled at `~/.mcph/state.json`. |
288
288
  | `MCPH_AUTO_LOAD` | No | Set to `1` or `true` to pre-activate the top recurring pack (from persisted pack-history) on startup — no LLM round-trip required. Skips silently when history is empty or no pack's namespaces are all installed. Default: off. Requires persistence to be enabled. |
289
+ | `MCPH_MIN_COMPLIANCE` | No | Minimum compliance grade (`A`, `B`, `C`, `D`, or `F`, case-insensitive) an installed server must report before `mcp_connect_activate` will load it. Ungraded servers always pass (don't punish unknown). `discover()` annotates below-grade servers in place and shows a "Compliance filter active" header when set. Invalid values log a warning and disable the filter. Default: unset (no filter). |
289
290
  | `MCP_CONNECT_TIMEOUT` | No | Connection timeout in ms for upstream servers (default: `15000`) |
290
291
  | `MCP_CONNECT_IDLE_THRESHOLD` | No | Baseline for idle auto-unload (default: `10`). The per-namespace adaptive cap is `[5, 50]` — bursty namespaces extend past the baseline, long-idle ones unload at it. |
291
292
 
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.28.1" : "dev";
949
+ var VERSION = true ? "0.30.0" : "dev";
950
950
  async function runDoctor(opts = {}) {
951
951
  const lines = [];
952
952
  const write = opts.out ?? ((s) => process.stdout.write(s));
@@ -1824,6 +1824,51 @@ function matchBundles(installedNamespaces) {
1824
1824
  function bundleActivateHint(bundle) {
1825
1825
  return `mcp_connect_activate({ namespaces: ${JSON.stringify(bundle.namespaces)} })`;
1826
1826
  }
1827
+ function topPartialBundles(installedNamespaces, limit) {
1828
+ if (limit <= 0) return [];
1829
+ const { partial } = matchBundles(installedNamespaces);
1830
+ return partial.slice().sort((a, b) => {
1831
+ if (a.missing.length !== b.missing.length) return a.missing.length - b.missing.length;
1832
+ if (a.have.length !== b.have.length) return b.have.length - a.have.length;
1833
+ return a.bundle.id.localeCompare(b.bundle.id);
1834
+ }).slice(0, limit);
1835
+ }
1836
+
1837
+ // src/compliance.ts
1838
+ var GRADE_ORDER = {
1839
+ A: 4,
1840
+ B: 3,
1841
+ C: 2,
1842
+ D: 1,
1843
+ F: 0
1844
+ };
1845
+ function gradeRank(grade) {
1846
+ if (!grade) return -1;
1847
+ const up = grade.toUpperCase();
1848
+ if (up in GRADE_ORDER) return GRADE_ORDER[up];
1849
+ return -1;
1850
+ }
1851
+ var invalidWarned = false;
1852
+ function parseMinCompliance(raw) {
1853
+ if (raw === void 0) return null;
1854
+ const trimmed = raw.trim();
1855
+ if (trimmed === "") return null;
1856
+ const up = trimmed.toUpperCase();
1857
+ if (up === "A" || up === "B" || up === "C" || up === "D" || up === "F") {
1858
+ return up;
1859
+ }
1860
+ if (!invalidWarned) {
1861
+ invalidWarned = true;
1862
+ log("warn", "Invalid MCPH_MIN_COMPLIANCE; filter disabled", { value: raw });
1863
+ }
1864
+ return null;
1865
+ }
1866
+ function passesMinCompliance(serverGrade, min) {
1867
+ if (min === null) return true;
1868
+ const serverRank = gradeRank(serverGrade);
1869
+ if (serverRank < 0) return true;
1870
+ return serverRank >= gradeRank(min);
1871
+ }
1827
1872
 
1828
1873
  // src/cost-estimate.ts
1829
1874
  var BYTES_PER_TOKEN = 4;
@@ -2319,7 +2364,7 @@ var META_TOOLS = {
2319
2364
  },
2320
2365
  activate: {
2321
2366
  name: "mcp_connect_activate",
2322
- description: 'Load one or more installed MCP servers\' tools into the current session by namespace. Each server adds its tools to your context, so load only what the current task needs. When you move on, unload servers you\'re done with via `mcp_connect_deactivate` before loading new ones. Tools are prefixed by namespace (e.g., "gh_create_issue"). Pass "server" for one or "servers" for multiple. Optionally pass `tools: [...]` to expose only those tools by name \u2014 the rest stay proxyable via mcp_connect_dispatch.',
2367
+ description: 'Load one or more installed MCP servers\' tools into the current session by namespace. Each server adds its tools to your context, so load only what the current task needs. When you move on, unload servers you\'re done with via `mcp_connect_deactivate` before loading new ones. Tools are prefixed by namespace (e.g., "gh_create_issue"). Pass "server" for one or "servers" for multiple. Optionally pass `tools: [...]` to expose only those tools by name \u2014 the rest stay proxyable via mcp_connect_dispatch. If `MCPH_MIN_COMPLIANCE` is set, activation refuses servers whose reported grade is below the floor (ungraded servers always pass); the refusal message names the grade and the env var to unset.',
2323
2368
  inputSchema: {
2324
2369
  type: "object",
2325
2370
  properties: {
@@ -3791,7 +3836,7 @@ function categorizeSpawnError(err) {
3791
3836
  }
3792
3837
  async function connectToUpstream(config, onDisconnect, onListChanged) {
3793
3838
  const client = new Client(
3794
- { name: "mcph", version: true ? "0.28.1" : "dev" },
3839
+ { name: "mcph", version: true ? "0.30.0" : "dev" },
3795
3840
  { capabilities: {} }
3796
3841
  );
3797
3842
  let transport;
@@ -4242,6 +4287,9 @@ function isPersistenceDisabled() {
4242
4287
  if (raw === void 0 || raw === "") return false;
4243
4288
  return raw === "1" || raw.toLowerCase() === "true";
4244
4289
  }
4290
+ function resolveMinCompliance() {
4291
+ return parseMinCompliance(process.env.MCPH_MIN_COMPLIANCE);
4292
+ }
4245
4293
  function isAutoLoadEnabled() {
4246
4294
  const raw = process.env.MCPH_AUTO_LOAD;
4247
4295
  if (raw === void 0 || raw === "") return false;
@@ -4305,7 +4353,7 @@ var ConnectServer = class _ConnectServer {
4305
4353
  this.apiUrl = apiUrl6;
4306
4354
  this.token = token6;
4307
4355
  this.server = new Server(
4308
- { name: "mcph", version: true ? "0.28.1" : "dev" },
4356
+ { name: "mcph", version: true ? "0.30.0" : "dev" },
4309
4357
  {
4310
4358
  capabilities: {
4311
4359
  tools: { listChanged: true },
@@ -5100,6 +5148,11 @@ var ConnectServer = class _ConnectServer {
5100
5148
  const lines = [context ? "Servers ranked by relevance:\n" : "Installed MCP servers:\n"];
5101
5149
  if (autoWarmed && sorted.length > 0) {
5102
5150
  lines.push(`Auto-loaded "${sorted[0].namespace}" \u2014 top match for your query.
5151
+ `);
5152
+ }
5153
+ const minCompliance = resolveMinCompliance();
5154
+ if (minCompliance !== null) {
5155
+ lines.push(`Compliance filter active: MCPH_MIN_COMPLIANCE=${minCompliance}
5103
5156
  `);
5104
5157
  }
5105
5158
  if (context) {
@@ -5160,7 +5213,17 @@ var ConnectServer = class _ConnectServer {
5160
5213
  costLabel = ` \u2014 ${formatCostLabel(estimateFromToolCache(cached))}`;
5161
5214
  }
5162
5215
  }
5163
- lines.push(` ${server.namespace} \u2014 ${server.name} [${status}] (${server.type})${relevance}${costLabel}`);
5216
+ let complianceLabel = "";
5217
+ if (minCompliance !== null && server.complianceGrade) {
5218
+ if (passesMinCompliance(server.complianceGrade, minCompliance)) {
5219
+ complianceLabel = ` [${server.complianceGrade}]`;
5220
+ } else {
5221
+ complianceLabel = ` (grade ${server.complianceGrade} \u2014 below MCPH_MIN_COMPLIANCE=${minCompliance}, won't auto-activate)`;
5222
+ }
5223
+ }
5224
+ lines.push(
5225
+ ` ${server.namespace} \u2014 ${server.name} [${status}] (${server.type})${relevance}${costLabel}${complianceLabel}`
5226
+ );
5164
5227
  const shadow = formatShadowLine(server);
5165
5228
  if (shadow) lines.push(` ${shadow}`);
5166
5229
  const warning = formatHealthWarning(connection?.health, this.activationFailures.get(server.namespace));
@@ -5185,6 +5248,14 @@ var ConnectServer = class _ConnectServer {
5185
5248
  lines.push(` ${o.bareName} \u2014 available in: ${o.namespaces.join(", ")}${suffix}`);
5186
5249
  }
5187
5250
  }
5251
+ const allInstalled = this.config.servers.map((s) => s.namespace);
5252
+ const bundleGaps = topPartialBundles(allInstalled, 3);
5253
+ if (bundleGaps.length > 0) {
5254
+ lines.push("\nBundle completions (install to unlock curated stacks):");
5255
+ for (const { bundle, have, missing } of bundleGaps) {
5256
+ lines.push(` ${bundle.id} \u2014 have: ${have.join(", ")}; add: ${missing.join(", ")}`);
5257
+ }
5258
+ }
5188
5259
  const inactive = this.config.servers.filter((s) => !s.isActive);
5189
5260
  if (inactive.length > 0) {
5190
5261
  lines.push("\nDisabled servers:");
@@ -5418,10 +5489,21 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
5418
5489
  const results = [];
5419
5490
  let anyChanged = false;
5420
5491
  let anyError = false;
5492
+ const minCompliance = resolveMinCompliance();
5421
5493
  const total = namespaces.length;
5422
5494
  let i = 0;
5423
5495
  for (const namespace of namespaces) {
5424
5496
  i += 1;
5497
+ if (minCompliance !== null) {
5498
+ const cfg = this.config?.servers.find((s) => s.namespace === namespace);
5499
+ if (cfg && !passesMinCompliance(cfg.complianceGrade, minCompliance)) {
5500
+ const grade = cfg.complianceGrade ?? "unknown";
5501
+ const message = `Refused to load "${namespace}": compliance grade ${grade} is below MCPH_MIN_COMPLIANCE=${minCompliance}. Unset MCPH_MIN_COMPLIANCE (or lower it) to override.`;
5502
+ results.push(message);
5503
+ anyError = true;
5504
+ continue;
5505
+ }
5506
+ }
5425
5507
  progress?.(`Loading ${namespace} (${i}/${total})`, i - 1, total);
5426
5508
  const r = await this.activateOne(namespace, progress);
5427
5509
  results.push(r.message);
@@ -6340,7 +6422,7 @@ ${installBlock}
6340
6422
  );
6341
6423
  process.exit(0);
6342
6424
  } else if (subcommand === "--version" || subcommand === "-V") {
6343
- process.stdout.write(`mcph ${true ? "0.28.1" : "dev"}
6425
+ process.stdout.write(`mcph ${true ? "0.30.0" : "dev"}
6344
6426
  `);
6345
6427
  process.exit(0);
6346
6428
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/mcph",
3
- "version": "0.28.1",
3
+ "version": "0.30.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)",