figma-code-agent-mcp 1.0.0

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.
Files changed (67) hide show
  1. package/dist/assetServer.d.ts +34 -0
  2. package/dist/assetServer.js +168 -0
  3. package/dist/codeGenerator/componentDetector.d.ts +57 -0
  4. package/dist/codeGenerator/componentDetector.js +171 -0
  5. package/dist/codeGenerator/index.d.ts +77 -0
  6. package/dist/codeGenerator/index.js +184 -0
  7. package/dist/codeGenerator/jsxGenerator.d.ts +46 -0
  8. package/dist/codeGenerator/jsxGenerator.js +182 -0
  9. package/dist/codeGenerator/styleConverter.d.ts +95 -0
  10. package/dist/codeGenerator/styleConverter.js +306 -0
  11. package/dist/hints.d.ts +14 -0
  12. package/dist/hints.js +105 -0
  13. package/dist/hub.d.ts +9 -0
  14. package/dist/hub.js +252 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +146 -0
  17. package/dist/sandbox.d.ts +18 -0
  18. package/dist/sandbox.js +154 -0
  19. package/dist/toolRegistry.d.ts +19 -0
  20. package/dist/toolRegistry.js +729 -0
  21. package/dist/tools/captureScreenshot.d.ts +28 -0
  22. package/dist/tools/captureScreenshot.js +31 -0
  23. package/dist/tools/cloneNode.d.ts +43 -0
  24. package/dist/tools/cloneNode.js +46 -0
  25. package/dist/tools/createFrame.d.ts +157 -0
  26. package/dist/tools/createFrame.js +114 -0
  27. package/dist/tools/createInstance.d.ts +38 -0
  28. package/dist/tools/createInstance.js +41 -0
  29. package/dist/tools/createRectangle.d.ts +108 -0
  30. package/dist/tools/createRectangle.js +77 -0
  31. package/dist/tools/createText.d.ts +81 -0
  32. package/dist/tools/createText.js +67 -0
  33. package/dist/tools/deleteNode.d.ts +15 -0
  34. package/dist/tools/deleteNode.js +18 -0
  35. package/dist/tools/execute.d.ts +17 -0
  36. package/dist/tools/execute.js +68 -0
  37. package/dist/tools/getCurrentPage.d.ts +16 -0
  38. package/dist/tools/getCurrentPage.js +19 -0
  39. package/dist/tools/getDesignContext.d.ts +42 -0
  40. package/dist/tools/getDesignContext.js +55 -0
  41. package/dist/tools/getLocalComponents.d.ts +20 -0
  42. package/dist/tools/getLocalComponents.js +23 -0
  43. package/dist/tools/getNode.d.ts +30 -0
  44. package/dist/tools/getNode.js +33 -0
  45. package/dist/tools/getSelection.d.ts +10 -0
  46. package/dist/tools/getSelection.js +13 -0
  47. package/dist/tools/getStyles.d.ts +17 -0
  48. package/dist/tools/getStyles.js +20 -0
  49. package/dist/tools/getVariables.d.ts +26 -0
  50. package/dist/tools/getVariables.js +29 -0
  51. package/dist/tools/moveNode.d.ts +28 -0
  52. package/dist/tools/moveNode.js +31 -0
  53. package/dist/tools/openInEditor.d.ts +21 -0
  54. package/dist/tools/openInEditor.js +98 -0
  55. package/dist/tools/searchNodes.d.ts +30 -0
  56. package/dist/tools/searchNodes.js +46 -0
  57. package/dist/tools/searchTools.d.ts +28 -0
  58. package/dist/tools/searchTools.js +28 -0
  59. package/dist/tools/swapComponent.d.ts +23 -0
  60. package/dist/tools/swapComponent.js +26 -0
  61. package/dist/tools/updateNode.d.ts +194 -0
  62. package/dist/tools/updateNode.js +163 -0
  63. package/dist/types.d.ts +101 -0
  64. package/dist/types.js +1 -0
  65. package/dist/websocket.d.ts +30 -0
  66. package/dist/websocket.js +282 -0
  67. package/package.json +29 -0
package/dist/hints.js ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Contextual hints to help AI agents improve their Figma workflow.
3
+ * These are appended to tool responses to encourage best practices.
4
+ */
5
+ // Track if we've shown the improvement agent hint
6
+ let improvementHintShown = false;
7
+ export const IMPROVEMENT_AGENT_PROMPT = `
8
+ 🔧 **MCP Feedback Request**: If you haven't already, launch a background subagent to monitor this session and provide feedback **after each user request**:
9
+
10
+ **1. Context Window Optimization** - After observing tool responses, suggest:
11
+ - Were responses too verbose? Could \`summary: true\` or lower \`depth\` have been used?
12
+ - Were too many children returned? Suggest appropriate \`childLimit\` values
13
+ - Could filters have narrowed results? (e.g., \`nameFilter\`, \`type\` filters)
14
+ - Were multiple calls made that could be batched or avoided?
15
+
16
+ **2. MCP Improvements** - Note friction and suggest:
17
+ - New tools that would help (e.g., "batch_update", "find_by_name", "apply_style")
18
+ - Missing parameters or response fields
19
+ - Better defaults to reduce context usage
20
+
21
+ **Available context-saving parameters:**
22
+ - \`get_node\`: summary=true (minimal fields), depth=0-2, childLimit=N
23
+ - \`get_local_components\`: nameFilter="...", limit=N
24
+ - \`get_variables\`: type=COLOR|FLOAT|STRING|BOOLEAN, collectionName="...", limit=N
25
+ - \`get_styles\`: type=PAINT|TEXT|EFFECT|GRID
26
+
27
+ Repository: MCP server code is in the current working directory.
28
+ `.trim();
29
+ export function getImprovementHint() {
30
+ if (improvementHintShown) {
31
+ return null;
32
+ }
33
+ improvementHintShown = true;
34
+ return IMPROVEMENT_AGENT_PROMPT;
35
+ }
36
+ export function resetImprovementHint() {
37
+ improvementHintShown = false;
38
+ }
39
+ export function getHintsForTool(context) {
40
+ const hints = [];
41
+ const { toolName, result, args } = context;
42
+ switch (toolName) {
43
+ case 'create_frame':
44
+ case 'create_rectangle':
45
+ case 'create_text':
46
+ hints.push('💡 Tip: Before creating more elements, consider using `get_local_components` to check for existing components that match your needs.', '💡 Tip: Use `get_styles` to find existing color and text styles to maintain design consistency.', '💡 Tip: Use `get_variables` to discover design tokens (colors, spacing) that should be used instead of hardcoded values.');
47
+ break;
48
+ case 'create_instance':
49
+ hints.push('💡 Tip: Use `get_node` with depth=1 on the instance to see overridable properties.', '💡 Tip: Component instances can have their nested text and properties overridden using `update_node`.');
50
+ break;
51
+ case 'get_selection':
52
+ if (Array.isArray(result) && result.length > 0) {
53
+ hints.push('💡 Tip: To understand the full structure of selected elements, use `get_node` with depth=2 or higher.', '💡 Tip: Consider using `capture_screenshot` to visually verify the current state before making changes.');
54
+ }
55
+ else {
56
+ hints.push('💡 Tip: No selection found. Ask the user to select elements in Figma, or use `get_node` with a specific node ID.');
57
+ }
58
+ break;
59
+ case 'get_node':
60
+ hints.push('💡 Tip: If this is a component instance, you can use `swap_component` to change its variant.', '💡 Tip: Use `clone_node` to duplicate this element with modifications.');
61
+ break;
62
+ case 'get_local_components':
63
+ hints.push('💡 Tip: Use `create_instance` with the componentId to add these components to your design.', '💡 Tip: Component sets (variants) can be explored further using `get_node` to see all variant properties.');
64
+ break;
65
+ case 'get_styles':
66
+ hints.push('💡 Tip: Apply these styles programmatically by referencing their color values in create/update operations.', '💡 Tip: Consider documenting which styles are being used for consistency.');
67
+ break;
68
+ case 'get_variables':
69
+ hints.push('💡 Tip: Variables are the foundation of a design system. Use these values instead of hardcoded colors/numbers.', '💡 Tip: Variable collections often represent different themes or modes (light/dark).');
70
+ break;
71
+ case 'capture_screenshot':
72
+ hints.push('💡 Tip: Screenshots are useful for verifying visual changes. Consider capturing before and after states.', '💡 Tip: Share screenshots with the user to confirm the design matches expectations.');
73
+ break;
74
+ case 'update_node':
75
+ case 'move_node':
76
+ case 'delete_node':
77
+ hints.push('💡 Tip: Use `capture_screenshot` to verify the visual result of your changes.', '💡 Tip: Consider using `get_node` to confirm the update was applied correctly.');
78
+ break;
79
+ }
80
+ return hints;
81
+ }
82
+ // Set to true to enable contextual hints in responses
83
+ const HINTS_ENABLED = false;
84
+ export function formatResponseWithHints(result, toolName, args) {
85
+ const resultJson = JSON.stringify(result, null, 2);
86
+ if (!HINTS_ENABLED) {
87
+ return resultJson;
88
+ }
89
+ const sections = [resultJson];
90
+ // Add improvement agent hint on first tool call
91
+ const improvementHint = getImprovementHint();
92
+ if (improvementHint) {
93
+ sections.push(improvementHint);
94
+ }
95
+ // Add contextual tool hints
96
+ const hints = getHintsForTool({ toolName, result, args });
97
+ if (hints.length > 0) {
98
+ // Select 1-2 random hints to avoid overwhelming the response
99
+ const selectedHints = hints
100
+ .sort(() => Math.random() - 0.5)
101
+ .slice(0, 2);
102
+ sections.push(selectedHints.join('\n'));
103
+ }
104
+ return sections.join('\n\n---\n');
105
+ }
package/dist/hub.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone WebSocket hub that routes messages between multiple MCP server
4
+ * instances and the Figma plugin. Auto-exits after an idle timeout when all
5
+ * clients have disconnected.
6
+ *
7
+ * Launched automatically by the first MCP server that starts.
8
+ */
9
+ export {};
package/dist/hub.js ADDED
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone WebSocket hub that routes messages between multiple MCP server
4
+ * instances and the Figma plugin. Auto-exits after an idle timeout when all
5
+ * clients have disconnected.
6
+ *
7
+ * Launched automatically by the first MCP server that starts.
8
+ */
9
+ import { WebSocketServer, WebSocket } from 'ws';
10
+ import crypto from 'node:crypto';
11
+ import fs from 'node:fs';
12
+ import os from 'node:os';
13
+ import path from 'node:path';
14
+ // ---------- port validation ----------
15
+ const port = parseInt(process.env.FIGMA_HUB_PORT || '3001', 10);
16
+ if (isNaN(port) || port < 1 || port > 65535) {
17
+ console.error('[Hub] Invalid port number');
18
+ process.exit(1);
19
+ }
20
+ const PORT = port;
21
+ // ---------- auth token ----------
22
+ const AUTH_TOKEN = crypto.randomBytes(32).toString('hex');
23
+ const TOKEN_PATH = path.join(os.homedir(), '.figma-mcp-hub-token');
24
+ fs.writeFileSync(TOKEN_PATH, AUTH_TOKEN, { mode: 0o600 });
25
+ function cleanup() {
26
+ try {
27
+ fs.unlinkSync(TOKEN_PATH);
28
+ }
29
+ catch { }
30
+ }
31
+ process.on('exit', cleanup);
32
+ const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
33
+ // ---------- state ----------
34
+ let figmaPlugin = null;
35
+ let pluginEditorType = null;
36
+ const mcpClients = new Map();
37
+ let idleTimer = null;
38
+ // ---------- rate limiting ----------
39
+ const messageTimestamps = new WeakMap();
40
+ const RATE_LIMIT_WINDOW = 10000; // 10 seconds
41
+ const RATE_LIMIT_MAX = 100;
42
+ function checkRateLimit(ws) {
43
+ const now = Date.now();
44
+ let timestamps = messageTimestamps.get(ws);
45
+ if (!timestamps) {
46
+ timestamps = [];
47
+ messageTimestamps.set(ws, timestamps);
48
+ }
49
+ // Remove old entries outside window
50
+ while (timestamps.length > 0 && timestamps[0] <= now - RATE_LIMIT_WINDOW) {
51
+ timestamps.shift();
52
+ }
53
+ if (timestamps.length >= RATE_LIMIT_MAX) {
54
+ return false; // rate limited
55
+ }
56
+ timestamps.push(now);
57
+ return true;
58
+ }
59
+ // ---------- idle management ----------
60
+ function resetIdleTimer() {
61
+ if (idleTimer)
62
+ clearTimeout(idleTimer);
63
+ idleTimer = null;
64
+ }
65
+ function startIdleTimerIfEmpty() {
66
+ if (mcpClients.size === 0 && !figmaPlugin) {
67
+ idleTimer = setTimeout(() => {
68
+ console.error('[Hub] No clients for 5 minutes — shutting down.');
69
+ process.exit(0);
70
+ }, IDLE_TIMEOUT_MS);
71
+ }
72
+ }
73
+ // ---------- helpers ----------
74
+ function broadcastSessionList() {
75
+ if (!figmaPlugin || figmaPlugin.readyState !== WebSocket.OPEN)
76
+ return;
77
+ const sessions = Array.from(mcpClients.values()).map((c) => ({
78
+ sessionId: c.sessionId,
79
+ clientInfo: c.clientInfo,
80
+ }));
81
+ figmaPlugin.send(JSON.stringify({ type: 'sessions', sessions }));
82
+ }
83
+ // ---------- server ----------
84
+ const wss = new WebSocketServer({ port: PORT, host: '127.0.0.1', maxPayload: 512 * 1024 });
85
+ wss.on('listening', () => {
86
+ console.error(`[Hub] Listening on port ${PORT}`);
87
+ });
88
+ wss.on('connection', (ws) => {
89
+ // Every new connection must send a `register` message first.
90
+ let registered = false;
91
+ const registrationTimeout = setTimeout(() => {
92
+ if (!registered) {
93
+ console.error('[Hub] Connection did not register in time — closing');
94
+ ws.close(4000, 'Registration timeout');
95
+ }
96
+ }, 5000);
97
+ ws.on('message', (raw) => {
98
+ if (!checkRateLimit(ws)) {
99
+ ws.send(JSON.stringify({ error: 'Rate limit exceeded. Max 100 messages per 10 seconds.' }));
100
+ return;
101
+ }
102
+ let msg;
103
+ try {
104
+ msg = JSON.parse(raw.toString());
105
+ }
106
+ catch {
107
+ return;
108
+ }
109
+ // ── registration ──
110
+ if (!registered && msg.type === 'register') {
111
+ registered = true;
112
+ clearTimeout(registrationTimeout);
113
+ resetIdleTimer();
114
+ if (msg.role === 'figma-plugin') {
115
+ figmaPlugin = ws;
116
+ console.error('[Hub] Figma plugin connected');
117
+ if (msg.editorType) {
118
+ pluginEditorType = msg.editorType;
119
+ }
120
+ // Send stored client infos so the plugin sees all agents
121
+ broadcastSessionList();
122
+ return;
123
+ }
124
+ if (msg.role === 'mcp-server') {
125
+ if (msg.token !== AUTH_TOKEN) {
126
+ console.error('[Hub] MCP server rejected: invalid token');
127
+ ws.close(4001, 'Invalid authentication token');
128
+ return;
129
+ }
130
+ const sessionId = msg.sessionId;
131
+ mcpClients.set(sessionId, { ws, sessionId, clientInfo: null });
132
+ console.error(`[Hub] MCP server registered: ${sessionId}`);
133
+ // Tell MCP server whether plugin is connected
134
+ ws.send(JSON.stringify({
135
+ type: 'hub-status',
136
+ pluginConnected: figmaPlugin !== null && figmaPlugin.readyState === WebSocket.OPEN,
137
+ editorType: pluginEditorType,
138
+ }));
139
+ broadcastSessionList();
140
+ return;
141
+ }
142
+ console.error('[Hub] Unknown role:', msg.role);
143
+ return;
144
+ }
145
+ if (!registered)
146
+ return; // ignore messages before registration
147
+ // ── messages FROM mcp-server ──
148
+ // Find which MCP client sent this
149
+ const sender = Array.from(mcpClients.values()).find((c) => c.ws === ws);
150
+ if (sender) {
151
+ // clientInfo update
152
+ if (msg.type === 'clientInfo') {
153
+ sender.clientInfo = msg.clientInfo;
154
+ console.error(`[Hub] Client info from ${sender.sessionId}: ${sender.clientInfo?.name}`);
155
+ broadcastSessionList();
156
+ return;
157
+ }
158
+ // command → forward to plugin with sessionId tag
159
+ if (msg.command) {
160
+ if (!figmaPlugin || figmaPlugin.readyState !== WebSocket.OPEN) {
161
+ // Send error response back
162
+ ws.send(JSON.stringify({
163
+ id: msg.id,
164
+ success: false,
165
+ error: 'Figma plugin is not connected. Please open the Figma plugin first.',
166
+ }));
167
+ return;
168
+ }
169
+ figmaPlugin.send(JSON.stringify({
170
+ ...msg,
171
+ sessionId: sender.sessionId,
172
+ }));
173
+ return;
174
+ }
175
+ return;
176
+ }
177
+ // ── messages FROM figma-plugin ──
178
+ if (ws === figmaPlugin) {
179
+ // editorType update
180
+ if (msg.type === 'editorType') {
181
+ pluginEditorType = msg.editorType;
182
+ console.error(`[Hub] Editor type: ${pluginEditorType}`);
183
+ return;
184
+ }
185
+ // response → route back to correct MCP server by sessionId
186
+ if (msg.id && msg.sessionId) {
187
+ const target = mcpClients.get(msg.sessionId);
188
+ if (target && target.ws.readyState === WebSocket.OPEN) {
189
+ // Strip sessionId before forwarding to MCP server
190
+ const { sessionId: _sid, ...response } = msg;
191
+ target.ws.send(JSON.stringify(response));
192
+ }
193
+ else {
194
+ console.error(`[Hub] No MCP client for session: ${msg.sessionId}`);
195
+ }
196
+ return;
197
+ }
198
+ // response without sessionId (legacy) — try to match by request ID
199
+ if (msg.id) {
200
+ // Broadcast to all MCP clients; only the one with the matching pending
201
+ // request will use it (safe because IDs are UUIDs).
202
+ for (const client of mcpClients.values()) {
203
+ if (client.ws.readyState === WebSocket.OPEN) {
204
+ client.ws.send(JSON.stringify(msg));
205
+ }
206
+ }
207
+ }
208
+ }
209
+ });
210
+ ws.on('close', () => {
211
+ clearTimeout(registrationTimeout);
212
+ if (ws === figmaPlugin) {
213
+ figmaPlugin = null;
214
+ pluginEditorType = null;
215
+ console.error('[Hub] Figma plugin disconnected');
216
+ // Notify all MCP servers
217
+ for (const client of mcpClients.values()) {
218
+ if (client.ws.readyState === WebSocket.OPEN) {
219
+ client.ws.send(JSON.stringify({ type: 'plugin-disconnected' }));
220
+ }
221
+ }
222
+ }
223
+ else {
224
+ // An MCP server disconnected
225
+ for (const [sid, client] of mcpClients) {
226
+ if (client.ws === ws) {
227
+ mcpClients.delete(sid);
228
+ console.error(`[Hub] MCP server disconnected: ${sid}`);
229
+ broadcastSessionList();
230
+ break;
231
+ }
232
+ }
233
+ }
234
+ startIdleTimerIfEmpty();
235
+ });
236
+ ws.on('error', (err) => {
237
+ console.error('[Hub] Socket error:', err.message);
238
+ });
239
+ });
240
+ wss.on('error', (error) => {
241
+ if (error.code === 'EADDRINUSE') {
242
+ // Another hub is already running — just exit silently.
243
+ console.error(`[Hub] Port ${PORT} already in use — another hub is running.`);
244
+ process.exit(0);
245
+ }
246
+ console.error('[Hub] Server error:', error);
247
+ });
248
+ // Start idle timer immediately (will be cleared once first client connects)
249
+ startIdleTimerIfEmpty();
250
+ // Graceful shutdown
251
+ process.on('SIGTERM', () => { cleanup(); process.exit(0); });
252
+ process.on('SIGINT', () => { cleanup(); process.exit(0); });
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
5
+ import { figmaWebSocket } from './websocket.js';
6
+ import { assetServer } from './assetServer.js';
7
+ // Direct tools (only those with special MCP response handling)
8
+ import { captureScreenshotSchema, captureScreenshot } from './tools/captureScreenshot.js';
9
+ import { getDesignContextSchema, getDesignContext } from './tools/getDesignContext.js';
10
+ // New tools
11
+ import { executeSchema } from './tools/execute.js';
12
+ import { searchToolsSchema } from './tools/searchTools.js';
13
+ import { createAndRunSandbox } from './sandbox.js';
14
+ import { searchRegistry } from './toolRegistry.js';
15
+ const server = new Server({
16
+ name: 'figma-code-agent',
17
+ version: '1.0.0',
18
+ }, {
19
+ capabilities: {
20
+ tools: {},
21
+ },
22
+ });
23
+ // Send client info to Figma plugin when MCP client is initialized
24
+ server.oninitialized = () => {
25
+ const clientVersion = server.getClientVersion();
26
+ // Include project path so plugin UI shows which session is connected
27
+ const cwd = process.cwd();
28
+ const projectName = cwd.split('/').pop() || cwd;
29
+ figmaWebSocket.setClientInfo({
30
+ name: clientVersion?.name || 'MCP Client',
31
+ version: clientVersion?.version,
32
+ projectPath: projectName,
33
+ });
34
+ };
35
+ // List available tools (4 total)
36
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
37
+ return {
38
+ tools: [
39
+ executeSchema,
40
+ searchToolsSchema,
41
+ captureScreenshotSchema,
42
+ getDesignContextSchema,
43
+ ],
44
+ };
45
+ });
46
+ // Handle tool calls
47
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
48
+ const { name, arguments: args = {} } = request.params;
49
+ try {
50
+ let result;
51
+ switch (name) {
52
+ case 'execute': {
53
+ const code = args.code;
54
+ if (!code) {
55
+ throw new Error('Missing required parameter: code');
56
+ }
57
+ const sandboxResult = await createAndRunSandbox(code);
58
+ const parts = [];
59
+ if (sandboxResult.error) {
60
+ parts.push(`Error: ${sandboxResult.error}`);
61
+ }
62
+ else {
63
+ parts.push(JSON.stringify(sandboxResult.result, null, 2));
64
+ }
65
+ if (sandboxResult.logs.length > 0) {
66
+ parts.push(`\n--- Console Output ---\n${sandboxResult.logs.join('\n')}`);
67
+ }
68
+ parts.push(`\n(executed in ${sandboxResult.duration}ms)`);
69
+ return {
70
+ content: [
71
+ {
72
+ type: 'text',
73
+ text: parts.join('\n'),
74
+ },
75
+ ],
76
+ isError: !!sandboxResult.error,
77
+ };
78
+ }
79
+ case 'search_tools': {
80
+ const query = args.query;
81
+ if (!query) {
82
+ throw new Error('Missing required parameter: query');
83
+ }
84
+ const category = args.category;
85
+ const detailLevel = args.detail_level || 'description';
86
+ result = searchRegistry(query, category, detailLevel);
87
+ break;
88
+ }
89
+ case 'capture_screenshot': {
90
+ const screenshotResult = await captureScreenshot(args);
91
+ const format = (args.format || 'PNG').toUpperCase();
92
+ const mimeType = format === 'JPG' ? 'image/jpeg' :
93
+ format === 'SVG' ? 'image/svg+xml' :
94
+ format === 'PDF' ? 'application/pdf' : 'image/png';
95
+ return {
96
+ content: [
97
+ {
98
+ type: 'image',
99
+ data: screenshotResult.data,
100
+ mimeType,
101
+ },
102
+ ],
103
+ };
104
+ }
105
+ case 'get_design_context':
106
+ result = await getDesignContext(args);
107
+ break;
108
+ default:
109
+ throw new Error(`Unknown tool: ${name}`);
110
+ }
111
+ return {
112
+ content: [
113
+ {
114
+ type: 'text',
115
+ text: JSON.stringify(result, null, 2),
116
+ },
117
+ ],
118
+ };
119
+ }
120
+ catch (error) {
121
+ const errorMessage = error instanceof Error ? error.message : String(error);
122
+ return {
123
+ content: [
124
+ {
125
+ type: 'text',
126
+ text: `Error: ${errorMessage}`,
127
+ },
128
+ ],
129
+ isError: true,
130
+ };
131
+ }
132
+ });
133
+ async function main() {
134
+ // Connect to WebSocket hub (launches hub if needed)
135
+ await figmaWebSocket.initialize();
136
+ // Initialize asset server for serving exported images
137
+ assetServer.initialize();
138
+ // Connect MCP server via stdio
139
+ const transport = new StdioServerTransport();
140
+ await server.connect(transport);
141
+ console.error('Figma MCP Server running on stdio');
142
+ }
143
+ main().catch((error) => {
144
+ console.error('Fatal error:', error);
145
+ process.exit(1);
146
+ });
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Code execution sandbox using Node.js vm module.
3
+ * Provides a figma.* API that delegates to figmaWebSocket.sendCommand().
4
+ */
5
+ export interface SandboxResult {
6
+ result: unknown;
7
+ logs: string[];
8
+ error?: string;
9
+ duration: number;
10
+ }
11
+ /**
12
+ * Format sandbox errors with contextual hints.
13
+ */
14
+ export declare function formatSandboxError(error: unknown): string;
15
+ /**
16
+ * Create a VM sandbox and run user code with the figma.* API.
17
+ */
18
+ export declare function createAndRunSandbox(code: string, timeoutMs?: number): Promise<SandboxResult>;