toolcapsule 0.1.0-alpha.1 → 0.1.0-alpha.10

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/README.md CHANGED
@@ -5,14 +5,17 @@
5
5
  [![License: MIT](https://img.shields.io/badge/license-MIT-black?style=flat-square)](LICENSE)
6
6
  [![GitHub Repo stars](https://img.shields.io/github/stars/RainSunMe/toolcapsule?style=flat-square)](https://github.com/RainSunMe/toolcapsule)
7
7
 
8
- > Heavy MCP tools don't belong in your prompt. Put them in a ToolCapsule.
8
+ > MCP-to-Skill for heavy MCP tools.
9
9
 
10
- **ToolCapsule** turns heavy MCP servers into lightweight, lazy-loaded, file-first **Agent Skills** with patch-and-retry workflows.
10
+ **ToolCapsule** turns heavy MCP servers into lightweight, lazy-loaded, file-first **Agent Skills** with saved runs and patch-and-retry recovery.
11
+
12
+ If you are looking for **lazy MCP**, **MCP to Skill**, **MCP-to-Skill**, or **Agent Skills for MCP tools**, ToolCapsule is built for that workflow.
11
13
 
12
14
  It is not a replacement for MCP or Skills. It is the missing workflow layer between them:
13
15
 
14
16
  ```text
15
17
  Heavy MCP server
18
+ → MCP-to-Skill workflow layer
16
19
  → compact Agent Skill
17
20
  → local args/content files
18
21
  → auditable tool runs
@@ -31,6 +34,36 @@ But large MCP servers can be expensive in agent contexts:
31
34
  - failed tool calls force the model to regenerate the whole call.
32
35
 
33
36
  ToolCapsule keeps the MCP server as the source of truth, but exposes it through a lightweight Skill and local artifacts.
37
+ Transport logs are quiet by default so remote MCP URLs are not printed during normal use. Set `TOOLCAPSULE_DEBUG=1` only when debugging.
38
+
39
+ ## MCP-to-Skill and lazy MCP
40
+
41
+ ToolCapsule is an **MCP-to-Skill workflow layer**. It imports existing MCP configurations and generates Agent Skills that let agents lazy-load MCP schemas only when needed.
42
+
43
+ This is the practical version of a lazy MCP workflow:
44
+
45
+ - MCP remains the protocol and source of truth;
46
+ - Skills become the agent-facing workflow;
47
+ - large payloads move into local files;
48
+ - failed calls become patchable artifacts instead of regenerated prompts.
49
+
50
+ ## Import your existing MCP setup
51
+
52
+ Already configured MCP in Claude Code, GitHub Copilot / VS Code, OpenCode, Gemini CLI, or Cursor? Import it instead of retyping URLs and commands:
53
+
54
+ ```bash
55
+ tcap import --dry-run
56
+ tcap import --name github --target claude
57
+ ```
58
+
59
+ ToolCapsule scans common workspace MCP config files such as `.mcp.json`, `.vscode/mcp.json`, `opencode.json`, `.gemini/settings.json`, and `.cursor/mcp.json`, then creates:
60
+
61
+ ```text
62
+ .toolcapsule/profiles/<server>.json
63
+ .claude/skills/<server>-mcp/SKILL.md
64
+ ```
65
+
66
+ Use `--target copilot`, `--target opencode`, `--target agents`, or `--target all` to write skills for other agents. User-level MCP configs are only inspected when you opt in with `--include-user`.
34
67
 
35
68
  ## What is a ToolCapsule?
36
69
 
@@ -47,7 +80,9 @@ Instead of making the model hold everything in the prompt, ToolCapsule stores he
47
80
  ```bash
48
81
  npm i -g toolcapsule
49
82
 
50
- toolcapsule init feishu --url https://mcp.example.com/mcp/xxx
83
+ toolcapsule install-skill
84
+ toolcapsule import --dry-run
85
+ toolcapsule import --name feishu --target claude
51
86
  toolcapsule tools feishu --brief
52
87
  toolcapsule schema feishu create-doc
53
88
  toolcapsule call feishu create-doc @args.json --save-run
@@ -64,7 +99,7 @@ tcap call feishu create-doc @args.json --save-run
64
99
  ## What it generates
65
100
 
66
101
  ```text
67
- .github/skills/feishu-mcp/
102
+ .claude/skills/feishu-mcp/
68
103
  SKILL.md # lightweight Agent Skill entrypoint
69
104
  toolcapsule.config.json # MCP transport/profile config
70
105
  scripts/README.md
@@ -113,8 +148,12 @@ Results vary by MCP server, model, and host.
113
148
  ## CLI
114
149
 
115
150
  ```text
116
- toolcapsule init <name> --url <remote-mcp-url>
117
- toolcapsule init <name> --command <stdio-command> --arg <arg>
151
+ toolcapsule init <name> --url <remote-mcp-url> --target claude
152
+ toolcapsule init <name> --command <stdio-command> --arg <arg> --target claude
153
+ toolcapsule install-skill --target claude
154
+ toolcapsule import --dry-run
155
+ toolcapsule import --name <server> --target claude
156
+ toolcapsule import --all --target all
118
157
  toolcapsule tools <profile> --brief
119
158
  toolcapsule describe <profile> <tool> --brief
120
159
  toolcapsule schema <profile> <tool>
@@ -146,12 +185,15 @@ Early alpha. APIs may change before v1.0.
146
185
 
147
186
  - [Concept](docs/concept.md)
148
187
  - [Architecture](docs/architecture.md)
188
+ - [Import existing MCP configuration](docs/importing-mcp.md)
149
189
  - [Patch and retry](docs/patch-and-retry.md)
150
190
  - [Benchmark methodology](docs/benchmark-methodology.md)
151
191
  - [Releasing](docs/releasing.md)
152
192
  - [Launch notes](docs/launch.md)
193
+ - [Screenshots and recordings](docs/screenshots.md)
153
194
  - [Next steps](docs/next-steps.md)
154
195
  - [Release checklist](docs/release-checklist.md)
196
+ - [Agent tool compatibility research](docs/agent-tool-compatibility.md)
155
197
  - [Roadmap](ROADMAP.md)
156
198
 
157
199
  ## License
package/dist/cli.js CHANGED
@@ -2,39 +2,51 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { readFile as readFile3 } from "fs/promises";
5
- import { join as join4 } from "path";
5
+ import { join as join6 } from "path";
6
6
  import { cac } from "cac";
7
7
  import pc from "picocolors";
8
8
 
9
9
  // src/mcp/client.ts
10
10
  import { spawn } from "child_process";
11
11
  import { once } from "events";
12
+ import { resolve } from "path";
12
13
  var McpClient = class {
13
14
  child;
14
15
  nextId = 1;
15
16
  buffer = "";
16
17
  pending = /* @__PURE__ */ new Map();
17
18
  timeoutMs;
19
+ debug;
20
+ clientVersion;
18
21
  constructor(profile, opts = {}) {
19
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";
20
25
  if (profile.transport.type === "remote") {
21
- this.child = spawn("npx", ["-y", "mcp-remote", profile.transport.url], {
22
- stdio: ["pipe", "pipe", "pipe"]
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 }
23
29
  });
24
30
  } else {
25
31
  this.child = spawn(profile.transport.command, profile.transport.args ?? [], {
26
- stdio: ["pipe", "pipe", "pipe"]
32
+ stdio: ["pipe", "pipe", "pipe"],
33
+ env: { ...process.env, ...profile.transport.env },
34
+ cwd: profile.transport.cwd ? resolve(profile.transport.cwd) : void 0
27
35
  });
28
36
  }
29
37
  this.child.stdout.setEncoding("utf8");
30
38
  this.child.stdout.on("data", (chunk) => this.onStdout(chunk));
31
- this.child.stderr.on("data", (chunk) => process.stderr.write(chunk));
39
+ this.child.stderr.on("data", (chunk) => this.onStderr(chunk));
32
40
  this.child.on("exit", (code, signal) => {
33
41
  const error = new Error(`MCP process exited early (code=${code}, signal=${signal})`);
34
42
  for (const waiter of this.pending.values()) waiter.reject(error);
35
43
  this.pending.clear();
36
44
  });
37
45
  }
46
+ onStderr(chunk) {
47
+ if (!this.debug) return;
48
+ process.stderr.write(redactSecrets(chunk.toString("utf8")));
49
+ }
38
50
  onStdout(chunk) {
39
51
  this.buffer += chunk;
40
52
  let newlineIndex;
@@ -63,14 +75,14 @@ var McpClient = class {
63
75
  await this.request("initialize", {
64
76
  protocolVersion: "2025-03-26",
65
77
  capabilities: {},
66
- clientInfo: { name: "toolcapsule", version: "0.1.0" }
78
+ clientInfo: { name: "toolcapsule", version: this.clientVersion }
67
79
  });
68
80
  this.notify("notifications/initialized", {});
69
81
  }
70
82
  async request(method, params) {
71
83
  const id = this.nextId++;
72
- const responsePromise = new Promise((resolve2, reject) => {
73
- this.pending.set(id, { resolve: resolve2, reject });
84
+ const responsePromise = new Promise((resolve3, reject) => {
85
+ this.pending.set(id, { resolve: resolve3, reject });
74
86
  });
75
87
  this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
76
88
  `);
@@ -90,7 +102,7 @@ var McpClient = class {
90
102
  }
91
103
  async close() {
92
104
  if (!this.child.killed) this.child.kill();
93
- if (this.child.exitCode === null) await Promise.race([once(this.child, "exit"), new Promise((resolve2) => setTimeout(resolve2, 500))]);
105
+ if (this.child.exitCode === null) await Promise.race([once(this.child, "exit"), new Promise((resolve3) => setTimeout(resolve3, 500))]);
94
106
  }
95
107
  async withTimeout(promise, label) {
96
108
  let timer;
@@ -104,15 +116,22 @@ var McpClient = class {
104
116
  }
105
117
  }
106
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
+ }
107
126
 
108
- // src/profile.ts
127
+ // src/mcp/importer.ts
109
128
  import { existsSync } from "fs";
129
+ import { homedir } from "os";
110
130
  import { join } from "path";
111
- import { z } from "zod";
112
131
 
113
132
  // src/utils/fs.ts
114
133
  import { mkdir, readFile, writeFile } from "fs/promises";
115
- import { dirname, resolve } from "path";
134
+ import { dirname, resolve as resolve2 } from "path";
116
135
  async function readJson(path) {
117
136
  return JSON.parse(await readFile(path, "utf8"));
118
137
  }
@@ -122,12 +141,128 @@ async function writeJson(path, value) {
122
141
  `);
123
142
  }
124
143
 
144
+ // src/mcp/importer.ts
145
+ var workspaceSources = [
146
+ { tool: "vscode", path: join(".vscode", "mcp.json") },
147
+ { tool: "claude", path: ".mcp.json" },
148
+ { tool: "opencode", path: "opencode.json" },
149
+ { tool: "gemini", path: join(".gemini", "settings.json") },
150
+ { tool: "cursor", path: join(".cursor", "mcp.json") }
151
+ ];
152
+ var userSources = [
153
+ { tool: "claude", path: join(homedir(), ".claude.json"), userLevel: true },
154
+ { tool: "opencode", path: join(homedir(), ".config", "opencode", "opencode.json"), userLevel: true },
155
+ { tool: "gemini", path: join(homedir(), ".gemini", "settings.json"), userLevel: true }
156
+ ];
157
+ function asRecord(value) {
158
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
159
+ }
160
+ function stringArray(value) {
161
+ return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
162
+ }
163
+ function stringRecord(value) {
164
+ const record = asRecord(value);
165
+ if (!record) return void 0;
166
+ const entries = Object.entries(record).filter((entry) => typeof entry[1] === "string");
167
+ return entries.length > 0 ? Object.fromEntries(entries) : void 0;
168
+ }
169
+ function cleanName(name) {
170
+ const cleaned = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
171
+ return cleaned || "imported-mcp";
172
+ }
173
+ function mapServer(name, source, raw) {
174
+ const server = asRecord(raw);
175
+ if (!server) return void 0;
176
+ const warnings = [];
177
+ let transport;
178
+ const type = typeof server.type === "string" ? server.type : void 0;
179
+ const url = typeof server.url === "string" ? server.url : typeof server.httpUrl === "string" ? server.httpUrl : void 0;
180
+ const commandValue = server.command;
181
+ const headers = stringRecord(server.headers);
182
+ const env = stringRecord(server.env) ?? stringRecord(server.environment);
183
+ const cwd = typeof server.cwd === "string" ? server.cwd : void 0;
184
+ if (url && (!type || ["http", "streamable-http", "remote", "sse", "ws"].includes(type))) {
185
+ transport = { type: "remote", url, ...headers ? { headers } : {}, ...env ? { env } : {} };
186
+ if (type === "sse" || type === "ws") warnings.push(`Imported ${type} server as remote URL; verify transport compatibility.`);
187
+ } else if (typeof commandValue === "string") {
188
+ transport = {
189
+ type: "stdio",
190
+ command: commandValue,
191
+ args: stringArray(server.args) ?? [],
192
+ ...env ? { env } : {},
193
+ ...cwd ? { cwd } : {}
194
+ };
195
+ } else if (Array.isArray(commandValue) && commandValue.every((item) => typeof item === "string") && commandValue.length > 0) {
196
+ transport = { type: "stdio", command: commandValue[0], args: commandValue.slice(1), ...env ? { env } : {}, ...cwd ? { cwd } : {} };
197
+ }
198
+ if (!transport) return void 0;
199
+ if (server.headers && !headers) warnings.push("Headers were present but not copied because they were not string values.");
200
+ if ((server.env || server.environment) && !env) warnings.push("Environment variables were present but not copied because they were not string values.");
201
+ if (server.includeTools || server.excludeTools) warnings.push("Tool include/exclude filters were not copied; use ToolCapsule brief/schema commands to choose tools manually.");
202
+ const profileName = cleanName(name);
203
+ return {
204
+ name: profileName,
205
+ source,
206
+ warnings,
207
+ profile: {
208
+ name: profileName,
209
+ transport
210
+ }
211
+ };
212
+ }
213
+ function serverEntries(source, config) {
214
+ if (source.tool === "vscode" || source.tool === "cursor") return Object.entries(asRecord(config.servers) ?? {});
215
+ if (source.tool === "opencode") return Object.entries(asRecord(config.mcp) ?? {});
216
+ if (source.tool === "claude") {
217
+ const projectEntries = Object.entries(asRecord(config.mcpServers) ?? {});
218
+ const projectConfigs = Object.entries(asRecord(config.projects) ?? {}).flatMap(
219
+ ([, project]) => Object.entries(asRecord(asRecord(project)?.mcpServers) ?? {})
220
+ );
221
+ return [...projectEntries, ...projectConfigs];
222
+ }
223
+ return Object.entries(asRecord(config.mcpServers) ?? {});
224
+ }
225
+ async function discoverMcpServers(opts = {}) {
226
+ const sources = opts.includeUser ? [...workspaceSources, ...userSources] : workspaceSources;
227
+ const discovered = [];
228
+ for (const source of sources) {
229
+ if (!existsSync(source.path)) continue;
230
+ const config = asRecord(await readJson(source.path));
231
+ if (!config) continue;
232
+ for (const [name, raw] of serverEntries(source, config)) {
233
+ const imported = mapServer(name, source, raw);
234
+ if (imported) discovered.push(imported);
235
+ }
236
+ }
237
+ return discovered;
238
+ }
239
+ function selectImportedServers(servers, name, all) {
240
+ if (all) return servers;
241
+ if (!name) return servers.length === 1 ? servers : [];
242
+ const normalized = cleanName(name);
243
+ return servers.filter((server) => server.name === normalized || server.name === name);
244
+ }
245
+
125
246
  // src/profile.ts
247
+ import { existsSync as existsSync2 } from "fs";
248
+ import { join as join2 } from "path";
249
+ import { z } from "zod";
126
250
  var profileSchema = z.object({
127
251
  name: z.string().min(1),
128
252
  transport: z.union([
129
- z.object({ type: z.literal("remote"), url: z.string().url() }),
130
- z.object({ type: z.literal("stdio"), command: z.string().min(1), args: z.array(z.string()).optional() })
253
+ z.object({
254
+ type: z.literal("remote"),
255
+ url: z.string().url(),
256
+ headers: z.record(z.string()).optional(),
257
+ env: z.record(z.string()).optional()
258
+ }),
259
+ z.object({
260
+ type: z.literal("stdio"),
261
+ command: z.string().min(1),
262
+ args: z.array(z.string()).optional(),
263
+ env: z.record(z.string()).optional(),
264
+ cwd: z.string().optional()
265
+ })
131
266
  ]),
132
267
  skill: z.object({
133
268
  name: z.string().optional(),
@@ -145,10 +280,10 @@ async function loadProfile(profilePathOrName) {
145
280
  const candidates = [
146
281
  profilePathOrName,
147
282
  `${profilePathOrName}.json`,
148
- join(".toolcapsule", "profiles", `${profilePathOrName}.json`),
149
- join(".github", "skills", `${profilePathOrName}-mcp`, "toolcapsule.config.json")
283
+ join2(".toolcapsule", "profiles", `${profilePathOrName}.json`),
284
+ join2(".github", "skills", `${profilePathOrName}-mcp`, "toolcapsule.config.json")
150
285
  ];
151
- const found = candidates.find((path) => existsSync(path));
286
+ const found = candidates.find((path) => existsSync2(path));
152
287
  if (!found) throw new Error(`Profile not found: ${profilePathOrName}`);
153
288
  return profileSchema.parse(await readJson(found));
154
289
  }
@@ -195,8 +330,18 @@ function summarizeTools(tools) {
195
330
 
196
331
  // src/skill/generator.ts
197
332
  import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
198
- import { dirname as dirname2, join as join2 } from "path";
333
+ import { dirname as dirname2, join as join3 } from "path";
199
334
  import Handlebars from "handlebars";
335
+ var defaultSkillTarget = "claude";
336
+ function skillOutputDir(skillName, target) {
337
+ if (target === "copilot") return join3(".github", "skills", skillName);
338
+ if (target === "claude") return join3(".claude", "skills", skillName);
339
+ if (target === "opencode") return join3(".opencode", "skills", skillName);
340
+ return join3(".agents", "skills", skillName);
341
+ }
342
+ function expandSkillTargets(target) {
343
+ return target === "all" ? ["copilot", "claude", "opencode", "agents"] : [target];
344
+ }
200
345
  var skillTemplate = `---
201
346
  name: {{skillName}}
202
347
  description: '{{description}}'
@@ -238,11 +383,10 @@ toolcapsule retry <run-dir>
238
383
  - Do not print secrets.
239
384
  - Review destructive tools before calling.
240
385
  `;
241
- async function generateSkill(profile, opts = {}) {
386
+ async function generateSkillAt(profile, outputDir) {
242
387
  const skillName = profile.skill?.name || `${profile.name}-mcp`;
243
- const outputDir = opts.outputDir || join2(".github", "skills", skillName);
244
- await mkdir2(join2(outputDir, "scripts"), { recursive: true });
245
- await mkdir2(join2(outputDir, "runs"), { recursive: true });
388
+ await mkdir2(join3(outputDir, "scripts"), { recursive: true });
389
+ await mkdir2(join3(outputDir, "runs"), { recursive: true });
246
390
  const template = Handlebars.compile(skillTemplate);
247
391
  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.`;
248
392
  const markdown = template({
@@ -251,10 +395,10 @@ async function generateSkill(profile, opts = {}) {
251
395
  title: `${profile.name} MCP Skill`,
252
396
  description: description.replace(/'/g, "''")
253
397
  });
254
- await writeFile2(join2(outputDir, "SKILL.md"), markdown);
255
- await writeJson(join2(outputDir, "toolcapsule.config.json"), profile);
398
+ await writeFile2(join3(outputDir, "SKILL.md"), markdown);
399
+ await writeJson(join3(outputDir, "toolcapsule.config.json"), profile);
256
400
  await writeFile2(
257
- join2(outputDir, "scripts", "README.md"),
401
+ join3(outputDir, "scripts", "README.md"),
258
402
  `# Scripts
259
403
 
260
404
  This skill uses the project-level \`toolcapsule\` CLI.
@@ -262,34 +406,125 @@ This skill uses the project-level \`toolcapsule\` CLI.
262
406
  );
263
407
  return outputDir;
264
408
  }
409
+ async function generateSkill(profile, opts = {}) {
410
+ const skillName = profile.skill?.name || `${profile.name}-mcp`;
411
+ const target = opts.target || defaultSkillTarget;
412
+ const outputs = opts.outputDir ? [await generateSkillAt(profile, opts.outputDir)] : await Promise.all(expandSkillTargets(target).map((item) => generateSkillAt(profile, skillOutputDir(skillName, item))));
413
+ return outputs.join(", ");
414
+ }
265
415
  async function writeProfile(path, profile) {
266
416
  await mkdir2(dirname2(path), { recursive: true });
267
417
  await writeJson(path, profile);
268
418
  }
269
419
 
420
+ // src/skill/installer.ts
421
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
422
+ import { join as join4 } from "path";
423
+ var agentSkill = `---
424
+ name: toolcapsule
425
+ 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.'
426
+ argument-hint: 'MCP server URL/command, tool name, args file, or retry task'
427
+ ---
428
+
429
+ # ToolCapsule Agent Skill
430
+
431
+ Use ToolCapsule when an agent needs to work with heavy MCP servers without carrying every tool schema in the prompt.
432
+
433
+ ## Install
434
+
435
+ If \`toolcapsule\` or \`tcap\` is missing:
436
+
437
+ \`\`\`bash
438
+ npm install -g toolcapsule
439
+ \`\`\`
440
+
441
+ ## Core workflow
442
+
443
+ 1. Initialize a profile and generated Skill:
444
+
445
+ \`\`\`bash
446
+ tcap init <name> --url <remote-mcp-url>
447
+ # or
448
+ tcap init <name> --command <stdio-command> --arg <arg>
449
+ \`\`\`
450
+
451
+ 2. Discover tools briefly:
452
+
453
+ \`\`\`bash
454
+ tcap tools <name> --brief
455
+ \`\`\`
456
+
457
+ 3. Inspect one tool only when needed:
458
+
459
+ \`\`\`bash
460
+ tcap schema <name> <tool>
461
+ \`\`\`
462
+
463
+ 4. Put complex arguments in a local JSON file.
464
+
465
+ 5. Call the MCP tool through the local args file:
466
+
467
+ \`\`\`bash
468
+ tcap call <name> <tool> @args.json --save-run
469
+ \`\`\`
470
+
471
+ 6. If the call fails, patch the local file and retry:
472
+
473
+ \`\`\`bash
474
+ tcap retry runs/<run-id>
475
+ \`\`\`
476
+
477
+ ## Safety
478
+
479
+ - Do not print or commit private MCP URLs, tokens, API keys, user IDs, or document IDs.
480
+ - Keep generated profiles and run artifacts local unless reviewed.
481
+ - Use \`TOOLCAPSULE_DEBUG=1\` only when debugging; normal transport logs are quiet by default.
482
+ - Prefer \`--brief\` and \`schema\` before reading full MCP schemas.
483
+
484
+ ## When to use
485
+
486
+ Use ToolCapsule for MCP servers with:
487
+
488
+ - many tools;
489
+ - long schemas;
490
+ - large Markdown/JSON payloads;
491
+ - document, ticket, wiki, dashboard, or batch workflows;
492
+ - failures that benefit from patching local artifacts instead of regenerating a full tool call.
493
+ `;
494
+ async function installAgentSkill(outputDir, target = defaultSkillTarget) {
495
+ const outputDirs = outputDir ? [outputDir] : expandSkillTargets(target).map((item) => skillOutputDir("toolcapsule", item));
496
+ await Promise.all(
497
+ outputDirs.map(async (dir) => {
498
+ await mkdir3(dir, { recursive: true });
499
+ await writeFile3(join4(dir, "SKILL.md"), agentSkill);
500
+ })
501
+ );
502
+ return outputDirs.join(", ");
503
+ }
504
+
270
505
  // src/runs/recorder.ts
271
- import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
272
- import { join as join3 } from "path";
506
+ import { mkdir as mkdir4, readFile as readFile2, writeFile as writeFile4 } from "fs/promises";
507
+ import { join as join5 } from "path";
273
508
  function createRunId() {
274
509
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
275
510
  }
276
511
  async function saveRun(baseDir, record) {
277
- const dir = join3(baseDir, record.id);
278
- await mkdir3(dir, { recursive: true });
279
- await writeJson(join3(dir, "run.json"), record);
280
- await writeJson(join3(dir, "request.json"), record.request);
281
- if (record.response !== void 0) await writeJson(join3(dir, "response.json"), record.response);
282
- if (record.error) await writeFile3(join3(dir, "error.txt"), record.error);
283
- await writeFile3(join3(dir, "command.txt"), `${record.command}
512
+ const dir = join5(baseDir, record.id);
513
+ await mkdir4(dir, { recursive: true });
514
+ await writeJson(join5(dir, "run.json"), record);
515
+ await writeJson(join5(dir, "request.json"), record.request);
516
+ if (record.response !== void 0) await writeJson(join5(dir, "response.json"), record.response);
517
+ if (record.error) await writeFile4(join5(dir, "error.txt"), record.error);
518
+ await writeFile4(join5(dir, "command.txt"), `${record.command}
284
519
  `);
285
520
  return dir;
286
521
  }
287
522
  async function loadRun(runDir) {
288
- return JSON.parse(await readFile2(join3(runDir, "run.json"), "utf8"));
523
+ return JSON.parse(await readFile2(join5(runDir, "run.json"), "utf8"));
289
524
  }
290
525
 
291
526
  // src/cli.ts
292
- import { writeFile as writeFile4 } from "fs/promises";
527
+ import { writeFile as writeFile5 } from "fs/promises";
293
528
 
294
529
  // src/utils/tokens.ts
295
530
  function roughTokens(value) {
@@ -302,8 +537,17 @@ function percentReduction(before, after) {
302
537
 
303
538
  // src/cli.ts
304
539
  var cli = cac("toolcapsule");
540
+ async function readPackageVersion() {
541
+ try {
542
+ const pkg = JSON.parse(await readFile3(new URL("../package.json", import.meta.url), "utf8"));
543
+ return pkg.version || "0.0.0";
544
+ } catch {
545
+ return "0.0.0";
546
+ }
547
+ }
548
+ var packageVersion = await readPackageVersion();
305
549
  async function withClient(profile, fn) {
306
- const client = new McpClient(profile);
550
+ const client = new McpClient(profile, { clientVersion: packageVersion });
307
551
  try {
308
552
  await client.init();
309
553
  return await fn(client);
@@ -314,13 +558,48 @@ async function withClient(profile, fn) {
314
558
  function readArgsPath(raw) {
315
559
  return raw.startsWith("@") ? raw.slice(1) : raw;
316
560
  }
317
- 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").action(async (name, options) => {
561
+ function readSkillTarget(raw) {
562
+ const target = raw || defaultSkillTarget;
563
+ if (["copilot", "claude", "opencode", "agents", "all"].includes(target)) return target;
564
+ throw new Error("Invalid --target. Use one of: copilot, claude, opencode, agents, all");
565
+ }
566
+ 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) => {
318
567
  if (!options.url && !options.command) throw new Error("Provide --url for remote MCP or --command for stdio MCP");
319
568
  const profile = options.url ? { name, transport: { type: "remote", url: options.url } } : { name, transport: { type: "stdio", command: options.command, args: options.arg ?? [] } };
320
- await writeProfile(join4(".toolcapsule", "profiles", `${name}.json`), profile);
321
- const out = await generateSkill(profile, options.output ? { outputDir: options.output } : {});
569
+ await writeProfile(join6(".toolcapsule", "profiles", `${name}.json`), profile);
570
+ const out = await generateSkill(profile, options.output ? { outputDir: options.output } : { target: readSkillTarget(options.target) });
322
571
  console.log(pc.green(`Created profile and skill at ${out}`));
323
572
  });
573
+ 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) => {
574
+ const out = await installAgentSkill(options.output, readSkillTarget(options.target));
575
+ console.log(pc.green(`Installed ToolCapsule Agent Skill at ${out}`));
576
+ });
577
+ 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(
578
+ async (options) => {
579
+ const discovered = await discoverMcpServers(options.includeUser ? { includeUser: true } : {});
580
+ if (discovered.length === 0) {
581
+ console.log("No importable MCP servers found.");
582
+ return;
583
+ }
584
+ if (options.dryRun) {
585
+ for (const server of discovered) {
586
+ console.log(`${server.name} ${server.source.tool} ${server.source.path}`);
587
+ for (const warning of server.warnings) console.log(pc.yellow(` warning: ${warning}`));
588
+ }
589
+ return;
590
+ }
591
+ const selected = selectImportedServers(discovered, options.name, options.all);
592
+ if (selected.length === 0) {
593
+ throw new Error("Multiple MCP servers found. Re-run with --dry-run, then pass --name <server> or --all.");
594
+ }
595
+ for (const server of selected) {
596
+ await writeProfile(join6(".toolcapsule", "profiles", `${server.profile.name}.json`), server.profile);
597
+ const out = await generateSkill(server.profile, { target: readSkillTarget(options.target) });
598
+ console.log(pc.green(`Imported ${server.name} from ${server.source.path} -> ${out}`));
599
+ for (const warning of server.warnings) console.log(pc.yellow(` warning: ${warning}`));
600
+ }
601
+ }
602
+ );
324
603
  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) => {
325
604
  const profile = await loadProfile(profileName);
326
605
  const result = await withClient(profile, (client) => client.listTools());
@@ -420,14 +699,14 @@ cli.command("benchmark <profile>", "Estimate schema savings for a profile").opti
420
699
 
421
700
  > Rough tokens are estimated from serialized schema length. Use this report to compare schema footprint before and after capsule summaries.
422
701
  ` : JSON.stringify(summary, null, 2);
423
- if (options.out) await writeFile4(options.out, output);
702
+ if (options.out) await writeFile5(options.out, output);
424
703
  console.log(output);
425
704
  });
426
705
  cli.command("render-readme", "Print website hero copy snippets").action(async () => {
427
706
  console.log(await readFile3(new URL("../docs/hero-copy.md", import.meta.url), "utf8"));
428
707
  });
429
708
  cli.help();
430
- cli.version("0.1.0-alpha.0");
709
+ cli.version(packageVersion);
431
710
  try {
432
711
  cli.parse();
433
712
  } catch (error) {