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.
- package/dist/assetServer.d.ts +34 -0
- package/dist/assetServer.js +168 -0
- package/dist/codeGenerator/componentDetector.d.ts +57 -0
- package/dist/codeGenerator/componentDetector.js +171 -0
- package/dist/codeGenerator/index.d.ts +77 -0
- package/dist/codeGenerator/index.js +184 -0
- package/dist/codeGenerator/jsxGenerator.d.ts +46 -0
- package/dist/codeGenerator/jsxGenerator.js +182 -0
- package/dist/codeGenerator/styleConverter.d.ts +95 -0
- package/dist/codeGenerator/styleConverter.js +306 -0
- package/dist/hints.d.ts +14 -0
- package/dist/hints.js +105 -0
- package/dist/hub.d.ts +9 -0
- package/dist/hub.js +252 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +146 -0
- package/dist/sandbox.d.ts +18 -0
- package/dist/sandbox.js +154 -0
- package/dist/toolRegistry.d.ts +19 -0
- package/dist/toolRegistry.js +729 -0
- package/dist/tools/captureScreenshot.d.ts +28 -0
- package/dist/tools/captureScreenshot.js +31 -0
- package/dist/tools/cloneNode.d.ts +43 -0
- package/dist/tools/cloneNode.js +46 -0
- package/dist/tools/createFrame.d.ts +157 -0
- package/dist/tools/createFrame.js +114 -0
- package/dist/tools/createInstance.d.ts +38 -0
- package/dist/tools/createInstance.js +41 -0
- package/dist/tools/createRectangle.d.ts +108 -0
- package/dist/tools/createRectangle.js +77 -0
- package/dist/tools/createText.d.ts +81 -0
- package/dist/tools/createText.js +67 -0
- package/dist/tools/deleteNode.d.ts +15 -0
- package/dist/tools/deleteNode.js +18 -0
- package/dist/tools/execute.d.ts +17 -0
- package/dist/tools/execute.js +68 -0
- package/dist/tools/getCurrentPage.d.ts +16 -0
- package/dist/tools/getCurrentPage.js +19 -0
- package/dist/tools/getDesignContext.d.ts +42 -0
- package/dist/tools/getDesignContext.js +55 -0
- package/dist/tools/getLocalComponents.d.ts +20 -0
- package/dist/tools/getLocalComponents.js +23 -0
- package/dist/tools/getNode.d.ts +30 -0
- package/dist/tools/getNode.js +33 -0
- package/dist/tools/getSelection.d.ts +10 -0
- package/dist/tools/getSelection.js +13 -0
- package/dist/tools/getStyles.d.ts +17 -0
- package/dist/tools/getStyles.js +20 -0
- package/dist/tools/getVariables.d.ts +26 -0
- package/dist/tools/getVariables.js +29 -0
- package/dist/tools/moveNode.d.ts +28 -0
- package/dist/tools/moveNode.js +31 -0
- package/dist/tools/openInEditor.d.ts +21 -0
- package/dist/tools/openInEditor.js +98 -0
- package/dist/tools/searchNodes.d.ts +30 -0
- package/dist/tools/searchNodes.js +46 -0
- package/dist/tools/searchTools.d.ts +28 -0
- package/dist/tools/searchTools.js +28 -0
- package/dist/tools/swapComponent.d.ts +23 -0
- package/dist/tools/swapComponent.js +26 -0
- package/dist/tools/updateNode.d.ts +194 -0
- package/dist/tools/updateNode.js +163 -0
- package/dist/types.d.ts +101 -0
- package/dist/types.js +1 -0
- package/dist/websocket.d.ts +30 -0
- package/dist/websocket.js +282 -0
- 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); });
|
package/dist/index.d.ts
ADDED
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>;
|