opencodekit 0.8.0 → 0.9.1

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.
@@ -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
+ };
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: chrome-devtools
3
+ description: Chrome DevTools for debugging, performance analysis, and browser automation. Use when debugging web apps, analyzing performance, inspecting network requests, or automating browser interactions.
4
+ mcp:
5
+ chrome-devtools:
6
+ command: npx
7
+ args: ["-y", "chrome-devtools-mcp@latest", "--stdio"]
8
+ ---
9
+
10
+ # Chrome DevTools (MCP)
11
+
12
+ Control and inspect a live Chrome browser via Chrome DevTools Protocol. Debug, analyze performance, inspect network, and automate browser interactions.
13
+
14
+ ## Quick Start
15
+
16
+ ```
17
+ skill_mcp(skill_name="chrome-devtools", tool_name="take_snapshot")
18
+ skill_mcp(skill_name="chrome-devtools", tool_name="navigate_page", arguments='{"type": "url", "url": "https://example.com"}')
19
+ ```
20
+
21
+ ## Tools
22
+
23
+ ### Input
24
+
25
+ | Tool | Description | Parameters |
26
+ | --------------- | -------------------- | ---------------------------- |
27
+ | `click` | Click element | `uid` |
28
+ | `fill` | Type text | `uid`, `value` |
29
+ | `fill_form` | Fill multiple fields | `elements` array |
30
+ | `hover` | Hover element | `uid` |
31
+ | `press_key` | Press key | `key` (e.g., "Enter") |
32
+ | `drag` | Drag element | `from_uid`, `to_uid` |
33
+ | `upload_file` | Upload file | `uid`, `filePath` |
34
+ | `handle_dialog` | Handle dialog | `action`: "accept"/"dismiss" |
35
+
36
+ ### Navigation
37
+
38
+ | Tool | Description | Parameters |
39
+ | --------------- | ------------- | ---------------------------------------------- |
40
+ | `navigate_page` | Navigate | `type`: "url"/"back"/"forward"/"reload", `url` |
41
+ | `new_page` | Open new page | `url` |
42
+ | `list_pages` | List pages | - |
43
+ | `select_page` | Switch page | `pageIdx` |
44
+ | `close_page` | Close page | `pageIdx` |
45
+ | `wait_for` | Wait for text | `text`, `timeout` |
46
+
47
+ ### Debugging
48
+
49
+ | Tool | Description | Parameters |
50
+ | ----------------------- | ------------------ | --------------------------- |
51
+ | `take_snapshot` | A11y tree snapshot | `verbose` |
52
+ | `take_screenshot` | Screenshot | `uid`, `fullPage`, `format` |
53
+ | `evaluate_script` | Run JS | `function`, `args` |
54
+ | `list_console_messages` | Console logs | `types` filter |
55
+ | `get_console_message` | Get message | `msgid` |
56
+
57
+ ### Network
58
+
59
+ | Tool | Description | Parameters |
60
+ | ----------------------- | --------------- | --------------------------- |
61
+ | `list_network_requests` | List requests | `resourceTypes`, `pageSize` |
62
+ | `get_network_request` | Request details | `reqid` |
63
+
64
+ ### Performance
65
+
66
+ | Tool | Description | Parameters |
67
+ | ----------------------------- | ----------- | ----------------------------- |
68
+ | `performance_start_trace` | Start trace | `reload`, `autoStop` |
69
+ | `performance_stop_trace` | Stop trace | - |
70
+ | `performance_analyze_insight` | Analyze | `insightSetId`, `insightName` |
71
+
72
+ ### Emulation
73
+
74
+ | Tool | Description | Parameters |
75
+ | ------------- | ------------------ | ---------------------------------------- |
76
+ | `emulate` | Emulate conditions | `networkConditions`, `cpuThrottlingRate` |
77
+ | `resize_page` | Resize viewport | `width`, `height` |
78
+
79
+ ## Tips
80
+
81
+ - **Always `take_snapshot` first** to get element `uid`s
82
+ - **Element uids change** after navigation - take fresh snapshot
83
+ - **Network conditions**: "Slow 3G", "Fast 3G", "Offline"
84
+
85
+ ## vs Playwright
86
+
87
+ - **chrome-devtools**: Performance profiling, network inspection, console - Chrome only
88
+ - **playwright**: Cross-browser testing - Chrome, Firefox, WebKit