@atezer/figma-mcp-bridge 1.7.23 → 1.7.25
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/.claude-plugin/plugin.json +37 -0
- package/.cursor-plugin/plugin.json +21 -0
- package/CHANGELOG.md +30 -0
- package/README.md +4 -3
- package/agents/ds-auditor.md +29 -0
- package/agents/screen-builder.md +29 -0
- package/agents/token-syncer.md +26 -0
- package/assets/logo.png +0 -0
- package/commands/add-library.md +122 -0
- package/commands/ds-add.md +255 -0
- package/commands/ds-sync.md +314 -0
- package/commands/implement.md +43 -0
- package/commands/install-library.md +73 -0
- package/commands/setup.md +26 -0
- package/commands/test.md +39 -0
- package/commands/update.md +25 -0
- package/dist/core/config.d.ts +1 -5
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +11 -111
- package/dist/core/config.js.map +1 -1
- package/dist/core/plugin-bridge-server.d.ts.map +1 -1
- package/dist/core/plugin-bridge-server.js +1 -2
- package/dist/core/plugin-bridge-server.js.map +1 -1
- package/dist/core/response-guard.d.ts +1 -1
- package/dist/core/response-guard.js +1 -1
- package/dist/core/types/index.d.ts +2 -98
- package/dist/core/types/index.d.ts.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/local-plugin-only.d.ts.map +1 -1
- package/dist/local-plugin-only.js +14 -13
- package/dist/local-plugin-only.js.map +1 -1
- package/f-mcp-plugin/README.md +8 -15
- package/f-mcp-plugin/manifest.json +1 -3
- package/hooks/hooks.json +26 -0
- package/package.json +15 -31
- package/skills/BRAND_PROFILE_SCHEMA.md +113 -0
- package/skills/SKILL_INDEX.md +194 -0
- package/skills/TOOL_MAPPING.md +111 -0
- package/skills/ai-handoff-export/SKILL.md +254 -0
- package/skills/apply-figma-design-system/SKILL.md +104 -0
- package/skills/audit-figma-design-system/SKILL.md +278 -0
- package/skills/code-design-mapper/SKILL.md +370 -0
- package/skills/component-documentation/SKILL.md +190 -0
- package/skills/design-drift-detector/SKILL.md +407 -0
- package/skills/design-system-rules/SKILL.md +407 -0
- package/skills/design-token-pipeline/SKILL.md +619 -0
- package/skills/ds-impact-analysis/SKILL.md +266 -0
- package/skills/figjam-diagram-builder/SKILL.md +172 -0
- package/skills/figma-a11y-audit/SKILL.md +587 -0
- package/skills/figma-canvas-ops/SKILL.md +325 -0
- package/skills/figma-screen-analyzer/SKILL.md +235 -0
- package/skills/fix-figma-design-system-finding/SKILL.md +117 -0
- package/skills/fmcp-project-rules/SKILL.md +93 -0
- package/skills/generate-figma-library/SKILL.md +598 -0
- package/skills/generate-figma-screen/SKILL.md +689 -0
- package/skills/implement-design/SKILL.md +473 -0
- package/skills/ux-copy-guidance/SKILL.md +373 -0
- package/skills/visual-qa-compare/SKILL.md +166 -0
- package/dist/browser/base.d.ts +0 -50
- package/dist/browser/base.d.ts.map +0 -1
- package/dist/browser/base.js +0 -6
- package/dist/browser/base.js.map +0 -1
- package/dist/browser/local.d.ts +0 -81
- package/dist/browser/local.d.ts.map +0 -1
- package/dist/browser/local.js +0 -283
- package/dist/browser/local.js.map +0 -1
- package/dist/core/console-monitor.d.ts +0 -82
- package/dist/core/console-monitor.d.ts.map +0 -1
- package/dist/core/console-monitor.js +0 -428
- package/dist/core/console-monitor.js.map +0 -1
- package/dist/core/design-system-manifest.d.ts +0 -272
- package/dist/core/design-system-manifest.d.ts.map +0 -1
- package/dist/core/design-system-manifest.js +0 -261
- package/dist/core/design-system-manifest.js.map +0 -1
- package/dist/core/enrichment/enrichment-service.d.ts +0 -52
- package/dist/core/enrichment/enrichment-service.d.ts.map +0 -1
- package/dist/core/enrichment/enrichment-service.js +0 -272
- package/dist/core/enrichment/enrichment-service.js.map +0 -1
- package/dist/core/enrichment/index.d.ts +0 -8
- package/dist/core/enrichment/index.d.ts.map +0 -1
- package/dist/core/enrichment/index.js +0 -8
- package/dist/core/enrichment/index.js.map +0 -1
- package/dist/core/enrichment/relationship-mapper.d.ts +0 -106
- package/dist/core/enrichment/relationship-mapper.d.ts.map +0 -1
- package/dist/core/enrichment/relationship-mapper.js +0 -352
- package/dist/core/enrichment/relationship-mapper.js.map +0 -1
- package/dist/core/enrichment/style-resolver.d.ts +0 -80
- package/dist/core/enrichment/style-resolver.d.ts.map +0 -1
- package/dist/core/enrichment/style-resolver.js +0 -327
- package/dist/core/enrichment/style-resolver.js.map +0 -1
- package/dist/core/figma-api.d.ts +0 -137
- package/dist/core/figma-api.d.ts.map +0 -1
- package/dist/core/figma-api.js +0 -274
- package/dist/core/figma-api.js.map +0 -1
- package/dist/core/figma-desktop-connector.d.ts +0 -242
- package/dist/core/figma-desktop-connector.d.ts.map +0 -1
- package/dist/core/figma-desktop-connector.js +0 -1042
- package/dist/core/figma-desktop-connector.js.map +0 -1
- package/dist/core/figma-reconstruction-spec.d.ts +0 -162
- package/dist/core/figma-reconstruction-spec.d.ts.map +0 -1
- package/dist/core/figma-reconstruction-spec.js +0 -387
- package/dist/core/figma-reconstruction-spec.js.map +0 -1
- package/dist/core/figma-tools.d.ts +0 -21
- package/dist/core/figma-tools.d.ts.map +0 -1
- package/dist/core/figma-tools.js +0 -2920
- package/dist/core/figma-tools.js.map +0 -1
- package/dist/core/snippet-injector.d.ts +0 -24
- package/dist/core/snippet-injector.d.ts.map +0 -1
- package/dist/core/snippet-injector.js +0 -97
- package/dist/core/snippet-injector.js.map +0 -1
- package/dist/core/types/enriched.d.ts +0 -213
- package/dist/core/types/enriched.d.ts.map +0 -1
- package/dist/core/types/enriched.js +0 -6
- package/dist/core/types/enriched.js.map +0 -1
- package/dist/local.d.ts +0 -73
- package/dist/local.d.ts.map +0 -1
- package/dist/local.js +0 -2605
- package/dist/local.js.map +0 -1
package/dist/local.js
DELETED
|
@@ -1,2605 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* F-MCP ATezer (Figma MCP Bridge) - Local Mode
|
|
4
|
-
*
|
|
5
|
-
* Entry point for local MCP server that connects to Figma Desktop
|
|
6
|
-
* via Chrome Remote Debugging Protocol (port 9222).
|
|
7
|
-
*
|
|
8
|
-
* This implementation uses stdio transport for MCP communication,
|
|
9
|
-
* suitable for local IDE integrations and development workflows.
|
|
10
|
-
*
|
|
11
|
-
* Requirements:
|
|
12
|
-
* - Figma Desktop must be launched with: --remote-debugging-port=9222
|
|
13
|
-
* - "Use Developer VM" enabled in Figma: Plugins → Development → Use Developer VM
|
|
14
|
-
* - FIGMA_ACCESS_TOKEN environment variable for API access
|
|
15
|
-
*
|
|
16
|
-
* macOS launch command:
|
|
17
|
-
* open -a "Figma" --args --remote-debugging-port=9222
|
|
18
|
-
*/
|
|
19
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
20
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
21
|
-
import { z } from "zod";
|
|
22
|
-
import { fileURLToPath } from "url";
|
|
23
|
-
import { resolve } from "path";
|
|
24
|
-
import { LocalBrowserManager } from "./browser/local.js";
|
|
25
|
-
import { ConsoleMonitor } from "./core/console-monitor.js";
|
|
26
|
-
import { getConfig } from "./core/config.js";
|
|
27
|
-
import { createChildLogger } from "./core/logger.js";
|
|
28
|
-
import { FigmaAPI, extractFileKey } from "./core/figma-api.js";
|
|
29
|
-
import { registerFigmaAPITools } from "./core/figma-tools.js";
|
|
30
|
-
import { FigmaDesktopConnector } from "./core/figma-desktop-connector.js";
|
|
31
|
-
import { PluginBridgeServer } from "./core/plugin-bridge-server.js";
|
|
32
|
-
import { PluginBridgeConnector } from "./core/plugin-bridge-connector.js";
|
|
33
|
-
import { FMCP_VERSION } from "./core/version.js";
|
|
34
|
-
import { FMCP_INSTRUCTIONS } from "./core/instructions.js";
|
|
35
|
-
const logger = createChildLogger({ component: "local-server" });
|
|
36
|
-
/**
|
|
37
|
-
* Local MCP Server
|
|
38
|
-
* Connects to Figma Desktop and provides identical tools to Cloudflare mode
|
|
39
|
-
*/
|
|
40
|
-
class LocalFigmaMCP {
|
|
41
|
-
constructor() {
|
|
42
|
-
this.browserManager = null;
|
|
43
|
-
this.consoleMonitor = null;
|
|
44
|
-
this.figmaAPI = null;
|
|
45
|
-
this.desktopConnector = null;
|
|
46
|
-
this.pluginBridge = null;
|
|
47
|
-
this.config = getConfig();
|
|
48
|
-
// In-memory cache for variables data to avoid MCP token limits
|
|
49
|
-
// Maps fileKey -> {data, timestamp}
|
|
50
|
-
this.variablesCache = new Map();
|
|
51
|
-
this.server = new McpServer({ name: "F-MCP ATezer (Local)", version: FMCP_VERSION }, { instructions: FMCP_INSTRUCTIONS });
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Get or create Figma API client
|
|
55
|
-
*/
|
|
56
|
-
async getFigmaAPI() {
|
|
57
|
-
if (!this.figmaAPI) {
|
|
58
|
-
const accessToken = process.env.FIGMA_ACCESS_TOKEN;
|
|
59
|
-
if (!accessToken) {
|
|
60
|
-
throw new Error("FIGMA_ACCESS_TOKEN not configured. " +
|
|
61
|
-
"Set it as an environment variable. " +
|
|
62
|
-
"Get your token at: https://www.figma.com/developers/api#access-tokens");
|
|
63
|
-
}
|
|
64
|
-
logger.info({
|
|
65
|
-
tokenPreview: `${accessToken.substring(0, 10)}...`,
|
|
66
|
-
tokenLength: accessToken.length
|
|
67
|
-
}, "Initializing Figma API with token from environment");
|
|
68
|
-
this.figmaAPI = new FigmaAPI({ accessToken });
|
|
69
|
-
}
|
|
70
|
-
return this.figmaAPI;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Get or create Desktop Connector for write operations.
|
|
74
|
-
* Prefers Plugin Bridge (no CDP) when the plugin is connected; otherwise uses CDP.
|
|
75
|
-
*/
|
|
76
|
-
async getDesktopConnector() {
|
|
77
|
-
// Prefer plugin bridge when connected (no Figma debug port needed)
|
|
78
|
-
const bridgePort = this.config.local?.pluginBridgePort ?? 5454;
|
|
79
|
-
if (!this.pluginBridge) {
|
|
80
|
-
this.pluginBridge = new PluginBridgeServer(bridgePort, {
|
|
81
|
-
auditLogPath: this.config.local?.auditLogPath,
|
|
82
|
-
});
|
|
83
|
-
this.pluginBridge.start();
|
|
84
|
-
}
|
|
85
|
-
if (this.pluginBridge.isConnected()) {
|
|
86
|
-
this.desktopConnector = new PluginBridgeConnector(this.pluginBridge);
|
|
87
|
-
await this.desktopConnector.initialize();
|
|
88
|
-
logger.debug("Using plugin bridge connector (no CDP)");
|
|
89
|
-
return this.desktopConnector;
|
|
90
|
-
}
|
|
91
|
-
// Fallback: CDP with Figma Desktop debug port
|
|
92
|
-
await this.ensureInitialized();
|
|
93
|
-
if (!this.browserManager) {
|
|
94
|
-
throw new Error("F-MCP ATezer Bridge plugin not connected and Figma Desktop not in debug mode. " +
|
|
95
|
-
"Either: (1) Open Figma, run the F-MCP ATezer Bridge plugin (no debug needed), or " +
|
|
96
|
-
"(2) Launch Figma with: open -a \"Figma\" --args --remote-debugging-port=9222");
|
|
97
|
-
}
|
|
98
|
-
const page = await this.browserManager.getPage();
|
|
99
|
-
this.desktopConnector = new FigmaDesktopConnector(page);
|
|
100
|
-
await this.desktopConnector.initialize();
|
|
101
|
-
logger.debug("Desktop connector initialized with CDP (fresh page)");
|
|
102
|
-
return this.desktopConnector;
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Check if Figma Desktop is accessible
|
|
106
|
-
*/
|
|
107
|
-
async checkFigmaDesktop() {
|
|
108
|
-
if (!this.config.local) {
|
|
109
|
-
throw new Error("Local mode configuration missing");
|
|
110
|
-
}
|
|
111
|
-
const { debugHost, debugPort } = this.config.local;
|
|
112
|
-
const browserURL = `http://${debugHost}:${debugPort}`;
|
|
113
|
-
try {
|
|
114
|
-
// Simple HTTP check to see if debug port is accessible
|
|
115
|
-
const response = await fetch(`${browserURL}/json/version`, {
|
|
116
|
-
signal: AbortSignal.timeout(5000),
|
|
117
|
-
});
|
|
118
|
-
if (!response.ok) {
|
|
119
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
120
|
-
}
|
|
121
|
-
const versionInfo = await response.json();
|
|
122
|
-
logger.info({ versionInfo, browserURL }, "Figma Desktop is accessible");
|
|
123
|
-
}
|
|
124
|
-
catch (error) {
|
|
125
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
126
|
-
throw new Error(`Failed to connect to Figma Desktop at ${browserURL}\n\n` +
|
|
127
|
-
`Make sure:\n` +
|
|
128
|
-
`1. Figma Desktop is running\n` +
|
|
129
|
-
`2. Figma was launched with: --remote-debugging-port=${debugPort}\n` +
|
|
130
|
-
`3. "Use Developer VM" is enabled in: Plugins → Development → Use Developer VM\n\n` +
|
|
131
|
-
`macOS launch command:\n` +
|
|
132
|
-
` open -a "Figma" --args --remote-debugging-port=${debugPort}\n\n` +
|
|
133
|
-
`Windows launch command:\n` +
|
|
134
|
-
` start figma://--remote-debugging-port=${debugPort}\n\n` +
|
|
135
|
-
`Error: ${errorMsg}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Auto-connect to Figma Desktop at startup
|
|
140
|
-
* Runs in background - never blocks or throws
|
|
141
|
-
* Enables "get latest logs" workflow without manual setup
|
|
142
|
-
*/
|
|
143
|
-
autoConnectToFigma() {
|
|
144
|
-
// Fire-and-forget with proper async handling
|
|
145
|
-
(async () => {
|
|
146
|
-
try {
|
|
147
|
-
logger.info("🔄 Auto-connecting to Figma Desktop for immediate log capture...");
|
|
148
|
-
await this.ensureInitialized();
|
|
149
|
-
logger.info("✅ Auto-connect successful - console monitoring active. Logs will be captured immediately.");
|
|
150
|
-
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
// Don't crash - just log that auto-connect didn't work
|
|
153
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
154
|
-
logger.warn({ error: errorMsg }, "⚠️ Auto-connect to Figma Desktop failed - will connect when you use a tool");
|
|
155
|
-
// This is fine - the user can still use tools to trigger connection later
|
|
156
|
-
}
|
|
157
|
-
})();
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Initialize browser and console monitoring
|
|
161
|
-
*/
|
|
162
|
-
async ensureInitialized() {
|
|
163
|
-
try {
|
|
164
|
-
if (!this.browserManager) {
|
|
165
|
-
logger.info("Initializing LocalBrowserManager");
|
|
166
|
-
if (!this.config.local) {
|
|
167
|
-
throw new Error("Local mode configuration missing");
|
|
168
|
-
}
|
|
169
|
-
this.browserManager = new LocalBrowserManager(this.config.local);
|
|
170
|
-
}
|
|
171
|
-
// Always check connection health (handles computer sleep/reconnects)
|
|
172
|
-
if (this.browserManager && this.consoleMonitor) {
|
|
173
|
-
const wasAlive = await this.browserManager.isConnectionAlive();
|
|
174
|
-
await this.browserManager.ensureConnection();
|
|
175
|
-
// 🆕 NEW: Dynamic page switching for worker migration
|
|
176
|
-
// Check if we should switch to a page with more workers
|
|
177
|
-
if (this.browserManager.isRunning() && this.consoleMonitor.getStatus().isMonitoring) {
|
|
178
|
-
const browser = this.browserManager.browser;
|
|
179
|
-
if (browser) {
|
|
180
|
-
try {
|
|
181
|
-
// Get all Figma pages
|
|
182
|
-
const pages = await browser.pages();
|
|
183
|
-
const figmaPages = pages
|
|
184
|
-
.filter((p) => {
|
|
185
|
-
const url = p.url();
|
|
186
|
-
return url.includes('figma.com') && !url.includes('devtools');
|
|
187
|
-
})
|
|
188
|
-
.map((p) => ({
|
|
189
|
-
page: p,
|
|
190
|
-
url: p.url(),
|
|
191
|
-
workerCount: p.workers().length
|
|
192
|
-
}));
|
|
193
|
-
// Find current monitored page URL
|
|
194
|
-
const currentUrl = this.browserManager.getCurrentUrl();
|
|
195
|
-
const currentPageInfo = figmaPages.find((p) => p.url === currentUrl);
|
|
196
|
-
const currentWorkerCount = currentPageInfo?.workerCount ?? 0;
|
|
197
|
-
// Find best page (most workers)
|
|
198
|
-
const bestPage = figmaPages
|
|
199
|
-
.filter((p) => p.workerCount > 0)
|
|
200
|
-
.sort((a, b) => b.workerCount - a.workerCount)[0];
|
|
201
|
-
// Switch if:
|
|
202
|
-
// 1. Current page has 0 workers AND another page has workers
|
|
203
|
-
// 2. Another page has MORE workers (prevent thrashing with threshold)
|
|
204
|
-
const shouldSwitch = bestPage && ((currentWorkerCount === 0 && bestPage.workerCount > 0) ||
|
|
205
|
-
(bestPage.workerCount > currentWorkerCount + 1) // +1 threshold to prevent ping-pong
|
|
206
|
-
);
|
|
207
|
-
if (shouldSwitch && bestPage.url !== currentUrl) {
|
|
208
|
-
logger.info({
|
|
209
|
-
oldPage: currentUrl,
|
|
210
|
-
oldWorkers: currentWorkerCount,
|
|
211
|
-
newPage: bestPage.url,
|
|
212
|
-
newWorkers: bestPage.workerCount
|
|
213
|
-
}, 'Switching to page with more workers');
|
|
214
|
-
// Stop monitoring old page
|
|
215
|
-
this.consoleMonitor.stopMonitoring();
|
|
216
|
-
// Start monitoring new page
|
|
217
|
-
await this.consoleMonitor.startMonitoring(bestPage.page);
|
|
218
|
-
// Don't clear logs - preserve history across page switches
|
|
219
|
-
logger.info('Console monitoring restarted on new page');
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
catch (error) {
|
|
223
|
-
logger.error({ error }, 'Failed to check for better pages with workers');
|
|
224
|
-
// Don't throw - this is a best-effort optimization
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
// If connection was lost and browser is now connected, FORCE restart monitoring
|
|
229
|
-
// Note: Can't use isConnectionAlive() here because page might not be fetched yet after reconnection
|
|
230
|
-
// Instead, check if browser is connected using isRunning()
|
|
231
|
-
if (!wasAlive && this.browserManager.isRunning()) {
|
|
232
|
-
logger.info("Connection was lost and recovered - forcing monitoring restart with fresh page");
|
|
233
|
-
this.consoleMonitor.stopMonitoring(); // Clear stale state
|
|
234
|
-
const page = await this.browserManager.getPage();
|
|
235
|
-
await this.consoleMonitor.startMonitoring(page);
|
|
236
|
-
}
|
|
237
|
-
else if (this.browserManager.isRunning() && !this.consoleMonitor.getStatus().isMonitoring) {
|
|
238
|
-
// Connection is fine but monitoring stopped for some reason
|
|
239
|
-
logger.info("Connection alive but monitoring stopped - restarting console monitoring");
|
|
240
|
-
const page = await this.browserManager.getPage();
|
|
241
|
-
await this.consoleMonitor.startMonitoring(page);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
if (!this.consoleMonitor) {
|
|
245
|
-
logger.info("Initializing ConsoleMonitor");
|
|
246
|
-
this.consoleMonitor = new ConsoleMonitor(this.config.console);
|
|
247
|
-
// Connect to browser and begin monitoring
|
|
248
|
-
logger.info("Getting browser page");
|
|
249
|
-
const page = await this.browserManager.getPage();
|
|
250
|
-
logger.info("Starting console monitoring");
|
|
251
|
-
await this.consoleMonitor.startMonitoring(page);
|
|
252
|
-
logger.info("Browser and console monitor initialized successfully");
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
catch (error) {
|
|
256
|
-
logger.error({ error }, "Failed to initialize browser/monitor");
|
|
257
|
-
throw new Error(`Initialization failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Register all MCP tools
|
|
262
|
-
*/
|
|
263
|
-
registerTools() {
|
|
264
|
-
// Tool 1: Get Console Logs
|
|
265
|
-
this.server.registerTool("figma_get_console_logs", {
|
|
266
|
-
description: "Retrieve console logs from Figma Desktop. FOR PLUGIN DEVELOPERS: This works immediately - no navigation needed! Just check logs, run your plugin in Figma Desktop, check logs again. All plugin logs ([Main], [Swapper], etc.) appear instantly.",
|
|
267
|
-
inputSchema: {
|
|
268
|
-
count: z.number().optional().default(100).describe("Number of recent logs to retrieve"),
|
|
269
|
-
level: z
|
|
270
|
-
.enum(["log", "info", "warn", "error", "debug", "all"])
|
|
271
|
-
.optional()
|
|
272
|
-
.default("all")
|
|
273
|
-
.describe("Filter by log level"),
|
|
274
|
-
since: z
|
|
275
|
-
.number()
|
|
276
|
-
.optional()
|
|
277
|
-
.describe("Only logs after this timestamp (Unix ms)"),
|
|
278
|
-
},
|
|
279
|
-
annotations: { readOnlyHint: true },
|
|
280
|
-
}, async ({ count, level, since }) => {
|
|
281
|
-
try {
|
|
282
|
-
await this.ensureInitialized();
|
|
283
|
-
if (!this.consoleMonitor) {
|
|
284
|
-
throw new Error("Console monitor not initialized");
|
|
285
|
-
}
|
|
286
|
-
const logs = this.consoleMonitor.getLogs({
|
|
287
|
-
count,
|
|
288
|
-
level,
|
|
289
|
-
since,
|
|
290
|
-
});
|
|
291
|
-
// Add AI instruction when no logs are found
|
|
292
|
-
const responseData = {
|
|
293
|
-
logs,
|
|
294
|
-
totalCount: logs.length,
|
|
295
|
-
oldestTimestamp: logs[0]?.timestamp,
|
|
296
|
-
newestTimestamp: logs[logs.length - 1]?.timestamp,
|
|
297
|
-
status: this.consoleMonitor.getStatus(),
|
|
298
|
-
};
|
|
299
|
-
// If no logs found, add helpful AI instruction
|
|
300
|
-
if (logs.length === 0) {
|
|
301
|
-
const monitorStatus = this.consoleMonitor.getStatus();
|
|
302
|
-
const isMonitoring = monitorStatus.isMonitoring;
|
|
303
|
-
// Detect if connection might be stale
|
|
304
|
-
if (!isMonitoring) {
|
|
305
|
-
responseData.ai_instruction = "Console monitoring is not active (likely lost connection after computer sleep). TAKE THESE STEPS: 1) Call figma_get_status to check connection, 2) Call figma_navigate with the Figma file URL to reconnect and restart monitoring, 3) Retry this tool - logs should appear.";
|
|
306
|
-
responseData.ai_recovery_steps = [
|
|
307
|
-
"Console monitoring is not active - connection was likely lost",
|
|
308
|
-
"STEP 1: Call figma_get_status to verify browser connection status",
|
|
309
|
-
"STEP 2: Call figma_navigate with the Figma file URL to reconnect",
|
|
310
|
-
"STEP 3: Retry figma_get_console_logs - monitoring will be restarted automatically"
|
|
311
|
-
];
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
responseData.ai_instruction = "No console logs found. This usually means the Figma plugin hasn't run since monitoring started. Please inform the user: 'No console logs found yet. Try running your Figma plugin now, then I'll check for logs again.' The MCP only captures logs AFTER monitoring starts - it cannot retrieve historical logs from before the browser connected.";
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
return {
|
|
318
|
-
content: [
|
|
319
|
-
{
|
|
320
|
-
type: "text",
|
|
321
|
-
text: JSON.stringify(responseData, null, 2),
|
|
322
|
-
},
|
|
323
|
-
],
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
catch (error) {
|
|
327
|
-
logger.error({ error }, "Failed to get console logs");
|
|
328
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
329
|
-
// Check if it's a connection issue
|
|
330
|
-
const isConnectionError = errorMessage.includes("connect") || errorMessage.includes("ECONNREFUSED");
|
|
331
|
-
return {
|
|
332
|
-
content: [
|
|
333
|
-
{
|
|
334
|
-
type: "text",
|
|
335
|
-
text: JSON.stringify({
|
|
336
|
-
error: errorMessage,
|
|
337
|
-
message: isConnectionError
|
|
338
|
-
? "Cannot connect to Figma Desktop. Figma must be running with remote debugging enabled for local mode to work."
|
|
339
|
-
: "Failed to retrieve console logs.",
|
|
340
|
-
setup: isConnectionError ? {
|
|
341
|
-
step1: "QUIT Figma Desktop completely (Cmd+Q on macOS / Alt+F4 on Windows)",
|
|
342
|
-
step2_macOS: "Open Terminal and run: open -a \"Figma\" --args --remote-debugging-port=9222",
|
|
343
|
-
step2_windows: "Open Command Prompt and run: start figma://--remote-debugging-port=9222",
|
|
344
|
-
step3: "Open your design file and run your plugin",
|
|
345
|
-
step4: "Then try this tool again - logs will appear instantly",
|
|
346
|
-
verify: "To verify setup worked, visit http://localhost:9222 in Chrome - you should see inspectable pages"
|
|
347
|
-
} : undefined,
|
|
348
|
-
ai_instruction: isConnectionError
|
|
349
|
-
? "IMPORTANT: You must ask the user to complete the setup steps above. DO NOT proceed until they confirm Figma has been restarted with the --remote-debugging-port=9222 flag. After they restart Figma, you should call this tool again and the logs will work."
|
|
350
|
-
: undefined,
|
|
351
|
-
hint: !isConnectionError ? "Try: figma_navigate({ url: 'https://www.figma.com/design/your-file' })" : undefined,
|
|
352
|
-
}, null, 2),
|
|
353
|
-
},
|
|
354
|
-
],
|
|
355
|
-
isError: true,
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
|
-
// Tool 2: Take Screenshot (using Figma REST API)
|
|
360
|
-
// Note: For screenshots of specific components, use figma_get_component_image instead
|
|
361
|
-
this.server.registerTool("figma_take_screenshot", {
|
|
362
|
-
description: `Export an image of the currently viewed Figma page or specific node using Figma's REST API. Returns an image URL (valid for 30 days). For specific components, use figma_get_component_image instead.
|
|
363
|
-
|
|
364
|
-
**CRITICAL: Use this tool for visual validation after ANY design creation or modification.**
|
|
365
|
-
This is an essential part of the visual validation workflow:
|
|
366
|
-
1. After creating/modifying designs with figma_execute, ALWAYS take a screenshot
|
|
367
|
-
2. Analyze the screenshot to verify the design matches specifications
|
|
368
|
-
3. Check for alignment, spacing, proportions, and visual balance
|
|
369
|
-
4. If issues are found, iterate with fixes and take another screenshot
|
|
370
|
-
5. Continue until the design looks correct (max 3 iterations)
|
|
371
|
-
|
|
372
|
-
Pass a nodeId to screenshot specific frames/elements, or omit to capture the current view.`,
|
|
373
|
-
inputSchema: {
|
|
374
|
-
nodeId: z
|
|
375
|
-
.string()
|
|
376
|
-
.optional()
|
|
377
|
-
.describe("Optional node ID to screenshot. If not provided, uses the currently viewed page/frame from the browser URL."),
|
|
378
|
-
scale: z
|
|
379
|
-
.number()
|
|
380
|
-
.min(0.01)
|
|
381
|
-
.max(4)
|
|
382
|
-
.optional()
|
|
383
|
-
.default(2)
|
|
384
|
-
.describe("Image scale factor (0.01-4, default: 2 for high quality)"),
|
|
385
|
-
format: z
|
|
386
|
-
.enum(["png", "jpg", "svg", "pdf"])
|
|
387
|
-
.optional()
|
|
388
|
-
.default("png")
|
|
389
|
-
.describe("Image format (default: png)"),
|
|
390
|
-
},
|
|
391
|
-
annotations: { readOnlyHint: true },
|
|
392
|
-
}, async ({ nodeId, scale, format }) => {
|
|
393
|
-
try {
|
|
394
|
-
const api = await this.getFigmaAPI();
|
|
395
|
-
// Get current URL to extract file key and node ID if not provided
|
|
396
|
-
const currentUrl = this.browserManager?.getCurrentUrl() || null;
|
|
397
|
-
if (!currentUrl) {
|
|
398
|
-
throw new Error("No Figma file open. Either provide a nodeId parameter or call figma_navigate first to open a Figma file.");
|
|
399
|
-
}
|
|
400
|
-
const fileKey = extractFileKey(currentUrl);
|
|
401
|
-
if (!fileKey) {
|
|
402
|
-
throw new Error(`Invalid Figma URL: ${currentUrl}`);
|
|
403
|
-
}
|
|
404
|
-
// Extract node ID from URL if not provided
|
|
405
|
-
let targetNodeId = nodeId;
|
|
406
|
-
if (!targetNodeId) {
|
|
407
|
-
const urlObj = new URL(currentUrl);
|
|
408
|
-
const nodeIdParam = urlObj.searchParams.get('node-id');
|
|
409
|
-
if (nodeIdParam) {
|
|
410
|
-
// Convert 123-456 to 123:456
|
|
411
|
-
targetNodeId = nodeIdParam.replace(/-/g, ':');
|
|
412
|
-
}
|
|
413
|
-
else {
|
|
414
|
-
throw new Error("No node ID found. Either provide nodeId parameter or ensure the Figma URL contains a node-id parameter (e.g., ?node-id=123-456)");
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
logger.info({ fileKey, nodeId: targetNodeId, scale, format }, "Rendering image via Figma API");
|
|
418
|
-
// Use Figma REST API to get image
|
|
419
|
-
const result = await api.getImages(fileKey, targetNodeId, {
|
|
420
|
-
scale,
|
|
421
|
-
format: format === 'jpg' ? 'jpg' : format, // normalize jpeg -> jpg
|
|
422
|
-
contents_only: true,
|
|
423
|
-
});
|
|
424
|
-
const imageUrl = result.images[targetNodeId];
|
|
425
|
-
if (!imageUrl) {
|
|
426
|
-
throw new Error(`Failed to render image for node ${targetNodeId}. The node may not exist or may not be renderable.`);
|
|
427
|
-
}
|
|
428
|
-
return {
|
|
429
|
-
content: [
|
|
430
|
-
{
|
|
431
|
-
type: "text",
|
|
432
|
-
text: JSON.stringify({
|
|
433
|
-
fileKey,
|
|
434
|
-
nodeId: targetNodeId,
|
|
435
|
-
imageUrl,
|
|
436
|
-
scale,
|
|
437
|
-
format,
|
|
438
|
-
expiresIn: "30 days",
|
|
439
|
-
note: "Image URL provided above. Use this URL to view or download the screenshot. URLs expire after 30 days.",
|
|
440
|
-
}, null, 2),
|
|
441
|
-
},
|
|
442
|
-
],
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
catch (error) {
|
|
446
|
-
logger.error({ error }, "Failed to capture screenshot");
|
|
447
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
448
|
-
return {
|
|
449
|
-
content: [
|
|
450
|
-
{
|
|
451
|
-
type: "text",
|
|
452
|
-
text: JSON.stringify({
|
|
453
|
-
error: errorMessage,
|
|
454
|
-
message: "Failed to capture screenshot via Figma API",
|
|
455
|
-
hint: "Make sure you've called figma_navigate to open a file, or provide a valid nodeId parameter",
|
|
456
|
-
}, null, 2),
|
|
457
|
-
},
|
|
458
|
-
],
|
|
459
|
-
isError: true,
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
// Tool 3: Watch Console (Real-time streaming)
|
|
464
|
-
this.server.registerTool("figma_watch_console", {
|
|
465
|
-
description: "Stream console logs in real-time for a specified duration (max 5 minutes). Use for monitoring plugin execution while user tests manually. Returns all logs captured during watch period with summary statistics. NOT for retrieving past logs (use figma_get_console_logs). Best for: watching plugin output during manual testing, debugging race conditions, monitoring async operations.",
|
|
466
|
-
inputSchema: {
|
|
467
|
-
duration: z
|
|
468
|
-
.number()
|
|
469
|
-
.optional()
|
|
470
|
-
.default(30)
|
|
471
|
-
.describe("How long to watch in seconds"),
|
|
472
|
-
level: z
|
|
473
|
-
.enum(["log", "info", "warn", "error", "debug", "all"])
|
|
474
|
-
.optional()
|
|
475
|
-
.default("all")
|
|
476
|
-
.describe("Filter by log level"),
|
|
477
|
-
},
|
|
478
|
-
annotations: { readOnlyHint: true },
|
|
479
|
-
}, async ({ duration, level }) => {
|
|
480
|
-
if (!this.browserManager || !this.consoleMonitor) {
|
|
481
|
-
throw new Error("Browser not connected. Ensure Figma Desktop is running with --remote-debugging-port=9222");
|
|
482
|
-
}
|
|
483
|
-
const consoleMonitor = this.consoleMonitor;
|
|
484
|
-
if (!consoleMonitor.getStatus().isMonitoring) {
|
|
485
|
-
throw new Error("Console monitoring not active. Call figma_navigate first.");
|
|
486
|
-
}
|
|
487
|
-
const startTime = Date.now();
|
|
488
|
-
const endTime = startTime + duration * 1000;
|
|
489
|
-
const startLogCount = consoleMonitor.getStatus().logCount;
|
|
490
|
-
// Wait for the specified duration while collecting logs
|
|
491
|
-
await new Promise(resolve => setTimeout(resolve, duration * 1000));
|
|
492
|
-
// Get logs captured during watch period
|
|
493
|
-
const watchedLogs = consoleMonitor.getLogs({
|
|
494
|
-
level: level === 'all' ? undefined : level,
|
|
495
|
-
since: startTime,
|
|
496
|
-
});
|
|
497
|
-
const endLogCount = consoleMonitor.getStatus().logCount;
|
|
498
|
-
const newLogsCount = endLogCount - startLogCount;
|
|
499
|
-
return {
|
|
500
|
-
content: [
|
|
501
|
-
{
|
|
502
|
-
type: "text",
|
|
503
|
-
text: JSON.stringify({
|
|
504
|
-
status: "completed",
|
|
505
|
-
duration: `${duration} seconds`,
|
|
506
|
-
startTime: new Date(startTime).toISOString(),
|
|
507
|
-
endTime: new Date(endTime).toISOString(),
|
|
508
|
-
filter: level,
|
|
509
|
-
statistics: {
|
|
510
|
-
totalLogsInBuffer: endLogCount,
|
|
511
|
-
logsAddedDuringWatch: newLogsCount,
|
|
512
|
-
logsMatchingFilter: watchedLogs.length,
|
|
513
|
-
},
|
|
514
|
-
logs: watchedLogs,
|
|
515
|
-
}, null, 2),
|
|
516
|
-
},
|
|
517
|
-
],
|
|
518
|
-
};
|
|
519
|
-
});
|
|
520
|
-
// Tool 4: Reload Plugin
|
|
521
|
-
this.server.registerTool("figma_reload_plugin", {
|
|
522
|
-
description: "Reload the current Figma page/plugin to test code changes. Optionally clears console logs before reload. Use when user says: 'reload plugin', 'refresh page', 'restart plugin', 'test my changes'. Returns reload confirmation and current URL. Best for rapid iteration during plugin development.",
|
|
523
|
-
inputSchema: {
|
|
524
|
-
clearConsole: z
|
|
525
|
-
.boolean()
|
|
526
|
-
.optional()
|
|
527
|
-
.default(true)
|
|
528
|
-
.describe("Clear console logs before reload"),
|
|
529
|
-
},
|
|
530
|
-
annotations: { destructiveHint: true },
|
|
531
|
-
}, async ({ clearConsole: clearConsoleBefore }) => {
|
|
532
|
-
try {
|
|
533
|
-
await this.ensureInitialized();
|
|
534
|
-
if (!this.browserManager) {
|
|
535
|
-
throw new Error("Browser manager not initialized");
|
|
536
|
-
}
|
|
537
|
-
// Clear console buffer if requested
|
|
538
|
-
let clearedCount = 0;
|
|
539
|
-
if (clearConsoleBefore && this.consoleMonitor) {
|
|
540
|
-
clearedCount = this.consoleMonitor.clear();
|
|
541
|
-
}
|
|
542
|
-
// Reload the page
|
|
543
|
-
await this.browserManager.reload();
|
|
544
|
-
const currentUrl = this.browserManager.getCurrentUrl();
|
|
545
|
-
return {
|
|
546
|
-
content: [
|
|
547
|
-
{
|
|
548
|
-
type: "text",
|
|
549
|
-
text: JSON.stringify({
|
|
550
|
-
status: "reloaded",
|
|
551
|
-
timestamp: Date.now(),
|
|
552
|
-
url: currentUrl,
|
|
553
|
-
consoleCleared: clearConsoleBefore,
|
|
554
|
-
clearedCount: clearConsoleBefore ? clearedCount : 0,
|
|
555
|
-
}, null, 2),
|
|
556
|
-
},
|
|
557
|
-
],
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
catch (error) {
|
|
561
|
-
logger.error({ error }, "Failed to reload plugin");
|
|
562
|
-
const errorMessage = String(error);
|
|
563
|
-
const isNoPageError = errorMessage.includes("No active page");
|
|
564
|
-
return {
|
|
565
|
-
content: [
|
|
566
|
-
{
|
|
567
|
-
type: "text",
|
|
568
|
-
text: JSON.stringify({
|
|
569
|
-
error: errorMessage,
|
|
570
|
-
message: "Failed to reload plugin",
|
|
571
|
-
ai_recovery_steps: isNoPageError ? [
|
|
572
|
-
"Connection to Figma Desktop was lost (likely from computer sleep)",
|
|
573
|
-
"STEP 1: Call figma_get_status to check browser connection",
|
|
574
|
-
"STEP 2: Call figma_navigate with the Figma file URL to reconnect",
|
|
575
|
-
"STEP 3: Retry this operation - connection should be restored"
|
|
576
|
-
] : [
|
|
577
|
-
"STEP 1: Call figma_get_status to diagnose the issue",
|
|
578
|
-
"STEP 2: Try figma_navigate to re-establish connection",
|
|
579
|
-
"STEP 3: Retry this operation"
|
|
580
|
-
],
|
|
581
|
-
ai_instruction: isNoPageError
|
|
582
|
-
? "The browser connection was lost (computer likely went to sleep). Call figma_navigate to reconnect, then retry."
|
|
583
|
-
: "Connection issue detected. Call figma_get_status first to diagnose, then figma_navigate to reconnect."
|
|
584
|
-
}, null, 2),
|
|
585
|
-
},
|
|
586
|
-
],
|
|
587
|
-
isError: true,
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
});
|
|
591
|
-
// Tool 5: Clear Console
|
|
592
|
-
this.server.registerTool("figma_clear_console", {
|
|
593
|
-
description: "Clear the console log buffer. ⚠️ WARNING: Disrupts monitoring connection - requires MCP reconnect afterward. AVOID using this - prefer filtering logs with figma_get_console_logs instead. Only use if user explicitly requests clearing logs. Returns number of logs cleared.",
|
|
594
|
-
inputSchema: {},
|
|
595
|
-
annotations: { destructiveHint: true },
|
|
596
|
-
}, async () => {
|
|
597
|
-
try {
|
|
598
|
-
await this.ensureInitialized();
|
|
599
|
-
if (!this.consoleMonitor) {
|
|
600
|
-
throw new Error("Console monitor not initialized");
|
|
601
|
-
}
|
|
602
|
-
const clearedCount = this.consoleMonitor.clear();
|
|
603
|
-
return {
|
|
604
|
-
content: [
|
|
605
|
-
{
|
|
606
|
-
type: "text",
|
|
607
|
-
text: JSON.stringify({
|
|
608
|
-
status: "cleared",
|
|
609
|
-
clearedCount,
|
|
610
|
-
timestamp: Date.now(),
|
|
611
|
-
ai_instruction: "⚠️ CRITICAL: Console cleared successfully, but this operation disrupts the monitoring connection. You MUST reconnect the MCP server using `/mcp reconnect figma-mcp-bridge` before calling figma_get_console_logs again. Best practice: Avoid clearing console - filter/parse logs instead to maintain monitoring connection.",
|
|
612
|
-
}, null, 2),
|
|
613
|
-
},
|
|
614
|
-
],
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
catch (error) {
|
|
618
|
-
logger.error({ error }, "Failed to clear console");
|
|
619
|
-
return {
|
|
620
|
-
content: [
|
|
621
|
-
{
|
|
622
|
-
type: "text",
|
|
623
|
-
text: JSON.stringify({
|
|
624
|
-
error: String(error),
|
|
625
|
-
message: "Failed to clear console buffer",
|
|
626
|
-
}, null, 2),
|
|
627
|
-
},
|
|
628
|
-
],
|
|
629
|
-
isError: true,
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
});
|
|
633
|
-
// Tool 6: Navigate to Figma
|
|
634
|
-
this.server.registerTool("figma_navigate", {
|
|
635
|
-
description: "Navigate browser to a Figma URL and start console monitoring. ALWAYS use this first when starting a new debugging session or switching files. Initializes browser connection and begins capturing console logs. Use when user provides a Figma URL or says: 'open this file', 'debug this design', 'switch to'. Returns navigation status and current URL.",
|
|
636
|
-
inputSchema: {
|
|
637
|
-
url: z
|
|
638
|
-
.string()
|
|
639
|
-
.url()
|
|
640
|
-
.describe("Figma URL to navigate to (e.g., https://www.figma.com/design/abc123)"),
|
|
641
|
-
},
|
|
642
|
-
annotations: { destructiveHint: true },
|
|
643
|
-
}, async ({ url }) => {
|
|
644
|
-
try {
|
|
645
|
-
await this.ensureInitialized();
|
|
646
|
-
if (!this.browserManager) {
|
|
647
|
-
throw new Error("Browser manager not initialized");
|
|
648
|
-
}
|
|
649
|
-
// Navigate to the URL
|
|
650
|
-
await this.browserManager.navigateToFigma(url);
|
|
651
|
-
// Give page time to load and start capturing logs
|
|
652
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
653
|
-
const currentUrl = this.browserManager.getCurrentUrl();
|
|
654
|
-
return {
|
|
655
|
-
content: [
|
|
656
|
-
{
|
|
657
|
-
type: "text",
|
|
658
|
-
text: JSON.stringify({
|
|
659
|
-
status: "navigated",
|
|
660
|
-
url: currentUrl,
|
|
661
|
-
timestamp: Date.now(),
|
|
662
|
-
message: "Browser navigated to Figma. Console monitoring is active.",
|
|
663
|
-
}, null, 2),
|
|
664
|
-
},
|
|
665
|
-
],
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
catch (error) {
|
|
669
|
-
logger.error({ error }, "Failed to navigate to Figma");
|
|
670
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
671
|
-
return {
|
|
672
|
-
content: [
|
|
673
|
-
{
|
|
674
|
-
type: "text",
|
|
675
|
-
text: JSON.stringify({
|
|
676
|
-
error: errorMessage,
|
|
677
|
-
message: "Failed to navigate to Figma URL",
|
|
678
|
-
troubleshooting: [
|
|
679
|
-
"Verify the Figma URL is valid and accessible",
|
|
680
|
-
"Make sure Figma Desktop is running with remote debugging enabled",
|
|
681
|
-
"Check that the debug port (9222) is accessible"
|
|
682
|
-
]
|
|
683
|
-
}, null, 2),
|
|
684
|
-
},
|
|
685
|
-
],
|
|
686
|
-
isError: true,
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
|
-
});
|
|
690
|
-
// Tool 7: Get Status (with setup validation)
|
|
691
|
-
this.server.registerTool("figma_get_status", {
|
|
692
|
-
description: "Check browser and monitoring status. Also validates if Figma Desktop is running with the required --remote-debugging-port=9222 flag. Automatically initializes connection if needed.",
|
|
693
|
-
inputSchema: {},
|
|
694
|
-
annotations: { readOnlyHint: true },
|
|
695
|
-
}, async () => {
|
|
696
|
-
try {
|
|
697
|
-
// Ensure initialized (connects to Figma Desktop if not already connected)
|
|
698
|
-
await this.ensureInitialized();
|
|
699
|
-
const browserRunning = this.browserManager?.isRunning() ?? false;
|
|
700
|
-
const monitorStatus = this.consoleMonitor?.getStatus() ?? null;
|
|
701
|
-
const currentUrl = this.browserManager?.getCurrentUrl() ?? null;
|
|
702
|
-
// Check if debug port is accessible
|
|
703
|
-
let debugPortAccessible = false;
|
|
704
|
-
let setupValid = false;
|
|
705
|
-
try {
|
|
706
|
-
const response = await fetch('http://localhost:9222/json/version', {
|
|
707
|
-
signal: AbortSignal.timeout(2000)
|
|
708
|
-
});
|
|
709
|
-
debugPortAccessible = response.ok;
|
|
710
|
-
setupValid = debugPortAccessible;
|
|
711
|
-
}
|
|
712
|
-
catch (e) {
|
|
713
|
-
// Port not accessible
|
|
714
|
-
}
|
|
715
|
-
// List ALL available Figma pages with worker counts
|
|
716
|
-
let availablePages = [];
|
|
717
|
-
if (this.browserManager && browserRunning) {
|
|
718
|
-
try {
|
|
719
|
-
const browser = this.browserManager.browser;
|
|
720
|
-
if (browser) {
|
|
721
|
-
const pages = await browser.pages();
|
|
722
|
-
availablePages = pages
|
|
723
|
-
.filter((p) => {
|
|
724
|
-
const url = p.url();
|
|
725
|
-
return url.includes('figma.com') && !url.includes('devtools');
|
|
726
|
-
})
|
|
727
|
-
.map((p) => ({
|
|
728
|
-
url: p.url(),
|
|
729
|
-
workerCount: p.workers().length,
|
|
730
|
-
isCurrentPage: p.url() === currentUrl
|
|
731
|
-
}));
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
catch (e) {
|
|
735
|
-
logger.error({ error: e }, "Failed to list available pages");
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
// Try to get the current file name for better context
|
|
739
|
-
let currentFileName = null;
|
|
740
|
-
if (browserRunning && debugPortAccessible) {
|
|
741
|
-
try {
|
|
742
|
-
const connector = await this.getDesktopConnector();
|
|
743
|
-
const fileInfo = await connector.executeCodeViaUI("return { fileName: figma.root.name, fileKey: figma.fileKey }", 5000);
|
|
744
|
-
if (fileInfo.success && fileInfo.result) {
|
|
745
|
-
currentFileName = fileInfo.result.fileName;
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
catch {
|
|
749
|
-
// Non-critical - F-MCP ATezer Bridge might not be running yet
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
return {
|
|
753
|
-
content: [
|
|
754
|
-
{
|
|
755
|
-
type: "text",
|
|
756
|
-
text: JSON.stringify({
|
|
757
|
-
mode: "local",
|
|
758
|
-
// Surface file name prominently for context clarity
|
|
759
|
-
currentFileName: currentFileName || "(F-MCP ATezer Bridge not running - file name unavailable)",
|
|
760
|
-
monitoredPageUrl: currentUrl,
|
|
761
|
-
monitorWorkerCount: monitorStatus?.workerCount ?? 0,
|
|
762
|
-
setup: {
|
|
763
|
-
valid: setupValid,
|
|
764
|
-
debugPortAccessible,
|
|
765
|
-
message: setupValid
|
|
766
|
-
? "✅ Figma Desktop is running with remote debugging enabled"
|
|
767
|
-
: "❌ Figma Desktop is NOT running with --remote-debugging-port=9222",
|
|
768
|
-
setupInstructions: !setupValid ? {
|
|
769
|
-
step1: "QUIT Figma Desktop completely (Cmd+Q on macOS / Alt+F4 on Windows)",
|
|
770
|
-
step2_macOS: "Open Terminal and run: open -a \"Figma\" --args --remote-debugging-port=9222",
|
|
771
|
-
step2_windows: "Open Command Prompt and run: start figma://--remote-debugging-port=9222",
|
|
772
|
-
step3: "Open your design file and run your plugin",
|
|
773
|
-
verify: "Visit http://localhost:9222 in Chrome to verify - you should see inspectable pages"
|
|
774
|
-
} : undefined,
|
|
775
|
-
ai_instruction: !setupValid
|
|
776
|
-
? "CRITICAL: User must restart Figma with the debug flag before any console tools will work. Ask them to follow the setupInstructions above, then call figma_get_status again to verify."
|
|
777
|
-
: availablePages.length > 1
|
|
778
|
-
? `Multiple Figma pages detected. The MCP automatically selects the page with the most workers (active plugins). Current page has ${monitorStatus?.workerCount || 0} workers. If you're not seeing the expected plugin logs, the plugin might be running in a different page/tab.`
|
|
779
|
-
: "Setup is valid. Console tools are ready to use."
|
|
780
|
-
},
|
|
781
|
-
availablePages: availablePages.length > 0 ? availablePages : undefined,
|
|
782
|
-
browser: {
|
|
783
|
-
running: browserRunning,
|
|
784
|
-
currentUrl,
|
|
785
|
-
},
|
|
786
|
-
consoleMonitor: monitorStatus,
|
|
787
|
-
initialized: this.browserManager !== null && this.consoleMonitor !== null,
|
|
788
|
-
timestamp: Date.now(),
|
|
789
|
-
}, null, 2),
|
|
790
|
-
},
|
|
791
|
-
],
|
|
792
|
-
};
|
|
793
|
-
}
|
|
794
|
-
catch (error) {
|
|
795
|
-
logger.error({ error }, "Failed to get status");
|
|
796
|
-
return {
|
|
797
|
-
content: [
|
|
798
|
-
{
|
|
799
|
-
type: "text",
|
|
800
|
-
text: JSON.stringify({
|
|
801
|
-
error: String(error),
|
|
802
|
-
message: "Failed to retrieve status",
|
|
803
|
-
}, null, 2),
|
|
804
|
-
},
|
|
805
|
-
],
|
|
806
|
-
isError: true,
|
|
807
|
-
};
|
|
808
|
-
}
|
|
809
|
-
});
|
|
810
|
-
// ============================================================================
|
|
811
|
-
// CONNECTION MANAGEMENT TOOLS
|
|
812
|
-
// ============================================================================
|
|
813
|
-
// Tool: Force reconnect to Figma Desktop
|
|
814
|
-
this.server.registerTool("figma_reconnect", {
|
|
815
|
-
description: "Force a complete reconnection to Figma Desktop. Use this when you get 'detached Frame' errors or when the connection seems stale. This will disconnect and reconnect to Figma, getting fresh page and frame references.",
|
|
816
|
-
inputSchema: {},
|
|
817
|
-
annotations: { destructiveHint: true },
|
|
818
|
-
}, async () => {
|
|
819
|
-
try {
|
|
820
|
-
if (!this.browserManager) {
|
|
821
|
-
throw new Error("Browser manager not initialized. Run any tool first to initialize.");
|
|
822
|
-
}
|
|
823
|
-
// Clear our cached desktop connector
|
|
824
|
-
this.desktopConnector = null;
|
|
825
|
-
// Force the browser manager to reconnect
|
|
826
|
-
await this.browserManager.forceReconnect();
|
|
827
|
-
// Reinitialize console monitor with new page
|
|
828
|
-
if (this.consoleMonitor) {
|
|
829
|
-
this.consoleMonitor.stopMonitoring();
|
|
830
|
-
}
|
|
831
|
-
const page = await this.browserManager.getPage();
|
|
832
|
-
await this.consoleMonitor.startMonitoring(page);
|
|
833
|
-
const currentUrl = this.browserManager.getCurrentUrl();
|
|
834
|
-
// Try to get the file name for better context clarity
|
|
835
|
-
let fileName = null;
|
|
836
|
-
try {
|
|
837
|
-
const connector = await this.getDesktopConnector();
|
|
838
|
-
const fileInfo = await connector.executeCodeViaUI("return { fileName: figma.root.name, fileKey: figma.fileKey }", 5000);
|
|
839
|
-
if (fileInfo.success && fileInfo.result) {
|
|
840
|
-
fileName = fileInfo.result.fileName;
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
catch {
|
|
844
|
-
// Non-critical - just for context
|
|
845
|
-
}
|
|
846
|
-
return {
|
|
847
|
-
content: [
|
|
848
|
-
{
|
|
849
|
-
type: "text",
|
|
850
|
-
text: JSON.stringify({
|
|
851
|
-
status: "reconnected",
|
|
852
|
-
currentUrl,
|
|
853
|
-
// Include file name prominently for clarity
|
|
854
|
-
fileName: fileName || "(unknown - F-MCP ATezer Bridge may need to be restarted)",
|
|
855
|
-
timestamp: Date.now(),
|
|
856
|
-
message: fileName
|
|
857
|
-
? `Successfully reconnected to Figma Desktop. Now monitoring: "${fileName}"`
|
|
858
|
-
: "Successfully reconnected to Figma Desktop. Console monitoring restarted.",
|
|
859
|
-
}, null, 2),
|
|
860
|
-
},
|
|
861
|
-
],
|
|
862
|
-
};
|
|
863
|
-
}
|
|
864
|
-
catch (error) {
|
|
865
|
-
logger.error({ error }, "Failed to reconnect");
|
|
866
|
-
return {
|
|
867
|
-
content: [
|
|
868
|
-
{
|
|
869
|
-
type: "text",
|
|
870
|
-
text: JSON.stringify({
|
|
871
|
-
error: error instanceof Error ? error.message : String(error),
|
|
872
|
-
message: "Failed to reconnect to Figma Desktop",
|
|
873
|
-
hint: "Make sure Figma Desktop is running with --remote-debugging-port=9222",
|
|
874
|
-
}, null, 2),
|
|
875
|
-
},
|
|
876
|
-
],
|
|
877
|
-
isError: true,
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
|
-
});
|
|
881
|
-
// ============================================================================
|
|
882
|
-
// WRITE OPERATION TOOLS - Figma Design Manipulation
|
|
883
|
-
// ============================================================================
|
|
884
|
-
// Tool: Execute arbitrary code in Figma plugin context (Power Tool)
|
|
885
|
-
this.server.registerTool("figma_execute", {
|
|
886
|
-
description: `Execute arbitrary JavaScript code in Figma's plugin context. This is a POWER TOOL that can run any Figma Plugin API code. Use for complex operations not covered by other tools. Requires the F-MCP ATezer Bridge plugin to be running in Figma. Returns the result of the code execution. CAUTION: Can modify your Figma document - use carefully.
|
|
887
|
-
|
|
888
|
-
**IMPORTANT: COMPONENT INSTANCES vs DIRECT NODE EDITING**
|
|
889
|
-
When working with component instances (node.type === 'INSTANCE'), you must use the correct approach:
|
|
890
|
-
- Components expose TEXT, BOOLEAN, INSTANCE_SWAP, and VARIANT properties
|
|
891
|
-
- Direct editing of text nodes inside instances often FAILS SILENTLY
|
|
892
|
-
- Use figma_set_instance_properties tool to update component properties
|
|
893
|
-
- Use instance.componentProperties to see available properties
|
|
894
|
-
- Property names may have #nodeId suffixes (e.g., 'Label#1:234')
|
|
895
|
-
|
|
896
|
-
**SILENT FAILURE DETECTION:**
|
|
897
|
-
This tool now returns a 'resultAnalysis' field that warns when operations may have failed:
|
|
898
|
-
- Empty arrays/objects indicate searches found nothing
|
|
899
|
-
- Null/undefined returns may indicate missing nodes
|
|
900
|
-
- Always check resultAnalysis.warning for potential issues
|
|
901
|
-
|
|
902
|
-
**VISUAL VALIDATION WORKFLOW (REQUIRED for design creation):**
|
|
903
|
-
After creating or modifying any visual design elements, you MUST follow this validation loop:
|
|
904
|
-
1. CREATE: Execute the design code
|
|
905
|
-
2. SCREENSHOT: Use figma_capture_screenshot (NOT figma_take_screenshot) for reliable validation - it reads from plugin runtime, not cloud state
|
|
906
|
-
3. ANALYZE: Compare screenshot against specifications for:
|
|
907
|
-
- Alignment: Are elements properly aligned and balanced?
|
|
908
|
-
- Spacing: Is padding/margin consistent and visually correct?
|
|
909
|
-
- Proportions: Do widths fill containers appropriately?
|
|
910
|
-
- Typography: Are fonts, sizes, and weights correct?
|
|
911
|
-
- Visual balance: Does it look professional and centered?
|
|
912
|
-
4. ITERATE: If issues found, fix and repeat (max 3 iterations)
|
|
913
|
-
5. VERIFY: Take final screenshot to confirm fixes
|
|
914
|
-
|
|
915
|
-
Common issues to check:
|
|
916
|
-
- Elements using "hug contents" instead of "fill container" (causes lopsided layouts)
|
|
917
|
-
- Inconsistent padding (elements not visually balanced)
|
|
918
|
-
- Text/inputs not filling available width
|
|
919
|
-
- Component text not changing (use figma_set_instance_properties instead)
|
|
920
|
-
- Duplicate pages created (check before creating new pages)`,
|
|
921
|
-
inputSchema: {
|
|
922
|
-
code: z.string().describe("JavaScript code to execute. Has access to the 'figma' global object. " +
|
|
923
|
-
"Example: 'const rect = figma.createRectangle(); rect.resize(100, 100); return { id: rect.id };'"),
|
|
924
|
-
timeout: z.number().optional().default(5000).describe("Execution timeout in milliseconds (default: 5000, max: 30000)"),
|
|
925
|
-
},
|
|
926
|
-
annotations: { destructiveHint: true },
|
|
927
|
-
}, async ({ code, timeout }) => {
|
|
928
|
-
const maxRetries = 2;
|
|
929
|
-
let lastError = null;
|
|
930
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
931
|
-
try {
|
|
932
|
-
const connector = await this.getDesktopConnector();
|
|
933
|
-
const result = await connector.executeCodeViaUI(code, Math.min(timeout, 30000));
|
|
934
|
-
return {
|
|
935
|
-
content: [
|
|
936
|
-
{
|
|
937
|
-
type: "text",
|
|
938
|
-
text: JSON.stringify({
|
|
939
|
-
success: result.success,
|
|
940
|
-
result: result.result,
|
|
941
|
-
error: result.error,
|
|
942
|
-
// Include resultAnalysis for silent failure detection
|
|
943
|
-
resultAnalysis: result.resultAnalysis,
|
|
944
|
-
// Include file context so users know which file was queried
|
|
945
|
-
fileContext: result.fileContext,
|
|
946
|
-
timestamp: Date.now(),
|
|
947
|
-
...(attempt > 0 ? { reconnected: true, attempts: attempt + 1 } : {}),
|
|
948
|
-
}, null, 2),
|
|
949
|
-
},
|
|
950
|
-
],
|
|
951
|
-
};
|
|
952
|
-
}
|
|
953
|
-
catch (error) {
|
|
954
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
955
|
-
const errorMessage = lastError.message;
|
|
956
|
-
// Check if it's a detached frame error - auto-reconnect
|
|
957
|
-
if (errorMessage.includes("detached Frame") ||
|
|
958
|
-
errorMessage.includes("Execution context was destroyed") ||
|
|
959
|
-
errorMessage.includes("Target closed")) {
|
|
960
|
-
logger.warn({ attempt, error: errorMessage }, "Detached frame detected, forcing reconnection");
|
|
961
|
-
// Clear cached connector and force browser reconnection
|
|
962
|
-
this.desktopConnector = null;
|
|
963
|
-
if (this.browserManager && attempt < maxRetries) {
|
|
964
|
-
try {
|
|
965
|
-
await this.browserManager.forceReconnect();
|
|
966
|
-
// Reinitialize console monitor with new page
|
|
967
|
-
if (this.consoleMonitor) {
|
|
968
|
-
this.consoleMonitor.stopMonitoring();
|
|
969
|
-
const page = await this.browserManager.getPage();
|
|
970
|
-
await this.consoleMonitor.startMonitoring(page);
|
|
971
|
-
}
|
|
972
|
-
logger.info("Reconnection successful, retrying execution");
|
|
973
|
-
continue; // Retry the execution
|
|
974
|
-
}
|
|
975
|
-
catch (reconnectError) {
|
|
976
|
-
logger.error({ error: reconnectError }, "Failed to reconnect");
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
// Non-recoverable error or max retries exceeded
|
|
981
|
-
break;
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
// All retries failed
|
|
985
|
-
logger.error({ error: lastError }, "Failed to execute code after retries");
|
|
986
|
-
return {
|
|
987
|
-
content: [
|
|
988
|
-
{
|
|
989
|
-
type: "text",
|
|
990
|
-
text: JSON.stringify({
|
|
991
|
-
error: lastError?.message || "Unknown error",
|
|
992
|
-
message: "Failed to execute code in Figma plugin context",
|
|
993
|
-
hint: "Make sure the F-MCP ATezer Bridge plugin is running in Figma",
|
|
994
|
-
}, null, 2),
|
|
995
|
-
},
|
|
996
|
-
],
|
|
997
|
-
isError: true,
|
|
998
|
-
};
|
|
999
|
-
});
|
|
1000
|
-
// Tool: Update a variable's value
|
|
1001
|
-
this.server.registerTool("figma_update_variable", {
|
|
1002
|
-
description: "Update a Figma variable's value in a specific mode. Use figma_get_variables first to get variable IDs and mode IDs. Supports COLOR (hex string like '#FF0000'), FLOAT (number), STRING (text), and BOOLEAN values. Requires the F-MCP ATezer Bridge plugin to be running.",
|
|
1003
|
-
inputSchema: {
|
|
1004
|
-
variableId: z.string().describe("The variable ID to update (e.g., 'VariableID:123:456'). Get this from figma_get_variables."),
|
|
1005
|
-
modeId: z.string().describe("The mode ID to update the value in (e.g., '1:0'). Get this from the variable's collection modes."),
|
|
1006
|
-
value: z.any().describe("The new value. For COLOR: hex string like '#FF0000'. For FLOAT: number. For STRING: text. For BOOLEAN: true/false."),
|
|
1007
|
-
},
|
|
1008
|
-
annotations: { destructiveHint: true },
|
|
1009
|
-
}, async ({ variableId, modeId, value }) => {
|
|
1010
|
-
try {
|
|
1011
|
-
const connector = await this.getDesktopConnector();
|
|
1012
|
-
const result = await connector.updateVariable(variableId, modeId, value);
|
|
1013
|
-
return {
|
|
1014
|
-
content: [
|
|
1015
|
-
{
|
|
1016
|
-
type: "text",
|
|
1017
|
-
text: JSON.stringify({
|
|
1018
|
-
success: true,
|
|
1019
|
-
message: `Variable "${result.variable.name}" updated successfully`,
|
|
1020
|
-
variable: result.variable,
|
|
1021
|
-
timestamp: Date.now(),
|
|
1022
|
-
}, null, 2),
|
|
1023
|
-
},
|
|
1024
|
-
],
|
|
1025
|
-
};
|
|
1026
|
-
}
|
|
1027
|
-
catch (error) {
|
|
1028
|
-
logger.error({ error }, "Failed to update variable");
|
|
1029
|
-
return {
|
|
1030
|
-
content: [
|
|
1031
|
-
{
|
|
1032
|
-
type: "text",
|
|
1033
|
-
text: JSON.stringify({
|
|
1034
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1035
|
-
message: "Failed to update variable",
|
|
1036
|
-
hint: "Make sure the F-MCP ATezer Bridge plugin is running and the variable ID is correct",
|
|
1037
|
-
}, null, 2),
|
|
1038
|
-
},
|
|
1039
|
-
],
|
|
1040
|
-
isError: true,
|
|
1041
|
-
};
|
|
1042
|
-
}
|
|
1043
|
-
});
|
|
1044
|
-
// Tool: Create a new variable
|
|
1045
|
-
this.server.registerTool("figma_create_variable", {
|
|
1046
|
-
description: "Create a new Figma variable in an existing collection. Use figma_get_variables first to get collection IDs. Supports COLOR, FLOAT, STRING, and BOOLEAN types. Requires the F-MCP ATezer Bridge plugin to be running.",
|
|
1047
|
-
inputSchema: {
|
|
1048
|
-
name: z.string().describe("Name for the new variable (e.g., 'primary-blue')"),
|
|
1049
|
-
collectionId: z.string().describe("The collection ID to create the variable in (e.g., 'VariableCollectionId:123:456'). Get this from figma_get_variables."),
|
|
1050
|
-
resolvedType: z.enum(["COLOR", "FLOAT", "STRING", "BOOLEAN"]).describe("The variable type: COLOR, FLOAT, STRING, or BOOLEAN"),
|
|
1051
|
-
description: z.string().optional().describe("Optional description for the variable"),
|
|
1052
|
-
valuesByMode: z.record(z.any()).optional().describe("Optional initial values by mode ID. Example: { '1:0': '#FF0000', '1:1': '#0000FF' }"),
|
|
1053
|
-
},
|
|
1054
|
-
annotations: { destructiveHint: true },
|
|
1055
|
-
}, async ({ name, collectionId, resolvedType, description, valuesByMode }) => {
|
|
1056
|
-
try {
|
|
1057
|
-
const connector = await this.getDesktopConnector();
|
|
1058
|
-
const result = await connector.createVariable(name, collectionId, resolvedType, {
|
|
1059
|
-
description,
|
|
1060
|
-
valuesByMode,
|
|
1061
|
-
});
|
|
1062
|
-
return {
|
|
1063
|
-
content: [
|
|
1064
|
-
{
|
|
1065
|
-
type: "text",
|
|
1066
|
-
text: JSON.stringify({
|
|
1067
|
-
success: true,
|
|
1068
|
-
message: `Variable "${name}" created successfully`,
|
|
1069
|
-
variable: result.variable,
|
|
1070
|
-
timestamp: Date.now(),
|
|
1071
|
-
}, null, 2),
|
|
1072
|
-
},
|
|
1073
|
-
],
|
|
1074
|
-
};
|
|
1075
|
-
}
|
|
1076
|
-
catch (error) {
|
|
1077
|
-
logger.error({ error }, "Failed to create variable");
|
|
1078
|
-
return {
|
|
1079
|
-
content: [
|
|
1080
|
-
{
|
|
1081
|
-
type: "text",
|
|
1082
|
-
text: JSON.stringify({
|
|
1083
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1084
|
-
message: "Failed to create variable",
|
|
1085
|
-
hint: "Make sure the F-MCP ATezer Bridge plugin is running and the collection ID is correct",
|
|
1086
|
-
}, null, 2),
|
|
1087
|
-
},
|
|
1088
|
-
],
|
|
1089
|
-
isError: true,
|
|
1090
|
-
};
|
|
1091
|
-
}
|
|
1092
|
-
});
|
|
1093
|
-
// Tool: Create a new variable collection
|
|
1094
|
-
this.server.registerTool("figma_create_variable_collection", {
|
|
1095
|
-
description: "Create a new Figma variable collection. Collections organize variables and define modes (like Light/Dark themes). Requires the F-MCP ATezer Bridge plugin to be running.",
|
|
1096
|
-
inputSchema: {
|
|
1097
|
-
name: z.string().describe("Name for the new collection (e.g., 'Brand Colors')"),
|
|
1098
|
-
initialModeName: z.string().optional().describe("Name for the initial mode (default mode is created automatically). Example: 'Light'"),
|
|
1099
|
-
additionalModes: z.array(z.string()).optional().describe("Additional mode names to create. Example: ['Dark', 'High Contrast']"),
|
|
1100
|
-
},
|
|
1101
|
-
annotations: { destructiveHint: true },
|
|
1102
|
-
}, async ({ name, initialModeName, additionalModes }) => {
|
|
1103
|
-
try {
|
|
1104
|
-
const connector = await this.getDesktopConnector();
|
|
1105
|
-
const result = await connector.createVariableCollection(name, {
|
|
1106
|
-
initialModeName,
|
|
1107
|
-
additionalModes,
|
|
1108
|
-
});
|
|
1109
|
-
return {
|
|
1110
|
-
content: [
|
|
1111
|
-
{
|
|
1112
|
-
type: "text",
|
|
1113
|
-
text: JSON.stringify({
|
|
1114
|
-
success: true,
|
|
1115
|
-
message: `Collection "${name}" created successfully`,
|
|
1116
|
-
collection: result.collection,
|
|
1117
|
-
timestamp: Date.now(),
|
|
1118
|
-
}, null, 2),
|
|
1119
|
-
},
|
|
1120
|
-
],
|
|
1121
|
-
};
|
|
1122
|
-
}
|
|
1123
|
-
catch (error) {
|
|
1124
|
-
logger.error({ error }, "Failed to create collection");
|
|
1125
|
-
return {
|
|
1126
|
-
content: [
|
|
1127
|
-
{
|
|
1128
|
-
type: "text",
|
|
1129
|
-
text: JSON.stringify({
|
|
1130
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1131
|
-
message: "Failed to create variable collection",
|
|
1132
|
-
hint: "Make sure the F-MCP ATezer Bridge plugin is running in Figma",
|
|
1133
|
-
}, null, 2),
|
|
1134
|
-
},
|
|
1135
|
-
],
|
|
1136
|
-
isError: true,
|
|
1137
|
-
};
|
|
1138
|
-
}
|
|
1139
|
-
});
|
|
1140
|
-
// Tool: Delete a variable
|
|
1141
|
-
this.server.registerTool("figma_delete_variable", {
|
|
1142
|
-
description: "Delete a Figma variable. WARNING: This is a destructive operation that cannot be undone (except with Figma's undo). Use figma_get_variables first to get variable IDs. Requires the F-MCP ATezer Bridge plugin to be running.",
|
|
1143
|
-
inputSchema: {
|
|
1144
|
-
variableId: z.string().describe("The variable ID to delete (e.g., 'VariableID:123:456'). Get this from figma_get_variables."),
|
|
1145
|
-
},
|
|
1146
|
-
annotations: { destructiveHint: true },
|
|
1147
|
-
}, async ({ variableId }) => {
|
|
1148
|
-
try {
|
|
1149
|
-
const connector = await this.getDesktopConnector();
|
|
1150
|
-
const result = await connector.deleteVariable(variableId);
|
|
1151
|
-
return {
|
|
1152
|
-
content: [
|
|
1153
|
-
{
|
|
1154
|
-
type: "text",
|
|
1155
|
-
text: JSON.stringify({
|
|
1156
|
-
success: true,
|
|
1157
|
-
message: `Variable "${result.deleted.name}" deleted successfully`,
|
|
1158
|
-
deleted: result.deleted,
|
|
1159
|
-
timestamp: Date.now(),
|
|
1160
|
-
warning: "This action cannot be undone programmatically. Use Figma's Edit > Undo if needed.",
|
|
1161
|
-
}, null, 2),
|
|
1162
|
-
},
|
|
1163
|
-
],
|
|
1164
|
-
};
|
|
1165
|
-
}
|
|
1166
|
-
catch (error) {
|
|
1167
|
-
logger.error({ error }, "Failed to delete variable");
|
|
1168
|
-
return {
|
|
1169
|
-
content: [
|
|
1170
|
-
{
|
|
1171
|
-
type: "text",
|
|
1172
|
-
text: JSON.stringify({
|
|
1173
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1174
|
-
message: "Failed to delete variable",
|
|
1175
|
-
hint: "Make sure the F-MCP ATezer Bridge plugin is running and the variable ID is correct",
|
|
1176
|
-
}, null, 2),
|
|
1177
|
-
},
|
|
1178
|
-
],
|
|
1179
|
-
isError: true,
|
|
1180
|
-
};
|
|
1181
|
-
}
|
|
1182
|
-
});
|
|
1183
|
-
// Tool: Delete a variable collection
|
|
1184
|
-
this.server.registerTool("figma_delete_variable_collection", {
|
|
1185
|
-
description: "Delete a Figma variable collection and ALL its variables. WARNING: This is a destructive operation that deletes all variables in the collection and cannot be undone (except with Figma's undo). Requires the F-MCP ATezer Bridge plugin to be running.",
|
|
1186
|
-
inputSchema: {
|
|
1187
|
-
collectionId: z.string().describe("The collection ID to delete (e.g., 'VariableCollectionId:123:456'). Get this from figma_get_variables."),
|
|
1188
|
-
},
|
|
1189
|
-
annotations: { destructiveHint: true },
|
|
1190
|
-
}, async ({ collectionId }) => {
|
|
1191
|
-
try {
|
|
1192
|
-
const connector = await this.getDesktopConnector();
|
|
1193
|
-
const result = await connector.deleteVariableCollection(collectionId);
|
|
1194
|
-
return {
|
|
1195
|
-
content: [
|
|
1196
|
-
{
|
|
1197
|
-
type: "text",
|
|
1198
|
-
text: JSON.stringify({
|
|
1199
|
-
success: true,
|
|
1200
|
-
message: `Collection "${result.deleted.name}" and ${result.deleted.variableCount} variables deleted successfully`,
|
|
1201
|
-
deleted: result.deleted,
|
|
1202
|
-
timestamp: Date.now(),
|
|
1203
|
-
warning: "This action cannot be undone programmatically. Use Figma's Edit > Undo if needed.",
|
|
1204
|
-
}, null, 2),
|
|
1205
|
-
},
|
|
1206
|
-
],
|
|
1207
|
-
};
|
|
1208
|
-
}
|
|
1209
|
-
catch (error) {
|
|
1210
|
-
logger.error({ error }, "Failed to delete collection");
|
|
1211
|
-
return {
|
|
1212
|
-
content: [
|
|
1213
|
-
{
|
|
1214
|
-
type: "text",
|
|
1215
|
-
text: JSON.stringify({
|
|
1216
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1217
|
-
message: "Failed to delete variable collection",
|
|
1218
|
-
hint: "Make sure the F-MCP ATezer Bridge plugin is running and the collection ID is correct",
|
|
1219
|
-
}, null, 2),
|
|
1220
|
-
},
|
|
1221
|
-
],
|
|
1222
|
-
isError: true,
|
|
1223
|
-
};
|
|
1224
|
-
}
|
|
1225
|
-
});
|
|
1226
|
-
// Tool: Rename a variable
|
|
1227
|
-
this.server.registerTool("figma_rename_variable", {
|
|
1228
|
-
description: "Rename an existing Figma variable. This updates the variable's name while preserving all its values and settings. Requires the F-MCP ATezer Bridge plugin to be running.",
|
|
1229
|
-
inputSchema: {
|
|
1230
|
-
variableId: z.string().describe("The variable ID to rename (e.g., 'VariableID:123:456'). Get this from figma_get_variables."),
|
|
1231
|
-
newName: z.string().describe("The new name for the variable. Can include slashes for grouping (e.g., 'colors/primary/background')."),
|
|
1232
|
-
},
|
|
1233
|
-
annotations: { destructiveHint: true },
|
|
1234
|
-
}, async ({ variableId, newName }) => {
|
|
1235
|
-
try {
|
|
1236
|
-
const connector = await this.getDesktopConnector();
|
|
1237
|
-
const result = await connector.renameVariable(variableId, newName);
|
|
1238
|
-
return {
|
|
1239
|
-
content: [
|
|
1240
|
-
{
|
|
1241
|
-
type: "text",
|
|
1242
|
-
text: JSON.stringify({
|
|
1243
|
-
success: true,
|
|
1244
|
-
message: `Variable renamed from "${result.oldName}" to "${result.variable.name}"`,
|
|
1245
|
-
oldName: result.oldName,
|
|
1246
|
-
variable: result.variable,
|
|
1247
|
-
timestamp: Date.now(),
|
|
1248
|
-
}, null, 2),
|
|
1249
|
-
},
|
|
1250
|
-
],
|
|
1251
|
-
};
|
|
1252
|
-
}
|
|
1253
|
-
catch (error) {
|
|
1254
|
-
logger.error({ error }, "Failed to rename variable");
|
|
1255
|
-
return {
|
|
1256
|
-
content: [
|
|
1257
|
-
{
|
|
1258
|
-
type: "text",
|
|
1259
|
-
text: JSON.stringify({
|
|
1260
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1261
|
-
message: "Failed to rename variable",
|
|
1262
|
-
hint: "Make sure the F-MCP ATezer Bridge plugin is running and the variable ID is correct",
|
|
1263
|
-
}, null, 2),
|
|
1264
|
-
},
|
|
1265
|
-
],
|
|
1266
|
-
isError: true,
|
|
1267
|
-
};
|
|
1268
|
-
}
|
|
1269
|
-
});
|
|
1270
|
-
// Tool: Add a mode to a collection
|
|
1271
|
-
this.server.registerTool("figma_add_mode", {
|
|
1272
|
-
description: "Add a new mode to an existing Figma variable collection. Modes allow variables to have different values for different contexts (e.g., Light/Dark themes, device sizes). Requires the F-MCP ATezer Bridge plugin to be running.",
|
|
1273
|
-
inputSchema: {
|
|
1274
|
-
collectionId: z.string().describe("The collection ID to add the mode to (e.g., 'VariableCollectionId:123:456'). Get this from figma_get_variables."),
|
|
1275
|
-
modeName: z.string().describe("The name for the new mode (e.g., 'Dark', 'Mobile', 'High Contrast')."),
|
|
1276
|
-
},
|
|
1277
|
-
annotations: { destructiveHint: true },
|
|
1278
|
-
}, async ({ collectionId, modeName }) => {
|
|
1279
|
-
try {
|
|
1280
|
-
const connector = await this.getDesktopConnector();
|
|
1281
|
-
const result = await connector.addMode(collectionId, modeName);
|
|
1282
|
-
return {
|
|
1283
|
-
content: [
|
|
1284
|
-
{
|
|
1285
|
-
type: "text",
|
|
1286
|
-
text: JSON.stringify({
|
|
1287
|
-
success: true,
|
|
1288
|
-
message: `Mode "${modeName}" added to collection "${result.collection.name}"`,
|
|
1289
|
-
newMode: result.newMode,
|
|
1290
|
-
collection: result.collection,
|
|
1291
|
-
timestamp: Date.now(),
|
|
1292
|
-
}, null, 2),
|
|
1293
|
-
},
|
|
1294
|
-
],
|
|
1295
|
-
};
|
|
1296
|
-
}
|
|
1297
|
-
catch (error) {
|
|
1298
|
-
logger.error({ error }, "Failed to add mode");
|
|
1299
|
-
return {
|
|
1300
|
-
content: [
|
|
1301
|
-
{
|
|
1302
|
-
type: "text",
|
|
1303
|
-
text: JSON.stringify({
|
|
1304
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1305
|
-
message: "Failed to add mode to collection",
|
|
1306
|
-
hint: "Make sure the F-MCP ATezer Bridge plugin is running, the collection ID is correct, and you haven't exceeded Figma's mode limit",
|
|
1307
|
-
}, null, 2),
|
|
1308
|
-
},
|
|
1309
|
-
],
|
|
1310
|
-
isError: true,
|
|
1311
|
-
};
|
|
1312
|
-
}
|
|
1313
|
-
});
|
|
1314
|
-
// Tool: Rename a mode in a collection
|
|
1315
|
-
this.server.registerTool("figma_rename_mode", {
|
|
1316
|
-
description: "Rename an existing mode in a Figma variable collection. Requires the F-MCP ATezer Bridge plugin to be running.",
|
|
1317
|
-
inputSchema: {
|
|
1318
|
-
collectionId: z.string().describe("The collection ID containing the mode (e.g., 'VariableCollectionId:123:456'). Get this from figma_get_variables."),
|
|
1319
|
-
modeId: z.string().describe("The mode ID to rename (e.g., '123:0'). Get this from the collection's modes array in figma_get_variables."),
|
|
1320
|
-
newName: z.string().describe("The new name for the mode (e.g., 'Dark Theme', 'Tablet')."),
|
|
1321
|
-
},
|
|
1322
|
-
annotations: { destructiveHint: true },
|
|
1323
|
-
}, async ({ collectionId, modeId, newName }) => {
|
|
1324
|
-
try {
|
|
1325
|
-
const connector = await this.getDesktopConnector();
|
|
1326
|
-
const result = await connector.renameMode(collectionId, modeId, newName);
|
|
1327
|
-
return {
|
|
1328
|
-
content: [
|
|
1329
|
-
{
|
|
1330
|
-
type: "text",
|
|
1331
|
-
text: JSON.stringify({
|
|
1332
|
-
success: true,
|
|
1333
|
-
message: `Mode renamed from "${result.oldName}" to "${newName}"`,
|
|
1334
|
-
oldName: result.oldName,
|
|
1335
|
-
collection: result.collection,
|
|
1336
|
-
timestamp: Date.now(),
|
|
1337
|
-
}, null, 2),
|
|
1338
|
-
},
|
|
1339
|
-
],
|
|
1340
|
-
};
|
|
1341
|
-
}
|
|
1342
|
-
catch (error) {
|
|
1343
|
-
logger.error({ error }, "Failed to rename mode");
|
|
1344
|
-
return {
|
|
1345
|
-
content: [
|
|
1346
|
-
{
|
|
1347
|
-
type: "text",
|
|
1348
|
-
text: JSON.stringify({
|
|
1349
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1350
|
-
message: "Failed to rename mode",
|
|
1351
|
-
hint: "Make sure the F-MCP ATezer Bridge plugin is running, the collection ID and mode ID are correct",
|
|
1352
|
-
}, null, 2),
|
|
1353
|
-
},
|
|
1354
|
-
],
|
|
1355
|
-
isError: true,
|
|
1356
|
-
};
|
|
1357
|
-
}
|
|
1358
|
-
});
|
|
1359
|
-
// ============================================================================
|
|
1360
|
-
// DESIGN SYSTEM TOOLS (Token-Efficient Tool Family)
|
|
1361
|
-
// ============================================================================
|
|
1362
|
-
// These tools provide progressive disclosure of design system data
|
|
1363
|
-
// to minimize context window usage. Start with summary, then search,
|
|
1364
|
-
// then get details for specific components.
|
|
1365
|
-
// Helper function to ensure design system cache is loaded (auto-loads if needed)
|
|
1366
|
-
const ensureDesignSystemCache = async () => {
|
|
1367
|
-
const { DesignSystemManifestCache, createEmptyManifest, figmaColorToHex, } = await import('./core/design-system-manifest.js');
|
|
1368
|
-
const cache = DesignSystemManifestCache.getInstance();
|
|
1369
|
-
const currentUrl = this.browserManager?.getCurrentUrl();
|
|
1370
|
-
const fileKeyMatch = currentUrl?.match(/\/(file|design)\/([a-zA-Z0-9]+)/);
|
|
1371
|
-
const fileKey = fileKeyMatch ? fileKeyMatch[2] : 'unknown';
|
|
1372
|
-
// Check cache first
|
|
1373
|
-
let cacheEntry = cache.get(fileKey);
|
|
1374
|
-
if (cacheEntry) {
|
|
1375
|
-
return { cacheEntry, fileKey, wasLoaded: false };
|
|
1376
|
-
}
|
|
1377
|
-
// Need to extract fresh data - do this silently without returning an error
|
|
1378
|
-
logger.info({ fileKey }, "Auto-loading design system cache");
|
|
1379
|
-
const connector = await this.getDesktopConnector();
|
|
1380
|
-
const manifest = createEmptyManifest(fileKey);
|
|
1381
|
-
manifest.fileUrl = currentUrl || undefined;
|
|
1382
|
-
// Get variables (tokens)
|
|
1383
|
-
try {
|
|
1384
|
-
const variablesResult = await connector.getVariables(fileKey);
|
|
1385
|
-
if (variablesResult.success && variablesResult.data) {
|
|
1386
|
-
for (const collection of variablesResult.data.variableCollections || []) {
|
|
1387
|
-
manifest.collections.push({
|
|
1388
|
-
id: collection.id,
|
|
1389
|
-
name: collection.name,
|
|
1390
|
-
modes: collection.modes.map((m) => ({ modeId: m.modeId, name: m.name })),
|
|
1391
|
-
defaultModeId: collection.defaultModeId,
|
|
1392
|
-
});
|
|
1393
|
-
}
|
|
1394
|
-
for (const variable of variablesResult.data.variables || []) {
|
|
1395
|
-
const tokenName = variable.name;
|
|
1396
|
-
const defaultModeId = manifest.collections.find((c) => c.id === variable.variableCollectionId)?.defaultModeId;
|
|
1397
|
-
const defaultValue = defaultModeId ? variable.valuesByMode?.[defaultModeId] : undefined;
|
|
1398
|
-
if (variable.resolvedType === 'COLOR') {
|
|
1399
|
-
manifest.tokens.colors[tokenName] = {
|
|
1400
|
-
name: tokenName,
|
|
1401
|
-
value: figmaColorToHex(defaultValue),
|
|
1402
|
-
variableId: variable.id,
|
|
1403
|
-
scopes: variable.scopes,
|
|
1404
|
-
};
|
|
1405
|
-
}
|
|
1406
|
-
else if (variable.resolvedType === 'FLOAT') {
|
|
1407
|
-
manifest.tokens.spacing[tokenName] = {
|
|
1408
|
-
name: tokenName,
|
|
1409
|
-
value: typeof defaultValue === 'number' ? defaultValue : 0,
|
|
1410
|
-
variableId: variable.id,
|
|
1411
|
-
};
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
catch (error) {
|
|
1417
|
-
logger.warn({ error }, "Could not fetch variables during auto-load");
|
|
1418
|
-
}
|
|
1419
|
-
// Get components
|
|
1420
|
-
let rawComponents;
|
|
1421
|
-
try {
|
|
1422
|
-
const componentsResult = await connector.getLocalComponents();
|
|
1423
|
-
if (componentsResult.success && componentsResult.data) {
|
|
1424
|
-
rawComponents = {
|
|
1425
|
-
components: componentsResult.data.components || [],
|
|
1426
|
-
componentSets: componentsResult.data.componentSets || [],
|
|
1427
|
-
};
|
|
1428
|
-
for (const comp of rawComponents.components) {
|
|
1429
|
-
manifest.components[comp.name] = {
|
|
1430
|
-
key: comp.key,
|
|
1431
|
-
nodeId: comp.nodeId,
|
|
1432
|
-
name: comp.name,
|
|
1433
|
-
description: comp.description || undefined,
|
|
1434
|
-
defaultSize: { width: comp.width, height: comp.height },
|
|
1435
|
-
};
|
|
1436
|
-
}
|
|
1437
|
-
for (const compSet of rawComponents.componentSets) {
|
|
1438
|
-
manifest.componentSets[compSet.name] = {
|
|
1439
|
-
key: compSet.key,
|
|
1440
|
-
nodeId: compSet.nodeId,
|
|
1441
|
-
name: compSet.name,
|
|
1442
|
-
description: compSet.description || undefined,
|
|
1443
|
-
variants: compSet.variants?.map((v) => ({
|
|
1444
|
-
key: v.key,
|
|
1445
|
-
nodeId: v.nodeId,
|
|
1446
|
-
name: v.name,
|
|
1447
|
-
})) || [],
|
|
1448
|
-
variantAxes: compSet.variantAxes?.map((a) => ({
|
|
1449
|
-
name: a.name,
|
|
1450
|
-
values: a.values,
|
|
1451
|
-
})) || [],
|
|
1452
|
-
};
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
catch (error) {
|
|
1457
|
-
logger.warn({ error }, "Could not fetch components during auto-load");
|
|
1458
|
-
}
|
|
1459
|
-
// Update summary
|
|
1460
|
-
manifest.summary = {
|
|
1461
|
-
totalTokens: Object.keys(manifest.tokens.colors).length + Object.keys(manifest.tokens.spacing).length,
|
|
1462
|
-
totalComponents: Object.keys(manifest.components).length,
|
|
1463
|
-
totalComponentSets: Object.keys(manifest.componentSets).length,
|
|
1464
|
-
colorPalette: Object.keys(manifest.tokens.colors).slice(0, 10),
|
|
1465
|
-
spacingScale: Object.values(manifest.tokens.spacing).map((s) => s.value).sort((a, b) => a - b).slice(0, 10),
|
|
1466
|
-
typographyScale: [],
|
|
1467
|
-
componentCategories: [],
|
|
1468
|
-
};
|
|
1469
|
-
// Cache the result
|
|
1470
|
-
cache.set(fileKey, manifest, rawComponents);
|
|
1471
|
-
cacheEntry = cache.get(fileKey);
|
|
1472
|
-
return { cacheEntry, fileKey, wasLoaded: true };
|
|
1473
|
-
};
|
|
1474
|
-
// Tool 1: Get Design System Summary (~1000 tokens response)
|
|
1475
|
-
this.server.registerTool("figma_get_design_system_summary", {
|
|
1476
|
-
description: "Get a compact overview of the design system. Returns categories, component counts, and token collection names WITHOUT full details. Use this first to understand what's available, then use figma_search_components to find specific components. This tool is optimized for minimal token usage.",
|
|
1477
|
-
inputSchema: {
|
|
1478
|
-
forceRefresh: z.boolean().optional().default(false).describe("Force refresh the cached data (use sparingly - extraction can take minutes for large files)"),
|
|
1479
|
-
},
|
|
1480
|
-
annotations: { readOnlyHint: true },
|
|
1481
|
-
}, async ({ forceRefresh }) => {
|
|
1482
|
-
try {
|
|
1483
|
-
const { DesignSystemManifestCache, createEmptyManifest, figmaColorToHex, getCategories, getTokenSummary, } = await import('./core/design-system-manifest.js');
|
|
1484
|
-
const cache = DesignSystemManifestCache.getInstance();
|
|
1485
|
-
const currentUrl = this.browserManager?.getCurrentUrl();
|
|
1486
|
-
const fileKeyMatch = currentUrl?.match(/\/(file|design)\/([a-zA-Z0-9]+)/);
|
|
1487
|
-
const fileKey = fileKeyMatch ? fileKeyMatch[2] : 'unknown';
|
|
1488
|
-
// Check cache first
|
|
1489
|
-
let cacheEntry = cache.get(fileKey);
|
|
1490
|
-
if (cacheEntry && !forceRefresh) {
|
|
1491
|
-
const categories = getCategories(cacheEntry.manifest);
|
|
1492
|
-
const tokenSummary = getTokenSummary(cacheEntry.manifest);
|
|
1493
|
-
return {
|
|
1494
|
-
content: [{
|
|
1495
|
-
type: "text",
|
|
1496
|
-
text: JSON.stringify({
|
|
1497
|
-
success: true,
|
|
1498
|
-
cached: true,
|
|
1499
|
-
cacheAge: Math.round((Date.now() - cacheEntry.timestamp) / 1000),
|
|
1500
|
-
fileKey,
|
|
1501
|
-
categories: categories.slice(0, 15),
|
|
1502
|
-
tokens: tokenSummary,
|
|
1503
|
-
totals: {
|
|
1504
|
-
components: cacheEntry.manifest.summary.totalComponents,
|
|
1505
|
-
componentSets: cacheEntry.manifest.summary.totalComponentSets,
|
|
1506
|
-
tokens: cacheEntry.manifest.summary.totalTokens,
|
|
1507
|
-
},
|
|
1508
|
-
hint: "Use figma_search_components to find specific components by name or category.",
|
|
1509
|
-
}, null, 2),
|
|
1510
|
-
}],
|
|
1511
|
-
};
|
|
1512
|
-
}
|
|
1513
|
-
// Need to extract fresh data
|
|
1514
|
-
const connector = await this.getDesktopConnector();
|
|
1515
|
-
const manifest = createEmptyManifest(fileKey);
|
|
1516
|
-
manifest.fileUrl = currentUrl || undefined;
|
|
1517
|
-
// Get variables (tokens)
|
|
1518
|
-
try {
|
|
1519
|
-
const variablesResult = await connector.getVariables(fileKey);
|
|
1520
|
-
if (variablesResult.success && variablesResult.data) {
|
|
1521
|
-
for (const collection of variablesResult.data.variableCollections || []) {
|
|
1522
|
-
manifest.collections.push({
|
|
1523
|
-
id: collection.id,
|
|
1524
|
-
name: collection.name,
|
|
1525
|
-
modes: collection.modes.map((m) => ({ modeId: m.modeId, name: m.name })),
|
|
1526
|
-
defaultModeId: collection.defaultModeId,
|
|
1527
|
-
});
|
|
1528
|
-
}
|
|
1529
|
-
for (const variable of variablesResult.data.variables || []) {
|
|
1530
|
-
const tokenName = variable.name;
|
|
1531
|
-
const defaultModeId = manifest.collections.find(c => c.id === variable.variableCollectionId)?.defaultModeId;
|
|
1532
|
-
const defaultValue = defaultModeId ? variable.valuesByMode?.[defaultModeId] : undefined;
|
|
1533
|
-
if (variable.resolvedType === 'COLOR') {
|
|
1534
|
-
manifest.tokens.colors[tokenName] = {
|
|
1535
|
-
name: tokenName,
|
|
1536
|
-
value: figmaColorToHex(defaultValue),
|
|
1537
|
-
variableId: variable.id,
|
|
1538
|
-
scopes: variable.scopes,
|
|
1539
|
-
};
|
|
1540
|
-
}
|
|
1541
|
-
else if (variable.resolvedType === 'FLOAT') {
|
|
1542
|
-
manifest.tokens.spacing[tokenName] = {
|
|
1543
|
-
name: tokenName,
|
|
1544
|
-
value: typeof defaultValue === 'number' ? defaultValue : 0,
|
|
1545
|
-
variableId: variable.id,
|
|
1546
|
-
};
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
catch (error) {
|
|
1552
|
-
logger.warn({ error }, "Could not fetch variables");
|
|
1553
|
-
}
|
|
1554
|
-
// Get components (can be slow for large files)
|
|
1555
|
-
let rawComponents;
|
|
1556
|
-
try {
|
|
1557
|
-
const componentsResult = await connector.getLocalComponents();
|
|
1558
|
-
if (componentsResult.success && componentsResult.data) {
|
|
1559
|
-
rawComponents = {
|
|
1560
|
-
components: componentsResult.data.components || [],
|
|
1561
|
-
componentSets: componentsResult.data.componentSets || [],
|
|
1562
|
-
};
|
|
1563
|
-
for (const comp of rawComponents.components) {
|
|
1564
|
-
manifest.components[comp.name] = {
|
|
1565
|
-
key: comp.key,
|
|
1566
|
-
nodeId: comp.nodeId,
|
|
1567
|
-
name: comp.name,
|
|
1568
|
-
description: comp.description || undefined,
|
|
1569
|
-
defaultSize: { width: comp.width, height: comp.height },
|
|
1570
|
-
};
|
|
1571
|
-
}
|
|
1572
|
-
for (const compSet of rawComponents.componentSets) {
|
|
1573
|
-
manifest.componentSets[compSet.name] = {
|
|
1574
|
-
key: compSet.key,
|
|
1575
|
-
nodeId: compSet.nodeId,
|
|
1576
|
-
name: compSet.name,
|
|
1577
|
-
description: compSet.description || undefined,
|
|
1578
|
-
variants: compSet.variants?.map((v) => ({
|
|
1579
|
-
key: v.key,
|
|
1580
|
-
nodeId: v.nodeId,
|
|
1581
|
-
name: v.name,
|
|
1582
|
-
})) || [],
|
|
1583
|
-
variantAxes: compSet.variantAxes?.map((a) => ({
|
|
1584
|
-
name: a.name,
|
|
1585
|
-
values: a.values,
|
|
1586
|
-
})) || [],
|
|
1587
|
-
};
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
|
-
catch (error) {
|
|
1592
|
-
logger.warn({ error }, "Could not fetch components");
|
|
1593
|
-
}
|
|
1594
|
-
// Update summary
|
|
1595
|
-
manifest.summary = {
|
|
1596
|
-
totalTokens: Object.keys(manifest.tokens.colors).length + Object.keys(manifest.tokens.spacing).length,
|
|
1597
|
-
totalComponents: Object.keys(manifest.components).length,
|
|
1598
|
-
totalComponentSets: Object.keys(manifest.componentSets).length,
|
|
1599
|
-
colorPalette: Object.keys(manifest.tokens.colors).slice(0, 10),
|
|
1600
|
-
spacingScale: Object.values(manifest.tokens.spacing).map(s => s.value).sort((a, b) => a - b).slice(0, 10),
|
|
1601
|
-
typographyScale: [],
|
|
1602
|
-
componentCategories: [],
|
|
1603
|
-
};
|
|
1604
|
-
// Cache the result
|
|
1605
|
-
cache.set(fileKey, manifest, rawComponents);
|
|
1606
|
-
const categories = getCategories(manifest);
|
|
1607
|
-
const tokenSummary = getTokenSummary(manifest);
|
|
1608
|
-
return {
|
|
1609
|
-
content: [{
|
|
1610
|
-
type: "text",
|
|
1611
|
-
text: JSON.stringify({
|
|
1612
|
-
success: true,
|
|
1613
|
-
cached: false,
|
|
1614
|
-
fileKey,
|
|
1615
|
-
categories: categories.slice(0, 15),
|
|
1616
|
-
tokens: tokenSummary,
|
|
1617
|
-
totals: {
|
|
1618
|
-
components: manifest.summary.totalComponents,
|
|
1619
|
-
componentSets: manifest.summary.totalComponentSets,
|
|
1620
|
-
tokens: manifest.summary.totalTokens,
|
|
1621
|
-
},
|
|
1622
|
-
hint: "Use figma_search_components to find specific components by name or category.",
|
|
1623
|
-
}, null, 2),
|
|
1624
|
-
}],
|
|
1625
|
-
};
|
|
1626
|
-
}
|
|
1627
|
-
catch (error) {
|
|
1628
|
-
logger.error({ error }, "Failed to get design system summary");
|
|
1629
|
-
return {
|
|
1630
|
-
content: [{
|
|
1631
|
-
type: "text",
|
|
1632
|
-
text: JSON.stringify({
|
|
1633
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1634
|
-
hint: "Make sure the F-MCP ATezer Bridge plugin is running in Figma",
|
|
1635
|
-
}, null, 2),
|
|
1636
|
-
}],
|
|
1637
|
-
isError: true,
|
|
1638
|
-
};
|
|
1639
|
-
}
|
|
1640
|
-
});
|
|
1641
|
-
// Tool 2: Search Components (~3000 tokens response max, paginated)
|
|
1642
|
-
this.server.registerTool("figma_search_components", {
|
|
1643
|
-
description: "Search for components by name, category, or description. Returns paginated results with component keys for instantiation. Automatically loads the design system cache if needed.",
|
|
1644
|
-
inputSchema: {
|
|
1645
|
-
query: z.string().optional().default("").describe("Search query to match component names or descriptions"),
|
|
1646
|
-
category: z.string().optional().describe("Filter by category (e.g., 'Button', 'Input', 'Card')"),
|
|
1647
|
-
limit: z.number().optional().default(10).describe("Maximum results to return (default: 10, max: 25)"),
|
|
1648
|
-
offset: z.number().optional().default(0).describe("Offset for pagination"),
|
|
1649
|
-
},
|
|
1650
|
-
annotations: { readOnlyHint: true },
|
|
1651
|
-
}, async ({ query, category, limit, offset }) => {
|
|
1652
|
-
try {
|
|
1653
|
-
const { searchComponents } = await import('./core/design-system-manifest.js');
|
|
1654
|
-
// Auto-load design system cache if needed (no error returned to user)
|
|
1655
|
-
const { cacheEntry } = await ensureDesignSystemCache();
|
|
1656
|
-
if (!cacheEntry) {
|
|
1657
|
-
return {
|
|
1658
|
-
content: [{
|
|
1659
|
-
type: "text",
|
|
1660
|
-
text: JSON.stringify({
|
|
1661
|
-
error: "Could not load design system data. Make sure the F-MCP ATezer Bridge plugin is running.",
|
|
1662
|
-
}, null, 2),
|
|
1663
|
-
}],
|
|
1664
|
-
isError: true,
|
|
1665
|
-
};
|
|
1666
|
-
}
|
|
1667
|
-
const effectiveLimit = Math.min(limit || 10, 25);
|
|
1668
|
-
const results = searchComponents(cacheEntry.manifest, query || "", {
|
|
1669
|
-
category,
|
|
1670
|
-
limit: effectiveLimit,
|
|
1671
|
-
offset: offset || 0,
|
|
1672
|
-
});
|
|
1673
|
-
return {
|
|
1674
|
-
content: [{
|
|
1675
|
-
type: "text",
|
|
1676
|
-
text: JSON.stringify({
|
|
1677
|
-
success: true,
|
|
1678
|
-
query: query || "(all)",
|
|
1679
|
-
category: category || "(all)",
|
|
1680
|
-
results: results.results,
|
|
1681
|
-
pagination: {
|
|
1682
|
-
offset: offset || 0,
|
|
1683
|
-
limit: effectiveLimit,
|
|
1684
|
-
total: results.total,
|
|
1685
|
-
hasMore: results.hasMore,
|
|
1686
|
-
},
|
|
1687
|
-
hint: results.hasMore
|
|
1688
|
-
? `Use offset=${(offset || 0) + effectiveLimit} to get more results.`
|
|
1689
|
-
: "Use figma_get_component_details with a component key for full details.",
|
|
1690
|
-
}, null, 2),
|
|
1691
|
-
}],
|
|
1692
|
-
};
|
|
1693
|
-
}
|
|
1694
|
-
catch (error) {
|
|
1695
|
-
logger.error({ error }, "Failed to search components");
|
|
1696
|
-
return {
|
|
1697
|
-
content: [{
|
|
1698
|
-
type: "text",
|
|
1699
|
-
text: JSON.stringify({
|
|
1700
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1701
|
-
}, null, 2),
|
|
1702
|
-
}],
|
|
1703
|
-
isError: true,
|
|
1704
|
-
};
|
|
1705
|
-
}
|
|
1706
|
-
});
|
|
1707
|
-
// Tool 3: Get Component Details (~500 tokens per component)
|
|
1708
|
-
this.server.registerTool("figma_get_component_details", {
|
|
1709
|
-
description: "Get full details for a specific component including all variants, properties, and keys needed for instantiation. Use the component key or name from figma_search_components.",
|
|
1710
|
-
inputSchema: {
|
|
1711
|
-
componentKey: z.string().optional().describe("The component key (preferred for exact match)"),
|
|
1712
|
-
componentName: z.string().optional().describe("The component name (used if key not provided)"),
|
|
1713
|
-
},
|
|
1714
|
-
annotations: { readOnlyHint: true },
|
|
1715
|
-
}, async ({ componentKey, componentName }) => {
|
|
1716
|
-
try {
|
|
1717
|
-
if (!componentKey && !componentName) {
|
|
1718
|
-
return {
|
|
1719
|
-
content: [{
|
|
1720
|
-
type: "text",
|
|
1721
|
-
text: JSON.stringify({
|
|
1722
|
-
error: "Either componentKey or componentName is required",
|
|
1723
|
-
}, null, 2),
|
|
1724
|
-
}],
|
|
1725
|
-
isError: true,
|
|
1726
|
-
};
|
|
1727
|
-
}
|
|
1728
|
-
// Auto-load design system cache if needed
|
|
1729
|
-
const { cacheEntry } = await ensureDesignSystemCache();
|
|
1730
|
-
if (!cacheEntry) {
|
|
1731
|
-
return {
|
|
1732
|
-
content: [{
|
|
1733
|
-
type: "text",
|
|
1734
|
-
text: JSON.stringify({
|
|
1735
|
-
error: "Could not load design system data. Make sure the F-MCP ATezer Bridge plugin is running.",
|
|
1736
|
-
}, null, 2),
|
|
1737
|
-
}],
|
|
1738
|
-
isError: true,
|
|
1739
|
-
};
|
|
1740
|
-
}
|
|
1741
|
-
// Search for the component
|
|
1742
|
-
let component = null;
|
|
1743
|
-
let isComponentSet = false;
|
|
1744
|
-
// Check component sets first (they have variants)
|
|
1745
|
-
for (const [name, compSet] of Object.entries(cacheEntry.manifest.componentSets)) {
|
|
1746
|
-
if ((componentKey && compSet.key === componentKey) || (componentName && name === componentName)) {
|
|
1747
|
-
component = compSet;
|
|
1748
|
-
isComponentSet = true;
|
|
1749
|
-
break;
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
// Check standalone components
|
|
1753
|
-
if (!component) {
|
|
1754
|
-
for (const [name, comp] of Object.entries(cacheEntry.manifest.components)) {
|
|
1755
|
-
if ((componentKey && comp.key === componentKey) || (componentName && name === componentName)) {
|
|
1756
|
-
component = comp;
|
|
1757
|
-
break;
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
if (!component) {
|
|
1762
|
-
return {
|
|
1763
|
-
content: [{
|
|
1764
|
-
type: "text",
|
|
1765
|
-
text: JSON.stringify({
|
|
1766
|
-
error: `Component not found: ${componentKey || componentName}`,
|
|
1767
|
-
hint: "Use figma_search_components to find available components.",
|
|
1768
|
-
}, null, 2),
|
|
1769
|
-
}],
|
|
1770
|
-
isError: true,
|
|
1771
|
-
};
|
|
1772
|
-
}
|
|
1773
|
-
return {
|
|
1774
|
-
content: [{
|
|
1775
|
-
type: "text",
|
|
1776
|
-
text: JSON.stringify({
|
|
1777
|
-
success: true,
|
|
1778
|
-
type: isComponentSet ? "componentSet" : "component",
|
|
1779
|
-
component,
|
|
1780
|
-
instantiation: {
|
|
1781
|
-
key: component.key,
|
|
1782
|
-
example: `Use figma_instantiate_component with componentKey: "${component.key}"`,
|
|
1783
|
-
},
|
|
1784
|
-
}, null, 2),
|
|
1785
|
-
}],
|
|
1786
|
-
};
|
|
1787
|
-
}
|
|
1788
|
-
catch (error) {
|
|
1789
|
-
logger.error({ error }, "Failed to get component details");
|
|
1790
|
-
return {
|
|
1791
|
-
content: [{
|
|
1792
|
-
type: "text",
|
|
1793
|
-
text: JSON.stringify({
|
|
1794
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1795
|
-
}, null, 2),
|
|
1796
|
-
}],
|
|
1797
|
-
isError: true,
|
|
1798
|
-
};
|
|
1799
|
-
}
|
|
1800
|
-
});
|
|
1801
|
-
// Tool 4: Get Token Values (~2000 tokens response max)
|
|
1802
|
-
this.server.registerTool("figma_get_token_values", {
|
|
1803
|
-
description: "Get actual values for design tokens (colors, spacing, etc). Use after figma_get_design_system_summary to get specific token values for implementation.",
|
|
1804
|
-
inputSchema: {
|
|
1805
|
-
type: z.enum(["colors", "spacing", "all"]).optional().default("all").describe("Type of tokens to retrieve"),
|
|
1806
|
-
filter: z.string().optional().describe("Filter token names (e.g., 'primary' to get all primary colors)"),
|
|
1807
|
-
limit: z.number().optional().default(50).describe("Maximum tokens to return (default: 50)"),
|
|
1808
|
-
},
|
|
1809
|
-
annotations: { readOnlyHint: true },
|
|
1810
|
-
}, async ({ type, filter, limit }) => {
|
|
1811
|
-
try {
|
|
1812
|
-
// Auto-load design system cache if needed
|
|
1813
|
-
const { cacheEntry } = await ensureDesignSystemCache();
|
|
1814
|
-
if (!cacheEntry) {
|
|
1815
|
-
return {
|
|
1816
|
-
content: [{
|
|
1817
|
-
type: "text",
|
|
1818
|
-
text: JSON.stringify({
|
|
1819
|
-
error: "Could not load design system data. Make sure the F-MCP ATezer Bridge plugin is running.",
|
|
1820
|
-
}, null, 2),
|
|
1821
|
-
}],
|
|
1822
|
-
isError: true,
|
|
1823
|
-
};
|
|
1824
|
-
}
|
|
1825
|
-
const tokens = cacheEntry.manifest.tokens;
|
|
1826
|
-
const effectiveLimit = Math.min(limit || 50, 100);
|
|
1827
|
-
const filterLower = filter?.toLowerCase();
|
|
1828
|
-
const result = {};
|
|
1829
|
-
if (type === "colors" || type === "all") {
|
|
1830
|
-
const colors = {};
|
|
1831
|
-
let count = 0;
|
|
1832
|
-
for (const [name, token] of Object.entries(tokens.colors)) {
|
|
1833
|
-
if (count >= effectiveLimit)
|
|
1834
|
-
break;
|
|
1835
|
-
if (!filterLower || name.toLowerCase().includes(filterLower)) {
|
|
1836
|
-
colors[name] = { value: token.value, scopes: token.scopes };
|
|
1837
|
-
count++;
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1840
|
-
result.colors = colors;
|
|
1841
|
-
}
|
|
1842
|
-
if (type === "spacing" || type === "all") {
|
|
1843
|
-
const spacing = {};
|
|
1844
|
-
let count = 0;
|
|
1845
|
-
for (const [name, token] of Object.entries(tokens.spacing)) {
|
|
1846
|
-
if (count >= effectiveLimit)
|
|
1847
|
-
break;
|
|
1848
|
-
if (!filterLower || name.toLowerCase().includes(filterLower)) {
|
|
1849
|
-
spacing[name] = { value: token.value };
|
|
1850
|
-
count++;
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
result.spacing = spacing;
|
|
1854
|
-
}
|
|
1855
|
-
return {
|
|
1856
|
-
content: [{
|
|
1857
|
-
type: "text",
|
|
1858
|
-
text: JSON.stringify({
|
|
1859
|
-
success: true,
|
|
1860
|
-
type,
|
|
1861
|
-
filter: filter || "(none)",
|
|
1862
|
-
tokens: result,
|
|
1863
|
-
hint: "Use these exact token names and values when generating designs.",
|
|
1864
|
-
}, null, 2),
|
|
1865
|
-
}],
|
|
1866
|
-
};
|
|
1867
|
-
}
|
|
1868
|
-
catch (error) {
|
|
1869
|
-
logger.error({ error }, "Failed to get token values");
|
|
1870
|
-
return {
|
|
1871
|
-
content: [{
|
|
1872
|
-
type: "text",
|
|
1873
|
-
text: JSON.stringify({
|
|
1874
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1875
|
-
}, null, 2),
|
|
1876
|
-
}],
|
|
1877
|
-
isError: true,
|
|
1878
|
-
};
|
|
1879
|
-
}
|
|
1880
|
-
});
|
|
1881
|
-
// Tool 5: Instantiate Component
|
|
1882
|
-
this.server.registerTool("figma_instantiate_component", {
|
|
1883
|
-
description: `Create an instance of a component from the design system. Works with both published library components (by key) and local/unpublished components (by nodeId).
|
|
1884
|
-
|
|
1885
|
-
**IMPORTANT: Always re-search before instantiating!**
|
|
1886
|
-
NodeIds are session-specific and may be stale from previous conversations. ALWAYS call figma_search_components at the start of each design session to get current, valid identifiers.
|
|
1887
|
-
|
|
1888
|
-
**VISUAL VALIDATION WORKFLOW:**
|
|
1889
|
-
After instantiating components, use figma_take_screenshot to verify the result looks correct. Check placement, sizing, and visual balance.`,
|
|
1890
|
-
inputSchema: {
|
|
1891
|
-
componentKey: z.string().optional().describe("The component key (for published library components). Get this from figma_search_components."),
|
|
1892
|
-
nodeId: z.string().optional().describe("The node ID (for local/unpublished components). Get this from figma_search_components. Required if componentKey doesn't work."),
|
|
1893
|
-
variant: z.record(z.string()).optional().describe("Variant properties to set (e.g., { Type: 'Simple', State: 'Active' })"),
|
|
1894
|
-
overrides: z.record(z.any()).optional().describe("Property overrides (e.g., { 'Button Label': 'Click Me' })"),
|
|
1895
|
-
position: z.object({
|
|
1896
|
-
x: z.number(),
|
|
1897
|
-
y: z.number(),
|
|
1898
|
-
}).optional().describe("Position on canvas (default: 0, 0)"),
|
|
1899
|
-
parentId: z.string().optional().describe("Parent node ID to append the instance to"),
|
|
1900
|
-
},
|
|
1901
|
-
annotations: { destructiveHint: true },
|
|
1902
|
-
}, async ({ componentKey, nodeId, variant, overrides, position, parentId }) => {
|
|
1903
|
-
try {
|
|
1904
|
-
if (!componentKey && !nodeId) {
|
|
1905
|
-
throw new Error("Either componentKey or nodeId is required");
|
|
1906
|
-
}
|
|
1907
|
-
const connector = await this.getDesktopConnector();
|
|
1908
|
-
const result = await connector.instantiateComponent(componentKey || "", {
|
|
1909
|
-
nodeId,
|
|
1910
|
-
position,
|
|
1911
|
-
overrides,
|
|
1912
|
-
variant,
|
|
1913
|
-
parentId,
|
|
1914
|
-
});
|
|
1915
|
-
if (!result.success) {
|
|
1916
|
-
throw new Error(String(result.error || "Failed to instantiate component"));
|
|
1917
|
-
}
|
|
1918
|
-
return {
|
|
1919
|
-
content: [
|
|
1920
|
-
{
|
|
1921
|
-
type: "text",
|
|
1922
|
-
text: JSON.stringify({
|
|
1923
|
-
success: true,
|
|
1924
|
-
message: "Component instantiated successfully",
|
|
1925
|
-
instance: result.instance,
|
|
1926
|
-
timestamp: Date.now(),
|
|
1927
|
-
}, null, 2),
|
|
1928
|
-
},
|
|
1929
|
-
],
|
|
1930
|
-
};
|
|
1931
|
-
}
|
|
1932
|
-
catch (error) {
|
|
1933
|
-
logger.error({ error }, "Failed to instantiate component");
|
|
1934
|
-
return {
|
|
1935
|
-
content: [
|
|
1936
|
-
{
|
|
1937
|
-
type: "text",
|
|
1938
|
-
text: JSON.stringify({
|
|
1939
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1940
|
-
message: "Failed to instantiate component",
|
|
1941
|
-
hint: "Make sure the component key is correct and the F-MCP ATezer Bridge plugin is running",
|
|
1942
|
-
}, null, 2),
|
|
1943
|
-
},
|
|
1944
|
-
],
|
|
1945
|
-
isError: true,
|
|
1946
|
-
};
|
|
1947
|
-
}
|
|
1948
|
-
});
|
|
1949
|
-
// ============================================================================
|
|
1950
|
-
// NEW: Component Property Management Tools
|
|
1951
|
-
// ============================================================================
|
|
1952
|
-
// Tool: Set Node Description
|
|
1953
|
-
this.server.registerTool("figma_set_description", {
|
|
1954
|
-
description: "Set the description text on a component, component set, or style. Descriptions appear in Dev Mode and help document design intent. Supports plain text and markdown formatting.",
|
|
1955
|
-
inputSchema: {
|
|
1956
|
-
nodeId: z.string().describe("The node ID of the component or style to update (e.g., '123:456')"),
|
|
1957
|
-
description: z.string().describe("The plain text description to set"),
|
|
1958
|
-
descriptionMarkdown: z.string().optional().describe("Optional rich text description using markdown formatting"),
|
|
1959
|
-
},
|
|
1960
|
-
annotations: { destructiveHint: true },
|
|
1961
|
-
}, async ({ nodeId, description, descriptionMarkdown }) => {
|
|
1962
|
-
try {
|
|
1963
|
-
const connector = await this.getDesktopConnector();
|
|
1964
|
-
const result = await connector.setNodeDescription(nodeId, description, descriptionMarkdown);
|
|
1965
|
-
if (!result.success) {
|
|
1966
|
-
throw new Error(result.error || "Failed to set description");
|
|
1967
|
-
}
|
|
1968
|
-
return {
|
|
1969
|
-
content: [{
|
|
1970
|
-
type: "text",
|
|
1971
|
-
text: JSON.stringify({
|
|
1972
|
-
success: true,
|
|
1973
|
-
message: "Description set successfully",
|
|
1974
|
-
node: result.node,
|
|
1975
|
-
}, null, 2),
|
|
1976
|
-
}],
|
|
1977
|
-
};
|
|
1978
|
-
}
|
|
1979
|
-
catch (error) {
|
|
1980
|
-
logger.error({ error }, "Failed to set description");
|
|
1981
|
-
return {
|
|
1982
|
-
content: [{
|
|
1983
|
-
type: "text",
|
|
1984
|
-
text: JSON.stringify({
|
|
1985
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1986
|
-
hint: "Make sure the node supports descriptions (components, component sets, styles)",
|
|
1987
|
-
}, null, 2),
|
|
1988
|
-
}],
|
|
1989
|
-
isError: true,
|
|
1990
|
-
};
|
|
1991
|
-
}
|
|
1992
|
-
});
|
|
1993
|
-
// Tool: Add Component Property
|
|
1994
|
-
this.server.registerTool("figma_add_component_property", {
|
|
1995
|
-
description: "Add a new component property to a component or component set. Properties enable dynamic content and behavior in component instances. Supported types: BOOLEAN (toggle), TEXT (string), INSTANCE_SWAP (component swap), VARIANT (variant selection).",
|
|
1996
|
-
inputSchema: {
|
|
1997
|
-
nodeId: z.string().describe("The component or component set node ID"),
|
|
1998
|
-
propertyName: z.string().describe("Name for the new property (e.g., 'Show Icon', 'Button Label')"),
|
|
1999
|
-
type: z.enum(["BOOLEAN", "TEXT", "INSTANCE_SWAP", "VARIANT"]).describe("Property type: BOOLEAN for toggles, TEXT for strings, INSTANCE_SWAP for component swaps, VARIANT for variant selection"),
|
|
2000
|
-
defaultValue: z.any().describe("Default value for the property. BOOLEAN: true/false, TEXT: string, INSTANCE_SWAP: component key, VARIANT: variant value"),
|
|
2001
|
-
},
|
|
2002
|
-
annotations: { destructiveHint: true },
|
|
2003
|
-
}, async ({ nodeId, propertyName, type, defaultValue }) => {
|
|
2004
|
-
try {
|
|
2005
|
-
const connector = await this.getDesktopConnector();
|
|
2006
|
-
const result = await connector.addComponentProperty(nodeId, propertyName, type, defaultValue);
|
|
2007
|
-
if (!result.success) {
|
|
2008
|
-
throw new Error(result.error || "Failed to add property");
|
|
2009
|
-
}
|
|
2010
|
-
return {
|
|
2011
|
-
content: [{
|
|
2012
|
-
type: "text",
|
|
2013
|
-
text: JSON.stringify({
|
|
2014
|
-
success: true,
|
|
2015
|
-
message: "Component property added",
|
|
2016
|
-
propertyName: result.propertyName,
|
|
2017
|
-
hint: "The property name includes a unique suffix (e.g., 'Show Icon#123:456'). Use the full name for editing/deleting.",
|
|
2018
|
-
}, null, 2),
|
|
2019
|
-
}],
|
|
2020
|
-
};
|
|
2021
|
-
}
|
|
2022
|
-
catch (error) {
|
|
2023
|
-
logger.error({ error }, "Failed to add component property");
|
|
2024
|
-
return {
|
|
2025
|
-
content: [{
|
|
2026
|
-
type: "text",
|
|
2027
|
-
text: JSON.stringify({
|
|
2028
|
-
error: error instanceof Error ? error.message : String(error),
|
|
2029
|
-
hint: "Cannot add properties to variant components. Add to the parent component set instead.",
|
|
2030
|
-
}, null, 2),
|
|
2031
|
-
}],
|
|
2032
|
-
isError: true,
|
|
2033
|
-
};
|
|
2034
|
-
}
|
|
2035
|
-
});
|
|
2036
|
-
// Tool: Edit Component Property
|
|
2037
|
-
this.server.registerTool("figma_edit_component_property", {
|
|
2038
|
-
description: "Edit an existing component property. Can change the name, default value, or preferred values (for INSTANCE_SWAP). Use the full property name including the unique suffix.",
|
|
2039
|
-
inputSchema: {
|
|
2040
|
-
nodeId: z.string().describe("The component or component set node ID"),
|
|
2041
|
-
propertyName: z.string().describe("The full property name with suffix (e.g., 'Show Icon#123:456')"),
|
|
2042
|
-
newValue: z.object({
|
|
2043
|
-
name: z.string().optional().describe("New name for the property"),
|
|
2044
|
-
defaultValue: z.any().optional().describe("New default value"),
|
|
2045
|
-
preferredValues: z.array(z.any()).optional().describe("Preferred values (INSTANCE_SWAP only)"),
|
|
2046
|
-
}).describe("Object with the values to update"),
|
|
2047
|
-
},
|
|
2048
|
-
annotations: { destructiveHint: true },
|
|
2049
|
-
}, async ({ nodeId, propertyName, newValue }) => {
|
|
2050
|
-
try {
|
|
2051
|
-
const connector = await this.getDesktopConnector();
|
|
2052
|
-
const result = await connector.editComponentProperty(nodeId, propertyName, newValue);
|
|
2053
|
-
if (!result.success) {
|
|
2054
|
-
throw new Error(result.error || "Failed to edit property");
|
|
2055
|
-
}
|
|
2056
|
-
return {
|
|
2057
|
-
content: [{
|
|
2058
|
-
type: "text",
|
|
2059
|
-
text: JSON.stringify({
|
|
2060
|
-
success: true,
|
|
2061
|
-
message: "Component property updated",
|
|
2062
|
-
propertyName: result.propertyName,
|
|
2063
|
-
}, null, 2),
|
|
2064
|
-
}],
|
|
2065
|
-
};
|
|
2066
|
-
}
|
|
2067
|
-
catch (error) {
|
|
2068
|
-
logger.error({ error }, "Failed to edit component property");
|
|
2069
|
-
return {
|
|
2070
|
-
content: [{
|
|
2071
|
-
type: "text",
|
|
2072
|
-
text: JSON.stringify({
|
|
2073
|
-
error: error instanceof Error ? error.message : String(error),
|
|
2074
|
-
}, null, 2),
|
|
2075
|
-
}],
|
|
2076
|
-
isError: true,
|
|
2077
|
-
};
|
|
2078
|
-
}
|
|
2079
|
-
});
|
|
2080
|
-
// Tool: Delete Component Property
|
|
2081
|
-
this.server.registerTool("figma_delete_component_property", {
|
|
2082
|
-
description: "Delete a component property. Only works with BOOLEAN, TEXT, and INSTANCE_SWAP properties (not VARIANT). This is a destructive operation.",
|
|
2083
|
-
inputSchema: {
|
|
2084
|
-
nodeId: z.string().describe("The component or component set node ID"),
|
|
2085
|
-
propertyName: z.string().describe("The full property name with suffix (e.g., 'Show Icon#123:456')"),
|
|
2086
|
-
},
|
|
2087
|
-
annotations: { destructiveHint: true },
|
|
2088
|
-
}, async ({ nodeId, propertyName }) => {
|
|
2089
|
-
try {
|
|
2090
|
-
const connector = await this.getDesktopConnector();
|
|
2091
|
-
const result = await connector.deleteComponentProperty(nodeId, propertyName);
|
|
2092
|
-
if (!result.success) {
|
|
2093
|
-
throw new Error(result.error || "Failed to delete property");
|
|
2094
|
-
}
|
|
2095
|
-
return {
|
|
2096
|
-
content: [{
|
|
2097
|
-
type: "text",
|
|
2098
|
-
text: JSON.stringify({
|
|
2099
|
-
success: true,
|
|
2100
|
-
message: "Component property deleted",
|
|
2101
|
-
}, null, 2),
|
|
2102
|
-
}],
|
|
2103
|
-
};
|
|
2104
|
-
}
|
|
2105
|
-
catch (error) {
|
|
2106
|
-
logger.error({ error }, "Failed to delete component property");
|
|
2107
|
-
return {
|
|
2108
|
-
content: [{
|
|
2109
|
-
type: "text",
|
|
2110
|
-
text: JSON.stringify({
|
|
2111
|
-
error: error instanceof Error ? error.message : String(error),
|
|
2112
|
-
hint: "Cannot delete VARIANT properties. Only BOOLEAN, TEXT, and INSTANCE_SWAP can be deleted.",
|
|
2113
|
-
}, null, 2),
|
|
2114
|
-
}],
|
|
2115
|
-
isError: true,
|
|
2116
|
-
};
|
|
2117
|
-
}
|
|
2118
|
-
});
|
|
2119
|
-
// ============================================================================
|
|
2120
|
-
// NEW: Node Manipulation Tools
|
|
2121
|
-
// ============================================================================
|
|
2122
|
-
// Tool: Resize Node
|
|
2123
|
-
this.server.registerTool("figma_resize_node", {
|
|
2124
|
-
description: "Resize a node to specific dimensions. By default respects child constraints; use withConstraints=false to ignore them.",
|
|
2125
|
-
inputSchema: {
|
|
2126
|
-
nodeId: z.string().describe("The node ID to resize"),
|
|
2127
|
-
width: z.number().describe("New width in pixels"),
|
|
2128
|
-
height: z.number().describe("New height in pixels"),
|
|
2129
|
-
withConstraints: z.boolean().optional().default(true).describe("Whether to apply child constraints during resize (default: true)"),
|
|
2130
|
-
},
|
|
2131
|
-
annotations: { destructiveHint: true },
|
|
2132
|
-
}, async ({ nodeId, width, height, withConstraints }) => {
|
|
2133
|
-
try {
|
|
2134
|
-
const connector = await this.getDesktopConnector();
|
|
2135
|
-
const result = await connector.resizeNode(nodeId, width, height, withConstraints);
|
|
2136
|
-
if (!result.success) {
|
|
2137
|
-
throw new Error(result.error || "Failed to resize node");
|
|
2138
|
-
}
|
|
2139
|
-
return {
|
|
2140
|
-
content: [{
|
|
2141
|
-
type: "text",
|
|
2142
|
-
text: JSON.stringify({
|
|
2143
|
-
success: true,
|
|
2144
|
-
message: `Node resized to ${width}x${height}`,
|
|
2145
|
-
node: result.node,
|
|
2146
|
-
}, null, 2),
|
|
2147
|
-
}],
|
|
2148
|
-
};
|
|
2149
|
-
}
|
|
2150
|
-
catch (error) {
|
|
2151
|
-
logger.error({ error }, "Failed to resize node");
|
|
2152
|
-
return {
|
|
2153
|
-
content: [{
|
|
2154
|
-
type: "text",
|
|
2155
|
-
text: JSON.stringify({
|
|
2156
|
-
error: error instanceof Error ? error.message : String(error),
|
|
2157
|
-
}, null, 2),
|
|
2158
|
-
}],
|
|
2159
|
-
isError: true,
|
|
2160
|
-
};
|
|
2161
|
-
}
|
|
2162
|
-
});
|
|
2163
|
-
// Tool: Move Node
|
|
2164
|
-
this.server.registerTool("figma_move_node", {
|
|
2165
|
-
description: "Move a node to a new position within its parent.",
|
|
2166
|
-
inputSchema: {
|
|
2167
|
-
nodeId: z.string().describe("The node ID to move"),
|
|
2168
|
-
x: z.number().describe("New X position"),
|
|
2169
|
-
y: z.number().describe("New Y position"),
|
|
2170
|
-
},
|
|
2171
|
-
annotations: { destructiveHint: true },
|
|
2172
|
-
}, async ({ nodeId, x, y }) => {
|
|
2173
|
-
try {
|
|
2174
|
-
const connector = await this.getDesktopConnector();
|
|
2175
|
-
const result = await connector.moveNode(nodeId, x, y);
|
|
2176
|
-
if (!result.success) {
|
|
2177
|
-
throw new Error(result.error || "Failed to move node");
|
|
2178
|
-
}
|
|
2179
|
-
return {
|
|
2180
|
-
content: [{
|
|
2181
|
-
type: "text",
|
|
2182
|
-
text: JSON.stringify({
|
|
2183
|
-
success: true,
|
|
2184
|
-
message: `Node moved to (${x}, ${y})`,
|
|
2185
|
-
node: result.node,
|
|
2186
|
-
}, null, 2),
|
|
2187
|
-
}],
|
|
2188
|
-
};
|
|
2189
|
-
}
|
|
2190
|
-
catch (error) {
|
|
2191
|
-
logger.error({ error }, "Failed to move node");
|
|
2192
|
-
return {
|
|
2193
|
-
content: [{
|
|
2194
|
-
type: "text",
|
|
2195
|
-
text: JSON.stringify({
|
|
2196
|
-
error: error instanceof Error ? error.message : String(error),
|
|
2197
|
-
}, null, 2),
|
|
2198
|
-
}],
|
|
2199
|
-
isError: true,
|
|
2200
|
-
};
|
|
2201
|
-
}
|
|
2202
|
-
});
|
|
2203
|
-
// Tool: Set Node Fills
|
|
2204
|
-
this.server.registerTool("figma_set_fills", {
|
|
2205
|
-
description: "Set the fill colors on a node. Accepts hex color strings (e.g., '#FF0000') or full paint objects.",
|
|
2206
|
-
inputSchema: {
|
|
2207
|
-
nodeId: z.string().describe("The node ID to modify"),
|
|
2208
|
-
fills: z.array(z.object({
|
|
2209
|
-
type: z.literal("SOLID").describe("Fill type (currently only SOLID supported)"),
|
|
2210
|
-
color: z.string().describe("Hex color string (e.g., '#FF0000', '#FF000080' for transparency)"),
|
|
2211
|
-
opacity: z.number().optional().describe("Opacity 0-1 (default: 1)"),
|
|
2212
|
-
})).describe("Array of fill objects"),
|
|
2213
|
-
},
|
|
2214
|
-
annotations: { destructiveHint: true },
|
|
2215
|
-
}, async ({ nodeId, fills }) => {
|
|
2216
|
-
try {
|
|
2217
|
-
const connector = await this.getDesktopConnector();
|
|
2218
|
-
const result = await connector.setNodeFills(nodeId, fills);
|
|
2219
|
-
if (!result.success) {
|
|
2220
|
-
throw new Error(result.error || "Failed to set fills");
|
|
2221
|
-
}
|
|
2222
|
-
return {
|
|
2223
|
-
content: [{
|
|
2224
|
-
type: "text",
|
|
2225
|
-
text: JSON.stringify({
|
|
2226
|
-
success: true,
|
|
2227
|
-
message: "Fills updated",
|
|
2228
|
-
node: result.node,
|
|
2229
|
-
}, null, 2),
|
|
2230
|
-
}],
|
|
2231
|
-
};
|
|
2232
|
-
}
|
|
2233
|
-
catch (error) {
|
|
2234
|
-
logger.error({ error }, "Failed to set fills");
|
|
2235
|
-
return {
|
|
2236
|
-
content: [{
|
|
2237
|
-
type: "text",
|
|
2238
|
-
text: JSON.stringify({
|
|
2239
|
-
error: error instanceof Error ? error.message : String(error),
|
|
2240
|
-
}, null, 2),
|
|
2241
|
-
}],
|
|
2242
|
-
isError: true,
|
|
2243
|
-
};
|
|
2244
|
-
}
|
|
2245
|
-
});
|
|
2246
|
-
// Tool: Set Node Strokes
|
|
2247
|
-
this.server.registerTool("figma_set_strokes", {
|
|
2248
|
-
description: "Set the stroke (border) on a node. Accepts hex color strings and optional stroke weight.",
|
|
2249
|
-
inputSchema: {
|
|
2250
|
-
nodeId: z.string().describe("The node ID to modify"),
|
|
2251
|
-
strokes: z.array(z.object({
|
|
2252
|
-
type: z.literal("SOLID").describe("Stroke type"),
|
|
2253
|
-
color: z.string().describe("Hex color string"),
|
|
2254
|
-
opacity: z.number().optional().describe("Opacity 0-1"),
|
|
2255
|
-
})).describe("Array of stroke objects"),
|
|
2256
|
-
strokeWeight: z.number().optional().describe("Stroke thickness in pixels"),
|
|
2257
|
-
},
|
|
2258
|
-
annotations: { destructiveHint: true },
|
|
2259
|
-
}, async ({ nodeId, strokes, strokeWeight }) => {
|
|
2260
|
-
try {
|
|
2261
|
-
const connector = await this.getDesktopConnector();
|
|
2262
|
-
const result = await connector.setNodeStrokes(nodeId, strokes, strokeWeight);
|
|
2263
|
-
if (!result.success) {
|
|
2264
|
-
throw new Error(result.error || "Failed to set strokes");
|
|
2265
|
-
}
|
|
2266
|
-
return {
|
|
2267
|
-
content: [{
|
|
2268
|
-
type: "text",
|
|
2269
|
-
text: JSON.stringify({
|
|
2270
|
-
success: true,
|
|
2271
|
-
message: "Strokes updated",
|
|
2272
|
-
node: result.node,
|
|
2273
|
-
}, null, 2),
|
|
2274
|
-
}],
|
|
2275
|
-
};
|
|
2276
|
-
}
|
|
2277
|
-
catch (error) {
|
|
2278
|
-
logger.error({ error }, "Failed to set strokes");
|
|
2279
|
-
return {
|
|
2280
|
-
content: [{
|
|
2281
|
-
type: "text",
|
|
2282
|
-
text: JSON.stringify({
|
|
2283
|
-
error: error instanceof Error ? error.message : String(error),
|
|
2284
|
-
}, null, 2),
|
|
2285
|
-
}],
|
|
2286
|
-
isError: true,
|
|
2287
|
-
};
|
|
2288
|
-
}
|
|
2289
|
-
});
|
|
2290
|
-
// Tool: Clone Node
|
|
2291
|
-
this.server.registerTool("figma_clone_node", {
|
|
2292
|
-
description: "Duplicate a node. The clone is placed at a slight offset from the original.",
|
|
2293
|
-
inputSchema: {
|
|
2294
|
-
nodeId: z.string().describe("The node ID to clone"),
|
|
2295
|
-
},
|
|
2296
|
-
annotations: { destructiveHint: true },
|
|
2297
|
-
}, async ({ nodeId }) => {
|
|
2298
|
-
try {
|
|
2299
|
-
const connector = await this.getDesktopConnector();
|
|
2300
|
-
const result = await connector.cloneNode(nodeId);
|
|
2301
|
-
if (!result.success) {
|
|
2302
|
-
throw new Error(result.error || "Failed to clone node");
|
|
2303
|
-
}
|
|
2304
|
-
return {
|
|
2305
|
-
content: [{
|
|
2306
|
-
type: "text",
|
|
2307
|
-
text: JSON.stringify({
|
|
2308
|
-
success: true,
|
|
2309
|
-
message: "Node cloned",
|
|
2310
|
-
clonedNode: result.node,
|
|
2311
|
-
}, null, 2),
|
|
2312
|
-
}],
|
|
2313
|
-
};
|
|
2314
|
-
}
|
|
2315
|
-
catch (error) {
|
|
2316
|
-
logger.error({ error }, "Failed to clone node");
|
|
2317
|
-
return {
|
|
2318
|
-
content: [{
|
|
2319
|
-
type: "text",
|
|
2320
|
-
text: JSON.stringify({
|
|
2321
|
-
error: error instanceof Error ? error.message : String(error),
|
|
2322
|
-
}, null, 2),
|
|
2323
|
-
}],
|
|
2324
|
-
isError: true,
|
|
2325
|
-
};
|
|
2326
|
-
}
|
|
2327
|
-
});
|
|
2328
|
-
// Tool: Delete Node
|
|
2329
|
-
this.server.registerTool("figma_delete_node", {
|
|
2330
|
-
description: "Delete a node from the canvas. WARNING: This is a destructive operation (can be undone with Figma's undo).",
|
|
2331
|
-
inputSchema: {
|
|
2332
|
-
nodeId: z.string().describe("The node ID to delete"),
|
|
2333
|
-
},
|
|
2334
|
-
annotations: { destructiveHint: true },
|
|
2335
|
-
}, async ({ nodeId }) => {
|
|
2336
|
-
try {
|
|
2337
|
-
const connector = await this.getDesktopConnector();
|
|
2338
|
-
const result = await connector.deleteNode(nodeId);
|
|
2339
|
-
if (!result.success) {
|
|
2340
|
-
throw new Error(result.error || "Failed to delete node");
|
|
2341
|
-
}
|
|
2342
|
-
return {
|
|
2343
|
-
content: [{
|
|
2344
|
-
type: "text",
|
|
2345
|
-
text: JSON.stringify({
|
|
2346
|
-
success: true,
|
|
2347
|
-
message: "Node deleted",
|
|
2348
|
-
deleted: result.deleted,
|
|
2349
|
-
}, null, 2),
|
|
2350
|
-
}],
|
|
2351
|
-
};
|
|
2352
|
-
}
|
|
2353
|
-
catch (error) {
|
|
2354
|
-
logger.error({ error }, "Failed to delete node");
|
|
2355
|
-
return {
|
|
2356
|
-
content: [{
|
|
2357
|
-
type: "text",
|
|
2358
|
-
text: JSON.stringify({
|
|
2359
|
-
error: error instanceof Error ? error.message : String(error),
|
|
2360
|
-
}, null, 2),
|
|
2361
|
-
}],
|
|
2362
|
-
isError: true,
|
|
2363
|
-
};
|
|
2364
|
-
}
|
|
2365
|
-
});
|
|
2366
|
-
// Tool: Rename Node
|
|
2367
|
-
this.server.registerTool("figma_rename_node", {
|
|
2368
|
-
description: "Rename a node in the layer panel.",
|
|
2369
|
-
inputSchema: {
|
|
2370
|
-
nodeId: z.string().describe("The node ID to rename"),
|
|
2371
|
-
newName: z.string().describe("The new name for the node"),
|
|
2372
|
-
},
|
|
2373
|
-
annotations: { destructiveHint: true },
|
|
2374
|
-
}, async ({ nodeId, newName }) => {
|
|
2375
|
-
try {
|
|
2376
|
-
const connector = await this.getDesktopConnector();
|
|
2377
|
-
const result = await connector.renameNode(nodeId, newName);
|
|
2378
|
-
if (!result.success) {
|
|
2379
|
-
throw new Error(result.error || "Failed to rename node");
|
|
2380
|
-
}
|
|
2381
|
-
return {
|
|
2382
|
-
content: [{
|
|
2383
|
-
type: "text",
|
|
2384
|
-
text: JSON.stringify({
|
|
2385
|
-
success: true,
|
|
2386
|
-
message: `Node renamed to "${newName}"`,
|
|
2387
|
-
node: result.node,
|
|
2388
|
-
}, null, 2),
|
|
2389
|
-
}],
|
|
2390
|
-
};
|
|
2391
|
-
}
|
|
2392
|
-
catch (error) {
|
|
2393
|
-
logger.error({ error }, "Failed to rename node");
|
|
2394
|
-
return {
|
|
2395
|
-
content: [{
|
|
2396
|
-
type: "text",
|
|
2397
|
-
text: JSON.stringify({
|
|
2398
|
-
error: error instanceof Error ? error.message : String(error),
|
|
2399
|
-
}, null, 2),
|
|
2400
|
-
}],
|
|
2401
|
-
isError: true,
|
|
2402
|
-
};
|
|
2403
|
-
}
|
|
2404
|
-
});
|
|
2405
|
-
// Tool: Set Text Content
|
|
2406
|
-
this.server.registerTool("figma_set_text", {
|
|
2407
|
-
description: "Set the text content of a text node. Optionally adjust font size.",
|
|
2408
|
-
inputSchema: {
|
|
2409
|
-
nodeId: z.string().describe("The text node ID"),
|
|
2410
|
-
text: z.string().describe("The new text content"),
|
|
2411
|
-
fontSize: z.number().optional().describe("Optional font size to set"),
|
|
2412
|
-
},
|
|
2413
|
-
annotations: { destructiveHint: true },
|
|
2414
|
-
}, async ({ nodeId, text, fontSize }) => {
|
|
2415
|
-
try {
|
|
2416
|
-
const connector = await this.getDesktopConnector();
|
|
2417
|
-
const result = await connector.setTextContent(nodeId, text, fontSize ? { fontSize } : undefined);
|
|
2418
|
-
if (!result.success) {
|
|
2419
|
-
throw new Error(result.error || "Failed to set text");
|
|
2420
|
-
}
|
|
2421
|
-
return {
|
|
2422
|
-
content: [{
|
|
2423
|
-
type: "text",
|
|
2424
|
-
text: JSON.stringify({
|
|
2425
|
-
success: true,
|
|
2426
|
-
message: "Text content updated",
|
|
2427
|
-
node: result.node,
|
|
2428
|
-
}, null, 2),
|
|
2429
|
-
}],
|
|
2430
|
-
};
|
|
2431
|
-
}
|
|
2432
|
-
catch (error) {
|
|
2433
|
-
logger.error({ error }, "Failed to set text content");
|
|
2434
|
-
return {
|
|
2435
|
-
content: [{
|
|
2436
|
-
type: "text",
|
|
2437
|
-
text: JSON.stringify({
|
|
2438
|
-
error: error instanceof Error ? error.message : String(error),
|
|
2439
|
-
hint: "Make sure the node is a TEXT node",
|
|
2440
|
-
}, null, 2),
|
|
2441
|
-
}],
|
|
2442
|
-
isError: true,
|
|
2443
|
-
};
|
|
2444
|
-
}
|
|
2445
|
-
});
|
|
2446
|
-
// Tool: Create Child Node
|
|
2447
|
-
this.server.registerTool("figma_create_child", {
|
|
2448
|
-
description: "Create a new child node inside a parent container. Useful for adding shapes, text, or frames to existing structures.",
|
|
2449
|
-
inputSchema: {
|
|
2450
|
-
parentId: z.string().describe("The parent node ID"),
|
|
2451
|
-
nodeType: z.enum(["RECTANGLE", "ELLIPSE", "FRAME", "TEXT", "LINE"]).describe("Type of node to create"),
|
|
2452
|
-
properties: z.object({
|
|
2453
|
-
name: z.string().optional().describe("Name for the new node"),
|
|
2454
|
-
x: z.number().optional().describe("X position within parent"),
|
|
2455
|
-
y: z.number().optional().describe("Y position within parent"),
|
|
2456
|
-
width: z.number().optional().describe("Width (default: 100)"),
|
|
2457
|
-
height: z.number().optional().describe("Height (default: 100)"),
|
|
2458
|
-
fills: z.array(z.object({
|
|
2459
|
-
type: z.literal("SOLID"),
|
|
2460
|
-
color: z.string(),
|
|
2461
|
-
})).optional().describe("Fill colors (hex strings)"),
|
|
2462
|
-
text: z.string().optional().describe("Text content (for TEXT nodes only)"),
|
|
2463
|
-
}).optional().describe("Properties for the new node"),
|
|
2464
|
-
},
|
|
2465
|
-
annotations: { destructiveHint: true },
|
|
2466
|
-
}, async ({ parentId, nodeType, properties }) => {
|
|
2467
|
-
try {
|
|
2468
|
-
const connector = await this.getDesktopConnector();
|
|
2469
|
-
const result = await connector.createChildNode(parentId, nodeType, properties);
|
|
2470
|
-
if (!result.success) {
|
|
2471
|
-
throw new Error(result.error || "Failed to create node");
|
|
2472
|
-
}
|
|
2473
|
-
return {
|
|
2474
|
-
content: [{
|
|
2475
|
-
type: "text",
|
|
2476
|
-
text: JSON.stringify({
|
|
2477
|
-
success: true,
|
|
2478
|
-
message: `Created ${nodeType} node`,
|
|
2479
|
-
node: result.node,
|
|
2480
|
-
}, null, 2),
|
|
2481
|
-
}],
|
|
2482
|
-
};
|
|
2483
|
-
}
|
|
2484
|
-
catch (error) {
|
|
2485
|
-
logger.error({ error }, "Failed to create child node");
|
|
2486
|
-
return {
|
|
2487
|
-
content: [{
|
|
2488
|
-
type: "text",
|
|
2489
|
-
text: JSON.stringify({
|
|
2490
|
-
error: error instanceof Error ? error.message : String(error),
|
|
2491
|
-
hint: "Make sure the parent node supports children (frames, groups, etc.)",
|
|
2492
|
-
}, null, 2),
|
|
2493
|
-
}],
|
|
2494
|
-
isError: true,
|
|
2495
|
-
};
|
|
2496
|
-
}
|
|
2497
|
-
});
|
|
2498
|
-
// Register Figma API tools (Tools 8-11)
|
|
2499
|
-
registerFigmaAPITools(this.server, () => this.getFigmaAPI(), () => this.browserManager?.getCurrentUrl() || null, () => this.consoleMonitor || null, () => this.browserManager || null, () => this.ensureInitialized(), this.variablesCache, // Pass cache for efficient variable queries
|
|
2500
|
-
() => this.getDesktopConnector() // Plugin Bridge (no CDP) or CDP connector for screenshot/setInstanceProperties
|
|
2501
|
-
);
|
|
2502
|
-
logger.info("All MCP tools registered successfully (including write operations)");
|
|
2503
|
-
}
|
|
2504
|
-
/**
|
|
2505
|
-
* Start the MCP server
|
|
2506
|
-
*/
|
|
2507
|
-
async start() {
|
|
2508
|
-
try {
|
|
2509
|
-
logger.info({ config: this.config }, "Starting F-MCP ATezer (Local Mode)");
|
|
2510
|
-
// Check if Figma Desktop is accessible (non-blocking, just for logging)
|
|
2511
|
-
logger.info("Checking Figma Desktop accessibility...");
|
|
2512
|
-
try {
|
|
2513
|
-
await this.checkFigmaDesktop();
|
|
2514
|
-
logger.info("✅ Figma Desktop is accessible and ready");
|
|
2515
|
-
}
|
|
2516
|
-
catch (error) {
|
|
2517
|
-
// Don't crash if Figma isn't running yet - just log a warning
|
|
2518
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2519
|
-
logger.warn({ error: errorMsg }, "⚠️ Figma Desktop not accessible yet - MCP will connect when you use a tool");
|
|
2520
|
-
console.error("\n⚠️ Figma Desktop Check:\n");
|
|
2521
|
-
console.error("Figma Desktop is not currently running with remote debugging enabled.");
|
|
2522
|
-
console.error("The MCP server will start anyway. To use tools, either:");
|
|
2523
|
-
console.error("1. Run the F-MCP ATezer Bridge plugin in Figma (no debug port needed), or");
|
|
2524
|
-
console.error("2. Launch Figma with: --remote-debugging-port=9222\n");
|
|
2525
|
-
}
|
|
2526
|
-
// Start plugin bridge server (plugin can connect without CDP)
|
|
2527
|
-
const bridgePort = this.config.local?.pluginBridgePort ?? 5454;
|
|
2528
|
-
this.pluginBridge = new PluginBridgeServer(bridgePort, {
|
|
2529
|
-
auditLogPath: this.config.local?.auditLogPath,
|
|
2530
|
-
});
|
|
2531
|
-
this.pluginBridge.start();
|
|
2532
|
-
logger.info({ port: bridgePort }, "Plugin bridge: ws://127.0.0.1:%s (no debug port needed when plugin connects)", bridgePort);
|
|
2533
|
-
// Register all tools
|
|
2534
|
-
this.registerTools();
|
|
2535
|
-
// Create stdio transport
|
|
2536
|
-
const transport = new StdioServerTransport();
|
|
2537
|
-
// Connect server to transport
|
|
2538
|
-
await this.server.connect(transport);
|
|
2539
|
-
logger.info("MCP server started successfully on stdio transport");
|
|
2540
|
-
// 🆕 AUTO-CONNECT: Start monitoring immediately if Figma Desktop is available
|
|
2541
|
-
// This enables "get latest logs" workflow without requiring manual setup
|
|
2542
|
-
this.autoConnectToFigma();
|
|
2543
|
-
}
|
|
2544
|
-
catch (error) {
|
|
2545
|
-
logger.error({ error }, "Failed to start MCP server");
|
|
2546
|
-
// Log helpful error message to stderr
|
|
2547
|
-
console.error("\n❌ Failed to start F-MCP ATezer:\n");
|
|
2548
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
2549
|
-
console.error("\n");
|
|
2550
|
-
process.exit(1);
|
|
2551
|
-
}
|
|
2552
|
-
}
|
|
2553
|
-
/**
|
|
2554
|
-
* Cleanup and shutdown
|
|
2555
|
-
*/
|
|
2556
|
-
async shutdown() {
|
|
2557
|
-
logger.info("Shutting down MCP server...");
|
|
2558
|
-
try {
|
|
2559
|
-
if (this.pluginBridge) {
|
|
2560
|
-
this.pluginBridge.stop();
|
|
2561
|
-
this.pluginBridge = null;
|
|
2562
|
-
}
|
|
2563
|
-
if (this.consoleMonitor) {
|
|
2564
|
-
await this.consoleMonitor.stopMonitoring();
|
|
2565
|
-
}
|
|
2566
|
-
if (this.browserManager) {
|
|
2567
|
-
await this.browserManager.close();
|
|
2568
|
-
}
|
|
2569
|
-
logger.info("MCP server shutdown complete");
|
|
2570
|
-
}
|
|
2571
|
-
catch (error) {
|
|
2572
|
-
logger.error({ error }, "Error during shutdown");
|
|
2573
|
-
}
|
|
2574
|
-
}
|
|
2575
|
-
}
|
|
2576
|
-
/**
|
|
2577
|
-
* Main entry point
|
|
2578
|
-
*/
|
|
2579
|
-
async function main() {
|
|
2580
|
-
const server = new LocalFigmaMCP();
|
|
2581
|
-
// Handle graceful shutdown
|
|
2582
|
-
process.on("SIGINT", async () => {
|
|
2583
|
-
await server.shutdown();
|
|
2584
|
-
process.exit(0);
|
|
2585
|
-
});
|
|
2586
|
-
process.on("SIGTERM", async () => {
|
|
2587
|
-
await server.shutdown();
|
|
2588
|
-
process.exit(0);
|
|
2589
|
-
});
|
|
2590
|
-
// Start the server
|
|
2591
|
-
await server.start();
|
|
2592
|
-
}
|
|
2593
|
-
// Run if executed directly
|
|
2594
|
-
// Note: On Windows, import.meta.url uses file:/// (3 slashes) while process.argv uses backslashes
|
|
2595
|
-
// We normalize both paths to compare correctly across platforms
|
|
2596
|
-
const currentFile = fileURLToPath(import.meta.url);
|
|
2597
|
-
const entryFile = process.argv[1] ? resolve(process.argv[1]) : "";
|
|
2598
|
-
if (currentFile === entryFile) {
|
|
2599
|
-
main().catch((error) => {
|
|
2600
|
-
console.error("Fatal error:", error);
|
|
2601
|
-
process.exit(1);
|
|
2602
|
-
});
|
|
2603
|
-
}
|
|
2604
|
-
export { LocalFigmaMCP };
|
|
2605
|
-
//# sourceMappingURL=local.js.map
|