@mp3wizard/figma-console-mcp 1.14.0 → 1.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -20
- package/dist/apps/design-system-dashboard/mcp-app.html +521 -0
- package/dist/apps/token-browser/mcp-app.html +410 -0
- package/dist/core/port-discovery.d.ts +11 -0
- package/dist/core/port-discovery.d.ts.map +1 -1
- package/dist/core/port-discovery.js +77 -0
- package/dist/core/port-discovery.js.map +1 -1
- package/dist/core/write-tools.d.ts.map +1 -1
- package/dist/core/write-tools.js +10 -2
- package/dist/core/write-tools.js.map +1 -1
- package/dist/local.d.ts +3 -1
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +147 -22
- package/dist/local.js.map +1 -1
- package/figma-desktop-bridge/code.js +60 -2
- package/figma-desktop-bridge/ui-full.html +1236 -0
- package/figma-desktop-bridge/ui.html +173 -1156
- package/package.json +85 -85
package/dist/local.js
CHANGED
|
@@ -16,8 +16,9 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
16
16
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17
17
|
import { z } from "zod";
|
|
18
18
|
import { fileURLToPath } from "url";
|
|
19
|
-
import { dirname, resolve } from "path";
|
|
20
|
-
import { realpathSync, existsSync, readFileSync } from "fs";
|
|
19
|
+
import { dirname, resolve, join } from "path";
|
|
20
|
+
import { realpathSync, existsSync, readFileSync, mkdirSync, copyFileSync, writeFileSync } from "fs";
|
|
21
|
+
import { homedir } from "os";
|
|
21
22
|
import { LocalBrowserManager } from "./browser/local.js";
|
|
22
23
|
import { ConsoleMonitor } from "./core/console-monitor.js";
|
|
23
24
|
import { getConfig } from "./core/config.js";
|
|
@@ -30,10 +31,40 @@ import { registerDesignSystemTools } from "./core/design-system-tools.js";
|
|
|
30
31
|
import { FigmaDesktopConnector } from "./core/figma-desktop-connector.js";
|
|
31
32
|
import { FigmaWebSocketServer } from "./core/websocket-server.js";
|
|
32
33
|
import { WebSocketConnector } from "./core/websocket-connector.js";
|
|
33
|
-
import { DEFAULT_WS_PORT, getPortRange, advertisePort, unadvertisePort, registerPortCleanup, discoverActiveInstances, cleanupStalePortFiles, refreshPortAdvertisement, HEARTBEAT_INTERVAL_MS, } from "./core/port-discovery.js";
|
|
34
|
+
import { DEFAULT_WS_PORT, getPortRange, advertisePort, unadvertisePort, registerPortCleanup, discoverActiveInstances, cleanupStalePortFiles, cleanupOrphanedProcesses, refreshPortAdvertisement, HEARTBEAT_INTERVAL_MS, } from "./core/port-discovery.js";
|
|
34
35
|
import { registerTokenBrowserApp } from "./apps/token-browser/server.js";
|
|
35
36
|
import { registerDesignSystemDashboardApp } from "./apps/design-system-dashboard/server.js";
|
|
36
37
|
const logger = createChildLogger({ component: "local-server" });
|
|
38
|
+
/**
|
|
39
|
+
* Copy plugin files to a stable directory (~/.figma-console-mcp/plugin/).
|
|
40
|
+
* This gives users a permanent, predictable path to import from instead of
|
|
41
|
+
* the volatile npx cache path that changes between updates.
|
|
42
|
+
*
|
|
43
|
+
* Returns the stable manifest path, or null if copy failed.
|
|
44
|
+
*/
|
|
45
|
+
function setupStablePluginDir(sourcePluginDir) {
|
|
46
|
+
try {
|
|
47
|
+
const stableDir = join(homedir(), ".figma-console-mcp", "plugin");
|
|
48
|
+
mkdirSync(stableDir, { recursive: true });
|
|
49
|
+
const filesToCopy = ["manifest.json", "code.js", "ui.html", "ui-full.html"];
|
|
50
|
+
for (const file of filesToCopy) {
|
|
51
|
+
const src = join(sourcePluginDir, file);
|
|
52
|
+
const dest = join(stableDir, file);
|
|
53
|
+
if (existsSync(src)) {
|
|
54
|
+
copyFileSync(src, dest);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Write a version marker so we can detect stale copies
|
|
58
|
+
const pkg = JSON.parse(readFileSync(join(sourcePluginDir, "..", "package.json"), "utf-8"));
|
|
59
|
+
writeFileSync(join(stableDir, ".version"), pkg.version, "utf-8");
|
|
60
|
+
logger.info({ stableDir }, "Plugin files copied to stable directory");
|
|
61
|
+
return join(stableDir, "manifest.json");
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
logger.warn({ error }, "Could not set up stable plugin directory (non-critical)");
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
37
68
|
/**
|
|
38
69
|
* Local MCP Server
|
|
39
70
|
* Connects to Figma Desktop and provides identical tools to Cloudflare mode
|
|
@@ -56,6 +87,8 @@ class LocalFigmaConsoleMCP {
|
|
|
56
87
|
// In-memory cache for variables data to avoid MCP token limits
|
|
57
88
|
// Maps fileKey -> {data, timestamp}
|
|
58
89
|
this.variablesCache = new Map();
|
|
90
|
+
/** Stable plugin directory path (set during startup) */
|
|
91
|
+
this.stablePluginPath = null;
|
|
59
92
|
this.server = new McpServer({
|
|
60
93
|
name: "Figma Console MCP (Local)",
|
|
61
94
|
version: "0.1.0",
|
|
@@ -226,9 +259,13 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
|
|
|
226
259
|
}
|
|
227
260
|
/**
|
|
228
261
|
* Resolve the path to the Desktop Bridge plugin manifest.
|
|
229
|
-
*
|
|
262
|
+
* Prefers the stable directory (~/.figma-console-mcp/plugin/) over the npx cache path.
|
|
230
263
|
*/
|
|
231
264
|
getPluginPath() {
|
|
265
|
+
// Prefer stable path — consistent across npx updates
|
|
266
|
+
if (this.stablePluginPath && existsSync(this.stablePluginPath)) {
|
|
267
|
+
return this.stablePluginPath;
|
|
268
|
+
}
|
|
232
269
|
try {
|
|
233
270
|
const thisFile = fileURLToPath(import.meta.url);
|
|
234
271
|
// From dist/local.js → go up to package root, then into figma-desktop-bridge
|
|
@@ -1466,7 +1503,15 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
|
|
|
1466
1503
|
|
|
1467
1504
|
**VALIDATION:** After creating/modifying visuals: screenshot with figma_capture_screenshot, check alignment/spacing/proportions, iterate up to 3x.
|
|
1468
1505
|
|
|
1469
|
-
**PLACEMENT:** Always create components inside a Section or Frame, never on blank canvas. Use parent.insertChild(0, bg) for z-ordering backgrounds behind content
|
|
1506
|
+
**PLACEMENT:** Always create components inside a Section or Frame, never on blank canvas. Use parent.insertChild(0, bg) for z-ordering backgrounds behind content.
|
|
1507
|
+
|
|
1508
|
+
**HOUSEKEEPING (MANDATORY):**
|
|
1509
|
+
Before creating: screenshot the target page to see existing content and find clear space.
|
|
1510
|
+
When creating: place inside a named Section, positioned BELOW or AWAY from existing content. Never overlap.
|
|
1511
|
+
After creating: screenshot to verify clean placement and no overlaps.
|
|
1512
|
+
On failure/retry: DELETE any partial artifacts (empty frames, orphaned layers, blank pages) before retrying. Use node.remove() to clean up.
|
|
1513
|
+
Pages: NEVER create a new page if one with that name already exists — use the existing one. If you created a blank page during a failed attempt, delete it.
|
|
1514
|
+
Layers: If your code creates helper frames, placeholder nodes, or intermediate layers that aren't part of the final result, remove them.`, {
|
|
1470
1515
|
code: z
|
|
1471
1516
|
.string()
|
|
1472
1517
|
.describe("JavaScript code to execute. Has access to the 'figma' global object. " +
|
|
@@ -1483,23 +1528,73 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
|
|
|
1483
1528
|
try {
|
|
1484
1529
|
const connector = await this.getDesktopConnector();
|
|
1485
1530
|
const result = await connector.executeCodeViaUI(code, Math.min(timeout, 30000));
|
|
1531
|
+
// Post-execution audit: detect common housekeeping issues
|
|
1532
|
+
// Runs automatically when the code creates pages, components, or frames
|
|
1533
|
+
const createsContent = /createPage|createComponent|createFrame|createSection|createRectangle|createEllipse/.test(code);
|
|
1534
|
+
let housekeepingWarnings = [];
|
|
1535
|
+
if (createsContent && result.success) {
|
|
1536
|
+
try {
|
|
1537
|
+
const auditResult = await connector.executeCodeViaUI(`
|
|
1538
|
+
var warnings = [];
|
|
1539
|
+
var pages = figma.root.children;
|
|
1540
|
+
// Check for duplicate page names
|
|
1541
|
+
var pageNames = {};
|
|
1542
|
+
for (var i = 0; i < pages.length; i++) {
|
|
1543
|
+
var name = pages[i].name;
|
|
1544
|
+
if (pageNames[name]) pageNames[name]++;
|
|
1545
|
+
else pageNames[name] = 1;
|
|
1546
|
+
}
|
|
1547
|
+
for (var name in pageNames) {
|
|
1548
|
+
if (pageNames[name] > 1) warnings.push('DUPLICATE_PAGE: ' + pageNames[name] + ' pages named "' + name + '" — delete the empty duplicate');
|
|
1549
|
+
}
|
|
1550
|
+
// Check for empty pages (likely from failed attempts)
|
|
1551
|
+
for (var i = 0; i < pages.length; i++) {
|
|
1552
|
+
if (pages[i].children.length === 0 && pages[i].name !== '---') {
|
|
1553
|
+
warnings.push('EMPTY_PAGE: "' + pages[i].name + '" has no content — delete if unintended');
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
// Check for nodes placed directly on page (not in section/frame)
|
|
1557
|
+
var currentPage = figma.currentPage;
|
|
1558
|
+
var floatingNodes = 0;
|
|
1559
|
+
for (var i = 0; i < currentPage.children.length; i++) {
|
|
1560
|
+
var child = currentPage.children[i];
|
|
1561
|
+
if (child.type === 'COMPONENT' || child.type === 'FRAME' || child.type === 'RECTANGLE') {
|
|
1562
|
+
if (currentPage.children.length > 1 && child.type !== 'SECTION') floatingNodes++;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
if (floatingNodes > 3) warnings.push('FLOATING_NODES: ' + floatingNodes + ' nodes placed directly on the page canvas — consider grouping inside a Section');
|
|
1566
|
+
return warnings;
|
|
1567
|
+
`, 5000);
|
|
1568
|
+
if (auditResult.success && Array.isArray(auditResult.result) && auditResult.result.length > 0) {
|
|
1569
|
+
housekeepingWarnings = auditResult.result;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
catch {
|
|
1573
|
+
// Audit is best-effort — don't fail the main operation
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
const response = {
|
|
1577
|
+
success: result.success,
|
|
1578
|
+
result: result.result,
|
|
1579
|
+
error: result.error,
|
|
1580
|
+
resultAnalysis: result.resultAnalysis,
|
|
1581
|
+
fileContext: result.fileContext,
|
|
1582
|
+
timestamp: Date.now(),
|
|
1583
|
+
...(attempt > 0
|
|
1584
|
+
? { reconnected: true, attempts: attempt + 1 }
|
|
1585
|
+
: {}),
|
|
1586
|
+
};
|
|
1587
|
+
if (housekeepingWarnings.length > 0) {
|
|
1588
|
+
response.housekeeping = {
|
|
1589
|
+
warnings: housekeepingWarnings,
|
|
1590
|
+
ai_instruction: "CLEANUP REQUIRED: The warnings above indicate housekeeping issues from your recent operation. Fix these NOW before proceeding — delete empty/duplicate pages, remove orphaned nodes, and move floating content into Sections.",
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1486
1593
|
return {
|
|
1487
1594
|
content: [
|
|
1488
1595
|
{
|
|
1489
1596
|
type: "text",
|
|
1490
|
-
text: JSON.stringify(
|
|
1491
|
-
success: result.success,
|
|
1492
|
-
result: result.result,
|
|
1493
|
-
error: result.error,
|
|
1494
|
-
// Include resultAnalysis for silent failure detection
|
|
1495
|
-
resultAnalysis: result.resultAnalysis,
|
|
1496
|
-
// Include file context so users know which file was queried
|
|
1497
|
-
fileContext: result.fileContext,
|
|
1498
|
-
timestamp: Date.now(),
|
|
1499
|
-
...(attempt > 0
|
|
1500
|
-
? { reconnected: true, attempts: attempt + 1 }
|
|
1501
|
-
: {}),
|
|
1502
|
-
}),
|
|
1597
|
+
text: JSON.stringify(response),
|
|
1503
1598
|
},
|
|
1504
1599
|
],
|
|
1505
1600
|
};
|
|
@@ -3934,7 +4029,7 @@ After instantiating components, use figma_take_screenshot to verify the result l
|
|
|
3934
4029
|
}
|
|
3935
4030
|
});
|
|
3936
4031
|
// Tool: Create Child Node
|
|
3937
|
-
this.server.tool("figma_create_child", "Create a new child node inside a parent container.
|
|
4032
|
+
this.server.tool("figma_create_child", "Create a new child node inside a parent container. Always place inside an existing Section or Frame — never on a bare page. If no suitable parent exists, create a Section first. Clean up any empty or orphaned nodes if the operation fails.", {
|
|
3938
4033
|
parentId: z.string().describe("The parent node ID"),
|
|
3939
4034
|
nodeType: z
|
|
3940
4035
|
.enum(["RECTANGLE", "ELLIPSE", "FRAME", "TEXT", "LINE"])
|
|
@@ -4858,13 +4953,30 @@ return {
|
|
|
4858
4953
|
async start() {
|
|
4859
4954
|
try {
|
|
4860
4955
|
logger.info({ config: this.config }, "Starting Figma Console MCP (Local Mode)");
|
|
4956
|
+
// Copy plugin files to stable directory (~/.figma-console-mcp/plugin/)
|
|
4957
|
+
// so users have a permanent import path that survives npx cache changes.
|
|
4958
|
+
try {
|
|
4959
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
4960
|
+
const packageRoot = dirname(dirname(thisFile));
|
|
4961
|
+
const sourcePluginDir = resolve(packageRoot, "figma-desktop-bridge");
|
|
4962
|
+
if (existsSync(sourcePluginDir)) {
|
|
4963
|
+
this.stablePluginPath = setupStablePluginDir(sourcePluginDir);
|
|
4964
|
+
}
|
|
4965
|
+
}
|
|
4966
|
+
catch {
|
|
4967
|
+
// Non-critical — stable dir is a convenience feature
|
|
4968
|
+
}
|
|
4861
4969
|
// Start WebSocket bridge server with port range fallback.
|
|
4862
4970
|
// If the preferred port is taken (e.g., Claude Desktop Chat tab already bound it),
|
|
4863
4971
|
// try subsequent ports in the range (9223-9232) so multiple instances can coexist.
|
|
4864
4972
|
const wsHost = process.env.FIGMA_WS_HOST || 'localhost';
|
|
4865
4973
|
this.wsPreferredPort = parseInt(process.env.FIGMA_WS_PORT || String(DEFAULT_WS_PORT), 10);
|
|
4866
|
-
// Clean up
|
|
4974
|
+
// Clean up stale/orphaned MCP server instances before trying to bind.
|
|
4975
|
+
// Phase 1: Remove stale port files and terminate zombie processes that have port files
|
|
4867
4976
|
cleanupStalePortFiles();
|
|
4977
|
+
// Phase 2: Deep scan for orphaned processes holding ports WITHOUT port files
|
|
4978
|
+
// (e.g., old instances from before port file tracking, or files already cleaned up)
|
|
4979
|
+
cleanupOrphanedProcesses(this.wsPreferredPort);
|
|
4868
4980
|
const portsToTry = getPortRange(this.wsPreferredPort);
|
|
4869
4981
|
let boundPort = null;
|
|
4870
4982
|
for (const port of portsToTry) {
|
|
@@ -5017,10 +5129,23 @@ async function main() {
|
|
|
5017
5129
|
const currentFile = fileURLToPath(import.meta.url);
|
|
5018
5130
|
const entryFile = process.argv[1] ? realpathSync(resolve(process.argv[1])) : "";
|
|
5019
5131
|
if (currentFile === entryFile) {
|
|
5020
|
-
// Handle --print-path: print the Desktop Bridge manifest path and exit
|
|
5132
|
+
// Handle --print-path: print the Desktop Bridge manifest path and exit.
|
|
5133
|
+
// Copies plugin files to the stable directory and prints that path so users
|
|
5134
|
+
// import the bootloader version (which auto-updates on every launch).
|
|
5021
5135
|
if (process.argv.includes("--print-path")) {
|
|
5022
5136
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
5023
|
-
const
|
|
5137
|
+
const sourceDir = resolve(packageRoot, "figma-desktop-bridge");
|
|
5138
|
+
// Ensure stable directory exists with latest files
|
|
5139
|
+
const stablePath = setupStablePluginDir(sourceDir);
|
|
5140
|
+
if (stablePath && existsSync(stablePath)) {
|
|
5141
|
+
console.log(stablePath);
|
|
5142
|
+
console.error("\nImport this manifest in Figma once — the bootloader will\n" +
|
|
5143
|
+
"automatically load the latest UI from the MCP server.\n" +
|
|
5144
|
+
"You won't need to re-import when the server updates.");
|
|
5145
|
+
process.exit(0);
|
|
5146
|
+
}
|
|
5147
|
+
// Fallback to npm package path if stable dir setup failed
|
|
5148
|
+
const manifestPath = resolve(sourceDir, "manifest.json");
|
|
5024
5149
|
if (existsSync(manifestPath)) {
|
|
5025
5150
|
console.log(manifestPath);
|
|
5026
5151
|
process.exit(0);
|