@rui.branco/figma-mcp 1.0.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 (4) hide show
  1. package/README.md +196 -0
  2. package/index.js +378 -0
  3. package/package.json +34 -0
  4. package/setup.js +69 -0
package/README.md ADDED
@@ -0,0 +1,196 @@
1
+ # Figma MCP Server
2
+
3
+ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that brings Figma designs directly into Claude Code. View design specifications, export frames as images, and reference visual context without leaving your development environment.
4
+
5
+ ## Overview
6
+
7
+ Implementing designs accurately requires constant reference to Figma. This MCP server eliminates context switching by:
8
+
9
+ - **Fetching design information** - Get file names, frame details, and dimensions
10
+ - **Exporting images** - Download frames as high-resolution PNGs
11
+ - **Smart section splitting** - Large frames are automatically split into readable sections
12
+ - **Seamless integration** - Works standalone or integrated with [jira-mcp](https://github.com/rui-branco/jira-mcp)
13
+
14
+ ## Features
15
+
16
+ | Feature | Description |
17
+ |---------|-------------|
18
+ | Design Info | File name, last modified, frame dimensions |
19
+ | Image Export | Export frames as PNG, SVG, JPG, or PDF |
20
+ | Auto-Scaling | 2x scale by default for retina clarity |
21
+ | Section Detection | Large frames split into individual sections |
22
+ | Batch Export | Export multiple frames efficiently |
23
+ | URL Parsing | Supports file, design, and prototype URLs |
24
+
25
+ ## Installation
26
+
27
+ ### Prerequisites
28
+
29
+ - Node.js 18+
30
+ - [Claude Code](https://claude.ai/code) CLI
31
+ - Figma account with API access
32
+
33
+ ### Step 1: Add to Claude Code
34
+
35
+ ```bash
36
+ claude mcp add --transport stdio figma -- npx -y @rui.branco/figma-mcp
37
+ ```
38
+
39
+ ### Step 2: Configure Credentials
40
+
41
+ Run the setup to configure your Figma token:
42
+
43
+ ```bash
44
+ npx @rui.branco/figma-mcp setup "YOUR_FIGMA_TOKEN"
45
+ ```
46
+
47
+ Or run interactively:
48
+
49
+ ```bash
50
+ npx @rui.branco/figma-mcp setup
51
+ ```
52
+
53
+ To get your **Personal Access Token**:
54
+ 1. Go to [Figma Settings](https://www.figma.com/settings)
55
+ 2. Scroll to "Personal access tokens"
56
+ 3. Click "Generate new token"
57
+ 4. Copy and paste the token
58
+
59
+ ### Step 3: Verify
60
+
61
+ Restart Claude Code and run `/mcp` to verify the server is loaded.
62
+
63
+ ### Alternative: Manual Installation
64
+
65
+ If you prefer to install manually:
66
+
67
+ ```bash
68
+ git clone https://github.com/rui-branco/figma-mcp.git ~/.config/figma-mcp
69
+ cd ~/.config/figma-mcp && npm install
70
+ node setup.js
71
+ ```
72
+
73
+ Then add to Claude Code:
74
+
75
+ ```bash
76
+ claude mcp add --transport stdio figma -- node $HOME/.config/figma-mcp/index.js
77
+ ```
78
+
79
+ ## Usage
80
+
81
+ ### Fetch a Design
82
+
83
+ ```
84
+ > Get this Figma design: https://www.figma.com/design/ABC123/MyProject?node-id=1-234
85
+ ```
86
+
87
+ ### Example Output
88
+
89
+ ```
90
+ # Figma File: MyProject
91
+
92
+ **Last Modified:** 2025-01-15T10:30:00Z
93
+
94
+ ## Selected Frame: Login Screen
95
+
96
+ **Type:** FRAME
97
+ **Size:** 1440 x 900
98
+
99
+ ### Sections (4):
100
+ - **Header** (FRAME, 1440x80)
101
+ - **Login Form** (FRAME, 400x350)
102
+ - **Footer** (FRAME, 1440x60)
103
+ - **Background** (FRAME, 1440x900)
104
+
105
+ ### Exported Sections:
106
+ - Header: ~/.config/figma-mcp/exports/ABC123_1_234.png
107
+ - Login Form: ~/.config/figma-mcp/exports/ABC123_1_235.png
108
+ - Footer: ~/.config/figma-mcp/exports/ABC123_1_236.png
109
+
110
+ [Images displayed inline]
111
+ ```
112
+
113
+ ## Smart Section Export
114
+
115
+ For large frames (>1500px wide or >2000px tall), the server automatically:
116
+
117
+ 1. Detects child frames, components, and groups
118
+ 2. Exports each section separately at 2x scale
119
+ 3. Provides better detail than a single compressed image
120
+ 4. Limits to 10 sections by default (configurable)
121
+
122
+ This is especially useful for:
123
+ - Full-page designs with multiple sections
124
+ - Component libraries
125
+ - Design system documentation
126
+
127
+ ## API Reference
128
+
129
+ ### Tools
130
+
131
+ | Tool | Description | Parameters |
132
+ |------|-------------|------------|
133
+ | `figma_get_design` | Fetch design from URL | `url` (required), `exportImage`, `exportChildren`, `maxChildren`, `scale` |
134
+ | `figma_export_frame` | Export specific frame | `fileKey`, `nodeId` (required), `format`, `scale` |
135
+
136
+ ### Parameters
137
+
138
+ | Parameter | Default | Description |
139
+ |-----------|---------|-------------|
140
+ | `exportImage` | `true` | Export images |
141
+ | `exportChildren` | `true` | Split large frames into sections |
142
+ | `maxChildren` | `10` | Maximum sections to export |
143
+ | `scale` | `2` | Export scale (1-4) |
144
+ | `format` | `png` | Export format (png, svg, jpg, pdf) |
145
+
146
+ ### Configuration
147
+
148
+ Config stored at `~/.config/figma-mcp/config.json`:
149
+
150
+ ```json
151
+ {
152
+ "token": "YOUR_FIGMA_TOKEN"
153
+ }
154
+ ```
155
+
156
+ ## Error Handling
157
+
158
+ The server provides clear error messages:
159
+
160
+ | Error | Meaning | Solution |
161
+ |-------|---------|----------|
162
+ | `Rate limit exceeded` | Too many API requests | Wait a few minutes |
163
+ | `Access denied` | Invalid token or no file access | Check token permissions |
164
+ | `File not found` | Invalid URL or deleted file | Verify the Figma URL |
165
+
166
+ ## Integration with Jira MCP
167
+
168
+ When used alongside [jira-mcp](https://github.com/rui-branco/jira-mcp):
169
+
170
+ 1. Figma links in Jira tickets are **automatically detected**
171
+ 2. Designs are **fetched without manual intervention**
172
+ 3. Images appear **inline with ticket context**
173
+
174
+ This creates a seamless workflow:
175
+ ```
176
+ > Get ticket PROJ-123
177
+
178
+ # Returns ticket details + auto-fetched Figma designs
179
+ ```
180
+
181
+ ## Security
182
+
183
+ - Tokens stored locally in `~/.config/figma-mcp/config.json`
184
+ - Config excluded from git via `.gitignore`
185
+ - Tokens only transmitted to Figma API
186
+ - Exports saved to `~/.config/figma-mcp/exports/`
187
+
188
+ ## License
189
+
190
+ MIT
191
+
192
+ ## Related
193
+
194
+ - [jira-mcp](https://github.com/rui-branco/jira-mcp) - Jira MCP server with Figma integration
195
+ - [Model Context Protocol](https://modelcontextprotocol.io/) - MCP specification
196
+ - [Figma API](https://www.figma.com/developers/api) - Figma REST API documentation
package/index.js ADDED
@@ -0,0 +1,378 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Handle setup command
4
+ if (process.argv[2] === "setup") {
5
+ require("./setup.js");
6
+ return;
7
+ }
8
+
9
+ const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
10
+ const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
11
+ const {
12
+ CallToolRequestSchema,
13
+ ListToolsRequestSchema,
14
+ } = require("@modelcontextprotocol/sdk/types.js");
15
+ const fs = require("fs");
16
+ const path = require("path");
17
+ const fetch = require("node-fetch");
18
+
19
+ // Load config
20
+ const configPath = path.join(process.env.HOME, ".config/figma-mcp/config.json");
21
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
22
+
23
+ const FIGMA_API = "https://api.figma.com/v1";
24
+ const exportsDir = path.join(process.env.HOME, ".config/figma-mcp/exports");
25
+
26
+ if (!fs.existsSync(exportsDir)) {
27
+ fs.mkdirSync(exportsDir, { recursive: true });
28
+ }
29
+
30
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
31
+
32
+ async function figmaFetch(endpoint, { maxRetries = 3, maxWaitSec = 30 } = {}) {
33
+ let attempts = 0;
34
+
35
+ while (true) {
36
+ const response = await fetch(`${FIGMA_API}${endpoint}`, {
37
+ headers: { "X-Figma-Token": config.token },
38
+ });
39
+
40
+ if (response.ok) {
41
+ return response.json();
42
+ }
43
+
44
+ if (response.status === 429) {
45
+ const retryAfterSec = Number(response.headers.get("retry-after")) || 60;
46
+
47
+ // Don't retry if wait is too long (monthly limit) or too many attempts
48
+ if (retryAfterSec > maxWaitSec || attempts++ >= maxRetries) {
49
+ const waitTime = retryAfterSec > 3600
50
+ ? `${Math.round(retryAfterSec / 3600)} hours (monthly limit reached)`
51
+ : `${retryAfterSec} seconds`;
52
+ throw new Error(`Figma API rate limit exceeded. Try again in ${waitTime}.`);
53
+ }
54
+
55
+ await sleep(retryAfterSec * 1000);
56
+ continue;
57
+ }
58
+
59
+ if (response.status === 403) {
60
+ throw new Error("Figma access denied. Check your token or file permissions.");
61
+ }
62
+ if (response.status === 404) {
63
+ throw new Error("Figma file not found. Check the URL.");
64
+ }
65
+ const text = await response.text();
66
+ throw new Error(`Figma API error: ${response.status} - ${text}`);
67
+ }
68
+ }
69
+
70
+ function normalizeNodeId(nodeId) {
71
+ if (!nodeId) return null;
72
+ return nodeId.replace(/-/g, ":");
73
+ }
74
+
75
+ function parseFigmaUrl(url) {
76
+ const urlObj = new URL(url);
77
+ const pathParts = urlObj.pathname.split("/");
78
+
79
+ let fileKey = null;
80
+ for (let i = 0; i < pathParts.length; i++) {
81
+ if (pathParts[i] === "file" || pathParts[i] === "design" || pathParts[i] === "proto") {
82
+ fileKey = pathParts[i + 1];
83
+ break;
84
+ }
85
+ }
86
+
87
+ if (!fileKey) throw new Error("Could not extract file key from Figma URL");
88
+
89
+ const rawNodeId = urlObj.searchParams.get("node-id");
90
+ const nodeId = normalizeNodeId(rawNodeId);
91
+
92
+ return { fileKey, nodeId };
93
+ }
94
+
95
+ async function getFileInfo(fileKey) {
96
+ return await figmaFetch(`/files/${fileKey}?depth=1`);
97
+ }
98
+
99
+ async function getNodeInfo(fileKey, nodeId, depth = 2) {
100
+ return await figmaFetch(`/files/${fileKey}/nodes?ids=${encodeURIComponent(nodeId)}&depth=${depth}`);
101
+ }
102
+
103
+ async function exportImage(fileKey, nodeId, format = "png", scale = 2) {
104
+ const exportData = await figmaFetch(
105
+ `/images/${fileKey}?ids=${encodeURIComponent(nodeId)}&format=${format}&scale=${scale}`
106
+ );
107
+
108
+ if (exportData.err) throw new Error(`Export error: ${exportData.err}`);
109
+
110
+ const imageUrl = exportData.images[nodeId];
111
+ if (!imageUrl) throw new Error("No image URL returned");
112
+
113
+ const response = await fetch(imageUrl);
114
+ if (!response.ok) throw new Error(`Failed to download image: ${response.status}`);
115
+
116
+ const buffer = await response.buffer();
117
+
118
+ const sanitizedNodeId = nodeId.replace(/[^a-zA-Z0-9-]/g, "_");
119
+ const filename = `${fileKey}_${sanitizedNodeId}.${format}`;
120
+ const localPath = path.join(exportsDir, filename);
121
+ fs.writeFileSync(localPath, buffer);
122
+
123
+ return { localPath, buffer, nodeId };
124
+ }
125
+
126
+ // Export multiple nodes in one API call (more efficient)
127
+ async function exportMultipleImages(fileKey, nodeIds, format = "png", scale = 2) {
128
+ const idsParam = nodeIds.join(",");
129
+ const exportData = await figmaFetch(
130
+ `/images/${fileKey}?ids=${encodeURIComponent(idsParam)}&format=${format}&scale=${scale}`
131
+ );
132
+
133
+ if (exportData.err) throw new Error(`Export error: ${exportData.err}`);
134
+
135
+ const results = [];
136
+ for (const nodeId of nodeIds) {
137
+ const imageUrl = exportData.images[nodeId];
138
+ if (imageUrl) {
139
+ try {
140
+ const response = await fetch(imageUrl);
141
+ if (response.ok) {
142
+ const buffer = await response.buffer();
143
+ const sanitizedNodeId = nodeId.replace(/[^a-zA-Z0-9-]/g, "_");
144
+ const filename = `${fileKey}_${sanitizedNodeId}.${format}`;
145
+ const localPath = path.join(exportsDir, filename);
146
+ fs.writeFileSync(localPath, buffer);
147
+ results.push({ localPath, buffer, nodeId });
148
+ }
149
+ } catch (e) {
150
+ // Skip failed downloads
151
+ }
152
+ }
153
+ }
154
+ return results;
155
+ }
156
+
157
+ // Find exportable children (FRAME, COMPONENT, GROUP with reasonable size)
158
+ function findExportableChildren(doc, minSize = 100) {
159
+ const children = [];
160
+ if (!doc.children) return children;
161
+
162
+ for (const child of doc.children) {
163
+ const isExportable = ["FRAME", "COMPONENT", "COMPONENT_SET", "GROUP", "SECTION"].includes(child.type);
164
+ const bb = child.absoluteBoundingBox;
165
+ const hasSize = bb && bb.width >= minSize && bb.height >= minSize;
166
+
167
+ if (isExportable && hasSize) {
168
+ children.push({
169
+ id: child.id,
170
+ name: child.name,
171
+ type: child.type,
172
+ width: bb ? Math.round(bb.width) : 0,
173
+ height: bb ? Math.round(bb.height) : 0,
174
+ });
175
+ }
176
+ }
177
+ return children;
178
+ }
179
+
180
+ async function getFigmaDesign(url, options = {}) {
181
+ const {
182
+ exportImage: shouldExport = true,
183
+ exportChildren = true, // NEW: export child frames separately
184
+ maxChildren = 10, // NEW: limit number of children to export
185
+ scale = 2,
186
+ } = options;
187
+
188
+ const { fileKey, nodeId } = parseFigmaUrl(url);
189
+
190
+ let output = "";
191
+ let images = [];
192
+
193
+ const fileInfo = await getFileInfo(fileKey);
194
+ output += `# Figma File: ${fileInfo.name}\n\n`;
195
+ output += `**Last Modified:** ${fileInfo.lastModified}\n`;
196
+
197
+ if (nodeId) {
198
+ try {
199
+ const nodeData = await getNodeInfo(fileKey, nodeId, 2);
200
+ const node = nodeData.nodes[nodeId];
201
+
202
+ if (node && node.document) {
203
+ const doc = node.document;
204
+ output += `\n## Selected Frame: ${doc.name}\n\n`;
205
+ output += `**Type:** ${doc.type}\n`;
206
+
207
+ if (doc.absoluteBoundingBox) {
208
+ const bb = doc.absoluteBoundingBox;
209
+ output += `**Size:** ${Math.round(bb.width)} x ${Math.round(bb.height)}\n`;
210
+ }
211
+
212
+ // Find exportable children
213
+ const exportableChildren = findExportableChildren(doc);
214
+
215
+ if (exportableChildren.length > 0) {
216
+ output += `\n### Sections (${exportableChildren.length}):\n`;
217
+ for (const child of exportableChildren) {
218
+ output += `- **${child.name}** (${child.type}, ${child.width}x${child.height})\n`;
219
+ }
220
+ }
221
+
222
+ // Export logic
223
+ if (shouldExport) {
224
+ // If frame has exportable children and is large, export children instead
225
+ const isLargeFrame = doc.absoluteBoundingBox &&
226
+ (doc.absoluteBoundingBox.width > 1500 || doc.absoluteBoundingBox.height > 2000);
227
+
228
+ if (exportChildren && exportableChildren.length > 0 && isLargeFrame) {
229
+ output += `\n### Exported Sections:\n`;
230
+
231
+ // Export children (up to maxChildren)
232
+ const childrenToExport = exportableChildren.slice(0, maxChildren);
233
+ const nodeIds = childrenToExport.map(c => c.id);
234
+
235
+ try {
236
+ const exportedImages = await exportMultipleImages(fileKey, nodeIds, "png", scale);
237
+
238
+ for (const img of exportedImages) {
239
+ const childInfo = childrenToExport.find(c => c.id === img.nodeId);
240
+ output += `- ${childInfo?.name || img.nodeId}: ${img.localPath}\n`;
241
+ images.push({
242
+ path: img.localPath,
243
+ buffer: img.buffer,
244
+ name: childInfo?.name || img.nodeId
245
+ });
246
+ }
247
+
248
+ if (exportableChildren.length > maxChildren) {
249
+ output += `\n_(${exportableChildren.length - maxChildren} more sections not exported)_\n`;
250
+ }
251
+ } catch (e) {
252
+ output += `Export failed: ${e.message}\n`;
253
+ }
254
+ } else {
255
+ // Export the whole frame
256
+ output += `\n### Exported Image:\n`;
257
+ try {
258
+ const { localPath, buffer } = await exportImage(fileKey, nodeId, "png", scale);
259
+ output += `Local path: ${localPath}\n`;
260
+ images.push({ path: localPath, buffer, name: doc.name });
261
+ } catch (e) {
262
+ output += `Export failed: ${e.message}\n`;
263
+ }
264
+ }
265
+ }
266
+ }
267
+ } catch (e) {
268
+ output += `\nCould not fetch node details: ${e.message}\n`;
269
+ }
270
+ } else {
271
+ // No specific node, list pages
272
+ if (fileInfo.document && fileInfo.document.children) {
273
+ output += `\n## Pages (${fileInfo.document.children.length}):\n\n`;
274
+ for (const page of fileInfo.document.children) {
275
+ output += `- **${page.name}**\n`;
276
+ }
277
+ }
278
+ }
279
+
280
+ return { text: output, images };
281
+ }
282
+
283
+ const server = new Server(
284
+ { name: "figma-mcp", version: "1.0.0" },
285
+ { capabilities: { tools: {} } }
286
+ );
287
+
288
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
289
+ return {
290
+ tools: [
291
+ {
292
+ name: "figma_get_design",
293
+ description: "Fetch a Figma design from a URL. For large frames with sections, automatically exports each section separately for better detail.",
294
+ inputSchema: {
295
+ type: "object",
296
+ properties: {
297
+ url: { type: "string", description: "The Figma URL" },
298
+ exportImage: { type: "boolean", description: "Export images (default: true)" },
299
+ exportChildren: { type: "boolean", description: "Export child sections separately for large frames (default: true)" },
300
+ maxChildren: { type: "number", description: "Max sections to export (default: 10)" },
301
+ scale: { type: "number", description: "Export scale 1-4 (default: 2)" },
302
+ },
303
+ required: ["url"],
304
+ },
305
+ },
306
+ {
307
+ name: "figma_export_frame",
308
+ description: "Export a specific Figma frame/node as an image",
309
+ inputSchema: {
310
+ type: "object",
311
+ properties: {
312
+ fileKey: { type: "string", description: "The Figma file key" },
313
+ nodeId: { type: "string", description: "The node ID (e.g., '123-456' or '123:456')" },
314
+ format: { type: "string", enum: ["png", "svg", "pdf", "jpg"], description: "Format (default: png)" },
315
+ scale: { type: "number", description: "Scale 0.01-4 (default: 2)" },
316
+ },
317
+ required: ["fileKey", "nodeId"],
318
+ },
319
+ },
320
+ ],
321
+ };
322
+ });
323
+
324
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
325
+ const { name, arguments: args } = request.params;
326
+
327
+ try {
328
+ if (name === "figma_get_design") {
329
+ const result = await getFigmaDesign(args.url, {
330
+ exportImage: args.exportImage !== false,
331
+ exportChildren: args.exportChildren !== false,
332
+ maxChildren: args.maxChildren || 10,
333
+ scale: args.scale || 2,
334
+ });
335
+
336
+ const content = [{ type: "text", text: result.text }];
337
+
338
+ for (const img of result.images) {
339
+ content.push({
340
+ type: "image",
341
+ data: img.buffer.toString("base64"),
342
+ mimeType: "image/png",
343
+ });
344
+ }
345
+
346
+ return { content };
347
+ } else if (name === "figma_export_frame") {
348
+ const nodeId = normalizeNodeId(args.nodeId);
349
+ const { localPath, buffer } = await exportImage(
350
+ args.fileKey,
351
+ nodeId,
352
+ args.format || "png",
353
+ args.scale || 2
354
+ );
355
+
356
+ return {
357
+ content: [
358
+ { type: "text", text: `Exported to: ${localPath}` },
359
+ { type: "image", data: buffer.toString("base64"), mimeType: "image/png" },
360
+ ],
361
+ };
362
+ } else {
363
+ throw new Error(`Unknown tool: ${name}`);
364
+ }
365
+ } catch (error) {
366
+ return {
367
+ content: [{ type: "text", text: `Error: ${error.message}` }],
368
+ isError: true,
369
+ };
370
+ }
371
+ });
372
+
373
+ async function main() {
374
+ const transport = new StdioServerTransport();
375
+ await server.connect(transport);
376
+ }
377
+
378
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@rui.branco/figma-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Figma MCP server for Claude Code - fetch designs, export frames, and view specs",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "figma-mcp": "./index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "setup.js"
12
+ ],
13
+ "scripts": {
14
+ "setup": "node setup.js"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "model-context-protocol",
19
+ "figma",
20
+ "claude",
21
+ "claude-code",
22
+ "design"
23
+ ],
24
+ "author": "Rui Branco",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/rui-branco/figma-mcp.git"
29
+ },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.25.3",
32
+ "node-fetch": "^2.7.0"
33
+ }
34
+ }
package/setup.js ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+
3
+ const readline = require("readline");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+
7
+ const configDir = path.join(process.env.HOME, ".config/figma-mcp");
8
+ const configPath = path.join(configDir, "config.json");
9
+
10
+ // Check for command line argument
11
+ let args = process.argv.slice(2);
12
+ // Skip "setup" arg if called via index.js
13
+ if (args[0] === "setup") args = args.slice(1);
14
+
15
+ if (args.length >= 1) {
16
+ // Non-interactive mode: node setup.js <token>
17
+ const [token] = args;
18
+
19
+ if (!fs.existsSync(configDir)) {
20
+ fs.mkdirSync(configDir, { recursive: true });
21
+ }
22
+
23
+ fs.writeFileSync(configPath, JSON.stringify({ token }, null, 2));
24
+ console.log(`Config saved to ${configPath}`);
25
+ process.exit(0);
26
+ }
27
+
28
+ // Interactive mode
29
+ const rl = readline.createInterface({
30
+ input: process.stdin,
31
+ output: process.stdout,
32
+ });
33
+
34
+ function ask(question) {
35
+ return new Promise((resolve) => {
36
+ rl.question(question, resolve);
37
+ });
38
+ }
39
+
40
+ async function setup() {
41
+ console.log("\n=== Figma MCP Setup ===\n");
42
+ console.log("To get your Figma personal access token:");
43
+ console.log("1. Go to https://www.figma.com/settings");
44
+ console.log("2. Scroll to 'Personal access tokens'");
45
+ console.log("3. Click 'Generate new token'");
46
+ console.log("4. Copy the token\n");
47
+
48
+ const token = await ask("Figma personal access token: ");
49
+
50
+ if (!fs.existsSync(configDir)) {
51
+ fs.mkdirSync(configDir, { recursive: true });
52
+ }
53
+
54
+ fs.writeFileSync(configPath, JSON.stringify({ token }, null, 2));
55
+ console.log(`\nConfig saved to ${configPath}`);
56
+
57
+ console.log("\n=== Setup Complete ===");
58
+ console.log("\nIf you haven't already, add to Claude Code with:\n");
59
+ console.log(" claude mcp add --transport stdio figma -- npx -y @rui.branco/figma-mcp");
60
+ console.log("\nThen restart Claude Code and run /mcp to verify.");
61
+
62
+ rl.close();
63
+ }
64
+
65
+ setup().catch((e) => {
66
+ console.error("Setup failed:", e.message);
67
+ rl.close();
68
+ process.exit(1);
69
+ });