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/README.md +46 -18
- package/dist/cli.js +521 -283
- package/dist/cli.js.map +1 -1
- package/docs/agent-tool-compatibility.md +14 -3
- package/docs/architecture.md +1 -1
- package/docs/blog-launch.md +13 -10
- package/docs/comparison.md +69 -0
- package/docs/concept.md +4 -4
- package/docs/hero-copy.md +1 -1
- package/docs/importing-mcp.md +36 -7
- package/docs/launch.md +13 -12
- package/docs/next-steps.md +5 -4
- package/examples/feishu/README.md +4 -1
- package/llms.txt +11 -15
- package/package.json +3 -2
- package/skills/toolcapsule/SKILL.md +122 -0
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
|
|
5
|
-
import { join as join6 } from "path";
|
|
4
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
6
5
|
import { cac } from "cac";
|
|
7
|
-
import
|
|
6
|
+
import pc2 from "picocolors";
|
|
8
7
|
|
|
9
|
-
// src/mcp/
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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 =
|
|
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/
|
|
263
|
-
import {
|
|
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
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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) =>
|
|
510
|
+
const found = candidates.find((path) => existsSync3(path));
|
|
303
511
|
if (!found) throw new Error(`Profile not found: ${profilePathOrName}`);
|
|
304
|
-
|
|
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
|
|
349
|
-
import { dirname as
|
|
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
|
|
354
|
-
if (target === "claude") return
|
|
355
|
-
if (target === "opencode") return
|
|
356
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
|
414
|
-
await writeJson(
|
|
415
|
-
await
|
|
416
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
437
|
-
import { join as
|
|
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
|
|
514
|
-
await
|
|
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
|
|
522
|
-
import { join as
|
|
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 =
|
|
528
|
-
await
|
|
529
|
-
await writeJson(
|
|
530
|
-
await writeJson(
|
|
531
|
-
if (record.response !== void 0) await writeJson(
|
|
532
|
-
if (record.error) await
|
|
533
|
-
await
|
|
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
|
|
734
|
+
return JSON.parse(await readFile4(join7(runDir, "run.json"), "utf8"));
|
|
539
735
|
}
|
|
540
736
|
|
|
541
737
|
// src/cli.ts
|
|
542
|
-
import { writeFile as
|
|
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
|
|
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
|
|
582
|
-
return
|
|
583
|
-
}
|
|
584
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|