@enactprotocol/cli 2.1.24 → 2.1.29

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 (57) hide show
  1. package/dist/commands/index.d.ts +2 -0
  2. package/dist/commands/index.d.ts.map +1 -1
  3. package/dist/commands/index.js +4 -0
  4. package/dist/commands/index.js.map +1 -1
  5. package/dist/commands/init/templates/claude.d.ts +1 -1
  6. package/dist/commands/init/templates/claude.d.ts.map +1 -1
  7. package/dist/commands/init/templates/claude.js +268 -28
  8. package/dist/commands/init/templates/claude.js.map +1 -1
  9. package/dist/commands/init/templates/tool-agents.d.ts +1 -1
  10. package/dist/commands/init/templates/tool-agents.d.ts.map +1 -1
  11. package/dist/commands/init/templates/tool-agents.js +90 -15
  12. package/dist/commands/init/templates/tool-agents.js.map +1 -1
  13. package/dist/commands/install/index.d.ts.map +1 -1
  14. package/dist/commands/install/index.js +9 -1
  15. package/dist/commands/install/index.js.map +1 -1
  16. package/dist/commands/learn/index.d.ts.map +1 -1
  17. package/dist/commands/learn/index.js +4 -11
  18. package/dist/commands/learn/index.js.map +1 -1
  19. package/dist/commands/mcp/index.d.ts.map +1 -1
  20. package/dist/commands/mcp/index.js +204 -53
  21. package/dist/commands/mcp/index.js.map +1 -1
  22. package/dist/commands/run/index.d.ts.map +1 -1
  23. package/dist/commands/run/index.js +380 -39
  24. package/dist/commands/run/index.js.map +1 -1
  25. package/dist/commands/validate/index.d.ts +11 -0
  26. package/dist/commands/validate/index.d.ts.map +1 -0
  27. package/dist/commands/validate/index.js +299 -0
  28. package/dist/commands/validate/index.js.map +1 -0
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +6 -2
  32. package/dist/index.js.map +1 -1
  33. package/dist/types.d.ts +2 -0
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/types.js.map +1 -1
  36. package/dist/utils/errors.d.ts +8 -1
  37. package/dist/utils/errors.d.ts.map +1 -1
  38. package/dist/utils/errors.js +13 -2
  39. package/dist/utils/errors.js.map +1 -1
  40. package/package.json +5 -5
  41. package/src/commands/index.ts +5 -0
  42. package/src/commands/init/templates/claude.ts +268 -28
  43. package/src/commands/init/templates/tool-agents.ts +90 -15
  44. package/src/commands/install/index.ts +11 -0
  45. package/src/commands/learn/index.ts +6 -11
  46. package/src/commands/mcp/index.ts +768 -0
  47. package/src/commands/run/README.md +68 -1
  48. package/src/commands/run/index.ts +475 -35
  49. package/src/commands/validate/index.ts +344 -0
  50. package/src/index.ts +8 -1
  51. package/src/types.ts +2 -0
  52. package/src/utils/errors.ts +26 -6
  53. package/tests/commands/init.test.ts +2 -2
  54. package/tests/commands/run.test.ts +260 -0
  55. package/tests/commands/validate.test.ts +81 -0
  56. package/tests/utils/errors.test.ts +36 -0
  57. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,768 @@
1
+ /**
2
+ * enact mcp command
3
+ *
4
+ * Manage MCP-exposed tools and toolsets.
5
+ *
6
+ * Subcommands:
7
+ * - install: Show configuration for MCP clients (Claude Code, etc.)
8
+ * - list: List tools exposed to MCP
9
+ * - add: Add a tool to MCP (from global installs)
10
+ * - remove: Remove a tool from MCP
11
+ * - toolsets: List available toolsets
12
+ * - use: Switch active toolset
13
+ * - toolset: Manage toolsets (create, delete, add, remove)
14
+ */
15
+
16
+ import { existsSync } from "node:fs";
17
+ import { homedir } from "node:os";
18
+ import { join } from "node:path";
19
+ import {
20
+ addMcpTool,
21
+ addToolToToolset,
22
+ createToolset,
23
+ deleteToolset,
24
+ getActiveToolset,
25
+ listMcpTools,
26
+ listToolsets,
27
+ loadToolsRegistry,
28
+ removeMcpTool,
29
+ removeToolFromToolset,
30
+ setActiveToolset,
31
+ syncMcpWithGlobalTools,
32
+ tryLoadManifestFromDir,
33
+ } from "@enactprotocol/shared";
34
+ import type { Command } from "commander";
35
+ import type { CommandContext, GlobalOptions } from "../../types";
36
+ import {
37
+ type TableColumn,
38
+ dim,
39
+ error,
40
+ formatError,
41
+ header,
42
+ info,
43
+ json,
44
+ keyValue,
45
+ newline,
46
+ select,
47
+ success,
48
+ table,
49
+ } from "../../utils";
50
+
51
+ interface McpInstallOptions extends GlobalOptions {
52
+ client?: string;
53
+ }
54
+
55
+ interface McpOptions extends GlobalOptions {
56
+ all?: boolean;
57
+ sync?: boolean;
58
+ }
59
+
60
+ interface ToolInfo {
61
+ name: string;
62
+ version: string;
63
+ description: string;
64
+ [key: string]: string;
65
+ }
66
+
67
+ /**
68
+ * Get the path to enact-mcp binary
69
+ */
70
+ function getEnactMcpPath(): string {
71
+ // For now, assume it's installed globally or via npm
72
+ // Could be enhanced to detect actual binary location
73
+ return "enact-mcp";
74
+ }
75
+
76
+ /**
77
+ * Known MCP client definitions
78
+ */
79
+ interface McpClient {
80
+ id: string;
81
+ name: string;
82
+ configPath: string | (() => string);
83
+ configFormat: "json" | "jsonc";
84
+ detected: boolean;
85
+ instructions: string;
86
+ configTemplate: (mcpPath: string) => string;
87
+ }
88
+
89
+ /**
90
+ * Detect installed MCP clients
91
+ */
92
+ function detectClients(): McpClient[] {
93
+ const home = homedir();
94
+ const platform = process.platform;
95
+
96
+ const clients: McpClient[] = [
97
+ {
98
+ id: "claude-code",
99
+ name: "Claude Code",
100
+ configPath: () => {
101
+ // Claude Code settings location varies by platform
102
+ if (platform === "darwin") {
103
+ return join(
104
+ home,
105
+ "Library",
106
+ "Application Support",
107
+ "Claude",
108
+ "claude_desktop_config.json"
109
+ );
110
+ }
111
+ if (platform === "win32") {
112
+ return join(home, "AppData", "Roaming", "Claude", "claude_desktop_config.json");
113
+ }
114
+ return join(home, ".config", "claude", "claude_desktop_config.json");
115
+ },
116
+ configFormat: "json",
117
+ detected: false,
118
+ instructions: "Add to mcpServers in your Claude Desktop config:",
119
+ configTemplate: (mcpPath) => `{
120
+ "mcpServers": {
121
+ "enact": {
122
+ "command": "${mcpPath}",
123
+ "args": []
124
+ }
125
+ }
126
+ }`,
127
+ },
128
+ {
129
+ id: "cursor",
130
+ name: "Cursor",
131
+ configPath: () => {
132
+ if (platform === "darwin") {
133
+ return join(
134
+ home,
135
+ "Library",
136
+ "Application Support",
137
+ "Cursor",
138
+ "User",
139
+ "globalStorage",
140
+ "cursor.mcp",
141
+ "config.json"
142
+ );
143
+ }
144
+ if (platform === "win32") {
145
+ return join(
146
+ home,
147
+ "AppData",
148
+ "Roaming",
149
+ "Cursor",
150
+ "User",
151
+ "globalStorage",
152
+ "cursor.mcp",
153
+ "config.json"
154
+ );
155
+ }
156
+ return join(
157
+ home,
158
+ ".config",
159
+ "Cursor",
160
+ "User",
161
+ "globalStorage",
162
+ "cursor.mcp",
163
+ "config.json"
164
+ );
165
+ },
166
+ configFormat: "json",
167
+ detected: false,
168
+ instructions: "Add to your Cursor MCP configuration:",
169
+ configTemplate: (mcpPath) => `{
170
+ "mcpServers": {
171
+ "enact": {
172
+ "command": "${mcpPath}",
173
+ "args": []
174
+ }
175
+ }
176
+ }`,
177
+ },
178
+ {
179
+ id: "vscode",
180
+ name: "VS Code (with MCP extension)",
181
+ configPath: () => {
182
+ if (platform === "darwin") {
183
+ return join(home, "Library", "Application Support", "Code", "User", "settings.json");
184
+ }
185
+ if (platform === "win32") {
186
+ return join(home, "AppData", "Roaming", "Code", "User", "settings.json");
187
+ }
188
+ return join(home, ".config", "Code", "User", "settings.json");
189
+ },
190
+ configFormat: "jsonc",
191
+ detected: false,
192
+ instructions: "Add to your VS Code settings.json:",
193
+ configTemplate: (mcpPath) => `{
194
+ "mcp.servers": {
195
+ "enact": {
196
+ "command": "${mcpPath}",
197
+ "args": []
198
+ }
199
+ }
200
+ }`,
201
+ },
202
+ {
203
+ id: "generic",
204
+ name: "Other MCP Client",
205
+ configPath: "",
206
+ configFormat: "json",
207
+ detected: true, // Always available
208
+ instructions: "Generic MCP server configuration:",
209
+ configTemplate: (mcpPath) => `{
210
+ "command": "${mcpPath}",
211
+ "args": []
212
+ }`,
213
+ },
214
+ ];
215
+
216
+ // Detect which clients are installed
217
+ for (const client of clients) {
218
+ if (client.id === "generic") continue;
219
+
220
+ const configPath =
221
+ typeof client.configPath === "function" ? client.configPath() : client.configPath;
222
+ // Check if the app directory exists (not just config file)
223
+ const appDir = join(configPath, "..", "..");
224
+ client.detected = existsSync(appDir) || existsSync(configPath);
225
+ }
226
+
227
+ return clients;
228
+ }
229
+
230
+ /**
231
+ * Show MCP configuration for a specific client
232
+ */
233
+ function showClientConfig(client: McpClient, mcpPath: string): void {
234
+ header(`Enact MCP Server - ${client.name}`);
235
+ newline();
236
+
237
+ info(client.instructions);
238
+ newline();
239
+ console.log(client.configTemplate(mcpPath));
240
+
241
+ if (client.id !== "generic") {
242
+ const configPath =
243
+ typeof client.configPath === "function" ? client.configPath() : client.configPath;
244
+ newline();
245
+ dim(`Config file: ${configPath}`);
246
+ }
247
+
248
+ if (client.id === "claude-code") {
249
+ newline();
250
+ dim('For HTTP mode (remote access), use args: ["--http", "--port", "3000"]');
251
+ }
252
+
253
+ newline();
254
+ dim("The MCP server exposes all tools in ~/.enact/mcp.json");
255
+ dim("Use 'enact mcp sync' to add globally installed tools");
256
+ dim("Use 'enact mcp list' to see available tools");
257
+ }
258
+
259
+ /**
260
+ * Show MCP configuration for various clients
261
+ */
262
+ async function installHandler(options: McpInstallOptions, isInteractive: boolean): Promise<void> {
263
+ const mcpPath = getEnactMcpPath();
264
+ const clients = detectClients();
265
+
266
+ // If --json, output config
267
+ if (options.json) {
268
+ const config = {
269
+ enact: {
270
+ command: mcpPath,
271
+ args: [] as string[],
272
+ },
273
+ };
274
+ json(config);
275
+ return;
276
+ }
277
+
278
+ // If client specified via flag, show that client's config
279
+ if (options.client) {
280
+ const client = clients.find(
281
+ (c) => c.id === options.client || c.name.toLowerCase().includes(options.client!.toLowerCase())
282
+ );
283
+ if (!client) {
284
+ error(`Unknown client: ${options.client}`);
285
+ newline();
286
+ dim(`Available clients: ${clients.map((c) => c.id).join(", ")}`);
287
+ process.exit(1);
288
+ }
289
+ showClientConfig(client, mcpPath);
290
+ return;
291
+ }
292
+
293
+ // Interactive mode: show detected clients and let user choose
294
+ if (isInteractive) {
295
+ const detectedClients = clients.filter((c) => c.detected);
296
+ const genericClient = detectedClients.find((c) => c.id === "generic");
297
+
298
+ if (detectedClients.length === 1 && genericClient) {
299
+ // No clients detected, show generic
300
+ showClientConfig(genericClient, mcpPath);
301
+ return;
302
+ }
303
+
304
+ header("Enact MCP Server Setup");
305
+ newline();
306
+
307
+ // Show which clients were detected
308
+ const installedClients = detectedClients.filter((c) => c.id !== "generic");
309
+ if (installedClients.length > 0) {
310
+ info(`Detected MCP clients: ${installedClients.map((c) => c.name).join(", ")}`);
311
+ newline();
312
+ }
313
+
314
+ // Let user select
315
+ const selectedId = await select(
316
+ "Select your MCP client:",
317
+ detectedClients.map((c) => {
318
+ const option: { value: string; label: string; hint?: string } = {
319
+ value: c.id,
320
+ label: c.name,
321
+ };
322
+ if (c.id !== "generic" && c.detected) {
323
+ option.hint = "detected";
324
+ }
325
+ return option;
326
+ })
327
+ );
328
+
329
+ if (!selectedId) {
330
+ info("Cancelled");
331
+ return;
332
+ }
333
+
334
+ const selectedClient = clients.find((c) => c.id === selectedId);
335
+ if (selectedClient) {
336
+ newline();
337
+ showClientConfig(selectedClient, mcpPath);
338
+ }
339
+ } else {
340
+ // Non-interactive: show all detected clients
341
+ const detectedClients = clients.filter((c) => c.detected && c.id !== "generic");
342
+ const genericClient = clients.find((c) => c.id === "generic");
343
+
344
+ if (detectedClients.length === 0 && genericClient) {
345
+ showClientConfig(genericClient, mcpPath);
346
+ } else if (detectedClients.length === 1 && detectedClients[0]) {
347
+ showClientConfig(detectedClients[0], mcpPath);
348
+ } else {
349
+ // Multiple clients detected, show them all
350
+ info("Multiple MCP clients detected. Use --client to specify one:");
351
+ newline();
352
+ for (const client of detectedClients) {
353
+ dim(` --client ${client.id} (${client.name})`);
354
+ }
355
+ newline();
356
+ dim("Or run interactively to select from a list.");
357
+ }
358
+ }
359
+ }
360
+
361
+ /**
362
+ * List MCP-exposed tools
363
+ */
364
+ async function listMcpHandler(options: McpOptions): Promise<void> {
365
+ const mcpTools = listMcpTools();
366
+ const activeToolset = getActiveToolset();
367
+
368
+ if (options.json) {
369
+ json({
370
+ tools: mcpTools,
371
+ activeToolset,
372
+ });
373
+ return;
374
+ }
375
+
376
+ if (mcpTools.length === 0) {
377
+ info("No tools exposed to MCP.");
378
+ newline();
379
+ dim("Add tools with 'enact mcp add <tool>'");
380
+ dim("Or sync with global installs: 'enact mcp sync'");
381
+ return;
382
+ }
383
+
384
+ header("MCP Tools");
385
+ if (activeToolset) {
386
+ keyValue("Active toolset", activeToolset);
387
+ }
388
+ newline();
389
+
390
+ const toolInfos: ToolInfo[] = mcpTools.map((tool) => {
391
+ const loaded = tryLoadManifestFromDir(tool.cachePath);
392
+ return {
393
+ name: tool.name,
394
+ version: tool.version,
395
+ description: loaded?.manifest.description ?? "-",
396
+ };
397
+ });
398
+
399
+ const columns: TableColumn[] = [
400
+ { key: "name", header: "Name", width: 30 },
401
+ { key: "version", header: "Version", width: 12 },
402
+ { key: "description", header: "Description", width: 45 },
403
+ ];
404
+
405
+ table(toolInfos, columns);
406
+ newline();
407
+ dim(`Total: ${mcpTools.length} tool(s)`);
408
+ }
409
+
410
+ /**
411
+ * Add a tool to MCP from global installs
412
+ */
413
+ async function addMcpHandler(
414
+ toolName: string,
415
+ options: McpOptions,
416
+ _ctx: CommandContext
417
+ ): Promise<void> {
418
+ // Get global tools to find the version
419
+ const globalRegistry = loadToolsRegistry("global");
420
+ const version = globalRegistry.tools[toolName];
421
+
422
+ if (!version) {
423
+ error(`Tool "${toolName}" is not installed globally.`);
424
+ newline();
425
+ dim(`Install it first with: enact install ${toolName} -g`);
426
+ process.exit(1);
427
+ }
428
+
429
+ addMcpTool(toolName, version);
430
+
431
+ if (options.json) {
432
+ json({ success: true, tool: toolName, version });
433
+ return;
434
+ }
435
+
436
+ success(`Added ${toolName}@${version} to MCP`);
437
+ }
438
+
439
+ /**
440
+ * Remove a tool from MCP
441
+ */
442
+ async function removeMcpHandler(toolName: string, options: McpOptions): Promise<void> {
443
+ const removed = removeMcpTool(toolName);
444
+
445
+ if (!removed) {
446
+ error(`Tool "${toolName}" is not in MCP registry.`);
447
+ process.exit(1);
448
+ }
449
+
450
+ if (options.json) {
451
+ json({ success: true, tool: toolName });
452
+ return;
453
+ }
454
+
455
+ success(`Removed ${toolName} from MCP`);
456
+ }
457
+
458
+ /**
459
+ * Sync MCP registry with global tools
460
+ */
461
+ async function syncMcpHandler(options: McpOptions): Promise<void> {
462
+ const globalRegistry = loadToolsRegistry("global");
463
+ const beforeCount = listMcpTools().length;
464
+
465
+ syncMcpWithGlobalTools(globalRegistry.tools);
466
+
467
+ const afterCount = listMcpTools().length;
468
+ const added = afterCount - beforeCount;
469
+
470
+ if (options.json) {
471
+ json({ success: true, added, total: afterCount });
472
+ return;
473
+ }
474
+
475
+ if (added > 0) {
476
+ success(`Synced ${added} new tool(s) from global installs`);
477
+ } else {
478
+ info("MCP registry is already in sync with global tools");
479
+ }
480
+ dim(`Total MCP tools: ${afterCount}`);
481
+ }
482
+
483
+ /**
484
+ * List toolsets
485
+ */
486
+ async function toolsetsHandler(options: McpOptions): Promise<void> {
487
+ const toolsets = listToolsets();
488
+
489
+ if (options.json) {
490
+ json(toolsets);
491
+ return;
492
+ }
493
+
494
+ if (toolsets.length === 0) {
495
+ info("No toolsets configured.");
496
+ newline();
497
+ dim("Create one with: enact mcp toolset create <name>");
498
+ return;
499
+ }
500
+
501
+ header("Toolsets");
502
+ newline();
503
+
504
+ for (const ts of toolsets) {
505
+ const prefix = ts.isActive ? "→ " : " ";
506
+ const suffix = ts.isActive ? " (active)" : "";
507
+ console.log(`${prefix}${ts.name}${suffix}`);
508
+ if (ts.tools.length > 0) {
509
+ dim(` ${ts.tools.join(", ")}`);
510
+ } else {
511
+ dim(" (empty)");
512
+ }
513
+ }
514
+ newline();
515
+ }
516
+
517
+ /**
518
+ * Switch active toolset
519
+ */
520
+ async function useHandler(toolsetName: string | undefined, options: McpOptions): Promise<void> {
521
+ if (options.all) {
522
+ // Use all tools (disable toolset filtering)
523
+ setActiveToolset(null);
524
+
525
+ if (options.json) {
526
+ json({ success: true, activeToolset: null });
527
+ return;
528
+ }
529
+
530
+ success("Using all MCP tools (no toolset filter)");
531
+ return;
532
+ }
533
+
534
+ if (!toolsetName) {
535
+ error("Please specify a toolset name or use --all");
536
+ process.exit(1);
537
+ }
538
+
539
+ const result = setActiveToolset(toolsetName);
540
+
541
+ if (!result) {
542
+ error(`Toolset "${toolsetName}" not found.`);
543
+ dim("List available toolsets with: enact mcp toolsets");
544
+ process.exit(1);
545
+ }
546
+
547
+ if (options.json) {
548
+ json({ success: true, activeToolset: toolsetName });
549
+ return;
550
+ }
551
+
552
+ success(`Now using toolset: ${toolsetName}`);
553
+ }
554
+
555
+ /**
556
+ * Toolset management subcommands
557
+ */
558
+ async function toolsetHandler(action: string, args: string[], options: McpOptions): Promise<void> {
559
+ switch (action) {
560
+ case "create": {
561
+ const name = args[0];
562
+ if (!name) {
563
+ error("Please specify a toolset name");
564
+ process.exit(1);
565
+ }
566
+ createToolset(name, []);
567
+ if (options.json) {
568
+ json({ success: true, action: "create", toolset: name });
569
+ return;
570
+ }
571
+ success(`Created toolset: ${name}`);
572
+ break;
573
+ }
574
+
575
+ case "delete": {
576
+ const name = args[0];
577
+ if (!name) {
578
+ error("Please specify a toolset name");
579
+ process.exit(1);
580
+ }
581
+ const deleted = deleteToolset(name);
582
+ if (!deleted) {
583
+ error(`Toolset "${name}" not found.`);
584
+ process.exit(1);
585
+ }
586
+ if (options.json) {
587
+ json({ success: true, action: "delete", toolset: name });
588
+ return;
589
+ }
590
+ success(`Deleted toolset: ${name}`);
591
+ break;
592
+ }
593
+
594
+ case "add": {
595
+ const [toolsetName, toolName] = args;
596
+ if (!toolsetName || !toolName) {
597
+ error("Usage: enact mcp toolset add <toolset> <tool>");
598
+ process.exit(1);
599
+ }
600
+ const added = addToolToToolset(toolsetName, toolName);
601
+ if (!added) {
602
+ error(`Toolset "${toolsetName}" not found.`);
603
+ process.exit(1);
604
+ }
605
+ if (options.json) {
606
+ json({ success: true, action: "add", toolset: toolsetName, tool: toolName });
607
+ return;
608
+ }
609
+ success(`Added ${toolName} to toolset ${toolsetName}`);
610
+ break;
611
+ }
612
+
613
+ case "remove": {
614
+ const [toolsetName, toolName] = args;
615
+ if (!toolsetName || !toolName) {
616
+ error("Usage: enact mcp toolset remove <toolset> <tool>");
617
+ process.exit(1);
618
+ }
619
+ const removed = removeToolFromToolset(toolsetName, toolName);
620
+ if (!removed) {
621
+ error(`Tool "${toolName}" not found in toolset "${toolsetName}".`);
622
+ process.exit(1);
623
+ }
624
+ if (options.json) {
625
+ json({ success: true, action: "remove", toolset: toolsetName, tool: toolName });
626
+ return;
627
+ }
628
+ success(`Removed ${toolName} from toolset ${toolsetName}`);
629
+ break;
630
+ }
631
+
632
+ default:
633
+ error(`Unknown toolset action: ${action}`);
634
+ newline();
635
+ dim("Available actions: create, delete, add, remove");
636
+ process.exit(1);
637
+ }
638
+ }
639
+
640
+ /**
641
+ * Configure the mcp command
642
+ */
643
+ export function configureMcpCommand(program: Command): void {
644
+ const mcp = program.command("mcp").description("Manage MCP-exposed tools and toolsets");
645
+
646
+ // enact mcp install
647
+ mcp
648
+ .command("install")
649
+ .description("Show configuration to add Enact MCP server to your AI client")
650
+ .option("--client <client>", "Target client (claude-code, cursor, vscode, generic)")
651
+ .option("--json", "Output as JSON")
652
+ .action(async (options: McpInstallOptions) => {
653
+ const isInteractive = process.stdout.isTTY ?? false;
654
+ try {
655
+ await installHandler(options, isInteractive);
656
+ } catch (err) {
657
+ error(formatError(err));
658
+ process.exit(1);
659
+ }
660
+ });
661
+
662
+ // enact mcp list
663
+ mcp
664
+ .command("list")
665
+ .alias("ls")
666
+ .description("List tools exposed to MCP clients")
667
+ .option("--json", "Output as JSON")
668
+ .action(async (options: McpOptions) => {
669
+ try {
670
+ await listMcpHandler(options);
671
+ } catch (err) {
672
+ error(formatError(err));
673
+ process.exit(1);
674
+ }
675
+ });
676
+
677
+ // enact mcp add <tool>
678
+ mcp
679
+ .command("add <tool>")
680
+ .description("Add a globally installed tool to MCP")
681
+ .option("--json", "Output as JSON")
682
+ .action(async (tool: string, options: McpOptions) => {
683
+ const ctx: CommandContext = {
684
+ cwd: process.cwd(),
685
+ options,
686
+ isCI: Boolean(process.env.CI),
687
+ isInteractive: process.stdout.isTTY ?? false,
688
+ };
689
+ try {
690
+ await addMcpHandler(tool, options, ctx);
691
+ } catch (err) {
692
+ error(formatError(err));
693
+ process.exit(1);
694
+ }
695
+ });
696
+
697
+ // enact mcp remove <tool>
698
+ mcp
699
+ .command("remove <tool>")
700
+ .alias("rm")
701
+ .description("Remove a tool from MCP")
702
+ .option("--json", "Output as JSON")
703
+ .action(async (tool: string, options: McpOptions) => {
704
+ try {
705
+ await removeMcpHandler(tool, options);
706
+ } catch (err) {
707
+ error(formatError(err));
708
+ process.exit(1);
709
+ }
710
+ });
711
+
712
+ // enact mcp sync
713
+ mcp
714
+ .command("sync")
715
+ .description("Sync MCP registry with globally installed tools")
716
+ .option("--json", "Output as JSON")
717
+ .action(async (options: McpOptions) => {
718
+ try {
719
+ await syncMcpHandler(options);
720
+ } catch (err) {
721
+ error(formatError(err));
722
+ process.exit(1);
723
+ }
724
+ });
725
+
726
+ // enact mcp toolsets
727
+ mcp
728
+ .command("toolsets")
729
+ .description("List available toolsets")
730
+ .option("--json", "Output as JSON")
731
+ .action(async (options: McpOptions) => {
732
+ try {
733
+ await toolsetsHandler(options);
734
+ } catch (err) {
735
+ error(formatError(err));
736
+ process.exit(1);
737
+ }
738
+ });
739
+
740
+ // enact mcp use [toolset]
741
+ mcp
742
+ .command("use [toolset]")
743
+ .description("Switch active toolset (or --all to use all tools)")
744
+ .option("--all", "Use all MCP tools (disable toolset filtering)")
745
+ .option("--json", "Output as JSON")
746
+ .action(async (toolset: string | undefined, options: McpOptions) => {
747
+ try {
748
+ await useHandler(toolset, options);
749
+ } catch (err) {
750
+ error(formatError(err));
751
+ process.exit(1);
752
+ }
753
+ });
754
+
755
+ // enact mcp toolset <action> [args...]
756
+ mcp
757
+ .command("toolset <action> [args...]")
758
+ .description("Manage toolsets (create, delete, add, remove)")
759
+ .option("--json", "Output as JSON")
760
+ .action(async (action: string, args: string[], options: McpOptions) => {
761
+ try {
762
+ await toolsetHandler(action, args, options);
763
+ } catch (err) {
764
+ error(formatError(err));
765
+ process.exit(1);
766
+ }
767
+ });
768
+ }