toolcapsule 0.1.0-alpha.11 → 0.1.0-alpha.13

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/cli.js CHANGED
@@ -1,137 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { readFile as readFile3 } from "fs/promises";
5
- import { join as join6 } from "path";
4
+ import { readFile as readFile5 } from "fs/promises";
6
5
  import { cac } from "cac";
7
- import pc from "picocolors";
6
+ import pc2 from "picocolors";
8
7
 
9
- // src/mcp/client.ts
10
- import { spawn } from "child_process";
11
- import { once } from "events";
12
- import { resolve } from "path";
13
- var McpClient = class {
14
- child;
15
- nextId = 1;
16
- buffer = "";
17
- pending = /* @__PURE__ */ new Map();
18
- timeoutMs;
19
- debug;
20
- clientVersion;
21
- constructor(profile, opts = {}) {
22
- this.timeoutMs = opts.timeoutMs ?? Number(process.env.TOOLCAPSULE_TIMEOUT_MS || "45000");
23
- this.debug = opts.debug ?? process.env.TOOLCAPSULE_DEBUG === "1";
24
- this.clientVersion = opts.clientVersion ?? "0.0.0";
25
- if (profile.transport.type === "remote") {
26
- this.child = spawn("npx", ["-y", "mcp-remote", profile.transport.url, ...headersToArgs(profile.transport.headers)], {
27
- stdio: ["pipe", "pipe", "pipe"],
28
- env: { ...process.env, ...profile.transport.env }
29
- });
30
- } else {
31
- this.child = spawn(profile.transport.command, profile.transport.args ?? [], {
32
- stdio: ["pipe", "pipe", "pipe"],
33
- env: { ...process.env, ...profile.transport.env },
34
- cwd: profile.transport.cwd ? resolve(profile.transport.cwd) : void 0
35
- });
36
- }
37
- this.child.stdout.setEncoding("utf8");
38
- this.child.stdout.on("data", (chunk) => this.onStdout(chunk));
39
- this.child.stderr.on("data", (chunk) => this.onStderr(chunk));
40
- this.child.on("exit", (code, signal) => {
41
- const error = new Error(`MCP process exited early (code=${code}, signal=${signal})`);
42
- for (const waiter of this.pending.values()) waiter.reject(error);
43
- this.pending.clear();
44
- });
45
- }
46
- onStderr(chunk) {
47
- if (!this.debug) return;
48
- process.stderr.write(redactSecrets(chunk.toString("utf8")));
49
- }
50
- onStdout(chunk) {
51
- this.buffer += chunk;
52
- let newlineIndex;
53
- while ((newlineIndex = this.buffer.indexOf("\n")) >= 0) {
54
- const line = this.buffer.slice(0, newlineIndex).trim();
55
- this.buffer = this.buffer.slice(newlineIndex + 1);
56
- if (!line) continue;
57
- let message;
58
- try {
59
- message = JSON.parse(line);
60
- } catch {
61
- process.stderr.write(`${line}
62
- `);
63
- continue;
64
- }
65
- if (typeof message.id === "number") {
66
- const waiter = this.pending.get(message.id);
67
- if (waiter) {
68
- this.pending.delete(message.id);
69
- waiter.resolve(message);
70
- }
71
- }
72
- }
73
- }
74
- async init() {
75
- await this.request("initialize", {
76
- protocolVersion: "2025-03-26",
77
- capabilities: {},
78
- clientInfo: { name: "toolcapsule", version: this.clientVersion }
79
- });
80
- this.notify("notifications/initialized", {});
81
- }
82
- async request(method, params) {
83
- const id = this.nextId++;
84
- const responsePromise = new Promise((resolve3, reject) => {
85
- this.pending.set(id, { resolve: resolve3, reject });
86
- });
87
- this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
88
- `);
89
- const response = await this.withTimeout(responsePromise, method);
90
- if (response.error) throw new Error(`${method} failed: ${JSON.stringify(response.error, null, 2)}`);
91
- return response.result;
92
- }
93
- notify(method, params) {
94
- this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
95
- `);
96
- }
97
- async listTools() {
98
- return await this.request("tools/list", {});
99
- }
100
- async callTool(name, args) {
101
- return await this.request("tools/call", { name, arguments: args });
102
- }
103
- async close() {
104
- if (!this.child.killed) this.child.kill();
105
- if (this.child.exitCode === null) await Promise.race([once(this.child, "exit"), new Promise((resolve3) => setTimeout(resolve3, 500))]);
106
- }
107
- async withTimeout(promise, label) {
108
- let timer;
109
- const timeout = new Promise((_, reject) => {
110
- timer = setTimeout(() => reject(new Error(`${label} timed out after ${this.timeoutMs}ms`)), this.timeoutMs);
111
- });
112
- try {
113
- return await Promise.race([promise, timeout]);
114
- } finally {
115
- if (timer) clearTimeout(timer);
116
- }
117
- }
118
- };
119
- function headersToArgs(headers) {
120
- if (!headers) return [];
121
- return Object.entries(headers).flatMap(([key, value]) => ["--header", `${key}:${value}`]);
122
- }
123
- function redactSecrets(text) {
124
- return text.replace(/https:\/\/mcp\.feishu\.cn\/mcp\/[^\s"']+/g, "https://mcp.feishu.cn/mcp/[redacted]").replace(/(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, "$1[redacted]").replace(/(token=)[^\s&"']+/gi, "$1[redacted]").replace(/(api[_-]?key=)[^\s&"']+/gi, "$1[redacted]");
125
- }
8
+ // src/mcp/inventory.ts
9
+ import { existsSync as existsSync2 } from "fs";
10
+ import { mkdir as mkdir2, readFile as readFile2, readdir, rename, writeFile as writeFile2 } from "fs/promises";
11
+ import { homedir as homedir3 } from "os";
12
+ import { basename, dirname as dirname2, join as join3 } from "path";
13
+ import pc from "picocolors";
126
14
 
127
15
  // src/mcp/importer.ts
128
16
  import { existsSync } from "fs";
129
17
  import { homedir } from "os";
130
- import { join } from "path";
18
+ import { isAbsolute, join, resolve as resolve2 } from "path";
131
19
 
132
20
  // src/utils/fs.ts
133
21
  import { mkdir, readFile, writeFile } from "fs/promises";
134
- import { dirname, resolve as resolve2 } from "path";
22
+ import { dirname, resolve } from "path";
135
23
  async function readJson(path) {
136
24
  return JSON.parse(await readFile(path, "utf8"));
137
25
  }
@@ -160,15 +48,26 @@ async function ensureToolCapsuleIgnored() {
160
48
  // src/mcp/importer.ts
161
49
  var workspaceSources = [
162
50
  { tool: "vscode", path: join(".vscode", "mcp.json") },
51
+ { tool: "generic", path: "mcp.json" },
163
52
  { tool: "claude", path: ".mcp.json" },
164
53
  { tool: "opencode", path: "opencode.json" },
165
54
  { tool: "gemini", path: join(".gemini", "settings.json") },
166
55
  { tool: "cursor", path: join(".cursor", "mcp.json") }
167
56
  ];
168
57
  var userSources = [
58
+ { tool: "vscode", path: join(homedir(), ".config", "Code", "User", "mcp.json"), userLevel: true },
59
+ { tool: "vscode", path: join(homedir(), ".config", "Code - Insiders", "User", "mcp.json"), userLevel: true },
60
+ { tool: "vscode", path: join(homedir(), ".vscode-server", "data", "User", "mcp.json"), userLevel: true },
61
+ { tool: "vscode", path: join(homedir(), ".vscode-server-insiders", "data", "User", "mcp.json"), userLevel: true },
169
62
  { tool: "claude", path: join(homedir(), ".claude.json"), userLevel: true },
170
63
  { tool: "opencode", path: join(homedir(), ".config", "opencode", "opencode.json"), userLevel: true },
171
- { tool: "gemini", path: join(homedir(), ".gemini", "settings.json"), userLevel: true }
64
+ { tool: "gemini", path: join(homedir(), ".gemini", "settings.json"), userLevel: true },
65
+ { tool: "cursor", path: join(homedir(), ".cursor", "mcp.json"), userLevel: true },
66
+ { tool: "cursor", path: join(homedir(), ".config", "Cursor", "User", "mcp.json"), userLevel: true },
67
+ { tool: "cursor", path: join(homedir(), ".config", "Cursor", "User", "settings.json"), userLevel: true }
68
+ ];
69
+ var managedSources = [
70
+ { tool: "claude", path: "/etc/claude-code/managed-mcp.json", managed: true }
172
71
  ];
173
72
  function asRecord(value) {
174
73
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
@@ -218,6 +117,7 @@ function mapServer(name, source, raw) {
218
117
  const profileName = cleanName(name);
219
118
  return {
220
119
  name: profileName,
120
+ originalName: name,
221
121
  source,
222
122
  warnings,
223
123
  profile: {
@@ -226,6 +126,25 @@ function mapServer(name, source, raw) {
226
126
  }
227
127
  };
228
128
  }
129
+ function linkedProfileForImportedServer(server, name = server.name) {
130
+ return {
131
+ name,
132
+ kind: "linked",
133
+ source: {
134
+ ...server.source,
135
+ path: isAbsolute(server.source.path) ? server.source.path : resolve2(server.source.path),
136
+ server: server.originalName
137
+ }
138
+ };
139
+ }
140
+ async function resolveProfileSource(source) {
141
+ if (!existsSync(source.path)) return void 0;
142
+ const config = asRecord(await readJson(source.path));
143
+ if (!config) return void 0;
144
+ const entry = serverEntries(source, config).find(([name]) => name === source.server);
145
+ if (!entry) return void 0;
146
+ return mapServer(entry[0], source, entry[1])?.profile;
147
+ }
229
148
  function serverEntries(source, config) {
230
149
  if (source.tool === "vscode" || source.tool === "cursor") return Object.entries(asRecord(config.servers) ?? {});
231
150
  if (source.tool === "opencode") return Object.entries(asRecord(config.mcp) ?? {});
@@ -239,7 +158,11 @@ function serverEntries(source, config) {
239
158
  return Object.entries(asRecord(config.mcpServers) ?? {});
240
159
  }
241
160
  async function discoverMcpServers(opts = {}) {
242
- const sources = opts.includeUser ? [...workspaceSources, ...userSources] : workspaceSources;
161
+ const sources = [
162
+ ...workspaceSources,
163
+ ...opts.includeUser ? userSources : [],
164
+ ...opts.includeManaged ? managedSources : []
165
+ ];
243
166
  const discovered = [];
244
167
  for (const source of sources) {
245
168
  if (!existsSync(source.path)) continue;
@@ -259,49 +182,345 @@ function selectImportedServers(servers, name, all) {
259
182
  return servers.filter((server) => server.name === normalized || server.name === name);
260
183
  }
261
184
 
262
- // src/profile.ts
263
- import { existsSync as existsSync2 } from "fs";
185
+ // src/paths.ts
186
+ import { homedir as homedir2 } from "os";
264
187
  import { join as join2 } from "path";
188
+ function toolCapsuleHome() {
189
+ return process.env.TOOLCAPSULE_HOME || join2(homedir2(), ".toolcapsule");
190
+ }
191
+ function userProfilePath(profileName) {
192
+ return join2(toolCapsuleHome(), "profiles", `${profileName}.json`);
193
+ }
194
+ function workspaceProfilePath(profileName) {
195
+ return join2(".toolcapsule", "profiles", `${profileName}.json`);
196
+ }
197
+ function workspaceRunBaseDir(profileName) {
198
+ return join2(".toolcapsule", "runs", profileName);
199
+ }
200
+
201
+ // src/mcp/inventory.ts
202
+ function sourceScope(server) {
203
+ if (server.source.managed) return "managed";
204
+ return server.source.userLevel ? "user" : "workspace";
205
+ }
206
+ function sourceStatus(server) {
207
+ return server.source.managed ? "managed" : "enabled";
208
+ }
209
+ async function profileItems(dir, scope) {
210
+ if (!existsSync2(dir)) return [];
211
+ const entries = await readdir(dir, { withFileTypes: true });
212
+ const items = [];
213
+ for (const entry of entries) {
214
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
215
+ const path = join3(dir, entry.name);
216
+ try {
217
+ const profile = await readJson(path);
218
+ items.push({
219
+ name: profile.name || basename(entry.name, ".json"),
220
+ status: "enabled",
221
+ scope,
222
+ source: "toolcapsule",
223
+ mode: profile.kind === "linked" ? "linked" : "snapshot",
224
+ path
225
+ });
226
+ } catch {
227
+ items.push({ name: basename(entry.name, ".json"), status: "error", scope, source: "toolcapsule", mode: "snapshot", path });
228
+ }
229
+ }
230
+ return items;
231
+ }
232
+ async function listMcpInventory(opts = {}) {
233
+ const nativeServers = await discoverMcpServers(opts.includeUser ? { includeUser: true, includeManaged: true } : {});
234
+ const nativeItems = nativeServers.map((server) => ({
235
+ name: server.name,
236
+ originalName: server.originalName,
237
+ status: sourceStatus(server),
238
+ scope: sourceScope(server),
239
+ source: server.source.tool,
240
+ mode: "native",
241
+ path: server.source.path,
242
+ warnings: server.warnings
243
+ }));
244
+ const workspaceProfiles = await profileItems(join3(".toolcapsule", "profiles"), "workspace");
245
+ const userProfiles = await profileItems(join3(toolCapsuleHome(), "profiles"), "user");
246
+ return [...nativeItems, ...workspaceProfiles, ...userProfiles];
247
+ }
248
+ function colorStatus(status) {
249
+ if (status === "enabled") return pc.green("\u25CF on");
250
+ if (status === "disabled") return pc.gray("\u25CB off");
251
+ if (status === "managed") return pc.yellow("\u25C6 managed");
252
+ return pc.red("\xD7 error");
253
+ }
254
+ function colorMode(mode) {
255
+ if (mode === "linked") return pc.cyan(mode);
256
+ if (mode === "snapshot") return pc.magenta(mode);
257
+ return pc.gray(mode);
258
+ }
259
+ function pad(value, width) {
260
+ return value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
261
+ }
262
+ function padDisplay(display, raw, width) {
263
+ return raw.length >= width ? display : `${display}${" ".repeat(width - raw.length)}`;
264
+ }
265
+ function formatInventory(items) {
266
+ if (items.length === 0) return "No MCP servers found.";
267
+ const rows = items.map((item) => ({
268
+ status: colorStatus(item.status),
269
+ statusRaw: item.status === "enabled" ? "\u25CF on" : item.status === "disabled" ? "\u25CB off" : item.status === "managed" ? "\u25C6 managed" : "\xD7 error",
270
+ name: item.originalName && item.originalName !== item.name ? `${item.name} (${item.originalName})` : item.name,
271
+ scope: item.scope,
272
+ source: item.source,
273
+ mode: colorMode(item.mode),
274
+ modeRaw: item.mode,
275
+ path: item.path.replace(`${homedir3()}/`, "~/")
276
+ }));
277
+ const widths = {
278
+ status: Math.max("STATUS".length, ...rows.map((row) => row.statusRaw.length)),
279
+ name: Math.max("NAME".length, ...rows.map((row) => row.name.length)),
280
+ scope: Math.max("SCOPE".length, ...rows.map((row) => row.scope.length)),
281
+ source: Math.max("SOURCE".length, ...rows.map((row) => row.source.length)),
282
+ mode: Math.max("MODE".length, ...rows.map((row) => row.modeRaw.length))
283
+ };
284
+ const lines = [
285
+ `${pad("STATUS", widths.status)} ${pad("NAME", widths.name)} ${pad("SCOPE", widths.scope)} ${pad("SOURCE", widths.source)} ${pad("MODE", widths.mode)} PATH`,
286
+ ...rows.map(
287
+ (row) => `${padDisplay(row.status, row.statusRaw, widths.status)} ${pad(row.name, widths.name)} ${pad(row.scope, widths.scope)} ${pad(row.source, widths.source)} ${padDisplay(row.mode, row.modeRaw, widths.mode)} ${row.path}`
288
+ )
289
+ ];
290
+ return lines.join("\n");
291
+ }
292
+ function disableKeys(tool) {
293
+ if (tool === "vscode" || tool === "cursor") return { fromKey: "servers", toKey: "disabledServers" };
294
+ if (tool === "opencode") return { fromKey: "mcp", toKey: "disabledMcp" };
295
+ return { fromKey: "mcpServers", toKey: "disabledMcpServers" };
296
+ }
297
+ async function disableNativeMcp(serverName, opts = {}) {
298
+ const servers = await discoverMcpServers(opts.includeUser ? { includeUser: true } : {});
299
+ const server = servers.find((item) => item.name === serverName || item.originalName === serverName);
300
+ if (!server) throw new Error(`Native MCP server not found: ${serverName}`);
301
+ if (server.source.managed) throw new Error(`Cannot modify managed MCP config: ${server.source.path}`);
302
+ const keys = disableKeys(server.source.tool);
303
+ const plan = { path: server.source.path, tool: server.source.tool, server: server.originalName, ...keys };
304
+ const message = `Disable native MCP ${plan.server} in ${plan.path}: move ${plan.fromKey}.${plan.server} -> ${plan.toKey}.${plan.server}`;
305
+ if (opts.dryRun !== false) return `${message}
306
+ Dry run only. Re-run with --yes to write changes.`;
307
+ const config = await readJson(plan.path);
308
+ const source = config[plan.fromKey] && typeof config[plan.fromKey] === "object" && !Array.isArray(config[plan.fromKey]) ? config[plan.fromKey] : {};
309
+ if (!(plan.server in source)) throw new Error(`Server ${plan.server} not found under ${plan.fromKey} in ${plan.path}`);
310
+ const disabled = config[plan.toKey] && typeof config[plan.toKey] === "object" && !Array.isArray(config[plan.toKey]) ? config[plan.toKey] : {};
311
+ disabled[plan.server] = source[plan.server];
312
+ delete source[plan.server];
313
+ config[plan.fromKey] = source;
314
+ config[plan.toKey] = disabled;
315
+ const backup = `${plan.path}.toolcapsule.bak`;
316
+ await mkdir2(dirname2(backup), { recursive: true }).catch(() => void 0);
317
+ if (existsSync2(plan.path)) await writeFile2(backup, await readFile2(plan.path, "utf8"));
318
+ await writeJson(plan.path, config);
319
+ return `${message}
320
+ Backup: ${backup}`;
321
+ }
322
+ async function disableToolCapsuleProfile(profileName) {
323
+ const candidates = [workspaceProfilePath(profileName), join3(toolCapsuleHome(), "profiles", `${profileName}.json`)];
324
+ const found = candidates.find((path) => existsSync2(path));
325
+ if (!found) throw new Error(`ToolCapsule profile not found: ${profileName}`);
326
+ const disabledPath = `${found}.disabled`;
327
+ await rename(found, disabledPath);
328
+ return `Disabled ToolCapsule profile ${profileName}: ${found} -> ${disabledPath}`;
329
+ }
330
+
331
+ // src/mcp/client.ts
332
+ import { spawn } from "child_process";
333
+ import { once } from "events";
334
+ import { resolve as resolve3 } from "path";
335
+ var McpClient = class {
336
+ child;
337
+ nextId = 1;
338
+ buffer = "";
339
+ pending = /* @__PURE__ */ new Map();
340
+ timeoutMs;
341
+ debug;
342
+ clientVersion;
343
+ constructor(profile, opts = {}) {
344
+ this.timeoutMs = opts.timeoutMs ?? Number(process.env.TOOLCAPSULE_TIMEOUT_MS || "45000");
345
+ this.debug = opts.debug ?? process.env.TOOLCAPSULE_DEBUG === "1";
346
+ this.clientVersion = opts.clientVersion ?? "0.0.0";
347
+ if (profile.transport.type === "remote") {
348
+ this.child = spawn("npx", ["-y", "mcp-remote", profile.transport.url, ...headersToArgs(profile.transport.headers)], {
349
+ stdio: ["pipe", "pipe", "pipe"],
350
+ env: { ...process.env, ...profile.transport.env }
351
+ });
352
+ } else {
353
+ this.child = spawn(profile.transport.command, profile.transport.args ?? [], {
354
+ stdio: ["pipe", "pipe", "pipe"],
355
+ env: { ...process.env, ...profile.transport.env },
356
+ cwd: profile.transport.cwd ? resolve3(profile.transport.cwd) : void 0
357
+ });
358
+ }
359
+ this.child.stdout.setEncoding("utf8");
360
+ this.child.stdout.on("data", (chunk) => this.onStdout(chunk));
361
+ this.child.stderr.on("data", (chunk) => this.onStderr(chunk));
362
+ this.child.on("exit", (code, signal) => {
363
+ const error = new Error(`MCP process exited early (code=${code}, signal=${signal})`);
364
+ for (const waiter of this.pending.values()) waiter.reject(error);
365
+ this.pending.clear();
366
+ });
367
+ }
368
+ onStderr(chunk) {
369
+ if (!this.debug) return;
370
+ process.stderr.write(redactSecrets(chunk.toString("utf8")));
371
+ }
372
+ onStdout(chunk) {
373
+ this.buffer += chunk;
374
+ let newlineIndex;
375
+ while ((newlineIndex = this.buffer.indexOf("\n")) >= 0) {
376
+ const line = this.buffer.slice(0, newlineIndex).trim();
377
+ this.buffer = this.buffer.slice(newlineIndex + 1);
378
+ if (!line) continue;
379
+ let message;
380
+ try {
381
+ message = JSON.parse(line);
382
+ } catch {
383
+ process.stderr.write(`${line}
384
+ `);
385
+ continue;
386
+ }
387
+ if (typeof message.id === "number") {
388
+ const waiter = this.pending.get(message.id);
389
+ if (waiter) {
390
+ this.pending.delete(message.id);
391
+ waiter.resolve(message);
392
+ }
393
+ }
394
+ }
395
+ }
396
+ async init() {
397
+ await this.request("initialize", {
398
+ protocolVersion: "2025-03-26",
399
+ capabilities: {},
400
+ clientInfo: { name: "toolcapsule", version: this.clientVersion }
401
+ });
402
+ this.notify("notifications/initialized", {});
403
+ }
404
+ async request(method, params) {
405
+ const id = this.nextId++;
406
+ const responsePromise = new Promise((resolve4, reject) => {
407
+ this.pending.set(id, { resolve: resolve4, reject });
408
+ });
409
+ this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
410
+ `);
411
+ const response = await this.withTimeout(responsePromise, method);
412
+ if (response.error) throw new Error(`${method} failed: ${JSON.stringify(response.error, null, 2)}`);
413
+ return response.result;
414
+ }
415
+ notify(method, params) {
416
+ this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
417
+ `);
418
+ }
419
+ async listTools() {
420
+ return await this.request("tools/list", {});
421
+ }
422
+ async callTool(name, args) {
423
+ return await this.request("tools/call", { name, arguments: args });
424
+ }
425
+ async close() {
426
+ if (!this.child.killed) this.child.kill();
427
+ if (this.child.exitCode === null) await Promise.race([once(this.child, "exit"), new Promise((resolve4) => setTimeout(resolve4, 500))]);
428
+ }
429
+ async withTimeout(promise, label) {
430
+ let timer;
431
+ const timeout = new Promise((_, reject) => {
432
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${this.timeoutMs}ms`)), this.timeoutMs);
433
+ });
434
+ try {
435
+ return await Promise.race([promise, timeout]);
436
+ } finally {
437
+ if (timer) clearTimeout(timer);
438
+ }
439
+ }
440
+ };
441
+ function headersToArgs(headers) {
442
+ if (!headers) return [];
443
+ return Object.entries(headers).flatMap(([key, value]) => ["--header", `${key}:${value}`]);
444
+ }
445
+ function redactSecrets(text) {
446
+ return text.replace(/https:\/\/mcp\.feishu\.cn\/mcp\/[^\s"']+/g, "https://mcp.feishu.cn/mcp/[redacted]").replace(/(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, "$1[redacted]").replace(/(token=)[^\s&"']+/gi, "$1[redacted]").replace(/(api[_-]?key=)[^\s&"']+/gi, "$1[redacted]");
447
+ }
448
+
449
+ // src/profile.ts
450
+ import { existsSync as existsSync3 } from "fs";
451
+ import { join as join4 } from "path";
265
452
  import { z } from "zod";
266
- var profileSchema = z.object({
453
+ var transportSchema = z.union([
454
+ z.object({
455
+ type: z.literal("remote"),
456
+ url: z.string().url(),
457
+ headers: z.record(z.string()).optional(),
458
+ env: z.record(z.string()).optional()
459
+ }),
460
+ z.object({
461
+ type: z.literal("stdio"),
462
+ command: z.string().min(1),
463
+ args: z.array(z.string()).optional(),
464
+ env: z.record(z.string()).optional(),
465
+ cwd: z.string().optional()
466
+ })
467
+ ]);
468
+ var skillSchema = z.object({
469
+ name: z.string().optional(),
470
+ description: z.string().optional()
471
+ }).optional();
472
+ var shortcutsSchema = z.record(
473
+ z.object({
474
+ tool: z.string(),
475
+ description: z.string().optional(),
476
+ args: z.record(z.enum(["string", "file", "json", "boolean", "number"])).optional()
477
+ })
478
+ ).optional();
479
+ var snapshotProfileSchema = z.object({
267
480
  name: z.string().min(1),
268
- transport: z.union([
269
- z.object({
270
- type: z.literal("remote"),
271
- url: z.string().url(),
272
- headers: z.record(z.string()).optional(),
273
- env: z.record(z.string()).optional()
274
- }),
275
- z.object({
276
- type: z.literal("stdio"),
277
- command: z.string().min(1),
278
- args: z.array(z.string()).optional(),
279
- env: z.record(z.string()).optional(),
280
- cwd: z.string().optional()
281
- })
282
- ]),
283
- skill: z.object({
284
- name: z.string().optional(),
285
- description: z.string().optional()
286
- }).optional(),
287
- shortcuts: z.record(
288
- z.object({
289
- tool: z.string(),
290
- description: z.string().optional(),
291
- args: z.record(z.enum(["string", "file", "json", "boolean", "number"])).optional()
292
- })
293
- ).optional()
481
+ kind: z.literal("snapshot").optional(),
482
+ transport: transportSchema,
483
+ skill: skillSchema,
484
+ shortcuts: shortcutsSchema
294
485
  });
486
+ var linkedProfileSchema = z.object({
487
+ name: z.string().min(1),
488
+ kind: z.literal("linked"),
489
+ source: z.object({
490
+ tool: z.enum(["vscode", "claude", "opencode", "gemini", "cursor", "generic"]),
491
+ path: z.string().min(1),
492
+ server: z.string().min(1),
493
+ userLevel: z.boolean().optional()
494
+ }),
495
+ skill: skillSchema,
496
+ shortcuts: shortcutsSchema
497
+ });
498
+ var profileSchema = z.union([snapshotProfileSchema, linkedProfileSchema]);
295
499
  async function loadProfile(profilePathOrName) {
296
500
  const candidates = [
297
501
  profilePathOrName,
298
502
  `${profilePathOrName}.json`,
299
- join2(".toolcapsule", "profiles", `${profilePathOrName}.json`),
300
- join2(".github", "skills", `${profilePathOrName}-mcp`, "toolcapsule.config.json")
503
+ workspaceProfilePath(profilePathOrName),
504
+ userProfilePath(profilePathOrName),
505
+ join4(".github", "skills", `${profilePathOrName}-mcp`, "toolcapsule.config.json"),
506
+ join4(".claude", "skills", `${profilePathOrName}-mcp`, "toolcapsule.config.json"),
507
+ join4(".opencode", "skills", `${profilePathOrName}-mcp`, "toolcapsule.config.json"),
508
+ join4(".agents", "skills", `${profilePathOrName}-mcp`, "toolcapsule.config.json")
301
509
  ];
302
- const found = candidates.find((path) => existsSync2(path));
510
+ const found = candidates.find((path) => existsSync3(path));
303
511
  if (!found) throw new Error(`Profile not found: ${profilePathOrName}`);
304
- return profileSchema.parse(await readJson(found));
512
+ const profile = profileSchema.parse(await readJson(found));
513
+ if (profile.kind !== "linked") return profile;
514
+ const resolved = await resolveProfileSource(profile.source);
515
+ if (!resolved) {
516
+ throw new Error(`Linked profile source not found: ${profile.source.path}#${profile.source.server}`);
517
+ }
518
+ return snapshotProfileSchema.parse({
519
+ ...resolved,
520
+ name: profile.name,
521
+ skill: profile.skill ?? resolved.skill,
522
+ shortcuts: profile.shortcuts ?? resolved.shortcuts
523
+ });
305
524
  }
306
525
 
307
526
  // src/schema/brief.ts
@@ -345,15 +564,15 @@ function summarizeTools(tools) {
345
564
  }
346
565
 
347
566
  // src/skill/generator.ts
348
- import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
349
- import { dirname as dirname2, join as join3 } from "path";
567
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
568
+ import { dirname as dirname3, join as join5 } from "path";
350
569
  import Handlebars from "handlebars";
351
570
  var defaultSkillTarget = "claude";
352
571
  function skillOutputDir(skillName, target) {
353
- if (target === "copilot") return join3(".github", "skills", skillName);
354
- if (target === "claude") return join3(".claude", "skills", skillName);
355
- if (target === "opencode") return join3(".opencode", "skills", skillName);
356
- return join3(".agents", "skills", skillName);
572
+ if (target === "copilot") return join5(".github", "skills", skillName);
573
+ if (target === "claude") return join5(".claude", "skills", skillName);
574
+ if (target === "opencode") return join5(".opencode", "skills", skillName);
575
+ return join5(".agents", "skills", skillName);
357
576
  }
358
577
  function expandSkillTargets(target) {
359
578
  return target === "all" ? ["copilot", "claude", "opencode", "agents"] : [target];
@@ -384,6 +603,18 @@ toolcapsule call {{profileName}} <tool> @args.json
384
603
  toolcapsule retry .toolcapsule/runs/{{profileName}}/<run-id>
385
604
  \`\`\`
386
605
 
606
+ {{#if toolsMarkdown}}
607
+ ## Tool summary
608
+
609
+ Use this summary for planning. Only run \`toolcapsule schema {{profileName}} <tool>\` when these brief details are insufficient.
610
+
611
+ {{{toolsMarkdown}}}
612
+ {{else}}
613
+ ## Tool discovery
614
+
615
+ Run \`toolcapsule tools {{profileName}} --brief\` once before choosing a tool. Then run \`toolcapsule schema {{profileName}} <tool>\` only when the brief summary is insufficient.
616
+ {{/if}}
617
+
387
618
  ## Workflow
388
619
 
389
620
  1. Use \`tools --brief\` to find the likely tool.
@@ -399,24 +630,48 @@ toolcapsule retry .toolcapsule/runs/{{profileName}}/<run-id>
399
630
  - Do not print secrets.
400
631
  - Review destructive tools before calling.
401
632
  `;
402
- async function generateSkillAt(profile, outputDir) {
633
+ function toolsMarkdown(tools) {
634
+ if (!tools || tools.length === 0) return void 0;
635
+ const summaries = summarizeTools(tools);
636
+ const rows = summaries.slice(0, 40).map((tool) => {
637
+ const required = Array.isArray(tool.required) && tool.required.length > 0 ? tool.required.join(", ") : "-";
638
+ const risk = tool.annotations?.destructiveHint ? "writes" : tool.annotations?.readOnlyHint ? "read" : "unknown";
639
+ const description = (tool.description || "-").replace(/\|/g, "\\|").slice(0, 120);
640
+ return `| \`${tool.name || "unknown"}\` | ${description} | ${required} | ${risk} |`;
641
+ });
642
+ return ["| Tool | Purpose | Required args | Risk |", "|---|---|---|---|", ...rows].join("\n");
643
+ }
644
+ async function fetchProfileTools(profile, opts = {}) {
645
+ if (profile.kind === "linked") return void 0;
646
+ const client = new McpClient(profile, { ...opts.clientVersion ? { clientVersion: opts.clientVersion } : {}, timeoutMs: 15e3 });
647
+ try {
648
+ await client.init();
649
+ return (await client.listTools()).tools;
650
+ } catch {
651
+ return void 0;
652
+ } finally {
653
+ await client.close().catch(() => void 0);
654
+ }
655
+ }
656
+ async function generateSkillAt(profile, outputDir, opts = {}) {
403
657
  const skillName = profile.skill?.name || `${profile.name}-mcp`;
404
- await mkdir2(join3(outputDir, "scripts"), { recursive: true });
658
+ await mkdir3(join5(outputDir, "scripts"), { recursive: true });
405
659
  const template = Handlebars.compile(skillTemplate);
406
660
  const description = profile.skill?.description || `Use when operating tools from the ${profile.name} MCP server. Lazy-load schemas, call tools through local files, and retry failed calls by patching artifacts.`;
407
661
  const markdown = template({
408
662
  skillName,
409
663
  profileName: profile.name,
410
664
  title: `${profile.name} MCP Skill`,
411
- description: description.replace(/'/g, "''")
665
+ description: description.replace(/'/g, "''"),
666
+ toolsMarkdown: toolsMarkdown(opts.tools)
412
667
  });
413
- await writeFile2(join3(outputDir, "SKILL.md"), markdown);
414
- await writeJson(join3(outputDir, "toolcapsule.config.json"), profile);
415
- await writeFile2(
416
- join3(outputDir, "scripts", "README.md"),
668
+ await writeFile3(join5(outputDir, "SKILL.md"), markdown);
669
+ if (opts.embedProfile) await writeJson(join5(outputDir, "toolcapsule.config.json"), profile);
670
+ await writeFile3(
671
+ join5(outputDir, "scripts", "README.md"),
417
672
  `# Scripts
418
673
 
419
- This skill uses the project-level \`toolcapsule\` CLI.
674
+ This skill uses the \`toolcapsule\` CLI and profiles resolved by name.
420
675
  `
421
676
  );
422
677
  return outputDir;
@@ -424,122 +679,63 @@ This skill uses the project-level \`toolcapsule\` CLI.
424
679
  async function generateSkill(profile, opts = {}) {
425
680
  const skillName = profile.skill?.name || `${profile.name}-mcp`;
426
681
  const target = opts.target || defaultSkillTarget;
427
- const outputs = opts.outputDir ? [await generateSkillAt(profile, opts.outputDir)] : await Promise.all(expandSkillTargets(target).map((item) => generateSkillAt(profile, skillOutputDir(skillName, item))));
682
+ const embedProfile = opts.embedProfile === true;
683
+ const atOptions = { embedProfile, ...opts.tools ? { tools: opts.tools } : {} };
684
+ const outputs = opts.outputDir ? [await generateSkillAt(profile, opts.outputDir, atOptions)] : await Promise.all(
685
+ expandSkillTargets(target).map(
686
+ (item) => generateSkillAt(profile, skillOutputDir(skillName, item), atOptions)
687
+ )
688
+ );
428
689
  return outputs.join(", ");
429
690
  }
430
691
  async function writeProfile(path, profile) {
431
- await mkdir2(dirname2(path), { recursive: true });
692
+ await mkdir3(dirname3(path), { recursive: true });
432
693
  await writeJson(path, profile);
433
694
  }
434
695
 
435
696
  // src/skill/installer.ts
436
- import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
437
- import { join as join4 } from "path";
438
- var agentSkill = `---
439
- name: toolcapsule
440
- description: 'Use when: converting MCP servers into lightweight Agent Skills, installing ToolCapsule, lazy-loading MCP schemas, calling MCP tools through local args files, or using patch-and-retry workflows for heavy MCP tools.'
441
- argument-hint: 'MCP server URL/command, tool name, args file, or retry task'
442
- ---
443
-
444
- # ToolCapsule Agent Skill
445
-
446
- Use ToolCapsule when an agent needs to work with heavy MCP servers without carrying every tool schema in the prompt.
447
-
448
- ## Install
449
-
450
- If \`toolcapsule\` or \`tcap\` is missing:
451
-
452
- \`\`\`bash
453
- npm install -g toolcapsule
454
- \`\`\`
455
-
456
- ## Core workflow
457
-
458
- 1. Initialize a profile and generated Skill:
459
-
460
- \`\`\`bash
461
- tcap init <name> --url <remote-mcp-url>
462
- # or
463
- tcap init <name> --command <stdio-command> --arg <arg>
464
- \`\`\`
465
-
466
- 2. Discover tools briefly:
467
-
468
- \`\`\`bash
469
- tcap tools <name> --brief
470
- \`\`\`
471
-
472
- 3. Inspect one tool only when needed:
473
-
474
- \`\`\`bash
475
- tcap schema <name> <tool>
476
- \`\`\`
477
-
478
- 4. Put complex arguments in a local JSON file.
479
-
480
- 5. Call the MCP tool through the local args file:
481
-
482
- \`\`\`bash
483
- tcap call <name> <tool> @args.json --save-run
484
- \`\`\`
485
-
486
- 6. If the call fails, patch the local file and retry:
487
-
488
- \`\`\`bash
489
- tcap retry .toolcapsule/runs/<name>/<run-id>
490
- \`\`\`
491
-
492
- ## Safety
493
-
494
- - Do not print or commit private MCP URLs, tokens, API keys, user IDs, or document IDs.
495
- - Keep generated profiles and run artifacts local unless reviewed.
496
- - Use \`TOOLCAPSULE_DEBUG=1\` only when debugging; normal transport logs are quiet by default.
497
- - Prefer \`--brief\` and \`schema\` before reading full MCP schemas.
498
-
499
- ## When to use
500
-
501
- Use ToolCapsule for MCP servers with:
502
-
503
- - many tools;
504
- - long schemas;
505
- - large Markdown/JSON payloads;
506
- - document, ticket, wiki, dashboard, or batch workflows;
507
- - failures that benefit from patching local artifacts instead of regenerating a full tool call.
508
- `;
697
+ import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
698
+ import { join as join6 } from "path";
509
699
  async function installAgentSkill(outputDir, target = defaultSkillTarget) {
700
+ let agentSkill;
701
+ try {
702
+ agentSkill = await readFile3(new URL("../skills/toolcapsule/SKILL.md", import.meta.url), "utf8");
703
+ } catch {
704
+ agentSkill = await readFile3(new URL("../../skills/toolcapsule/SKILL.md", import.meta.url), "utf8");
705
+ }
510
706
  const outputDirs = outputDir ? [outputDir] : expandSkillTargets(target).map((item) => skillOutputDir("toolcapsule", item));
511
707
  await Promise.all(
512
708
  outputDirs.map(async (dir) => {
513
- await mkdir3(dir, { recursive: true });
514
- await writeFile3(join4(dir, "SKILL.md"), agentSkill);
709
+ await mkdir4(dir, { recursive: true });
710
+ await writeFile4(join6(dir, "SKILL.md"), agentSkill);
515
711
  })
516
712
  );
517
713
  return outputDirs.join(", ");
518
714
  }
519
715
 
520
716
  // src/runs/recorder.ts
521
- import { mkdir as mkdir4, readFile as readFile2, writeFile as writeFile4 } from "fs/promises";
522
- import { join as join5 } from "path";
717
+ import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
718
+ import { join as join7 } from "path";
523
719
  function createRunId() {
524
720
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
525
721
  }
526
722
  async function saveRun(baseDir, record) {
527
- const dir = join5(baseDir, record.id);
528
- await mkdir4(dir, { recursive: true });
529
- await writeJson(join5(dir, "run.json"), record);
530
- await writeJson(join5(dir, "request.json"), record.request);
531
- if (record.response !== void 0) await writeJson(join5(dir, "response.json"), record.response);
532
- if (record.error) await writeFile4(join5(dir, "error.txt"), record.error);
533
- await writeFile4(join5(dir, "command.txt"), `${record.command}
723
+ const dir = join7(baseDir, record.id);
724
+ await mkdir5(dir, { recursive: true });
725
+ await writeJson(join7(dir, "run.json"), record);
726
+ await writeJson(join7(dir, "request.json"), record.request);
727
+ if (record.response !== void 0) await writeJson(join7(dir, "response.json"), record.response);
728
+ if (record.error) await writeFile5(join7(dir, "error.txt"), record.error);
729
+ await writeFile5(join7(dir, "command.txt"), `${record.command}
534
730
  `);
535
731
  return dir;
536
732
  }
537
733
  async function loadRun(runDir) {
538
- return JSON.parse(await readFile2(join5(runDir, "run.json"), "utf8"));
734
+ return JSON.parse(await readFile4(join7(runDir, "run.json"), "utf8"));
539
735
  }
540
736
 
541
737
  // src/cli.ts
542
- import { writeFile as writeFile5 } from "fs/promises";
738
+ import { writeFile as writeFile6 } from "fs/promises";
543
739
 
544
740
  // src/utils/tokens.ts
545
741
  function roughTokens(value) {
@@ -554,7 +750,7 @@ function percentReduction(before, after) {
554
750
  var cli = cac("toolcapsule");
555
751
  async function readPackageVersion() {
556
752
  try {
557
- const pkg = JSON.parse(await readFile3(new URL("../package.json", import.meta.url), "utf8"));
753
+ const pkg = JSON.parse(await readFile5(new URL("../package.json", import.meta.url), "utf8"));
558
754
  return pkg.version || "0.0.0";
559
755
  } catch {
560
756
  return "0.0.0";
@@ -578,23 +774,47 @@ function readSkillTarget(raw) {
578
774
  if (["copilot", "claude", "opencode", "agents", "all"].includes(target)) return target;
579
775
  throw new Error("Invalid --target. Use one of: copilot, claude, opencode, agents, all");
580
776
  }
581
- function runBaseDir(profileName) {
582
- return join6(".toolcapsule", "runs", profileName);
583
- }
584
- cli.command("init <name>", "Create a profile and generated Agent Skill for an MCP server").option("--url <url>", "Remote MCP URL").option("--command <command>", "stdio MCP command").option("--arg <arg>", "stdio MCP argument, repeatable", { type: [String] }).option("--output <dir>", "Skill output directory").option("--target <target>", "Skill target: copilot, claude, opencode, agents, all", { default: defaultSkillTarget }).action(async (name, options) => {
777
+ function renameSnapshotProfile(profile, name) {
778
+ return { ...profile, name };
779
+ }
780
+ async function writeImportedServer(server, opts) {
781
+ const profileName = opts.as || server.name;
782
+ const local = opts.local === true;
783
+ const copy = opts.copy === true;
784
+ const profilePath = local ? workspaceProfilePath(profileName) : userProfilePath(profileName);
785
+ if (local) await ensureToolCapsuleIgnored();
786
+ const profile = copy ? renameSnapshotProfile(server.profile, profileName) : linkedProfileForImportedServer(server, profileName);
787
+ await writeProfile(profilePath, profile);
788
+ const tools = await fetchProfileTools(renameSnapshotProfile(server.profile, profileName), { clientVersion: packageVersion });
789
+ const skillOptions = { target: readSkillTarget(opts.target), embedProfile: local, ...tools ? { tools } : {} };
790
+ const out = await generateSkill(profile, skillOptions);
791
+ const mode = copy ? "snapshot" : "linked";
792
+ console.log(pc2.green(`Imported ${server.name} as ${profileName} from ${server.source.path} -> ${profilePath} (${mode}), ${out}`));
793
+ for (const warning of server.warnings) console.log(pc2.yellow(` warning: ${warning}`));
794
+ }
795
+ cli.command("init <name>", "Create a profile and generated Agent Skill for an MCP server").option("--url <url>", "Remote MCP URL").option("--command <command>", "stdio MCP command").option("--arg <arg>", "stdio MCP argument, repeatable", { type: [String] }).option("--output <dir>", "Skill output directory").option("--target <target>", "Skill target: copilot, claude, opencode, agents, all", { default: defaultSkillTarget }).option("--local", "Store the MCP profile in this workspace instead of ~/.toolcapsule").action(async (name, options) => {
585
796
  if (!options.url && !options.command) throw new Error("Provide --url for remote MCP or --command for stdio MCP");
797
+ const local = options.local === true;
586
798
  const profile = options.url ? { name, transport: { type: "remote", url: options.url } } : { name, transport: { type: "stdio", command: options.command, args: options.arg ?? [] } };
587
- await ensureToolCapsuleIgnored();
588
- await writeProfile(join6(".toolcapsule", "profiles", `${name}.json`), profile);
589
- const out = await generateSkill(profile, options.output ? { outputDir: options.output } : { target: readSkillTarget(options.target) });
590
- console.log(pc.green(`Created profile and skill at ${out}`));
799
+ const profilePath = local ? workspaceProfilePath(name) : userProfilePath(name);
800
+ if (local) await ensureToolCapsuleIgnored();
801
+ await writeProfile(profilePath, profile);
802
+ const tools = await fetchProfileTools(profile, { clientVersion: packageVersion });
803
+ const skillOptions = options.output ? { outputDir: options.output, embedProfile: local, ...tools ? { tools } : {} } : { target: readSkillTarget(options.target), embedProfile: local, ...tools ? { tools } : {} };
804
+ const out = await generateSkill(
805
+ profile,
806
+ skillOptions
807
+ );
808
+ console.log(pc2.green(`Created profile at ${profilePath} and skill at ${out}`));
591
809
  });
592
810
  cli.command("install-skill", "Install the generic ToolCapsule Agent Skill into this workspace").option("--output <dir>", "Skill output directory").option("--target <target>", "Skill target: copilot, claude, opencode, agents, all", { default: defaultSkillTarget }).action(async (options) => {
593
811
  const out = await installAgentSkill(options.output, readSkillTarget(options.target));
594
- console.log(pc.green(`Installed ToolCapsule Agent Skill at ${out}`));
812
+ console.log(pc2.green(`Installed ToolCapsule Agent Skill at ${out}`));
595
813
  });
596
- cli.command("import", "Import existing MCP configuration into ToolCapsule profiles and skills").option("--include-user", "Also inspect user-level MCP config files").option("--name <name>", "Import only one MCP server by name").option("--all", "Import all discovered MCP servers").option("--target <target>", "Skill target: copilot, claude, opencode, agents, all", { default: defaultSkillTarget }).option("--dry-run", "List importable MCP servers without writing files").action(
814
+ cli.command("import", "Import existing MCP configuration into ToolCapsule profiles and skills").option("--include-user", "Also inspect user-level MCP config files").option("--name <name>", "Import only one MCP server by name").option("--as <name>", "Store the imported server under a different ToolCapsule profile name").option("--all", "Import all discovered MCP servers").option("--target <target>", "Skill target: copilot, claude, opencode, agents, all", { default: defaultSkillTarget }).option("--local", "Store imported MCP profiles in this workspace instead of ~/.toolcapsule").option("--copy", "Copy MCP transport details into a ToolCapsule snapshot instead of linking the source config").option("--dry-run", "List importable MCP servers without writing files").action(
597
815
  async (options) => {
816
+ const local = options.local === true;
817
+ const copy = options.copy === true;
598
818
  const discovered = await discoverMcpServers(options.includeUser ? { includeUser: true } : {});
599
819
  if (discovered.length === 0) {
600
820
  console.log("No importable MCP servers found.");
@@ -603,7 +823,7 @@ cli.command("import", "Import existing MCP configuration into ToolCapsule profil
603
823
  if (options.dryRun) {
604
824
  for (const server of discovered) {
605
825
  console.log(`${server.name} ${server.source.tool} ${server.source.path}`);
606
- for (const warning of server.warnings) console.log(pc.yellow(` warning: ${warning}`));
826
+ for (const warning of server.warnings) console.log(pc2.yellow(` warning: ${warning}`));
607
827
  }
608
828
  return;
609
829
  }
@@ -612,14 +832,32 @@ cli.command("import", "Import existing MCP configuration into ToolCapsule profil
612
832
  throw new Error("Multiple MCP servers found. Re-run with --dry-run, then pass --name <server> or --all.");
613
833
  }
614
834
  for (const server of selected) {
615
- await ensureToolCapsuleIgnored();
616
- await writeProfile(join6(".toolcapsule", "profiles", `${server.profile.name}.json`), server.profile);
617
- const out = await generateSkill(server.profile, { target: readSkillTarget(options.target) });
618
- console.log(pc.green(`Imported ${server.name} from ${server.source.path} -> ${out}`));
619
- for (const warning of server.warnings) console.log(pc.yellow(` warning: ${warning}`));
835
+ await writeImportedServer(server, { as: options.as, local, copy, target: options.target });
620
836
  }
621
837
  }
622
838
  );
839
+ cli.command("mcp <action> [name]", "List, enable, or disable MCP servers").option("--include-user", "Also inspect user-level MCP config files").option("--json", "Print JSON output for list").option("--as <name>", "Store an enabled server under a different ToolCapsule profile name").option("--target <target>", "Skill target: copilot, claude, opencode, agents, all", { default: defaultSkillTarget }).option("--local", "Store ToolCapsule profile in this workspace").option("--copy", "Copy MCP transport details into a ToolCapsule snapshot instead of linking source config").option("--native", "For disable: disable the native MCP config instead of the ToolCapsule profile").option("--yes", "Apply native disable changes; otherwise native disable is dry-run").action(async (action, name, options) => {
840
+ if (action === "list") {
841
+ const items = await listMcpInventory(options.includeUser ? { includeUser: true } : {});
842
+ console.log(options.json ? JSON.stringify(items, null, 2) : formatInventory(items));
843
+ return;
844
+ }
845
+ if (action === "enable") {
846
+ if (!name) throw new Error("Usage: tcap mcp enable <server> [--as <profile>]");
847
+ const discovered = await discoverMcpServers(options.includeUser ? { includeUser: true } : {});
848
+ const selected = selectImportedServers(discovered, name, false);
849
+ if (selected.length !== 1) throw new Error(`Expected one MCP server named ${name}; run tcap mcp list --include-user first.`);
850
+ await writeImportedServer(selected[0], { as: options.as, local: options.local, copy: options.copy, target: options.target });
851
+ return;
852
+ }
853
+ if (action === "disable") {
854
+ if (!name) throw new Error("Usage: tcap mcp disable <profile|server>");
855
+ const message = options.native ? await disableNativeMcp(name, { ...options.includeUser ? { includeUser: true } : {}, dryRun: options.yes !== true }) : await disableToolCapsuleProfile(name);
856
+ console.log(message);
857
+ return;
858
+ }
859
+ throw new Error("Unknown mcp action. Use: list, enable, disable");
860
+ });
623
861
  cli.command("tools <profile>", "List MCP tools").option("--brief", "Print compact tool summaries").option("--names", "Print tool names only").option("--json", "Print raw JSON").action(async (profileName, options) => {
624
862
  const profile = await loadProfile(profileName);
625
863
  const result = await withClient(profile, (client) => client.listTools());
@@ -653,7 +891,7 @@ cli.command("call <profile> <tool> <args>", "Call an MCP tool with JSON args or
653
891
  console.log(JSON.stringify(response, null, 2));
654
892
  if (options.saveRun) {
655
893
  await ensureToolCapsuleIgnored();
656
- const dir = await saveRun(runBaseDir(profile.name), {
894
+ const dir = await saveRun(workspaceRunBaseDir(profile.name), {
657
895
  id: runId,
658
896
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
659
897
  profile: profile.name,
@@ -664,12 +902,12 @@ cli.command("call <profile> <tool> <args>", "Call an MCP tool with JSON args or
664
902
  request,
665
903
  response
666
904
  });
667
- console.error(pc.green(`Saved run: ${dir}`));
905
+ console.error(pc2.green(`Saved run: ${dir}`));
668
906
  }
669
907
  } catch (error) {
670
908
  if (options.saveRun) {
671
909
  await ensureToolCapsuleIgnored();
672
- const dir = await saveRun(runBaseDir(profile.name), {
910
+ const dir = await saveRun(workspaceRunBaseDir(profile.name), {
673
911
  id: runId,
674
912
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
675
913
  profile: profile.name,
@@ -680,7 +918,7 @@ cli.command("call <profile> <tool> <args>", "Call an MCP tool with JSON args or
680
918
  request,
681
919
  error: error instanceof Error ? error.message : String(error)
682
920
  });
683
- console.error(pc.yellow(`Saved failed run: ${dir}`));
921
+ console.error(pc2.yellow(`Saved failed run: ${dir}`));
684
922
  }
685
923
  throw error;
686
924
  }
@@ -721,18 +959,18 @@ cli.command("benchmark <profile>", "Estimate schema savings for a profile").opti
721
959
 
722
960
  > Rough tokens are estimated from serialized schema length. Use this report to compare schema footprint before and after capsule summaries.
723
961
  ` : JSON.stringify(summary, null, 2);
724
- if (options.out) await writeFile5(options.out, output);
962
+ if (options.out) await writeFile6(options.out, output);
725
963
  console.log(output);
726
964
  });
727
965
  cli.command("render-readme", "Print website hero copy snippets").action(async () => {
728
- console.log(await readFile3(new URL("../docs/hero-copy.md", import.meta.url), "utf8"));
966
+ console.log(await readFile5(new URL("../docs/hero-copy.md", import.meta.url), "utf8"));
729
967
  });
730
968
  cli.help();
731
969
  cli.version(packageVersion);
732
970
  try {
733
971
  cli.parse();
734
972
  } catch (error) {
735
- console.error(pc.red(error instanceof Error ? error.message : String(error)));
973
+ console.error(pc2.red(error instanceof Error ? error.message : String(error)));
736
974
  process.exit(1);
737
975
  }
738
976
  //# sourceMappingURL=cli.js.map