@yawlabs/mcph 0.28.1 → 0.29.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.29.0 — 2026-04-18
6
+
7
+ - **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).
8
+
5
9
  ## 0.28.1 — 2026-04-18
6
10
 
7
11
  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.29.0" : "dev";
950
950
  async function runDoctor(opts = {}) {
951
951
  const lines = [];
952
952
  const write = opts.out ?? ((s) => process.stdout.write(s));
@@ -1825,6 +1825,42 @@ function bundleActivateHint(bundle) {
1825
1825
  return `mcp_connect_activate({ namespaces: ${JSON.stringify(bundle.namespaces)} })`;
1826
1826
  }
1827
1827
 
1828
+ // src/compliance.ts
1829
+ var GRADE_ORDER = {
1830
+ A: 4,
1831
+ B: 3,
1832
+ C: 2,
1833
+ D: 1,
1834
+ F: 0
1835
+ };
1836
+ function gradeRank(grade) {
1837
+ if (!grade) return -1;
1838
+ const up = grade.toUpperCase();
1839
+ if (up in GRADE_ORDER) return GRADE_ORDER[up];
1840
+ return -1;
1841
+ }
1842
+ var invalidWarned = false;
1843
+ function parseMinCompliance(raw) {
1844
+ if (raw === void 0) return null;
1845
+ const trimmed = raw.trim();
1846
+ if (trimmed === "") return null;
1847
+ const up = trimmed.toUpperCase();
1848
+ if (up === "A" || up === "B" || up === "C" || up === "D" || up === "F") {
1849
+ return up;
1850
+ }
1851
+ if (!invalidWarned) {
1852
+ invalidWarned = true;
1853
+ log("warn", "Invalid MCPH_MIN_COMPLIANCE; filter disabled", { value: raw });
1854
+ }
1855
+ return null;
1856
+ }
1857
+ function passesMinCompliance(serverGrade, min) {
1858
+ if (min === null) return true;
1859
+ const serverRank = gradeRank(serverGrade);
1860
+ if (serverRank < 0) return true;
1861
+ return serverRank >= gradeRank(min);
1862
+ }
1863
+
1828
1864
  // src/cost-estimate.ts
1829
1865
  var BYTES_PER_TOKEN = 4;
1830
1866
  var CACHED_TOOL_SCHEMA_PAD_BYTES = 200;
@@ -2319,7 +2355,7 @@ var META_TOOLS = {
2319
2355
  },
2320
2356
  activate: {
2321
2357
  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.',
2358
+ 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
2359
  inputSchema: {
2324
2360
  type: "object",
2325
2361
  properties: {
@@ -3791,7 +3827,7 @@ function categorizeSpawnError(err) {
3791
3827
  }
3792
3828
  async function connectToUpstream(config, onDisconnect, onListChanged) {
3793
3829
  const client = new Client(
3794
- { name: "mcph", version: true ? "0.28.1" : "dev" },
3830
+ { name: "mcph", version: true ? "0.29.0" : "dev" },
3795
3831
  { capabilities: {} }
3796
3832
  );
3797
3833
  let transport;
@@ -4242,6 +4278,9 @@ function isPersistenceDisabled() {
4242
4278
  if (raw === void 0 || raw === "") return false;
4243
4279
  return raw === "1" || raw.toLowerCase() === "true";
4244
4280
  }
4281
+ function resolveMinCompliance() {
4282
+ return parseMinCompliance(process.env.MCPH_MIN_COMPLIANCE);
4283
+ }
4245
4284
  function isAutoLoadEnabled() {
4246
4285
  const raw = process.env.MCPH_AUTO_LOAD;
4247
4286
  if (raw === void 0 || raw === "") return false;
@@ -4305,7 +4344,7 @@ var ConnectServer = class _ConnectServer {
4305
4344
  this.apiUrl = apiUrl6;
4306
4345
  this.token = token6;
4307
4346
  this.server = new Server(
4308
- { name: "mcph", version: true ? "0.28.1" : "dev" },
4347
+ { name: "mcph", version: true ? "0.29.0" : "dev" },
4309
4348
  {
4310
4349
  capabilities: {
4311
4350
  tools: { listChanged: true },
@@ -5100,6 +5139,11 @@ var ConnectServer = class _ConnectServer {
5100
5139
  const lines = [context ? "Servers ranked by relevance:\n" : "Installed MCP servers:\n"];
5101
5140
  if (autoWarmed && sorted.length > 0) {
5102
5141
  lines.push(`Auto-loaded "${sorted[0].namespace}" \u2014 top match for your query.
5142
+ `);
5143
+ }
5144
+ const minCompliance = resolveMinCompliance();
5145
+ if (minCompliance !== null) {
5146
+ lines.push(`Compliance filter active: MCPH_MIN_COMPLIANCE=${minCompliance}
5103
5147
  `);
5104
5148
  }
5105
5149
  if (context) {
@@ -5160,7 +5204,17 @@ var ConnectServer = class _ConnectServer {
5160
5204
  costLabel = ` \u2014 ${formatCostLabel(estimateFromToolCache(cached))}`;
5161
5205
  }
5162
5206
  }
5163
- lines.push(` ${server.namespace} \u2014 ${server.name} [${status}] (${server.type})${relevance}${costLabel}`);
5207
+ let complianceLabel = "";
5208
+ if (minCompliance !== null && server.complianceGrade) {
5209
+ if (passesMinCompliance(server.complianceGrade, minCompliance)) {
5210
+ complianceLabel = ` [${server.complianceGrade}]`;
5211
+ } else {
5212
+ complianceLabel = ` (grade ${server.complianceGrade} \u2014 below MCPH_MIN_COMPLIANCE=${minCompliance}, won't auto-activate)`;
5213
+ }
5214
+ }
5215
+ lines.push(
5216
+ ` ${server.namespace} \u2014 ${server.name} [${status}] (${server.type})${relevance}${costLabel}${complianceLabel}`
5217
+ );
5164
5218
  const shadow = formatShadowLine(server);
5165
5219
  if (shadow) lines.push(` ${shadow}`);
5166
5220
  const warning = formatHealthWarning(connection?.health, this.activationFailures.get(server.namespace));
@@ -5418,10 +5472,21 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
5418
5472
  const results = [];
5419
5473
  let anyChanged = false;
5420
5474
  let anyError = false;
5475
+ const minCompliance = resolveMinCompliance();
5421
5476
  const total = namespaces.length;
5422
5477
  let i = 0;
5423
5478
  for (const namespace of namespaces) {
5424
5479
  i += 1;
5480
+ if (minCompliance !== null) {
5481
+ const cfg = this.config?.servers.find((s) => s.namespace === namespace);
5482
+ if (cfg && !passesMinCompliance(cfg.complianceGrade, minCompliance)) {
5483
+ const grade = cfg.complianceGrade ?? "unknown";
5484
+ const message = `Refused to load "${namespace}": compliance grade ${grade} is below MCPH_MIN_COMPLIANCE=${minCompliance}. Unset MCPH_MIN_COMPLIANCE (or lower it) to override.`;
5485
+ results.push(message);
5486
+ anyError = true;
5487
+ continue;
5488
+ }
5489
+ }
5425
5490
  progress?.(`Loading ${namespace} (${i}/${total})`, i - 1, total);
5426
5491
  const r = await this.activateOne(namespace, progress);
5427
5492
  results.push(r.message);
@@ -6340,7 +6405,7 @@ ${installBlock}
6340
6405
  );
6341
6406
  process.exit(0);
6342
6407
  } else if (subcommand === "--version" || subcommand === "-V") {
6343
- process.stdout.write(`mcph ${true ? "0.28.1" : "dev"}
6408
+ process.stdout.write(`mcph ${true ? "0.29.0" : "dev"}
6344
6409
  `);
6345
6410
  process.exit(0);
6346
6411
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/mcph",
3
- "version": "0.28.1",
3
+ "version": "0.29.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)",