@atezer/figma-mcp-bridge 1.7.30 → 1.9.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/CHANGELOG.md +408 -0
- package/README.md +8 -8
- 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 +26 -0
- package/dist/core/plugin-bridge-connector.d.ts.map +1 -1
- package/dist/core/plugin-bridge-connector.js +18 -2
- package/dist/core/plugin-bridge-connector.js.map +1 -1
- package/dist/core/plugin-bridge-server.d.ts +16 -0
- package/dist/core/plugin-bridge-server.d.ts.map +1 -1
- package/dist/core/plugin-bridge-server.js +83 -1
- 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 +334 -101
- package/dist/local-plugin-only.js.map +1 -1
- package/f-mcp-plugin/code.js +514 -29
- package/f-mcp-plugin/ui.html +90 -14
- 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 -243
- package/skills/fmcp-ds-audit-orchestrator/SKILL.md +205 -0
- package/skills/fmcp-intent-router/SKILL.md +574 -0
- 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 +360 -6
- 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,42 +109,8 @@ function getErrorHint(category) {
|
|
|
98
109
|
default: return "Hata mesajini kontrol et.";
|
|
99
110
|
}
|
|
100
111
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const warnings = [];
|
|
104
|
-
// 1. FILL before appendChild — must set FILL *after* node is in auto-layout parent
|
|
105
|
-
if (/layoutSizing(?:Horizontal|Vertical)\s*=\s*['"]FILL['"]/i.test(code)) {
|
|
106
|
-
const fillIdx = code.search(/layoutSizing(?:Horizontal|Vertical)\s*=\s*['"]FILL['"]/i);
|
|
107
|
-
const appendIdx = code.indexOf("appendChild");
|
|
108
|
-
if (appendIdx === -1 || fillIdx < appendIdx) {
|
|
109
|
-
warnings.push("layoutSizingHorizontal/Vertical = 'FILL' appendChild'dan ONCE ayarlanmis. " +
|
|
110
|
-
"Oncesinde hata verir. FILL'i appendChild SONRASINA tasi.");
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
// 2. Sync API usage — should use Async versions
|
|
114
|
-
const syncApis = [
|
|
115
|
-
{ sync: "getLocalPaintStyles(", async: "getLocalPaintStylesAsync(" },
|
|
116
|
-
{ sync: "getLocalTextStyles(", async: "getLocalTextStylesAsync(" },
|
|
117
|
-
{ sync: "getLocalEffectStyles(", async: "getLocalEffectStylesAsync(" },
|
|
118
|
-
{ sync: "getLocalGridStyles(", async: "getLocalGridStylesAsync(" },
|
|
119
|
-
];
|
|
120
|
-
for (const api of syncApis) {
|
|
121
|
-
if (code.includes(api.sync) && !code.includes(api.async)) {
|
|
122
|
-
warnings.push(`Sync API '${api.sync.slice(0, -1)}' tespit edildi. 'await ${api.async.slice(0, -1)}' kullanin — dynamic-page modunda sync API'ler calismaz.`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
// 3. Font not loaded before text modification
|
|
126
|
-
if ((/\.characters\s*=/.test(code) || code.includes(".insertCharacters") || code.includes(".deleteCharacters")) &&
|
|
127
|
-
!code.includes("loadFontAsync")) {
|
|
128
|
-
warnings.push("Text icerik degisikligi (characters) tespit edildi, ancak loadFontAsync cagrisi yok. " +
|
|
129
|
-
"Metin degistirmeden once 'await figma.loadFontAsync(node.fontName)' ekleyin.");
|
|
130
|
-
}
|
|
131
|
-
// 4. Sync page assignment — does not work
|
|
132
|
-
if (/figma\.currentPage\s*=/.test(code) && !code.includes("setCurrentPageAsync")) {
|
|
133
|
-
warnings.push("'figma.currentPage = ...' calismaz. 'await figma.setCurrentPageAsync(page)' kullanin.");
|
|
134
|
-
}
|
|
135
|
-
return warnings;
|
|
136
|
-
}
|
|
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.
|
|
137
114
|
/** Wrap a tool handler with try-catch to prevent unhandled rejections. */
|
|
138
115
|
function safeToolHandler(handler) {
|
|
139
116
|
return async (params) => {
|
|
@@ -172,6 +149,49 @@ export async function main() {
|
|
|
172
149
|
const cache = new ResponseCache();
|
|
173
150
|
/** Invalidate cache after any mutating operation. */
|
|
174
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
|
+
}
|
|
175
195
|
const server = new McpServer({
|
|
176
196
|
name: "F-MCP ATezer Bridge (Plugin-only)",
|
|
177
197
|
version: FMCP_VERSION,
|
|
@@ -198,8 +218,9 @@ export async function main() {
|
|
|
198
218
|
};
|
|
199
219
|
});
|
|
200
220
|
// ---- figma_get_file_data_plugin (no REST, no token) ----
|
|
221
|
+
// v1.8.0: Defaults already conservative (depth=1, verbosity="summary"). Cached + truncated.
|
|
201
222
|
server.registerTool("figma_get_file_data", {
|
|
202
|
-
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.",
|
|
203
224
|
inputSchema: {
|
|
204
225
|
figmaUrl: z.string().optional().describe("Figma or FigJam file URL; fileKey is extracted from the link for routing."),
|
|
205
226
|
fileKey: z.string().optional().describe("Target a specific connected file. Use figma_list_connected_files to see available files."),
|
|
@@ -210,16 +231,14 @@ export async function main() {
|
|
|
210
231
|
includeTypography: z.boolean().optional(),
|
|
211
232
|
includeCodeReady: z.boolean().optional(),
|
|
212
233
|
outputHint: z.enum(["react", "tailwind"]).optional(),
|
|
234
|
+
debug: z.boolean().optional().describe("Bypass cache and include _responseGuard fields."),
|
|
213
235
|
},
|
|
214
236
|
annotations: { readOnlyHint: true },
|
|
215
|
-
}, async ({ figmaUrl, fileKey, depth, verbosity, includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint }) => {
|
|
237
|
+
}, async ({ figmaUrl, fileKey, depth, verbosity, includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint, debug }) => {
|
|
216
238
|
try {
|
|
217
239
|
const resolvedKey = resolveFileKey(figmaUrl, fileKey);
|
|
218
240
|
if (figmaUrl && !resolvedKey) {
|
|
219
|
-
return
|
|
220
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Invalid Figma/FigJam URL: could not extract file key." }) }],
|
|
221
|
-
isError: true,
|
|
222
|
-
};
|
|
241
|
+
return errorResult("Invalid Figma/FigJam URL: could not extract file key.");
|
|
223
242
|
}
|
|
224
243
|
const conn = getConnector(bridge, resolvedKey);
|
|
225
244
|
const opts = includeLayout !== undefined ||
|
|
@@ -229,13 +248,12 @@ export async function main() {
|
|
|
229
248
|
outputHint !== undefined
|
|
230
249
|
? { includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint }
|
|
231
250
|
: undefined;
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
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 });
|
|
239
257
|
}
|
|
240
258
|
catch (err) {
|
|
241
259
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -246,23 +264,26 @@ export async function main() {
|
|
|
246
264
|
}
|
|
247
265
|
});
|
|
248
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.
|
|
249
269
|
server.registerTool("figma_get_design_context", {
|
|
250
|
-
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.",
|
|
251
271
|
inputSchema: {
|
|
252
272
|
figmaUrl: z.string().optional().describe("Figma or FigJam file URL; fileKey and optional node-id are extracted for routing."),
|
|
253
273
|
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
254
274
|
nodeId: z.string().optional(),
|
|
255
|
-
depth: z.number().min(0).max(3).optional().default(2),
|
|
256
|
-
verbosity: z.enum(["summary", "standard", "full"]).optional().default("standard"),
|
|
257
|
-
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."),
|
|
258
278
|
includeLayout: z.boolean().optional(),
|
|
259
279
|
includeVisual: z.boolean().optional(),
|
|
260
280
|
includeTypography: z.boolean().optional(),
|
|
261
281
|
includeCodeReady: z.boolean().optional(),
|
|
262
282
|
outputHint: z.enum(["react", "tailwind"]).optional(),
|
|
283
|
+
debug: z.boolean().optional().describe("Bypass cache and include _responseGuard/_metrics fields."),
|
|
263
284
|
},
|
|
264
285
|
annotations: { readOnlyHint: true },
|
|
265
|
-
}, 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 }) => {
|
|
266
287
|
try {
|
|
267
288
|
const { fileKey: resolvedKey, nodeId: resolvedNodeId } = resolveDesignContextParams({ figmaUrl, fileKey, nodeId });
|
|
268
289
|
if (figmaUrl && !resolvedKey) {
|
|
@@ -281,15 +302,15 @@ export async function main() {
|
|
|
281
302
|
? { excludeScreenshot, includeLayout, includeVisual, includeTypography, includeCodeReady, outputHint }
|
|
282
303
|
: undefined;
|
|
283
304
|
const effectiveNodeId = resolvedNodeId ?? nodeId?.trim();
|
|
284
|
-
|
|
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
|
|
285
309
|
? await conn.getNodeContext(effectiveNodeId, depth, verbosity, opts)
|
|
286
|
-
: await conn.getDocumentStructure(depth, verbosity, opts);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
? data
|
|
291
|
-
: JSON.stringify(data);
|
|
292
|
-
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 });
|
|
293
314
|
}
|
|
294
315
|
catch (err) {
|
|
295
316
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -365,11 +386,9 @@ export async function main() {
|
|
|
365
386
|
// ---- figma_execute ----
|
|
366
387
|
server.registerTool("figma_execute", {
|
|
367
388
|
description: "Run JavaScript in the Figma plugin context. Full Plugin API available. Use fileKey or figmaUrl to target a specific file. " +
|
|
368
|
-
"
|
|
369
|
-
"
|
|
370
|
-
"
|
|
371
|
-
"(3) call loadFontAsync before .characters=, " +
|
|
372
|
-
"(4) use setCurrentPageAsync not figma.currentPage=. " +
|
|
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. " +
|
|
373
392
|
"For component instances: use setProperties({...}), NOT findAll(TEXT).",
|
|
374
393
|
inputSchema: {
|
|
375
394
|
figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
|
|
@@ -385,11 +404,26 @@ export async function main() {
|
|
|
385
404
|
isError: true,
|
|
386
405
|
};
|
|
387
406
|
}
|
|
388
|
-
const clampedTimeout = Math.max(3000, Math.min(timeout ?? 15000,
|
|
407
|
+
const clampedTimeout = Math.max(3000, Math.min(timeout ?? 15000, 30000));
|
|
389
408
|
invalidateCache();
|
|
390
|
-
//
|
|
409
|
+
// v1.8.1: Structured warnings with SEVERE vs ADVISORY severity
|
|
391
410
|
const codeWarnings = analyzeCodeForWarnings(code);
|
|
392
|
-
const
|
|
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
|
+
: {};
|
|
393
427
|
const startTime = Date.now();
|
|
394
428
|
try {
|
|
395
429
|
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
@@ -407,6 +441,7 @@ export async function main() {
|
|
|
407
441
|
catch { /* safe fallback */ }
|
|
408
442
|
return {
|
|
409
443
|
content: [{ type: "text", text: JSON.stringify({
|
|
444
|
+
...dsViolations, // v1.8.1: SEVERE warnings FIRST, before anything else
|
|
410
445
|
...result,
|
|
411
446
|
errorCategory: category,
|
|
412
447
|
_metrics: { durationMs, timeoutMs: clampedTimeout },
|
|
@@ -419,8 +454,15 @@ export async function main() {
|
|
|
419
454
|
let enriched;
|
|
420
455
|
try {
|
|
421
456
|
enriched = typeof result === "object" && result !== null
|
|
422
|
-
? {
|
|
423
|
-
|
|
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;
|
|
424
466
|
}
|
|
425
467
|
catch {
|
|
426
468
|
enriched = result;
|
|
@@ -451,21 +493,116 @@ export async function main() {
|
|
|
451
493
|
};
|
|
452
494
|
}
|
|
453
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
|
+
}));
|
|
454
578
|
// ---- figma_capture_screenshot ----
|
|
579
|
+
// v1.8.0: Default JPG@1x (~80% smaller base64 vs PNG@2x). Override via params.
|
|
455
580
|
server.registerTool("figma_capture_screenshot", {
|
|
456
|
-
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.",
|
|
457
582
|
inputSchema: {
|
|
458
583
|
figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
|
|
459
584
|
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
460
585
|
nodeId: z.string().optional(),
|
|
461
|
-
format: z.enum(["PNG", "JPG"]).optional().default("PNG"),
|
|
462
|
-
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."),
|
|
463
589
|
},
|
|
464
590
|
annotations: { readOnlyHint: true },
|
|
465
|
-
}, safeToolHandler(async ({ figmaUrl, fileKey, nodeId, format, scale }) => {
|
|
591
|
+
}, safeToolHandler(async ({ figmaUrl, fileKey, nodeId, format, scale, jpegQuality }) => {
|
|
466
592
|
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
467
|
-
const result = await conn.captureScreenshot(nodeId ?? null, { format, scale });
|
|
468
|
-
|
|
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 });
|
|
469
606
|
}));
|
|
470
607
|
// ---- figma_set_instance_properties ----
|
|
471
608
|
server.registerTool("figma_set_instance_properties", {
|
|
@@ -733,35 +870,37 @@ export async function main() {
|
|
|
733
870
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
734
871
|
}));
|
|
735
872
|
server.registerTool("figma_get_component_image", {
|
|
736
|
-
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).",
|
|
737
874
|
inputSchema: {
|
|
738
875
|
nodeId: z.string(),
|
|
739
|
-
scale: z.number().min(0.5).max(4).optional().default(2),
|
|
740
|
-
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),
|
|
741
879
|
},
|
|
742
880
|
annotations: { readOnlyHint: true },
|
|
743
|
-
}, safeToolHandler(async ({ nodeId, scale, format }) => {
|
|
881
|
+
}, safeToolHandler(async ({ nodeId, scale, format, jpegQuality }) => {
|
|
744
882
|
const conn = getConnector(bridge);
|
|
745
|
-
const result = await conn.captureScreenshot(nodeId, { scale, format });
|
|
746
|
-
return
|
|
883
|
+
const result = await conn.captureScreenshot(nodeId, { scale, format, jpegQuality });
|
|
884
|
+
return toolResult(result, "figma_get_component_image");
|
|
747
885
|
}));
|
|
748
886
|
server.registerTool("figma_get_component_for_development", {
|
|
749
|
-
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).",
|
|
750
888
|
inputSchema: {
|
|
751
889
|
nodeId: z.string(),
|
|
752
|
-
scale: z.number().min(0.5).max(4).optional().default(2),
|
|
753
|
-
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),
|
|
754
893
|
},
|
|
755
894
|
annotations: { readOnlyHint: true },
|
|
756
|
-
}, safeToolHandler(async ({ nodeId, scale, format }) => {
|
|
895
|
+
}, safeToolHandler(async ({ nodeId, scale, format, jpegQuality }) => {
|
|
757
896
|
const conn = getConnector(bridge);
|
|
758
897
|
const [component, screenshot] = await Promise.all([
|
|
759
898
|
conn.getComponentFromPluginUI(nodeId),
|
|
760
|
-
conn.captureScreenshot(nodeId, { scale, format }),
|
|
899
|
+
conn.captureScreenshot(nodeId, { scale, format, jpegQuality }),
|
|
761
900
|
]);
|
|
762
901
|
const comp = component?.component ?? component;
|
|
763
902
|
const out = { success: true, component: comp, image: screenshot?.image ?? screenshot?.data };
|
|
764
|
-
return
|
|
903
|
+
return toolResult(out, "figma_get_component_for_development");
|
|
765
904
|
}));
|
|
766
905
|
// ---- Batch variables & setup_design_tokens & arrange_component_set ----
|
|
767
906
|
server.registerTool("figma_batch_create_variables", {
|
|
@@ -1038,6 +1177,11 @@ export async function main() {
|
|
|
1038
1177
|
const listening = bridge.isListening();
|
|
1039
1178
|
const currentPort = bridge.getPort();
|
|
1040
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;
|
|
1041
1185
|
let msg;
|
|
1042
1186
|
if (!listening) {
|
|
1043
1187
|
msg = startError
|
|
@@ -1046,6 +1190,8 @@ export async function main() {
|
|
|
1046
1190
|
}
|
|
1047
1191
|
else if (connected) {
|
|
1048
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;
|
|
1049
1195
|
}
|
|
1050
1196
|
else {
|
|
1051
1197
|
msg = PLUGIN_NOT_CONNECTED;
|
|
@@ -1067,30 +1213,57 @@ export async function main() {
|
|
|
1067
1213
|
connectedClients: clientCount,
|
|
1068
1214
|
connectedFiles,
|
|
1069
1215
|
bridgePort: currentPort,
|
|
1216
|
+
serverVersion: FMCP_VERSION,
|
|
1070
1217
|
...(autoIncremented && { preferredPort: bridge.getPreferredPort(), autoIncremented }),
|
|
1071
1218
|
message: msg,
|
|
1072
1219
|
...(startError && { startError }),
|
|
1073
1220
|
...(portHint && { portHint }),
|
|
1221
|
+
...(versionWarning && { versionWarning }),
|
|
1074
1222
|
}),
|
|
1075
1223
|
}],
|
|
1076
1224
|
};
|
|
1077
1225
|
});
|
|
1078
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).
|
|
1079
1230
|
server.registerTool("figma_create_frame", {
|
|
1080
|
-
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.",
|
|
1081
1234
|
inputSchema: {
|
|
1082
1235
|
name: z.string().optional().default("Frame").describe("Frame name"),
|
|
1083
1236
|
x: z.number().optional().describe("X position. If omitted, auto-positions to the right of existing content"),
|
|
1084
1237
|
y: z.number().optional().default(0),
|
|
1085
1238
|
width: z.number().optional().default(200),
|
|
1086
1239
|
height: z.number().optional().default(200),
|
|
1087
|
-
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+)."),
|
|
1088
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."),
|
|
1089
1260
|
},
|
|
1090
|
-
}, 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 }) => {
|
|
1091
1262
|
try {
|
|
1263
|
+
invalidateCache();
|
|
1092
1264
|
const conn = getConnector(bridge);
|
|
1093
1265
|
const autoPosition = x === undefined && !parentId;
|
|
1266
|
+
const useAutoLayout = layoutMode !== "NONE";
|
|
1094
1267
|
const code = `
|
|
1095
1268
|
${autoPosition ? `
|
|
1096
1269
|
let posX = 0;
|
|
@@ -1108,9 +1281,64 @@ export async function main() {
|
|
|
1108
1281
|
frame.name = ${JSON.stringify(name)};
|
|
1109
1282
|
frame.x = posX; frame.y = ${y};
|
|
1110
1283
|
frame.resize(${width}, ${height});
|
|
1111
|
-
${
|
|
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
|
+
` : ""}
|
|
1112
1339
|
${parentId ? `const parent = await figma.getNodeByIdAsync(${JSON.stringify(parentId)}); if (parent && 'appendChild' in parent) parent.appendChild(frame);` : ""}
|
|
1113
|
-
|
|
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 };
|
|
1114
1342
|
`;
|
|
1115
1343
|
const result = await conn.executeCodeViaUI(code, 10000);
|
|
1116
1344
|
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }] };
|
|
@@ -1218,11 +1446,12 @@ export async function main() {
|
|
|
1218
1446
|
server.registerTool("figma_export_nodes", {
|
|
1219
1447
|
description: "Export one or multiple nodes as SVG, PNG, JPG, or PDF. Returns base64-encoded data for each node. " +
|
|
1220
1448
|
"Supports batch export (up to 50 nodes). No REST API token needed — uses plugin exportAsync. " +
|
|
1221
|
-
"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.",
|
|
1222
1451
|
inputSchema: {
|
|
1223
1452
|
nodeIds: z.array(z.string()).min(1).max(50).describe("Node IDs to export (1-50)"),
|
|
1224
1453
|
format: z.enum(["PNG", "SVG", "JPG", "PDF"]).optional().default("PNG").describe("Export format"),
|
|
1225
|
-
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)"),
|
|
1226
1455
|
svgOutlineText: z.boolean().optional().describe("SVG: render text as outlines (default true)"),
|
|
1227
1456
|
svgIncludeId: z.boolean().optional().describe("SVG: include node IDs in attributes"),
|
|
1228
1457
|
},
|
|
@@ -1277,9 +1506,13 @@ export async function main() {
|
|
|
1277
1506
|
});
|
|
1278
1507
|
// ---- figma_search_assets (team library search via plugin) ----
|
|
1279
1508
|
server.registerTool("figma_search_assets", {
|
|
1280
|
-
description: "Search for
|
|
1281
|
-
"
|
|
1282
|
-
"
|
|
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. " +
|
|
1283
1516
|
"Pass assetTypes to filter: ['variables'], ['components'], or both (default).",
|
|
1284
1517
|
inputSchema: {
|
|
1285
1518
|
figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
|