@yawlabs/mcph 0.29.0 → 0.31.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.31.0 — 2026-04-18
6
+
7
+ - **"Did you mean?" suggestions on `mcp_connect_activate`** — When a caller tries to activate a namespace that doesn't exist, the error message now splits the two underlying cases: (a) not installed at all (with up to 3 fuzzy-matched installed namespaces via substring containment or ≤2 edit distance, or a pointer to `mcp_connect_discover` when nothing is close), and (b) installed but disabled in the dashboard (with a pointer to `mcp.hosting` to enable). Replaces the previous conflated "`X` not found or disabled" message.
8
+
9
+ ## 0.30.0 — 2026-04-18
10
+
11
+ - **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.
12
+
5
13
  ## 0.29.0 — 2026-04-18
6
14
 
7
15
  - **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).
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.29.0" : "dev";
949
+ var VERSION = true ? "0.31.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,15 @@ 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
+ }
1827
1836
 
1828
1837
  // src/compliance.ts
1829
1838
  var GRADE_ORDER = {
@@ -2126,6 +2135,60 @@ function stepBindingKey(step, index) {
2126
2135
  return typeof step.id === "string" && step.id.length > 0 ? step.id : String(index);
2127
2136
  }
2128
2137
 
2138
+ // src/fuzzy.ts
2139
+ function levenshtein(a, b) {
2140
+ if (a === b) return 0;
2141
+ const aLen = a.length;
2142
+ const bLen = b.length;
2143
+ if (aLen === 0) return bLen;
2144
+ if (bLen === 0) return aLen;
2145
+ let prev = new Array(bLen + 1);
2146
+ let curr = new Array(bLen + 1);
2147
+ for (let j = 0; j <= bLen; j++) prev[j] = j;
2148
+ for (let i = 1; i <= aLen; i++) {
2149
+ curr[0] = i;
2150
+ for (let j = 1; j <= bLen; j++) {
2151
+ const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
2152
+ curr[j] = Math.min(
2153
+ curr[j - 1] + 1,
2154
+ // insertion
2155
+ prev[j] + 1,
2156
+ // deletion
2157
+ prev[j - 1] + cost
2158
+ // substitution
2159
+ );
2160
+ }
2161
+ [prev, curr] = [curr, prev];
2162
+ }
2163
+ return prev[bLen];
2164
+ }
2165
+ function closestNames(query, candidates, limit) {
2166
+ if (limit <= 0) return [];
2167
+ const q = query.toLowerCase();
2168
+ const scored = [];
2169
+ for (const c of candidates) {
2170
+ if (c === query) continue;
2171
+ const lc = c.toLowerCase();
2172
+ let score = null;
2173
+ if (lc === q) {
2174
+ score = 0;
2175
+ } else if (lc.startsWith(q) || q.startsWith(lc)) {
2176
+ score = 1;
2177
+ } else if (lc.includes(q) || q.includes(lc)) {
2178
+ score = 2;
2179
+ } else {
2180
+ const d = levenshtein(q, lc);
2181
+ if (d <= 2) score = 2 + d;
2182
+ }
2183
+ if (score !== null) scored.push({ name: c, score });
2184
+ }
2185
+ scored.sort((a, b) => {
2186
+ if (a.score !== b.score) return a.score - b.score;
2187
+ return a.name.localeCompare(b.name);
2188
+ });
2189
+ return scored.slice(0, limit).map((s) => s.name);
2190
+ }
2191
+
2129
2192
  // src/guide.ts
2130
2193
  import { readFile as readFile5 } from "fs/promises";
2131
2194
  var GUIDE_READ_TIMEOUT_MS = 1e3;
@@ -3827,7 +3890,7 @@ function categorizeSpawnError(err) {
3827
3890
  }
3828
3891
  async function connectToUpstream(config, onDisconnect, onListChanged) {
3829
3892
  const client = new Client(
3830
- { name: "mcph", version: true ? "0.29.0" : "dev" },
3893
+ { name: "mcph", version: true ? "0.31.0" : "dev" },
3831
3894
  { capabilities: {} }
3832
3895
  );
3833
3896
  let transport;
@@ -4344,7 +4407,7 @@ var ConnectServer = class _ConnectServer {
4344
4407
  this.apiUrl = apiUrl6;
4345
4408
  this.token = token6;
4346
4409
  this.server = new Server(
4347
- { name: "mcph", version: true ? "0.29.0" : "dev" },
4410
+ { name: "mcph", version: true ? "0.31.0" : "dev" },
4348
4411
  {
4349
4412
  capabilities: {
4350
4413
  tools: { listChanged: true },
@@ -5239,6 +5302,14 @@ var ConnectServer = class _ConnectServer {
5239
5302
  lines.push(` ${o.bareName} \u2014 available in: ${o.namespaces.join(", ")}${suffix}`);
5240
5303
  }
5241
5304
  }
5305
+ const allInstalled = this.config.servers.map((s) => s.namespace);
5306
+ const bundleGaps = topPartialBundles(allInstalled, 3);
5307
+ if (bundleGaps.length > 0) {
5308
+ lines.push("\nBundle completions (install to unlock curated stacks):");
5309
+ for (const { bundle, have, missing } of bundleGaps) {
5310
+ lines.push(` ${bundle.id} \u2014 have: ${have.join(", ")}; add: ${missing.join(", ")}`);
5311
+ }
5312
+ }
5242
5313
  const inactive = this.config.servers.filter((s) => !s.isActive);
5243
5314
  if (inactive.length > 0) {
5244
5315
  lines.push("\nDisabled servers:");
@@ -5303,10 +5374,21 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
5303
5374
  serverId: existing.config.id
5304
5375
  };
5305
5376
  }
5306
- const serverConfig = this.config?.servers.find((s) => s.namespace === namespace && s.isActive);
5307
- if (!serverConfig) {
5308
- return { ok: false, isChanged: false, message: `"${namespace}" not found or disabled.` };
5377
+ const anyMatch = this.config?.servers.find((s) => s.namespace === namespace);
5378
+ if (!anyMatch) {
5379
+ const allNamespaces = this.config?.servers.map((s) => s.namespace) ?? [];
5380
+ const suggestions = closestNames(namespace, allNamespaces, 3);
5381
+ const hint = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : " Use mcp_connect_discover to see installed servers.";
5382
+ return { ok: false, isChanged: false, message: `"${namespace}" is not installed.${hint}` };
5383
+ }
5384
+ if (!anyMatch.isActive) {
5385
+ return {
5386
+ ok: false,
5387
+ isChanged: false,
5388
+ message: `"${namespace}" is installed but disabled. Enable it at https://mcp.hosting to activate.`
5389
+ };
5309
5390
  }
5391
+ const serverConfig = anyMatch;
5310
5392
  if (!profileAllows(this.profile, namespace)) {
5311
5393
  return {
5312
5394
  ok: false,
@@ -6405,7 +6487,7 @@ ${installBlock}
6405
6487
  );
6406
6488
  process.exit(0);
6407
6489
  } else if (subcommand === "--version" || subcommand === "-V") {
6408
- process.stdout.write(`mcph ${true ? "0.29.0" : "dev"}
6490
+ process.stdout.write(`mcph ${true ? "0.31.0" : "dev"}
6409
6491
  `);
6410
6492
  process.exit(0);
6411
6493
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/mcph",
3
- "version": "0.29.0",
3
+ "version": "0.31.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)",