@atezer/figma-mcp-bridge 1.9.4 → 1.9.6
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 +103 -0
- package/README.md +19 -2
- package/dist/core/discovery-counter.d.ts +54 -0
- package/dist/core/discovery-counter.d.ts.map +1 -0
- package/dist/core/discovery-counter.js +140 -0
- package/dist/core/discovery-counter.js.map +1 -0
- package/dist/core/plugin-bridge-connector.d.ts +5 -0
- package/dist/core/plugin-bridge-connector.d.ts.map +1 -1
- package/dist/core/plugin-bridge-connector.js.map +1 -1
- package/dist/local-plugin-only.d.ts.map +1 -1
- package/dist/local-plugin-only.js +192 -22
- package/dist/local-plugin-only.js.map +1 -1
- package/f-mcp-plugin/code.js +299 -33
- package/f-mcp-plugin/ui.html +1 -1
- package/package.json +1 -1
- package/skills/figma-canvas-ops/SKILL.md +20 -0
- package/skills/fmcp-intent-router/SKILL.md +42 -0
- package/skills/fmcp-screen-orchestrator/SKILL.md +29 -0
|
@@ -19,6 +19,10 @@ import { PluginBridgeConnector } from "./core/plugin-bridge-connector.js";
|
|
|
19
19
|
import { parseFigmaUrl } from "./core/figma-url.js";
|
|
20
20
|
import { truncateRestResponse, truncatePluginResponse } from "./core/response-guard.js";
|
|
21
21
|
import { analyzeCodeForWarnings } from "./core/code-warnings.js";
|
|
22
|
+
import { discoveryCounter } from "./core/discovery-counter.js";
|
|
23
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { homedir } from "node:os";
|
|
22
26
|
import { resolveDevice, DEVICE_PRESETS } from "./core/device-presets.js";
|
|
23
27
|
import { closeAuditLog } from "./core/audit-log.js";
|
|
24
28
|
import { FMCP_VERSION } from "./core/version.js";
|
|
@@ -34,6 +38,103 @@ const logger = createChildLogger({ component: "plugin-only-mcp" });
|
|
|
34
38
|
*/
|
|
35
39
|
const LEGACY_DEFAULTS = process.env.FMCP_LEGACY_DEFAULTS === "1";
|
|
36
40
|
/** Resolve fileKey from figmaUrl (parse) or explicit fileKey. Returns undefined if neither yields a key. */
|
|
41
|
+
/**
|
|
42
|
+
* v1.9.5: Screenshot dosya kayıt konumu.
|
|
43
|
+
* ~/.fmcp/screenshots/ altında timestamp-nodeId.ext formatı.
|
|
44
|
+
*/
|
|
45
|
+
const SCREENSHOT_DIR = join(homedir(), ".fmcp", "screenshots");
|
|
46
|
+
function ensureScreenshotDir() {
|
|
47
|
+
try {
|
|
48
|
+
mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
catch { /* ignore EEXIST */ }
|
|
51
|
+
return SCREENSHOT_DIR;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* v1.9.5: Base64 image payload'u disk'e yaz, filePath döndür.
|
|
55
|
+
* Base64 decode edilir, dosyaya binary olarak yazılır.
|
|
56
|
+
*/
|
|
57
|
+
function saveBase64ToFile(base64, format, nodeIdHint) {
|
|
58
|
+
ensureScreenshotDir();
|
|
59
|
+
const ts = Date.now();
|
|
60
|
+
const safeNodeId = (nodeIdHint ?? "node").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
61
|
+
const ext = format.toLowerCase() === "png" ? "png" : "jpg";
|
|
62
|
+
const fileName = `${ts}-${safeNodeId}.${ext}`;
|
|
63
|
+
const filePath = join(SCREENSHOT_DIR, fileName);
|
|
64
|
+
const buffer = Buffer.from(base64, "base64");
|
|
65
|
+
writeFileSync(filePath, buffer);
|
|
66
|
+
return { filePath, fileSize: buffer.length };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* v1.9.5: Screenshot result'ı returnMode'a göre post-process.
|
|
70
|
+
* - file: base64'u disk'e yaz, image.base64 yerine filePath döndür
|
|
71
|
+
* - regions: regions[].image.base64'u tek tek disk'e yaz, regions[].filePath döndür
|
|
72
|
+
* - summary: plugin zaten metadata döndürdü, dokunma
|
|
73
|
+
* - base64: eski davranış, dokunma (ama _warning ekle)
|
|
74
|
+
*/
|
|
75
|
+
async function postProcessScreenshotResult(result, returnMode, format, nodeId) {
|
|
76
|
+
if (!result || typeof result !== "object" || !result.success)
|
|
77
|
+
return result;
|
|
78
|
+
if (returnMode === "file") {
|
|
79
|
+
// Plugin single-image response: { success, image: { base64, format, width, height } }
|
|
80
|
+
if (result.image && typeof result.image === "object" && typeof result.image.base64 === "string") {
|
|
81
|
+
try {
|
|
82
|
+
const { filePath, fileSize } = saveBase64ToFile(result.image.base64, format, nodeId);
|
|
83
|
+
const { base64, ...imageMetaRest } = result.image;
|
|
84
|
+
return {
|
|
85
|
+
success: true,
|
|
86
|
+
filePath,
|
|
87
|
+
fileSize,
|
|
88
|
+
dimensions: { width: result.image.width, height: result.image.height },
|
|
89
|
+
format: result.image.format ?? format,
|
|
90
|
+
nodeId: nodeId ?? null,
|
|
91
|
+
imageMeta: imageMetaRest,
|
|
92
|
+
hint: "v1.9.5 file mode: Screenshot diske yazildi. Claude Desktop 'open <filePath>' ile acabilir. Base64 context'te YOK (~30K token tasarrufu). Onceki davranis icin returnMode: 'base64'.",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
return { ...result, _warning: `File save failed: ${err instanceof Error ? err.message : String(err)}. Falling back to base64 mode.` };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (returnMode === "regions" && Array.isArray(result.regions)) {
|
|
101
|
+
const processedRegions = result.regions.map((region, idx) => {
|
|
102
|
+
if (region.image && typeof region.image.base64 === "string") {
|
|
103
|
+
try {
|
|
104
|
+
const regionNodeId = region.nodeId ?? `${nodeId}-region-${idx}`;
|
|
105
|
+
const { filePath, fileSize } = saveBase64ToFile(region.image.base64, format, regionNodeId);
|
|
106
|
+
const { base64, ...imageMetaRest } = region.image;
|
|
107
|
+
return {
|
|
108
|
+
name: region.name,
|
|
109
|
+
nodeId: region.nodeId,
|
|
110
|
+
filePath,
|
|
111
|
+
fileSize,
|
|
112
|
+
dimensions: { width: region.image.width, height: region.image.height },
|
|
113
|
+
imageMeta: imageMetaRest,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
return { ...region, _warning: `File save failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return region;
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
...result,
|
|
124
|
+
regions: processedRegions,
|
|
125
|
+
hint: "v1.9.5 regions mode: Her region ayri dosyada. Ilgili olanlari 'open <filePath>' ile ac. Tumune bakmana gerek yok. Base64 context'te YOK.",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (returnMode === "base64") {
|
|
129
|
+
// Legacy — add warning
|
|
130
|
+
return {
|
|
131
|
+
...result,
|
|
132
|
+
_warning: "BASE64_MODE: Context'e ~30K token eklendi. Bir sonraki call'da 'file' veya 'regions' tercih et.",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// summary mode — plugin already returned metadata-only, no post-processing needed
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
37
138
|
function resolveFileKey(figmaUrl, explicitFileKey) {
|
|
38
139
|
if (explicitFileKey && explicitFileKey.trim())
|
|
39
140
|
return explicitFileKey.trim();
|
|
@@ -389,7 +490,10 @@ export async function main() {
|
|
|
389
490
|
"v1.8.1+: Static analysis detects design-system discipline violations (hardcoded colors, missing token bindings, no-instance usage, hardcoded typography). " +
|
|
390
491
|
"SEVERE warnings are promoted to the top of the response as _designSystemViolations — Claude must read and self-correct. " +
|
|
391
492
|
"Also detects gotchas: FILL/ABSOLUTE before appendChild, sync API usage, missing loadFontAsync, sync currentPage assignment. " +
|
|
392
|
-
"For component instances: use setProperties({...}), NOT findAll(TEXT)."
|
|
493
|
+
"For component instances: use setProperties({...}), NOT findAll(TEXT). " +
|
|
494
|
+
"v1.9.6+: Post-execute scan — eğer kod `return { createdNodeIds: [...] }` veya `nodeIds`/`ids`/`frameId`/`rootId`/`nodeId` döndürürse " +
|
|
495
|
+
"plugin oluşturulan node'ları otomatik tarar, unbound fill/padding/radius/text-style varsa response'a `_POST_EXECUTE_SCAN_BLOCKING: true` ve " +
|
|
496
|
+
"`_postExecuteViolations` alanı ekler. Bu flag varsa execute geçersiz sayılır — kodu düzelt (setBoundVariable/setTextStyleIdAsync ekle) ve tekrar çalıştır.",
|
|
393
497
|
inputSchema: {
|
|
394
498
|
figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
|
|
395
499
|
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
@@ -406,6 +510,8 @@ export async function main() {
|
|
|
406
510
|
}
|
|
407
511
|
const clampedTimeout = Math.max(3000, Math.min(timeout ?? 15000, 30000));
|
|
408
512
|
invalidateCache();
|
|
513
|
+
// v1.9.5: Discovery budget tracking — code pattern analysis (read-only vs mutation)
|
|
514
|
+
const budget = discoveryCounter.track("figma_execute", code);
|
|
409
515
|
// v1.8.1: Structured warnings with SEVERE vs ADVISORY severity
|
|
410
516
|
const codeWarnings = analyzeCodeForWarnings(code);
|
|
411
517
|
const severeWarnings = codeWarnings.filter((w) => w.severity === "SEVERE");
|
|
@@ -425,8 +531,16 @@ export async function main() {
|
|
|
425
531
|
},
|
|
426
532
|
}
|
|
427
533
|
: {};
|
|
428
|
-
|
|
429
|
-
|
|
534
|
+
// v1.9.5: Discovery budget warnings merge with advisory warnings
|
|
535
|
+
const combinedWarnings = [
|
|
536
|
+
...advisoryWarnings.map((w) => w.message),
|
|
537
|
+
...(budget.warnings ?? []),
|
|
538
|
+
];
|
|
539
|
+
const warningsField = combinedWarnings.length > 0
|
|
540
|
+
? { _warnings: combinedWarnings }
|
|
541
|
+
: {};
|
|
542
|
+
const budgetBlockingField = budget._DISCOVERY_BUDGET_EXCEEDED_BLOCKING
|
|
543
|
+
? { _DISCOVERY_BUDGET_EXCEEDED_BLOCKING: true }
|
|
430
544
|
: {};
|
|
431
545
|
const startTime = Date.now();
|
|
432
546
|
try {
|
|
@@ -445,6 +559,7 @@ export async function main() {
|
|
|
445
559
|
catch { /* safe fallback */ }
|
|
446
560
|
return {
|
|
447
561
|
content: [{ type: "text", text: JSON.stringify({
|
|
562
|
+
...budgetBlockingField, // v1.9.5: discovery BLOCKING flag at top
|
|
448
563
|
...dsViolations, // v1.8.1: SEVERE warnings FIRST, before anything else
|
|
449
564
|
...result,
|
|
450
565
|
errorCategory: category,
|
|
@@ -455,17 +570,36 @@ export async function main() {
|
|
|
455
570
|
isError: true,
|
|
456
571
|
};
|
|
457
572
|
}
|
|
573
|
+
// v1.9.6: Post-execute scan result → BLOCKING signal injection
|
|
574
|
+
const postScan = typeof result === "object" && result !== null
|
|
575
|
+
? result._postExecuteScan
|
|
576
|
+
: undefined;
|
|
577
|
+
const postScanBlocking = {};
|
|
578
|
+
if (postScan && typeof postScan.violationCount === "number" && postScan.violationCount > 0) {
|
|
579
|
+
postScanBlocking._POST_EXECUTE_SCAN_BLOCKING = true;
|
|
580
|
+
postScanBlocking._postExecuteViolations = {
|
|
581
|
+
count: postScan.violationCount,
|
|
582
|
+
totalChecked: postScan.totalChecked ?? 0,
|
|
583
|
+
severity: "BLOCKING",
|
|
584
|
+
message: "❌ v1.9.6 POST-EXECUTE SCAN: Olusturulan node'larda " + postScan.violationCount + " unbound tespit edildi. Kodu duzelt — her unbound icin setBoundVariable veya setTextStyleIdAsync cagrisi ekle.",
|
|
585
|
+
violations: postScan.violations ?? [],
|
|
586
|
+
action: "❌ BLOCKING: Execute sonucu KABUL EDILMEZ. Listelenen nodeId'lerdeki unbound node'lari bagla ve kodu tekrar calistir. Skip edemezsin.",
|
|
587
|
+
retry_required: true,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
458
590
|
let enriched;
|
|
459
591
|
try {
|
|
460
592
|
enriched = typeof result === "object" && result !== null
|
|
461
593
|
? {
|
|
594
|
+
...postScanBlocking, // v1.9.6: post-execute BLOCKING top
|
|
595
|
+
...budgetBlockingField, // v1.9.5: discovery BLOCKING flag at top
|
|
462
596
|
...dsViolations, // v1.8.1: SEVERE warnings at top level
|
|
463
597
|
...result,
|
|
464
598
|
_metrics: { durationMs, timeoutMs: clampedTimeout },
|
|
465
599
|
...warningsField,
|
|
466
600
|
}
|
|
467
|
-
: severeWarnings.length > 0 || advisoryWarnings.length > 0
|
|
468
|
-
? { ...dsViolations, result, ...warningsField }
|
|
601
|
+
: severeWarnings.length > 0 || advisoryWarnings.length > 0 || combinedWarnings.length > 0
|
|
602
|
+
? { ...budgetBlockingField, ...dsViolations, result, ...warningsField }
|
|
469
603
|
: result;
|
|
470
604
|
}
|
|
471
605
|
catch {
|
|
@@ -610,10 +744,18 @@ export async function main() {
|
|
|
610
744
|
const result = await conn.scanDsCompliance({ nodeId, threshold, expectedDs });
|
|
611
745
|
return toolResult(result, "figma_scan_ds_compliance");
|
|
612
746
|
}));
|
|
613
|
-
// ---- figma_capture_screenshot ----
|
|
614
|
-
// v1.8.0: Default JPG@1x (~80% smaller base64 vs PNG@2x).
|
|
747
|
+
// ---- figma_capture_screenshot (v1.9.5: method selection) ----
|
|
748
|
+
// v1.8.0: Default JPG@1x (~80% smaller base64 vs PNG@2x).
|
|
749
|
+
// v1.9.5: returnMode param — Claude bağlama göre file/summary/regions/base64 seçer.
|
|
750
|
+
// Default "file": screenshot ~/.fmcp/screenshots/ altına yazılır, filePath döner,
|
|
751
|
+
// base64 context'e girmez (30K → 0.3K token tasarrufu).
|
|
615
752
|
server.registerTool("figma_capture_screenshot", {
|
|
616
|
-
description: "
|
|
753
|
+
description: "v1.9.5: 4 returnMode ile screenshot. Default 'file' (dosyaya yazar, base64 context'te YOK). " +
|
|
754
|
+
"'summary' screenshot çekmeden metadata özeti (planlama için), " +
|
|
755
|
+
"'regions' büyük ekranları children/slices olarak parçalar, " +
|
|
756
|
+
"'base64' eski davranış (opt-in, ~30K token maliyetli). " +
|
|
757
|
+
"Context-aware fallback: >%80 context kullanımında base64/file → summary'ye otomatik düşer. " +
|
|
758
|
+
"Karar ağacı: planlama→summary, teslimat→file, scroll'lu ekran→regions, son çare→base64.",
|
|
617
759
|
inputSchema: {
|
|
618
760
|
figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
|
|
619
761
|
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
@@ -621,23 +763,51 @@ export async function main() {
|
|
|
621
763
|
format: z.enum(["PNG", "JPG"]).optional().default(LEGACY_DEFAULTS ? "PNG" : "JPG"),
|
|
622
764
|
scale: z.number().optional().default(LEGACY_DEFAULTS ? 2 : 1),
|
|
623
765
|
jpegQuality: z.number().min(30).max(100).optional().default(70).describe("JPEG quality 30-100. Ignored when format=PNG."),
|
|
766
|
+
returnMode: z
|
|
767
|
+
.enum(["file", "base64", "summary", "regions"])
|
|
768
|
+
.optional()
|
|
769
|
+
.default("file")
|
|
770
|
+
.describe("v1.9.5 method: 'file' (default, disk + filePath), 'base64' (legacy, context'e dahil), " +
|
|
771
|
+
"'summary' (metadata-only, screenshotsuz), 'regions' (parçalı — children veya slices)."),
|
|
772
|
+
regionStrategy: z
|
|
773
|
+
.enum(["children", "slices"])
|
|
774
|
+
.optional()
|
|
775
|
+
.default("children")
|
|
776
|
+
.describe("returnMode='regions' için: 'children' = node'un top-level child'ları ayrı ayrı, 'slices' = dikey slice'lar."),
|
|
777
|
+
maxRegions: z.number().min(1).max(20).optional().default(8).describe("returnMode='regions' için: maks region sayısı."),
|
|
778
|
+
sliceHeight: z.number().min(200).max(2000).optional().default(600).describe("regionStrategy='slices' için slice yüksekliği (px)."),
|
|
779
|
+
requestedSlices: z.array(z.number()).optional().describe("regionStrategy='slices' için spesifik slice index'leri (örn: [0,2] → sadece 1. ve 3. slice)."),
|
|
624
780
|
},
|
|
625
781
|
annotations: { readOnlyHint: true },
|
|
626
|
-
}, safeToolHandler(async ({ figmaUrl, fileKey, nodeId, format, scale, jpegQuality }) => {
|
|
782
|
+
}, safeToolHandler(async ({ figmaUrl, fileKey, nodeId, format, scale, jpegQuality, returnMode, regionStrategy, maxRegions, sliceHeight, requestedSlices, }) => {
|
|
783
|
+
// v1.9.5: Track discovery budget
|
|
784
|
+
const budget = discoveryCounter.track("figma_capture_screenshot");
|
|
627
785
|
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
628
|
-
const result = await conn.captureScreenshot(nodeId ?? null, {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
//
|
|
639
|
-
|
|
640
|
-
|
|
786
|
+
const result = await conn.captureScreenshot(nodeId ?? null, {
|
|
787
|
+
format,
|
|
788
|
+
scale,
|
|
789
|
+
jpegQuality,
|
|
790
|
+
returnMode,
|
|
791
|
+
regionStrategy,
|
|
792
|
+
maxRegions,
|
|
793
|
+
sliceHeight,
|
|
794
|
+
requestedSlices,
|
|
795
|
+
});
|
|
796
|
+
// v1.9.5 post-processing: base64 payload'ları file'a yaz (mode='file' veya 'regions')
|
|
797
|
+
const processed = await postProcessScreenshotResult(result, returnMode, format, nodeId);
|
|
798
|
+
// Budget warning injection
|
|
799
|
+
const budgetFields = {};
|
|
800
|
+
if (budget.warnings && budget.warnings.length > 0) {
|
|
801
|
+
budgetFields._warnings = [...(processed._warnings ?? []), ...budget.warnings];
|
|
802
|
+
}
|
|
803
|
+
if (budget._DISCOVERY_BUDGET_EXCEEDED_BLOCKING) {
|
|
804
|
+
budgetFields._DISCOVERY_BUDGET_EXCEEDED_BLOCKING = true;
|
|
805
|
+
}
|
|
806
|
+
const enriched = typeof processed === "object" && processed !== null
|
|
807
|
+
? { ...processed, ...budgetFields }
|
|
808
|
+
: processed;
|
|
809
|
+
// skipGuard: true — base64 intact kalır (legacy mode için gerekli)
|
|
810
|
+
return toolResult(enriched, "figma_capture_screenshot", { skipGuard: true });
|
|
641
811
|
}));
|
|
642
812
|
// ---- figma_set_instance_properties ----
|
|
643
813
|
server.registerTool("figma_set_instance_properties", {
|