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