figma-console-mcp 1.2.5 β 1.3.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 +5 -2
- package/dist/cloudflare/core/figma-api.js +93 -0
- package/dist/cloudflare/core/figma-desktop-connector.js +60 -8
- package/dist/cloudflare/core/figma-tools.js +31 -6
- package/dist/cloudflare/index.js +1179 -91
- package/dist/core/figma-api.d.ts +38 -0
- package/dist/core/figma-api.d.ts.map +1 -1
- package/dist/core/figma-api.js +93 -0
- package/dist/core/figma-api.js.map +1 -1
- package/dist/core/figma-desktop-connector.d.ts.map +1 -1
- package/dist/core/figma-desktop-connector.js +60 -8
- package/dist/core/figma-desktop-connector.js.map +1 -1
- package/dist/core/figma-tools.d.ts.map +1 -1
- package/dist/core/figma-tools.js +31 -6
- package/dist/core/figma-tools.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# Figma Console MCP Server
|
|
2
2
|
|
|
3
3
|
[](https://modelcontextprotocol.io/)
|
|
4
|
+
[](https://www.npmjs.com/package/figma-console-mcp)
|
|
4
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://docs.figma-console-mcp.southleft.com)
|
|
5
7
|
|
|
6
|
-
> **Model Context Protocol server
|
|
8
|
+
> **Your design system as an API.** Model Context Protocol server that bridges design and developmentβgiving AI assistants complete access to Figma for **extraction**, **creation**, and **debugging**.
|
|
7
9
|
|
|
8
10
|
## What is this?
|
|
9
11
|
|
|
@@ -593,7 +595,8 @@ MIT - See [LICENSE](LICENSE) file for details.
|
|
|
593
595
|
|
|
594
596
|
## π Links
|
|
595
597
|
|
|
596
|
-
-
|
|
598
|
+
- π **[Documentation Site](https://docs.figma-console-mcp.southleft.com)** β Complete guides, tutorials, and API reference
|
|
599
|
+
- π [Local Docs](docs/) β Documentation source files
|
|
597
600
|
- π [Report Issues](https://github.com/southleft/figma-console-mcp/issues)
|
|
598
601
|
- π¬ [Discussions](https://github.com/southleft/figma-console-mcp/discussions)
|
|
599
602
|
- π [Model Context Protocol](https://modelcontextprotocol.io/)
|
|
@@ -21,6 +21,62 @@ export function extractFileKey(url) {
|
|
|
21
21
|
return null;
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Extract comprehensive URL info including branch and node IDs
|
|
26
|
+
* Supports both URL formats:
|
|
27
|
+
* - Path-based: /design/{fileKey}/branch/{branchKey}/{fileName}
|
|
28
|
+
* - Query-based: /design/{fileKey}/{fileName}?branch-id={branchId}
|
|
29
|
+
*
|
|
30
|
+
* @example https://www.figma.com/design/abc123/branch/xyz789/My-File?node-id=1-2
|
|
31
|
+
* -> { fileKey: 'abc123', branchId: 'xyz789', nodeId: '1:2' }
|
|
32
|
+
* @example https://www.figma.com/design/abc123/My-File?branch-id=xyz789&node-id=1-2
|
|
33
|
+
* -> { fileKey: 'abc123', branchId: 'xyz789', nodeId: '1:2' }
|
|
34
|
+
*/
|
|
35
|
+
export function extractFigmaUrlInfo(url) {
|
|
36
|
+
try {
|
|
37
|
+
const urlObj = new URL(url);
|
|
38
|
+
// First try: Path-based branch format /design/{fileKey}/branch/{branchKey}/{fileName}
|
|
39
|
+
const branchPathMatch = urlObj.pathname.match(/\/(design|file)\/([a-zA-Z0-9]+)\/branch\/([a-zA-Z0-9]+)/);
|
|
40
|
+
if (branchPathMatch) {
|
|
41
|
+
const fileKey = branchPathMatch[2];
|
|
42
|
+
const branchId = branchPathMatch[3];
|
|
43
|
+
const nodeIdParam = urlObj.searchParams.get('node-id');
|
|
44
|
+
const nodeId = nodeIdParam ? nodeIdParam.replace(/-/g, ':') : undefined;
|
|
45
|
+
return { fileKey, branchId, nodeId };
|
|
46
|
+
}
|
|
47
|
+
// Second try: Standard format /design/{fileKey}/{fileName} with optional ?branch-id=
|
|
48
|
+
const standardMatch = urlObj.pathname.match(/\/(design|file)\/([a-zA-Z0-9]+)/);
|
|
49
|
+
if (!standardMatch)
|
|
50
|
+
return null;
|
|
51
|
+
const fileKey = standardMatch[2];
|
|
52
|
+
const branchId = urlObj.searchParams.get('branch-id') || undefined;
|
|
53
|
+
const nodeIdParam = urlObj.searchParams.get('node-id');
|
|
54
|
+
// Convert node-id from URL format (1-2) to Figma format (1:2)
|
|
55
|
+
const nodeId = nodeIdParam ? nodeIdParam.replace(/-/g, ':') : undefined;
|
|
56
|
+
return { fileKey, branchId, nodeId };
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
logger.error({ error, url }, 'Failed to extract Figma URL info');
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Wrap a promise with a timeout
|
|
65
|
+
* @param promise The promise to wrap
|
|
66
|
+
* @param ms Timeout in milliseconds
|
|
67
|
+
* @param label Label for error message
|
|
68
|
+
* @returns Promise that rejects if timeout exceeded
|
|
69
|
+
*/
|
|
70
|
+
export function withTimeout(promise, ms, label) {
|
|
71
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
72
|
+
const timeoutId = setTimeout(() => {
|
|
73
|
+
reject(new Error(`${label} timed out after ${ms}ms`));
|
|
74
|
+
}, ms);
|
|
75
|
+
// Ensure timeout is cleared if promise resolves first
|
|
76
|
+
promise.finally(() => clearTimeout(timeoutId));
|
|
77
|
+
});
|
|
78
|
+
return Promise.race([promise, timeoutPromise]);
|
|
79
|
+
}
|
|
24
80
|
/**
|
|
25
81
|
* Figma API Client
|
|
26
82
|
* Makes authenticated requests to Figma REST API
|
|
@@ -95,6 +151,43 @@ export class FigmaAPI {
|
|
|
95
151
|
}
|
|
96
152
|
return this.request(endpoint);
|
|
97
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Resolve a branch key from a branch ID
|
|
156
|
+
* If branchId is provided, fetches branch data and returns the branch's unique key
|
|
157
|
+
* Otherwise returns the main file key unchanged
|
|
158
|
+
* @param fileKey The main file key from the URL
|
|
159
|
+
* @param branchId Optional branch ID from URL query param (branch-id)
|
|
160
|
+
* @returns The effective file key to use for API calls (branch key if on branch, otherwise fileKey)
|
|
161
|
+
*/
|
|
162
|
+
async getBranchKey(fileKey, branchId) {
|
|
163
|
+
if (!branchId) {
|
|
164
|
+
return fileKey;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
logger.info({ fileKey, branchId }, 'Resolving branch key');
|
|
168
|
+
const fileData = await this.getFile(fileKey, { branch_data: true });
|
|
169
|
+
const branches = fileData.branches || [];
|
|
170
|
+
// Try to find branch by key (branchId might already be the key)
|
|
171
|
+
// or by matching other identifiers
|
|
172
|
+
const branch = branches.find((b) => b.key === branchId || b.name === branchId);
|
|
173
|
+
if (branch?.key) {
|
|
174
|
+
logger.info({ fileKey, branchId, branchKey: branch.key, branchName: branch.name }, 'Resolved branch key');
|
|
175
|
+
return branch.key;
|
|
176
|
+
}
|
|
177
|
+
// If branchId looks like a file key (alphanumeric), it might already be the branch key
|
|
178
|
+
// In this case, return it directly as it may be usable
|
|
179
|
+
if (/^[a-zA-Z0-9]+$/.test(branchId)) {
|
|
180
|
+
logger.info({ fileKey, branchId }, 'Branch ID appears to be a key, using directly');
|
|
181
|
+
return branchId;
|
|
182
|
+
}
|
|
183
|
+
logger.warn({ fileKey, branchId, availableBranches: branches.map((b) => ({ key: b.key, name: b.name })) }, 'Branch not found in file, using main file key');
|
|
184
|
+
return fileKey;
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
logger.error({ error, fileKey, branchId }, 'Failed to resolve branch key, using main file key');
|
|
188
|
+
return fileKey;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
98
191
|
/**
|
|
99
192
|
* GET /v1/files/:file_key/variables/local
|
|
100
193
|
* Get local variables (design tokens) from a file
|
|
@@ -101,6 +101,11 @@ export class FigmaDesktopConnector {
|
|
|
101
101
|
// Try to find plugin UI iframe with variables data
|
|
102
102
|
for (const frame of frames) {
|
|
103
103
|
try {
|
|
104
|
+
// Check if frame is still attached before accessing it
|
|
105
|
+
if (frame.isDetached()) {
|
|
106
|
+
logger.debug('Skipping detached frame');
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
104
109
|
const frameUrl = frame.url();
|
|
105
110
|
await this.page.evaluate((url) => {
|
|
106
111
|
console.log(`[DESKTOP_CONNECTOR] Checking frame: ${url}`);
|
|
@@ -128,10 +133,31 @@ export class FigmaDesktopConnector {
|
|
|
128
133
|
}
|
|
129
134
|
}
|
|
130
135
|
catch (frameError) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
const errorMsg = frameError instanceof Error ? frameError.message : String(frameError);
|
|
137
|
+
const isDetachedError = errorMsg.includes('detached') || errorMsg.includes('Execution context was destroyed');
|
|
138
|
+
// Safely get frame URL (may fail if frame is detached)
|
|
139
|
+
let safeFrameUrl = 'unknown';
|
|
140
|
+
try {
|
|
141
|
+
safeFrameUrl = frame.url();
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
safeFrameUrl = '(detached)';
|
|
145
|
+
}
|
|
146
|
+
if (isDetachedError) {
|
|
147
|
+
logger.debug({ frameUrl: safeFrameUrl }, 'Frame was detached during variables check, trying next');
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// Only log to browser console if we can (page may still be accessible)
|
|
151
|
+
try {
|
|
152
|
+
await this.page.evaluate((url, err) => {
|
|
153
|
+
console.log(`[DESKTOP_CONNECTOR] Frame ${url} check failed: ${err}`);
|
|
154
|
+
}, safeFrameUrl, errorMsg);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Page also unavailable, just log locally
|
|
158
|
+
}
|
|
159
|
+
logger.debug({ error: frameError, frameUrl: safeFrameUrl }, 'Frame check failed, trying next');
|
|
160
|
+
}
|
|
135
161
|
continue;
|
|
136
162
|
}
|
|
137
163
|
}
|
|
@@ -167,6 +193,11 @@ export class FigmaDesktopConnector {
|
|
|
167
193
|
// Try to find plugin UI iframe with requestComponentData function
|
|
168
194
|
for (const frame of frames) {
|
|
169
195
|
try {
|
|
196
|
+
// Check if frame is still attached before accessing it
|
|
197
|
+
if (frame.isDetached()) {
|
|
198
|
+
logger.debug('Skipping detached frame');
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
170
201
|
const frameUrl = frame.url();
|
|
171
202
|
await this.page.evaluate((url) => {
|
|
172
203
|
console.log(`[DESKTOP_CONNECTOR] Checking frame: ${url}`);
|
|
@@ -196,10 +227,31 @@ export class FigmaDesktopConnector {
|
|
|
196
227
|
}
|
|
197
228
|
}
|
|
198
229
|
catch (frameError) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
230
|
+
const errorMsg = frameError instanceof Error ? frameError.message : String(frameError);
|
|
231
|
+
const isDetachedError = errorMsg.includes('detached') || errorMsg.includes('Execution context was destroyed');
|
|
232
|
+
// Safely get frame URL (may fail if frame is detached)
|
|
233
|
+
let safeFrameUrl = 'unknown';
|
|
234
|
+
try {
|
|
235
|
+
safeFrameUrl = frame.url();
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
safeFrameUrl = '(detached)';
|
|
239
|
+
}
|
|
240
|
+
if (isDetachedError) {
|
|
241
|
+
logger.debug({ frameUrl: safeFrameUrl }, 'Frame was detached during component check, trying next');
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
// Only log to browser console if we can (page may still be accessible)
|
|
245
|
+
try {
|
|
246
|
+
await this.page.evaluate((url, err) => {
|
|
247
|
+
console.log(`[DESKTOP_CONNECTOR] Frame ${url} check failed: ${err}`);
|
|
248
|
+
}, safeFrameUrl, errorMsg);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// Page also unavailable, just log locally
|
|
252
|
+
}
|
|
253
|
+
logger.debug({ error: frameError, frameUrl: safeFrameUrl }, 'Frame check failed, trying next');
|
|
254
|
+
}
|
|
203
255
|
continue;
|
|
204
256
|
}
|
|
205
257
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* MCP tool definitions for Figma REST API data extraction
|
|
4
4
|
*/
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { extractFileKey, formatVariables, formatComponentData } from "./figma-api.js";
|
|
6
|
+
import { extractFileKey, extractFigmaUrlInfo, formatVariables, formatComponentData, withTimeout } from "./figma-api.js";
|
|
7
7
|
import { createChildLogger } from "./logger.js";
|
|
8
8
|
import { EnrichmentService } from "./enrichment/index.js";
|
|
9
9
|
import { SnippetInjector } from "./snippet-injector.js";
|
|
@@ -886,7 +886,7 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
|
|
|
886
886
|
"instead of just alias references. Useful for getting color hex values without manual resolution. " +
|
|
887
887
|
"Default: false."),
|
|
888
888
|
}, async ({ fileUrl, includePublished, verbosity, enrich, include_usage, include_dependencies, include_exports, export_formats, format, collection, namePattern, mode, returnAsLinks, refreshCache, useConsoleFallback, parseFromConsole, page, pageSize, resolveAliases }) => {
|
|
889
|
-
// Extract fileKey outside try block so
|
|
889
|
+
// Extract fileKey and optional branchId outside try block so they're available in catch block
|
|
890
890
|
const url = fileUrl || getCurrentUrl();
|
|
891
891
|
if (!url) {
|
|
892
892
|
return {
|
|
@@ -902,8 +902,9 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
|
|
|
902
902
|
isError: true,
|
|
903
903
|
};
|
|
904
904
|
}
|
|
905
|
-
|
|
906
|
-
|
|
905
|
+
// Use extractFigmaUrlInfo to get fileKey, branchId, and nodeId
|
|
906
|
+
const urlInfo = extractFigmaUrlInfo(url);
|
|
907
|
+
if (!urlInfo) {
|
|
907
908
|
return {
|
|
908
909
|
content: [
|
|
909
910
|
{
|
|
@@ -917,6 +918,14 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
|
|
|
917
918
|
isError: true,
|
|
918
919
|
};
|
|
919
920
|
}
|
|
921
|
+
// For branch URLs, the branchId IS the file key to use for API calls
|
|
922
|
+
// Figma branch URLs contain the branch key directly in the path
|
|
923
|
+
const fileKey = urlInfo.branchId || urlInfo.fileKey;
|
|
924
|
+
const mainFileKey = urlInfo.fileKey;
|
|
925
|
+
const branchId = urlInfo.branchId;
|
|
926
|
+
if (branchId) {
|
|
927
|
+
logger.info({ mainFileKey, branchId, effectiveFileKey: fileKey }, 'Branch URL detected, using branch key for API calls');
|
|
928
|
+
}
|
|
920
929
|
try {
|
|
921
930
|
// =====================================================================
|
|
922
931
|
// CACHE-FIRST LOGIC: Check if we have cached data before fetching
|
|
@@ -1197,7 +1206,8 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
|
|
|
1197
1206
|
try {
|
|
1198
1207
|
logger.info({ fileKey, includePublished, verbosity, enrich }, "Fetching variables via REST API (priority: token detected)");
|
|
1199
1208
|
const api = await getFigmaAPI();
|
|
1200
|
-
|
|
1209
|
+
// Wrap API call with timeout to prevent indefinite hangs (30s timeout)
|
|
1210
|
+
const { local, published } = await withTimeout(api.getAllVariables(fileKey), 30000, 'Figma Variables API');
|
|
1201
1211
|
let localFormatted = formatVariables(local);
|
|
1202
1212
|
let publishedFormatted = includePublished
|
|
1203
1213
|
? formatVariables(published)
|
|
@@ -1411,7 +1421,22 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, getCon
|
|
|
1411
1421
|
}
|
|
1412
1422
|
catch (restError) {
|
|
1413
1423
|
const errorMessage = restError instanceof Error ? restError.message : String(restError);
|
|
1414
|
-
|
|
1424
|
+
// Detect specific error types for better logging and handling
|
|
1425
|
+
const isTimeout = errorMessage.includes('timed out');
|
|
1426
|
+
const isRateLimit = errorMessage.includes('429') || errorMessage.toLowerCase().includes('rate limit');
|
|
1427
|
+
const isAuthError = errorMessage.includes('403') || errorMessage.includes('401');
|
|
1428
|
+
if (isTimeout) {
|
|
1429
|
+
logger.warn({ error: errorMessage, fileKey }, "REST API timed out after 30s, falling back to Desktop Bridge");
|
|
1430
|
+
}
|
|
1431
|
+
else if (isRateLimit) {
|
|
1432
|
+
logger.warn({ error: errorMessage, fileKey }, "REST API rate limited (429), falling back to Desktop Bridge");
|
|
1433
|
+
}
|
|
1434
|
+
else if (isAuthError) {
|
|
1435
|
+
logger.warn({ error: errorMessage, fileKey }, "REST API auth error, check FIGMA_ACCESS_TOKEN validity");
|
|
1436
|
+
}
|
|
1437
|
+
else {
|
|
1438
|
+
logger.warn({ error: errorMessage, fileKey }, "REST API failed, will try Desktop Bridge fallback");
|
|
1439
|
+
}
|
|
1415
1440
|
// Don't throw - fall through to Desktop Bridge
|
|
1416
1441
|
}
|
|
1417
1442
|
}
|