@mp3wizard/figma-console-mcp 1.32.3 → 1.34.1
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 +25 -17
- package/dist/cloudflare/core/cloud-websocket-connector.js +18 -0
- package/dist/cloudflare/core/design-system-manifest.js +19 -14
- package/dist/cloudflare/core/design-system-tools.js +43 -34
- package/dist/cloudflare/core/diagnose-tool.js +4 -0
- package/dist/cloudflare/core/enrichment/enrichment-service.js +11 -5
- package/dist/cloudflare/core/enrichment/style-resolver.js +38 -18
- package/dist/cloudflare/core/figma-api.js +118 -54
- package/dist/cloudflare/core/figma-tools.js +179 -63
- package/dist/cloudflare/core/port-discovery.js +404 -31
- package/dist/cloudflare/core/tokens/alias-resolver.js +75 -5
- package/dist/cloudflare/core/tokens/config.js +10 -0
- package/dist/cloudflare/core/tokens/dialect.js +232 -0
- package/dist/cloudflare/core/tokens/figma-converter.js +144 -16
- package/dist/cloudflare/core/tokens/formatters/css-vars.js +21 -12
- package/dist/cloudflare/core/tokens/formatters/dtcg.js +106 -30
- package/dist/cloudflare/core/tokens/formatters/json.js +28 -10
- package/dist/cloudflare/core/tokens/formatters/scss.js +19 -13
- package/dist/cloudflare/core/tokens/formatters/style-dictionary-v3.js +15 -9
- package/dist/cloudflare/core/tokens/formatters/tailwind-v4.js +14 -9
- package/dist/cloudflare/core/tokens/formatters/tokens-studio.js +11 -5
- package/dist/cloudflare/core/tokens/index.js +2 -1
- package/dist/cloudflare/core/tokens/parsers/dtcg.js +32 -5
- package/dist/cloudflare/core/tokens/schemas.js +4 -0
- package/dist/cloudflare/core/tokens-tools.js +1017 -88
- package/dist/cloudflare/core/version-tools.js +44 -3
- package/dist/cloudflare/core/websocket-connector.js +42 -0
- package/dist/cloudflare/core/websocket-server.js +99 -8
- package/dist/cloudflare/core/write-tools.js +355 -86
- package/dist/cloudflare/index.js +7 -7
- package/dist/core/design-system-manifest.d.ts +1 -0
- package/dist/core/design-system-manifest.d.ts.map +1 -1
- package/dist/core/design-system-manifest.js +19 -14
- package/dist/core/design-system-manifest.js.map +1 -1
- package/dist/core/design-system-tools.d.ts.map +1 -1
- package/dist/core/design-system-tools.js +43 -34
- package/dist/core/design-system-tools.js.map +1 -1
- package/dist/core/diagnose-tool.d.ts +8 -0
- package/dist/core/diagnose-tool.d.ts.map +1 -1
- package/dist/core/diagnose-tool.js +4 -0
- package/dist/core/diagnose-tool.js.map +1 -1
- package/dist/core/enrichment/enrichment-service.d.ts.map +1 -1
- package/dist/core/enrichment/enrichment-service.js +11 -5
- package/dist/core/enrichment/enrichment-service.js.map +1 -1
- package/dist/core/enrichment/style-resolver.d.ts +7 -2
- package/dist/core/enrichment/style-resolver.d.ts.map +1 -1
- package/dist/core/enrichment/style-resolver.js +38 -18
- package/dist/core/enrichment/style-resolver.js.map +1 -1
- package/dist/core/figma-api.d.ts +18 -9
- package/dist/core/figma-api.d.ts.map +1 -1
- package/dist/core/figma-api.js +118 -54
- package/dist/core/figma-api.js.map +1 -1
- package/dist/core/figma-connector.d.ts +12 -0
- package/dist/core/figma-connector.d.ts.map +1 -1
- package/dist/core/figma-tools.d.ts.map +1 -1
- package/dist/core/figma-tools.js +179 -63
- package/dist/core/figma-tools.js.map +1 -1
- package/dist/core/port-discovery.d.ts +40 -0
- package/dist/core/port-discovery.d.ts.map +1 -1
- package/dist/core/port-discovery.js +404 -31
- package/dist/core/port-discovery.js.map +1 -1
- package/dist/core/tokens/alias-resolver.d.ts +45 -3
- package/dist/core/tokens/alias-resolver.d.ts.map +1 -1
- package/dist/core/tokens/alias-resolver.js +75 -5
- package/dist/core/tokens/alias-resolver.js.map +1 -1
- package/dist/core/tokens/config.d.ts +28 -0
- package/dist/core/tokens/config.d.ts.map +1 -1
- package/dist/core/tokens/config.js +10 -0
- package/dist/core/tokens/config.js.map +1 -1
- package/dist/core/tokens/dialect.d.ts +107 -0
- package/dist/core/tokens/dialect.d.ts.map +1 -0
- package/dist/core/tokens/dialect.js +233 -0
- package/dist/core/tokens/dialect.js.map +1 -0
- package/dist/core/tokens/figma-converter.d.ts +23 -2
- package/dist/core/tokens/figma-converter.d.ts.map +1 -1
- package/dist/core/tokens/figma-converter.js +144 -16
- package/dist/core/tokens/figma-converter.js.map +1 -1
- package/dist/core/tokens/formatters/css-vars.d.ts.map +1 -1
- package/dist/core/tokens/formatters/css-vars.js +21 -12
- package/dist/core/tokens/formatters/css-vars.js.map +1 -1
- package/dist/core/tokens/formatters/dtcg.d.ts +2 -2
- package/dist/core/tokens/formatters/dtcg.d.ts.map +1 -1
- package/dist/core/tokens/formatters/dtcg.js +106 -30
- package/dist/core/tokens/formatters/dtcg.js.map +1 -1
- package/dist/core/tokens/formatters/json.d.ts.map +1 -1
- package/dist/core/tokens/formatters/json.js +28 -10
- package/dist/core/tokens/formatters/json.js.map +1 -1
- package/dist/core/tokens/formatters/scss.d.ts.map +1 -1
- package/dist/core/tokens/formatters/scss.js +19 -13
- package/dist/core/tokens/formatters/scss.js.map +1 -1
- package/dist/core/tokens/formatters/style-dictionary-v3.d.ts.map +1 -1
- package/dist/core/tokens/formatters/style-dictionary-v3.js +15 -9
- package/dist/core/tokens/formatters/style-dictionary-v3.js.map +1 -1
- package/dist/core/tokens/formatters/tailwind-v4.d.ts.map +1 -1
- package/dist/core/tokens/formatters/tailwind-v4.js +14 -9
- package/dist/core/tokens/formatters/tailwind-v4.js.map +1 -1
- package/dist/core/tokens/formatters/tokens-studio.d.ts.map +1 -1
- package/dist/core/tokens/formatters/tokens-studio.js +11 -5
- package/dist/core/tokens/formatters/tokens-studio.js.map +1 -1
- package/dist/core/tokens/index.d.ts +2 -1
- package/dist/core/tokens/index.d.ts.map +1 -1
- package/dist/core/tokens/index.js +2 -1
- package/dist/core/tokens/index.js.map +1 -1
- package/dist/core/tokens/parsers/dtcg.js +32 -5
- package/dist/core/tokens/parsers/dtcg.js.map +1 -1
- package/dist/core/tokens/schemas.d.ts +3 -0
- package/dist/core/tokens/schemas.d.ts.map +1 -1
- package/dist/core/tokens/schemas.js +4 -0
- package/dist/core/tokens/schemas.js.map +1 -1
- package/dist/core/tokens/types.d.ts +57 -1
- package/dist/core/tokens/types.d.ts.map +1 -1
- package/dist/core/tokens/types.js.map +1 -1
- package/dist/core/tokens-tools.d.ts +250 -7
- package/dist/core/tokens-tools.d.ts.map +1 -1
- package/dist/core/tokens-tools.js +1017 -88
- package/dist/core/tokens-tools.js.map +1 -1
- package/dist/core/version-tools.d.ts.map +1 -1
- package/dist/core/version-tools.js +44 -3
- package/dist/core/version-tools.js.map +1 -1
- package/dist/core/websocket-connector.d.ts +38 -0
- package/dist/core/websocket-connector.d.ts.map +1 -1
- package/dist/core/websocket-connector.js +42 -0
- package/dist/core/websocket-connector.js.map +1 -1
- package/dist/core/websocket-server.d.ts +23 -0
- package/dist/core/websocket-server.d.ts.map +1 -1
- package/dist/core/websocket-server.js +99 -8
- package/dist/core/websocket-server.js.map +1 -1
- package/dist/core/write-tools.d.ts.map +1 -1
- package/dist/core/write-tools.js +355 -86
- package/dist/core/write-tools.js.map +1 -1
- package/dist/local.d.ts +0 -1
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +253 -63
- package/dist/local.js.map +1 -1
- package/figma-desktop-bridge/code.js +382 -28
- package/figma-desktop-bridge/ui.html +578 -292
- package/package.json +2 -2
|
@@ -7,13 +7,23 @@ const logger = createChildLogger({ component: 'figma-api' });
|
|
|
7
7
|
const FIGMA_API_BASE = 'https://api.figma.com/v1';
|
|
8
8
|
/**
|
|
9
9
|
* Extract file key from Figma URL
|
|
10
|
+
* Branch-aware: for branch URLs (/design/KEY/branch/BRANCHKEY/Name) returns the
|
|
11
|
+
* BRANCH key — the branch is its own file in the REST API, and returning the
|
|
12
|
+
* main key would silently target the wrong file (comments/diffs/file-data).
|
|
10
13
|
* @example https://www.figma.com/design/abc123/My-File -> abc123
|
|
14
|
+
* @example https://www.figma.com/design/abc123/branch/xyz789/My-File -> xyz789
|
|
11
15
|
*/
|
|
12
16
|
export function extractFileKey(url) {
|
|
13
17
|
try {
|
|
14
18
|
const urlObj = new URL(url);
|
|
15
|
-
//
|
|
16
|
-
|
|
19
|
+
// Branch URLs: /design/FILE_KEY/branch/BRANCH_KEY — the branch key is the
|
|
20
|
+
// effective file key for all REST API calls.
|
|
21
|
+
const branchMatch = urlObj.pathname.match(/\/(design|file|board|slides)\/[a-zA-Z0-9]+\/branch\/([a-zA-Z0-9]+)/);
|
|
22
|
+
if (branchMatch) {
|
|
23
|
+
return branchMatch[2];
|
|
24
|
+
}
|
|
25
|
+
// Match patterns like /design/FILE_KEY, /file/FILE_KEY, /board/FILE_KEY (FigJam), /slides/FILE_KEY
|
|
26
|
+
const match = urlObj.pathname.match(/\/(design|file|board|slides)\/([a-zA-Z0-9]+)/);
|
|
17
27
|
return match ? match[2] : null;
|
|
18
28
|
}
|
|
19
29
|
catch (error) {
|
|
@@ -21,6 +31,15 @@ export function extractFileKey(url) {
|
|
|
21
31
|
return null;
|
|
22
32
|
}
|
|
23
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Normalize a node ID to Figma's canonical colon form.
|
|
36
|
+
* URLs use dashes ("695-313") but the REST API keys response maps by colon
|
|
37
|
+
* form ("695:313") — indexing a response with a dashed ID silently returns
|
|
38
|
+
* undefined, which reads as "node may not exist".
|
|
39
|
+
*/
|
|
40
|
+
export function normalizeNodeId(nodeId) {
|
|
41
|
+
return nodeId.replace(/-/g, ':');
|
|
42
|
+
}
|
|
24
43
|
/**
|
|
25
44
|
* Extract comprehensive URL info including branch and node IDs
|
|
26
45
|
* Supports both URL formats:
|
|
@@ -36,7 +55,7 @@ export function extractFigmaUrlInfo(url) {
|
|
|
36
55
|
try {
|
|
37
56
|
const urlObj = new URL(url);
|
|
38
57
|
// 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]+)/);
|
|
58
|
+
const branchPathMatch = urlObj.pathname.match(/\/(design|file|board|slides)\/([a-zA-Z0-9]+)\/branch\/([a-zA-Z0-9]+)/);
|
|
40
59
|
if (branchPathMatch) {
|
|
41
60
|
const fileKey = branchPathMatch[2];
|
|
42
61
|
const branchId = branchPathMatch[3];
|
|
@@ -45,7 +64,7 @@ export function extractFigmaUrlInfo(url) {
|
|
|
45
64
|
return { fileKey, branchId, nodeId };
|
|
46
65
|
}
|
|
47
66
|
// Second try: Standard format /design/{fileKey}/{fileName} with optional ?branch-id=
|
|
48
|
-
const standardMatch = urlObj.pathname.match(/\/(design|file)\/([a-zA-Z0-9]+)/);
|
|
67
|
+
const standardMatch = urlObj.pathname.match(/\/(design|file|board|slides)\/([a-zA-Z0-9]+)/);
|
|
49
68
|
if (!standardMatch)
|
|
50
69
|
return null;
|
|
51
70
|
const fileKey = standardMatch[2];
|
|
@@ -109,14 +128,54 @@ export class FigmaAPI {
|
|
|
109
128
|
else {
|
|
110
129
|
headers['X-Figma-Token'] = this.accessToken;
|
|
111
130
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
131
|
+
// Up to 3 total attempts on 429 (rate limit), honoring Retry-After when
|
|
132
|
+
// present (seconds), else exponential backoff (1s, 2s). Delays here run
|
|
133
|
+
// inside the request promise — callers wrapping with withTimeout() still
|
|
134
|
+
// get their race-based rejection if the overall wait exceeds their budget.
|
|
135
|
+
const MAX_ATTEMPTS = 3;
|
|
136
|
+
let response;
|
|
137
|
+
for (let attempt = 1;; attempt++) {
|
|
138
|
+
response = await fetch(url, {
|
|
139
|
+
...options,
|
|
140
|
+
headers,
|
|
141
|
+
});
|
|
142
|
+
if (response.status !== 429 || attempt >= MAX_ATTEMPTS) {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
const retryAfterHeader = response.headers.get('retry-after');
|
|
146
|
+
const retryAfterSeconds = retryAfterHeader ? Number(retryAfterHeader) : NaN;
|
|
147
|
+
// Cap Retry-After waits at 30s so a pathological header can't hang callers.
|
|
148
|
+
const delayMs = Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0
|
|
149
|
+
? Math.min(retryAfterSeconds, 30) * 1000
|
|
150
|
+
: attempt * 1000; // 1s after attempt 1, 2s after attempt 2
|
|
151
|
+
logger.warn({ url, attempt, delayMs, retryAfterHeader }, 'Figma API rate limited (429), retrying');
|
|
152
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
153
|
+
}
|
|
116
154
|
if (!response.ok) {
|
|
117
155
|
const errorText = await response.text();
|
|
118
156
|
logger.error({ status: response.status, statusText: response.statusText, body: errorText }, 'Figma API request failed');
|
|
119
|
-
|
|
157
|
+
// The `Figma API error (<status>):` prefix is LOAD-BEARING — downstream
|
|
158
|
+
// tool handlers branch on errorMessage.includes("403") etc. Keep it
|
|
159
|
+
// verbatim at the start of the message; only APPEND guidance after it.
|
|
160
|
+
let message = `Figma API error (${response.status}): ${errorText}`;
|
|
161
|
+
// Token-auth failures (expired/invalid token) get actionable guidance.
|
|
162
|
+
// Plan-gate 403s (e.g. Variables API on non-Enterprise) do NOT match the
|
|
163
|
+
// token pattern and must not be flagged as auth errors.
|
|
164
|
+
const isAuthError = (response.status === 401 || response.status === 403) &&
|
|
165
|
+
/invalid token|token expired|unauthorized/i.test(errorText);
|
|
166
|
+
if (isAuthError) {
|
|
167
|
+
message +=
|
|
168
|
+
' — Your Figma access token is expired or invalid. Generate a new personal access token at figma.com → Settings → Security → Personal access tokens, then update FIGMA_ACCESS_TOKEN in your MCP config. If Figma Desktop is open with the Desktop Bridge plugin running, most tools work without any REST token.';
|
|
169
|
+
}
|
|
170
|
+
else if (response.status === 429) {
|
|
171
|
+
message += ' Rate limited by Figma — wait a moment and retry.';
|
|
172
|
+
}
|
|
173
|
+
const err = new Error(message);
|
|
174
|
+
err.status = response.status;
|
|
175
|
+
if (isAuthError) {
|
|
176
|
+
err.isAuthError = true;
|
|
177
|
+
}
|
|
178
|
+
throw err;
|
|
120
179
|
}
|
|
121
180
|
const data = await response.json();
|
|
122
181
|
return data;
|
|
@@ -145,43 +204,6 @@ export class FigmaAPI {
|
|
|
145
204
|
}
|
|
146
205
|
return this.request(endpoint);
|
|
147
206
|
}
|
|
148
|
-
/**
|
|
149
|
-
* Resolve a branch key from a branch ID
|
|
150
|
-
* If branchId is provided, fetches branch data and returns the branch's unique key
|
|
151
|
-
* Otherwise returns the main file key unchanged
|
|
152
|
-
* @param fileKey The main file key from the URL
|
|
153
|
-
* @param branchId Optional branch ID from URL query param (branch-id)
|
|
154
|
-
* @returns The effective file key to use for API calls (branch key if on branch, otherwise fileKey)
|
|
155
|
-
*/
|
|
156
|
-
async getBranchKey(fileKey, branchId) {
|
|
157
|
-
if (!branchId) {
|
|
158
|
-
return fileKey;
|
|
159
|
-
}
|
|
160
|
-
try {
|
|
161
|
-
logger.info({ fileKey, branchId }, 'Resolving branch key');
|
|
162
|
-
const fileData = await this.getFile(fileKey, { branch_data: true });
|
|
163
|
-
const branches = fileData.branches || [];
|
|
164
|
-
// Try to find branch by key (branchId might already be the key)
|
|
165
|
-
// or by matching other identifiers
|
|
166
|
-
const branch = branches.find((b) => b.key === branchId || b.name === branchId);
|
|
167
|
-
if (branch?.key) {
|
|
168
|
-
logger.info({ fileKey, branchId, branchKey: branch.key, branchName: branch.name }, 'Resolved branch key');
|
|
169
|
-
return branch.key;
|
|
170
|
-
}
|
|
171
|
-
// If branchId looks like a file key (alphanumeric), it might already be the branch key
|
|
172
|
-
// In this case, return it directly as it may be usable
|
|
173
|
-
if (/^[a-zA-Z0-9]+$/.test(branchId)) {
|
|
174
|
-
logger.info({ fileKey, branchId }, 'Branch ID appears to be a key, using directly');
|
|
175
|
-
return branchId;
|
|
176
|
-
}
|
|
177
|
-
logger.warn({ fileKey, branchId, availableBranches: branches.map((b) => ({ key: b.key, name: b.name })) }, 'Branch not found in file, using main file key');
|
|
178
|
-
return fileKey;
|
|
179
|
-
}
|
|
180
|
-
catch (error) {
|
|
181
|
-
logger.error({ error, fileKey, branchId }, 'Failed to resolve branch key, using main file key');
|
|
182
|
-
return fileKey;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
207
|
/**
|
|
186
208
|
* GET /v1/files/:file_key/variables/local
|
|
187
209
|
* Get local variables (design tokens) from a file
|
|
@@ -205,11 +227,17 @@ export class FigmaAPI {
|
|
|
205
227
|
/**
|
|
206
228
|
* GET /v1/files/:file_key/nodes
|
|
207
229
|
* Get specific nodes by ID
|
|
230
|
+
*
|
|
231
|
+
* Node IDs are normalized to Figma's colon form ("695-313" -> "695:313")
|
|
232
|
+
* before the request, and the response map is aliased back under any
|
|
233
|
+
* dashed IDs the caller passed — Figma keys the response by colon-format
|
|
234
|
+
* IDs, so without the alias a dashed lookup silently returns undefined.
|
|
208
235
|
*/
|
|
209
236
|
async getNodes(fileKey, nodeIds, options) {
|
|
210
237
|
let endpoint = `/files/${fileKey}/nodes`;
|
|
238
|
+
const normalizedIds = nodeIds.map((id) => normalizeNodeId(id));
|
|
211
239
|
const params = new URLSearchParams();
|
|
212
|
-
params.append('ids',
|
|
240
|
+
params.append('ids', normalizedIds.join(','));
|
|
213
241
|
if (options?.version)
|
|
214
242
|
params.append('version', options.version);
|
|
215
243
|
if (options?.depth !== undefined)
|
|
@@ -219,7 +247,19 @@ export class FigmaAPI {
|
|
|
219
247
|
if (options?.plugin_data)
|
|
220
248
|
params.append('plugin_data', options.plugin_data);
|
|
221
249
|
endpoint += `?${params.toString()}`;
|
|
222
|
-
|
|
250
|
+
const response = await this.request(endpoint);
|
|
251
|
+
// Defensive aliasing: let callers index response.nodes by the ID form
|
|
252
|
+
// they originally passed (dashed), not just Figma's colon form.
|
|
253
|
+
if (response?.nodes) {
|
|
254
|
+
for (let i = 0; i < nodeIds.length; i++) {
|
|
255
|
+
const original = nodeIds[i];
|
|
256
|
+
const normalized = normalizedIds[i];
|
|
257
|
+
if (original !== normalized && response.nodes[normalized] !== undefined && response.nodes[original] === undefined) {
|
|
258
|
+
response.nodes[original] = response.nodes[normalized];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return response;
|
|
223
263
|
}
|
|
224
264
|
/**
|
|
225
265
|
* GET /v1/files/:file_key/styles
|
|
@@ -271,8 +311,12 @@ export class FigmaAPI {
|
|
|
271
311
|
*/
|
|
272
312
|
async getImages(fileKey, nodeIds, options) {
|
|
273
313
|
const params = new URLSearchParams();
|
|
274
|
-
// Handle single or multiple node IDs
|
|
275
|
-
|
|
314
|
+
// Handle single or multiple node IDs. Normalize to Figma's colon form
|
|
315
|
+
// ("695-313" -> "695:313") — the response map is keyed by colon IDs, so
|
|
316
|
+
// a dashed request ID would otherwise never match its own result.
|
|
317
|
+
const originalIds = Array.isArray(nodeIds) ? nodeIds : [nodeIds];
|
|
318
|
+
const normalizedIds = originalIds.map((id) => normalizeNodeId(id));
|
|
319
|
+
const ids = normalizedIds.join(',');
|
|
276
320
|
params.append('ids', ids);
|
|
277
321
|
// Add optional parameters
|
|
278
322
|
if (options?.scale !== undefined)
|
|
@@ -291,7 +335,19 @@ export class FigmaAPI {
|
|
|
291
335
|
params.append('contents_only', options.contents_only.toString());
|
|
292
336
|
const endpoint = `/images/${fileKey}?${params.toString()}`;
|
|
293
337
|
logger.info({ fileKey, ids, options }, 'Rendering images');
|
|
294
|
-
|
|
338
|
+
const response = await this.request(endpoint);
|
|
339
|
+
// Defensive aliasing: callers may index the images map by the dashed
|
|
340
|
+
// ID they passed. Figma keys it by colon-format ID — alias both forms.
|
|
341
|
+
if (response?.images) {
|
|
342
|
+
for (let i = 0; i < originalIds.length; i++) {
|
|
343
|
+
const original = originalIds[i];
|
|
344
|
+
const normalized = normalizedIds[i];
|
|
345
|
+
if (original !== normalized && response.images[normalized] !== undefined && response.images[original] === undefined) {
|
|
346
|
+
response.images[original] = response.images[normalized];
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return response;
|
|
295
351
|
}
|
|
296
352
|
/**
|
|
297
353
|
* GET /v1/files/:file_key/comments
|
|
@@ -356,15 +412,20 @@ export class FigmaAPI {
|
|
|
356
412
|
const [localResult, publishedResult] = await Promise.all([
|
|
357
413
|
this.getLocalVariables(fileKey).catch((err) => {
|
|
358
414
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
415
|
+
logger.warn({ fileKey, error: errorMsg }, 'getLocalVariables failed; returning empty fallback');
|
|
359
416
|
return { error: errorMsg, variables: {}, variableCollections: {} };
|
|
360
417
|
}),
|
|
361
418
|
this.getPublishedVariables(fileKey).catch((err) => {
|
|
362
419
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
420
|
+
logger.warn({ fileKey, error: errorMsg }, 'getPublishedVariables failed; returning empty fallback');
|
|
363
421
|
return { error: errorMsg, variables: {} };
|
|
364
422
|
}),
|
|
365
423
|
]);
|
|
424
|
+
// Fallback shape must match the success path (already-unwrapped meta):
|
|
425
|
+
// { variables, variableCollections } — NOT { meta: {...} } — so consumers
|
|
426
|
+
// like formatVariables() see a consistent shape either way.
|
|
366
427
|
return {
|
|
367
|
-
local: 'error' in localResult ? {
|
|
428
|
+
local: 'error' in localResult ? { variables: {}, variableCollections: {} } : localResult,
|
|
368
429
|
published: 'error' in publishedResult ? { variables: {} } : publishedResult,
|
|
369
430
|
...(('error' in localResult) && { localError: localResult.error }),
|
|
370
431
|
...(('error' in publishedResult) && { publishedError: publishedResult.error }),
|
|
@@ -372,10 +433,13 @@ export class FigmaAPI {
|
|
|
372
433
|
}
|
|
373
434
|
/**
|
|
374
435
|
* Helper: Get component metadata with properties
|
|
436
|
+
* Normalizes dashed node IDs ("695-313") to colon form ("695:313") so the
|
|
437
|
+
* response-map lookup matches Figma's colon-keyed response.
|
|
375
438
|
*/
|
|
376
439
|
async getComponentData(fileKey, nodeId, depth = 4) {
|
|
377
|
-
const
|
|
378
|
-
|
|
440
|
+
const normalizedId = normalizeNodeId(nodeId);
|
|
441
|
+
const response = await this.getNodes(fileKey, [normalizedId], { depth });
|
|
442
|
+
return response.nodes?.[normalizedId] ?? response.nodes?.[nodeId];
|
|
379
443
|
}
|
|
380
444
|
/**
|
|
381
445
|
* Helper: Search for components by name
|
|
@@ -147,27 +147,34 @@ function adaptiveResponse(responseData, options) {
|
|
|
147
147
|
],
|
|
148
148
|
};
|
|
149
149
|
}
|
|
150
|
-
// Determine compression level and message
|
|
150
|
+
// Determine compression level and message. The "AUTO-COMPRESSED" wording is
|
|
151
|
+
// only truthful when a compressionCallback actually runs — without one the
|
|
152
|
+
// full payload is returned and the banner must say so instead of claiming
|
|
153
|
+
// a reduction that never happened.
|
|
151
154
|
let compressionLevel = "info";
|
|
152
155
|
let aiInstruction = "";
|
|
153
156
|
let shouldCompress = false;
|
|
157
|
+
const canCompress = !!options.compressionCallback;
|
|
154
158
|
if (sizeKB > RESPONSE_SIZE_THRESHOLDS.MAX_SIZE_KB) {
|
|
155
159
|
compressionLevel = "emergency";
|
|
156
160
|
shouldCompress = true;
|
|
157
|
-
aiInstruction =
|
|
158
|
-
`⚠️ RESPONSE AUTO-COMPRESSED: The ${options.toolName} response was automatically reduced because the full response would be ${sizeKB.toFixed(0)}KB, which would exhaust Claude Desktop's context window.\n\n
|
|
161
|
+
aiInstruction = canCompress
|
|
162
|
+
? `⚠️ RESPONSE AUTO-COMPRESSED: The ${options.toolName} response was automatically reduced because the full response would be ${sizeKB.toFixed(0)}KB, which would exhaust Claude Desktop's context window.\n\n`
|
|
163
|
+
: `⚠️ LARGE RESPONSE: The ${options.toolName} response is ${sizeKB.toFixed(0)}KB and may exhaust the context window. Retry with filters or pagination to reduce it.\n\n`;
|
|
159
164
|
}
|
|
160
165
|
else if (sizeKB > RESPONSE_SIZE_THRESHOLDS.CRITICAL_SIZE_KB) {
|
|
161
166
|
compressionLevel = "critical";
|
|
162
167
|
shouldCompress = true;
|
|
163
|
-
aiInstruction =
|
|
164
|
-
`⚠️ RESPONSE AUTO-COMPRESSED: The ${options.toolName} response was automatically reduced because it would be ${sizeKB.toFixed(0)}KB, risking context window exhaustion.\n\n
|
|
168
|
+
aiInstruction = canCompress
|
|
169
|
+
? `⚠️ RESPONSE AUTO-COMPRESSED: The ${options.toolName} response was automatically reduced because it would be ${sizeKB.toFixed(0)}KB, risking context window exhaustion.\n\n`
|
|
170
|
+
: `⚠️ LARGE RESPONSE: The ${options.toolName} response is ${sizeKB.toFixed(0)}KB, risking context window exhaustion. Retry with filters or pagination to reduce it.\n\n`;
|
|
165
171
|
}
|
|
166
172
|
else if (sizeKB > RESPONSE_SIZE_THRESHOLDS.WARNING_SIZE_KB) {
|
|
167
173
|
compressionLevel = "warning";
|
|
168
174
|
shouldCompress = true;
|
|
169
|
-
aiInstruction =
|
|
170
|
-
`ℹ️ RESPONSE OPTIMIZED: The ${options.toolName} response was automatically reduced because it would be ${sizeKB.toFixed(0)}KB.\n\n
|
|
175
|
+
aiInstruction = canCompress
|
|
176
|
+
? `ℹ️ RESPONSE OPTIMIZED: The ${options.toolName} response was automatically reduced because it would be ${sizeKB.toFixed(0)}KB.\n\n`
|
|
177
|
+
: `ℹ️ LARGE RESPONSE: The ${options.toolName} response is ${sizeKB.toFixed(0)}KB. Filters or pagination can reduce it.\n\n`;
|
|
171
178
|
}
|
|
172
179
|
// Map compression level to verbosity level
|
|
173
180
|
const verbosityMap = {
|
|
@@ -816,13 +823,17 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
|
|
|
816
823
|
return adaptiveResponse(finalResponse, {
|
|
817
824
|
toolName: "figma_get_file_data",
|
|
818
825
|
compressionCallback: (adjustedLevel) => {
|
|
819
|
-
// Re-apply node filtering with lower verbosity
|
|
820
|
-
|
|
826
|
+
// Re-apply node filtering with lower verbosity. "inventory"
|
|
827
|
+
// (the emergency tier) isn't a filterNode level — clamp it to
|
|
828
|
+
// "summary", the strongest filter, instead of letting it fall
|
|
829
|
+
// through filterNode's default branch which returns the RAW
|
|
830
|
+
// node (that ballooned "compressed" responses). Always
|
|
831
|
+
// re-filter, even when the caller asked for verbosity='full' —
|
|
832
|
+
// an emergency downgrade must never echo the raw document.
|
|
833
|
+
const level = (adjustedLevel === "standard" ? "standard" : "summary");
|
|
821
834
|
const refiltered = {
|
|
822
835
|
...finalResponse,
|
|
823
|
-
document:
|
|
824
|
-
? filterNode(fileData.document, level)
|
|
825
|
-
: fileData.document,
|
|
836
|
+
document: filterNode(fileData.document, level),
|
|
826
837
|
verbosity: level,
|
|
827
838
|
};
|
|
828
839
|
return refiltered;
|
|
@@ -866,7 +877,7 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
|
|
|
866
877
|
* removed in the Phase 3 cleanup. Setting parseFromConsole=true now
|
|
867
878
|
* throws an identified error pointing the caller at the bridge.
|
|
868
879
|
*/
|
|
869
|
-
server.tool("figma_get_variables", "Extract design tokens and variables from a Figma file with code export support (CSS, Tailwind, TypeScript, Sass). Use when user asks for: design system tokens, variables, color/spacing values, theme data, or code exports. Handles multi-mode variables (Light/Dark themes). NOT for component metadata (use figma_get_component).
|
|
880
|
+
server.tool("figma_get_variables", "Extract design tokens and variables from a Figma file with code export support (CSS, Tailwind, TypeScript, Sass). Use when user asks for: design system tokens, variables, color/spacing values, theme data, or code exports. Handles multi-mode variables (Light/Dark themes). NOT for component metadata (use figma_get_component). Returns a compact summary by default — pass format='full' for the complete dataset or format='filtered' with collection/namePattern/mode filters for specific variables. Supports verbosity control to prevent token exhaustion. Resolution order: Desktop Bridge plugin (works on any plan) → Variables REST API (Enterprise only) → Styles API as a partial fallback. TIP: For full design system extraction (tokens + components + styles combined), prefer figma_get_design_system_kit instead — it returns everything in one optimized call.", {
|
|
870
881
|
fileUrl: z
|
|
871
882
|
.string()
|
|
872
883
|
.url()
|
|
@@ -905,9 +916,9 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
|
|
|
905
916
|
format: z
|
|
906
917
|
.enum(["summary", "filtered", "full"])
|
|
907
918
|
.optional()
|
|
908
|
-
.default("full")
|
|
909
919
|
.describe("Response format: 'summary' (~2K tokens with overview and names only), 'filtered' (apply collection/name/mode filters), 'full' (complete dataset from cache or fetch). " +
|
|
910
|
-
"
|
|
920
|
+
"Default: summary (token-efficient). When format is omitted, filter params (collection/namePattern/mode) auto-select 'filtered', and enrichment/export/resolveAliases/returnAsLinks params auto-select 'full'. " +
|
|
921
|
+
"Full format returns all data but may be auto-summarized if >25K tokens."),
|
|
911
922
|
collection: z
|
|
912
923
|
.string()
|
|
913
924
|
.optional()
|
|
@@ -964,6 +975,28 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
|
|
|
964
975
|
"instead of just alias references. Useful for getting color hex values without manual resolution. " +
|
|
965
976
|
"Default: false."),
|
|
966
977
|
}, async ({ fileUrl, includePublished, verbosity, enrich, include_usage, include_dependencies, include_exports, export_formats, format, collection, namePattern, mode, returnAsLinks, refreshCache, useConsoleFallback, parseFromConsole, page, pageSize, resolveAliases }) => {
|
|
978
|
+
// Smart format default (token efficiency): when the caller doesn't pick a
|
|
979
|
+
// format, use the cheapest one their other parameters allow. Explicit
|
|
980
|
+
// format callers are unaffected. Only parameters without schema defaults
|
|
981
|
+
// (or defaulting to false) can signal intent — verbosity, includePublished,
|
|
982
|
+
// and page/pageSize are always populated by their defaults.
|
|
983
|
+
if (!format) {
|
|
984
|
+
if (collection || namePattern || mode) {
|
|
985
|
+
format = "filtered";
|
|
986
|
+
}
|
|
987
|
+
else if (enrich ||
|
|
988
|
+
include_usage ||
|
|
989
|
+
include_dependencies ||
|
|
990
|
+
include_exports ||
|
|
991
|
+
(export_formats && export_formats.length > 0) ||
|
|
992
|
+
resolveAliases ||
|
|
993
|
+
returnAsLinks) {
|
|
994
|
+
format = "full";
|
|
995
|
+
}
|
|
996
|
+
else {
|
|
997
|
+
format = "summary";
|
|
998
|
+
}
|
|
999
|
+
}
|
|
967
1000
|
// Extract fileKey and optional branchId outside try block so they're available in catch block
|
|
968
1001
|
const url = fileUrl || getCurrentUrl();
|
|
969
1002
|
if (!url) {
|
|
@@ -1297,6 +1330,53 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
|
|
|
1297
1330
|
let publishedFormatted = includePublished
|
|
1298
1331
|
? formatVariables(published)
|
|
1299
1332
|
: null;
|
|
1333
|
+
// Cache the FULL dataset in the Desktop-connection shape BEFORE
|
|
1334
|
+
// any format/verbosity/pagination mutation. The cache reader
|
|
1335
|
+
// expects { variables, variableCollections }; caching the
|
|
1336
|
+
// stripped, paginated page (the old behavior) poisoned every
|
|
1337
|
+
// follow-up filtered/summary call for the whole TTL window.
|
|
1338
|
+
// Variable objects are shallow-copied so downstream verbosity
|
|
1339
|
+
// stripping can't reach into the cached entries.
|
|
1340
|
+
if (variablesCache) {
|
|
1341
|
+
evictOldestCacheEntry(variablesCache);
|
|
1342
|
+
variablesCache.set(fileKey, {
|
|
1343
|
+
data: {
|
|
1344
|
+
fileKey,
|
|
1345
|
+
source: "rest_api",
|
|
1346
|
+
timestamp: Date.now(),
|
|
1347
|
+
variables: (localFormatted.variables || []).map((v) => ({ ...v })),
|
|
1348
|
+
variableCollections: (localFormatted.collections || []).map((c) => ({ ...c })),
|
|
1349
|
+
},
|
|
1350
|
+
timestamp: Date.now(),
|
|
1351
|
+
});
|
|
1352
|
+
logger.info({ fileKey, variableCount: localFormatted.variables?.length }, "Cached full REST API variables dataset");
|
|
1353
|
+
}
|
|
1354
|
+
// Honor format='summary' on the REST path (historically only the
|
|
1355
|
+
// cache and Desktop Bridge paths summarized). Runs AFTER the
|
|
1356
|
+
// cache write so summary calls still populate the full dataset.
|
|
1357
|
+
if (format === 'summary') {
|
|
1358
|
+
const summary = generateSummary({
|
|
1359
|
+
fileKey,
|
|
1360
|
+
timestamp: Date.now(),
|
|
1361
|
+
source: 'rest_api',
|
|
1362
|
+
variables: localFormatted.variables,
|
|
1363
|
+
variableCollections: localFormatted.collections,
|
|
1364
|
+
});
|
|
1365
|
+
logger.info({ fileKey, estimatedTokens: estimateTokens(summary) }, 'Generated summary from REST API data');
|
|
1366
|
+
return {
|
|
1367
|
+
content: [
|
|
1368
|
+
{
|
|
1369
|
+
type: "text",
|
|
1370
|
+
text: JSON.stringify({
|
|
1371
|
+
fileKey,
|
|
1372
|
+
source: "rest_api",
|
|
1373
|
+
format: "summary",
|
|
1374
|
+
data: summary,
|
|
1375
|
+
}),
|
|
1376
|
+
},
|
|
1377
|
+
],
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1300
1380
|
// DEBUG: Check if valuesByMode exists before filtering
|
|
1301
1381
|
if (localFormatted.variables[0]) {
|
|
1302
1382
|
logger.info({
|
|
@@ -1374,31 +1454,8 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
|
|
|
1374
1454
|
publishedFormatted.variables = publishedFormatted.variables.slice(startIdx, endIdx);
|
|
1375
1455
|
}
|
|
1376
1456
|
}
|
|
1377
|
-
// Cache
|
|
1378
|
-
|
|
1379
|
-
fileKey,
|
|
1380
|
-
local: {
|
|
1381
|
-
summary: localFormatted.summary,
|
|
1382
|
-
collections: localFormatted.collections,
|
|
1383
|
-
variables: localFormatted.variables,
|
|
1384
|
-
},
|
|
1385
|
-
...(includePublished &&
|
|
1386
|
-
publishedFormatted && {
|
|
1387
|
-
published: {
|
|
1388
|
-
summary: publishedFormatted.summary,
|
|
1389
|
-
collections: publishedFormatted.collections,
|
|
1390
|
-
variables: publishedFormatted.variables,
|
|
1391
|
-
},
|
|
1392
|
-
}),
|
|
1393
|
-
verbosity: verbosity || "standard",
|
|
1394
|
-
enriched: enrich || false,
|
|
1395
|
-
timestamp: Date.now(),
|
|
1396
|
-
source: "rest_api",
|
|
1397
|
-
};
|
|
1398
|
-
if (variablesCache) {
|
|
1399
|
-
variablesCache.set(fileKey, { data: dataForCache, timestamp: Date.now() });
|
|
1400
|
-
logger.info({ fileKey }, "Cached REST API variables");
|
|
1401
|
-
}
|
|
1457
|
+
// (Cache write happens above, pre-mutation — the response below
|
|
1458
|
+
// is built from the filtered/paginated working copies.)
|
|
1402
1459
|
// Apply alias resolution if requested (REST API format has local.variables)
|
|
1403
1460
|
if (resolveAliases && localFormatted.variables?.length > 0) {
|
|
1404
1461
|
// Build maps from local variables and collections
|
|
@@ -1742,6 +1799,46 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
|
|
|
1742
1799
|
if (!localError && local) {
|
|
1743
1800
|
let localFormatted = formatVariables(local);
|
|
1744
1801
|
let publishedFormatted = includePublished ? formatVariables(published) : null;
|
|
1802
|
+
// Cache the FULL dataset pre-mutation in the Desktop shape
|
|
1803
|
+
// (same poisoning fix as the primary REST path above).
|
|
1804
|
+
if (variablesCache) {
|
|
1805
|
+
evictOldestCacheEntry(variablesCache);
|
|
1806
|
+
variablesCache.set(fileKey, {
|
|
1807
|
+
data: {
|
|
1808
|
+
fileKey,
|
|
1809
|
+
source: "rest_api",
|
|
1810
|
+
timestamp: Date.now(),
|
|
1811
|
+
variables: (localFormatted.variables || []).map((v) => ({ ...v })),
|
|
1812
|
+
variableCollections: (localFormatted.collections || []).map((c) => ({ ...c })),
|
|
1813
|
+
},
|
|
1814
|
+
timestamp: Date.now(),
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
// Honor format='summary' on the REST fallback path too
|
|
1818
|
+
// (after the cache write, so the full dataset is retained).
|
|
1819
|
+
if (format === 'summary') {
|
|
1820
|
+
const summary = generateSummary({
|
|
1821
|
+
fileKey,
|
|
1822
|
+
timestamp: Date.now(),
|
|
1823
|
+
source: 'rest_api',
|
|
1824
|
+
variables: localFormatted.variables,
|
|
1825
|
+
variableCollections: localFormatted.collections,
|
|
1826
|
+
});
|
|
1827
|
+
logger.info({ fileKey, estimatedTokens: estimateTokens(summary) }, 'Generated summary from REST API fallback data');
|
|
1828
|
+
return {
|
|
1829
|
+
content: [
|
|
1830
|
+
{
|
|
1831
|
+
type: "text",
|
|
1832
|
+
text: JSON.stringify({
|
|
1833
|
+
fileKey,
|
|
1834
|
+
source: "rest_api",
|
|
1835
|
+
format: "summary",
|
|
1836
|
+
data: summary,
|
|
1837
|
+
}),
|
|
1838
|
+
},
|
|
1839
|
+
],
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1745
1842
|
// Apply filters
|
|
1746
1843
|
if (format === 'filtered') {
|
|
1747
1844
|
const filteredLocal = applyFilters({ variables: localFormatted.variables, variableCollections: localFormatted.collections }, { collection, namePattern, mode }, verbosity || "standard");
|
|
@@ -1752,19 +1849,6 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
|
|
|
1752
1849
|
const verbosityFiltered = applyFilters({ variables: localFormatted.variables, variableCollections: localFormatted.collections }, {}, verbosity);
|
|
1753
1850
|
localFormatted = { ...localFormatted, collections: verbosityFiltered.variableCollections, variables: verbosityFiltered.variables };
|
|
1754
1851
|
}
|
|
1755
|
-
// Cache
|
|
1756
|
-
const dataForCache = {
|
|
1757
|
-
fileKey,
|
|
1758
|
-
local: { summary: localFormatted.summary, collections: localFormatted.collections, variables: localFormatted.variables },
|
|
1759
|
-
...(includePublished && publishedFormatted && { published: { summary: publishedFormatted.summary, collections: publishedFormatted.collections, variables: publishedFormatted.variables } }),
|
|
1760
|
-
verbosity: verbosity || "standard",
|
|
1761
|
-
enriched: enrich || false,
|
|
1762
|
-
timestamp: Date.now(),
|
|
1763
|
-
source: "rest_api",
|
|
1764
|
-
};
|
|
1765
|
-
if (variablesCache) {
|
|
1766
|
-
variablesCache.set(fileKey, { data: dataForCache, timestamp: Date.now() });
|
|
1767
|
-
}
|
|
1768
1852
|
// Apply alias resolution
|
|
1769
1853
|
if (resolveAliases && localFormatted.variables?.length > 0) {
|
|
1770
1854
|
const allVariablesMap = new Map();
|
|
@@ -1825,6 +1909,27 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
|
|
|
1825
1909
|
catch (error) {
|
|
1826
1910
|
logger.error({ error }, "Failed to get variables");
|
|
1827
1911
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1912
|
+
// A 403 here has two very different causes: an expired/invalid token
|
|
1913
|
+
// (isAuthError, set centrally by FigmaAPI.request) or the Variables
|
|
1914
|
+
// API's Enterprise plan gate. Only the latter should be diagnosed as
|
|
1915
|
+
// "requires Enterprise" — telling a user with a dead token to buy
|
|
1916
|
+
// Enterprise is actively wrong.
|
|
1917
|
+
const isTokenError = error?.isAuthError === true;
|
|
1918
|
+
if (isTokenError) {
|
|
1919
|
+
return {
|
|
1920
|
+
content: [
|
|
1921
|
+
{
|
|
1922
|
+
type: "text",
|
|
1923
|
+
text: JSON.stringify({
|
|
1924
|
+
error: errorMessage,
|
|
1925
|
+
message: "Failed to retrieve Figma variables — REST token is expired or invalid",
|
|
1926
|
+
hint: "Generate a new personal access token at figma.com → Settings → Security → Personal access tokens and update FIGMA_ACCESS_TOKEN. Or open the Desktop Bridge plugin in Figma Desktop — variables work on any plan without a REST token.",
|
|
1927
|
+
}),
|
|
1928
|
+
},
|
|
1929
|
+
],
|
|
1930
|
+
isError: true,
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1828
1933
|
// FIXED: Jump directly to Styles API (fast) instead of full file data (slow)
|
|
1829
1934
|
if (errorMessage.includes("403")) {
|
|
1830
1935
|
try {
|
|
@@ -1879,11 +1984,12 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
|
|
|
1879
1984
|
"The file structure may not contain extractable styles",
|
|
1880
1985
|
"There may be a network or authentication issue"
|
|
1881
1986
|
],
|
|
1882
|
-
suggestion: "Please ensure the file is accessible and try again, or check if your token has the necessary permissions.",
|
|
1987
|
+
suggestion: "Please ensure the file is accessible and try again, or check if your token has the necessary permissions. With the Desktop Bridge plugin open in Figma Desktop, variables work on any plan without a REST token.",
|
|
1883
1988
|
technical: styleError instanceof Error ? styleError.message : String(styleError)
|
|
1884
1989
|
}),
|
|
1885
1990
|
},
|
|
1886
1991
|
],
|
|
1992
|
+
isError: true,
|
|
1887
1993
|
};
|
|
1888
1994
|
}
|
|
1889
1995
|
}
|
|
@@ -1896,8 +2002,8 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
|
|
|
1896
2002
|
error: errorMessage,
|
|
1897
2003
|
message: "Failed to retrieve Figma variables",
|
|
1898
2004
|
hint: errorMessage.includes("403")
|
|
1899
|
-
? "Variables API requires Enterprise plan.
|
|
1900
|
-
: "Make sure FIGMA_ACCESS_TOKEN is configured and has appropriate permissions",
|
|
2005
|
+
? "The Variables REST API requires an Enterprise plan. Open the Desktop Bridge plugin in Figma Desktop instead — variables work on any plan through the bridge."
|
|
2006
|
+
: "Make sure FIGMA_ACCESS_TOKEN is configured and has appropriate permissions, or open the Desktop Bridge plugin in Figma Desktop",
|
|
1901
2007
|
}),
|
|
1902
2008
|
},
|
|
1903
2009
|
],
|
|
@@ -2251,14 +2357,15 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
|
|
|
2251
2357
|
return adaptiveResponse(finalResponse, {
|
|
2252
2358
|
toolName: "figma_get_styles",
|
|
2253
2359
|
compressionCallback: (adjustedLevel) => {
|
|
2254
|
-
// Re-apply style filtering with lower verbosity
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2360
|
+
// Re-apply style filtering with lower verbosity. Clamp the
|
|
2361
|
+
// emergency "inventory" tier to "summary" (filterStyle has no
|
|
2362
|
+
// inventory branch) and always re-filter — even for
|
|
2363
|
+
// verbosity='full' callers — so compression can never echo
|
|
2364
|
+
// the unfiltered payload.
|
|
2365
|
+
const level = (adjustedLevel === "standard" ? "standard" : "summary");
|
|
2259
2366
|
return {
|
|
2260
2367
|
...finalResponse,
|
|
2261
|
-
styles:
|
|
2368
|
+
styles: styles.map((style) => filterStyle(style, level)),
|
|
2262
2369
|
verbosity: level,
|
|
2263
2370
|
};
|
|
2264
2371
|
},
|
|
@@ -2310,6 +2417,9 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
|
|
|
2310
2417
|
.describe("Image format (default: png)"),
|
|
2311
2418
|
}, async ({ fileUrl, nodeId, scale, format }) => {
|
|
2312
2419
|
try {
|
|
2420
|
+
// Accept URL-format ids (123-456); the REST response maps key by
|
|
2421
|
+
// colon format (123:456).
|
|
2422
|
+
nodeId = nodeId.replace(/-/g, ":");
|
|
2313
2423
|
let api;
|
|
2314
2424
|
try {
|
|
2315
2425
|
api = await getFigmaAPI();
|
|
@@ -3151,8 +3261,14 @@ export function registerFigmaAPITools(server, getFigmaAPI, getCurrentUrl, variab
|
|
|
3151
3261
|
text: JSON.stringify({
|
|
3152
3262
|
success: true,
|
|
3153
3263
|
instance: result.instance,
|
|
3264
|
+
// Plugin-side warnings (e.g. property names that didn't
|
|
3265
|
+
// match anything) — without these a partial apply looks
|
|
3266
|
+
// like a full success.
|
|
3267
|
+
warnings: result.warnings?.length ? result.warnings : undefined,
|
|
3154
3268
|
metadata: {
|
|
3155
|
-
note:
|
|
3269
|
+
note: result.warnings?.length
|
|
3270
|
+
? "Some properties were applied, but see warnings — not every requested property matched. Use figma_capture_screenshot to verify visual changes."
|
|
3271
|
+
: "Instance properties updated successfully. Use figma_capture_screenshot to verify visual changes.",
|
|
3156
3272
|
},
|
|
3157
3273
|
}),
|
|
3158
3274
|
},
|