@atezer/figma-mcp-bridge 1.7.29 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +403 -0
- package/README.md +4 -3
- package/agents/_orchestrator-protocol.md +185 -0
- package/agents/ds-auditor.md +73 -22
- package/agents/screen-builder.md +60 -22
- package/agents/token-syncer.md +63 -19
- package/dist/core/code-warnings.d.ts +38 -0
- package/dist/core/code-warnings.d.ts.map +1 -0
- package/dist/core/code-warnings.js +191 -0
- package/dist/core/code-warnings.js.map +1 -0
- package/dist/core/device-presets.d.ts +49 -0
- package/dist/core/device-presets.d.ts.map +1 -0
- package/dist/core/device-presets.js +141 -0
- package/dist/core/device-presets.js.map +1 -0
- package/dist/core/instructions.d.ts +4 -2
- package/dist/core/instructions.d.ts.map +1 -1
- package/dist/core/instructions.js +239 -29
- package/dist/core/instructions.js.map +1 -1
- package/dist/core/plugin-bridge-connector.d.ts +32 -0
- package/dist/core/plugin-bridge-connector.d.ts.map +1 -1
- package/dist/core/plugin-bridge-connector.js +31 -2
- package/dist/core/plugin-bridge-connector.js.map +1 -1
- package/dist/core/plugin-bridge-server.d.ts +8 -0
- package/dist/core/plugin-bridge-server.d.ts.map +1 -1
- package/dist/core/plugin-bridge-server.js +27 -2
- package/dist/core/plugin-bridge-server.js.map +1 -1
- package/dist/core/response-guard.d.ts +23 -0
- package/dist/core/response-guard.d.ts.map +1 -1
- package/dist/core/response-guard.js +113 -0
- package/dist/core/response-guard.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.d.ts.map +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/local-plugin-only.d.ts.map +1 -1
- package/dist/local-plugin-only.js +504 -85
- package/dist/local-plugin-only.js.map +1 -1
- package/f-mcp-plugin/code.js +514 -29
- package/f-mcp-plugin/ui.html +62 -6
- package/package.json +1 -1
- package/skills/SKILL_INDEX.md +13 -1
- package/skills/apply-figma-design-system/SKILL.md +37 -0
- package/skills/audit-figma-design-system/SKILL.md +38 -0
- package/skills/code-design-mapper/SKILL.md +37 -0
- package/skills/design-token-pipeline/SKILL.md +44 -0
- package/skills/figma-canvas-ops/SKILL.md +200 -240
- package/skills/fmcp-ds-audit-orchestrator/SKILL.md +205 -0
- package/skills/fmcp-intent-router/SKILL.md +574 -0
- package/skills/fmcp-project-rules/SKILL.md +9 -5
- package/skills/fmcp-screen-orchestrator/SKILL.md +166 -0
- package/skills/fmcp-screen-recipes/SKILL.md +528 -0
- package/skills/fmcp-token-sync-orchestrator/SKILL.md +198 -0
- package/skills/generate-figma-library/SKILL.md +38 -0
- package/skills/generate-figma-screen/SKILL.md +382 -19
- package/skills/implement-design/SKILL.md +32 -0
- package/skills/inspiration-intake/SKILL.md +220 -0
- package/skills/visual-qa-compare/SKILL.md +33 -0
|
@@ -17,11 +17,22 @@ import { createChildLogger } from "./core/logger.js";
|
|
|
17
17
|
import { PluginBridgeServer } from "./core/plugin-bridge-server.js";
|
|
18
18
|
import { PluginBridgeConnector } from "./core/plugin-bridge-connector.js";
|
|
19
19
|
import { parseFigmaUrl } from "./core/figma-url.js";
|
|
20
|
-
import { truncateRestResponse } from "./core/response-guard.js";
|
|
20
|
+
import { truncateRestResponse, truncatePluginResponse } from "./core/response-guard.js";
|
|
21
|
+
import { analyzeCodeForWarnings } from "./core/code-warnings.js";
|
|
22
|
+
import { resolveDevice, DEVICE_PRESETS } from "./core/device-presets.js";
|
|
21
23
|
import { closeAuditLog } from "./core/audit-log.js";
|
|
22
24
|
import { FMCP_VERSION } from "./core/version.js";
|
|
23
25
|
import { ResponseCache } from "./core/response-cache.js";
|
|
24
26
|
const logger = createChildLogger({ component: "plugin-only-mcp" });
|
|
27
|
+
/**
|
|
28
|
+
* Legacy default flag — when set, restores pre-v1.8.0 default values for
|
|
29
|
+
* read-only tools (depth=2, verbosity="standard", scale=2, format="PNG").
|
|
30
|
+
* Allows downstream consumers to opt back into the heavier defaults during
|
|
31
|
+
* the v1.8.0 → v1.9.0 transition. Will be removed in v1.9.0.
|
|
32
|
+
*
|
|
33
|
+
* Set: FMCP_LEGACY_DEFAULTS=1
|
|
34
|
+
*/
|
|
35
|
+
const LEGACY_DEFAULTS = process.env.FMCP_LEGACY_DEFAULTS === "1";
|
|
25
36
|
/** Resolve fileKey from figmaUrl (parse) or explicit fileKey. Returns undefined if neither yields a key. */
|
|
26
37
|
function resolveFileKey(figmaUrl, explicitFileKey) {
|
|
27
38
|
if (explicitFileKey && explicitFileKey.trim())
|
|
@@ -98,6 +109,8 @@ function getErrorHint(category) {
|
|
|
98
109
|
default: return "Hata mesajini kontrol et.";
|
|
99
110
|
}
|
|
100
111
|
}
|
|
112
|
+
// analyzeCodeForWarnings + CodeWarning type moved to ./core/code-warnings.ts (v1.8.1)
|
|
113
|
+
// Imported at the top of this file — this comment marks where the helper used to live.
|
|
101
114
|
/** Wrap a tool handler with try-catch to prevent unhandled rejections. */
|
|
102
115
|
function safeToolHandler(handler) {
|
|
103
116
|
return async (params) => {
|
|
@@ -136,6 +149,49 @@ export async function main() {
|
|
|
136
149
|
const cache = new ResponseCache();
|
|
137
150
|
/** Invalidate cache after any mutating operation. */
|
|
138
151
|
function invalidateCache() { cache.invalidate(); }
|
|
152
|
+
/**
|
|
153
|
+
* Build a stable cache key from tool name + params. Sort keys for determinism.
|
|
154
|
+
* fileKey is included to avoid cross-file leakage in multi-file sessions.
|
|
155
|
+
*/
|
|
156
|
+
function makeCacheKey(toolName, params) {
|
|
157
|
+
const sortedEntries = Object.entries(params)
|
|
158
|
+
.filter(([_, v]) => v !== undefined)
|
|
159
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
160
|
+
return `${toolName}::${JSON.stringify(sortedEntries)}`;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Shared envelope wrapper for plugin tool results. Applies truncatePluginResponse
|
|
164
|
+
* unless skipGuard is true (for cache hits whose data was already guarded).
|
|
165
|
+
* When debug=true, the _responseGuard marker is preserved; otherwise stripped.
|
|
166
|
+
*/
|
|
167
|
+
function toolResult(data, toolName, opts) {
|
|
168
|
+
let payload;
|
|
169
|
+
if (data === undefined || data === null) {
|
|
170
|
+
payload = { success: false, error: "No data from plugin" };
|
|
171
|
+
}
|
|
172
|
+
else if (opts?.skipGuard) {
|
|
173
|
+
payload = data;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
const result = truncatePluginResponse(data, toolName);
|
|
177
|
+
payload = result.data;
|
|
178
|
+
// Strip _responseGuard marker unless debug=true
|
|
179
|
+
if (!opts?.debug && payload && typeof payload === "object" && payload._responseGuard) {
|
|
180
|
+
const stripped = { ...payload };
|
|
181
|
+
delete stripped._responseGuard;
|
|
182
|
+
payload = stripped;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const text = typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
186
|
+
return { content: [{ type: "text", text }] };
|
|
187
|
+
}
|
|
188
|
+
/** Helper for error responses with consistent shape. */
|
|
189
|
+
function errorResult(msg) {
|
|
190
|
+
return {
|
|
191
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }) }],
|
|
192
|
+
isError: true,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
139
195
|
const server = new McpServer({
|
|
140
196
|
name: "F-MCP ATezer Bridge (Plugin-only)",
|
|
141
197
|
version: FMCP_VERSION,
|
|
@@ -162,8 +218,9 @@ export async function main() {
|
|
|
162
218
|
};
|
|
163
219
|
});
|
|
164
220
|
// ---- figma_get_file_data_plugin (no REST, no token) ----
|
|
221
|
+
// v1.8.0: Defaults already conservative (depth=1, verbosity="summary"). Cached + truncated.
|
|
165
222
|
server.registerTool("figma_get_file_data", {
|
|
166
|
-
description: "Get file structure and document tree from the open Figma file. No REST API or token. Use fileKey or figmaUrl to target a specific file when multiple plugins are connected
|
|
223
|
+
description: "Get file structure and document tree from the open Figma file. No REST API or token. Use fileKey or figmaUrl to target a specific file when multiple plugins are connected. Defaults to depth=1, verbosity='summary'. Cached 60s per session.",
|
|
167
224
|
inputSchema: {
|
|
168
225
|
figmaUrl: z.string().optional().describe("Figma or FigJam file URL; fileKey is extracted from the link for routing."),
|
|
169
226
|
fileKey: z.string().optional().describe("Target a specific connected file. Use figma_list_connected_files to see available files."),
|
|
@@ -174,16 +231,14 @@ export async function main() {
|
|
|
174
231
|
includeTypography: z.boolean().optional(),
|
|
175
232
|
includeCodeReady: z.boolean().optional(),
|
|
176
233
|
outputHint: z.enum(["react", "tailwind"]).optional(),
|
|
234
|
+
debug: z.boolean().optional().describe("Bypass cache and include _responseGuard fields."),
|
|
177
235
|
},
|
|
178
236
|
annotations: { readOnlyHint: true },
|
|
179
|
-
}, async ({ figmaUrl, fileKey, depth, verbosity, includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint }) => {
|
|
237
|
+
}, async ({ figmaUrl, fileKey, depth, verbosity, includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint, debug }) => {
|
|
180
238
|
try {
|
|
181
239
|
const resolvedKey = resolveFileKey(figmaUrl, fileKey);
|
|
182
240
|
if (figmaUrl && !resolvedKey) {
|
|
183
|
-
return
|
|
184
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Invalid Figma/FigJam URL: could not extract file key." }) }],
|
|
185
|
-
isError: true,
|
|
186
|
-
};
|
|
241
|
+
return errorResult("Invalid Figma/FigJam URL: could not extract file key.");
|
|
187
242
|
}
|
|
188
243
|
const conn = getConnector(bridge, resolvedKey);
|
|
189
244
|
const opts = includeLayout !== undefined ||
|
|
@@ -193,13 +248,12 @@ export async function main() {
|
|
|
193
248
|
outputHint !== undefined
|
|
194
249
|
? { includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint }
|
|
195
250
|
: undefined;
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return { content: [{ type: "text", text }] };
|
|
251
|
+
const cacheK = makeCacheKey("figma_get_file_data", { resolvedKey, depth, verbosity, opts });
|
|
252
|
+
const cached = debug ? null : cache.get(cacheK, 60_000);
|
|
253
|
+
const data = cached ?? await conn.getDocumentStructure(depth, verbosity, opts);
|
|
254
|
+
if (!cached)
|
|
255
|
+
cache.set(cacheK, data);
|
|
256
|
+
return toolResult(data, "figma_get_file_data", { skipGuard: !!cached, debug });
|
|
203
257
|
}
|
|
204
258
|
catch (err) {
|
|
205
259
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -210,23 +264,26 @@ export async function main() {
|
|
|
210
264
|
}
|
|
211
265
|
});
|
|
212
266
|
// ---- figma_get_design_context (get_design_context tarzı, token tasarruflu, Figma token yok) ----
|
|
267
|
+
// v1.8.0: Context-safe defaults — depth=1, verbosity="summary"
|
|
268
|
+
// Override with FMCP_LEGACY_DEFAULTS=1 env var or pass explicit params.
|
|
213
269
|
server.registerTool("figma_get_design_context", {
|
|
214
|
-
description: "Design context for a node or whole file: structure + text, layout/visual/typography.
|
|
270
|
+
description: "Design context for a node or whole file: structure + text, layout/visual/typography. Defaults to depth=1, verbosity='summary' for context safety. Pass depth/verbosity explicitly for deeper data. Cached 60s per session.",
|
|
215
271
|
inputSchema: {
|
|
216
272
|
figmaUrl: z.string().optional().describe("Figma or FigJam file URL; fileKey and optional node-id are extracted for routing."),
|
|
217
273
|
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
218
274
|
nodeId: z.string().optional(),
|
|
219
|
-
depth: z.number().min(0).max(3).optional().default(2),
|
|
220
|
-
verbosity: z.enum(["summary", "standard", "full"]).optional().default("standard"),
|
|
221
|
-
excludeScreenshot: z.boolean().optional(),
|
|
275
|
+
depth: z.number().min(0).max(3).optional().default(LEGACY_DEFAULTS ? 2 : 1),
|
|
276
|
+
verbosity: z.enum(["summary", "standard", "full"]).optional().default(LEGACY_DEFAULTS ? "standard" : "summary"),
|
|
277
|
+
excludeScreenshot: z.boolean().optional().describe("Reserved for future use; plugin currently does not embed screenshots in design_context."),
|
|
222
278
|
includeLayout: z.boolean().optional(),
|
|
223
279
|
includeVisual: z.boolean().optional(),
|
|
224
280
|
includeTypography: z.boolean().optional(),
|
|
225
281
|
includeCodeReady: z.boolean().optional(),
|
|
226
282
|
outputHint: z.enum(["react", "tailwind"]).optional(),
|
|
283
|
+
debug: z.boolean().optional().describe("Bypass cache and include _responseGuard/_metrics fields."),
|
|
227
284
|
},
|
|
228
285
|
annotations: { readOnlyHint: true },
|
|
229
|
-
}, async ({ figmaUrl, fileKey, nodeId, depth, verbosity, excludeScreenshot, includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint }) => {
|
|
286
|
+
}, async ({ figmaUrl, fileKey, nodeId, depth, verbosity, excludeScreenshot, includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint, debug }) => {
|
|
230
287
|
try {
|
|
231
288
|
const { fileKey: resolvedKey, nodeId: resolvedNodeId } = resolveDesignContextParams({ figmaUrl, fileKey, nodeId });
|
|
232
289
|
if (figmaUrl && !resolvedKey) {
|
|
@@ -245,15 +302,15 @@ export async function main() {
|
|
|
245
302
|
? { excludeScreenshot, includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint }
|
|
246
303
|
: undefined;
|
|
247
304
|
const effectiveNodeId = resolvedNodeId ?? nodeId?.trim();
|
|
248
|
-
|
|
305
|
+
// Cache lookup (60s TTL) — bypassed when debug=true
|
|
306
|
+
const cacheK = makeCacheKey("figma_get_design_context", { resolvedKey, effectiveNodeId, depth, verbosity, opts });
|
|
307
|
+
const cached = debug ? null : cache.get(cacheK, 60_000);
|
|
308
|
+
const data = cached ?? (effectiveNodeId
|
|
249
309
|
? await conn.getNodeContext(effectiveNodeId, depth, verbosity, opts)
|
|
250
|
-
: await conn.getDocumentStructure(depth, verbosity, opts);
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
? data
|
|
255
|
-
: JSON.stringify(data);
|
|
256
|
-
return { content: [{ type: "text", text }] };
|
|
310
|
+
: await conn.getDocumentStructure(depth, verbosity, opts));
|
|
311
|
+
if (!cached)
|
|
312
|
+
cache.set(cacheK, data);
|
|
313
|
+
return toolResult(data, "figma_get_design_context", { skipGuard: !!cached, debug });
|
|
257
314
|
}
|
|
258
315
|
catch (err) {
|
|
259
316
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -328,7 +385,11 @@ export async function main() {
|
|
|
328
385
|
}));
|
|
329
386
|
// ---- figma_execute ----
|
|
330
387
|
server.registerTool("figma_execute", {
|
|
331
|
-
description: "Run JavaScript in the Figma plugin context. Full Plugin API available. Use fileKey or figmaUrl to target a specific file."
|
|
388
|
+
description: "Run JavaScript in the Figma plugin context. Full Plugin API available. Use fileKey or figmaUrl to target a specific file. " +
|
|
389
|
+
"v1.8.1+: Static analysis detects design-system discipline violations (hardcoded colors, missing token bindings, no-instance usage, hardcoded typography). " +
|
|
390
|
+
"SEVERE warnings are promoted to the top of the response as _designSystemViolations — Claude must read and self-correct. " +
|
|
391
|
+
"Also detects gotchas: FILL/ABSOLUTE before appendChild, sync API usage, missing loadFontAsync, sync currentPage assignment. " +
|
|
392
|
+
"For component instances: use setProperties({...}), NOT findAll(TEXT).",
|
|
332
393
|
inputSchema: {
|
|
333
394
|
figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
|
|
334
395
|
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
@@ -343,8 +404,26 @@ export async function main() {
|
|
|
343
404
|
isError: true,
|
|
344
405
|
};
|
|
345
406
|
}
|
|
346
|
-
const clampedTimeout = Math.max(3000, Math.min(timeout ?? 15000,
|
|
407
|
+
const clampedTimeout = Math.max(3000, Math.min(timeout ?? 15000, 30000));
|
|
347
408
|
invalidateCache();
|
|
409
|
+
// v1.8.1: Structured warnings with SEVERE vs ADVISORY severity
|
|
410
|
+
const codeWarnings = analyzeCodeForWarnings(code);
|
|
411
|
+
const severeWarnings = codeWarnings.filter((w) => w.severity === "SEVERE");
|
|
412
|
+
const advisoryWarnings = codeWarnings.filter((w) => w.severity === "ADVISORY");
|
|
413
|
+
// SEVERE warnings go to a prominent field that Claude cannot ignore
|
|
414
|
+
const dsViolations = severeWarnings.length > 0
|
|
415
|
+
? {
|
|
416
|
+
_designSystemViolations: {
|
|
417
|
+
count: severeWarnings.length,
|
|
418
|
+
message: "⚠️ DESIGN SYSTEM DISIPLIN IHLALI TESPIT EDILDI — kodu duzelt ve tekrar calistir. Bu ekran kabul edilmez.",
|
|
419
|
+
violations: severeWarnings.map((w) => ({ category: w.category, message: w.message })),
|
|
420
|
+
action: "Her ihlal icin uygun DS token binding veya component instance kullan. Detay: figma-canvas-ops SKILL Kural 10.",
|
|
421
|
+
},
|
|
422
|
+
}
|
|
423
|
+
: {};
|
|
424
|
+
const warningsField = advisoryWarnings.length > 0
|
|
425
|
+
? { _warnings: advisoryWarnings.map((w) => w.message) }
|
|
426
|
+
: {};
|
|
348
427
|
const startTime = Date.now();
|
|
349
428
|
try {
|
|
350
429
|
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
@@ -362,10 +441,12 @@ export async function main() {
|
|
|
362
441
|
catch { /* safe fallback */ }
|
|
363
442
|
return {
|
|
364
443
|
content: [{ type: "text", text: JSON.stringify({
|
|
444
|
+
...dsViolations, // v1.8.1: SEVERE warnings FIRST, before anything else
|
|
365
445
|
...result,
|
|
366
446
|
errorCategory: category,
|
|
367
447
|
_metrics: { durationMs, timeoutMs: clampedTimeout },
|
|
368
448
|
hint,
|
|
449
|
+
...warningsField,
|
|
369
450
|
}) }],
|
|
370
451
|
isError: true,
|
|
371
452
|
};
|
|
@@ -373,8 +454,15 @@ export async function main() {
|
|
|
373
454
|
let enriched;
|
|
374
455
|
try {
|
|
375
456
|
enriched = typeof result === "object" && result !== null
|
|
376
|
-
? {
|
|
377
|
-
|
|
457
|
+
? {
|
|
458
|
+
...dsViolations, // v1.8.1: SEVERE warnings at top level
|
|
459
|
+
...result,
|
|
460
|
+
_metrics: { durationMs, timeoutMs: clampedTimeout },
|
|
461
|
+
...warningsField,
|
|
462
|
+
}
|
|
463
|
+
: severeWarnings.length > 0 || advisoryWarnings.length > 0
|
|
464
|
+
? { ...dsViolations, result, ...warningsField }
|
|
465
|
+
: result;
|
|
378
466
|
}
|
|
379
467
|
catch {
|
|
380
468
|
enriched = result;
|
|
@@ -399,26 +487,122 @@ export async function main() {
|
|
|
399
487
|
error: msg,
|
|
400
488
|
_metrics: { durationMs, timeoutMs: clampedTimeout },
|
|
401
489
|
hint,
|
|
490
|
+
...warningsField,
|
|
402
491
|
}) }],
|
|
403
492
|
isError: true,
|
|
404
493
|
};
|
|
405
494
|
}
|
|
406
495
|
}));
|
|
496
|
+
// ============================================================================
|
|
497
|
+
// v1.8.1: HIGH-LEVEL "FAST AND CORRECT" TOOLS
|
|
498
|
+
// ============================================================================
|
|
499
|
+
//
|
|
500
|
+
// These tools sit above figma_execute and provide one-call solutions for
|
|
501
|
+
// common workflows. Claude should prefer these over hand-written execute
|
|
502
|
+
// code when possible — they handle token binding, auto-layout preservation,
|
|
503
|
+
// and DS instance preservation automatically.
|
|
504
|
+
// ============================================================================
|
|
505
|
+
// ---- figma_clone_screen_to_device ----
|
|
506
|
+
// v1.8.2: NARROW USE CASE ONLY — device migration.
|
|
507
|
+
// For alternatives/variations/new designs, use figma_execute with the
|
|
508
|
+
// generate-figma-screen SKILL Step 5 "build from scratch" pattern.
|
|
509
|
+
// Clone will copy benchmark's existing mistakes (hardcoded rectangles,
|
|
510
|
+
// missing token bindings, non-responsive layouts).
|
|
511
|
+
server.registerTool("figma_clone_screen_to_device", {
|
|
512
|
+
description: "⚠️ NARROW USE CASE — Device migration ONLY. Clone a Figma screen to a target device " +
|
|
513
|
+
"dimension, preserving library instances, bound variables, and auto-layout. " +
|
|
514
|
+
"USE ONLY WHEN: same design system + same layout structure + only screen size changes. " +
|
|
515
|
+
"DO NOT USE FOR: creating alternatives, variations, or new designs — these REQUIRE " +
|
|
516
|
+
"building from scratch with figma_execute following the generate-figma-screen SKILL " +
|
|
517
|
+
"Step 5 pattern (search_assets → instantiate_component → setBoundVariable → auto-layout FILL). " +
|
|
518
|
+
"Clone copies benchmark's EXISTING mistakes (hardcoded rectangles, missing token bindings, " +
|
|
519
|
+
"non-responsive layouts). Benchmark is INSPIRATION, not a copy source for variations. " +
|
|
520
|
+
"If the user says 'alternatif', 'varyasyon', 'farklı', 'yeni', 'tasarla' — USE figma_execute + Step 5, NOT this tool. " +
|
|
521
|
+
"Device presets: iPhone 17, iPhone 16 Pro Max, Android Compact, iPad Pro 11, Desktop, " +
|
|
522
|
+
"and more. Custom: 'WxH' format.",
|
|
523
|
+
inputSchema: {
|
|
524
|
+
figmaUrl: z.string().optional().describe("Figma file URL for routing."),
|
|
525
|
+
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
526
|
+
sourceNodeId: z.string().describe("Node ID of the source screen to clone (e.g. '139:3407')"),
|
|
527
|
+
targetDevice: z.string().describe("Device preset name (e.g. 'iPhone 17', 'Android Compact') or custom 'WxH' (e.g. '1200x800')"),
|
|
528
|
+
newName: z.string().optional().describe("Name for the cloned screen (default: source name + device suffix)"),
|
|
529
|
+
targetParentId: z.string().optional().describe("Parent node to place the clone under (default: current page)"),
|
|
530
|
+
position: z
|
|
531
|
+
.object({ x: z.number(), y: z.number() })
|
|
532
|
+
.optional()
|
|
533
|
+
.describe("Explicit position for the clone (default: auto-placed right of source)"),
|
|
534
|
+
},
|
|
535
|
+
annotations: { destructiveHint: true },
|
|
536
|
+
}, safeToolHandler(async ({ figmaUrl, fileKey, sourceNodeId, targetDevice, newName, targetParentId, position, }) => {
|
|
537
|
+
// Resolve device name to concrete dimensions
|
|
538
|
+
const resolved = resolveDevice(targetDevice);
|
|
539
|
+
if (!resolved) {
|
|
540
|
+
return errorResult(`Unknown device preset '${targetDevice}'. Supported presets: ${DEVICE_PRESETS.map((p) => p.name).join(", ")}. For custom, use 'WxH' format (e.g. '1200x800').`);
|
|
541
|
+
}
|
|
542
|
+
invalidateCache();
|
|
543
|
+
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
544
|
+
const result = await conn.cloneScreenToDevice({
|
|
545
|
+
sourceNodeId,
|
|
546
|
+
targetWidth: resolved.width,
|
|
547
|
+
targetHeight: resolved.height,
|
|
548
|
+
targetDeviceName: resolved.name,
|
|
549
|
+
newName,
|
|
550
|
+
targetParentId,
|
|
551
|
+
position,
|
|
552
|
+
});
|
|
553
|
+
return toolResult(result, "figma_clone_screen_to_device");
|
|
554
|
+
}));
|
|
555
|
+
// ---- figma_validate_screen ----
|
|
556
|
+
// Post-creation audit tool. Walks a node tree and scores its DS compliance
|
|
557
|
+
// across 3 dimensions: instance coverage, token binding coverage, auto-layout
|
|
558
|
+
// coverage. Returns pass/fail + actionable violations.
|
|
559
|
+
server.registerTool("figma_validate_screen", {
|
|
560
|
+
description: "Validate a screen against design-system discipline criteria. Returns a compliance score (0-100) " +
|
|
561
|
+
"across 3 dimensions: instance coverage (library usage), token binding coverage (bound variables), " +
|
|
562
|
+
"and auto-layout coverage. Use this AFTER creating a screen to verify DS compliance. " +
|
|
563
|
+
"If score < minScore, Claude should delete the screen and rebuild it using DS components + token bindings. " +
|
|
564
|
+
"Read-only — never mutates the file.",
|
|
565
|
+
inputSchema: {
|
|
566
|
+
figmaUrl: z.string().optional().describe("Figma file URL for routing."),
|
|
567
|
+
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
568
|
+
nodeId: z.string().describe("Node ID of the screen to validate"),
|
|
569
|
+
expectedDs: z.string().optional().describe("Expected DS library name (e.g. '❖ SUI') for library match scoring"),
|
|
570
|
+
minScore: z.number().min(0).max(100).optional().default(80).describe("Minimum acceptable score (0-100). Below this, the screen is considered non-compliant."),
|
|
571
|
+
},
|
|
572
|
+
annotations: { readOnlyHint: true },
|
|
573
|
+
}, safeToolHandler(async ({ figmaUrl, fileKey, nodeId, expectedDs, minScore, }) => {
|
|
574
|
+
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
575
|
+
const result = await conn.validateScreen({ nodeId, expectedDs, minScore });
|
|
576
|
+
return toolResult(result, "figma_validate_screen");
|
|
577
|
+
}));
|
|
407
578
|
// ---- figma_capture_screenshot ----
|
|
579
|
+
// v1.8.0: Default JPG@1x (~80% smaller base64 vs PNG@2x). Override via params.
|
|
408
580
|
server.registerTool("figma_capture_screenshot", {
|
|
409
|
-
description: "Capture screenshot of a node or current view from the plugin. No REST API.
|
|
581
|
+
description: "Capture screenshot of a node or current view from the plugin. No REST API. Defaults to JPG@1x (q70) for context safety. Use scale/format/jpegQuality to override.",
|
|
410
582
|
inputSchema: {
|
|
411
583
|
figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
|
|
412
584
|
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
413
585
|
nodeId: z.string().optional(),
|
|
414
|
-
format: z.enum(["PNG", "JPG"]).optional().default("PNG"),
|
|
415
|
-
scale: z.number().optional().default(2),
|
|
586
|
+
format: z.enum(["PNG", "JPG"]).optional().default(LEGACY_DEFAULTS ? "PNG" : "JPG"),
|
|
587
|
+
scale: z.number().optional().default(LEGACY_DEFAULTS ? 2 : 1),
|
|
588
|
+
jpegQuality: z.number().min(30).max(100).optional().default(70).describe("JPEG quality 30-100. Ignored when format=PNG."),
|
|
416
589
|
},
|
|
417
590
|
annotations: { readOnlyHint: true },
|
|
418
|
-
}, safeToolHandler(async ({ figmaUrl, fileKey, nodeId, format, scale }) => {
|
|
591
|
+
}, safeToolHandler(async ({ figmaUrl, fileKey, nodeId, format, scale, jpegQuality }) => {
|
|
419
592
|
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
420
|
-
const result = await conn.captureScreenshot(nodeId ?? null, { format, scale });
|
|
421
|
-
|
|
593
|
+
const result = await conn.captureScreenshot(nodeId ?? null, { format, scale, jpegQuality });
|
|
594
|
+
// v1.9.6 Fix M1 — skipGuard: true → truncatePluginResponse'u bypass et.
|
|
595
|
+
// Gerekçe: screenshot response {success, image: {base64, ...}} şeklinde.
|
|
596
|
+
// pruneNodeTree screenshot payload'una (node tree değil) uymadığı için
|
|
597
|
+
// final fallback truncateResponse({maxStringLength: 200}) devreye girip
|
|
598
|
+
// base64 string'i 200 karaktere kırpıyordu → Claude Desktop `{}` empty
|
|
599
|
+
// object görüyordu (FP-1-R-v2 Known Limitation #8 root cause).
|
|
600
|
+
// skipGuard: true ile payload olduğu gibi JSON.stringify edilir, base64
|
|
601
|
+
// intact kalır. Claude image render etmez (text content block) ama
|
|
602
|
+
// base64 string'i görür, kullanabilir, böylece empty object problemi
|
|
603
|
+
// çözülür. Image content block upgrade'i (MCP image type) Part 5
|
|
604
|
+
// sonraki adımı — safeToolHandler generic refactor'u gerektirir.
|
|
605
|
+
return toolResult(result, "figma_capture_screenshot", { skipGuard: true });
|
|
422
606
|
}));
|
|
423
607
|
// ---- figma_set_instance_properties ----
|
|
424
608
|
server.registerTool("figma_set_instance_properties", {
|
|
@@ -581,7 +765,9 @@ export async function main() {
|
|
|
581
765
|
}));
|
|
582
766
|
// ---- Node operations (short list) ----
|
|
583
767
|
server.registerTool("figma_instantiate_component", {
|
|
584
|
-
description: "Create a component instance. Use componentKey from figma_search_components or
|
|
768
|
+
description: "Create a component instance. Use componentKey from figma_search_components, figma_search_assets, or REST API. " +
|
|
769
|
+
"Supports library components (importComponentByKeyAsync) and local components (by nodeId). " +
|
|
770
|
+
"After creation: use overrides with setProperties({...}) for component properties — do NOT use findAll(TEXT) to modify instance text.",
|
|
585
771
|
inputSchema: {
|
|
586
772
|
componentKey: z.string(),
|
|
587
773
|
options: z
|
|
@@ -684,35 +870,37 @@ export async function main() {
|
|
|
684
870
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
685
871
|
}));
|
|
686
872
|
server.registerTool("figma_get_component_image", {
|
|
687
|
-
description: "Get screenshot of a node (component/frame). Returns base64 image.
|
|
873
|
+
description: "Get screenshot of a node (component/frame). Returns base64 image. Defaults to JPG@1x q70 (v1.8.0 context-safe).",
|
|
688
874
|
inputSchema: {
|
|
689
875
|
nodeId: z.string(),
|
|
690
|
-
scale: z.number().min(0.5).max(4).optional().default(2),
|
|
691
|
-
format: z.enum(["PNG", "JPG"]).optional().default("PNG"),
|
|
876
|
+
scale: z.number().min(0.5).max(4).optional().default(LEGACY_DEFAULTS ? 2 : 1),
|
|
877
|
+
format: z.enum(["PNG", "JPG"]).optional().default(LEGACY_DEFAULTS ? "PNG" : "JPG"),
|
|
878
|
+
jpegQuality: z.number().min(30).max(100).optional().default(70),
|
|
692
879
|
},
|
|
693
880
|
annotations: { readOnlyHint: true },
|
|
694
|
-
}, safeToolHandler(async ({ nodeId, scale, format }) => {
|
|
881
|
+
}, safeToolHandler(async ({ nodeId, scale, format, jpegQuality }) => {
|
|
695
882
|
const conn = getConnector(bridge);
|
|
696
|
-
const result = await conn.captureScreenshot(nodeId, { scale, format });
|
|
697
|
-
return
|
|
883
|
+
const result = await conn.captureScreenshot(nodeId, { scale, format, jpegQuality });
|
|
884
|
+
return toolResult(result, "figma_get_component_image");
|
|
698
885
|
}));
|
|
699
886
|
server.registerTool("figma_get_component_for_development", {
|
|
700
|
-
description: "Get component metadata plus base64 screenshot in one call. For design-to-code workflows.",
|
|
887
|
+
description: "Get component metadata plus base64 screenshot in one call. For design-to-code workflows. Defaults to JPG@1x q70 (v1.8.0 context-safe).",
|
|
701
888
|
inputSchema: {
|
|
702
889
|
nodeId: z.string(),
|
|
703
|
-
scale: z.number().min(0.5).max(4).optional().default(2),
|
|
704
|
-
format: z.enum(["PNG", "JPG"]).optional().default("PNG"),
|
|
890
|
+
scale: z.number().min(0.5).max(4).optional().default(LEGACY_DEFAULTS ? 2 : 1),
|
|
891
|
+
format: z.enum(["PNG", "JPG"]).optional().default(LEGACY_DEFAULTS ? "PNG" : "JPG"),
|
|
892
|
+
jpegQuality: z.number().min(30).max(100).optional().default(70),
|
|
705
893
|
},
|
|
706
894
|
annotations: { readOnlyHint: true },
|
|
707
|
-
}, safeToolHandler(async ({ nodeId, scale, format }) => {
|
|
895
|
+
}, safeToolHandler(async ({ nodeId, scale, format, jpegQuality }) => {
|
|
708
896
|
const conn = getConnector(bridge);
|
|
709
897
|
const [component, screenshot] = await Promise.all([
|
|
710
898
|
conn.getComponentFromPluginUI(nodeId),
|
|
711
|
-
conn.captureScreenshot(nodeId, { scale, format }),
|
|
899
|
+
conn.captureScreenshot(nodeId, { scale, format, jpegQuality }),
|
|
712
900
|
]);
|
|
713
901
|
const comp = component?.component ?? component;
|
|
714
902
|
const out = { success: true, component: comp, image: screenshot?.image ?? screenshot?.data };
|
|
715
|
-
return
|
|
903
|
+
return toolResult(out, "figma_get_component_for_development");
|
|
716
904
|
}));
|
|
717
905
|
// ---- Batch variables & setup_design_tokens & arrange_component_set ----
|
|
718
906
|
server.registerTool("figma_batch_create_variables", {
|
|
@@ -989,6 +1177,11 @@ export async function main() {
|
|
|
989
1177
|
const listening = bridge.isListening();
|
|
990
1178
|
const currentPort = bridge.getPort();
|
|
991
1179
|
const startError = bridge.getStartError();
|
|
1180
|
+
// v1.8.0+: detect plugin/server version mismatch
|
|
1181
|
+
const outdatedPlugins = connectedFiles.filter((f) => f.pluginVersion === null || (f.pluginVersion && f.pluginVersion !== FMCP_VERSION));
|
|
1182
|
+
const versionWarning = outdatedPlugins.length > 0
|
|
1183
|
+
? `⚠️ Plugin version mismatch detected: ${outdatedPlugins.map(f => `${f.fileName || "?"} (plugin v${f.pluginVersion ?? "<1.8.0"}, server v${FMCP_VERSION})`).join(", ")}. Reinstall the plugin from f-mcp-plugin/ for full v1.8.0 context-safe defaults.`
|
|
1184
|
+
: undefined;
|
|
992
1185
|
let msg;
|
|
993
1186
|
if (!listening) {
|
|
994
1187
|
msg = startError
|
|
@@ -997,6 +1190,8 @@ export async function main() {
|
|
|
997
1190
|
}
|
|
998
1191
|
else if (connected) {
|
|
999
1192
|
msg = `F-MCP ATezer Bridge: ${clientCount} plugin(s) connected on port ${currentPort}. You can use all figma_* tools.`;
|
|
1193
|
+
if (versionWarning)
|
|
1194
|
+
msg += " " + versionWarning;
|
|
1000
1195
|
}
|
|
1001
1196
|
else {
|
|
1002
1197
|
msg = PLUGIN_NOT_CONNECTED;
|
|
@@ -1018,30 +1213,57 @@ export async function main() {
|
|
|
1018
1213
|
connectedClients: clientCount,
|
|
1019
1214
|
connectedFiles,
|
|
1020
1215
|
bridgePort: currentPort,
|
|
1216
|
+
serverVersion: FMCP_VERSION,
|
|
1021
1217
|
...(autoIncremented && { preferredPort: bridge.getPreferredPort(), autoIncremented }),
|
|
1022
1218
|
message: msg,
|
|
1023
1219
|
...(startError && { startError }),
|
|
1024
1220
|
...(portHint && { portHint }),
|
|
1221
|
+
...(versionWarning && { versionWarning }),
|
|
1025
1222
|
}),
|
|
1026
1223
|
}],
|
|
1027
1224
|
};
|
|
1028
1225
|
});
|
|
1029
1226
|
// ---- Node Creation Tools ----
|
|
1227
|
+
// v1.8.0: figma_create_frame now supports auto-layout out of the box.
|
|
1228
|
+
// Default layoutMode="VERTICAL" with sensible padding/gap. Pass layoutMode="NONE"
|
|
1229
|
+
// for a free-form frame (legacy behavior).
|
|
1030
1230
|
server.registerTool("figma_create_frame", {
|
|
1031
|
-
description: "Create a new frame node
|
|
1231
|
+
description: "Create a new frame node with optional auto-layout. Returns the created node ID. " +
|
|
1232
|
+
"v1.8.0: defaults to layoutMode='VERTICAL' with paddingTop/Bottom=16, paddingLeft/Right=16, itemSpacing=12, " +
|
|
1233
|
+
"primaryAxisSizingMode='AUTO', counterAxisSizingMode='AUTO'. Pass layoutMode='NONE' for legacy free-form frames.",
|
|
1032
1234
|
inputSchema: {
|
|
1033
1235
|
name: z.string().optional().default("Frame").describe("Frame name"),
|
|
1034
1236
|
x: z.number().optional().describe("X position. If omitted, auto-positions to the right of existing content"),
|
|
1035
1237
|
y: z.number().optional().default(0),
|
|
1036
1238
|
width: z.number().optional().default(200),
|
|
1037
1239
|
height: z.number().optional().default(200),
|
|
1038
|
-
fillColor: z.string().optional().describe("Hex color e.g. '#ffffff'"),
|
|
1240
|
+
fillColor: z.string().optional().describe("Hex color e.g. '#ffffff'. DEPRECATED — prefer fillVariableKey for DS token binding (v1.8.1+)."),
|
|
1039
1241
|
parentId: z.string().optional().describe("Parent node ID (default: current page)"),
|
|
1242
|
+
// Auto-layout parameters (v1.8.0)
|
|
1243
|
+
layoutMode: z.enum(["NONE", "HORIZONTAL", "VERTICAL"]).optional().default("VERTICAL").describe("Auto-layout direction. VERTICAL by default; pass 'NONE' for free-form frames."),
|
|
1244
|
+
paddingTop: z.number().optional().default(16),
|
|
1245
|
+
paddingBottom: z.number().optional().default(16),
|
|
1246
|
+
paddingLeft: z.number().optional().default(16),
|
|
1247
|
+
paddingRight: z.number().optional().default(16),
|
|
1248
|
+
itemSpacing: z.number().optional().default(12).describe("Gap between auto-layout children"),
|
|
1249
|
+
primaryAxisSizingMode: z.enum(["FIXED", "AUTO"]).optional().default("AUTO").describe("AUTO = hug contents, FIXED = use width/height"),
|
|
1250
|
+
counterAxisSizingMode: z.enum(["FIXED", "AUTO"]).optional().default("AUTO"),
|
|
1251
|
+
primaryAxisAlignItems: z.enum(["MIN", "CENTER", "MAX", "SPACE_BETWEEN"]).optional().describe("Main-axis alignment (MIN=top/left, MAX=bottom/right)"),
|
|
1252
|
+
counterAxisAlignItems: z.enum(["MIN", "CENTER", "MAX", "BASELINE"]).optional().describe("Cross-axis alignment"),
|
|
1253
|
+
layoutWrap: z.enum(["NO_WRAP", "WRAP"]).optional().describe("Wrap children when they exceed primary axis"),
|
|
1254
|
+
// v1.8.1+: DS token binding params — PREFER over hardcoded fillColor / padding / radius
|
|
1255
|
+
fillVariableKey: z.string().optional().describe("DS variable key for fill binding (from figma_get_library_variables). Takes precedence over fillColor."),
|
|
1256
|
+
paddingVariableKey: z.string().optional().describe("DS spacing variable key — applies to all 4 paddings via setBoundVariable."),
|
|
1257
|
+
itemSpacingVariableKey: z.string().optional().describe("DS spacing variable key for itemSpacing via setBoundVariable."),
|
|
1258
|
+
cornerRadiusVariableKey: z.string().optional().describe("DS radius variable key for cornerRadius via setBoundVariable."),
|
|
1259
|
+
cornerRadius: z.number().optional().describe("Hardcoded corner radius in px. DEPRECATED — prefer cornerRadiusVariableKey."),
|
|
1040
1260
|
},
|
|
1041
|
-
}, async ({ name, x, y, width, height, fillColor, parentId }) => {
|
|
1261
|
+
}, async ({ name, x, y, width, height, fillColor, parentId, layoutMode, paddingTop, paddingBottom, paddingLeft, paddingRight, itemSpacing, primaryAxisSizingMode, counterAxisSizingMode, primaryAxisAlignItems, counterAxisAlignItems, layoutWrap, fillVariableKey, paddingVariableKey, itemSpacingVariableKey, cornerRadiusVariableKey, cornerRadius }) => {
|
|
1042
1262
|
try {
|
|
1263
|
+
invalidateCache();
|
|
1043
1264
|
const conn = getConnector(bridge);
|
|
1044
1265
|
const autoPosition = x === undefined && !parentId;
|
|
1266
|
+
const useAutoLayout = layoutMode !== "NONE";
|
|
1045
1267
|
const code = `
|
|
1046
1268
|
${autoPosition ? `
|
|
1047
1269
|
let posX = 0;
|
|
@@ -1059,9 +1281,64 @@ export async function main() {
|
|
|
1059
1281
|
frame.name = ${JSON.stringify(name)};
|
|
1060
1282
|
frame.x = posX; frame.y = ${y};
|
|
1061
1283
|
frame.resize(${width}, ${height});
|
|
1062
|
-
${
|
|
1284
|
+
${useAutoLayout ? `
|
|
1285
|
+
frame.layoutMode = ${JSON.stringify(layoutMode)};
|
|
1286
|
+
frame.paddingTop = ${paddingTop};
|
|
1287
|
+
frame.paddingBottom = ${paddingBottom};
|
|
1288
|
+
frame.paddingLeft = ${paddingLeft};
|
|
1289
|
+
frame.paddingRight = ${paddingRight};
|
|
1290
|
+
frame.itemSpacing = ${itemSpacing};
|
|
1291
|
+
frame.primaryAxisSizingMode = ${JSON.stringify(primaryAxisSizingMode)};
|
|
1292
|
+
frame.counterAxisSizingMode = ${JSON.stringify(counterAxisSizingMode)};
|
|
1293
|
+
${primaryAxisAlignItems ? `frame.primaryAxisAlignItems = ${JSON.stringify(primaryAxisAlignItems)};` : ""}
|
|
1294
|
+
${counterAxisAlignItems ? `frame.counterAxisAlignItems = ${JSON.stringify(counterAxisAlignItems)};` : ""}
|
|
1295
|
+
${layoutWrap ? `frame.layoutWrap = ${JSON.stringify(layoutWrap)};` : ""}
|
|
1296
|
+
` : ""}
|
|
1297
|
+
${cornerRadius != null ? `frame.cornerRadius = ${cornerRadius};` : ""}
|
|
1298
|
+
// v1.8.1: DS token binding takes precedence over hardcoded values
|
|
1299
|
+
${fillVariableKey ? `
|
|
1300
|
+
try {
|
|
1301
|
+
const fillVar = await figma.variables.importVariableByKeyAsync(${JSON.stringify(fillVariableKey)});
|
|
1302
|
+
const baseFill = { type: 'SOLID', color: { r: 1, g: 1, b: 1 }, opacity: 1 };
|
|
1303
|
+
const boundFill = figma.variables.setBoundVariableForPaint(baseFill, 'color', fillVar);
|
|
1304
|
+
frame.fills = [boundFill];
|
|
1305
|
+
} catch (fillBindErr) {
|
|
1306
|
+
console.warn('[figma_create_frame] fillVariableKey binding failed:', fillBindErr.message);
|
|
1307
|
+
}
|
|
1308
|
+
` : fillColor ? `frame.fills = [{ type: 'SOLID', color: { r: parseInt('${fillColor}'.slice(1,3),16)/255, g: parseInt('${fillColor}'.slice(3,5),16)/255, b: parseInt('${fillColor}'.slice(5,7),16)/255 } }];` : ""}
|
|
1309
|
+
${paddingVariableKey ? `
|
|
1310
|
+
try {
|
|
1311
|
+
const padVar = await figma.variables.importVariableByKeyAsync(${JSON.stringify(paddingVariableKey)});
|
|
1312
|
+
frame.setBoundVariable('paddingTop', padVar);
|
|
1313
|
+
frame.setBoundVariable('paddingBottom', padVar);
|
|
1314
|
+
frame.setBoundVariable('paddingLeft', padVar);
|
|
1315
|
+
frame.setBoundVariable('paddingRight', padVar);
|
|
1316
|
+
} catch (padBindErr) {
|
|
1317
|
+
console.warn('[figma_create_frame] paddingVariableKey binding failed:', padBindErr.message);
|
|
1318
|
+
}
|
|
1319
|
+
` : ""}
|
|
1320
|
+
${itemSpacingVariableKey ? `
|
|
1321
|
+
try {
|
|
1322
|
+
const gapVar = await figma.variables.importVariableByKeyAsync(${JSON.stringify(itemSpacingVariableKey)});
|
|
1323
|
+
frame.setBoundVariable('itemSpacing', gapVar);
|
|
1324
|
+
} catch (gapBindErr) {
|
|
1325
|
+
console.warn('[figma_create_frame] itemSpacingVariableKey binding failed:', gapBindErr.message);
|
|
1326
|
+
}
|
|
1327
|
+
` : ""}
|
|
1328
|
+
${cornerRadiusVariableKey ? `
|
|
1329
|
+
try {
|
|
1330
|
+
const radVar = await figma.variables.importVariableByKeyAsync(${JSON.stringify(cornerRadiusVariableKey)});
|
|
1331
|
+
frame.setBoundVariable('topLeftRadius', radVar);
|
|
1332
|
+
frame.setBoundVariable('topRightRadius', radVar);
|
|
1333
|
+
frame.setBoundVariable('bottomLeftRadius', radVar);
|
|
1334
|
+
frame.setBoundVariable('bottomRightRadius', radVar);
|
|
1335
|
+
} catch (radBindErr) {
|
|
1336
|
+
console.warn('[figma_create_frame] cornerRadiusVariableKey binding failed:', radBindErr.message);
|
|
1337
|
+
}
|
|
1338
|
+
` : ""}
|
|
1063
1339
|
${parentId ? `const parent = await figma.getNodeByIdAsync(${JSON.stringify(parentId)}); if (parent && 'appendChild' in parent) parent.appendChild(frame);` : ""}
|
|
1064
|
-
|
|
1340
|
+
const boundCount = frame.boundVariables ? Object.keys(frame.boundVariables).length : 0;
|
|
1341
|
+
return { id: frame.id, name: frame.name, width: frame.width, height: frame.height, x: frame.x, y: frame.y, layoutMode: frame.layoutMode, boundVariableCount: boundCount };
|
|
1065
1342
|
`;
|
|
1066
1343
|
const result = await conn.executeCodeViaUI(code, 10000);
|
|
1067
1344
|
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }] };
|
|
@@ -1071,14 +1348,16 @@ export async function main() {
|
|
|
1071
1348
|
}
|
|
1072
1349
|
});
|
|
1073
1350
|
server.registerTool("figma_create_text", {
|
|
1074
|
-
description: "Create a new text node on the current page. Returns the created node ID."
|
|
1351
|
+
description: "Create a new text node on the current page. Returns the created node ID. " +
|
|
1352
|
+
"IMPORTANT: fontFamily defaults to 'Inter' — if using a design system (e.g. SUI uses SHBGrotesk), specify the DS font. " +
|
|
1353
|
+
"For DS text with proper token binding, prefer figma_execute with importStyleByKeyAsync + setTextStyleIdAsync instead.",
|
|
1075
1354
|
inputSchema: {
|
|
1076
1355
|
text: z.string().describe("Text content"),
|
|
1077
1356
|
x: z.number().optional().default(0),
|
|
1078
1357
|
y: z.number().optional().default(0),
|
|
1079
1358
|
name: z.string().optional().describe("Node name (default: text content)"),
|
|
1080
1359
|
fontSize: z.number().optional().default(16),
|
|
1081
|
-
fontFamily: z.string().optional().default("Inter"),
|
|
1360
|
+
fontFamily: z.string().optional().default("Inter").describe("Font family — defaults to Inter. Specify DS font if using a design system (e.g. SHBGrotesk for SUI)."),
|
|
1082
1361
|
fontStyle: z.string().optional().default("Regular"),
|
|
1083
1362
|
fillColor: z.string().optional().describe("Text color hex e.g. '#000000'"),
|
|
1084
1363
|
parentId: z.string().optional().describe("Parent node ID"),
|
|
@@ -1167,11 +1446,12 @@ export async function main() {
|
|
|
1167
1446
|
server.registerTool("figma_export_nodes", {
|
|
1168
1447
|
description: "Export one or multiple nodes as SVG, PNG, JPG, or PDF. Returns base64-encoded data for each node. " +
|
|
1169
1448
|
"Supports batch export (up to 50 nodes). No REST API token needed — uses plugin exportAsync. " +
|
|
1170
|
-
"SVG preserves vectors; PNG/JPG are rasterized at configurable scale."
|
|
1449
|
+
"SVG preserves vectors; PNG/JPG are rasterized at configurable scale. " +
|
|
1450
|
+
"v1.8.0: default scale=1 for context safety (was 2). Override for high-DPI exports.",
|
|
1171
1451
|
inputSchema: {
|
|
1172
1452
|
nodeIds: z.array(z.string()).min(1).max(50).describe("Node IDs to export (1-50)"),
|
|
1173
1453
|
format: z.enum(["PNG", "SVG", "JPG", "PDF"]).optional().default("PNG").describe("Export format"),
|
|
1174
|
-
scale: z.number().min(0.5).max(4).optional().default(2).describe("Scale factor (0.5-4, default
|
|
1454
|
+
scale: z.number().min(0.5).max(4).optional().default(LEGACY_DEFAULTS ? 2 : 1).describe("Scale factor (0.5-4, default 1)"),
|
|
1175
1455
|
svgOutlineText: z.boolean().optional().describe("SVG: render text as outlines (default true)"),
|
|
1176
1456
|
svgIncludeId: z.boolean().optional().describe("SVG: include node IDs in attributes"),
|
|
1177
1457
|
},
|
|
@@ -1226,36 +1506,175 @@ export async function main() {
|
|
|
1226
1506
|
});
|
|
1227
1507
|
// ---- figma_search_assets (team library search via plugin) ----
|
|
1228
1508
|
server.registerTool("figma_search_assets", {
|
|
1229
|
-
description: "Search for
|
|
1230
|
-
"
|
|
1509
|
+
description: "Search for design system assets in the current Figma file. Returns: " +
|
|
1510
|
+
"(1) team library VARIABLES via figma.teamLibrary API (all enabled libraries), " +
|
|
1511
|
+
"(2) file-local COMPONENTS / COMPONENT_SETS, and " +
|
|
1512
|
+
"(3) v1.8.0+: REMOTE LIBRARY COMPONENTS discovered by scanning existing INSTANCE nodes (returned as 'libraryComponents'). " +
|
|
1513
|
+
"For library components to appear, at least one DS instance must exist in the file — place one manually first if empty. " +
|
|
1514
|
+
"Pass currentPageOnly=false to scan all pages for instance discovery. " +
|
|
1515
|
+
"Use the returned componentKey with figma_instantiate_component to place new instances. " +
|
|
1516
|
+
"Pass assetTypes to filter: ['variables'], ['components'], or both (default).",
|
|
1231
1517
|
inputSchema: {
|
|
1518
|
+
figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
|
|
1519
|
+
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
1232
1520
|
query: z.string().optional().describe("Search query to filter by name"),
|
|
1521
|
+
assetTypes: z.array(z.string()).optional().describe("Asset types to search: 'variables', 'components'. Default: both."),
|
|
1522
|
+
limit: z.number().min(1).max(80).optional().describe("Max results per asset type (default 25, max 80)"),
|
|
1523
|
+
currentPageOnly: z.boolean().optional().describe("For components: search current page only (default true)"),
|
|
1233
1524
|
},
|
|
1234
1525
|
annotations: { readOnlyHint: true },
|
|
1235
|
-
}, async ({ query }) => {
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1526
|
+
}, safeToolHandler(async ({ figmaUrl, fileKey, query, assetTypes, limit, currentPageOnly }) => {
|
|
1527
|
+
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
1528
|
+
const result = await conn.searchLibraryAssets({
|
|
1529
|
+
query: query || undefined,
|
|
1530
|
+
assetTypes: assetTypes?.length ? assetTypes : undefined,
|
|
1531
|
+
limit: limit ?? undefined,
|
|
1532
|
+
currentPageOnly,
|
|
1533
|
+
});
|
|
1534
|
+
const data = result;
|
|
1535
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...data }) }] };
|
|
1536
|
+
}));
|
|
1537
|
+
// ---- figma_get_library_variables (team library variable discovery with import keys) ----
|
|
1538
|
+
server.registerTool("figma_get_library_variables", {
|
|
1539
|
+
description: "List variables from team library collections with import keys. " +
|
|
1540
|
+
"Uses figma.teamLibrary API — works in the TARGET file, no need to connect the DS source file. " +
|
|
1541
|
+
"Returns variable name, key (for importVariableByKeyAsync), resolvedType, collection, and library name. " +
|
|
1542
|
+
"Use the returned keys with figma_bind_variable or figma.variables.importVariableByKeyAsync() in figma_execute.",
|
|
1543
|
+
inputSchema: {
|
|
1544
|
+
figmaUrl: z.string().optional().describe("Figma file URL for routing."),
|
|
1545
|
+
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
1546
|
+
query: z.string().optional().describe("Filter variables by name (case-insensitive contains)"),
|
|
1547
|
+
collectionName: z.string().optional().describe("Filter by collection name (exact match)"),
|
|
1548
|
+
libraryName: z.string().optional().describe("Filter by library name (exact match, e.g. '❖ SUI')"),
|
|
1549
|
+
limit: z.number().min(1).max(500).optional().describe("Max results (default 100)"),
|
|
1550
|
+
},
|
|
1551
|
+
annotations: { readOnlyHint: true },
|
|
1552
|
+
}, safeToolHandler(async ({ figmaUrl, fileKey, query, collectionName, libraryName, limit }) => {
|
|
1553
|
+
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
1554
|
+
const maxResults = limit ?? 100;
|
|
1555
|
+
const q = query ? query.toLowerCase() : "";
|
|
1556
|
+
const code = `
|
|
1557
|
+
if (!figma.teamLibrary) return { success: false, error: "teamLibrary API not available" };
|
|
1558
|
+
var cols = await figma.teamLibrary.getAvailableLibraryVariableCollectionsAsync();
|
|
1559
|
+
var filtered = cols;
|
|
1560
|
+
${collectionName ? `filtered = filtered.filter(function(c) { return c.name === ${JSON.stringify(collectionName)}; });` : ""}
|
|
1561
|
+
${libraryName ? `filtered = filtered.filter(function(c) { return c.libraryName === ${JSON.stringify(libraryName)}; });` : ""}
|
|
1562
|
+
var results = [];
|
|
1563
|
+
for (var ci = 0; ci < filtered.length && results.length < ${maxResults}; ci++) {
|
|
1564
|
+
var col = filtered[ci];
|
|
1565
|
+
var vars = await figma.teamLibrary.getVariablesInLibraryCollectionAsync(col.key);
|
|
1566
|
+
for (var vi = 0; vi < vars.length && results.length < ${maxResults}; vi++) {
|
|
1567
|
+
var v = vars[vi];
|
|
1568
|
+
var nm = (v.name || "").toLowerCase();
|
|
1569
|
+
if (!${JSON.stringify(q)} || nm.indexOf(${JSON.stringify(q)}) >= 0) {
|
|
1570
|
+
results.push({ name: v.name, key: v.key, resolvedType: v.resolvedType, collection: col.name, library: col.libraryName });
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return { success: true, count: results.length, variables: results };
|
|
1575
|
+
`;
|
|
1576
|
+
const result = await conn.executeCodeViaUI(code, 30000);
|
|
1577
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1578
|
+
}));
|
|
1579
|
+
// ---- figma_bind_variable (import variable and bind to node property) ----
|
|
1580
|
+
server.registerTool("figma_bind_variable", {
|
|
1581
|
+
description: "Import a library variable by key and bind it to a node property. " +
|
|
1582
|
+
"For colors: binds to fills or strokes via setBoundVariableForPaint. " +
|
|
1583
|
+
"For spacing/sizing: binds via setBoundVariable (paddingLeft, itemSpacing, cornerRadius, etc.). " +
|
|
1584
|
+
"Get variableKey from figma_get_library_variables. " +
|
|
1585
|
+
"The node's fill/spacing will dynamically update when the DS token changes.",
|
|
1586
|
+
inputSchema: {
|
|
1587
|
+
figmaUrl: z.string().optional().describe("Figma file URL for routing."),
|
|
1588
|
+
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
1589
|
+
nodeId: z.string().describe("Target node ID"),
|
|
1590
|
+
variableKey: z.string().describe("Variable import key from figma_get_library_variables"),
|
|
1591
|
+
property: z.enum([
|
|
1592
|
+
"fills", "strokes",
|
|
1593
|
+
"paddingLeft", "paddingRight", "paddingTop", "paddingBottom",
|
|
1594
|
+
"itemSpacing", "counterAxisSpacing",
|
|
1595
|
+
"topLeftRadius", "topRightRadius", "bottomLeftRadius", "bottomRightRadius", "cornerRadius",
|
|
1596
|
+
"strokeWeight", "opacity",
|
|
1597
|
+
"width", "height", "minWidth", "minHeight", "maxWidth", "maxHeight",
|
|
1598
|
+
]).describe("Node property to bind the variable to"),
|
|
1599
|
+
paintIndex: z.number().optional().default(0).describe("For fills/strokes: which paint index (default 0)"),
|
|
1600
|
+
},
|
|
1601
|
+
annotations: { destructiveHint: true },
|
|
1602
|
+
}, safeToolHandler(async ({ figmaUrl, fileKey, nodeId, variableKey, property, paintIndex }) => {
|
|
1603
|
+
invalidateCache();
|
|
1604
|
+
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
1605
|
+
const idx = paintIndex ?? 0;
|
|
1606
|
+
const code = `
|
|
1607
|
+
var variable = await figma.variables.importVariableByKeyAsync(${JSON.stringify(variableKey)});
|
|
1608
|
+
var node = await figma.getNodeByIdAsync(${JSON.stringify(nodeId)});
|
|
1609
|
+
if (!node) throw new Error("Node not found: " + ${JSON.stringify(nodeId)});
|
|
1610
|
+
var prop = ${JSON.stringify(property)};
|
|
1611
|
+
if (prop === "fills" || prop === "strokes") {
|
|
1612
|
+
var paints = [];
|
|
1613
|
+
for (var i = 0; i < node[prop].length; i++) paints.push(node[prop][i]);
|
|
1614
|
+
if (!paints[${idx}]) throw new Error("No paint at index ${idx} on " + prop);
|
|
1615
|
+
var boundPaint = figma.variables.setBoundVariableForPaint(paints[${idx}], "color", variable);
|
|
1616
|
+
paints[${idx}] = boundPaint;
|
|
1617
|
+
node[prop] = paints;
|
|
1618
|
+
} else {
|
|
1619
|
+
node.setBoundVariable(prop, variable);
|
|
1620
|
+
}
|
|
1621
|
+
return { success: true, nodeId: ${JSON.stringify(nodeId)}, property: prop, variableName: variable.name, variableId: variable.id };
|
|
1622
|
+
`;
|
|
1623
|
+
const result = await conn.executeCodeViaUI(code, 10000);
|
|
1624
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1625
|
+
}));
|
|
1626
|
+
// ---- figma_import_style (import text/paint/effect style from library) ----
|
|
1627
|
+
server.registerTool("figma_import_style", {
|
|
1628
|
+
description: "Import a text, paint, or effect style from a team library by key, and optionally apply it to a node. " +
|
|
1629
|
+
"IMPORTANT: This API only imports PUBLISHED LIBRARY styles, NOT local file styles. " +
|
|
1630
|
+
"For local styles, use 'node.fillStyleId = style.id' (or textStyleId/effectStyleId) directly via figma_execute. " +
|
|
1631
|
+
"Get library style keys from .claude/libraries/ cache or REST API: figma_rest_api GET /v1/files/{fileKey}/styles. " +
|
|
1632
|
+
"For TEXT styles: applies via setTextStyleIdAsync (includes font, size, weight). " +
|
|
1633
|
+
"For PAINT styles: applies via fillStyleId. For EFFECT styles: applies via effectStyleId.",
|
|
1634
|
+
inputSchema: {
|
|
1635
|
+
figmaUrl: z.string().optional().describe("Figma file URL for routing."),
|
|
1636
|
+
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
1637
|
+
styleKey: z.string().describe("Library style key (must be from a PUBLISHED team library, not a local style)"),
|
|
1638
|
+
nodeId: z.string().optional().describe("Node ID to apply the style to (optional — omit to just import)"),
|
|
1639
|
+
},
|
|
1640
|
+
annotations: { destructiveHint: true },
|
|
1641
|
+
}, safeToolHandler(async ({ figmaUrl, fileKey, styleKey, nodeId }) => {
|
|
1642
|
+
invalidateCache();
|
|
1643
|
+
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
1644
|
+
const code = `
|
|
1645
|
+
var style;
|
|
1646
|
+
try {
|
|
1647
|
+
style = await figma.importStyleByKeyAsync(${JSON.stringify(styleKey)});
|
|
1648
|
+
} catch (e) {
|
|
1649
|
+
var origMsg = e && e.message ? e.message : String(e);
|
|
1650
|
+
throw new Error(
|
|
1651
|
+
"importStyleByKeyAsync failed for key '" + ${JSON.stringify(styleKey)} + "'. " +
|
|
1652
|
+
"This API only works with PUBLISHED LIBRARY styles. " +
|
|
1653
|
+
"Local file styles cannot be imported this way — use 'node.fillStyleId/textStyleId/effectStyleId = <localStyleId>' directly via figma_execute. " +
|
|
1654
|
+
"To find library style keys, use REST API: GET /v1/files/{fileKey}/styles. " +
|
|
1655
|
+
"Original error: " + origMsg
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
var applied = false;
|
|
1659
|
+
${nodeId ? `
|
|
1660
|
+
var node = await figma.getNodeByIdAsync(${JSON.stringify(nodeId)});
|
|
1661
|
+
if (!node) throw new Error("Node not found: " + ${JSON.stringify(nodeId)});
|
|
1662
|
+
if (style.type === "TEXT" && node.type === "TEXT") {
|
|
1663
|
+
await node.setTextStyleIdAsync(style.id);
|
|
1664
|
+
applied = true;
|
|
1665
|
+
} else if (style.type === "PAINT") {
|
|
1666
|
+
node.fillStyleId = style.id;
|
|
1667
|
+
applied = true;
|
|
1668
|
+
} else if (style.type === "EFFECT") {
|
|
1669
|
+
node.effectStyleId = style.id;
|
|
1670
|
+
applied = true;
|
|
1671
|
+
}
|
|
1672
|
+
` : ""}
|
|
1673
|
+
return { success: true, styleId: style.id, styleName: style.name, styleType: style.type, applied: applied };
|
|
1674
|
+
`;
|
|
1675
|
+
const result = await conn.executeCodeViaUI(code, 10000);
|
|
1676
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1677
|
+
}));
|
|
1259
1678
|
// ---- figma_plugin_diagnostics ----
|
|
1260
1679
|
server.registerTool("figma_plugin_diagnostics", {
|
|
1261
1680
|
description: "Get diagnostic info about plugin connection health: uptime, connected clients, " +
|