opencodekit 0.8.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 +5 -6
- 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/handoff.md +8 -12
- package/dist/template/.opencode/command/research.md +10 -10
- package/dist/template/.opencode/command/status.md +4 -6
- package/dist/template/.opencode/opencode.json +1 -23
- package/dist/template/.opencode/plugin/skill-mcp.ts +458 -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,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,214 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: figma
|
|
3
|
+
description: Access Figma design data via Framelink MCP. Fetch layout, styles, components from Figma files/frames. Use when implementing UI from Figma designs, extracting design tokens, or downloading assets.
|
|
4
|
+
mcp:
|
|
5
|
+
figma:
|
|
6
|
+
command: npx
|
|
7
|
+
args: ["-y", "figma-developer-mcp", "--stdio"]
|
|
8
|
+
env:
|
|
9
|
+
FIGMA_API_KEY: "${FIGMA_API_KEY}"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Figma Design Data (MCP)
|
|
13
|
+
|
|
14
|
+
Access Figma design data via the Framelink MCP server. When this skill is loaded, the `figma` MCP server auto-starts and exposes tools to fetch design data and download images.
|
|
15
|
+
|
|
16
|
+
## Prerequisites
|
|
17
|
+
|
|
18
|
+
Set your Figma API key as an environment variable:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
export FIGMA_API_KEY="your-figma-personal-access-token"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
To create a Figma Personal Access Token:
|
|
25
|
+
|
|
26
|
+
1. Go to Figma → Settings → Account → Personal access tokens
|
|
27
|
+
2. Create a new token with read access
|
|
28
|
+
3. See: https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
After loading this skill, use `skill_mcp` to invoke Figma tools:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
skill_mcp(skill_name="figma", tool_name="get_figma_data", arguments='{"fileKey": "abc123", "nodeId": "1234:5678"}')
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Available Tools
|
|
39
|
+
|
|
40
|
+
### get_figma_data
|
|
41
|
+
|
|
42
|
+
Fetch comprehensive Figma file data including layout, content, visuals, and component information.
|
|
43
|
+
|
|
44
|
+
| Parameter | Type | Required | Description |
|
|
45
|
+
| --------- | ------ | -------- | ------------------------------------------------------------------------------------------------- |
|
|
46
|
+
| `fileKey` | string | Yes | The Figma file key (from URL: `figma.com/file/<fileKey>/...` or `figma.com/design/<fileKey>/...`) |
|
|
47
|
+
| `nodeId` | string | No | Specific node ID (from URL param `node-id=<nodeId>`). Format: `1234:5678` or `1234-5678` |
|
|
48
|
+
| `depth` | number | No | Levels deep to traverse. Only use if explicitly requested by user. |
|
|
49
|
+
|
|
50
|
+
**Returns:** YAML-formatted design data with:
|
|
51
|
+
|
|
52
|
+
- `metadata` - File/node information
|
|
53
|
+
- `nodes` - Simplified node tree with layout, styles, text content
|
|
54
|
+
- `globalVars` - Shared styles and variables
|
|
55
|
+
|
|
56
|
+
### download_figma_images
|
|
57
|
+
|
|
58
|
+
Download SVG and PNG images/icons from a Figma file.
|
|
59
|
+
|
|
60
|
+
| Parameter | Type | Required | Description |
|
|
61
|
+
| ----------- | ------ | -------- | --------------------------------------------- |
|
|
62
|
+
| `fileKey` | string | Yes | The Figma file key |
|
|
63
|
+
| `nodes` | array | Yes | Array of node objects to download (see below) |
|
|
64
|
+
| `localPath` | string | Yes | Absolute path to save images |
|
|
65
|
+
| `pngScale` | number | No | Export scale for PNGs (default: 2) |
|
|
66
|
+
|
|
67
|
+
**Node object structure:**
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"nodeId": "1234:5678",
|
|
72
|
+
"fileName": "icon-name.svg",
|
|
73
|
+
"imageRef": "optional-for-image-fills"
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Workflow
|
|
78
|
+
|
|
79
|
+
### 1. Extract File Key and Node ID from Figma URL
|
|
80
|
+
|
|
81
|
+
Figma URLs follow these patterns:
|
|
82
|
+
|
|
83
|
+
- `https://www.figma.com/file/<fileKey>/<fileName>?node-id=<nodeId>`
|
|
84
|
+
- `https://www.figma.com/design/<fileKey>/<fileName>?node-id=<nodeId>`
|
|
85
|
+
|
|
86
|
+
Example: `https://www.figma.com/design/abc123xyz/MyDesign?node-id=1234-5678`
|
|
87
|
+
|
|
88
|
+
- `fileKey`: `abc123xyz`
|
|
89
|
+
- `nodeId`: `1234-5678` (or `1234:5678` - both formats work)
|
|
90
|
+
|
|
91
|
+
### 2. Fetch Design Data
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
# Fetch specific frame/component
|
|
95
|
+
skill_mcp(skill_name="figma", tool_name="get_figma_data", arguments='{"fileKey": "abc123xyz", "nodeId": "1234:5678"}')
|
|
96
|
+
|
|
97
|
+
# Fetch entire file (use sparingly - can be large)
|
|
98
|
+
skill_mcp(skill_name="figma", tool_name="get_figma_data", arguments='{"fileKey": "abc123xyz"}')
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 3. Download Assets (Optional)
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
skill_mcp(skill_name="figma", tool_name="download_figma_images", arguments='{
|
|
105
|
+
"fileKey": "abc123xyz",
|
|
106
|
+
"nodes": [
|
|
107
|
+
{"nodeId": "1234:5678", "fileName": "hero-image.png"},
|
|
108
|
+
{"nodeId": "5678:9012", "fileName": "icon-arrow.svg"}
|
|
109
|
+
],
|
|
110
|
+
"localPath": "/absolute/path/to/assets"
|
|
111
|
+
}')
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Examples
|
|
115
|
+
|
|
116
|
+
### Implement a Component from Figma
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
# User provides: https://www.figma.com/design/abc123/Dashboard?node-id=100-200
|
|
120
|
+
|
|
121
|
+
# 1. Fetch the design data
|
|
122
|
+
skill_mcp(skill_name="figma", tool_name="get_figma_data", arguments='{"fileKey": "abc123", "nodeId": "100-200"}')
|
|
123
|
+
|
|
124
|
+
# 2. Review the returned YAML for:
|
|
125
|
+
# - Layout structure (flex, grid, spacing)
|
|
126
|
+
# - Typography (font, size, weight, line-height)
|
|
127
|
+
# - Colors (fills, strokes)
|
|
128
|
+
# - Dimensions and constraints
|
|
129
|
+
|
|
130
|
+
# 3. Implement the component using the extracted data
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Extract Design Tokens
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
# Fetch a design system file
|
|
137
|
+
skill_mcp(skill_name="figma", tool_name="get_figma_data", arguments='{"fileKey": "designSystemKey"}')
|
|
138
|
+
|
|
139
|
+
# The globalVars section contains:
|
|
140
|
+
# - Color styles
|
|
141
|
+
# - Typography styles
|
|
142
|
+
# - Effect styles (shadows, blurs)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Download Icons
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
skill_mcp(skill_name="figma", tool_name="download_figma_images", arguments='{
|
|
149
|
+
"fileKey": "iconLibraryKey",
|
|
150
|
+
"nodes": [
|
|
151
|
+
{"nodeId": "10:20", "fileName": "icon-home.svg"},
|
|
152
|
+
{"nodeId": "10:30", "fileName": "icon-settings.svg"},
|
|
153
|
+
{"nodeId": "10:40", "fileName": "icon-user.svg"}
|
|
154
|
+
],
|
|
155
|
+
"localPath": "/project/src/assets/icons",
|
|
156
|
+
"pngScale": 2
|
|
157
|
+
}')
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Data Structure
|
|
161
|
+
|
|
162
|
+
The `get_figma_data` tool returns simplified, AI-optimized data:
|
|
163
|
+
|
|
164
|
+
```yaml
|
|
165
|
+
metadata:
|
|
166
|
+
name: "Component Name"
|
|
167
|
+
lastModified: "2024-01-15T..."
|
|
168
|
+
|
|
169
|
+
nodes:
|
|
170
|
+
- id: "1234:5678"
|
|
171
|
+
name: "Button"
|
|
172
|
+
type: "FRAME"
|
|
173
|
+
layout:
|
|
174
|
+
mode: "HORIZONTAL"
|
|
175
|
+
padding: { top: 12, right: 24, bottom: 12, left: 24 }
|
|
176
|
+
gap: 8
|
|
177
|
+
size: { width: 120, height: 48 }
|
|
178
|
+
fills:
|
|
179
|
+
- type: "SOLID"
|
|
180
|
+
color: { r: 0.2, g: 0.4, b: 1, a: 1 }
|
|
181
|
+
cornerRadius: 8
|
|
182
|
+
children:
|
|
183
|
+
- id: "1234:5679"
|
|
184
|
+
name: "Label"
|
|
185
|
+
type: "TEXT"
|
|
186
|
+
content: "Click me"
|
|
187
|
+
textStyle:
|
|
188
|
+
fontFamily: "Inter"
|
|
189
|
+
fontSize: 16
|
|
190
|
+
fontWeight: 600
|
|
191
|
+
|
|
192
|
+
globalVars:
|
|
193
|
+
styles:
|
|
194
|
+
"S:abc123": { name: "Primary", fills: [...] }
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Tips
|
|
198
|
+
|
|
199
|
+
- **Always use nodeId** when provided - fetching entire files is slow and context-heavy
|
|
200
|
+
- **Node IDs with `-` or `:`** both work - the MCP handles conversion
|
|
201
|
+
- **Check globalVars** for reusable styles before hardcoding values
|
|
202
|
+
- **Use depth parameter sparingly** - only when explicitly needed
|
|
203
|
+
- **For images/icons**, identify nodes with `imageRef` in the data for proper downloading
|
|
204
|
+
- **SVG vs PNG**: Use `.svg` for icons/vectors, `.png` for photos/complex images
|
|
205
|
+
|
|
206
|
+
## Troubleshooting
|
|
207
|
+
|
|
208
|
+
**"Invalid API key"**: Ensure `FIGMA_API_KEY` environment variable is set correctly.
|
|
209
|
+
|
|
210
|
+
**"File not found"**: Verify the fileKey is correct and you have access to the file.
|
|
211
|
+
|
|
212
|
+
**"Node not found"**: Check the nodeId format. Try both `1234:5678` and `1234-5678`.
|
|
213
|
+
|
|
214
|
+
**Large response**: Use `nodeId` to fetch specific frames instead of entire files.
|