@yawlabs/mcph 0.43.0 → 0.44.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.44.0 — 2026-04-18
6
+
7
+ - **`mcph install --list` + `mcph install --all`** — Two new modes on the install subcommand. `--list` is read-only: it enumerates every client/scope combo for the current OS and shows whether an `mcp.hosting` entry is already wired up, plus a path-per-row and a one-line summary (`N/M client scopes have mcp.hosting configured on linux`). No token, no network, no writes — just a diagnostic view that mirrors the `doctor` CLIENTS section but without the rest of doctor's noise. `--all` walks `INSTALL_TARGETS`, picks the default scope per client (user where supported, else the first non-project-dir scope, else skipped unless `--project-dir` is passed), and calls `runInstall` in a loop — so `--dry-run`, `--force`, `--skip`, and `--token` all propagate as expected. Status is aggregated into a single summary line, and the process exit code is non-zero if any sub-install failed so CI can still gate on one-shot onboarding. Works around the main drop-off during setup ("which client am I supposed to pick?") by offering both the answer (`--list`) and the sledgehammer (`--all`) from the same subcommand.
8
+
5
9
  ## 0.43.0 — 2026-04-18
6
10
 
7
11
  - **`mcph servers <namespace-filter>` — positional filter** — Passing a bare positional argument now filters the listing to servers whose namespace contains that substring (case-insensitive): `mcph servers git` matches both `github` and `gitlab`. Applies to both the text table and the `--json` output so the two surfaces agree. Summary line reflects the filtered count, and a filter that matches nothing prints an explanatory "No servers match …" instead of an empty table (which previously looked like an empty account).
package/README.md CHANGED
@@ -65,6 +65,15 @@ Helpful flags:
65
65
  - `--force` / `--skip` — overwrite or leave an existing `mcp.hosting` entry. Without either, mcph prompts (TTY) or refuses (non-TTY).
66
66
  - `--no-mcph-config` — write only the client config; leave `~/.mcph/config.json` untouched.
67
67
 
68
+ Or install into every detected client at once:
69
+
70
+ ```bash
71
+ mcph install --list # read-only: detect clients + show install state per scope
72
+ mcph install --all --token mcp_pat_… # one-shot: install into every user-scope client on this machine
73
+ ```
74
+
75
+ `--list` never writes (no token needed). `--all` installs into every client whose user-scope target is resolvable on this OS — Claude Desktop is skipped on Linux, VS Code is skipped unless `--project-dir` is given (it's workspace-only). Aggregate exit code is non-zero if any sub-install fails.
76
+
68
77
  Or [edit the JSON by hand](#manual-install) if you'd rather.
69
78
 
70
79
  ### Diagnose problems — `mcph doctor`
package/dist/index.js CHANGED
@@ -663,7 +663,18 @@ var SUBCOMMAND_SPEC = [
663
663
  {
664
664
  name: "install",
665
665
  positional: [...INSTALL_CLIENTS],
666
- flags: ["--scope", "--token", "--project-dir", "--os", "--force", "--skip", "--dry-run", "--no-mcph-config"]
666
+ flags: [
667
+ "--scope",
668
+ "--token",
669
+ "--project-dir",
670
+ "--os",
671
+ "--force",
672
+ "--skip",
673
+ "--dry-run",
674
+ "--no-mcph-config",
675
+ "--list",
676
+ "--all"
677
+ ]
667
678
  },
668
679
  { name: "doctor", flags: ["--json", "--help"] },
669
680
  { name: "servers", flags: ["--json", "--help"] },
@@ -1435,7 +1446,7 @@ function selectFlakyNamespaces(entries, limit) {
1435
1446
  }
1436
1447
 
1437
1448
  // src/doctor-cmd.ts
1438
- var VERSION = true ? "0.43.0" : "dev";
1449
+ var VERSION = true ? "0.44.0" : "dev";
1439
1450
  async function runDoctor(opts = {}) {
1440
1451
  if (opts.json) return runDoctorJson(opts);
1441
1452
  const lines = [];
@@ -1769,6 +1780,62 @@ function walkContainer(root, path4) {
1769
1780
  if (typeof cur !== "object" || cur === null || Array.isArray(cur)) return null;
1770
1781
  return cur;
1771
1782
  }
1783
+ async function probeClientsAsync(opts) {
1784
+ const result = [];
1785
+ for (const target of INSTALL_TARGETS) {
1786
+ const unavailable = !target.availableOn.includes(opts.os);
1787
+ if (unavailable) {
1788
+ result.push({
1789
+ clientId: target.clientId,
1790
+ scope: target.scopes[0].scope,
1791
+ path: "(n/a)",
1792
+ exists: false,
1793
+ hasMcphEntry: false,
1794
+ malformed: false,
1795
+ unavailable: true
1796
+ });
1797
+ continue;
1798
+ }
1799
+ for (const scope of target.scopes) {
1800
+ const resolved = resolveInstallPath({
1801
+ clientId: target.clientId,
1802
+ scope: scope.scope,
1803
+ os: opts.os,
1804
+ home: opts.home,
1805
+ projectDir: scope.requiresProjectDir ? opts.cwd : void 0
1806
+ });
1807
+ const exists3 = existsSync(resolved.absolute);
1808
+ let hasMcphEntry = false;
1809
+ let malformed = false;
1810
+ if (exists3) {
1811
+ try {
1812
+ const raw = await readFile3(resolved.absolute, "utf8");
1813
+ if (raw.trim().length > 0) {
1814
+ const parsed = parseJsonc(raw);
1815
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1816
+ const container = walkContainer(parsed, resolved.containerPath);
1817
+ if (container) hasMcphEntry = ENTRY_NAME in container;
1818
+ } else {
1819
+ malformed = true;
1820
+ }
1821
+ }
1822
+ } catch {
1823
+ malformed = true;
1824
+ }
1825
+ }
1826
+ result.push({
1827
+ clientId: target.clientId,
1828
+ scope: scope.scope,
1829
+ path: resolved.absolute,
1830
+ exists: exists3,
1831
+ hasMcphEntry,
1832
+ malformed,
1833
+ unavailable: false
1834
+ });
1835
+ }
1836
+ }
1837
+ return result;
1838
+ }
1772
1839
  async function fetchLatestVersion(override) {
1773
1840
  if (override) {
1774
1841
  try {
@@ -1950,7 +2017,7 @@ import { homedir as homedir5 } from "os";
1950
2017
  import { dirname as dirname2 } from "path";
1951
2018
  import { join as join5, resolve as resolve2 } from "path";
1952
2019
  import { createInterface } from "readline/promises";
1953
- var USAGE = "Usage: mcph install <claude-code|claude-desktop|cursor|vscode> [--scope user|project|local]\n [--token <mcp_pat_\u2026>] [--project-dir <path>] [--os macos|linux|windows]\n [--force | --skip] [--dry-run] [--no-mcph-config]";
2020
+ var USAGE = "Usage: mcph install <claude-code|claude-desktop|cursor|vscode> [--scope user|project|local]\n [--token <mcp_pat_\u2026>] [--project-dir <path>] [--os macos|linux|windows]\n [--force | --skip] [--dry-run] [--no-mcph-config]\n mcph install --list (detect clients; no writes)\n mcph install --all [--token <mcp_pat_\u2026>] (install into every detected client)";
1954
2021
  async function runInstall(opts) {
1955
2022
  const stdout = opts.io?.stdout ?? process.stdout;
1956
2023
  const stderr = opts.io?.stderr ?? process.stderr;
@@ -1965,10 +2032,21 @@ async function runInstall(opts) {
1965
2032
  stderr.write(`${s}
1966
2033
  `);
1967
2034
  };
2035
+ if (opts.listOnly && opts.all) {
2036
+ err("mcph install: --list and --all are mutually exclusive");
2037
+ return { written: [], wouldWrite: [], messages, exitCode: 2 };
2038
+ }
2039
+ if (opts.listOnly) return runInstallList(opts, log2);
2040
+ if (opts.all) return runInstallAll(opts, log2, err);
1968
2041
  if (opts.force && opts.skip) {
1969
2042
  err("mcph install: --force and --skip are mutually exclusive");
1970
2043
  return { written: [], wouldWrite: [], messages, exitCode: 2 };
1971
2044
  }
2045
+ if (!opts.clientId) {
2046
+ err(`mcph install: client argument required
2047
+ ${USAGE}`);
2048
+ return { written: [], wouldWrite: [], messages, exitCode: 2 };
2049
+ }
1972
2050
  const target = INSTALL_TARGETS.find((t) => t.clientId === opts.clientId);
1973
2051
  if (!target) {
1974
2052
  err(`mcph install: unknown client ${opts.clientId}
@@ -2294,6 +2372,12 @@ function parseInstallArgs(argv) {
2294
2372
  case "--no-mcph-config":
2295
2373
  opts.skipMcphConfig = true;
2296
2374
  break;
2375
+ case "--list":
2376
+ opts.listOnly = true;
2377
+ break;
2378
+ case "--all":
2379
+ opts.all = true;
2380
+ break;
2297
2381
  case "-h":
2298
2382
  case "--help":
2299
2383
  return { ok: false, error: USAGE };
@@ -2303,6 +2387,16 @@ ${USAGE}` };
2303
2387
  positional.push(a);
2304
2388
  }
2305
2389
  }
2390
+ if (opts.listOnly || opts.all) {
2391
+ if (positional.length > 0) {
2392
+ return {
2393
+ ok: false,
2394
+ error: `mcph install: ${opts.listOnly ? "--list" : "--all"} does not take a client argument.
2395
+ ${USAGE}`
2396
+ };
2397
+ }
2398
+ return { ok: true, options: opts };
2399
+ }
2306
2400
  if (positional.length !== 1)
2307
2401
  return { ok: false, error: `Expected exactly one client argument, got ${positional.length}.
2308
2402
  ${USAGE}` };
@@ -2316,6 +2410,127 @@ ${USAGE}` };
2316
2410
  opts.clientId = clientId;
2317
2411
  return { ok: true, options: opts };
2318
2412
  }
2413
+ async function runInstallList(opts, log2) {
2414
+ const home = opts.home ?? homedir5();
2415
+ const cwd = opts.cwd ?? process.cwd();
2416
+ const os = opts.os ?? CURRENT_OS;
2417
+ const probes = await probeClientsAsync({ home, os, cwd });
2418
+ const rows = probes.map((p) => ({
2419
+ client: INSTALL_TARGETS.find((t) => t.clientId === p.clientId)?.label ?? p.clientId,
2420
+ scope: p.scope,
2421
+ path: displayPath(p.path, home),
2422
+ status: statusFor(p)
2423
+ }));
2424
+ const installed = probes.filter((p) => p.hasMcphEntry).length;
2425
+ const available = probes.filter((p) => !p.unavailable).length;
2426
+ log2(`${installed}/${available} client scopes have mcp.hosting configured on ${os}.`);
2427
+ log2("");
2428
+ const widths = {
2429
+ client: Math.max("CLIENT".length, ...rows.map((r) => r.client.length)),
2430
+ scope: Math.max("SCOPE".length, ...rows.map((r) => r.scope.length)),
2431
+ path: Math.max("PATH".length, ...rows.map((r) => r.path.length)),
2432
+ status: Math.max("STATUS".length, ...rows.map((r) => r.status.length))
2433
+ };
2434
+ const header = ` ${"CLIENT".padEnd(widths.client)} ${"SCOPE".padEnd(widths.scope)} ${"PATH".padEnd(widths.path)} ${"STATUS".padEnd(widths.status)}`;
2435
+ log2(header);
2436
+ for (const r of rows) {
2437
+ log2(
2438
+ ` ${r.client.padEnd(widths.client)} ${r.scope.padEnd(widths.scope)} ${r.path.padEnd(widths.path)} ${r.status.padEnd(widths.status)}`
2439
+ );
2440
+ }
2441
+ log2("");
2442
+ log2("Install into a specific client: `mcph install <client> [--scope user|project|local]`");
2443
+ log2("Install into every available user-scope client: `mcph install --all`");
2444
+ return { written: [], wouldWrite: [], messages: [], exitCode: 0 };
2445
+ }
2446
+ function statusFor(p) {
2447
+ if (p.unavailable) return "unavailable";
2448
+ if (p.malformed) return "malformed";
2449
+ if (p.hasMcphEntry) return "installed";
2450
+ if (p.exists) return "other-entries";
2451
+ return "not installed";
2452
+ }
2453
+ function displayPath(abs, home) {
2454
+ if (abs === "(n/a)") return abs;
2455
+ if (home && abs.startsWith(home)) {
2456
+ const tail = abs.slice(home.length).replace(/^[\\/]/, "");
2457
+ return `~${process.platform === "win32" ? "\\" : "/"}${tail}`;
2458
+ }
2459
+ return abs;
2460
+ }
2461
+ async function runInstallAll(opts, log2, err) {
2462
+ const os = opts.os ?? CURRENT_OS;
2463
+ const targets = INSTALL_TARGETS.filter((t) => t.availableOn.includes(os));
2464
+ if (targets.length === 0) {
2465
+ err(`mcph install --all: no installable clients on ${os}.`);
2466
+ return { written: [], wouldWrite: [], messages: [], exitCode: 1 };
2467
+ }
2468
+ const plans = [];
2469
+ const skipped = [];
2470
+ for (const t of targets) {
2471
+ const userScope = t.scopes.find((s) => s.scope === "user");
2472
+ if (userScope) {
2473
+ plans.push({ clientId: t.clientId, scope: "user" });
2474
+ continue;
2475
+ }
2476
+ const firstNoProj = t.scopes.find((s) => !s.requiresProjectDir);
2477
+ if (firstNoProj) {
2478
+ plans.push({ clientId: t.clientId, scope: firstNoProj.scope });
2479
+ continue;
2480
+ }
2481
+ if (opts.projectDir) {
2482
+ plans.push({ clientId: t.clientId, scope: t.scopes[0].scope });
2483
+ continue;
2484
+ }
2485
+ skipped.push({
2486
+ clientId: t.clientId,
2487
+ reason: `requires --project-dir (scopes: ${t.scopes.map((s) => s.scope).join(", ")})`
2488
+ });
2489
+ }
2490
+ log2(`Installing into ${plans.length} client${plans.length === 1 ? "" : "s"}\u2026`);
2491
+ if (skipped.length > 0) {
2492
+ for (const s of skipped) log2(` skip ${s.clientId}: ${s.reason}`);
2493
+ }
2494
+ log2("");
2495
+ const aggregateWritten = [];
2496
+ const aggregateWouldWrite = [];
2497
+ const aggregateMessages = [];
2498
+ let failed = 0;
2499
+ let succeeded = 0;
2500
+ for (const plan of plans) {
2501
+ log2(`\u2500\u2500 ${plan.clientId} (${plan.scope}) \u2500\u2500`);
2502
+ const result = await runInstall({
2503
+ ...opts,
2504
+ listOnly: false,
2505
+ all: false,
2506
+ clientId: plan.clientId,
2507
+ scope: plan.scope
2508
+ });
2509
+ aggregateWritten.push(...result.written);
2510
+ aggregateWouldWrite.push(...result.wouldWrite);
2511
+ aggregateMessages.push(...result.messages);
2512
+ if (result.exitCode === 0) succeeded += 1;
2513
+ else failed += 1;
2514
+ log2("");
2515
+ }
2516
+ const totalPlanned = plans.length;
2517
+ if (failed === 0) {
2518
+ log2(`\u2713 ${succeeded}/${totalPlanned} clients installed successfully.`);
2519
+ return {
2520
+ written: aggregateWritten,
2521
+ wouldWrite: aggregateWouldWrite,
2522
+ messages: aggregateMessages,
2523
+ exitCode: 0
2524
+ };
2525
+ }
2526
+ err(`${failed}/${totalPlanned} client install${failed === 1 ? "" : "s"} failed. ${succeeded} succeeded.`);
2527
+ return {
2528
+ written: aggregateWritten,
2529
+ wouldWrite: aggregateWouldWrite,
2530
+ messages: aggregateMessages,
2531
+ exitCode: 1
2532
+ };
2533
+ }
2319
2534
  var INSTALL_USAGE = USAGE;
2320
2535
 
2321
2536
  // src/reset-learning-cmd.ts
@@ -4519,7 +4734,7 @@ function categorizeSpawnError(err) {
4519
4734
  }
4520
4735
  async function connectToUpstream(config, onDisconnect, onListChanged) {
4521
4736
  const client = new Client(
4522
- { name: "mcph", version: true ? "0.43.0" : "dev" },
4737
+ { name: "mcph", version: true ? "0.44.0" : "dev" },
4523
4738
  { capabilities: {} }
4524
4739
  );
4525
4740
  let transport;
@@ -5000,7 +5215,7 @@ var ConnectServer = class _ConnectServer {
5000
5215
  this.apiUrl = apiUrl6;
5001
5216
  this.token = token6;
5002
5217
  this.server = new Server(
5003
- { name: "mcph", version: true ? "0.43.0" : "dev" },
5218
+ { name: "mcph", version: true ? "0.44.0" : "dev" },
5004
5219
  {
5005
5220
  capabilities: {
5006
5221
  tools: { listChanged: true },
@@ -7276,7 +7491,7 @@ ${installBlock}
7276
7491
  );
7277
7492
  process.exit(0);
7278
7493
  } else if (subcommand === "--version" || subcommand === "-V") {
7279
- process.stdout.write(`mcph ${true ? "0.43.0" : "dev"}
7494
+ process.stdout.write(`mcph ${true ? "0.44.0" : "dev"}
7280
7495
  `);
7281
7496
  process.exit(0);
7282
7497
  } else if (subcommand && !subcommand.startsWith("-")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/mcph",
3
- "version": "0.43.0",
3
+ "version": "0.44.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)",