@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/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
- * Works for both NPX installs (buried in npm cache) and local git clones.
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. Useful for adding shapes, text, or frames to existing structures.", {
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 any stale port files from crashed instances before trying to bind
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 manifestPath = resolve(packageRoot, "figma-desktop-bridge", "manifest.json");
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);