@proofofwork-agency/toolpin 0.2.3

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 (61) hide show
  1. package/CONTRIBUTING.md +117 -0
  2. package/LICENSE +183 -0
  3. package/README.md +323 -0
  4. package/SECURITY.md +61 -0
  5. package/action.yml +134 -0
  6. package/dist/canonicalJson.js +38 -0
  7. package/dist/capabilities.js +139 -0
  8. package/dist/ci.js +26 -0
  9. package/dist/cli.js +1843 -0
  10. package/dist/clientSupport.js +76 -0
  11. package/dist/codexToml.js +213 -0
  12. package/dist/config.js +337 -0
  13. package/dist/constants.js +3 -0
  14. package/dist/continueYaml.js +76 -0
  15. package/dist/doctor.js +163 -0
  16. package/dist/install.js +191 -0
  17. package/dist/installed.js +405 -0
  18. package/dist/integrity.js +14 -0
  19. package/dist/inventory.js +169 -0
  20. package/dist/packageIntegrity.js +153 -0
  21. package/dist/plan.js +595 -0
  22. package/dist/policy.js +310 -0
  23. package/dist/registry.js +1610 -0
  24. package/dist/runtimeAdvisory.js +80 -0
  25. package/dist/safeFetch.js +157 -0
  26. package/dist/sarif.js +162 -0
  27. package/dist/scan.js +113 -0
  28. package/dist/search.js +44 -0
  29. package/dist/secrets.js +165 -0
  30. package/dist/signing.js +146 -0
  31. package/dist/tester.js +240 -0
  32. package/dist/trust.js +528 -0
  33. package/dist/tui/app.js +1731 -0
  34. package/dist/tui/command.js +50 -0
  35. package/dist/tui/configSnippet.js +11 -0
  36. package/dist/tui/constants.js +37 -0
  37. package/dist/tui/format.js +31 -0
  38. package/dist/tui/installedState.js +23 -0
  39. package/dist/tui/layout.js +65 -0
  40. package/dist/tui/selectors.js +282 -0
  41. package/dist/tui/types.js +1 -0
  42. package/dist/tui/ui/trust.js +77 -0
  43. package/dist/tui/views/installed.js +82 -0
  44. package/dist/tui/views/panels.js +637 -0
  45. package/dist/tui.js +12 -0
  46. package/dist/types.js +1 -0
  47. package/dist/verificationTrust.js +103 -0
  48. package/dist/verify.js +537 -0
  49. package/dist/version.js +1 -0
  50. package/dist/versions.js +127 -0
  51. package/docs/assets/readme/terminal-demo.svg +174 -0
  52. package/docs/assets/readme/tui-browse-overview.jpg +0 -0
  53. package/docs/assets/readme/tui-config-preview.jpg +0 -0
  54. package/docs/assets/readme/tui-help.jpg +0 -0
  55. package/docs/assets/readme/tui-installed-inventory.jpg +0 -0
  56. package/docs/how-to/catch-drift-in-ci.md +189 -0
  57. package/docs/how-to/custom-registries.md +156 -0
  58. package/docs/how-to/toolpin-curated-registry.md +153 -0
  59. package/package.json +76 -0
  60. package/registry/README.md +92 -0
  61. package/registry/v0/servers +115 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1843 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { verifyFrozenInstall } from "./ci.js";
4
+ import { clientsForScope, exportClientConfig, isClientName, PROJECT_CLIENTS } from "./config.js";
5
+ import { codexTomlFromClientConfig } from "./codexToml.js";
6
+ import { continueYamlFromClientConfig } from "./continueYaml.js";
7
+ import { installableClientsForServer } from "./clientSupport.js";
8
+ import { doctorLockfile } from "./doctor.js";
9
+ import { adoptInstalledServer, testInstalledServer, updateAllInstalledServers, updateInstalledServer } from "./installed.js";
10
+ import { installServerConfig, removeServerConfig } from "./install.js";
11
+ import { listInstalledServers } from "./inventory.js";
12
+ import { buildInstallPlan, readLockfile, readLockfileDigest, removeLockfileEntry, verifyAgainstLockfile, writeLockfile } from "./plan.js";
13
+ import { DEFAULT_LOCKFILE_PATH, DEFAULT_POLICY_PATH, DEFAULT_SIGNATURE_PATH } from "./constants.js";
14
+ import { enforcePolicy, evaluatePolicy, readPolicy, readPolicyDigest } from "./policy.js";
15
+ import { CacheSchemaError, enrichGlamaTarget, enrichSmitheryTarget, fetchRegistry, latestOnly, listRegistrySources, listRegistrySourceStatuses, normalizeEntries, readCache, readCacheMetadata, refreshCache, updateRegistrySourceEnabled } from "./registry.js";
16
+ import { searchServers } from "./search.js";
17
+ import { scanServerMetadata, scanToolDescriptions } from "./scan.js";
18
+ import { auditSecrets } from "./secrets.js";
19
+ import { ciSarifResult, ciSarifResults, sarifLog, scanSarifResults, verificationSarifResults } from "./sarif.js";
20
+ import { readPublicKeyFingerprint, signLockfile, verifyLockfileSignature } from "./signing.js";
21
+ import { testServer } from "./tester.js";
22
+ import { evidenceStatus, evidenceSummary, hasFreshTrustedArtifactEvidence, scoreServer, trustCapExplanation, trustedArtifactEvidenceProblem, trustProfileScore, trustTier } from "./trust.js";
23
+ import { localHttpRuntimeAdvisory } from "./runtimeAdvisory.js";
24
+ import { verifyServer } from "./verify.js";
25
+ import { TOOLPIN_VERSION } from "./version.js";
26
+ import { compareLockedToLatest, knownVersions } from "./versions.js";
27
+ const args = normalizeArgs(process.argv.slice(2));
28
+ const CLIENT_USAGE = "claude|cursor|vscode|codex|opencode|windsurf|cline|continue|gemini|zed|roo|generic|all";
29
+ const TOOLPIN_NPM_PACKAGE = "@proofofwork-agency/toolpin";
30
+ const VALUE_FLAGS = new Set([
31
+ "-c",
32
+ "-s",
33
+ "--client",
34
+ "--expect-digest",
35
+ "--file",
36
+ "--key",
37
+ "--limit",
38
+ "--pages",
39
+ "--package-manager",
40
+ "--policy",
41
+ "--public-key",
42
+ "--scope",
43
+ "--signature",
44
+ "--source",
45
+ "--target",
46
+ "--timeout",
47
+ "--version",
48
+ ]);
49
+ const KNOWN_FLAGS = new Set([
50
+ ...VALUE_FLAGS,
51
+ "--all",
52
+ "--allow-hosted-directory-targets",
53
+ "--dry-run",
54
+ "--global",
55
+ "-g",
56
+ "--help",
57
+ "-h",
58
+ "--json",
59
+ "--live",
60
+ "--no-policy",
61
+ "--project",
62
+ "--require-verified",
63
+ "-p",
64
+ "--sarif",
65
+ "--skip-live-verification",
66
+ "--skip-live-verify",
67
+ "--update-lock",
68
+ "--verify",
69
+ "-v",
70
+ ]);
71
+ const OK_COLOR = "\x1b[32m";
72
+ const WARN_COLOR = "\x1b[33m";
73
+ const ERR_COLOR = "\x1b[31m";
74
+ const CYAN_COLOR = "\x1b[36m";
75
+ const MUTED_COLOR = "\x1b[90m";
76
+ main().catch((error) => {
77
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
78
+ process.exitCode = 1;
79
+ });
80
+ async function main() {
81
+ const command = args[0] ?? "help";
82
+ const rest = args.slice(1);
83
+ if (command !== "help" && command !== "--help" && command !== "-h") {
84
+ validateFlags(command, rest);
85
+ if (isHelp(rest)) {
86
+ commandHelp(command);
87
+ return;
88
+ }
89
+ }
90
+ switch (command) {
91
+ case "version":
92
+ case "--version":
93
+ case "-v":
94
+ console.log(`toolpin ${TOOLPIN_VERSION}`);
95
+ return;
96
+ case "upgrade":
97
+ await upgrade(rest);
98
+ return;
99
+ case "ingest":
100
+ await ingest(rest);
101
+ return;
102
+ case "search":
103
+ await search(rest);
104
+ return;
105
+ case "info":
106
+ await info(rest);
107
+ return;
108
+ case "audit":
109
+ await audit(rest);
110
+ return;
111
+ case "scan":
112
+ await scan(rest);
113
+ return;
114
+ case "verify":
115
+ await verify(rest);
116
+ return;
117
+ case "versions":
118
+ await versions(rest);
119
+ return;
120
+ case "registry":
121
+ await registry(rest);
122
+ return;
123
+ case "sources":
124
+ await registry(["list", ...rest]);
125
+ return;
126
+ case "outdated":
127
+ await outdated(rest);
128
+ return;
129
+ case "list":
130
+ case "ls":
131
+ case "installed":
132
+ await listInstalled(rest);
133
+ return;
134
+ case "plan":
135
+ await plan(rest);
136
+ return;
137
+ case "install":
138
+ await install(rest);
139
+ return;
140
+ case "adopt":
141
+ await adoptInstalled(rest);
142
+ return;
143
+ case "update":
144
+ await updateInstalled(rest);
145
+ return;
146
+ case "policy":
147
+ await policy(rest);
148
+ return;
149
+ case "secrets":
150
+ await secrets(rest);
151
+ return;
152
+ case "remove":
153
+ await remove(rest, "remove");
154
+ return;
155
+ case "uninstall":
156
+ await remove(rest, "uninstall");
157
+ return;
158
+ case "ci":
159
+ await ci(rest);
160
+ return;
161
+ case "doctor":
162
+ await doctor(rest);
163
+ return;
164
+ case "test":
165
+ await test(rest);
166
+ return;
167
+ case "test-installed":
168
+ await testInstalled(rest);
169
+ return;
170
+ case "lock":
171
+ await lock(rest);
172
+ return;
173
+ case "export-config":
174
+ await exportConfig(rest);
175
+ return;
176
+ case "tui":
177
+ await runTui(rest);
178
+ return;
179
+ case "help":
180
+ case "--help":
181
+ case "-h":
182
+ help();
183
+ return;
184
+ default:
185
+ throw new Error(`Unknown command: ${command}. Run \`toolpin help\`.`);
186
+ }
187
+ }
188
+ async function upgrade(rest) {
189
+ const dryRun = hasFlag(rest, "--dry-run");
190
+ const json = hasFlag(rest, "--json");
191
+ const target = stringFlag(rest, "--target", "latest");
192
+ const packageManager = upgradePackageManager(rest);
193
+ const command = upgradeCommand(packageManager, target);
194
+ const result = {
195
+ package: TOOLPIN_NPM_PACKAGE,
196
+ currentVersion: TOOLPIN_VERSION,
197
+ target,
198
+ packageManager,
199
+ command: [command.executable, ...command.args],
200
+ dryRun,
201
+ };
202
+ if (json) {
203
+ if (!dryRun)
204
+ throw new Error("toolpin upgrade --json requires --dry-run because package-manager output is streamed directly.");
205
+ console.log(JSON.stringify(result, null, 2));
206
+ return;
207
+ }
208
+ printHeader("ToolPin Upgrade");
209
+ printField("current", TOOLPIN_VERSION);
210
+ printField("target", target);
211
+ printField("command", command.display);
212
+ if (dryRun) {
213
+ printField("status", "dry run; no changes made", WARN_COLOR);
214
+ return;
215
+ }
216
+ await runUpgradeCommand(command);
217
+ printField("status", "upgrade command completed", OK_COLOR);
218
+ printBullet("Run `tpn -v` or `toolpin --version` in a new shell to verify the active binary.");
219
+ }
220
+ async function ingest(rest) {
221
+ const limit = numberFlag(rest, "--limit", 100);
222
+ const pages = numberFlag(rest, "--pages", 10);
223
+ const source = sourceFlag(rest, "all");
224
+ const result = await refreshCache({ limit, maxPages: pages, source });
225
+ const entries = result.entries;
226
+ console.log(`Cached ${entries.length} registry versions from ${source} in .toolpin/registry-cache.json`);
227
+ if (result.lastError)
228
+ console.error(`Source diagnostics: ${result.lastError}`);
229
+ }
230
+ async function search(rest) {
231
+ const query = positional(rest).join(" ");
232
+ if (!query)
233
+ throw new Error("Usage: toolpin search <query> [--limit 10] [--live] [--json]");
234
+ const limit = numberFlag(rest, "--limit", 10);
235
+ const servers = await loadServers(rest, { search: query });
236
+ const results = searchServers(latestOnly(servers), query, limit);
237
+ if (hasFlag(rest, "--json")) {
238
+ console.log(JSON.stringify({ query, count: results.length, results }, null, 2));
239
+ return;
240
+ }
241
+ printHeader(`Search results for "${query}"`);
242
+ for (const result of results) {
243
+ const server = result.server;
244
+ const packages = server.packageTypes.length ? server.packageTypes.join(",") : "none";
245
+ const remotes = server.remoteTypes.length ? server.remoteTypes.join(",") : "none";
246
+ printSubhead(`${server.name}@${server.version}`);
247
+ printField("title", server.title);
248
+ if (server.description)
249
+ printField("about", truncate(server.description, 140));
250
+ printField("source", `${server.registrySource} trust ${trustTier(result.trust)} / ${trustProfileScore(result.trust)}% profile / ${evidenceStatus(result.trust)}`);
251
+ printField("targets", `packages ${packages}; remotes ${remotes}`);
252
+ printField("evidence", evidenceSummary(result.trust));
253
+ printCapExplanation(result.trust);
254
+ if (result.trust.badges.length)
255
+ printField("badges", result.trust.badges.join(", "));
256
+ }
257
+ }
258
+ async function info(rest) {
259
+ const name = positional(rest)[0];
260
+ if (!name)
261
+ throw new Error("Usage: toolpin info <server-name> [--json] [--live]");
262
+ const server = await findServer(rest, name);
263
+ const trust = scoreServer(server);
264
+ if (hasFlag(rest, "--json")) {
265
+ console.log(JSON.stringify({ server, trust }, null, 2));
266
+ return;
267
+ }
268
+ printHeader(`${server.name}@${server.version}`);
269
+ printField("title", server.title);
270
+ if (server.description)
271
+ printField("about", server.description);
272
+ if (server.repositoryUrl)
273
+ printField("repo", server.repositoryUrl);
274
+ printField("packages", server.packageTypes.join(", ") || "none");
275
+ printField("remotes", server.remoteTypes.join(", ") || "none");
276
+ printField("registry", server.registrySource);
277
+ if (server.resolutionNote)
278
+ printField("resolved", server.resolutionNote, WARN_COLOR);
279
+ printField("trust", `${trustTier(trust)} / ${trustProfileScore(trust)}% profile / ${evidenceStatus(trust)}`);
280
+ printField("evidence", evidenceSummary(trust));
281
+ printCapExplanation(trust);
282
+ if (trust.gatedBy?.length)
283
+ printField("gated by", trust.gatedBy.join(", "));
284
+ if (trust.badges.length)
285
+ printField("badges", trust.badges.join(", "));
286
+ for (const issue of trust.issues) {
287
+ printBullet(`${issue.severity.toUpperCase()}: ${issue.message}`);
288
+ }
289
+ }
290
+ async function audit(rest) {
291
+ const values = positional(rest);
292
+ if (values[0] === "server") {
293
+ await auditServer(rest.filter((value, index) => index !== 0));
294
+ return;
295
+ }
296
+ if (values[0]) {
297
+ console.error("Warning: `toolpin audit <server>` is deprecated; use `toolpin audit server <server>` for a one-server trust report.");
298
+ await auditServer(rest);
299
+ return;
300
+ }
301
+ const path = stringFlag(rest, "--file", DEFAULT_LOCKFILE_PATH);
302
+ const policyPath = stringFlag(rest, "--policy", DEFAULT_POLICY_PATH);
303
+ const scope = scopeFlag(rest, "all");
304
+ const client = hasAnyFlag(rest, ["--client", "-c"]) ? clientFlag(rest, "all") : "all";
305
+ if (scope !== "all" && scope !== "project" && scope !== "global")
306
+ throw new Error("--scope must be all, project, or global");
307
+ const findings = [];
308
+ const [inventory, doctorReport, secretsReport, lockfile, policyConfig] = await Promise.all([
309
+ listInstalledServers({ scope, client }),
310
+ doctorLockfile(path, scope).catch((error) => ({ ok: false, checked: 0, issues: [{ key: path, kind: "unreadable", client: "generic", serverName: path, file: path, message: error instanceof Error ? error.message : String(error) }] })),
311
+ auditSecrets(path, scope).catch((error) => ({ ok: false, checked: 0, findings: [{ kind: "unreadable_config", key: path, client: "generic", serverName: path, file: path, message: error instanceof Error ? error.message : String(error) }] })),
312
+ readLockfile(path).catch((error) => {
313
+ findings.push({ code: "lockfile_unreadable", severity: "critical", message: error instanceof Error ? error.message : String(error), key: path });
314
+ return undefined;
315
+ }),
316
+ readPolicy(policyPath).catch((error) => {
317
+ findings.push({ code: "policy_unreadable", severity: "critical", message: error instanceof Error ? error.message : String(error), key: policyPath });
318
+ return undefined;
319
+ }),
320
+ ]);
321
+ for (const issue of inventory.issues)
322
+ findings.push({ code: `inventory_${issue.kind}`, severity: "critical", message: issue.message, key: issue.file });
323
+ for (const issue of doctorReport.issues)
324
+ findings.push({ code: `doctor_${issue.kind}`, severity: issue.kind === "drift" || issue.kind === "unreadable" ? "critical" : "warning", message: issue.message, key: issue.key });
325
+ for (const finding of secretsReport.findings)
326
+ findings.push({ code: `secret_${finding.kind}`, severity: finding.kind === "plaintext_secret" || finding.kind === "secret_prefix" ? "critical" : "warning", message: finding.message, key: finding.key });
327
+ const policyReports = [];
328
+ const verificationReports = [];
329
+ if (lockfile) {
330
+ for (const [key, locked] of Object.entries(lockfile.servers)) {
331
+ const problem = trustedArtifactEvidenceProblem(locked.trust.evidence ?? []);
332
+ const hasArtifactEvidence = (locked.trust.evidence ?? []).some((entry) => entry.code === "oci_digest_verified" || entry.code === "mcpb_sha256_verified" || entry.code === "npm_integrity_verified");
333
+ if (hasArtifactEvidence && problem)
334
+ findings.push({ code: "verified_evidence_incomplete", severity: "warning", message: `${key}: ${problem}`, key });
335
+ if (hasFlag(rest, "--require-verified") && (locked.trust.verifiedProvenance !== true || !hasFreshTrustedArtifactEvidence(locked.trust.evidence ?? []))) {
336
+ findings.push({ code: "require_verified_failed", severity: "critical", message: `${key}: ${locked.trust.verifiedProvenance === true ? problem : "missing verified provenance"}`, key });
337
+ }
338
+ if (policyConfig) {
339
+ const policyReport = evaluatePolicy(locked, policyConfig);
340
+ policyReports.push(policyReport);
341
+ for (const issue of policyReport.issues)
342
+ findings.push({ code: `policy_${issue.code}`, severity: "critical", message: issue.message, key: policyReport.key });
343
+ }
344
+ if (hasFlag(rest, "--verify")) {
345
+ try {
346
+ const server = await findExactServer(rest, locked.name, locked.resolved?.source ?? sourceFlag(rest, "all"));
347
+ const liveVerification = liveVerificationEnabled(rest);
348
+ const verification = await verifyServer(server, {
349
+ liveRemoteProbe: liveVerification,
350
+ livePackageProbe: liveVerification,
351
+ timeoutMs: numberFlag(rest, "--timeout", 15000),
352
+ requireVerified: hasFlag(rest, "--require-verified"),
353
+ });
354
+ verificationReports.push(verification);
355
+ if (!verification.ok)
356
+ findings.push({ code: "verification_failed", severity: "critical", message: `${key}: verification failed`, key });
357
+ if (hasFlag(rest, "--require-verified") && verificationOutcome(verification) !== "verified") {
358
+ findings.push({ code: "require_verified_failed", severity: "critical", message: `${key}: verification is ${verificationOutcome(verification)}`, key });
359
+ }
360
+ }
361
+ catch (error) {
362
+ findings.push({ code: "verification_unavailable", severity: hasFlag(rest, "--require-verified") ? "critical" : "warning", message: `${key}: ${error instanceof Error ? error.message : String(error)}`, key });
363
+ }
364
+ }
365
+ }
366
+ }
367
+ const report = {
368
+ ok: !findings.some((finding) => finding.severity === "critical"),
369
+ checked: {
370
+ lockfile: lockfile ? Object.keys(lockfile.servers).length : 0,
371
+ inventory: inventory.checked,
372
+ doctor: doctorReport.checked,
373
+ secrets: secretsReport.checked,
374
+ },
375
+ findings,
376
+ inventory,
377
+ doctor: doctorReport,
378
+ secrets: secretsReport,
379
+ policy: policyConfig ? { ok: policyReports.every((entry) => entry.ok), reports: policyReports } : undefined,
380
+ verification: hasFlag(rest, "--verify") ? { ok: verificationReports.every((entry) => entry.ok), reports: verificationReports } : undefined,
381
+ };
382
+ if (hasFlag(rest, "--json")) {
383
+ console.log(JSON.stringify(report, null, 2));
384
+ }
385
+ else {
386
+ printHeader(report.ok ? "Audit OK" : "Audit findings");
387
+ printField("lockfile", path);
388
+ printField("checked", `${report.checked.lockfile} locked, ${report.checked.inventory} config file(s)`);
389
+ for (const finding of findings)
390
+ printBullet(`${finding.severity.toUpperCase()}: ${finding.code}${finding.key ? ` ${finding.key}` : ""}: ${finding.message}`);
391
+ }
392
+ if (!report.ok)
393
+ process.exitCode = 1;
394
+ }
395
+ async function auditServer(rest) {
396
+ const name = positional(rest)[0];
397
+ if (!name)
398
+ throw new Error("Usage: toolpin audit [--file mcp-lock.json] [--scope all|project|global] [--client all] [--verify] [--require-verified] [--json]\n toolpin audit server <server-name> [--live] [--json]");
399
+ const server = await findServer(rest, name);
400
+ const trust = scoreServer(server);
401
+ if (hasFlag(rest, "--json")) {
402
+ console.log(JSON.stringify({ kind: "server_trust_report", name: server.name, version: server.version, trust }, null, 2));
403
+ return;
404
+ }
405
+ printHeader(`Server trust report: ${server.name}@${server.version}`);
406
+ printField("trust", `${trustTier(trust)} / ${trustProfileScore(trust)}% profile / ${evidenceStatus(trust)}`);
407
+ printField("evidence", evidenceSummary(trust));
408
+ printCapExplanation(trust);
409
+ }
410
+ async function scan(rest) {
411
+ const name = positional(rest)[0];
412
+ if (!name)
413
+ throw new Error("Usage: toolpin scan <server-name> [--source toolpin|official|docker|all|custom-id] [--live] [--json] [--sarif] [--timeout 15000]");
414
+ const server = await findServer(rest, name);
415
+ const generatedAt = new Date().toISOString();
416
+ const scans = [scanServerMetadata(server, generatedAt)];
417
+ let liveProbe;
418
+ if (hasFlag(rest, "--live")) {
419
+ liveProbe = await testServer(server, numberFlag(rest, "--timeout", 15000));
420
+ if (liveProbe.ok) {
421
+ scans.push(scanToolDescriptions(liveProbe.tools, { generatedAt }));
422
+ }
423
+ else if (!hasAnyFlag(rest, ["--json", "--sarif"])) {
424
+ console.error(`Live probe skipped tool-description scan: ${liveProbe.message}`);
425
+ }
426
+ }
427
+ const findings = scans.flatMap((entry) => entry.findings);
428
+ if (hasFlag(rest, "--sarif")) {
429
+ console.log(JSON.stringify(sarifLog(scanSarifResults(scans), { generatedAt, executionSuccessful: true }), null, 2));
430
+ return;
431
+ }
432
+ if (hasFlag(rest, "--json")) {
433
+ console.log(JSON.stringify({
434
+ server: {
435
+ name: server.name,
436
+ version: server.version,
437
+ registrySource: server.registrySource,
438
+ },
439
+ liveProbe: liveProbe ? { ok: liveProbe.ok, message: liveProbe.message, toolCount: liveProbe.tools.length } : undefined,
440
+ scannedDescriptions: scans.reduce((count, entry) => count + entry.scannedDescriptions, 0),
441
+ findings,
442
+ scans,
443
+ }, null, 2));
444
+ return;
445
+ }
446
+ printHeader(`Description scan: ${server.name}@${server.version}`);
447
+ printField("registry", `${server.registrySource} metadata`);
448
+ printField("scanned", `${scans.reduce((count, entry) => count + entry.scannedDescriptions, 0)} description(s)`);
449
+ printField("findings", `${findings.length} advisory finding(s)`);
450
+ for (const finding of findings) {
451
+ printBullet(`${finding.severity.toUpperCase()}: ${finding.code}: ${finding.subject}: ${finding.message}`);
452
+ }
453
+ }
454
+ async function verify(rest) {
455
+ const name = positional(rest)[0];
456
+ if (!name)
457
+ throw new Error("Usage: toolpin verify <server-name> [--source toolpin|official|docker|all|custom-id] [--live] [--json] [--sarif] [--timeout 15000] [--skip-live-verification] [--require-verified]");
458
+ const server = await findServer(rest, name);
459
+ const liveVerification = liveVerificationEnabled(rest);
460
+ const report = await verifyServer(server, {
461
+ liveRemoteProbe: liveVerification,
462
+ livePackageProbe: liveVerification,
463
+ timeoutMs: numberFlag(rest, "--timeout", 15000),
464
+ requireVerified: hasFlag(rest, "--require-verified"),
465
+ });
466
+ if (hasFlag(rest, "--sarif")) {
467
+ console.log(JSON.stringify(sarifLog(verificationSarifResults(report), { executionSuccessful: report.ok }), null, 2));
468
+ }
469
+ else if (hasFlag(rest, "--json")) {
470
+ console.log(JSON.stringify(report, null, 2));
471
+ }
472
+ else {
473
+ printVerificationReport(report);
474
+ }
475
+ if (!report.ok) {
476
+ process.exitCode = 1;
477
+ }
478
+ }
479
+ async function versions(rest) {
480
+ const name = positional(rest)[0];
481
+ if (!name)
482
+ throw new Error("Usage: toolpin versions <server-name> [--source toolpin|official|docker|all|custom-id] [--live] [--limit 10] [--json]");
483
+ const servers = await loadServers(rest, { search: name });
484
+ const exactName = latestOnly(servers).find((server) => server.name === name)?.name
485
+ ?? searchServers(latestOnly(servers), name, 1)[0]?.server.name
486
+ ?? name;
487
+ const entries = knownVersions(servers, exactName).slice(0, numberFlag(rest, "--limit", 10));
488
+ if (hasFlag(rest, "--json")) {
489
+ console.log(JSON.stringify({ name: exactName, versions: entries }, null, 2));
490
+ return;
491
+ }
492
+ printHeader(`Known versions: ${exactName}`);
493
+ if (!entries.length) {
494
+ printField("status", "no versions found in current cache/source");
495
+ return;
496
+ }
497
+ for (const entry of entries) {
498
+ printBullet(`${entry.version}${entry.isLatest ? " latest" : ""} ${entry.source}`);
499
+ }
500
+ }
501
+ async function registry(rest) {
502
+ const subcommand = rest[0] ?? "list";
503
+ if (subcommand === "enable" || subcommand === "disable") {
504
+ const sourceId = rest[1];
505
+ if (!sourceId)
506
+ throw new Error("Usage: toolpin registry enable <source-id>\n toolpin registry disable <source-id>");
507
+ await updateRegistrySourceEnabled(sourceId, subcommand === "enable");
508
+ printHeader("Registry source updated");
509
+ printField("source", sourceId);
510
+ printField("enabled", subcommand === "enable" ? "yes" : "no");
511
+ return;
512
+ }
513
+ if (subcommand !== "list") {
514
+ throw new Error("Usage: toolpin registry list [--json]\n toolpin registry enable <source-id>\n toolpin registry disable <source-id>");
515
+ }
516
+ const sources = await listRegistrySourceStatuses();
517
+ const cache = await readCacheMetadata().catch(() => undefined);
518
+ if (hasFlag(rest, "--json")) {
519
+ console.log(JSON.stringify({ sources, cache }, null, 2));
520
+ return;
521
+ }
522
+ printHeader("Registry sources");
523
+ for (const source of sources) {
524
+ const partition = cache?.sources[source.id];
525
+ printSubhead(source.id);
526
+ printField("label", source.label);
527
+ printField("type", source.type ?? "unknown");
528
+ if (source.adapter)
529
+ printField("adapter", source.adapter);
530
+ printField("mode", source.mode);
531
+ printField("status", partition?.status ?? source.status ?? (source.enabled ? "ready" : "disabled"));
532
+ printField("trust", source.trust);
533
+ printField("enabled", source.enabled ? "yes" : "no");
534
+ if (source.pinned)
535
+ printField("pinned", "required");
536
+ printField("auth", source.authRequired ? "required" : "none");
537
+ if (source.url)
538
+ printField("url", source.url);
539
+ if (partition) {
540
+ printField("cached", `${partition.entries.length} versions / ${latestOnly(normalizeEntries(partition.entries)).length} latest servers`);
541
+ printField("last fetched", partition.generatedAt);
542
+ printField("fetched", `accepted ${partition.accepted}, skipped ${partition.skipped}, malformed ${partition.malformed}, failed ${partition.failed}`);
543
+ if (partition.pageInfo)
544
+ printField("pages", `${partition.pageInfo.fetchedPages}/${partition.pageInfo.maxPages} hasMore=${partition.pageInfo.hasMore}`);
545
+ if (partition.lastError)
546
+ printField("last error", partition.lastError);
547
+ }
548
+ if (source.setupHint)
549
+ printField("setup", source.setupHint);
550
+ printField("about", source.description);
551
+ }
552
+ }
553
+ async function outdated(rest) {
554
+ const path = stringFlag(rest, "--file", "mcp-lock.json");
555
+ const lockfile = await readLockfile(path);
556
+ const rows = [];
557
+ for (const [key, locked] of Object.entries(lockfile.servers)) {
558
+ const servers = await loadServers(rest, {
559
+ search: locked.name,
560
+ source: locked.resolved?.source ?? sourceFlag(rest, "all"),
561
+ });
562
+ const comparison = compareLockedToLatest(locked.name, locked.version, servers);
563
+ rows.push({
564
+ key,
565
+ name: locked.name,
566
+ client: locked.client,
567
+ source: locked.resolved?.source ?? "unknown",
568
+ locked: locked.version,
569
+ latest: comparison.latestVersion ?? "unknown",
570
+ status: comparison.status,
571
+ previous: comparison.previousVersions.map((entry) => entry.version),
572
+ });
573
+ }
574
+ if (hasFlag(rest, "--json")) {
575
+ console.log(JSON.stringify({ file: path, checked: rows.length, updates: rows.filter((row) => row.status === "update-available").length, servers: rows }, null, 2));
576
+ return;
577
+ }
578
+ printHeader("Outdated check");
579
+ printField("file", path);
580
+ printField("checked", `${rows.length} locked server/client entrie(s)`);
581
+ if (!rows.length) {
582
+ printField("status", "lockfile has no server entries");
583
+ return;
584
+ }
585
+ for (const row of rows) {
586
+ const marker = row.status === "update-available" ? "update available" : row.status;
587
+ printSubhead(`${row.name} (${row.client})`);
588
+ printField("locked", row.locked);
589
+ printField("latest", row.latest);
590
+ printField("status", marker);
591
+ if (row.previous.length)
592
+ printField("previous", row.previous.slice(0, 5).join(", "));
593
+ }
594
+ }
595
+ async function plan(rest) {
596
+ const values = positional(rest);
597
+ const name = values[0];
598
+ if (!name)
599
+ throw new Error(`Usage: toolpin plan <server-name> --client ${CLIENT_USAGE} [--live]`);
600
+ const client = clientFlag(rest, "generic");
601
+ const server = await findServer(rest, name);
602
+ if (client === "all") {
603
+ const { clients, skipped } = installableClientsForServer(server, PROJECT_CLIENTS);
604
+ printClientSkips(skipped);
605
+ if (!clients.length)
606
+ throw noInstallableClientsError(server.name, skipped);
607
+ console.log(JSON.stringify(clients.map((targetClient) => buildInstallPlan(server, targetClient)), null, 2));
608
+ }
609
+ else {
610
+ console.log(JSON.stringify(buildInstallPlan(server, client), null, 2));
611
+ }
612
+ }
613
+ async function lock(rest) {
614
+ if (rest[0] === "digest") {
615
+ await lockDigest(rest.slice(1));
616
+ return;
617
+ }
618
+ if (rest[0] === "sign") {
619
+ await lockSign(rest.slice(1));
620
+ return;
621
+ }
622
+ if (rest[0] === "verify-signature") {
623
+ await lockVerifySignature(rest.slice(1));
624
+ return;
625
+ }
626
+ if (rest[0] === "key-fingerprint") {
627
+ await lockKeyFingerprint(rest.slice(1));
628
+ return;
629
+ }
630
+ const values = positional(rest);
631
+ const name = values[0];
632
+ if (!name)
633
+ throw new Error(`Usage: toolpin lock <server-name> --client ${CLIENT_USAGE} [--live] [--verify [--skip-live-verification | --skip-live-verify] [--timeout 15000]]\n toolpin lock digest [--file mcp-lock.json] [--json]\n toolpin lock key-fingerprint --public-key public.pem [--json]\n toolpin lock sign --policy .toolpin/policy.json --key private.pem [--file mcp-lock.json] [--signature mcp-lock.sig]\n toolpin lock verify-signature --policy .toolpin/policy.json --public-key public.pem [--file mcp-lock.json] [--signature mcp-lock.sig]`);
634
+ const client = clientFlag(rest, "generic");
635
+ const path = stringFlag(rest, "--file", DEFAULT_LOCKFILE_PATH);
636
+ const scope = scopeFlag(rest, "project");
637
+ const server = await findServer(rest, name);
638
+ let verifiedCapabilityManifest;
639
+ let verificationReport;
640
+ if (hasFlag(rest, "--verify")) {
641
+ const liveVerification = liveVerificationEnabled(rest);
642
+ const report = await verifyServer(server, {
643
+ liveRemoteProbe: liveVerification,
644
+ livePackageProbe: liveVerification,
645
+ timeoutMs: numberFlag(rest, "--timeout", 15000),
646
+ requireVerified: hasFlag(rest, "--require-verified"),
647
+ });
648
+ if (!report.ok) {
649
+ throw new Error([
650
+ "Lock refused because verification failed.",
651
+ ...report.issues.map((issue) => `- ${issue.severity}: ${issue.code}: ${issue.message}`),
652
+ ].join("\n"));
653
+ }
654
+ verifiedCapabilityManifest = report.capabilityManifest;
655
+ verificationReport = report;
656
+ }
657
+ let lockfile;
658
+ if (client === "all") {
659
+ const { clients, skipped } = installableClientsForServer(server, PROJECT_CLIENTS);
660
+ printClientSkips(skipped);
661
+ if (!clients.length)
662
+ throw noInstallableClientsError(server.name, skipped);
663
+ for (const targetClient of clients) {
664
+ lockfile = await writeLockfile(buildInstallPlan(server, targetClient, { scope, capabilityManifest: verifiedCapabilityManifest, verificationReport }), path);
665
+ }
666
+ }
667
+ else {
668
+ lockfile = await writeLockfile(buildInstallPlan(server, client, { scope, capabilityManifest: verifiedCapabilityManifest, verificationReport }), path);
669
+ }
670
+ printHeader("Lockfile updated");
671
+ printField("server", `${server.name}@${server.version}`);
672
+ printField("file", path);
673
+ printField("entries", `${Object.keys(lockfile?.servers ?? {}).length} server/client entrie(s)`);
674
+ }
675
+ async function lockKeyFingerprint(rest) {
676
+ const keyPath = stringAnyFlag(rest, ["--public-key", "--key"], "");
677
+ if (!keyPath)
678
+ throw new Error("Usage: toolpin lock key-fingerprint --public-key public.pem [--json]");
679
+ const fingerprint = await readPublicKeyFingerprint(keyPath);
680
+ if (hasFlag(rest, "--json")) {
681
+ console.log(JSON.stringify({ publicKey: keyPath, fingerprint }, null, 2));
682
+ }
683
+ else {
684
+ console.log(fingerprint);
685
+ }
686
+ }
687
+ async function lockDigest(rest) {
688
+ const path = stringFlag(rest, "--file", DEFAULT_LOCKFILE_PATH);
689
+ const digest = await readLockfileDigest(path);
690
+ if (hasFlag(rest, "--json")) {
691
+ console.log(JSON.stringify({ file: path, digest }, null, 2));
692
+ }
693
+ else {
694
+ console.log(digest);
695
+ }
696
+ }
697
+ async function lockSign(rest) {
698
+ const keyPath = stringFlag(rest, "--key", "");
699
+ const policyPath = stringFlag(rest, "--policy", "");
700
+ if (!keyPath || !policyPath)
701
+ throw new Error("Usage: toolpin lock sign --policy .toolpin/policy.json --key private.pem [--file mcp-lock.json] [--signature mcp-lock.sig]");
702
+ const path = stringFlag(rest, "--file", DEFAULT_LOCKFILE_PATH);
703
+ const signaturePath = stringFlag(rest, "--signature", DEFAULT_SIGNATURE_PATH);
704
+ const envelope = await signLockfile(path, keyPath, signaturePath, { policyPath });
705
+ if (hasFlag(rest, "--json")) {
706
+ console.log(JSON.stringify({ file: path, signature: signaturePath, envelope }, null, 2));
707
+ }
708
+ else {
709
+ printHeader(`Signed ${path}`);
710
+ printField("file", path);
711
+ printField("digest", envelope.lockfileDigest);
712
+ printField("policy", envelope.policyDigest ?? "none");
713
+ printField("key", envelope.publicKeyFingerprint);
714
+ printField("signature", signaturePath);
715
+ }
716
+ }
717
+ async function lockVerifySignature(rest) {
718
+ const keyPath = stringAnyFlag(rest, ["--public-key", "--key"], "");
719
+ const policyPath = stringFlag(rest, "--policy", "");
720
+ if (!keyPath || !policyPath)
721
+ throw new Error("Usage: toolpin lock verify-signature --policy .toolpin/policy.json --public-key public.pem [--file mcp-lock.json] [--signature mcp-lock.sig]");
722
+ const path = stringFlag(rest, "--file", DEFAULT_LOCKFILE_PATH);
723
+ const signaturePath = stringFlag(rest, "--signature", DEFAULT_SIGNATURE_PATH);
724
+ const report = await verifyLockfileSignature(path, keyPath, signaturePath, { policyPath });
725
+ if (hasFlag(rest, "--json")) {
726
+ console.log(JSON.stringify({ file: path, signature: signaturePath, report }, null, 2));
727
+ }
728
+ else {
729
+ printHeader(`${report.ok ? "OK" : "FAILED"} ${report.message.replace(/\.$/, "")}`);
730
+ printField("file", path);
731
+ printField("signature", signaturePath);
732
+ printField("policy", report.policyDigest ?? "none");
733
+ if (report.publicKeyFingerprint)
734
+ printField("key", report.publicKeyFingerprint);
735
+ printField("result", report.message);
736
+ }
737
+ if (!report.ok)
738
+ process.exitCode = 1;
739
+ }
740
+ async function exportConfig(rest) {
741
+ const values = positional(rest);
742
+ const name = values[0];
743
+ if (!name)
744
+ throw new Error(`Usage: toolpin export-config <server-name> --client ${CLIENT_USAGE} [--live]`);
745
+ const client = clientFlag(rest, "generic");
746
+ const server = await findServer(rest, name);
747
+ if (client === "all") {
748
+ const { clients, skipped } = installableClientsForServer(server, PROJECT_CLIENTS);
749
+ printClientSkips(skipped);
750
+ const exported = Object.fromEntries(clients.map((targetClient) => [targetClient, exportClientConfig(server, targetClient).config]));
751
+ console.log(JSON.stringify(exported, null, 2));
752
+ return;
753
+ }
754
+ const exported = exportClientConfig(server, client);
755
+ if (client === "codex") {
756
+ console.log(codexTomlFromClientConfig(exported.config));
757
+ }
758
+ else if (client === "continue") {
759
+ console.log(continueYamlFromClientConfig(exported.config));
760
+ }
761
+ else {
762
+ console.log(JSON.stringify(exported.config, null, 2));
763
+ }
764
+ if (exported.notes.length) {
765
+ console.error(`Notes: ${exported.notes.join(" ")}`);
766
+ }
767
+ }
768
+ async function findServer(rest, name) {
769
+ const servers = await loadServers(rest, { search: name });
770
+ const requestedVersion = serverVersionFlag(rest);
771
+ let resolved;
772
+ if (requestedVersion) {
773
+ resolved = servers.find((server) => server.name === name && server.version === requestedVersion);
774
+ if (!resolved) {
775
+ const matchedName = latestOnly(servers).find((server) => server.name === name)?.name
776
+ ?? searchServers(latestOnly(servers), name, 1)[0]?.server.name;
777
+ resolved = matchedName
778
+ ? servers.find((server) => server.name === matchedName && server.version === requestedVersion)
779
+ : undefined;
780
+ }
781
+ if (!resolved) {
782
+ throw new Error(`No server version ${requestedVersion} found for ${name}. Run \`toolpin versions ${name}\` to list known versions.`);
783
+ }
784
+ }
785
+ else {
786
+ resolved = latestOnly(servers).find((server) => server.name === name)
787
+ ?? searchServers(latestOnly(servers), name, 1)[0]?.server;
788
+ }
789
+ if (!resolved) {
790
+ throw new Error(`No server found for ${name}. Try \`toolpin ingest\` or pass --live.`);
791
+ }
792
+ return resolveServerTargets(rest, resolved);
793
+ }
794
+ async function findExactServer(rest, name, source) {
795
+ const servers = await loadServers(rest, { search: name, source });
796
+ const exact = latestOnly(servers).find((server) => server.name === name);
797
+ if (exact)
798
+ return resolveServerTargets(rest, exact);
799
+ throw new Error(`No exact server found for ${name} in ${source}. Try \`toolpin ingest\` or pass --live.`);
800
+ }
801
+ async function resolveServerTargets(rest, server) {
802
+ return enrichGlamaTarget(await enrichSmitheryTarget(server, {
803
+ allowHostedDirectoryTargets: hasFlag(rest, "--allow-hosted-directory-targets"),
804
+ }));
805
+ }
806
+ async function loadServers(rest, liveOptions = {}) {
807
+ let entries;
808
+ const source = liveOptions.source ?? sourceFlag(rest, "all");
809
+ const registrySources = await listRegistrySources();
810
+ const knownSources = new Set(registrySources.map((entry) => entry.id));
811
+ const enabledSources = new Set(registrySources.filter((entry) => entry.enabled).map((entry) => entry.id));
812
+ if (source !== "all" && !knownSources.has(source)) {
813
+ throw new Error(`Unknown registry source: ${source}. Add it to .toolpin/registries.json or run \`toolpin registry list\`.`);
814
+ }
815
+ if (source !== "all" && !enabledSources.has(source)) {
816
+ throw new Error(`Registry source ${source} is disabled. Run \`toolpin registry enable ${source}\` to enable it.`);
817
+ }
818
+ if (hasFlag(rest, "--live")) {
819
+ entries = await fetchRegistry({ maxPages: 3, search: liveOptions.search, source });
820
+ }
821
+ else {
822
+ try {
823
+ entries = await readCache();
824
+ if (!cacheHasSource(entries, source, enabledSources)) {
825
+ entries = await fetchRegistry({ maxPages: 3, search: liveOptions.search, source });
826
+ }
827
+ }
828
+ catch (error) {
829
+ if (error instanceof CacheSchemaError)
830
+ throw error;
831
+ entries = await fetchRegistry({ maxPages: 3, search: liveOptions.search, source });
832
+ }
833
+ }
834
+ const servers = normalizeEntries(entries);
835
+ return source === "all"
836
+ ? servers.filter((server) => enabledSources.has(server.registrySource))
837
+ : servers.filter((server) => server.registrySource === source);
838
+ }
839
+ async function install(rest) {
840
+ const values = positional(rest);
841
+ const name = values[0];
842
+ if (!name)
843
+ throw new Error(`Usage: toolpin install <server-name> --client ${CLIENT_USAGE} [--scope project|global] [--live]`);
844
+ const client = clientFlag(rest, "generic");
845
+ const scope = scopeFlag(rest, "project");
846
+ const updateLock = hasFlag(rest, "--update-lock");
847
+ const verifyBeforeInstall = hasFlag(rest, "--verify");
848
+ const requireVerified = hasFlag(rest, "--require-verified");
849
+ const policyPath = stringFlag(rest, "--policy", DEFAULT_POLICY_PATH);
850
+ if (scope !== "project" && scope !== "global") {
851
+ throw new Error("--scope must be project or global");
852
+ }
853
+ console.error(`Resolving ${name} from ${sourceFlag(rest, "all")} registry source...`);
854
+ const server = await findServer(rest, name);
855
+ let verifiedCapabilityManifest;
856
+ let verificationReport;
857
+ if (verifyBeforeInstall) {
858
+ const liveVerification = liveVerificationEnabled(rest);
859
+ const report = await verifyServer(server, {
860
+ liveRemoteProbe: liveVerification,
861
+ livePackageProbe: liveVerification,
862
+ timeoutMs: numberFlag(rest, "--timeout", 15000),
863
+ requireVerified,
864
+ });
865
+ if (!report.ok) {
866
+ throw new Error([
867
+ "Install refused because verification failed.",
868
+ ...report.issues.map((issue) => `- ${issue.severity}: ${issue.code}: ${issue.message}`),
869
+ ].join("\n"));
870
+ }
871
+ verificationReport = report;
872
+ verifiedCapabilityManifest = report.capabilityManifest;
873
+ }
874
+ const allClients = client === "all" ? installableClientsForServer(server, clientsForScope(scope)) : undefined;
875
+ if (allClients) {
876
+ printClientSkips(allClients.skipped);
877
+ if (!allClients.clients.length)
878
+ throw noInstallableClientsError(server.name, allClients.skipped);
879
+ }
880
+ const clients = allClients?.clients ?? [client];
881
+ const plans = clients.map((targetClient) => buildInstallPlan(server, targetClient, { capabilityManifest: verifiedCapabilityManifest, verificationReport, scope }));
882
+ if (!hasFlag(rest, "--no-policy")) {
883
+ const violations = [];
884
+ for (const plan of plans) {
885
+ const report = await enforcePolicy(plan, policyPath);
886
+ if (!report.ok) {
887
+ violations.push(`${report.key}: ${report.issues.map((issue) => `${issue.code}: ${issue.message}`).join("; ")}`);
888
+ }
889
+ }
890
+ if (violations.length) {
891
+ throw new Error([
892
+ `Install refused by policy ${policyPath}.`,
893
+ ...violations.map((violation) => `- ${violation}`),
894
+ ].join("\n"));
895
+ }
896
+ }
897
+ if (!updateLock) {
898
+ const mismatches = [];
899
+ for (const plan of plans) {
900
+ const verification = await verifyAgainstLockfile(plan, DEFAULT_LOCKFILE_PATH);
901
+ if (!verification.ok) {
902
+ mismatches.push(`${verification.key}: ${verification.messages.join("; ")}`);
903
+ }
904
+ }
905
+ if (mismatches.length) {
906
+ throw new Error([
907
+ "Install refused because resolved metadata differs from mcp-lock.json.",
908
+ ...mismatches.map((message) => `- ${message}`),
909
+ "Run `toolpin lock <server-name> --client ...` or repeat install with `--update-lock` after reviewing the drift.",
910
+ ].join("\n"));
911
+ }
912
+ }
913
+ console.error(`Installing ${server.name}@${server.version} into ${client} ${scope} config...`);
914
+ printHeader("Install");
915
+ printField("server", `${server.name}@${server.version}`, OK_COLOR);
916
+ printField("registry", server.registrySource, CYAN_COLOR);
917
+ if (server.resolutionNote)
918
+ printField("resolved", server.resolutionNote, WARN_COLOR);
919
+ const installTrust = plans[0]?.trust ?? scoreServer(server);
920
+ printField("trust", `${trustTier(installTrust)} / ${trustProfileScore(installTrust)}% profile / ${evidenceStatus(installTrust)}`, trustTierColor(trustTier(installTrust)));
921
+ printField("evidence", evidenceSummary(installTrust));
922
+ printCapExplanation(installTrust);
923
+ printField("verify", verificationStatus(verifyBeforeInstall, verificationReport), verifyBeforeInstall ? (verificationOutcome(verificationReport) === "verified" ? OK_COLOR : WARN_COLOR) : MUTED_COLOR);
924
+ printField("scope", scope === "project" ? "project folder" : "global current user");
925
+ printField("clients", clients.join(", "));
926
+ for (const [index, targetClient] of clients.entries()) {
927
+ const result = await installServerConfig(server, targetClient, scope);
928
+ await writeLockfile(plans[index], DEFAULT_LOCKFILE_PATH);
929
+ printSubhead(`${result.client} ${result.scope}`);
930
+ printField("config", `${result.action}: ${result.file}`, OK_COLOR);
931
+ printField("lock", "mcp-lock.json updated", OK_COLOR);
932
+ for (const note of result.notes)
933
+ printBullet(note);
934
+ }
935
+ printField("done", `installed for ${client === "all" ? "all supported clients in this scope" : clients.join(", ")}`, OK_COLOR);
936
+ }
937
+ async function testInstalled(rest) {
938
+ if (isHelp(rest)) {
939
+ console.log(`Usage: toolpin test-installed <server-name> --client ${CLIENT_USAGE.replace("|all", "")} --scope project|global [--timeout 15000] [--json]`);
940
+ return;
941
+ }
942
+ const values = positional(rest);
943
+ const name = values[0];
944
+ if (!name)
945
+ throw new Error(`Usage: toolpin test-installed <server-name> --client ${CLIENT_USAGE.replace("|all", "")} --scope project|global [--timeout 15000] [--json]`);
946
+ const client = clientFlag(rest, "generic");
947
+ if (client === "all")
948
+ throw new Error("test-installed requires one --client value, not all.");
949
+ const scope = scopeFlag(rest, "project");
950
+ if (scope !== "project" && scope !== "global")
951
+ throw new Error("--scope must be project or global");
952
+ const timeoutMs = numberFlag(rest, "--timeout", 15000);
953
+ const result = await testInstalledServer({ serverName: name, client, scope, timeoutMs });
954
+ if (hasFlag(rest, "--json")) {
955
+ console.log(JSON.stringify(result, null, 2));
956
+ }
957
+ else {
958
+ printHeader(result.ok ? "Installed test OK" : "Installed test failed");
959
+ printField("server", result.serverName);
960
+ printField("client", client);
961
+ printField("scope", scope);
962
+ printField("target", result.target);
963
+ printField("duration", `${result.durationMs}ms`);
964
+ printField("message", result.message);
965
+ if (result.tools.length) {
966
+ printSubhead("Tools");
967
+ for (const tool of result.tools) {
968
+ printBullet(`${tool.name}${tool.description ? `: ${truncate(tool.description, 120)}` : ""}`);
969
+ }
970
+ }
971
+ }
972
+ if (!result.ok)
973
+ process.exitCode = 1;
974
+ }
975
+ async function adoptInstalled(rest) {
976
+ if (isHelp(rest)) {
977
+ console.log(`Usage: toolpin adopt <installed-name> --client ${CLIENT_USAGE.replace("|all", "")} --scope project|global [--source all] [--live] [--file mcp-lock.json] [--verify] [--policy .toolpin/policy.json] [--no-policy] [--dry-run] [--json]`);
978
+ return;
979
+ }
980
+ const values = positional(rest);
981
+ const name = values[0];
982
+ if (!name)
983
+ throw new Error(`Usage: toolpin adopt <installed-name> --client ${CLIENT_USAGE.replace("|all", "")} --scope project|global [--source all] [--live] [--file mcp-lock.json] [--verify] [--policy .toolpin/policy.json] [--no-policy] [--dry-run] [--json]`);
984
+ const client = clientFlag(rest, "generic");
985
+ if (client === "all")
986
+ throw new Error("adopt requires one --client value, not all.");
987
+ const scope = scopeFlag(rest, "project");
988
+ if (scope !== "project" && scope !== "global")
989
+ throw new Error("--scope must be project or global");
990
+ const servers = await loadServers(rest, { source: sourceFlag(rest, "all") });
991
+ const result = await adoptInstalledServer({
992
+ installedName: name,
993
+ client,
994
+ scope,
995
+ servers,
996
+ lockfilePath: stringFlag(rest, "--file", "mcp-lock.json"),
997
+ verify: hasFlag(rest, "--verify"),
998
+ requireVerified: hasFlag(rest, "--require-verified"),
999
+ timeoutMs: numberFlag(rest, "--timeout", 15000),
1000
+ policyPath: stringFlag(rest, "--policy", ".toolpin/policy.json"),
1001
+ enforcePolicy: !hasFlag(rest, "--no-policy"),
1002
+ dryRun: hasFlag(rest, "--dry-run"),
1003
+ });
1004
+ printInstalledMutationResult(result, hasFlag(rest, "--json"));
1005
+ }
1006
+ async function updateInstalled(rest) {
1007
+ if (isHelp(rest)) {
1008
+ console.log(`Usage: toolpin update <server-name> --client ${CLIENT_USAGE.replace("|all", "")} --scope project|global [--version <server-version>] [--source all] [--live] [--file mcp-lock.json] [--verify] [--policy .toolpin/policy.json] [--no-policy] [--dry-run] [--json]
1009
+ toolpin update --all [--scope all|project|global] [--client <client|all>] [--source all] [--live] [--file mcp-lock.json] [--dry-run] [--json]`);
1010
+ return;
1011
+ }
1012
+ const servers = await loadServers(rest, { source: sourceFlag(rest, "all") });
1013
+ if (hasFlag(rest, "--all")) {
1014
+ const client = hasAnyFlag(rest, ["--client", "-c"]) ? clientFlag(rest, "generic") : "all";
1015
+ const result = await updateAllInstalledServers({
1016
+ scope: scopeFlag(rest, "all"),
1017
+ client,
1018
+ servers,
1019
+ lockfilePath: stringFlag(rest, "--file", "mcp-lock.json"),
1020
+ verify: hasFlag(rest, "--verify"),
1021
+ requireVerified: hasFlag(rest, "--require-verified"),
1022
+ timeoutMs: numberFlag(rest, "--timeout", 15000),
1023
+ policyPath: stringFlag(rest, "--policy", ".toolpin/policy.json"),
1024
+ enforcePolicy: !hasFlag(rest, "--no-policy"),
1025
+ dryRun: hasFlag(rest, "--dry-run"),
1026
+ });
1027
+ printInstalledUpdateAllResult(result, hasFlag(rest, "--json"));
1028
+ return;
1029
+ }
1030
+ const values = positional(rest);
1031
+ const name = values[0];
1032
+ if (!name)
1033
+ throw new Error(`Usage: toolpin update <server-name> --client ${CLIENT_USAGE.replace("|all", "")} --scope project|global [--version <server-version>] [--source all] [--live] [--file mcp-lock.json] [--dry-run] [--json]`);
1034
+ const client = clientFlag(rest, "generic");
1035
+ if (client === "all")
1036
+ throw new Error("update <server-name> requires one --client value, not all.");
1037
+ const scope = scopeFlag(rest, "project");
1038
+ if (scope !== "project" && scope !== "global")
1039
+ throw new Error("--scope must be project or global");
1040
+ const result = await updateInstalledServer({
1041
+ serverName: name,
1042
+ client,
1043
+ scope,
1044
+ servers,
1045
+ version: stringFlag(rest, "--version", ""),
1046
+ lockfilePath: stringFlag(rest, "--file", "mcp-lock.json"),
1047
+ verify: hasFlag(rest, "--verify"),
1048
+ requireVerified: hasFlag(rest, "--require-verified"),
1049
+ timeoutMs: numberFlag(rest, "--timeout", 15000),
1050
+ policyPath: stringFlag(rest, "--policy", ".toolpin/policy.json"),
1051
+ enforcePolicy: !hasFlag(rest, "--no-policy"),
1052
+ dryRun: hasFlag(rest, "--dry-run"),
1053
+ });
1054
+ printInstalledMutationResult(result, hasFlag(rest, "--json"));
1055
+ }
1056
+ async function remove(rest, command = "remove") {
1057
+ if (isHelp(rest)) {
1058
+ console.log(`Usage: toolpin ${command} <server-name> [--client ${CLIENT_USAGE}] [--scope project|global] [--file mcp-lock.json]`);
1059
+ return;
1060
+ }
1061
+ const values = positional(rest);
1062
+ const name = values[0];
1063
+ if (!name)
1064
+ throw new Error(`Usage: toolpin ${command} <server-name> [--client ${CLIENT_USAGE}] [--scope project|global] [--file mcp-lock.json]`);
1065
+ const client = hasAnyFlag(rest, ["--client", "-c"]) ? clientFlag(rest, "generic") : "all";
1066
+ const scope = scopeFlag(rest, "project");
1067
+ const path = stringFlag(rest, "--file", "mcp-lock.json");
1068
+ if (scope !== "project" && scope !== "global") {
1069
+ throw new Error("--scope must be project or global");
1070
+ }
1071
+ await readLockfile(path);
1072
+ const clients = client === "all" ? clientsForScope(scope) : [client];
1073
+ printHeader("Remove");
1074
+ printField("server", name);
1075
+ printField("scope", scope);
1076
+ for (const targetClient of clients) {
1077
+ const runtimeAdvisory = await localHttpRuntimeAdvisory(name, targetClient, scope).catch(() => undefined);
1078
+ const configResult = await removeServerConfig(name, targetClient, scope);
1079
+ const lockResult = await removeLockfileEntry(name, targetClient, path);
1080
+ const status = configResult.action === "removed" || lockResult.removed ? "removed" : "missing";
1081
+ printSubhead(`${targetClient}: ${status}`);
1082
+ printField("config", configResult.action);
1083
+ printField("lock", lockResult.removed ? "removed" : "missing");
1084
+ if (runtimeAdvisory && configResult.action === "removed")
1085
+ printField("runtime", runtimeAdvisory.message, WARN_COLOR);
1086
+ for (const note of configResult.notes)
1087
+ printBullet(note);
1088
+ }
1089
+ }
1090
+ async function listInstalled(rest) {
1091
+ if (isHelp(rest)) {
1092
+ console.log(`Usage: toolpin list [--scope all|project|global] [--client ${CLIENT_USAGE}] [--json]`);
1093
+ return;
1094
+ }
1095
+ const scope = scopeFlag(rest, "all");
1096
+ const client = hasAnyFlag(rest, ["--client", "-c"]) ? clientFlag(rest, "generic") : "all";
1097
+ const report = await listInstalledServers({ scope, client });
1098
+ if (hasFlag(rest, "--json")) {
1099
+ console.log(JSON.stringify(report, null, 2));
1100
+ return;
1101
+ }
1102
+ printHeader("Installed MCP servers");
1103
+ printField("scope", scopeDescription(scope));
1104
+ printField("client", client === "all" ? "all supported clients" : client);
1105
+ printField("checked", `${report.checked} config file(s)`);
1106
+ if (!report.entries.length) {
1107
+ printField("status", "no installed MCP server entries found");
1108
+ }
1109
+ else {
1110
+ let previousGroup = "";
1111
+ for (const entry of report.entries) {
1112
+ const group = `${entry.scope} ${entry.client}`;
1113
+ if (group !== previousGroup) {
1114
+ printSubhead(group);
1115
+ printField("file", entry.file);
1116
+ previousGroup = group;
1117
+ }
1118
+ printBullet(entry.serverName);
1119
+ }
1120
+ }
1121
+ for (const issue of report.issues) {
1122
+ printSubhead(`${issue.scope} ${issue.client}: ${issue.kind}`);
1123
+ if (issue.file)
1124
+ printField("file", issue.file);
1125
+ printField("message", issue.message);
1126
+ }
1127
+ }
1128
+ async function ci(rest) {
1129
+ if (isHelp(rest)) {
1130
+ ciHelp();
1131
+ return;
1132
+ }
1133
+ const path = stringFlag(rest, "--file", DEFAULT_LOCKFILE_PATH);
1134
+ const expectedDigest = stringFlag(rest, "--expect-digest", "");
1135
+ const signaturePath = stringFlag(rest, "--signature", "");
1136
+ const publicKeyPath = stringFlag(rest, "--public-key", "");
1137
+ const verifyBeforeUse = hasFlag(rest, "--verify");
1138
+ const requireVerified = hasFlag(rest, "--require-verified");
1139
+ const policyPath = stringFlag(rest, "--policy", DEFAULT_POLICY_PATH);
1140
+ const enforcePolicies = !hasFlag(rest, "--no-policy");
1141
+ const sarif = hasFlag(rest, "--sarif");
1142
+ if (signaturePath || publicKeyPath) {
1143
+ if (!signaturePath || !publicKeyPath)
1144
+ throw new Error("toolpin ci requires both --signature and --public-key when verifying a lock signature.");
1145
+ let signature;
1146
+ try {
1147
+ signature = await verifyLockfileSignature(path, publicKeyPath, signaturePath, { policyPath });
1148
+ }
1149
+ catch (error) {
1150
+ const message = error instanceof Error ? error.message : String(error);
1151
+ if (sarif) {
1152
+ console.log(JSON.stringify(sarifLog([ciSarifResult("ci_signature_failed", `Lockfile signature verification failed for ${path}: ${message}`, path)], { executionSuccessful: false }), null, 2));
1153
+ process.exitCode = 1;
1154
+ return;
1155
+ }
1156
+ throw error;
1157
+ }
1158
+ if (!signature.ok) {
1159
+ if (sarif) {
1160
+ console.log(JSON.stringify(sarifLog([ciSarifResult("ci_signature_failed", `Lockfile signature verification failed for ${path}: ${signature.message}`, path)], { executionSuccessful: false }), null, 2));
1161
+ process.exitCode = 1;
1162
+ return;
1163
+ }
1164
+ throw new Error(`Lockfile signature verification failed for ${path}: ${signature.message}`);
1165
+ }
1166
+ }
1167
+ if (expectedDigest) {
1168
+ const actualDigest = await readLockfileDigest(path);
1169
+ if (actualDigest !== expectedDigest) {
1170
+ if (sarif) {
1171
+ console.log(JSON.stringify(sarifLog([ciSarifResult("ci_digest_mismatch", `Lockfile digest mismatch for ${path}: expected ${expectedDigest}, got ${actualDigest}`, path)], { executionSuccessful: false }), null, 2));
1172
+ process.exitCode = 1;
1173
+ return;
1174
+ }
1175
+ throw new Error(`Lockfile digest mismatch for ${path}: expected ${expectedDigest}, got ${actualDigest}`);
1176
+ }
1177
+ }
1178
+ const report = await verifyFrozenInstall(path, async (locked) => {
1179
+ const server = await findExactServer(rest, locked.name, locked.resolved?.source ?? sourceFlag(rest, "all"));
1180
+ let verifiedCapabilityManifest;
1181
+ let verification;
1182
+ if (hasAnyFlag(rest, ["--skip-live-verification", "--skip-live-verify"]) && lockedHasLivePins(locked)) {
1183
+ throw new Error(`${locked.name} has live capability pins in ${path}; --skip-live-verification is not allowed for pinned CI entries.`);
1184
+ }
1185
+ if (verifyBeforeUse) {
1186
+ const liveVerification = liveVerificationEnabled(rest);
1187
+ verification = await verifyServer(server, {
1188
+ liveRemoteProbe: liveVerification,
1189
+ livePackageProbe: liveVerification,
1190
+ timeoutMs: numberFlag(rest, "--timeout", 15000),
1191
+ requireVerified,
1192
+ });
1193
+ if (!verification.ok) {
1194
+ throw new Error([
1195
+ `Verification failed for ${locked.name}:`,
1196
+ ...verification.issues.map((issue) => `- ${issue.severity}: ${issue.code}: ${issue.message}`),
1197
+ ].join("\n"));
1198
+ }
1199
+ verifiedCapabilityManifest = verification.capabilityManifest;
1200
+ }
1201
+ const plan = buildInstallPlan(server, locked.client, { capabilityManifest: verifiedCapabilityManifest, verificationReport: verification, scope: locked.scope ?? "project" });
1202
+ if (enforcePolicies) {
1203
+ const policy = await enforcePolicy(plan, policyPath);
1204
+ if (!policy.ok) {
1205
+ throw new Error(policy.issues.map((issue) => `${issue.code}: ${issue.message}`).join("; "));
1206
+ }
1207
+ }
1208
+ return plan;
1209
+ });
1210
+ if (sarif) {
1211
+ console.log(JSON.stringify(sarifLog(ciSarifResults(report, path), { executionSuccessful: report.ok }), null, 2));
1212
+ if (!report.ok)
1213
+ process.exitCode = 1;
1214
+ return;
1215
+ }
1216
+ if (!report.ok) {
1217
+ throw new Error([
1218
+ `Frozen install failed for ${path}.`,
1219
+ ...report.issues.flatMap((issue) => [`- ${issue.key}:`, ...issue.messages.map((message) => ` - ${message}`)]),
1220
+ ].join("\n"));
1221
+ }
1222
+ printHeader("Frozen install OK");
1223
+ printField("file", path);
1224
+ printField("checked", `${report.checked} locked server/client entrie(s)`);
1225
+ }
1226
+ async function policy(rest) {
1227
+ const subcommand = rest[0] ?? "help";
1228
+ const values = rest.slice(1);
1229
+ if (subcommand === "digest") {
1230
+ const policyPath = stringFlag(values, "--policy", stringFlag(values, "--file", DEFAULT_POLICY_PATH));
1231
+ const digest = await readPolicyDigest(policyPath);
1232
+ if (!digest)
1233
+ throw new Error(`Policy file not found: ${policyPath}`);
1234
+ if (hasFlag(values, "--json")) {
1235
+ console.log(JSON.stringify({ file: policyPath, digest }, null, 2));
1236
+ }
1237
+ else {
1238
+ console.log(digest);
1239
+ }
1240
+ return;
1241
+ }
1242
+ if (subcommand !== "check") {
1243
+ throw new Error(`Usage: toolpin policy digest [--policy .toolpin/policy.json] [--json]\n toolpin policy check <server-name> --client ${CLIENT_USAGE} [--scope project|global] [--policy .toolpin/policy.json] [--json] [--live]`);
1244
+ }
1245
+ const name = positional(values)[0];
1246
+ if (!name)
1247
+ throw new Error(`Usage: toolpin policy check <server-name> --client ${CLIENT_USAGE} [--scope project|global] [--policy .toolpin/policy.json] [--json] [--live]`);
1248
+ const client = clientFlag(values, "generic");
1249
+ const scope = scopeFlag(values, "project");
1250
+ const policyPath = stringFlag(values, "--policy", DEFAULT_POLICY_PATH);
1251
+ if (scope !== "project" && scope !== "global") {
1252
+ throw new Error("--scope must be project or global");
1253
+ }
1254
+ const server = await findServer(values, name);
1255
+ const clients = client === "all" ? clientsForScope(scope) : [client];
1256
+ const reports = await Promise.all(clients.map(async (targetClient) => enforcePolicy(buildInstallPlan(server, targetClient), policyPath)));
1257
+ if (hasFlag(values, "--json")) {
1258
+ console.log(JSON.stringify({ ok: reports.every((report) => report.ok), reports }, null, 2));
1259
+ }
1260
+ else {
1261
+ for (const report of reports) {
1262
+ printSubhead(`${report.ok ? "OK" : "DENIED"} ${report.key}`);
1263
+ for (const issue of report.issues)
1264
+ printBullet(`${issue.code}: ${issue.message}`);
1265
+ }
1266
+ }
1267
+ if (reports.some((report) => !report.ok))
1268
+ process.exitCode = 1;
1269
+ }
1270
+ async function secrets(rest) {
1271
+ const subcommand = rest[0] ?? "help";
1272
+ const values = rest.slice(1);
1273
+ if (subcommand !== "audit") {
1274
+ throw new Error("Usage: toolpin secrets audit [--file mcp-lock.json] [--scope all|project|global] [--json]");
1275
+ }
1276
+ const path = stringFlag(values, "--file", "mcp-lock.json");
1277
+ const scope = scopeFlag(values, "all");
1278
+ if (scope !== "all" && scope !== "project" && scope !== "global") {
1279
+ throw new Error("--scope must be all, project, or global");
1280
+ }
1281
+ const report = await auditSecrets(path, scope);
1282
+ if (hasFlag(values, "--json")) {
1283
+ console.log(JSON.stringify(report, null, 2));
1284
+ }
1285
+ else if (report.ok) {
1286
+ printHeader("Secrets audit OK");
1287
+ printField("checked", `${report.checked} locked server/client entrie(s)`);
1288
+ printField("scope", scopeDescription(scope));
1289
+ }
1290
+ else {
1291
+ printHeader("Secrets audit findings");
1292
+ printField("findings", String(report.findings.length));
1293
+ printField("checked", `${report.checked} locked server/client entrie(s)`);
1294
+ for (const finding of report.findings) {
1295
+ const secret = finding.secretName ? ` ${finding.secretSource}:${finding.secretName}` : "";
1296
+ const scopeLabel = finding.scope ? ` [${finding.scope}]` : "";
1297
+ printBullet(`${finding.kind} ${finding.key}${scopeLabel}${secret}: ${finding.message}`);
1298
+ printField("file", finding.file || "no file");
1299
+ if (finding.redactedValue)
1300
+ printField("value", finding.redactedValue);
1301
+ }
1302
+ }
1303
+ if (!report.ok)
1304
+ process.exitCode = 1;
1305
+ }
1306
+ async function doctor(rest) {
1307
+ if (isHelp(rest)) {
1308
+ doctorHelp();
1309
+ return;
1310
+ }
1311
+ const path = stringFlag(rest, "--file", "mcp-lock.json");
1312
+ const scope = scopeFlag(rest, "all");
1313
+ if (scope !== "all" && scope !== "project" && scope !== "global") {
1314
+ throw new Error("--scope must be all, project, or global");
1315
+ }
1316
+ const report = await doctorLockfile(path, scope);
1317
+ if (hasFlag(rest, "--json")) {
1318
+ console.log(JSON.stringify(report, null, 2));
1319
+ }
1320
+ else if (report.ok) {
1321
+ printHeader("Doctor OK");
1322
+ printField("checked", `${report.checked} locked server/client entrie(s)`);
1323
+ printField("scope", scopeDescription(scope));
1324
+ }
1325
+ else {
1326
+ printHeader("Doctor issues");
1327
+ printField("issues", String(report.issues.length));
1328
+ printField("checked", `${report.checked} locked server/client entrie(s)`);
1329
+ for (const issue of report.issues) {
1330
+ const scopeLabel = issue.scope ? ` [${issue.scope}]` : "";
1331
+ printBullet(`${issue.kind} ${issue.key}${scopeLabel}: ${issue.message}`);
1332
+ printField("file", issue.file);
1333
+ }
1334
+ }
1335
+ if (!report.ok)
1336
+ process.exitCode = 1;
1337
+ }
1338
+ function ciHelp() {
1339
+ console.log("Usage: toolpin ci [--file mcp-lock.json] [--expect-digest sha256-...] [--signature mcp-lock.sig --public-key public.pem] [--policy .toolpin/policy.json] [--no-policy] [--source toolpin|official|docker|all|id] [--live] [--verify [--require-verified] [--skip-live-verification | --skip-live-verify] [--timeout 15000]] [--sarif]");
1340
+ }
1341
+ function doctorHelp() {
1342
+ console.log("Usage: toolpin doctor [--file mcp-lock.json] [--scope|-s all|project|global] [--global|-g] [--json]");
1343
+ }
1344
+ function commandHelp(command) {
1345
+ switch (command) {
1346
+ case "upgrade":
1347
+ upgradeHelp();
1348
+ return;
1349
+ case "search":
1350
+ console.log("Usage: toolpin search <query> [--source toolpin|official|docker|all|custom-id] [--limit 10] [--live] [--json]");
1351
+ return;
1352
+ case "ci":
1353
+ ciHelp();
1354
+ return;
1355
+ case "registry":
1356
+ case "sources":
1357
+ console.log("Usage: toolpin registry list [--json]\n toolpin registry enable <source-id>\n toolpin registry disable <source-id>");
1358
+ return;
1359
+ case "audit":
1360
+ console.log("Usage: toolpin audit [--file mcp-lock.json] [--scope all|project|global] [--client all] [--policy .toolpin/policy.json] [--verify] [--require-verified] [--json]\n toolpin audit server <server-name> [--source toolpin|official|docker|all|custom-id] [--live] [--json]");
1361
+ return;
1362
+ case "scan":
1363
+ console.log("Usage: toolpin scan <server-name> [--source toolpin|official|docker|all|custom-id] [--live] [--json] [--sarif] [--timeout 15000]\nDescription scan only; use `toolpin verify` for artifact evidence verification and `toolpin audit` for local install audit.");
1364
+ return;
1365
+ case "verify":
1366
+ console.log("Usage: toolpin verify <server-name> [--source toolpin|official|docker|all|custom-id] [--live] [--json] [--sarif] [--timeout 15000] [--skip-live-verification] [--require-verified]");
1367
+ return;
1368
+ case "doctor":
1369
+ doctorHelp();
1370
+ return;
1371
+ case "list":
1372
+ case "ls":
1373
+ case "installed":
1374
+ console.log(`Usage: toolpin list [--scope all|project|global] [--client ${CLIENT_USAGE}] [--json]`);
1375
+ return;
1376
+ case "remove":
1377
+ case "uninstall":
1378
+ console.log(`Usage: toolpin ${command} <server-name> [--client ${CLIENT_USAGE}] [--scope project|global] [--file mcp-lock.json]`);
1379
+ return;
1380
+ case "lock":
1381
+ console.log(`Usage: toolpin lock <server-name> --client ${CLIENT_USAGE} [--scope project|global] [--file mcp-lock.json] [--verify [--skip-live-verification | --skip-live-verify] [--timeout 15000]]
1382
+ toolpin lock digest [--file mcp-lock.json] [--json]
1383
+ toolpin lock key-fingerprint --public-key public.pem [--json]
1384
+ toolpin lock sign --policy .toolpin/policy.json --key private.pem [--file mcp-lock.json] [--signature mcp-lock.sig]
1385
+ toolpin lock verify-signature --policy .toolpin/policy.json --public-key public.pem [--file mcp-lock.json] [--signature mcp-lock.sig]`);
1386
+ return;
1387
+ case "policy":
1388
+ console.log(`Usage: toolpin policy digest [--policy .toolpin/policy.json] [--json]
1389
+ toolpin policy check <server-name> --client ${CLIENT_USAGE} [--scope project|global] [--policy .toolpin/policy.json] [--json] [--live]`);
1390
+ return;
1391
+ case "tui":
1392
+ printTuiHelp();
1393
+ return;
1394
+ default:
1395
+ help();
1396
+ }
1397
+ }
1398
+ function upgradeHelp() {
1399
+ console.log("Usage: toolpin upgrade [--target latest|<version>] [--package-manager npm|pnpm|yarn|bun] [--dry-run] [--json]\n tpn upgrade [--target latest]");
1400
+ }
1401
+ async function test(rest) {
1402
+ const values = positional(rest);
1403
+ const name = values[0];
1404
+ if (!name)
1405
+ throw new Error("Usage: toolpin test <server-name> [--source toolpin|official|docker|all|custom-id] [--live] [--timeout 15000] [--json]");
1406
+ const timeout = numberFlag(rest, "--timeout", 15000);
1407
+ const server = await findServer(rest, name);
1408
+ const json = hasFlag(rest, "--json");
1409
+ if (!json) {
1410
+ console.error(`Testing ${server.name}@${server.version} (${server.registrySource}) with ${timeout}ms timeout...`);
1411
+ }
1412
+ const result = await testServer(server, timeout);
1413
+ if (json) {
1414
+ console.log(JSON.stringify(result, null, 2));
1415
+ }
1416
+ else {
1417
+ printHeader(result.ok ? "Test OK" : "Test failed");
1418
+ printField("server", result.serverName);
1419
+ printField("target", result.target);
1420
+ printField("duration", `${result.durationMs}ms`);
1421
+ printField("message", result.message);
1422
+ if (result.tools.length) {
1423
+ printSubhead("Tools");
1424
+ for (const tool of result.tools) {
1425
+ printBullet(`${tool.name}${tool.description ? `: ${truncate(tool.description, 120)}` : ""}`);
1426
+ }
1427
+ }
1428
+ }
1429
+ if (!result.ok)
1430
+ process.exitCode = 1;
1431
+ }
1432
+ function help() {
1433
+ console.log(`ToolPin ${TOOLPIN_VERSION}
1434
+ Trusted install, lockfile, and governance for MCP servers.
1435
+
1436
+ Quick start
1437
+ toolpin tui
1438
+ tpn upgrade
1439
+ toolpin --version
1440
+ tpn -v
1441
+ toolpin ingest
1442
+ toolpin search github
1443
+ toolpin install <server> --client claude --update-lock
1444
+
1445
+ Discovery
1446
+ toolpin ingest [--source toolpin|official|docker|all|custom-id] [--limit 100] [--pages 10]
1447
+ toolpin registry list [--json]
1448
+ toolpin registry enable <source-id>
1449
+ toolpin registry disable <source-id>
1450
+ toolpin sources [--json]
1451
+ toolpin search <query> [--source toolpin|official|docker|all|custom-id] [--limit 10] [--live] [--json]
1452
+ toolpin info <server> [--version <server-version>] [--source toolpin|official|docker|all|custom-id] [--json] [--live]
1453
+ toolpin scan <server> [--version <server-version>] [--source toolpin|official|docker|all|custom-id] [--live] [--json] [--sarif] [--timeout 15000] # description scan
1454
+ toolpin verify <server> [--version <server-version>] [--source toolpin|official|docker|all|custom-id] [--live] [--json] [--sarif] [--timeout 15000] [--skip-live-verification] [--require-verified]
1455
+ toolpin versions <server> [--source toolpin|official|docker|all|custom-id] [--live] [--limit 10] [--json]
1456
+ toolpin test <server> [--version <server-version>] [--source toolpin|official|docker|all|custom-id] [--live] [--timeout 15000] [--json]
1457
+ toolpin test-installed <server> --client|-c <client> --scope|-s project|global [--timeout 15000] [--json]
1458
+
1459
+ Install and config
1460
+ toolpin list|installed [--scope|-s all|project|global] [--client|-c <client|all>] [--json]
1461
+ toolpin plan <server> --client|-c <client> [--version <server-version>] [--source toolpin|official|docker|all|custom-id] [--live]
1462
+ toolpin install <server> --client|-c <client|all> [--version <server-version>] [--scope|-s project|global] [--source toolpin|official|docker|all|custom-id] [--global|-g] [--update-lock] [--verify] [--require-verified] [--policy .toolpin/policy.json] [--no-policy]
1463
+ toolpin adopt <installed> --client|-c <client> --scope|-s project|global [--source toolpin|official|docker|all|custom-id] [--live] [--dry-run] [--json]
1464
+ toolpin update <server> --client|-c <client> --scope|-s project|global [--version <server-version>] [--source toolpin|official|docker|all|custom-id] [--live] [--dry-run] [--json]
1465
+ toolpin update --all [--scope|-s all|project|global] [--client|-c <client|all>] [--source toolpin|official|docker|all|custom-id] [--live] [--dry-run] [--json]
1466
+ toolpin remove <server> [--client|-c <client|all>] [--scope|-s project|global] [--global|-g]
1467
+ toolpin uninstall <server> [--client|-c <client|all>] [--scope|-s project|global] [--global|-g]
1468
+ toolpin export-config <server> --client|-c <client|all> [--version <server-version>] [--source toolpin|official|docker|all|custom-id] [--live]
1469
+
1470
+ Lock and governance
1471
+ toolpin audit [--file mcp-lock.json] [--scope|-s all|project|global] [--client|-c <client|all>] [--verify] [--require-verified] [--json]
1472
+ toolpin audit server <server> [--version <server-version>] [--source toolpin|official|docker|all|custom-id] [--live] [--json]
1473
+ toolpin ci [--file mcp-lock.json] [--expect-digest sha256-...] [--signature mcp-lock.sig --public-key public.pem] [--policy .toolpin/policy.json] [--no-policy] [--source toolpin|official|docker|all|id] [--live] [--verify [--require-verified] [--skip-live-verification | --skip-live-verify] [--timeout 15000]] [--sarif]
1474
+ toolpin outdated [--file mcp-lock.json] [--source toolpin|official|docker|all|custom-id] [--live] [--json]
1475
+ toolpin doctor [--file mcp-lock.json] [--scope|-s all|project|global] [--global|-g] [--json]
1476
+ toolpin secrets audit [--file mcp-lock.json] [--scope|-s all|project|global] [--global|-g] [--json]
1477
+ toolpin policy digest [--policy .toolpin/policy.json] [--json]
1478
+ toolpin policy check <server> --client|-c <client|all> [--version <server-version>] [--source toolpin|official|docker|all|custom-id] [--policy .toolpin/policy.json]
1479
+ toolpin lock <server> --client|-c <client|all> [--version <server-version>] [--source toolpin|official|docker|all|custom-id] [--scope project|global] [--file mcp-lock.json]
1480
+ toolpin lock digest [--file mcp-lock.json] [--json]
1481
+ toolpin lock key-fingerprint --public-key public.pem [--json]
1482
+ toolpin lock sign --policy .toolpin/policy.json --key private.pem [--signature mcp-lock.sig] [--json]
1483
+ toolpin lock verify-signature --policy .toolpin/policy.json --public-key public.pem [--signature mcp-lock.sig] [--json]
1484
+
1485
+ Maintenance
1486
+ toolpin upgrade [--target latest|<version>] [--package-manager npm|pnpm|yarn|bun] [--dry-run]
1487
+ tpn upgrade
1488
+ tpn -v
1489
+
1490
+ Trust output
1491
+ score is metadata completeness; tier is evidence-gated
1492
+ verified requires a pinned target plus artifact proof
1493
+ cap explains why an otherwise strong score was limited
1494
+
1495
+ Common options
1496
+ --source toolpin|official|docker|all|id
1497
+ choose registry source; all means enabled sources
1498
+ --live fetch instead of cache
1499
+ --json machine-readable output where supported
1500
+ --sarif SARIF 2.1.0 output where supported
1501
+ --allow-hosted-directory-targets opt in to hosted Smithery directory targets
1502
+ toolpin --version, -v print ToolPin version
1503
+ --version <server-version> select a known server version for server commands
1504
+ --scope, -s project|global project folder vs current-user config
1505
+ --global, -g npm-style shortcut for --scope global
1506
+ --project, -p shortcut for --scope project
1507
+ --client, -c <client|all> target client config
1508
+ --target latest|<version> package target for toolpin upgrade
1509
+
1510
+ Clients
1511
+ ${CLIENT_USAGE.replaceAll("|", ", ")}
1512
+ `);
1513
+ }
1514
+ function printHeader(title) {
1515
+ console.log(title);
1516
+ console.log("-".repeat(Math.min(72, Math.max(8, title.length))));
1517
+ }
1518
+ function printSubhead(title) {
1519
+ console.log(`\n ${title}`);
1520
+ }
1521
+ function printField(label, value, color) {
1522
+ console.log(` ${label.padEnd(10)} ${colorize(value, color)}`);
1523
+ }
1524
+ function printCapExplanation(report) {
1525
+ const explanation = trustCapExplanation(report);
1526
+ if (explanation)
1527
+ printField("cap", explanation, WARN_COLOR);
1528
+ }
1529
+ function printBullet(value) {
1530
+ console.log(` - ${colorize(value, MUTED_COLOR)}`);
1531
+ }
1532
+ function printClientSkips(skipped) {
1533
+ for (const skip of skipped) {
1534
+ console.error(`Skipping ${skip.client}: ${skip.reason}`);
1535
+ }
1536
+ }
1537
+ function noInstallableClientsError(serverName, skipped) {
1538
+ return new Error([
1539
+ `No ToolPin-installable clients are available for ${serverName} in the selected scope.`,
1540
+ ...skipped.map((skip) => `- ${skip.client}: ${skip.reason}`),
1541
+ ].join("\n"));
1542
+ }
1543
+ function scopeDescription(scope) {
1544
+ return scope === "all" ? "all supported project/global configs" : `${scope} config`;
1545
+ }
1546
+ function printInstalledMutationResult(result, json) {
1547
+ if (json) {
1548
+ console.log(JSON.stringify(result, null, 2));
1549
+ return;
1550
+ }
1551
+ printHeader(`${result.dryRun ? "Dry run" : "Installed"} ${result.action}`);
1552
+ printField("server", result.serverName);
1553
+ printField("target", `${result.targetName}@${result.toVersion}`);
1554
+ printField("client", result.client);
1555
+ printField("scope", result.scope);
1556
+ if (result.fromVersion)
1557
+ printField("version", `${result.fromVersion} -> ${result.toVersion}`);
1558
+ printField("lockfile", result.lockfileWritten ? `${result.lockfilePath} updated` : `${result.lockfilePath} not written`);
1559
+ printSubhead("Plan");
1560
+ for (const line of result.planned)
1561
+ printBullet(line);
1562
+ if (result.removedAlias)
1563
+ printField("alias", `${result.removedAlias.action}: ${result.removedAlias.file}`);
1564
+ if (result.config)
1565
+ printField("config", `${result.config.action}: ${result.config.file}`);
1566
+ }
1567
+ function printInstalledUpdateAllResult(result, json) {
1568
+ if (json) {
1569
+ console.log(JSON.stringify(result, null, 2));
1570
+ return;
1571
+ }
1572
+ printHeader(`${result.dryRun ? "Dry run" : "Installed"} update all`);
1573
+ printField("scope", scopeDescription(result.scope));
1574
+ printField("client", result.client === "all" ? "all supported clients" : result.client);
1575
+ printField("updated", String(result.updated.length));
1576
+ printField("adoptable", `${result.skippedAdoptable.length} skipped`);
1577
+ if (result.updated.length) {
1578
+ printSubhead("Updated");
1579
+ for (const entry of result.updated) {
1580
+ printBullet(`${entry.serverName} -> ${entry.targetName}@${entry.toVersion} (${entry.client}/${entry.scope})`);
1581
+ }
1582
+ }
1583
+ if (result.skippedAdoptable.length) {
1584
+ printSubhead("Skipped adoptable");
1585
+ for (const entry of result.skippedAdoptable) {
1586
+ printBullet(`${entry.serverName} -> ${entry.targetName} (${entry.client}/${entry.scope}); run toolpin adopt ${entry.serverName} --client ${entry.client} --scope ${entry.scope}`);
1587
+ }
1588
+ }
1589
+ }
1590
+ async function runTui(rest) {
1591
+ if (rest.includes("--help") || rest.includes("-h")) {
1592
+ printTuiHelp();
1593
+ return;
1594
+ }
1595
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1596
+ throw new Error("toolpin tui requires an interactive terminal: stdin and stdout must both be TTYs.");
1597
+ }
1598
+ const { runTui: renderTui } = await import("./tui.js");
1599
+ renderTui();
1600
+ }
1601
+ function printTuiHelp() {
1602
+ console.log(`Usage: toolpin tui
1603
+
1604
+ Opens the ToolPin ${TOOLPIN_VERSION} full-screen terminal UI.
1605
+ Browse rows show full evidence labels (REVIEW, UNVERIFIED, BLOCKED, EVIDENCE).
1606
+ Browse defaults to source-first ordering: toolpin, official, docker, then other enabled sources.
1607
+ Use g for the exact source filter and a to cycle sort modes.
1608
+ Overview separates evidence tier, metadata profile score, pillar scores,
1609
+ and cap reasons; cap explains the evidence gate limit.`);
1610
+ }
1611
+ function hasFlag(values, flag) {
1612
+ return values.includes(flag);
1613
+ }
1614
+ function normalizeArgs(values) {
1615
+ const normalized = [];
1616
+ for (const value of values) {
1617
+ const equalIndex = value.indexOf("=");
1618
+ if (value.startsWith("-") && equalIndex > 1) {
1619
+ normalized.push(value.slice(0, equalIndex), value.slice(equalIndex + 1));
1620
+ }
1621
+ else {
1622
+ normalized.push(value);
1623
+ }
1624
+ }
1625
+ return normalized;
1626
+ }
1627
+ function validateFlags(command, values) {
1628
+ for (const value of values) {
1629
+ if (!value.startsWith("-"))
1630
+ continue;
1631
+ if (KNOWN_FLAGS.has(value))
1632
+ continue;
1633
+ const suggestion = nearestFlag(value);
1634
+ throw new Error(`Unknown flag for ${command}: ${value}.${suggestion ? ` Did you mean ${suggestion}?` : ""}`);
1635
+ }
1636
+ }
1637
+ function nearestFlag(value) {
1638
+ let best;
1639
+ for (const flag of KNOWN_FLAGS) {
1640
+ const distance = editDistance(value, flag);
1641
+ if (!best || distance < best.distance)
1642
+ best = { flag, distance };
1643
+ }
1644
+ return best && best.distance <= 3 ? best.flag : undefined;
1645
+ }
1646
+ function editDistance(left, right) {
1647
+ const rows = Array.from({ length: left.length + 1 }, () => new Array(right.length + 1).fill(0));
1648
+ for (let i = 0; i <= left.length; i += 1)
1649
+ rows[i][0] = i;
1650
+ for (let j = 0; j <= right.length; j += 1)
1651
+ rows[0][j] = j;
1652
+ for (let i = 1; i <= left.length; i += 1) {
1653
+ for (let j = 1; j <= right.length; j += 1) {
1654
+ rows[i][j] = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + (left[i - 1] === right[j - 1] ? 0 : 1));
1655
+ }
1656
+ }
1657
+ return rows[left.length][right.length];
1658
+ }
1659
+ function hasAnyFlag(values, flags) {
1660
+ return flags.some((flag) => hasFlag(values, flag));
1661
+ }
1662
+ function isHelp(values) {
1663
+ return hasAnyFlag(values, ["--help", "-h"]);
1664
+ }
1665
+ function stringFlag(values, flag, fallback) {
1666
+ const index = values.indexOf(flag);
1667
+ return index >= 0 ? (values[index + 1] ?? fallback) : fallback;
1668
+ }
1669
+ function stringAnyFlag(values, flags, fallback) {
1670
+ for (const flag of flags) {
1671
+ const index = values.indexOf(flag);
1672
+ if (index >= 0)
1673
+ return values[index + 1] ?? fallback;
1674
+ }
1675
+ return fallback;
1676
+ }
1677
+ function numberFlag(values, flag, fallback) {
1678
+ const value = stringFlag(values, flag, String(fallback));
1679
+ const parsed = Number.parseInt(value, 10);
1680
+ return Number.isFinite(parsed) ? parsed : fallback;
1681
+ }
1682
+ function clientFlag(values, fallback) {
1683
+ const value = stringAnyFlag(values, ["--client", "-c"], fallback);
1684
+ if (value === "all" || isClientName(value)) {
1685
+ return value;
1686
+ }
1687
+ throw new Error(`--client/-c must be ${CLIENT_USAGE.replaceAll("|", ", ")}`);
1688
+ }
1689
+ function sourceFlag(values, fallback) {
1690
+ const value = stringFlag(values, "--source", fallback);
1691
+ if (/^[a-zA-Z0-9._/-]+$/.test(value)) {
1692
+ return value;
1693
+ }
1694
+ throw new Error("--source must be all or a registry source id");
1695
+ }
1696
+ function upgradePackageManager(values) {
1697
+ const requested = stringFlag(values, "--package-manager", detectPackageManager());
1698
+ if (requested === "npm" || requested === "pnpm" || requested === "yarn" || requested === "bun")
1699
+ return requested;
1700
+ throw new Error("--package-manager must be npm, pnpm, yarn, or bun");
1701
+ }
1702
+ function detectPackageManager() {
1703
+ const userAgent = process.env.npm_config_user_agent ?? "";
1704
+ if (userAgent.startsWith("pnpm/"))
1705
+ return "pnpm";
1706
+ if (userAgent.startsWith("yarn/"))
1707
+ return "yarn";
1708
+ if (userAgent.startsWith("bun/"))
1709
+ return "bun";
1710
+ return "npm";
1711
+ }
1712
+ function upgradeCommand(packageManager, target) {
1713
+ if (!target || target.startsWith("-"))
1714
+ throw new Error("--target requires a package version or dist-tag.");
1715
+ const spec = `${TOOLPIN_NPM_PACKAGE}@${target}`;
1716
+ const executable = packageManagerExecutable(packageManager);
1717
+ const args = packageManager === "npm"
1718
+ ? ["install", "-g", spec]
1719
+ : packageManager === "pnpm"
1720
+ ? ["add", "-g", spec]
1721
+ : packageManager === "yarn"
1722
+ ? ["global", "add", spec]
1723
+ : ["add", "-g", spec];
1724
+ return {
1725
+ packageManager,
1726
+ executable,
1727
+ args,
1728
+ display: [executable, ...args].join(" "),
1729
+ };
1730
+ }
1731
+ function packageManagerExecutable(packageManager) {
1732
+ return process.platform === "win32" ? `${packageManager}.cmd` : packageManager;
1733
+ }
1734
+ async function runUpgradeCommand(command) {
1735
+ const exitCode = await new Promise((resolve, reject) => {
1736
+ const child = spawn(command.executable, command.args, { stdio: "inherit" });
1737
+ child.on("error", reject);
1738
+ child.on("close", resolve);
1739
+ });
1740
+ if (exitCode !== 0) {
1741
+ throw new Error(`Upgrade command failed with exit code ${exitCode ?? "unknown"}: ${command.display}`);
1742
+ }
1743
+ }
1744
+ function serverVersionFlag(values) {
1745
+ const index = values.indexOf("--version");
1746
+ if (index < 0)
1747
+ return undefined;
1748
+ const value = values[index + 1];
1749
+ if (!value || value.startsWith("-"))
1750
+ throw new Error("--version requires a server version value.");
1751
+ return value;
1752
+ }
1753
+ function scopeFlag(values, fallback) {
1754
+ const value = hasAnyFlag(values, ["--global", "-g"])
1755
+ ? "global"
1756
+ : hasAnyFlag(values, ["--project", "-p"])
1757
+ ? "project"
1758
+ : stringAnyFlag(values, ["--scope", "-s"], fallback);
1759
+ if (["all", "project", "global"].includes(value))
1760
+ return value;
1761
+ throw new Error("--scope/-s must be all, project, or global");
1762
+ }
1763
+ function cacheHasSource(entries, source, enabledSources = new Set()) {
1764
+ const sources = new Set(normalizeEntries(entries).map((server) => server.registrySource));
1765
+ return source === "all" ? [...enabledSources].every((enabled) => sources.has(enabled)) : sources.has(source);
1766
+ }
1767
+ function positional(values) {
1768
+ const result = [];
1769
+ for (let index = 0; index < values.length; index += 1) {
1770
+ const value = values[index];
1771
+ if (value.startsWith("-")) {
1772
+ if (VALUE_FLAGS.has(value))
1773
+ index += 1;
1774
+ continue;
1775
+ }
1776
+ result.push(value);
1777
+ }
1778
+ return result;
1779
+ }
1780
+ function lockedHasLivePins(locked) {
1781
+ return Boolean(locked.capabilityManifest?.toolDescriptionHash || locked.capabilityManifest?.toolManifestHash);
1782
+ }
1783
+ function liveVerificationEnabled(values) {
1784
+ return !hasAnyFlag(values, ["--skip-live-verification", "--skip-live-verify"]);
1785
+ }
1786
+ function truncate(value, maxLength) {
1787
+ return value.length > maxLength ? `${value.slice(0, Math.max(0, maxLength - 3))}...` : value;
1788
+ }
1789
+ function verificationStatus(verifyRequested, report) {
1790
+ if (!verifyRequested)
1791
+ return "skipped";
1792
+ return verificationOutcome(report);
1793
+ }
1794
+ function verificationOutcome(report) {
1795
+ if (!report || !report.ok)
1796
+ return "failed";
1797
+ const hasPin = report.evidence.some((entry) => (entry.status === "passed" || entry.status === "declared") && ["package_pin", "digest_present", "file_hash_present"].includes(entry.code));
1798
+ if (report.verifiedProvenance === true && hasPin && hasFreshTrustedArtifactEvidence(report.evidence))
1799
+ return "verified";
1800
+ return "incomplete";
1801
+ }
1802
+ function trustTierColor(tier) {
1803
+ if (tier === "verified")
1804
+ return OK_COLOR;
1805
+ if (tier === "conditional")
1806
+ return MUTED_COLOR;
1807
+ return ERR_COLOR;
1808
+ }
1809
+ function colorize(value, color) {
1810
+ if (!color || !process.stdout.isTTY || process.env.NO_COLOR)
1811
+ return value;
1812
+ return `${color}${value}\x1b[0m`;
1813
+ }
1814
+ function printVerificationReport(report) {
1815
+ printHeader(`Verification ${verificationOutcome(report)}: ${report.serverName}@${report.serverVersion}`);
1816
+ if (report.badges.length)
1817
+ printField("badges", report.badges.join(", "));
1818
+ printField("evidence", evidenceSummary(report));
1819
+ for (const entry of report.evidence) {
1820
+ if (entry.verificationMethod) {
1821
+ const anchor = entry.trustAnchor ? ` via ${entry.trustAnchor}` : "";
1822
+ printField("method", `${entry.code}: ${entry.verificationMethod}${anchor}`);
1823
+ }
1824
+ }
1825
+ printField("packages", report.capabilityManifest.packageTypes.join(", ") || "none");
1826
+ printField("transport", report.capabilityManifest.transports.join(", ") || "none");
1827
+ if (report.capabilityManifest.remoteHosts.length)
1828
+ printField("hosts", report.capabilityManifest.remoteHosts.join(", "));
1829
+ if (report.capabilityManifest.secrets.length) {
1830
+ printField("secrets", report.capabilityManifest.secrets.map((secret) => `${secret.source}:${secret.name}`).join(", "));
1831
+ }
1832
+ if (report.capabilityManifest.toolDescriptionHash) {
1833
+ const hash = report.capabilityManifest.toolDescriptionHash;
1834
+ printField("tools hash", `${hash.algorithm}-${hash.value} (${hash.toolCount} tool(s))`);
1835
+ }
1836
+ if (report.capabilityManifest.toolDescriptionScan) {
1837
+ const scan = report.capabilityManifest.toolDescriptionScan;
1838
+ printField("scan", `${scan.findings.length} advisory finding(s) across ${scan.scannedDescriptions} description(s)`);
1839
+ }
1840
+ for (const issue of report.issues) {
1841
+ printBullet(`${issue.severity.toUpperCase()}: ${issue.code}: ${issue.message}`);
1842
+ }
1843
+ }