@yawlabs/mcph 0.39.0 → 0.41.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 +8 -0
- package/dist/index.js +437 -175
- package/package.json +1 -1
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.41.0 — 2026-04-18
|
|
6
|
+
|
|
7
|
+
- **`mcph doctor --json` — machine-readable diagnostic output** — Doctor already tracks a lot of state (config files, token source, env overrides, persisted learning, installed clients, shell-history shadow hits, upgrade availability, diagnosis summary) and the text output optimises for pasting into a support ticket. `--json` emits the same data as a single structured blob so dashboards, CI scripts, and support tooling can pick fields with `jq` instead of parsing the text layout. Token is fingerprinted the same way in both modes (never raw). Section data is 1:1 with the text renderer: config (token/apiBase/loadedFiles/warnings), env overrides (null when unset), state (path/savedAt/entries; `disabled: true` when `MCPH_DISABLE_PERSISTENCE` is set), reliability (same `selectFlakyNamespaces` rollup that `mcp_connect_health` and the text RELIABILITY section use), clients probe results, shell shadow hits, upgrade info, and the exit-code diagnosis. Completes the `--json` pattern across `servers`, `bundles`, and now `doctor` — every CLI that reads state has a pipeline mode.
|
|
8
|
+
|
|
9
|
+
## 0.40.0 — 2026-04-18
|
|
10
|
+
|
|
11
|
+
- **`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.
|
|
12
|
+
|
|
5
13
|
## 0.39.0 — 2026-04-18
|
|
6
14
|
|
|
7
15
|
- **`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/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
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
|
|
72
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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
|
|
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,8 +1219,9 @@ function selectFlakyNamespaces(entries, limit) {
|
|
|
1004
1219
|
}
|
|
1005
1220
|
|
|
1006
1221
|
// src/doctor-cmd.ts
|
|
1007
|
-
var VERSION = true ? "0.
|
|
1222
|
+
var VERSION = true ? "0.41.0" : "dev";
|
|
1008
1223
|
async function runDoctor(opts = {}) {
|
|
1224
|
+
if (opts.json) return runDoctorJson(opts);
|
|
1009
1225
|
const lines = [];
|
|
1010
1226
|
const write = opts.out ?? ((s) => process.stdout.write(s));
|
|
1011
1227
|
const print = (s = "") => {
|
|
@@ -1094,6 +1310,101 @@ async function runDoctor(opts = {}) {
|
|
|
1094
1310
|
}
|
|
1095
1311
|
return { exitCode, lines, snapshot: { version: VERSION, config, clients } };
|
|
1096
1312
|
}
|
|
1313
|
+
async function runDoctorJson(opts) {
|
|
1314
|
+
const lines = [];
|
|
1315
|
+
const write = opts.out ?? ((s) => process.stdout.write(s));
|
|
1316
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
1317
|
+
const home = opts.home ?? homedir4();
|
|
1318
|
+
const os = opts.os ?? CURRENT_OS;
|
|
1319
|
+
const env = opts.env ?? process.env;
|
|
1320
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1321
|
+
const config = await loadMcphConfig({ cwd, home, env });
|
|
1322
|
+
const clients = probeClients({ home, os, cwd });
|
|
1323
|
+
const envVarNames = [
|
|
1324
|
+
"MCPH_POLL_INTERVAL",
|
|
1325
|
+
"MCPH_SERVER_CAP",
|
|
1326
|
+
"MCPH_MIN_COMPLIANCE",
|
|
1327
|
+
"MCPH_AUTO_LOAD",
|
|
1328
|
+
"MCPH_PRUNE_RESPONSES"
|
|
1329
|
+
];
|
|
1330
|
+
const envOverrides = {};
|
|
1331
|
+
for (const name of envVarNames) {
|
|
1332
|
+
const raw = env[name];
|
|
1333
|
+
envOverrides[name] = raw === void 0 || raw === "" ? null : raw;
|
|
1334
|
+
}
|
|
1335
|
+
const persistRaw = env.MCPH_DISABLE_PERSISTENCE;
|
|
1336
|
+
const persistDisabled = persistRaw !== void 0 && persistRaw !== "" && (persistRaw === "1" || persistRaw.toLowerCase() === "true");
|
|
1337
|
+
const state = persistDisabled ? { disabled: true, path: null, savedAt: null, learningEntries: null, packHistoryEntries: null } : await (async () => {
|
|
1338
|
+
const filePath = join4(userConfigDir(home), STATE_FILENAME);
|
|
1339
|
+
const persisted = await loadState(filePath);
|
|
1340
|
+
const fresh = persisted.savedAt === 0;
|
|
1341
|
+
return {
|
|
1342
|
+
disabled: false,
|
|
1343
|
+
path: filePath,
|
|
1344
|
+
savedAt: fresh ? null : new Date(persisted.savedAt).toISOString(),
|
|
1345
|
+
learningEntries: fresh ? 0 : Object.keys(persisted.learning).length,
|
|
1346
|
+
packHistoryEntries: fresh ? 0 : persisted.packHistory.length
|
|
1347
|
+
};
|
|
1348
|
+
})();
|
|
1349
|
+
const reliability = [];
|
|
1350
|
+
if (!persistDisabled) {
|
|
1351
|
+
const filePath = join4(userConfigDir(home), STATE_FILENAME);
|
|
1352
|
+
const persisted = await loadState(filePath);
|
|
1353
|
+
if (persisted.savedAt !== 0) {
|
|
1354
|
+
const entries = Object.entries(persisted.learning).map(([namespace, usage]) => ({ namespace, usage }));
|
|
1355
|
+
for (const { namespace, usage } of selectFlakyNamespaces(entries, 5)) {
|
|
1356
|
+
reliability.push({
|
|
1357
|
+
namespace,
|
|
1358
|
+
dispatched: usage.dispatched,
|
|
1359
|
+
succeeded: usage.succeeded,
|
|
1360
|
+
successRate: usage.succeeded / usage.dispatched,
|
|
1361
|
+
lastUsedAt: new Date(usage.lastUsedAt).toISOString()
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
const shellShadows = scanShellHistoryForShadows({ home, env });
|
|
1367
|
+
const skipCheck = opts.skipRegistryCheck === true || Boolean(process.env.VITEST);
|
|
1368
|
+
const latest = skipCheck ? null : await fetchLatestVersion(opts.registryFetch);
|
|
1369
|
+
const stale = latest !== null && VERSION !== "dev" && compareSemver(VERSION, latest) < 0;
|
|
1370
|
+
let exitCode = 0;
|
|
1371
|
+
let summary;
|
|
1372
|
+
if (config.token === null) {
|
|
1373
|
+
exitCode = 1;
|
|
1374
|
+
summary = "No token resolved \u2014 mcph cannot start.";
|
|
1375
|
+
} else if (config.warnings.length > 0) {
|
|
1376
|
+
exitCode = 2;
|
|
1377
|
+
summary = "Token present, but warnings need attention.";
|
|
1378
|
+
} else {
|
|
1379
|
+
summary = stale ? "Healthy, but an upgrade is available." : "All good. mcph should start cleanly.";
|
|
1380
|
+
}
|
|
1381
|
+
const snapshotJson = {
|
|
1382
|
+
timestamp,
|
|
1383
|
+
version: VERSION,
|
|
1384
|
+
platform: os,
|
|
1385
|
+
token: { fingerprint: tokenFingerprint(config.token), source: config.tokenSource },
|
|
1386
|
+
apiBase: { value: config.apiBase, source: config.apiBaseSource },
|
|
1387
|
+
loadedFiles: config.loadedFiles.map((f) => ({
|
|
1388
|
+
scope: f.scope,
|
|
1389
|
+
path: f.path,
|
|
1390
|
+
...f.version !== void 0 ? { schemaVersion: f.version } : {},
|
|
1391
|
+
schemaAhead: f.version !== void 0 && f.version > CURRENT_SCHEMA_VERSION
|
|
1392
|
+
})),
|
|
1393
|
+
warnings: config.warnings,
|
|
1394
|
+
env: envOverrides,
|
|
1395
|
+
state,
|
|
1396
|
+
reliability,
|
|
1397
|
+
clients,
|
|
1398
|
+
shellShadows,
|
|
1399
|
+
upgrade: { current: VERSION, latest, stale },
|
|
1400
|
+
diagnosis: { exitCode, summary }
|
|
1401
|
+
};
|
|
1402
|
+
const blob = JSON.stringify(snapshotJson, null, 2);
|
|
1403
|
+
lines.push(blob);
|
|
1404
|
+
write(`${blob}
|
|
1405
|
+
`);
|
|
1406
|
+
return { exitCode, lines, snapshot: { version: VERSION, config, clients } };
|
|
1407
|
+
}
|
|
1097
1408
|
function renderEnvSection(opts) {
|
|
1098
1409
|
const { env, print } = opts;
|
|
1099
1410
|
const vars = [
|
|
@@ -1965,79 +2276,6 @@ async function shutdownAnalytics() {
|
|
|
1965
2276
|
dispatchBuffer.length = 0;
|
|
1966
2277
|
}
|
|
1967
2278
|
|
|
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
2279
|
// src/compliance.ts
|
|
2042
2280
|
var GRADE_ORDER = {
|
|
2043
2281
|
A: 4,
|
|
@@ -4065,7 +4303,7 @@ function categorizeSpawnError(err) {
|
|
|
4065
4303
|
}
|
|
4066
4304
|
async function connectToUpstream(config, onDisconnect, onListChanged) {
|
|
4067
4305
|
const client = new Client(
|
|
4068
|
-
{ name: "mcph", version: true ? "0.
|
|
4306
|
+
{ name: "mcph", version: true ? "0.41.0" : "dev" },
|
|
4069
4307
|
{ capabilities: {} }
|
|
4070
4308
|
);
|
|
4071
4309
|
let transport;
|
|
@@ -4546,7 +4784,7 @@ var ConnectServer = class _ConnectServer {
|
|
|
4546
4784
|
this.apiUrl = apiUrl6;
|
|
4547
4785
|
this.token = token6;
|
|
4548
4786
|
this.server = new Server(
|
|
4549
|
-
{ name: "mcph", version: true ? "0.
|
|
4787
|
+
{ name: "mcph", version: true ? "0.41.0" : "dev" },
|
|
4550
4788
|
{
|
|
4551
4789
|
capabilities: {
|
|
4552
4790
|
tools: { listChanged: true },
|
|
@@ -6709,6 +6947,7 @@ var KNOWN_SUBCOMMANDS = [
|
|
|
6709
6947
|
"doctor",
|
|
6710
6948
|
"reset-learning",
|
|
6711
6949
|
"servers",
|
|
6950
|
+
"bundles",
|
|
6712
6951
|
"help",
|
|
6713
6952
|
"--help",
|
|
6714
6953
|
"-h",
|
|
@@ -6727,7 +6966,21 @@ if (subcommand === "compliance") {
|
|
|
6727
6966
|
}
|
|
6728
6967
|
runInstall(parsed.options).then((r) => process.exit(r.exitCode));
|
|
6729
6968
|
} else if (subcommand === "doctor") {
|
|
6730
|
-
|
|
6969
|
+
const doctorArgs = process.argv.slice(3);
|
|
6970
|
+
const doctorJson = doctorArgs.includes("--json");
|
|
6971
|
+
const doctorUnknown = doctorArgs.find((a) => a !== "--json" && a !== "--help" && a !== "-h");
|
|
6972
|
+
if (doctorArgs.includes("--help") || doctorArgs.includes("-h")) {
|
|
6973
|
+
process.stdout.write(
|
|
6974
|
+
"Usage: mcph doctor [--json]\n\n Print a diagnostic of your mcph setup.\n\n --json Emit machine-readable JSON instead of text.\n"
|
|
6975
|
+
);
|
|
6976
|
+
process.exit(0);
|
|
6977
|
+
}
|
|
6978
|
+
if (doctorUnknown) {
|
|
6979
|
+
process.stderr.write(`mcph doctor: unknown argument "${doctorUnknown}"
|
|
6980
|
+
`);
|
|
6981
|
+
process.exit(2);
|
|
6982
|
+
}
|
|
6983
|
+
runDoctor({ json: doctorJson }).then((r) => process.exit(r.exitCode));
|
|
6731
6984
|
} else if (subcommand === "reset-learning") {
|
|
6732
6985
|
runResetLearning().then((r) => process.exit(r.exitCode));
|
|
6733
6986
|
} else if (subcommand === "servers") {
|
|
@@ -6738,6 +6991,14 @@ if (subcommand === "compliance") {
|
|
|
6738
6991
|
process.exit(2);
|
|
6739
6992
|
}
|
|
6740
6993
|
runServersCommand(parsed.options).then((r) => process.exit(r.exitCode));
|
|
6994
|
+
} else if (subcommand === "bundles") {
|
|
6995
|
+
const parsed = parseBundlesArgs(process.argv.slice(3));
|
|
6996
|
+
if (!parsed.ok) {
|
|
6997
|
+
process.stderr.write(`${parsed.error}
|
|
6998
|
+
`);
|
|
6999
|
+
process.exit(2);
|
|
7000
|
+
}
|
|
7001
|
+
runBundlesCommand(parsed.options).then((r) => process.exit(r.exitCode));
|
|
6741
7002
|
} else if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
|
|
6742
7003
|
const installBlock = ` ${INSTALL_USAGE.replace(/^Usage: /, "").replace(/\n/g, "\n ")}`;
|
|
6743
7004
|
process.stdout.write(
|
|
@@ -6747,8 +7008,9 @@ if (subcommand === "compliance") {
|
|
|
6747
7008
|
Usage:
|
|
6748
7009
|
mcph Run as MCP server (requires a token)
|
|
6749
7010
|
mcph install <client> [flags] Auto-edit an MCP client's config to launch mcph
|
|
6750
|
-
mcph doctor
|
|
7011
|
+
mcph doctor [--json] Print loaded config + detected clients (support diagnostic)
|
|
6751
7012
|
mcph servers [--json] List servers configured in your mcp.hosting dashboard
|
|
7013
|
+
mcph bundles [list|match] Browse curated multi-server bundles
|
|
6752
7014
|
mcph compliance <target> [flags] Run the compliance suite against an MCP server
|
|
6753
7015
|
mcph reset-learning Clear cross-session learning history (~/.mcph/state.json)
|
|
6754
7016
|
mcph --version Print version
|
|
@@ -6771,7 +7033,7 @@ ${installBlock}
|
|
|
6771
7033
|
);
|
|
6772
7034
|
process.exit(0);
|
|
6773
7035
|
} else if (subcommand === "--version" || subcommand === "-V") {
|
|
6774
|
-
process.stdout.write(`mcph ${true ? "0.
|
|
7036
|
+
process.stdout.write(`mcph ${true ? "0.41.0" : "dev"}
|
|
6775
7037
|
`);
|
|
6776
7038
|
process.exit(0);
|
|
6777
7039
|
} else if (subcommand && !subcommand.startsWith("-")) {
|
package/package.json
CHANGED