opencodekit 0.7.0 → 0.9.0
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 +65 -27
- package/dist/template/.opencode/README.md +14 -19
- package/dist/template/.opencode/agent/build.md +4 -6
- package/dist/template/.opencode/agent/explore.md +18 -18
- package/dist/template/.opencode/agent/planner.md +4 -7
- package/dist/template/.opencode/agent/review.md +1 -2
- package/dist/template/.opencode/agent/rush.md +4 -6
- package/dist/template/.opencode/agent/scout.md +45 -38
- package/dist/template/.opencode/agent/vision.md +16 -24
- package/dist/template/.opencode/command/analyze-project.md +9 -9
- package/dist/template/.opencode/command/create.md +9 -4
- package/dist/template/.opencode/command/finish.md +12 -17
- package/dist/template/.opencode/command/fix-ci.md +10 -9
- package/dist/template/.opencode/command/fix-types.md +4 -11
- package/dist/template/.opencode/command/handoff.md +14 -18
- package/dist/template/.opencode/command/implement.md +11 -11
- package/dist/template/.opencode/command/import-plan.md +25 -14
- package/dist/template/.opencode/command/integration-test.md +1 -1
- package/dist/template/.opencode/command/issue.md +10 -9
- package/dist/template/.opencode/command/new-feature.md +4 -6
- package/dist/template/.opencode/command/plan.md +3 -5
- package/dist/template/.opencode/command/pr.md +2 -4
- package/dist/template/.opencode/command/research-and-implement.md +1 -1
- package/dist/template/.opencode/command/research.md +13 -15
- package/dist/template/.opencode/command/resume.md +2 -2
- package/dist/template/.opencode/command/revert-feature.md +5 -7
- package/dist/template/.opencode/command/status.md +8 -10
- package/dist/template/.opencode/dcp.jsonc +20 -2
- package/dist/template/.opencode/opencode.json +20 -35
- package/dist/template/.opencode/package.json +1 -1
- package/dist/template/.opencode/plugin/beads.ts +667 -0
- package/dist/template/.opencode/plugin/compaction.ts +80 -0
- package/dist/template/.opencode/plugin/skill-mcp.ts +458 -0
- package/dist/template/.opencode/skill/beads/SKILL.md +419 -0
- package/dist/template/.opencode/skill/beads/references/BOUNDARIES.md +218 -0
- package/dist/template/.opencode/skill/beads/references/DEPENDENCIES.md +130 -0
- package/dist/template/.opencode/skill/beads/references/RESUMABILITY.md +180 -0
- package/dist/template/.opencode/skill/beads/references/WORKFLOWS.md +222 -0
- package/dist/template/.opencode/skill/figma/SKILL.md +214 -0
- package/dist/template/.opencode/skill/playwright/SKILL.md +187 -0
- package/package.json +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compaction Plugin - Customizes session compaction for context continuity
|
|
5
|
+
*
|
|
6
|
+
* This plugin injects additional context into the compaction process,
|
|
7
|
+
* including beads state and memory file references.
|
|
8
|
+
*/
|
|
9
|
+
export const CompactionPlugin: Plugin = async (ctx) => {
|
|
10
|
+
const { $, directory } = ctx;
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
"experimental.session.compacting": async (input, output) => {
|
|
14
|
+
// Inject beads state if available
|
|
15
|
+
let beadsContext = "";
|
|
16
|
+
try {
|
|
17
|
+
const result =
|
|
18
|
+
await $`cd ${directory} && bd list --status in_progress --json 2>/dev/null`.quiet();
|
|
19
|
+
if (result.stdout) {
|
|
20
|
+
const inProgress = JSON.parse(result.stdout.toString());
|
|
21
|
+
if (inProgress.length > 0) {
|
|
22
|
+
beadsContext = `\n## Active Beads\n${inProgress.map((b: { id: string; title: string }) => `- ${b.id}: ${b.title}`).join("\n")}`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// Beads not available, skip
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Inject custom context
|
|
30
|
+
output.context.push(`## Session Context
|
|
31
|
+
${beadsContext}
|
|
32
|
+
## Memory Files to Check
|
|
33
|
+
- .opencode/memory/project/gotchas.md - Non-obvious behaviors discovered
|
|
34
|
+
- .opencode/memory/project/commands.md - Build/test commands learned
|
|
35
|
+
- .opencode/memory/project/conventions.md - Code patterns observed
|
|
36
|
+
|
|
37
|
+
## Persistence Rules
|
|
38
|
+
- PRESERVE: Bead IDs, todo items, file paths, line numbers, user constraints
|
|
39
|
+
- DROP: Failed attempts, superseded info, verbose tool outputs, exploration dead-ends
|
|
40
|
+
- Include enough context that a new session can continue seamlessly`);
|
|
41
|
+
|
|
42
|
+
// Replace the entire compaction prompt for more control
|
|
43
|
+
output.prompt = `You are summarizing a coding session for context continuity.
|
|
44
|
+
|
|
45
|
+
## Output Structure
|
|
46
|
+
|
|
47
|
+
Use these sections:
|
|
48
|
+
|
|
49
|
+
### COMPLETED
|
|
50
|
+
- What was done (with file paths)
|
|
51
|
+
- Bead IDs closed and why
|
|
52
|
+
|
|
53
|
+
### IN PROGRESS
|
|
54
|
+
- Current task and bead ID (if any)
|
|
55
|
+
- Files being modified (exact paths)
|
|
56
|
+
- Current todo state (preserve TodoWrite items)
|
|
57
|
+
|
|
58
|
+
### NEXT
|
|
59
|
+
- What needs to be done next
|
|
60
|
+
- Blockers or pending decisions
|
|
61
|
+
|
|
62
|
+
### CONSTRAINTS
|
|
63
|
+
- User preferences that must persist
|
|
64
|
+
- Rules or requirements stated by user
|
|
65
|
+
- Technical decisions and rationale
|
|
66
|
+
|
|
67
|
+
### PERSIST TO MEMORY
|
|
68
|
+
- Gotchas discovered → suggest for project/gotchas.md
|
|
69
|
+
- Commands learned → suggest for project/commands.md
|
|
70
|
+
- Patterns observed → suggest for project/conventions.md
|
|
71
|
+
|
|
72
|
+
## Rules
|
|
73
|
+
|
|
74
|
+
- PRESERVE: Bead IDs, todo items, file paths, line numbers, user constraints
|
|
75
|
+
- DROP: Failed attempts, superseded info, verbose tool outputs, exploration dead-ends
|
|
76
|
+
- Be concise but complete - this summary replaces the full conversation
|
|
77
|
+
- Include enough context that a new session can continue seamlessly`;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
};
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { type ChildProcess, spawn } from "child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { type Plugin, tool } from "@opencode-ai/plugin";
|
|
6
|
+
|
|
7
|
+
interface McpServerConfig {
|
|
8
|
+
command: string;
|
|
9
|
+
args?: string[];
|
|
10
|
+
env?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface McpClient {
|
|
14
|
+
process: ChildProcess;
|
|
15
|
+
config: McpServerConfig;
|
|
16
|
+
requestId: number;
|
|
17
|
+
pendingRequests: Map<
|
|
18
|
+
number,
|
|
19
|
+
{ resolve: (v: any) => void; reject: (e: any) => void }
|
|
20
|
+
>;
|
|
21
|
+
capabilities?: {
|
|
22
|
+
tools?: any[];
|
|
23
|
+
resources?: any[];
|
|
24
|
+
prompts?: any[];
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface SkillMcpState {
|
|
29
|
+
clients: Map<string, McpClient>; // key: skillName:serverName
|
|
30
|
+
loadedSkills: Map<string, Record<string, McpServerConfig>>; // skillName -> mcp configs
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseYamlFrontmatter(content: string): {
|
|
34
|
+
frontmatter: any;
|
|
35
|
+
body: string;
|
|
36
|
+
} {
|
|
37
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
38
|
+
if (!match) return { frontmatter: {}, body: content };
|
|
39
|
+
|
|
40
|
+
const yamlStr = match[1];
|
|
41
|
+
const body = match[2];
|
|
42
|
+
|
|
43
|
+
// Simple YAML parser for our use case
|
|
44
|
+
const frontmatter: any = {};
|
|
45
|
+
let currentKey = "";
|
|
46
|
+
let currentIndent = 0;
|
|
47
|
+
let mcpConfig: any = null;
|
|
48
|
+
let serverName = "";
|
|
49
|
+
let serverConfig: any = {};
|
|
50
|
+
|
|
51
|
+
for (const line of yamlStr.split("\n")) {
|
|
52
|
+
const trimmed = line.trim();
|
|
53
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
54
|
+
|
|
55
|
+
const indent = line.search(/\S/);
|
|
56
|
+
const keyMatch = trimmed.match(/^(\w+):\s*(.*)$/);
|
|
57
|
+
|
|
58
|
+
if (keyMatch) {
|
|
59
|
+
const [, key, value] = keyMatch;
|
|
60
|
+
|
|
61
|
+
if (indent === 0) {
|
|
62
|
+
// Top-level key
|
|
63
|
+
if (key === "mcp") {
|
|
64
|
+
mcpConfig = {};
|
|
65
|
+
frontmatter.mcp = mcpConfig;
|
|
66
|
+
} else {
|
|
67
|
+
frontmatter[key] = value || undefined;
|
|
68
|
+
}
|
|
69
|
+
currentKey = key;
|
|
70
|
+
currentIndent = indent;
|
|
71
|
+
} else if (mcpConfig !== null && indent === 2) {
|
|
72
|
+
// Server name under mcp
|
|
73
|
+
serverName = key;
|
|
74
|
+
serverConfig = {};
|
|
75
|
+
mcpConfig[serverName] = serverConfig;
|
|
76
|
+
} else if (serverConfig && indent === 4) {
|
|
77
|
+
// Server config property
|
|
78
|
+
if (key === "command") {
|
|
79
|
+
serverConfig.command = value;
|
|
80
|
+
} else if (key === "args") {
|
|
81
|
+
// Parse inline array or set up for multi-line
|
|
82
|
+
if (value.startsWith("[")) {
|
|
83
|
+
try {
|
|
84
|
+
serverConfig.args = JSON.parse(value);
|
|
85
|
+
} catch {
|
|
86
|
+
serverConfig.args = [];
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
serverConfig.args = [];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} else if (trimmed.startsWith("- ") && serverConfig?.args) {
|
|
94
|
+
// Array item for args
|
|
95
|
+
const item = trimmed.slice(2).replace(/^["']|["']$/g, "");
|
|
96
|
+
serverConfig.args.push(item);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { frontmatter, body };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function findSkillPath(skillName: string, projectDir: string): string | null {
|
|
104
|
+
const locations = [
|
|
105
|
+
join(projectDir, ".opencode", "skill", skillName, "SKILL.md"),
|
|
106
|
+
join(homedir(), ".config", "opencode", "skill", skillName, "SKILL.md"),
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
for (const loc of locations) {
|
|
110
|
+
if (existsSync(loc)) return loc;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const SkillMcpPlugin: Plugin = async ({ directory }) => {
|
|
116
|
+
const state: SkillMcpState = {
|
|
117
|
+
clients: new Map(),
|
|
118
|
+
loadedSkills: new Map(),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
function getClientKey(skillName: string, serverName: string): string {
|
|
122
|
+
return `${skillName}:${serverName}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function sendRequest(
|
|
126
|
+
client: McpClient,
|
|
127
|
+
method: string,
|
|
128
|
+
params?: any,
|
|
129
|
+
): Promise<any> {
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const id = ++client.requestId;
|
|
132
|
+
const request = {
|
|
133
|
+
jsonrpc: "2.0",
|
|
134
|
+
id,
|
|
135
|
+
method,
|
|
136
|
+
params: params || {},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
client.pendingRequests.set(id, { resolve, reject });
|
|
140
|
+
|
|
141
|
+
const timeout = setTimeout(() => {
|
|
142
|
+
client.pendingRequests.delete(id);
|
|
143
|
+
reject(new Error(`Request timeout: ${method}`));
|
|
144
|
+
}, 30000);
|
|
145
|
+
|
|
146
|
+
client.pendingRequests.set(id, {
|
|
147
|
+
resolve: (v) => {
|
|
148
|
+
clearTimeout(timeout);
|
|
149
|
+
resolve(v);
|
|
150
|
+
},
|
|
151
|
+
reject: (e) => {
|
|
152
|
+
clearTimeout(timeout);
|
|
153
|
+
reject(e);
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
client.process.stdin?.write(JSON.stringify(request) + "\n");
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function connectServer(
|
|
162
|
+
skillName: string,
|
|
163
|
+
serverName: string,
|
|
164
|
+
config: McpServerConfig,
|
|
165
|
+
): Promise<McpClient> {
|
|
166
|
+
const key = getClientKey(skillName, serverName);
|
|
167
|
+
|
|
168
|
+
// Return existing client if connected
|
|
169
|
+
const existing = state.clients.get(key);
|
|
170
|
+
if (existing && !existing.process.killed) {
|
|
171
|
+
return existing;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Spawn MCP server process
|
|
175
|
+
const proc = spawn(config.command, config.args || [], {
|
|
176
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
177
|
+
env: { ...process.env, ...config.env },
|
|
178
|
+
shell: true,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const client: McpClient = {
|
|
182
|
+
process: proc,
|
|
183
|
+
config,
|
|
184
|
+
requestId: 0,
|
|
185
|
+
pendingRequests: new Map(),
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// Handle stdout (JSON-RPC responses)
|
|
189
|
+
let buffer = "";
|
|
190
|
+
proc.stdout?.on("data", (data) => {
|
|
191
|
+
buffer += data.toString();
|
|
192
|
+
const lines = buffer.split("\n");
|
|
193
|
+
buffer = lines.pop() || "";
|
|
194
|
+
|
|
195
|
+
for (const line of lines) {
|
|
196
|
+
if (!line.trim()) continue;
|
|
197
|
+
try {
|
|
198
|
+
const response = JSON.parse(line);
|
|
199
|
+
if (response.id !== undefined) {
|
|
200
|
+
const pending = client.pendingRequests.get(response.id);
|
|
201
|
+
if (pending) {
|
|
202
|
+
client.pendingRequests.delete(response.id);
|
|
203
|
+
if (response.error) {
|
|
204
|
+
pending.reject(new Error(response.error.message));
|
|
205
|
+
} else {
|
|
206
|
+
pending.resolve(response.result);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} catch {}
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
proc.on("error", (err) => {
|
|
215
|
+
console.error(`MCP server error [${key}]:`, err.message);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
proc.on("exit", (code) => {
|
|
219
|
+
state.clients.delete(key);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
state.clients.set(key, client);
|
|
223
|
+
|
|
224
|
+
// Initialize connection
|
|
225
|
+
try {
|
|
226
|
+
await sendRequest(client, "initialize", {
|
|
227
|
+
protocolVersion: "2024-11-05",
|
|
228
|
+
capabilities: {},
|
|
229
|
+
clientInfo: { name: "opencode-skill-mcp", version: "1.0.0" },
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Send initialized notification
|
|
233
|
+
proc.stdin?.write(
|
|
234
|
+
JSON.stringify({
|
|
235
|
+
jsonrpc: "2.0",
|
|
236
|
+
method: "notifications/initialized",
|
|
237
|
+
}) + "\n",
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// Discover capabilities
|
|
241
|
+
try {
|
|
242
|
+
const toolsResult = await sendRequest(client, "tools/list", {});
|
|
243
|
+
client.capabilities = { tools: toolsResult.tools || [] };
|
|
244
|
+
} catch {
|
|
245
|
+
client.capabilities = { tools: [] };
|
|
246
|
+
}
|
|
247
|
+
} catch (e: any) {
|
|
248
|
+
proc.kill();
|
|
249
|
+
state.clients.delete(key);
|
|
250
|
+
throw new Error(`Failed to initialize MCP server: ${e.message}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return client;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function disconnectAll() {
|
|
257
|
+
for (const [key, client] of state.clients) {
|
|
258
|
+
client.process.kill();
|
|
259
|
+
}
|
|
260
|
+
state.clients.clear();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
tool: {
|
|
265
|
+
skill_mcp: tool({
|
|
266
|
+
description: `Invoke MCP tools from skill-embedded MCP servers.
|
|
267
|
+
|
|
268
|
+
When a skill declares MCP servers in its YAML frontmatter, use this tool to:
|
|
269
|
+
- List available tools: skill_mcp(skill_name="playwright", list_tools=true)
|
|
270
|
+
- Call a tool: skill_mcp(skill_name="playwright", tool_name="browser_navigate", arguments='{"url": "..."}')
|
|
271
|
+
|
|
272
|
+
The skill must be loaded first via the skill() tool to register its MCP config.`,
|
|
273
|
+
args: {
|
|
274
|
+
skill_name: tool.schema
|
|
275
|
+
.string()
|
|
276
|
+
.describe("Name of the loaded skill with MCP config"),
|
|
277
|
+
mcp_name: tool.schema
|
|
278
|
+
.string()
|
|
279
|
+
.optional()
|
|
280
|
+
.describe("Specific MCP server name (if skill has multiple)"),
|
|
281
|
+
list_tools: tool.schema
|
|
282
|
+
.boolean()
|
|
283
|
+
.optional()
|
|
284
|
+
.describe("List available tools from this MCP"),
|
|
285
|
+
tool_name: tool.schema
|
|
286
|
+
.string()
|
|
287
|
+
.optional()
|
|
288
|
+
.describe("MCP tool to invoke"),
|
|
289
|
+
arguments: tool.schema
|
|
290
|
+
.string()
|
|
291
|
+
.optional()
|
|
292
|
+
.describe("JSON string of tool arguments"),
|
|
293
|
+
},
|
|
294
|
+
async execute(args) {
|
|
295
|
+
const {
|
|
296
|
+
skill_name,
|
|
297
|
+
mcp_name,
|
|
298
|
+
list_tools,
|
|
299
|
+
tool_name,
|
|
300
|
+
arguments: argsJson,
|
|
301
|
+
} = args;
|
|
302
|
+
|
|
303
|
+
if (!skill_name) {
|
|
304
|
+
return JSON.stringify({ error: "skill_name required" });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Find skill and parse its MCP config
|
|
308
|
+
const skillPath = findSkillPath(skill_name, directory);
|
|
309
|
+
if (!skillPath) {
|
|
310
|
+
return JSON.stringify({ error: `Skill '${skill_name}' not found` });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const content = readFileSync(skillPath, "utf-8");
|
|
314
|
+
const { frontmatter } = parseYamlFrontmatter(content);
|
|
315
|
+
|
|
316
|
+
if (!frontmatter.mcp || Object.keys(frontmatter.mcp).length === 0) {
|
|
317
|
+
return JSON.stringify({
|
|
318
|
+
error: `Skill '${skill_name}' has no MCP config`,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Determine which MCP server to use
|
|
323
|
+
const mcpServers = frontmatter.mcp as Record<string, McpServerConfig>;
|
|
324
|
+
const serverNames = Object.keys(mcpServers);
|
|
325
|
+
const targetServer = mcp_name || serverNames[0];
|
|
326
|
+
|
|
327
|
+
if (!mcpServers[targetServer]) {
|
|
328
|
+
return JSON.stringify({
|
|
329
|
+
error: `MCP server '${targetServer}' not found in skill`,
|
|
330
|
+
available: serverNames,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const serverConfig = mcpServers[targetServer];
|
|
335
|
+
|
|
336
|
+
// Connect to MCP server
|
|
337
|
+
let client: McpClient;
|
|
338
|
+
try {
|
|
339
|
+
client = await connectServer(
|
|
340
|
+
skill_name,
|
|
341
|
+
targetServer,
|
|
342
|
+
serverConfig,
|
|
343
|
+
);
|
|
344
|
+
} catch (e: any) {
|
|
345
|
+
return JSON.stringify({ error: `Failed to connect: ${e.message}` });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// List tools
|
|
349
|
+
if (list_tools) {
|
|
350
|
+
return JSON.stringify(
|
|
351
|
+
{
|
|
352
|
+
mcp: targetServer,
|
|
353
|
+
tools:
|
|
354
|
+
client.capabilities?.tools?.map((t: any) => ({
|
|
355
|
+
name: t.name,
|
|
356
|
+
description: t.description,
|
|
357
|
+
schema: t.inputSchema,
|
|
358
|
+
})) || [],
|
|
359
|
+
},
|
|
360
|
+
null,
|
|
361
|
+
2,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Call tool
|
|
366
|
+
if (tool_name) {
|
|
367
|
+
let toolArgs = {};
|
|
368
|
+
if (argsJson) {
|
|
369
|
+
try {
|
|
370
|
+
toolArgs = JSON.parse(argsJson);
|
|
371
|
+
} catch (e) {
|
|
372
|
+
return JSON.stringify({ error: "Invalid JSON in arguments" });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
const result = await sendRequest(client, "tools/call", {
|
|
378
|
+
name: tool_name,
|
|
379
|
+
arguments: toolArgs,
|
|
380
|
+
});
|
|
381
|
+
return JSON.stringify({ result }, null, 2);
|
|
382
|
+
} catch (e: any) {
|
|
383
|
+
return JSON.stringify({
|
|
384
|
+
error: `Tool call failed: ${e.message}`,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return JSON.stringify({
|
|
390
|
+
error: "Specify either list_tools=true or tool_name to call",
|
|
391
|
+
mcp: targetServer,
|
|
392
|
+
available_tools:
|
|
393
|
+
client.capabilities?.tools?.map((t: any) => t.name) || [],
|
|
394
|
+
});
|
|
395
|
+
},
|
|
396
|
+
}),
|
|
397
|
+
|
|
398
|
+
skill_mcp_status: tool({
|
|
399
|
+
description: "Show status of connected MCP servers from skills.",
|
|
400
|
+
args: {},
|
|
401
|
+
async execute() {
|
|
402
|
+
const servers: any[] = [];
|
|
403
|
+
for (const [key, client] of state.clients) {
|
|
404
|
+
const [skillName, serverName] = key.split(":");
|
|
405
|
+
servers.push({
|
|
406
|
+
skill: skillName,
|
|
407
|
+
server: serverName,
|
|
408
|
+
connected: !client.process.killed,
|
|
409
|
+
tools: client.capabilities?.tools?.length || 0,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return JSON.stringify({
|
|
413
|
+
connected_servers: servers,
|
|
414
|
+
count: servers.length,
|
|
415
|
+
});
|
|
416
|
+
},
|
|
417
|
+
}),
|
|
418
|
+
|
|
419
|
+
skill_mcp_disconnect: tool({
|
|
420
|
+
description:
|
|
421
|
+
"Disconnect MCP servers. Use when done with browser automation etc.",
|
|
422
|
+
args: {
|
|
423
|
+
skill_name: tool.schema
|
|
424
|
+
.string()
|
|
425
|
+
.optional()
|
|
426
|
+
.describe("Specific skill to disconnect (all if omitted)"),
|
|
427
|
+
},
|
|
428
|
+
async execute(args) {
|
|
429
|
+
if (args.skill_name) {
|
|
430
|
+
const toDisconnect: string[] = [];
|
|
431
|
+
for (const key of state.clients.keys()) {
|
|
432
|
+
if (key.startsWith(args.skill_name + ":")) {
|
|
433
|
+
toDisconnect.push(key);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
for (const key of toDisconnect) {
|
|
437
|
+
const client = state.clients.get(key);
|
|
438
|
+
client?.process.kill();
|
|
439
|
+
state.clients.delete(key);
|
|
440
|
+
}
|
|
441
|
+
return JSON.stringify({ disconnected: toDisconnect });
|
|
442
|
+
} else {
|
|
443
|
+
const count = state.clients.size;
|
|
444
|
+
disconnectAll();
|
|
445
|
+
return JSON.stringify({ disconnected: "all", count });
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
}),
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
event: async ({ event }) => {
|
|
452
|
+
// Cleanup on session idle (closest available event)
|
|
453
|
+
if (event.type === "session.idle") {
|
|
454
|
+
// Optional: could disconnect idle servers here
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
};
|