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 +48 -6
- package/dist/cli.js +321 -42
- package/dist/cli.js.map +1 -1
- package/docs/agent-tool-compatibility.md +183 -0
- package/docs/architecture.md +29 -2
- package/docs/concept.md +27 -2
- package/docs/hero-copy.md +3 -1
- package/docs/importing-mcp.md +125 -0
- package/docs/launch.md +32 -13
- package/docs/next-steps.md +21 -1
- package/docs/releasing.md +7 -3
- package/docs/screenshots.md +71 -0
- package/examples/feishu/README.md +3 -1
- package/examples/generic-stdio/README.md +21 -0
- package/examples/generic-stdio/create-doc.args.json +4 -0
- package/llms.txt +81 -0
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -5,14 +5,17 @@
|
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](https://github.com/RainSunMe/toolcapsule)
|
|
7
7
|
|
|
8
|
-
>
|
|
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
|
|
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
|
|
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
|
-
.
|
|
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
|
|
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) =>
|
|
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:
|
|
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((
|
|
73
|
-
this.pending.set(id, { resolve:
|
|
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((
|
|
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/
|
|
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({
|
|
130
|
-
|
|
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
|
-
|
|
149
|
-
|
|
283
|
+
join2(".toolcapsule", "profiles", `${profilePathOrName}.json`),
|
|
284
|
+
join2(".github", "skills", `${profilePathOrName}-mcp`, "toolcapsule.config.json")
|
|
150
285
|
];
|
|
151
|
-
const found = candidates.find((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
|
|
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
|
|
386
|
+
async function generateSkillAt(profile, outputDir) {
|
|
242
387
|
const skillName = profile.skill?.name || `${profile.name}-mcp`;
|
|
243
|
-
|
|
244
|
-
await mkdir2(
|
|
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(
|
|
255
|
-
await writeJson(
|
|
398
|
+
await writeFile2(join3(outputDir, "SKILL.md"), markdown);
|
|
399
|
+
await writeJson(join3(outputDir, "toolcapsule.config.json"), profile);
|
|
256
400
|
await writeFile2(
|
|
257
|
-
|
|
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
|
|
272
|
-
import { join as
|
|
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 =
|
|
278
|
-
await
|
|
279
|
-
await writeJson(
|
|
280
|
-
await writeJson(
|
|
281
|
-
if (record.response !== void 0) await writeJson(
|
|
282
|
-
if (record.error) await
|
|
283
|
-
await
|
|
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(
|
|
523
|
+
return JSON.parse(await readFile2(join5(runDir, "run.json"), "utf8"));
|
|
289
524
|
}
|
|
290
525
|
|
|
291
526
|
// src/cli.ts
|
|
292
|
-
import { writeFile as
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
709
|
+
cli.version(packageVersion);
|
|
431
710
|
try {
|
|
432
711
|
cli.parse();
|
|
433
712
|
} catch (error) {
|