@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.
- package/README.md +196 -0
- package/index.js +378 -0
- package/package.json +34 -0
- 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
|
+
});
|