@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.
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +4 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/init/templates/claude.d.ts +1 -1
- package/dist/commands/init/templates/claude.d.ts.map +1 -1
- package/dist/commands/init/templates/claude.js +268 -28
- package/dist/commands/init/templates/claude.js.map +1 -1
- package/dist/commands/init/templates/tool-agents.d.ts +1 -1
- package/dist/commands/init/templates/tool-agents.d.ts.map +1 -1
- package/dist/commands/init/templates/tool-agents.js +90 -15
- package/dist/commands/init/templates/tool-agents.js.map +1 -1
- package/dist/commands/install/index.d.ts.map +1 -1
- package/dist/commands/install/index.js +9 -1
- package/dist/commands/install/index.js.map +1 -1
- package/dist/commands/learn/index.d.ts.map +1 -1
- package/dist/commands/learn/index.js +4 -11
- package/dist/commands/learn/index.js.map +1 -1
- package/dist/commands/mcp/index.d.ts.map +1 -1
- package/dist/commands/mcp/index.js +204 -53
- package/dist/commands/mcp/index.js.map +1 -1
- package/dist/commands/run/index.d.ts.map +1 -1
- package/dist/commands/run/index.js +380 -39
- package/dist/commands/run/index.js.map +1 -1
- package/dist/commands/validate/index.d.ts +11 -0
- package/dist/commands/validate/index.d.ts.map +1 -0
- package/dist/commands/validate/index.js +299 -0
- package/dist/commands/validate/index.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/errors.d.ts +8 -1
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +13 -2
- package/dist/utils/errors.js.map +1 -1
- package/package.json +5 -5
- package/src/commands/index.ts +5 -0
- package/src/commands/init/templates/claude.ts +268 -28
- package/src/commands/init/templates/tool-agents.ts +90 -15
- package/src/commands/install/index.ts +11 -0
- package/src/commands/learn/index.ts +6 -11
- package/src/commands/mcp/index.ts +768 -0
- package/src/commands/run/README.md +68 -1
- package/src/commands/run/index.ts +475 -35
- package/src/commands/validate/index.ts +344 -0
- package/src/index.ts +8 -1
- package/src/types.ts +2 -0
- package/src/utils/errors.ts +26 -6
- package/tests/commands/init.test.ts +2 -2
- package/tests/commands/run.test.ts +260 -0
- package/tests/commands/validate.test.ts +81 -0
- package/tests/utils/errors.test.ts +36 -0
- 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
|
+
}
|