@atezer/figma-mcp-bridge 1.9.4 → 1.9.5
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 +56 -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 +170 -21
- package/dist/local-plugin-only.js.map +1 -1
- package/f-mcp-plugin/code.js +198 -32
- 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 +9 -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();
|
|
@@ -406,6 +507,8 @@ export async function main() {
|
|
|
406
507
|
}
|
|
407
508
|
const clampedTimeout = Math.max(3000, Math.min(timeout ?? 15000, 30000));
|
|
408
509
|
invalidateCache();
|
|
510
|
+
// v1.9.5: Discovery budget tracking — code pattern analysis (read-only vs mutation)
|
|
511
|
+
const budget = discoveryCounter.track("figma_execute", code);
|
|
409
512
|
// v1.8.1: Structured warnings with SEVERE vs ADVISORY severity
|
|
410
513
|
const codeWarnings = analyzeCodeForWarnings(code);
|
|
411
514
|
const severeWarnings = codeWarnings.filter((w) => w.severity === "SEVERE");
|
|
@@ -425,8 +528,16 @@ export async function main() {
|
|
|
425
528
|
},
|
|
426
529
|
}
|
|
427
530
|
: {};
|
|
428
|
-
|
|
429
|
-
|
|
531
|
+
// v1.9.5: Discovery budget warnings merge with advisory warnings
|
|
532
|
+
const combinedWarnings = [
|
|
533
|
+
...advisoryWarnings.map((w) => w.message),
|
|
534
|
+
...(budget.warnings ?? []),
|
|
535
|
+
];
|
|
536
|
+
const warningsField = combinedWarnings.length > 0
|
|
537
|
+
? { _warnings: combinedWarnings }
|
|
538
|
+
: {};
|
|
539
|
+
const budgetBlockingField = budget._DISCOVERY_BUDGET_EXCEEDED_BLOCKING
|
|
540
|
+
? { _DISCOVERY_BUDGET_EXCEEDED_BLOCKING: true }
|
|
430
541
|
: {};
|
|
431
542
|
const startTime = Date.now();
|
|
432
543
|
try {
|
|
@@ -445,6 +556,7 @@ export async function main() {
|
|
|
445
556
|
catch { /* safe fallback */ }
|
|
446
557
|
return {
|
|
447
558
|
content: [{ type: "text", text: JSON.stringify({
|
|
559
|
+
...budgetBlockingField, // v1.9.5: discovery BLOCKING flag at top
|
|
448
560
|
...dsViolations, // v1.8.1: SEVERE warnings FIRST, before anything else
|
|
449
561
|
...result,
|
|
450
562
|
errorCategory: category,
|
|
@@ -459,13 +571,14 @@ export async function main() {
|
|
|
459
571
|
try {
|
|
460
572
|
enriched = typeof result === "object" && result !== null
|
|
461
573
|
? {
|
|
574
|
+
...budgetBlockingField, // v1.9.5: discovery BLOCKING flag at top
|
|
462
575
|
...dsViolations, // v1.8.1: SEVERE warnings at top level
|
|
463
576
|
...result,
|
|
464
577
|
_metrics: { durationMs, timeoutMs: clampedTimeout },
|
|
465
578
|
...warningsField,
|
|
466
579
|
}
|
|
467
|
-
: severeWarnings.length > 0 || advisoryWarnings.length > 0
|
|
468
|
-
? { ...dsViolations, result, ...warningsField }
|
|
580
|
+
: severeWarnings.length > 0 || advisoryWarnings.length > 0 || combinedWarnings.length > 0
|
|
581
|
+
? { ...budgetBlockingField, ...dsViolations, result, ...warningsField }
|
|
469
582
|
: result;
|
|
470
583
|
}
|
|
471
584
|
catch {
|
|
@@ -610,10 +723,18 @@ export async function main() {
|
|
|
610
723
|
const result = await conn.scanDsCompliance({ nodeId, threshold, expectedDs });
|
|
611
724
|
return toolResult(result, "figma_scan_ds_compliance");
|
|
612
725
|
}));
|
|
613
|
-
// ---- figma_capture_screenshot ----
|
|
614
|
-
// v1.8.0: Default JPG@1x (~80% smaller base64 vs PNG@2x).
|
|
726
|
+
// ---- figma_capture_screenshot (v1.9.5: method selection) ----
|
|
727
|
+
// v1.8.0: Default JPG@1x (~80% smaller base64 vs PNG@2x).
|
|
728
|
+
// v1.9.5: returnMode param — Claude bağlama göre file/summary/regions/base64 seçer.
|
|
729
|
+
// Default "file": screenshot ~/.fmcp/screenshots/ altına yazılır, filePath döner,
|
|
730
|
+
// base64 context'e girmez (30K → 0.3K token tasarrufu).
|
|
615
731
|
server.registerTool("figma_capture_screenshot", {
|
|
616
|
-
description: "
|
|
732
|
+
description: "v1.9.5: 4 returnMode ile screenshot. Default 'file' (dosyaya yazar, base64 context'te YOK). " +
|
|
733
|
+
"'summary' screenshot çekmeden metadata özeti (planlama için), " +
|
|
734
|
+
"'regions' büyük ekranları children/slices olarak parçalar, " +
|
|
735
|
+
"'base64' eski davranış (opt-in, ~30K token maliyetli). " +
|
|
736
|
+
"Context-aware fallback: >%80 context kullanımında base64/file → summary'ye otomatik düşer. " +
|
|
737
|
+
"Karar ağacı: planlama→summary, teslimat→file, scroll'lu ekran→regions, son çare→base64.",
|
|
617
738
|
inputSchema: {
|
|
618
739
|
figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
|
|
619
740
|
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
@@ -621,23 +742,51 @@ export async function main() {
|
|
|
621
742
|
format: z.enum(["PNG", "JPG"]).optional().default(LEGACY_DEFAULTS ? "PNG" : "JPG"),
|
|
622
743
|
scale: z.number().optional().default(LEGACY_DEFAULTS ? 2 : 1),
|
|
623
744
|
jpegQuality: z.number().min(30).max(100).optional().default(70).describe("JPEG quality 30-100. Ignored when format=PNG."),
|
|
745
|
+
returnMode: z
|
|
746
|
+
.enum(["file", "base64", "summary", "regions"])
|
|
747
|
+
.optional()
|
|
748
|
+
.default("file")
|
|
749
|
+
.describe("v1.9.5 method: 'file' (default, disk + filePath), 'base64' (legacy, context'e dahil), " +
|
|
750
|
+
"'summary' (metadata-only, screenshotsuz), 'regions' (parçalı — children veya slices)."),
|
|
751
|
+
regionStrategy: z
|
|
752
|
+
.enum(["children", "slices"])
|
|
753
|
+
.optional()
|
|
754
|
+
.default("children")
|
|
755
|
+
.describe("returnMode='regions' için: 'children' = node'un top-level child'ları ayrı ayrı, 'slices' = dikey slice'lar."),
|
|
756
|
+
maxRegions: z.number().min(1).max(20).optional().default(8).describe("returnMode='regions' için: maks region sayısı."),
|
|
757
|
+
sliceHeight: z.number().min(200).max(2000).optional().default(600).describe("regionStrategy='slices' için slice yüksekliği (px)."),
|
|
758
|
+
requestedSlices: z.array(z.number()).optional().describe("regionStrategy='slices' için spesifik slice index'leri (örn: [0,2] → sadece 1. ve 3. slice)."),
|
|
624
759
|
},
|
|
625
760
|
annotations: { readOnlyHint: true },
|
|
626
|
-
}, safeToolHandler(async ({ figmaUrl, fileKey, nodeId, format, scale, jpegQuality }) => {
|
|
761
|
+
}, safeToolHandler(async ({ figmaUrl, fileKey, nodeId, format, scale, jpegQuality, returnMode, regionStrategy, maxRegions, sliceHeight, requestedSlices, }) => {
|
|
762
|
+
// v1.9.5: Track discovery budget
|
|
763
|
+
const budget = discoveryCounter.track("figma_capture_screenshot");
|
|
627
764
|
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
|
-
|
|
765
|
+
const result = await conn.captureScreenshot(nodeId ?? null, {
|
|
766
|
+
format,
|
|
767
|
+
scale,
|
|
768
|
+
jpegQuality,
|
|
769
|
+
returnMode,
|
|
770
|
+
regionStrategy,
|
|
771
|
+
maxRegions,
|
|
772
|
+
sliceHeight,
|
|
773
|
+
requestedSlices,
|
|
774
|
+
});
|
|
775
|
+
// v1.9.5 post-processing: base64 payload'ları file'a yaz (mode='file' veya 'regions')
|
|
776
|
+
const processed = await postProcessScreenshotResult(result, returnMode, format, nodeId);
|
|
777
|
+
// Budget warning injection
|
|
778
|
+
const budgetFields = {};
|
|
779
|
+
if (budget.warnings && budget.warnings.length > 0) {
|
|
780
|
+
budgetFields._warnings = [...(processed._warnings ?? []), ...budget.warnings];
|
|
781
|
+
}
|
|
782
|
+
if (budget._DISCOVERY_BUDGET_EXCEEDED_BLOCKING) {
|
|
783
|
+
budgetFields._DISCOVERY_BUDGET_EXCEEDED_BLOCKING = true;
|
|
784
|
+
}
|
|
785
|
+
const enriched = typeof processed === "object" && processed !== null
|
|
786
|
+
? { ...processed, ...budgetFields }
|
|
787
|
+
: processed;
|
|
788
|
+
// skipGuard: true — base64 intact kalır (legacy mode için gerekli)
|
|
789
|
+
return toolResult(enriched, "figma_capture_screenshot", { skipGuard: true });
|
|
641
790
|
}));
|
|
642
791
|
// ---- figma_set_instance_properties ----
|
|
643
792
|
server.registerTool("figma_set_instance_properties", {
|