opencodekit 0.16.10 → 0.16.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/dist/template/.opencode/AGENTS.md +85 -32
- package/dist/template/.opencode/agent/build.md +24 -2
- package/dist/template/.opencode/command/handoff.md +37 -0
- package/dist/template/.opencode/command/start.md +6 -0
- package/dist/template/.opencode/command/status.md +33 -7
- package/dist/template/.opencode/dcp.jsonc +79 -79
- package/dist/template/.opencode/memory.db-shm +0 -0
- package/dist/template/.opencode/memory.db-wal +0 -0
- package/dist/template/.opencode/opencode.json +905 -885
- package/dist/template/.opencode/plugin/compaction.ts +52 -51
- package/dist/template/.opencode/plugin/memory.ts +22 -551
- package/dist/template/.opencode/plugin/skill-mcp.ts +514 -486
- package/dist/template/.opencode/plugin/swarm-enforcer.ts +43 -119
- package/dist/template/.opencode/skill/agent-teams/SKILL.md +242 -0
- package/dist/template/.opencode/skill/compaction/SKILL.md +338 -0
- package/dist/template/.opencode/skill/jira/SKILL.md +177 -50
- package/dist/template/.opencode/skill/jira/mcp.json +2 -10
- package/package.json +1 -1
- package/dist/template/.opencode/plugin/env-ctx.ts +0 -34
- package/dist/template/.opencode/plugin/lsp.ts +0 -301
- package/dist/template/.opencode/plugin/truncator.ts +0 -59
|
@@ -6,28 +6,31 @@ import type { Plugin } from "@opencode-ai/plugin";
|
|
|
6
6
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
7
7
|
|
|
8
8
|
interface McpServerConfig {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
command: string;
|
|
10
|
+
args?: string[];
|
|
11
|
+
env?: Record<string, string>;
|
|
12
|
+
includeTools?: string[]; // Ampcode-style tool filtering with glob patterns
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
interface McpClient {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
process: ChildProcess;
|
|
17
|
+
config: McpServerConfig;
|
|
18
|
+
requestId: number;
|
|
19
|
+
pendingRequests: Map<
|
|
20
|
+
number,
|
|
21
|
+
{ resolve: (v: any) => void; reject: (e: any) => void }
|
|
22
|
+
>;
|
|
23
|
+
capabilities?: {
|
|
24
|
+
tools?: any[];
|
|
25
|
+
resources?: any[];
|
|
26
|
+
prompts?: any[];
|
|
27
|
+
};
|
|
28
|
+
filteredTools?: any[]; // Tools after includeTools filtering
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
interface SkillMcpState {
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
clients: Map<string, McpClient>; // key: skillName:serverName
|
|
33
|
+
loadedSkills: Map<string, Record<string, McpServerConfig>>; // skillName -> mcp configs
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
/**
|
|
@@ -35,109 +38,109 @@ interface SkillMcpState {
|
|
|
35
38
|
* Supports: exact match, * (any chars), ? (single char)
|
|
36
39
|
*/
|
|
37
40
|
function matchGlobPattern(pattern: string, toolName: string): boolean {
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
// Exact match
|
|
42
|
+
if (pattern === toolName) return true;
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// Convert glob to regex
|
|
45
|
+
const regexPattern = pattern
|
|
46
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&") // Escape special regex chars
|
|
47
|
+
.replace(/\*/g, ".*") // * -> .*
|
|
48
|
+
.replace(/\?/g, "."); // ? -> .
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
51
|
+
return regex.test(toolName);
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
/**
|
|
52
55
|
* Filter tools based on includeTools patterns
|
|
53
56
|
*/
|
|
54
57
|
function filterTools(allTools: any[], includePatterns?: string[]): any[] {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
if (!includePatterns || includePatterns.length === 0) {
|
|
59
|
+
return allTools; // No filtering, return all
|
|
60
|
+
}
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
return allTools.filter((tool) =>
|
|
63
|
+
includePatterns.some((pattern) => matchGlobPattern(pattern, tool.name)),
|
|
64
|
+
);
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
function parseYamlFrontmatter(content: string): {
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
frontmatter: any;
|
|
69
|
+
body: string;
|
|
67
70
|
} {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
71
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
72
|
+
if (!match) return { frontmatter: {}, body: content };
|
|
73
|
+
|
|
74
|
+
const yamlStr = match[1];
|
|
75
|
+
const body = match[2];
|
|
76
|
+
|
|
77
|
+
// Simple YAML parser for our use case
|
|
78
|
+
const frontmatter: any = {};
|
|
79
|
+
let mcpConfig: any = null;
|
|
80
|
+
let serverName = "";
|
|
81
|
+
let serverConfig: any = {};
|
|
82
|
+
let currentArrayKey = "";
|
|
83
|
+
|
|
84
|
+
for (const line of yamlStr.split("\n")) {
|
|
85
|
+
const trimmed = line.trim();
|
|
86
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
87
|
+
|
|
88
|
+
const indent = line.search(/\S/);
|
|
89
|
+
const keyMatch = trimmed.match(/^([\w-]+):\s*(.*)$/);
|
|
90
|
+
|
|
91
|
+
if (keyMatch) {
|
|
92
|
+
const [, key, value] = keyMatch;
|
|
93
|
+
|
|
94
|
+
if (indent === 0) {
|
|
95
|
+
// Top-level key
|
|
96
|
+
if (key === "mcp") {
|
|
97
|
+
mcpConfig = {};
|
|
98
|
+
frontmatter.mcp = mcpConfig;
|
|
99
|
+
} else {
|
|
100
|
+
frontmatter[key] = value || undefined;
|
|
101
|
+
}
|
|
102
|
+
currentArrayKey = "";
|
|
103
|
+
} else if (mcpConfig !== null && indent === 2) {
|
|
104
|
+
// Server name under mcp
|
|
105
|
+
serverName = key;
|
|
106
|
+
serverConfig = {};
|
|
107
|
+
mcpConfig[serverName] = serverConfig;
|
|
108
|
+
currentArrayKey = "";
|
|
109
|
+
} else if (serverConfig && indent === 4) {
|
|
110
|
+
// Server config property
|
|
111
|
+
if (key === "command") {
|
|
112
|
+
serverConfig.command = value;
|
|
113
|
+
currentArrayKey = "";
|
|
114
|
+
} else if (key === "args" || key === "includeTools") {
|
|
115
|
+
// Parse inline array or set up for multi-line
|
|
116
|
+
if (value.startsWith("[")) {
|
|
117
|
+
try {
|
|
118
|
+
serverConfig[key] = JSON.parse(value);
|
|
119
|
+
} catch {
|
|
120
|
+
serverConfig[key] = [];
|
|
121
|
+
}
|
|
122
|
+
currentArrayKey = "";
|
|
123
|
+
} else {
|
|
124
|
+
serverConfig[key] = [];
|
|
125
|
+
currentArrayKey = key;
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
currentArrayKey = "";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} else if (
|
|
132
|
+
trimmed.startsWith("- ") &&
|
|
133
|
+
serverConfig &&
|
|
134
|
+
currentArrayKey &&
|
|
135
|
+
serverConfig[currentArrayKey]
|
|
136
|
+
) {
|
|
137
|
+
// Array item for args or includeTools
|
|
138
|
+
const item = trimmed.slice(2).replace(/^["']|["']$/g, "");
|
|
139
|
+
serverConfig[currentArrayKey].push(item);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { frontmatter, body };
|
|
141
144
|
}
|
|
142
145
|
|
|
143
146
|
/**
|
|
@@ -145,196 +148,200 @@ function parseYamlFrontmatter(content: string): {
|
|
|
145
148
|
* Priority: mcp.json > YAML frontmatter (Ampcode pattern)
|
|
146
149
|
*/
|
|
147
150
|
function loadMcpConfig(
|
|
148
|
-
|
|
149
|
-
|
|
151
|
+
skillDir: string,
|
|
152
|
+
skillPath: string,
|
|
150
153
|
): Record<string, McpServerConfig> | null {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
154
|
+
// Try mcp.json first (Ampcode pattern)
|
|
155
|
+
const mcpJsonPath = join(skillDir, "mcp.json");
|
|
156
|
+
if (existsSync(mcpJsonPath)) {
|
|
157
|
+
try {
|
|
158
|
+
const mcpJson = JSON.parse(readFileSync(mcpJsonPath, "utf-8"));
|
|
159
|
+
return mcpJson;
|
|
160
|
+
} catch {
|
|
161
|
+
// Fall through to YAML
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Fall back to YAML frontmatter
|
|
166
|
+
const content = readFileSync(skillPath, "utf-8");
|
|
167
|
+
const { frontmatter } = parseYamlFrontmatter(content);
|
|
168
|
+
|
|
169
|
+
if (frontmatter.mcp && Object.keys(frontmatter.mcp).length > 0) {
|
|
170
|
+
return frontmatter.mcp;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return null;
|
|
171
174
|
}
|
|
172
175
|
|
|
173
176
|
function findSkillPath(skillName: string, projectDir: string): string | null {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
177
|
+
const locations = [
|
|
178
|
+
join(projectDir, ".opencode", "skill", skillName, "SKILL.md"),
|
|
179
|
+
join(homedir(), ".config", "opencode", "skill", skillName, "SKILL.md"),
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
for (const loc of locations) {
|
|
183
|
+
if (existsSync(loc)) return loc;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
183
186
|
}
|
|
184
187
|
|
|
185
188
|
export const SkillMcpPlugin: Plugin = async ({ directory }) => {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
189
|
+
const state: SkillMcpState = {
|
|
190
|
+
clients: new Map(),
|
|
191
|
+
loadedSkills: new Map(),
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
function getClientKey(skillName: string, serverName: string): string {
|
|
195
|
+
return `${skillName}:${serverName}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function sendRequest(
|
|
199
|
+
client: McpClient,
|
|
200
|
+
method: string,
|
|
201
|
+
params?: any,
|
|
202
|
+
): Promise<any> {
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
const id = ++client.requestId;
|
|
205
|
+
const request = {
|
|
206
|
+
jsonrpc: "2.0",
|
|
207
|
+
id,
|
|
208
|
+
method,
|
|
209
|
+
params: params || {},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
client.pendingRequests.set(id, { resolve, reject });
|
|
213
|
+
|
|
214
|
+
const timeout = setTimeout(() => {
|
|
215
|
+
client.pendingRequests.delete(id);
|
|
216
|
+
reject(new Error(`Request timeout: ${method}`));
|
|
217
|
+
}, 30000);
|
|
218
|
+
|
|
219
|
+
client.pendingRequests.set(id, {
|
|
220
|
+
resolve: (v) => {
|
|
221
|
+
clearTimeout(timeout);
|
|
222
|
+
resolve(v);
|
|
223
|
+
},
|
|
224
|
+
reject: (e) => {
|
|
225
|
+
clearTimeout(timeout);
|
|
226
|
+
reject(e);
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
client.process.stdin?.write(`${JSON.stringify(request)}\n`);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function connectServer(
|
|
235
|
+
skillName: string,
|
|
236
|
+
serverName: string,
|
|
237
|
+
config: McpServerConfig,
|
|
238
|
+
): Promise<McpClient> {
|
|
239
|
+
const key = getClientKey(skillName, serverName);
|
|
240
|
+
|
|
241
|
+
// Return existing client if connected
|
|
242
|
+
const existing = state.clients.get(key);
|
|
243
|
+
if (existing && !existing.process.killed) {
|
|
244
|
+
return existing;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Spawn MCP server process
|
|
248
|
+
const proc = spawn(config.command, config.args || [], {
|
|
249
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
250
|
+
env: { ...process.env, ...config.env },
|
|
251
|
+
shell: true,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const client: McpClient = {
|
|
255
|
+
process: proc,
|
|
256
|
+
config,
|
|
257
|
+
requestId: 0,
|
|
258
|
+
pendingRequests: new Map(),
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// Handle stdout (JSON-RPC responses)
|
|
262
|
+
let buffer = "";
|
|
263
|
+
proc.stdout?.on("data", (data) => {
|
|
264
|
+
buffer += data.toString();
|
|
265
|
+
const lines = buffer.split("\n");
|
|
266
|
+
buffer = lines.pop() || "";
|
|
267
|
+
|
|
268
|
+
for (const line of lines) {
|
|
269
|
+
if (!line.trim()) continue;
|
|
270
|
+
try {
|
|
271
|
+
const response = JSON.parse(line);
|
|
272
|
+
if (response.id !== undefined) {
|
|
273
|
+
const pending = client.pendingRequests.get(response.id);
|
|
274
|
+
if (pending) {
|
|
275
|
+
client.pendingRequests.delete(response.id);
|
|
276
|
+
if (response.error) {
|
|
277
|
+
pending.reject(new Error(response.error.message));
|
|
278
|
+
} else {
|
|
279
|
+
pending.resolve(response.result);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} catch {}
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
proc.on("error", (err) => {
|
|
288
|
+
console.error(`MCP server error [${key}]:`, err.message);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
proc.on("exit", () => {
|
|
292
|
+
state.clients.delete(key);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
state.clients.set(key, client);
|
|
296
|
+
|
|
297
|
+
// Initialize connection
|
|
298
|
+
try {
|
|
299
|
+
await sendRequest(client, "initialize", {
|
|
300
|
+
protocolVersion: "2024-11-05",
|
|
301
|
+
capabilities: {},
|
|
302
|
+
clientInfo: { name: "opencode-skill-mcp", version: "1.1.0" },
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Send initialized notification
|
|
306
|
+
proc.stdin?.write(
|
|
307
|
+
`${JSON.stringify({
|
|
308
|
+
jsonrpc: "2.0",
|
|
309
|
+
method: "notifications/initialized",
|
|
310
|
+
})}\n`,
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// Discover capabilities
|
|
314
|
+
try {
|
|
315
|
+
const toolsResult = await sendRequest(client, "tools/list", {});
|
|
316
|
+
const allTools = toolsResult.tools || [];
|
|
317
|
+
client.capabilities = { tools: allTools };
|
|
318
|
+
|
|
319
|
+
// Apply includeTools filtering (Ampcode pattern)
|
|
320
|
+
client.filteredTools = filterTools(allTools, config.includeTools);
|
|
321
|
+
} catch {
|
|
322
|
+
client.capabilities = { tools: [] };
|
|
323
|
+
client.filteredTools = [];
|
|
324
|
+
}
|
|
325
|
+
} catch (e: any) {
|
|
326
|
+
proc.kill();
|
|
327
|
+
state.clients.delete(key);
|
|
328
|
+
throw new Error(`Failed to initialize MCP server: ${e.message}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return client;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function disconnectAll() {
|
|
335
|
+
for (const [, client] of state.clients) {
|
|
336
|
+
client.process.kill();
|
|
337
|
+
}
|
|
338
|
+
state.clients.clear();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
tool: {
|
|
343
|
+
skill_mcp: tool({
|
|
344
|
+
description: `Invoke MCP tools from skill-embedded MCP servers.
|
|
338
345
|
|
|
339
346
|
When a skill declares MCP servers (via mcp.json or YAML frontmatter), use this tool to:
|
|
340
347
|
- List available tools: skill_mcp(skill_name="playwright", list_tools=true)
|
|
@@ -342,201 +349,222 @@ When a skill declares MCP servers (via mcp.json or YAML frontmatter), use this t
|
|
|
342
349
|
|
|
343
350
|
Skills can use "includeTools" to filter which MCP tools are exposed (reduces token usage).
|
|
344
351
|
The skill must be loaded first via the skill() tool to register its MCP config.`,
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
352
|
+
args: {
|
|
353
|
+
skill_name: tool.schema
|
|
354
|
+
.string()
|
|
355
|
+
.describe("Name of the loaded skill with MCP config"),
|
|
356
|
+
mcp_name: tool.schema
|
|
357
|
+
.string()
|
|
358
|
+
.optional()
|
|
359
|
+
.describe("Specific MCP server name (if skill has multiple)"),
|
|
360
|
+
list_tools: tool.schema
|
|
361
|
+
.boolean()
|
|
362
|
+
.optional()
|
|
363
|
+
.describe("List available tools from this MCP"),
|
|
364
|
+
tool_name: tool.schema
|
|
365
|
+
.string()
|
|
366
|
+
.optional()
|
|
367
|
+
.describe("MCP tool to invoke"),
|
|
368
|
+
arguments: tool.schema
|
|
369
|
+
.string()
|
|
370
|
+
.optional()
|
|
371
|
+
.describe("JSON string of tool arguments"),
|
|
372
|
+
},
|
|
373
|
+
async execute(args) {
|
|
374
|
+
const {
|
|
375
|
+
skill_name,
|
|
376
|
+
mcp_name,
|
|
377
|
+
list_tools,
|
|
378
|
+
tool_name,
|
|
379
|
+
arguments: argsJson,
|
|
380
|
+
} = args;
|
|
381
|
+
|
|
382
|
+
if (!skill_name) {
|
|
383
|
+
return JSON.stringify({ error: "skill_name required" });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Find skill path
|
|
387
|
+
const skillPath = findSkillPath(skill_name, directory);
|
|
388
|
+
if (!skillPath) {
|
|
389
|
+
return JSON.stringify({ error: `Skill '${skill_name}' not found` });
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Load MCP config from mcp.json or YAML frontmatter
|
|
393
|
+
const skillDir = dirname(skillPath);
|
|
394
|
+
const mcpConfig = loadMcpConfig(skillDir, skillPath);
|
|
395
|
+
|
|
396
|
+
if (!mcpConfig) {
|
|
397
|
+
return JSON.stringify({
|
|
398
|
+
error: `Skill '${skill_name}' has no MCP config (check mcp.json or YAML frontmatter)`,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Determine which MCP server to use
|
|
403
|
+
const serverNames = Object.keys(mcpConfig);
|
|
404
|
+
const targetServer = mcp_name || serverNames[0];
|
|
405
|
+
|
|
406
|
+
if (!mcpConfig[targetServer]) {
|
|
407
|
+
return JSON.stringify({
|
|
408
|
+
error: `MCP server '${targetServer}' not found in skill`,
|
|
409
|
+
available: serverNames,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const serverConfig = mcpConfig[targetServer];
|
|
414
|
+
|
|
415
|
+
// Connect to MCP server
|
|
416
|
+
let client: McpClient;
|
|
417
|
+
try {
|
|
418
|
+
client = await connectServer(
|
|
419
|
+
skill_name,
|
|
420
|
+
targetServer,
|
|
421
|
+
serverConfig,
|
|
422
|
+
);
|
|
423
|
+
} catch (e: any) {
|
|
424
|
+
return JSON.stringify({ error: `Failed to connect: ${e.message}` });
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// List tools (filtered by includeTools if configured)
|
|
428
|
+
if (list_tools) {
|
|
429
|
+
const totalTools = client.capabilities?.tools?.length || 0;
|
|
430
|
+
const filteredTools = client.filteredTools || [];
|
|
431
|
+
const isFiltered =
|
|
432
|
+
serverConfig.includeTools && serverConfig.includeTools.length > 0;
|
|
433
|
+
|
|
434
|
+
return JSON.stringify(
|
|
435
|
+
{
|
|
436
|
+
mcp: targetServer,
|
|
437
|
+
tools: filteredTools.map((t: any) => ({
|
|
438
|
+
name: t.name,
|
|
439
|
+
description: t.description,
|
|
440
|
+
schema: t.inputSchema,
|
|
441
|
+
})),
|
|
442
|
+
...(isFiltered && {
|
|
443
|
+
filtering: {
|
|
444
|
+
patterns: serverConfig.includeTools,
|
|
445
|
+
showing: filteredTools.length,
|
|
446
|
+
total: totalTools,
|
|
447
|
+
tokenSavings: `~${Math.round((1 - filteredTools.length / totalTools) * 100)}%`,
|
|
448
|
+
},
|
|
449
|
+
}),
|
|
450
|
+
},
|
|
451
|
+
null,
|
|
452
|
+
2,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Call tool
|
|
457
|
+
if (tool_name) {
|
|
458
|
+
// Validate tool is in filtered list (if filtering is enabled)
|
|
459
|
+
if (
|
|
460
|
+
serverConfig.includeTools &&
|
|
461
|
+
serverConfig.includeTools.length > 0
|
|
462
|
+
) {
|
|
463
|
+
const isAllowed = client.filteredTools?.some(
|
|
464
|
+
(t: any) => t.name === tool_name,
|
|
465
|
+
);
|
|
466
|
+
if (!isAllowed) {
|
|
467
|
+
return JSON.stringify({
|
|
468
|
+
error: `Tool '${tool_name}' is not in includeTools filter`,
|
|
469
|
+
allowed: client.filteredTools?.map((t: any) => t.name) || [],
|
|
470
|
+
hint: "Add this tool to includeTools in mcp.json or YAML frontmatter",
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
let toolArgs = {};
|
|
476
|
+
if (argsJson) {
|
|
477
|
+
try {
|
|
478
|
+
toolArgs = JSON.parse(argsJson);
|
|
479
|
+
} catch {
|
|
480
|
+
return JSON.stringify({ error: "Invalid JSON in arguments" });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
const result = await sendRequest(client, "tools/call", {
|
|
486
|
+
name: tool_name,
|
|
487
|
+
arguments: toolArgs,
|
|
488
|
+
});
|
|
489
|
+
return JSON.stringify({ result }, null, 2);
|
|
490
|
+
} catch (e: any) {
|
|
491
|
+
return JSON.stringify({
|
|
492
|
+
error: `Tool call failed: ${e.message}`,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return JSON.stringify({
|
|
498
|
+
error: "Specify either list_tools=true or tool_name to call",
|
|
499
|
+
mcp: targetServer,
|
|
500
|
+
available_tools:
|
|
501
|
+
client.filteredTools?.map((t: any) => t.name) || [],
|
|
502
|
+
});
|
|
503
|
+
},
|
|
504
|
+
}),
|
|
505
|
+
|
|
506
|
+
skill_mcp_status: tool({
|
|
507
|
+
description: "Show status of connected MCP servers from skills.",
|
|
508
|
+
args: {},
|
|
509
|
+
async execute() {
|
|
510
|
+
const servers: any[] = [];
|
|
511
|
+
for (const [key, client] of state.clients) {
|
|
512
|
+
const [skillName, serverName] = key.split(":");
|
|
513
|
+
const totalTools = client.capabilities?.tools?.length || 0;
|
|
514
|
+
const filteredTools = client.filteredTools?.length || 0;
|
|
515
|
+
const isFiltered =
|
|
516
|
+
client.config.includeTools &&
|
|
517
|
+
client.config.includeTools.length > 0;
|
|
518
|
+
|
|
519
|
+
servers.push({
|
|
520
|
+
skill: skillName,
|
|
521
|
+
server: serverName,
|
|
522
|
+
connected: !client.process.killed,
|
|
523
|
+
tools: filteredTools,
|
|
524
|
+
...(isFiltered && {
|
|
525
|
+
filtering: {
|
|
526
|
+
total: totalTools,
|
|
527
|
+
filtered: filteredTools,
|
|
528
|
+
},
|
|
529
|
+
}),
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
return JSON.stringify({
|
|
533
|
+
connected_servers: servers,
|
|
534
|
+
count: servers.length,
|
|
535
|
+
});
|
|
536
|
+
},
|
|
537
|
+
}),
|
|
538
|
+
|
|
539
|
+
skill_mcp_disconnect: tool({
|
|
540
|
+
description:
|
|
541
|
+
"Disconnect MCP servers. Use when done with browser automation etc.",
|
|
542
|
+
args: {
|
|
543
|
+
skill_name: tool.schema
|
|
544
|
+
.string()
|
|
545
|
+
.optional()
|
|
546
|
+
.describe("Specific skill to disconnect (all if omitted)"),
|
|
547
|
+
},
|
|
548
|
+
async execute(args) {
|
|
549
|
+
if (args.skill_name) {
|
|
550
|
+
const toDisconnect: string[] = [];
|
|
551
|
+
for (const key of state.clients.keys()) {
|
|
552
|
+
if (key.startsWith(`${args.skill_name}:`)) {
|
|
553
|
+
toDisconnect.push(key);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
for (const key of toDisconnect) {
|
|
557
|
+
const client = state.clients.get(key);
|
|
558
|
+
client?.process.kill();
|
|
559
|
+
state.clients.delete(key);
|
|
560
|
+
}
|
|
561
|
+
return JSON.stringify({ disconnected: toDisconnect });
|
|
562
|
+
}
|
|
563
|
+
const count = state.clients.size;
|
|
564
|
+
disconnectAll();
|
|
565
|
+
return JSON.stringify({ disconnected: "all", count });
|
|
566
|
+
},
|
|
567
|
+
}),
|
|
568
|
+
},
|
|
569
|
+
};
|
|
542
570
|
};
|