@yawlabs/mcph 0.39.0 → 0.40.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/index.js +325 -173
  3. 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.40.0 — 2026-04-18
6
+
7
+ - **`mcph bundles` CLI subcommand** — CLI counterpart to the `mcp_connect_bundles` meta-tool (v0.28.0). Two actions mirror the meta-tool's `action` parameter: `list` prints every curated bundle grouped by category with activate hints (static, no network, no token needed — good for browsing or sharing in onboarding docs), and `match` partitions the curated set against the user's enabled servers from the backend into ready-to-activate vs partially-installed, so a human can see in the terminal what the LLM-facing tool would suggest. The LLM tool has always been primary surface, but "what bundles exist?" is a frequent enough support question that surfacing them in the CLI earns its keep. Match only counts `isActive: true` servers — disabled ones don't auto-activate, so they shouldn't count toward "ready" — matching the LLM tool's filter so both surfaces agree. Partial bundles sort fewest-missing first to match the discover inline hint ranking. `--json` emits machine-readable output (`{bundles}` for list, `{installed, ready, partial}` for match). Exit codes: 0 success, 1 match needs a token and none resolved, 2 match couldn't reach the backend.
8
+
5
9
  ## 0.39.0 — 2026-04-18
6
10
 
7
11
  - **`mcph servers` CLI subcommand** — Lists the servers currently configured for your account in the mcp.hosting dashboard, hitting the same `/api/connect/config` endpoint that `runServer` polls at startup. Fills a gap between `mcph doctor` (local state: config files, clients, state.json) and the web dashboard: users can sanity-check their dashboard edits from the terminal, support engineers can ask for `mcph servers --json` output in a ticket, and scripts can pick a namespace up-front before piping into `mcph compliance` or `mcph install`. Table view groups the relevant columns (namespace, name, type, enabled/disabled, compliance grade, cached tool count) and is sorted alphabetically by namespace for diffable re-runs; `--json` emits the raw backend response verbatim. Exit codes: 0 success, 1 no token, 2 fetch error.
package/dist/index.js CHANGED
@@ -1,104 +1,76 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/compliance-cmd.ts
4
- import { spawn } from "child_process";
5
- import { request } from "undici";
6
- async function runComplianceCommand(argv) {
7
- const publish = argv.includes("--publish");
8
- const args = argv.filter((a) => a !== "--publish");
9
- if (args.length === 0) {
10
- process.stderr.write(
11
- '\n Usage: mcph compliance <target> [extraArgs...] [--publish]\n\n Examples:\n mcph compliance "npx -y @modelcontextprotocol/server-filesystem /tmp"\n mcph compliance https://example.com/mcp --publish\n\n'
12
- );
13
- return 1;
3
+ // src/bundles.ts
4
+ var CURATED_BUNDLES = [
5
+ {
6
+ id: "devops-incident",
7
+ name: "DevOps Incident Triage",
8
+ description: "GitHub + PagerDuty + Slack for on-call triage",
9
+ namespaces: ["github", "pagerduty", "slack"],
10
+ category: "ops"
11
+ },
12
+ {
13
+ id: "pr-review",
14
+ name: "PR Review",
15
+ description: "GitHub + Linear for issue-to-PR traceability",
16
+ namespaces: ["github", "linear"],
17
+ category: "dev"
18
+ },
19
+ {
20
+ id: "growth-stack",
21
+ name: "Growth Stack",
22
+ description: "HubSpot + Slack + GA for lifecycle + funnel signals",
23
+ namespaces: ["hubspot", "slack", "ga"],
24
+ category: "growth"
25
+ },
26
+ {
27
+ id: "data-ops",
28
+ name: "Data Ops",
29
+ description: "Postgres + S3 + Snowflake for pipeline debugging",
30
+ namespaces: ["postgres", "s3", "snowflake"],
31
+ category: "data"
32
+ },
33
+ {
34
+ id: "product-release",
35
+ name: "Product Release",
36
+ description: "GitHub + Linear + Slack for ship-day coordination",
37
+ namespaces: ["github", "linear", "slack"],
38
+ category: "dev"
39
+ },
40
+ {
41
+ id: "support-ops",
42
+ name: "Support Ops",
43
+ description: "Zendesk + Slack + HubSpot for escalation handoffs",
44
+ namespaces: ["zendesk", "slack", "hubspot"],
45
+ category: "ops"
14
46
  }
15
- const apiUrl6 = process.env.MCPH_URL ?? "https://mcp.hosting";
16
- const report = await runTest(args);
17
- if (!report) return 1;
18
- printSummary(report);
19
- if (publish) {
20
- const result = await publishReport(apiUrl6, report);
21
- if (!result) return 1;
22
- process.stdout.write(`
23
- Published: ${result.reportUrl}
24
- `);
25
- process.stdout.write(`Badge: ${result.badgeUrl}
26
- `);
27
- if (result.deleteToken) {
28
- process.stdout.write(`
29
- Delete token (save this): ${result.deleteToken}
30
- `);
47
+ ];
48
+ function matchBundles(installedNamespaces) {
49
+ const installed = new Set(installedNamespaces);
50
+ const ready = [];
51
+ const partial = [];
52
+ for (const bundle of CURATED_BUNDLES) {
53
+ const have = bundle.namespaces.filter((ns) => installed.has(ns));
54
+ const missing = bundle.namespaces.filter((ns) => !installed.has(ns));
55
+ if (missing.length === 0) {
56
+ ready.push(bundle);
57
+ } else if (have.length > 0) {
58
+ partial.push({ bundle, have, missing });
31
59
  }
32
60
  }
33
- return 0;
34
- }
35
- function runTest(args) {
36
- return new Promise((resolve4) => {
37
- const child = spawn("npx", ["-y", "@yawlabs/mcp-compliance", "test", "--format", "json", ...args], {
38
- stdio: ["ignore", "pipe", "inherit"],
39
- shell: process.platform === "win32"
40
- });
41
- let stdout = "";
42
- child.stdout.on("data", (chunk) => {
43
- stdout += chunk.toString();
44
- });
45
- child.on("error", (err) => {
46
- process.stderr.write(`
47
- Failed to launch mcp-compliance: ${err.message}
48
- `);
49
- resolve4(null);
50
- });
51
- child.on("close", (code) => {
52
- try {
53
- const parsed = JSON.parse(stdout);
54
- if (!parsed.grade || !parsed.summary) {
55
- process.stderr.write(`
56
- mcp-compliance returned unexpected JSON (exit ${code}).
57
- `);
58
- resolve4(null);
59
- return;
60
- }
61
- resolve4(parsed);
62
- } catch {
63
- process.stderr.write(`
64
- mcp-compliance exited ${code} without valid JSON output.
65
- `);
66
- resolve4(null);
67
- }
68
- });
69
- });
61
+ return { ready, partial };
70
62
  }
71
- function printSummary(report) {
72
- const { grade, score, summary, url } = report;
73
- process.stdout.write(
74
- `
75
- Compliance: ${grade} (${score.toFixed(1)}%) \u2014 ${summary.passed}/${summary.total} passed, ${summary.requiredPassed}/${summary.required} required
76
- Target: ${url}
77
- `
78
- );
63
+ function bundleActivateHint(bundle) {
64
+ return `mcp_connect_activate({ namespaces: ${JSON.stringify(bundle.namespaces)} })`;
79
65
  }
80
- async function publishReport(apiUrl6, report) {
81
- try {
82
- const res = await request(`${apiUrl6.replace(/\/$/, "")}/api/compliance/ext`, {
83
- method: "POST",
84
- headers: { "Content-Type": "application/json" },
85
- body: JSON.stringify(report)
86
- });
87
- if (res.statusCode !== 200) {
88
- const body = await res.body.text().catch(() => "");
89
- process.stderr.write(`
90
- Publish failed: HTTP ${res.statusCode}${body ? ` \u2014 ${body}` : ""}
91
- `);
92
- return null;
93
- }
94
- const parsed = await res.body.json();
95
- return parsed;
96
- } catch (err) {
97
- process.stderr.write(`
98
- Publish failed: ${err?.message ?? String(err)}
99
- `);
100
- return null;
101
- }
66
+ function topPartialBundles(installedNamespaces, limit) {
67
+ if (limit <= 0) return [];
68
+ const { partial } = matchBundles(installedNamespaces);
69
+ return partial.slice().sort((a, b) => {
70
+ if (a.missing.length !== b.missing.length) return a.missing.length - b.missing.length;
71
+ if (a.have.length !== b.have.length) return b.have.length - a.have.length;
72
+ return a.bundle.id.localeCompare(b.bundle.id);
73
+ }).slice(0, limit);
102
74
  }
103
75
 
104
76
  // src/config-loader.ts
@@ -468,7 +440,7 @@ function profileAllows(profile, namespace) {
468
440
  }
469
441
 
470
442
  // src/config.ts
471
- import { request as request2 } from "undici";
443
+ import { request } from "undici";
472
444
  async function fetchConfig(apiUrl6, token6, currentVersion) {
473
445
  const url = `${apiUrl6.replace(/\/$/, "")}/api/connect/config`;
474
446
  const headers = {
@@ -478,7 +450,7 @@ async function fetchConfig(apiUrl6, token6, currentVersion) {
478
450
  if (currentVersion) {
479
451
  headers["If-None-Match"] = `"${currentVersion}"`;
480
452
  }
481
- const res = await request2(url, {
453
+ const res = await request(url, {
482
454
  method: "GET",
483
455
  headers,
484
456
  headersTimeout: 1e4,
@@ -534,6 +506,249 @@ var ConfigError = class extends Error {
534
506
  fatal;
535
507
  };
536
508
 
509
+ // src/bundles-cmd.ts
510
+ var BUNDLES_USAGE = `Usage: mcph bundles [list|match] [--json]
511
+
512
+ Curated multi-server bundles \u2014 hand-picked stacks you can activate in one step.
513
+
514
+ list List every curated bundle (default, no network).
515
+ match Partition bundles against your installed servers (reads the backend).
516
+
517
+ --json Emit machine-readable JSON instead of a table.`;
518
+ function parseBundlesArgs(argv) {
519
+ let action = "list";
520
+ let json = false;
521
+ let actionSet = false;
522
+ for (const a of argv) {
523
+ if (a === "--json") {
524
+ json = true;
525
+ } else if (a === "--help" || a === "-h") {
526
+ return { ok: false, error: BUNDLES_USAGE };
527
+ } else if (a === "list" || a === "match") {
528
+ if (actionSet) {
529
+ return { ok: false, error: `mcph bundles: action already set to "${action}" (got "${a}")
530
+
531
+ ${BUNDLES_USAGE}` };
532
+ }
533
+ action = a;
534
+ actionSet = true;
535
+ } else {
536
+ return { ok: false, error: `mcph bundles: unknown argument "${a}"
537
+
538
+ ${BUNDLES_USAGE}` };
539
+ }
540
+ }
541
+ return { ok: true, options: { action, json } };
542
+ }
543
+ async function runBundlesCommand(opts = {}) {
544
+ const write = opts.out ?? ((s) => process.stdout.write(s));
545
+ const writeErr = opts.err ?? ((s) => process.stderr.write(s));
546
+ const lines = [];
547
+ const print = (s = "") => {
548
+ lines.push(s);
549
+ write(`${s}
550
+ `);
551
+ };
552
+ const printErr = (s) => {
553
+ lines.push(s);
554
+ writeErr(`${s}
555
+ `);
556
+ };
557
+ const action = opts.action ?? "list";
558
+ if (action === "list") {
559
+ if (opts.json) {
560
+ print(JSON.stringify({ bundles: CURATED_BUNDLES }, null, 2));
561
+ } else {
562
+ renderList(print);
563
+ }
564
+ return { exitCode: 0, lines };
565
+ }
566
+ const config = await loadMcphConfig({
567
+ cwd: opts.cwd,
568
+ home: opts.home,
569
+ env: opts.env
570
+ });
571
+ if (!config.token) {
572
+ printErr("mcph bundles match: no token resolved. Run `mcph install <client> --token mcp_pat_\u2026` or set MCPH_TOKEN.");
573
+ return { exitCode: 1, lines };
574
+ }
575
+ const fetcher = opts.fetcher ?? fetchConfig;
576
+ let backend;
577
+ try {
578
+ backend = await fetcher(config.apiBase, config.token);
579
+ } catch (err) {
580
+ const msg = err instanceof ConfigError || err instanceof Error ? err.message : String(err);
581
+ printErr(`mcph bundles match: ${msg}`);
582
+ return { exitCode: 2, lines };
583
+ }
584
+ if (!backend) {
585
+ printErr("mcph bundles match: backend returned no data (unexpected 304).");
586
+ return { exitCode: 2, lines };
587
+ }
588
+ const installed = backend.servers.filter((s) => s.isActive).map((s) => s.namespace);
589
+ const match = matchBundles(installed);
590
+ if (opts.json) {
591
+ print(JSON.stringify({ installed, ...match }, null, 2));
592
+ return { exitCode: 0, lines };
593
+ }
594
+ renderMatch(match, installed, print);
595
+ return { exitCode: 0, lines };
596
+ }
597
+ function renderList(print) {
598
+ print(`${CURATED_BUNDLES.length} curated bundles`);
599
+ print("");
600
+ const byCategory = /* @__PURE__ */ new Map();
601
+ for (const b of CURATED_BUNDLES) {
602
+ const list = byCategory.get(b.category) ?? [];
603
+ list.push(b);
604
+ byCategory.set(b.category, list);
605
+ }
606
+ const categories = [...byCategory.keys()].sort();
607
+ for (const cat of categories) {
608
+ const list = (byCategory.get(cat) ?? []).slice().sort((a, b) => a.id.localeCompare(b.id));
609
+ print(` [${cat}]`);
610
+ for (const b of list) {
611
+ print(` ${b.id.padEnd(18)} ${b.name}`);
612
+ print(` ${b.description}`);
613
+ print(` \u2192 ${bundleActivateHint(b)}`);
614
+ }
615
+ print("");
616
+ }
617
+ }
618
+ function renderMatch(match, installed, print) {
619
+ const installedList = installed.length === 0 ? "(none)" : installed.slice().sort().join(", ");
620
+ print(`Checked ${CURATED_BUNDLES.length} bundles against ${installed.length} enabled servers: ${installedList}`);
621
+ print("");
622
+ if (match.ready.length === 0 && match.partial.length === 0) {
623
+ print("No curated bundles match your current config.");
624
+ print("Run `mcph bundles list` to see the full catalog.");
625
+ return;
626
+ }
627
+ if (match.ready.length > 0) {
628
+ print("Ready to activate (every namespace installed):");
629
+ for (const b of match.ready.slice().sort((a, c) => a.id.localeCompare(c.id))) {
630
+ print(` ${b.id.padEnd(18)} ${b.description}`);
631
+ print(` \u2192 ${bundleActivateHint(b)}`);
632
+ }
633
+ print("");
634
+ }
635
+ if (match.partial.length > 0) {
636
+ const sorted = match.partial.slice().sort((a, b) => {
637
+ if (a.missing.length !== b.missing.length) return a.missing.length - b.missing.length;
638
+ if (a.have.length !== b.have.length) return b.have.length - a.have.length;
639
+ return a.bundle.id.localeCompare(b.bundle.id);
640
+ });
641
+ print("Partially installed (install more to complete):");
642
+ for (const entry of sorted) {
643
+ const have = entry.have.join(", ");
644
+ const missing = entry.missing.join(", ");
645
+ print(` ${entry.bundle.id.padEnd(18)} have: ${have}; missing: ${missing}`);
646
+ }
647
+ print("");
648
+ }
649
+ }
650
+
651
+ // src/compliance-cmd.ts
652
+ import { spawn } from "child_process";
653
+ import { request as request2 } from "undici";
654
+ async function runComplianceCommand(argv) {
655
+ const publish = argv.includes("--publish");
656
+ const args = argv.filter((a) => a !== "--publish");
657
+ if (args.length === 0) {
658
+ process.stderr.write(
659
+ '\n Usage: mcph compliance <target> [extraArgs...] [--publish]\n\n Examples:\n mcph compliance "npx -y @modelcontextprotocol/server-filesystem /tmp"\n mcph compliance https://example.com/mcp --publish\n\n'
660
+ );
661
+ return 1;
662
+ }
663
+ const apiUrl6 = process.env.MCPH_URL ?? "https://mcp.hosting";
664
+ const report = await runTest(args);
665
+ if (!report) return 1;
666
+ printSummary(report);
667
+ if (publish) {
668
+ const result = await publishReport(apiUrl6, report);
669
+ if (!result) return 1;
670
+ process.stdout.write(`
671
+ Published: ${result.reportUrl}
672
+ `);
673
+ process.stdout.write(`Badge: ${result.badgeUrl}
674
+ `);
675
+ if (result.deleteToken) {
676
+ process.stdout.write(`
677
+ Delete token (save this): ${result.deleteToken}
678
+ `);
679
+ }
680
+ }
681
+ return 0;
682
+ }
683
+ function runTest(args) {
684
+ return new Promise((resolve4) => {
685
+ const child = spawn("npx", ["-y", "@yawlabs/mcp-compliance", "test", "--format", "json", ...args], {
686
+ stdio: ["ignore", "pipe", "inherit"],
687
+ shell: process.platform === "win32"
688
+ });
689
+ let stdout = "";
690
+ child.stdout.on("data", (chunk) => {
691
+ stdout += chunk.toString();
692
+ });
693
+ child.on("error", (err) => {
694
+ process.stderr.write(`
695
+ Failed to launch mcp-compliance: ${err.message}
696
+ `);
697
+ resolve4(null);
698
+ });
699
+ child.on("close", (code) => {
700
+ try {
701
+ const parsed = JSON.parse(stdout);
702
+ if (!parsed.grade || !parsed.summary) {
703
+ process.stderr.write(`
704
+ mcp-compliance returned unexpected JSON (exit ${code}).
705
+ `);
706
+ resolve4(null);
707
+ return;
708
+ }
709
+ resolve4(parsed);
710
+ } catch {
711
+ process.stderr.write(`
712
+ mcp-compliance exited ${code} without valid JSON output.
713
+ `);
714
+ resolve4(null);
715
+ }
716
+ });
717
+ });
718
+ }
719
+ function printSummary(report) {
720
+ const { grade, score, summary, url } = report;
721
+ process.stdout.write(
722
+ `
723
+ Compliance: ${grade} (${score.toFixed(1)}%) \u2014 ${summary.passed}/${summary.total} passed, ${summary.requiredPassed}/${summary.required} required
724
+ Target: ${url}
725
+ `
726
+ );
727
+ }
728
+ async function publishReport(apiUrl6, report) {
729
+ try {
730
+ const res = await request2(`${apiUrl6.replace(/\/$/, "")}/api/compliance/ext`, {
731
+ method: "POST",
732
+ headers: { "Content-Type": "application/json" },
733
+ body: JSON.stringify(report)
734
+ });
735
+ if (res.statusCode !== 200) {
736
+ const body = await res.body.text().catch(() => "");
737
+ process.stderr.write(`
738
+ Publish failed: HTTP ${res.statusCode}${body ? ` \u2014 ${body}` : ""}
739
+ `);
740
+ return null;
741
+ }
742
+ const parsed = await res.body.json();
743
+ return parsed;
744
+ } catch (err) {
745
+ process.stderr.write(`
746
+ Publish failed: ${err?.message ?? String(err)}
747
+ `);
748
+ return null;
749
+ }
750
+ }
751
+
537
752
  // src/doctor-cmd.ts
538
753
  import { existsSync, readFileSync, statSync } from "fs";
539
754
  import { readFile as readFile3 } from "fs/promises";
@@ -1004,7 +1219,7 @@ function selectFlakyNamespaces(entries, limit) {
1004
1219
  }
1005
1220
 
1006
1221
  // src/doctor-cmd.ts
1007
- var VERSION = true ? "0.39.0" : "dev";
1222
+ var VERSION = true ? "0.40.0" : "dev";
1008
1223
  async function runDoctor(opts = {}) {
1009
1224
  const lines = [];
1010
1225
  const write = opts.out ?? ((s) => process.stdout.write(s));
@@ -1965,79 +2180,6 @@ async function shutdownAnalytics() {
1965
2180
  dispatchBuffer.length = 0;
1966
2181
  }
1967
2182
 
1968
- // src/bundles.ts
1969
- var CURATED_BUNDLES = [
1970
- {
1971
- id: "devops-incident",
1972
- name: "DevOps Incident Triage",
1973
- description: "GitHub + PagerDuty + Slack for on-call triage",
1974
- namespaces: ["github", "pagerduty", "slack"],
1975
- category: "ops"
1976
- },
1977
- {
1978
- id: "pr-review",
1979
- name: "PR Review",
1980
- description: "GitHub + Linear for issue-to-PR traceability",
1981
- namespaces: ["github", "linear"],
1982
- category: "dev"
1983
- },
1984
- {
1985
- id: "growth-stack",
1986
- name: "Growth Stack",
1987
- description: "HubSpot + Slack + GA for lifecycle + funnel signals",
1988
- namespaces: ["hubspot", "slack", "ga"],
1989
- category: "growth"
1990
- },
1991
- {
1992
- id: "data-ops",
1993
- name: "Data Ops",
1994
- description: "Postgres + S3 + Snowflake for pipeline debugging",
1995
- namespaces: ["postgres", "s3", "snowflake"],
1996
- category: "data"
1997
- },
1998
- {
1999
- id: "product-release",
2000
- name: "Product Release",
2001
- description: "GitHub + Linear + Slack for ship-day coordination",
2002
- namespaces: ["github", "linear", "slack"],
2003
- category: "dev"
2004
- },
2005
- {
2006
- id: "support-ops",
2007
- name: "Support Ops",
2008
- description: "Zendesk + Slack + HubSpot for escalation handoffs",
2009
- namespaces: ["zendesk", "slack", "hubspot"],
2010
- category: "ops"
2011
- }
2012
- ];
2013
- function matchBundles(installedNamespaces) {
2014
- const installed = new Set(installedNamespaces);
2015
- const ready = [];
2016
- const partial = [];
2017
- for (const bundle of CURATED_BUNDLES) {
2018
- const have = bundle.namespaces.filter((ns) => installed.has(ns));
2019
- const missing = bundle.namespaces.filter((ns) => !installed.has(ns));
2020
- if (missing.length === 0) {
2021
- ready.push(bundle);
2022
- } else if (have.length > 0) {
2023
- partial.push({ bundle, have, missing });
2024
- }
2025
- }
2026
- return { ready, partial };
2027
- }
2028
- function bundleActivateHint(bundle) {
2029
- return `mcp_connect_activate({ namespaces: ${JSON.stringify(bundle.namespaces)} })`;
2030
- }
2031
- function topPartialBundles(installedNamespaces, limit) {
2032
- if (limit <= 0) return [];
2033
- const { partial } = matchBundles(installedNamespaces);
2034
- return partial.slice().sort((a, b) => {
2035
- if (a.missing.length !== b.missing.length) return a.missing.length - b.missing.length;
2036
- if (a.have.length !== b.have.length) return b.have.length - a.have.length;
2037
- return a.bundle.id.localeCompare(b.bundle.id);
2038
- }).slice(0, limit);
2039
- }
2040
-
2041
2183
  // src/compliance.ts
2042
2184
  var GRADE_ORDER = {
2043
2185
  A: 4,
@@ -4065,7 +4207,7 @@ function categorizeSpawnError(err) {
4065
4207
  }
4066
4208
  async function connectToUpstream(config, onDisconnect, onListChanged) {
4067
4209
  const client = new Client(
4068
- { name: "mcph", version: true ? "0.39.0" : "dev" },
4210
+ { name: "mcph", version: true ? "0.40.0" : "dev" },
4069
4211
  { capabilities: {} }
4070
4212
  );
4071
4213
  let transport;
@@ -4546,7 +4688,7 @@ var ConnectServer = class _ConnectServer {
4546
4688
  this.apiUrl = apiUrl6;
4547
4689
  this.token = token6;
4548
4690
  this.server = new Server(
4549
- { name: "mcph", version: true ? "0.39.0" : "dev" },
4691
+ { name: "mcph", version: true ? "0.40.0" : "dev" },
4550
4692
  {
4551
4693
  capabilities: {
4552
4694
  tools: { listChanged: true },
@@ -6709,6 +6851,7 @@ var KNOWN_SUBCOMMANDS = [
6709
6851
  "doctor",
6710
6852
  "reset-learning",
6711
6853
  "servers",
6854
+ "bundles",
6712
6855
  "help",
6713
6856
  "--help",
6714
6857
  "-h",
@@ -6738,6 +6881,14 @@ if (subcommand === "compliance") {
6738
6881
  process.exit(2);
6739
6882
  }
6740
6883
  runServersCommand(parsed.options).then((r) => process.exit(r.exitCode));
6884
+ } else if (subcommand === "bundles") {
6885
+ const parsed = parseBundlesArgs(process.argv.slice(3));
6886
+ if (!parsed.ok) {
6887
+ process.stderr.write(`${parsed.error}
6888
+ `);
6889
+ process.exit(2);
6890
+ }
6891
+ runBundlesCommand(parsed.options).then((r) => process.exit(r.exitCode));
6741
6892
  } else if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
6742
6893
  const installBlock = ` ${INSTALL_USAGE.replace(/^Usage: /, "").replace(/\n/g, "\n ")}`;
6743
6894
  process.stdout.write(
@@ -6749,6 +6900,7 @@ if (subcommand === "compliance") {
6749
6900
  mcph install <client> [flags] Auto-edit an MCP client's config to launch mcph
6750
6901
  mcph doctor Print loaded config + detected clients (support diagnostic)
6751
6902
  mcph servers [--json] List servers configured in your mcp.hosting dashboard
6903
+ mcph bundles [list|match] Browse curated multi-server bundles
6752
6904
  mcph compliance <target> [flags] Run the compliance suite against an MCP server
6753
6905
  mcph reset-learning Clear cross-session learning history (~/.mcph/state.json)
6754
6906
  mcph --version Print version
@@ -6771,7 +6923,7 @@ ${installBlock}
6771
6923
  );
6772
6924
  process.exit(0);
6773
6925
  } else if (subcommand === "--version" || subcommand === "-V") {
6774
- process.stdout.write(`mcph ${true ? "0.39.0" : "dev"}
6926
+ process.stdout.write(`mcph ${true ? "0.40.0" : "dev"}
6775
6927
  `);
6776
6928
  process.exit(0);
6777
6929
  } else if (subcommand && !subcommand.startsWith("-")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/mcph",
3
- "version": "0.39.0",
3
+ "version": "0.40.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)",