with-figma 0.1.6 → 0.2.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/bin/with-figma.js +0 -6
- package/figma-plugin/ui.html +83 -143
- package/mcp-server/dist/index.js +47 -294
- package/package.json +2 -3
package/bin/with-figma.js
CHANGED
|
@@ -24,9 +24,6 @@ if (command === "init") {
|
|
|
24
24
|
mcpJson.servers["with-figma"] = {
|
|
25
25
|
command: "npx",
|
|
26
26
|
args: ["-y", "with-figma", "serve"],
|
|
27
|
-
env: {
|
|
28
|
-
OPENAI_API_KEY: "${env:OPENAI_API_KEY}",
|
|
29
|
-
},
|
|
30
27
|
};
|
|
31
28
|
fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2) + "\n");
|
|
32
29
|
console.log("✓ .vscode/mcp.json updated");
|
|
@@ -44,9 +41,6 @@ if (command === "init") {
|
|
|
44
41
|
claudeJson.mcpServers["with-figma"] = {
|
|
45
42
|
command: "npx",
|
|
46
43
|
args: ["-y", "with-figma", "serve"],
|
|
47
|
-
env: {
|
|
48
|
-
OPENAI_API_KEY: "${env:OPENAI_API_KEY}",
|
|
49
|
-
},
|
|
50
44
|
};
|
|
51
45
|
fs.writeFileSync(claudePath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
52
46
|
console.log("✓ .claude/settings.json updated");
|
package/figma-plugin/ui.html
CHANGED
|
@@ -13,9 +13,8 @@
|
|
|
13
13
|
height: 100vh;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
/* ─── Header ─── */
|
|
17
16
|
.header {
|
|
18
|
-
padding:
|
|
17
|
+
padding: 10px 16px;
|
|
19
18
|
border-bottom: 1px solid var(--figma-color-border, #e5e5e5);
|
|
20
19
|
display: flex;
|
|
21
20
|
align-items: center;
|
|
@@ -25,13 +24,14 @@
|
|
|
25
24
|
width: 8px; height: 8px;
|
|
26
25
|
border-radius: 50%;
|
|
27
26
|
background: #ccc;
|
|
27
|
+
flex-shrink: 0;
|
|
28
28
|
}
|
|
29
29
|
.status-dot.connected { background: #18a957; }
|
|
30
30
|
.status-dot.connecting { background: #f5a623; animation: pulse 1s infinite; }
|
|
31
31
|
@keyframes pulse { 50% { opacity: 0.5; } }
|
|
32
|
-
.header-title { font-weight: 600; flex: 1; }
|
|
32
|
+
.header-title { font-weight: 600; flex: 1; font-size: 13px; }
|
|
33
|
+
.header-status { font-size: 11px; color: var(--figma-color-text-secondary, #999); }
|
|
33
34
|
|
|
34
|
-
/* ─── Selection Info ─── */
|
|
35
35
|
.selection-bar {
|
|
36
36
|
padding: 8px 16px;
|
|
37
37
|
background: var(--figma-color-bg-secondary, #f5f5f5);
|
|
@@ -44,96 +44,69 @@
|
|
|
44
44
|
color: var(--figma-color-text, #333);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/*
|
|
48
|
-
.
|
|
47
|
+
/* Activity Log */
|
|
48
|
+
.log-area {
|
|
49
49
|
flex: 1;
|
|
50
50
|
overflow-y: auto;
|
|
51
|
-
padding: 16px;
|
|
51
|
+
padding: 12px 16px;
|
|
52
52
|
display: flex;
|
|
53
53
|
flex-direction: column;
|
|
54
|
-
gap:
|
|
54
|
+
gap: 6px;
|
|
55
55
|
}
|
|
56
|
-
.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
.log-entry {
|
|
57
|
+
display: flex;
|
|
58
|
+
gap: 8px;
|
|
59
|
+
align-items: flex-start;
|
|
60
|
+
font-size: 12px;
|
|
60
61
|
line-height: 1.5;
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
.message.user {
|
|
64
|
-
align-self: flex-end;
|
|
65
|
-
background: #0d99ff;
|
|
66
|
-
color: #fff;
|
|
67
|
-
border-bottom-right-radius: 4px;
|
|
68
|
-
}
|
|
69
|
-
.message.assistant {
|
|
70
|
-
align-self: flex-start;
|
|
71
|
-
background: var(--figma-color-bg-secondary, #f0f0f0);
|
|
72
|
-
border-bottom-left-radius: 4px;
|
|
62
|
+
padding: 4px 0;
|
|
73
63
|
}
|
|
74
|
-
.
|
|
75
|
-
|
|
64
|
+
.log-entry .log-time {
|
|
65
|
+
color: var(--figma-color-text-tertiary, #aaa);
|
|
76
66
|
font-size: 11px;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
padding: 4px;
|
|
67
|
+
flex-shrink: 0;
|
|
68
|
+
font-variant-numeric: tabular-nums;
|
|
80
69
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
border-top: 1px solid var(--figma-color-border, #e5e5e5);
|
|
86
|
-
display: flex;
|
|
87
|
-
gap: 8px;
|
|
70
|
+
.log-entry .log-icon {
|
|
71
|
+
flex-shrink: 0;
|
|
72
|
+
width: 16px;
|
|
73
|
+
text-align: center;
|
|
88
74
|
}
|
|
89
|
-
.
|
|
90
|
-
flex: 1;
|
|
91
|
-
border: 1px solid var(--figma-color-border, #ddd);
|
|
92
|
-
border-radius: 8px;
|
|
93
|
-
padding: 8px 12px;
|
|
94
|
-
font-size: 13px;
|
|
95
|
-
font-family: inherit;
|
|
96
|
-
resize: none;
|
|
97
|
-
outline: none;
|
|
98
|
-
background: var(--figma-color-bg, #fff);
|
|
75
|
+
.log-entry .log-text {
|
|
99
76
|
color: var(--figma-color-text, #333);
|
|
100
|
-
|
|
101
|
-
max-height: 120px;
|
|
77
|
+
word-break: break-word;
|
|
102
78
|
}
|
|
103
|
-
.
|
|
104
|
-
.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
79
|
+
.log-entry.status .log-text { color: var(--figma-color-text-secondary, #888); }
|
|
80
|
+
.log-entry.action .log-text { color: #0d99ff; }
|
|
81
|
+
.log-entry.success .log-text { color: #18a957; }
|
|
82
|
+
.log-entry.error .log-text { color: #f24822; }
|
|
83
|
+
|
|
84
|
+
.empty-state {
|
|
85
|
+
flex: 1;
|
|
86
|
+
display: flex;
|
|
87
|
+
align-items: center;
|
|
88
|
+
justify-content: center;
|
|
89
|
+
color: var(--figma-color-text-tertiary, #aaa);
|
|
90
|
+
font-size: 12px;
|
|
91
|
+
text-align: center;
|
|
92
|
+
padding: 40px;
|
|
93
|
+
line-height: 1.6;
|
|
114
94
|
}
|
|
115
|
-
.input-area button:hover { background: #0b85e0; }
|
|
116
|
-
.input-area button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
117
95
|
</style>
|
|
118
96
|
</head>
|
|
119
97
|
<body>
|
|
120
98
|
<div class="header">
|
|
121
99
|
<div class="status-dot" id="statusDot"></div>
|
|
122
|
-
<span class="header-title">With Figma
|
|
123
|
-
<span
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
|
-
<div class="selection-bar" id="selectionBar">
|
|
127
|
-
No selection
|
|
100
|
+
<span class="header-title">With Figma</span>
|
|
101
|
+
<span class="header-status" id="statusText">Disconnected</span>
|
|
128
102
|
</div>
|
|
129
103
|
|
|
130
|
-
<div class="
|
|
131
|
-
<div class="message system">Select an element and describe what you want to do.</div>
|
|
132
|
-
</div>
|
|
104
|
+
<div class="selection-bar" id="selectionBar">No selection</div>
|
|
133
105
|
|
|
134
|
-
<div class="
|
|
135
|
-
<
|
|
136
|
-
|
|
106
|
+
<div class="log-area" id="logArea">
|
|
107
|
+
<div class="empty-state" id="emptyState">
|
|
108
|
+
Select elements in Figma,<br>then tell your AI agent in VS Code<br>what to do with them.
|
|
109
|
+
</div>
|
|
137
110
|
</div>
|
|
138
111
|
|
|
139
112
|
<script>
|
|
@@ -144,9 +117,8 @@
|
|
|
144
117
|
let currentPage = null;
|
|
145
118
|
let reconnectTimer = null;
|
|
146
119
|
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
const sendBtn = document.getElementById("sendBtn");
|
|
120
|
+
const logArea = document.getElementById("logArea");
|
|
121
|
+
const emptyState = document.getElementById("emptyState");
|
|
150
122
|
const statusDot = document.getElementById("statusDot");
|
|
151
123
|
const statusText = document.getElementById("statusText");
|
|
152
124
|
const selectionBar = document.getElementById("selectionBar");
|
|
@@ -156,19 +128,13 @@
|
|
|
156
128
|
function connect() {
|
|
157
129
|
if (ws && ws.readyState <= 1) return;
|
|
158
130
|
setStatus("connecting");
|
|
159
|
-
|
|
160
131
|
ws = new WebSocket(WS_URL);
|
|
161
132
|
|
|
162
133
|
ws.onopen = () => {
|
|
163
134
|
setStatus("connected");
|
|
164
|
-
|
|
165
|
-
// Send current selection state
|
|
135
|
+
addLog("status", "Connected to MCP server");
|
|
166
136
|
if (currentSelection.length > 0) {
|
|
167
|
-
ws.send(JSON.stringify({
|
|
168
|
-
type: "selection-update",
|
|
169
|
-
nodes: currentSelection,
|
|
170
|
-
page: currentPage,
|
|
171
|
-
}));
|
|
137
|
+
ws.send(JSON.stringify({ type: "selection-update", nodes: currentSelection, page: currentPage }));
|
|
172
138
|
}
|
|
173
139
|
};
|
|
174
140
|
|
|
@@ -176,9 +142,7 @@
|
|
|
176
142
|
try {
|
|
177
143
|
const msg = JSON.parse(event.data);
|
|
178
144
|
handleServerMessage(msg);
|
|
179
|
-
} catch (e) {
|
|
180
|
-
console.error("Failed to parse message:", e);
|
|
181
|
-
}
|
|
145
|
+
} catch (e) {}
|
|
182
146
|
};
|
|
183
147
|
|
|
184
148
|
ws.onclose = () => {
|
|
@@ -186,9 +150,7 @@
|
|
|
186
150
|
scheduleReconnect();
|
|
187
151
|
};
|
|
188
152
|
|
|
189
|
-
ws.onerror = () =>
|
|
190
|
-
ws.close();
|
|
191
|
-
};
|
|
153
|
+
ws.onerror = () => ws.close();
|
|
192
154
|
}
|
|
193
155
|
|
|
194
156
|
function scheduleReconnect() {
|
|
@@ -199,28 +161,23 @@
|
|
|
199
161
|
function setStatus(state) {
|
|
200
162
|
statusDot.className = "status-dot " + (state === "connected" ? "connected" : state === "connecting" ? "connecting" : "");
|
|
201
163
|
statusText.textContent = state === "connected" ? "Connected" : state === "connecting" ? "Connecting..." : "Disconnected";
|
|
202
|
-
sendBtn.disabled = state !== "connected";
|
|
203
164
|
}
|
|
204
165
|
|
|
205
|
-
// ─── Server
|
|
166
|
+
// ─── Server messages ──────────────────────────────────────────
|
|
206
167
|
|
|
207
168
|
function handleServerMessage(msg) {
|
|
208
169
|
switch (msg.type) {
|
|
209
|
-
case "
|
|
210
|
-
|
|
170
|
+
case "activity":
|
|
171
|
+
addLog(msg.level || "status", msg.text);
|
|
211
172
|
break;
|
|
212
|
-
|
|
213
173
|
case "figma-command":
|
|
214
|
-
// Relay command to Figma plugin sandbox
|
|
215
174
|
parent.postMessage({ pluginMessage: msg.command }, "*");
|
|
216
175
|
break;
|
|
217
|
-
|
|
218
176
|
case "request-selection":
|
|
219
177
|
parent.postMessage({ pluginMessage: { type: "get-selection" } }, "*");
|
|
220
178
|
break;
|
|
221
|
-
|
|
222
179
|
case "status":
|
|
223
|
-
|
|
180
|
+
addLog("status", msg.text);
|
|
224
181
|
break;
|
|
225
182
|
}
|
|
226
183
|
}
|
|
@@ -236,13 +193,8 @@
|
|
|
236
193
|
currentSelection = msg.nodes;
|
|
237
194
|
currentPage = msg.page;
|
|
238
195
|
updateSelectionUI();
|
|
239
|
-
// Forward to server
|
|
240
196
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
241
|
-
ws.send(JSON.stringify({
|
|
242
|
-
type: "selection-update",
|
|
243
|
-
nodes: msg.nodes,
|
|
244
|
-
page: msg.page,
|
|
245
|
-
}));
|
|
197
|
+
ws.send(JSON.stringify({ type: "selection-update", nodes: msg.nodes, page: msg.page }));
|
|
246
198
|
}
|
|
247
199
|
break;
|
|
248
200
|
|
|
@@ -252,7 +204,6 @@
|
|
|
252
204
|
}
|
|
253
205
|
break;
|
|
254
206
|
|
|
255
|
-
// Forward all result messages to server
|
|
256
207
|
case "node-data":
|
|
257
208
|
case "node-created":
|
|
258
209
|
case "node-modified":
|
|
@@ -265,57 +216,46 @@
|
|
|
265
216
|
}
|
|
266
217
|
};
|
|
267
218
|
|
|
268
|
-
// ─── UI
|
|
219
|
+
// ─── UI ───────────────────────────────────────────────────────
|
|
269
220
|
|
|
270
221
|
function updateSelectionUI() {
|
|
271
222
|
if (currentSelection.length === 0) {
|
|
272
223
|
selectionBar.innerHTML = "No selection";
|
|
273
224
|
} else if (currentSelection.length === 1) {
|
|
274
225
|
const n = currentSelection[0];
|
|
275
|
-
selectionBar.innerHTML =
|
|
226
|
+
selectionBar.innerHTML = '<span class="node-name">' + esc(n.name) + '</span> (' + n.type + ') — ' + Math.round(n.width) + '×' + Math.round(n.height);
|
|
276
227
|
} else {
|
|
277
|
-
selectionBar.innerHTML =
|
|
228
|
+
selectionBar.innerHTML = '<span class="node-name">' + currentSelection.length + ' elements</span> selected';
|
|
278
229
|
}
|
|
279
230
|
}
|
|
280
231
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
232
|
+
const ICONS = { status: "○", action: "▶", success: "✓", error: "✗" };
|
|
233
|
+
|
|
234
|
+
function addLog(level, text) {
|
|
235
|
+
if (emptyState) emptyState.remove();
|
|
236
|
+
const entry = document.createElement("div");
|
|
237
|
+
entry.className = "log-entry " + level;
|
|
288
238
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}));
|
|
302
|
-
input.value = "";
|
|
303
|
-
input.style.height = "auto";
|
|
239
|
+
const now = new Date();
|
|
240
|
+
const time = now.getHours().toString().padStart(2, "0") + ":" +
|
|
241
|
+
now.getMinutes().toString().padStart(2, "0") + ":" +
|
|
242
|
+
now.getSeconds().toString().padStart(2, "0");
|
|
243
|
+
|
|
244
|
+
entry.innerHTML =
|
|
245
|
+
'<span class="log-time">' + time + '</span>' +
|
|
246
|
+
'<span class="log-icon">' + (ICONS[level] || "○") + '</span>' +
|
|
247
|
+
'<span class="log-text">' + esc(text) + '</span>';
|
|
248
|
+
|
|
249
|
+
logArea.appendChild(entry);
|
|
250
|
+
logArea.scrollTop = logArea.scrollHeight;
|
|
304
251
|
}
|
|
305
252
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
input.addEventListener("input", () => {
|
|
314
|
-
input.style.height = "auto";
|
|
315
|
-
input.style.height = Math.min(input.scrollHeight, 120) + "px";
|
|
316
|
-
});
|
|
253
|
+
function esc(s) {
|
|
254
|
+
const d = document.createElement("div");
|
|
255
|
+
d.textContent = s;
|
|
256
|
+
return d.innerHTML;
|
|
257
|
+
}
|
|
317
258
|
|
|
318
|
-
// Start connection
|
|
319
259
|
connect();
|
|
320
260
|
</script>
|
|
321
261
|
</body>
|
package/mcp-server/dist/index.js
CHANGED
|
@@ -30114,238 +30114,27 @@ var StdioServerTransport = class {
|
|
|
30114
30114
|
|
|
30115
30115
|
// mcp-server/index.ts
|
|
30116
30116
|
var import_ws = require("ws");
|
|
30117
|
-
|
|
30118
|
-
// mcp-server/agent.ts
|
|
30119
|
-
var import_openai = __toESM(require("openai"));
|
|
30120
|
-
var TOOLS = [
|
|
30121
|
-
{
|
|
30122
|
-
type: "function",
|
|
30123
|
-
function: {
|
|
30124
|
-
name: "create_frame",
|
|
30125
|
-
description: "Create a new frame (artboard)",
|
|
30126
|
-
parameters: {
|
|
30127
|
-
type: "object",
|
|
30128
|
-
properties: {
|
|
30129
|
-
name: { type: "string" },
|
|
30130
|
-
width: { type: "number", default: 375 },
|
|
30131
|
-
height: { type: "number", default: 812 },
|
|
30132
|
-
x: { type: "number", default: 0 },
|
|
30133
|
-
y: { type: "number", default: 0 }
|
|
30134
|
-
},
|
|
30135
|
-
required: ["name"]
|
|
30136
|
-
}
|
|
30137
|
-
}
|
|
30138
|
-
},
|
|
30139
|
-
{
|
|
30140
|
-
type: "function",
|
|
30141
|
-
function: {
|
|
30142
|
-
name: "create_rectangle",
|
|
30143
|
-
description: "Create a rectangle, optionally inside a parent frame",
|
|
30144
|
-
parameters: {
|
|
30145
|
-
type: "object",
|
|
30146
|
-
properties: {
|
|
30147
|
-
name: { type: "string" },
|
|
30148
|
-
width: { type: "number" },
|
|
30149
|
-
height: { type: "number" },
|
|
30150
|
-
x: { type: "number", default: 0 },
|
|
30151
|
-
y: { type: "number", default: 0 },
|
|
30152
|
-
parentId: { type: "string" },
|
|
30153
|
-
cornerRadius: { type: "number" },
|
|
30154
|
-
fillColor: {
|
|
30155
|
-
type: "object",
|
|
30156
|
-
properties: { r: { type: "number" }, g: { type: "number" }, b: { type: "number" }, a: { type: "number" } }
|
|
30157
|
-
}
|
|
30158
|
-
},
|
|
30159
|
-
required: ["width", "height"]
|
|
30160
|
-
}
|
|
30161
|
-
}
|
|
30162
|
-
},
|
|
30163
|
-
{
|
|
30164
|
-
type: "function",
|
|
30165
|
-
function: {
|
|
30166
|
-
name: "create_text",
|
|
30167
|
-
description: "Create a text element",
|
|
30168
|
-
parameters: {
|
|
30169
|
-
type: "object",
|
|
30170
|
-
properties: {
|
|
30171
|
-
characters: { type: "string" },
|
|
30172
|
-
name: { type: "string" },
|
|
30173
|
-
fontSize: { type: "number", default: 16 },
|
|
30174
|
-
x: { type: "number", default: 0 },
|
|
30175
|
-
y: { type: "number", default: 0 },
|
|
30176
|
-
parentId: { type: "string" },
|
|
30177
|
-
fillColor: {
|
|
30178
|
-
type: "object",
|
|
30179
|
-
properties: { r: { type: "number" }, g: { type: "number" }, b: { type: "number" }, a: { type: "number" } }
|
|
30180
|
-
}
|
|
30181
|
-
},
|
|
30182
|
-
required: ["characters"]
|
|
30183
|
-
}
|
|
30184
|
-
}
|
|
30185
|
-
},
|
|
30186
|
-
{
|
|
30187
|
-
type: "function",
|
|
30188
|
-
function: {
|
|
30189
|
-
name: "modify_node",
|
|
30190
|
-
description: "Modify properties of an existing node",
|
|
30191
|
-
parameters: {
|
|
30192
|
-
type: "object",
|
|
30193
|
-
properties: {
|
|
30194
|
-
nodeId: { type: "string" },
|
|
30195
|
-
properties: { type: "object" }
|
|
30196
|
-
},
|
|
30197
|
-
required: ["nodeId", "properties"]
|
|
30198
|
-
}
|
|
30199
|
-
}
|
|
30200
|
-
},
|
|
30201
|
-
{
|
|
30202
|
-
type: "function",
|
|
30203
|
-
function: {
|
|
30204
|
-
name: "delete_node",
|
|
30205
|
-
description: "Delete a node",
|
|
30206
|
-
parameters: {
|
|
30207
|
-
type: "object",
|
|
30208
|
-
properties: { nodeId: { type: "string" } },
|
|
30209
|
-
required: ["nodeId"]
|
|
30210
|
-
}
|
|
30211
|
-
}
|
|
30212
|
-
},
|
|
30213
|
-
{
|
|
30214
|
-
type: "function",
|
|
30215
|
-
function: {
|
|
30216
|
-
name: "get_node",
|
|
30217
|
-
description: "Get detailed info about a node by ID",
|
|
30218
|
-
parameters: {
|
|
30219
|
-
type: "object",
|
|
30220
|
-
properties: { nodeId: { type: "string" } },
|
|
30221
|
-
required: ["nodeId"]
|
|
30222
|
-
}
|
|
30223
|
-
}
|
|
30224
|
-
}
|
|
30225
|
-
];
|
|
30226
|
-
async function executeTool(name, args, deps) {
|
|
30227
|
-
try {
|
|
30228
|
-
switch (name) {
|
|
30229
|
-
case "create_frame": {
|
|
30230
|
-
const r = await deps.sendToFigma({ type: "create-frame", ...args });
|
|
30231
|
-
return JSON.stringify(r.node);
|
|
30232
|
-
}
|
|
30233
|
-
case "create_rectangle": {
|
|
30234
|
-
const cmd = { type: "create-rectangle", ...args };
|
|
30235
|
-
if (args.fillColor) {
|
|
30236
|
-
cmd.fills = [{ type: "SOLID", color: args.fillColor }];
|
|
30237
|
-
delete cmd.fillColor;
|
|
30238
|
-
}
|
|
30239
|
-
const r = await deps.sendToFigma(cmd);
|
|
30240
|
-
return JSON.stringify(r.node);
|
|
30241
|
-
}
|
|
30242
|
-
case "create_text": {
|
|
30243
|
-
const cmd = { type: "create-text", ...args };
|
|
30244
|
-
if (args.fillColor) {
|
|
30245
|
-
cmd.fills = [{ type: "SOLID", color: args.fillColor }];
|
|
30246
|
-
delete cmd.fillColor;
|
|
30247
|
-
}
|
|
30248
|
-
const r = await deps.sendToFigma(cmd);
|
|
30249
|
-
return JSON.stringify(r.node);
|
|
30250
|
-
}
|
|
30251
|
-
case "modify_node": {
|
|
30252
|
-
const r = await deps.sendToFigma({ type: "modify-node", nodeId: args.nodeId, properties: args.properties });
|
|
30253
|
-
return JSON.stringify(r.node);
|
|
30254
|
-
}
|
|
30255
|
-
case "delete_node": {
|
|
30256
|
-
const r = await deps.sendToFigma({ type: "delete-node", nodeId: args.nodeId });
|
|
30257
|
-
return `Deleted: ${r.nodeId}`;
|
|
30258
|
-
}
|
|
30259
|
-
case "get_node": {
|
|
30260
|
-
const r = await deps.sendToFigma({ type: "get-node", nodeId: args.nodeId });
|
|
30261
|
-
return JSON.stringify(r.node);
|
|
30262
|
-
}
|
|
30263
|
-
default:
|
|
30264
|
-
return `Unknown tool: ${name}`;
|
|
30265
|
-
}
|
|
30266
|
-
} catch (err) {
|
|
30267
|
-
return `Error: ${err.message}`;
|
|
30268
|
-
}
|
|
30269
|
-
}
|
|
30270
|
-
var SYSTEM_PROMPT = `You are a Figma design agent. You receive messages from a user via a Figma plugin chat.
|
|
30271
|
-
You directly manipulate their Figma document using the provided tools.
|
|
30272
|
-
|
|
30273
|
-
Guidelines:
|
|
30274
|
-
- When creating wireframes: use a Frame as container, Rectangles for sections/buttons/cards, Text for labels
|
|
30275
|
-
- Mobile wireframe: 375\xD7812, Desktop: 1440\xD7900
|
|
30276
|
-
- Wireframe fills: light gray (r:0.95,g:0.95,b:0.95), text: dark (r:0.2,g:0.2,b:0.2)
|
|
30277
|
-
- Respond in the same language as the user
|
|
30278
|
-
- Keep responses short \u2014 the chat panel is small
|
|
30279
|
-
- When the user has selected elements, use their IDs to modify/inspect them`;
|
|
30280
|
-
var conversationHistory = [
|
|
30281
|
-
{ role: "system", content: SYSTEM_PROMPT }
|
|
30282
|
-
];
|
|
30283
|
-
async function handleChatMessage(text, selection, page, deps) {
|
|
30284
|
-
const apiKey = process.env.OPENAI_API_KEY;
|
|
30285
|
-
if (!apiKey) {
|
|
30286
|
-
deps.sendChat("OPENAI_API_KEY \uD658\uACBD\uBCC0\uC218\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
|
|
30287
|
-
return;
|
|
30288
|
-
}
|
|
30289
|
-
const openai = new import_openai.default({ apiKey });
|
|
30290
|
-
let userContent = text;
|
|
30291
|
-
if (selection.length > 0) {
|
|
30292
|
-
userContent += `
|
|
30293
|
-
|
|
30294
|
-
[Selected elements: ${JSON.stringify(selection)}]`;
|
|
30295
|
-
}
|
|
30296
|
-
if (page) {
|
|
30297
|
-
userContent += `
|
|
30298
|
-
[Page: ${page.name}]`;
|
|
30299
|
-
}
|
|
30300
|
-
conversationHistory.push({ role: "user", content: userContent });
|
|
30301
|
-
deps.sendChat("\uC791\uC5C5 \uC911...");
|
|
30302
|
-
try {
|
|
30303
|
-
for (let i = 0; i < 20; i++) {
|
|
30304
|
-
const response = await openai.chat.completions.create({
|
|
30305
|
-
model: process.env.OPENAI_MODEL || "gpt-4o",
|
|
30306
|
-
messages: conversationHistory,
|
|
30307
|
-
tools: TOOLS,
|
|
30308
|
-
tool_choice: "auto"
|
|
30309
|
-
});
|
|
30310
|
-
const msg = response.choices[0].message;
|
|
30311
|
-
conversationHistory.push(msg);
|
|
30312
|
-
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
30313
|
-
for (const tc of msg.tool_calls) {
|
|
30314
|
-
const args = JSON.parse(tc.function.arguments);
|
|
30315
|
-
console.error(`[agent] ${tc.function.name}(${JSON.stringify(args)})`);
|
|
30316
|
-
const result = await executeTool(tc.function.name, args, deps);
|
|
30317
|
-
conversationHistory.push({ role: "tool", tool_call_id: tc.id, content: result });
|
|
30318
|
-
}
|
|
30319
|
-
continue;
|
|
30320
|
-
}
|
|
30321
|
-
if (msg.content) {
|
|
30322
|
-
deps.sendChat(msg.content);
|
|
30323
|
-
}
|
|
30324
|
-
break;
|
|
30325
|
-
}
|
|
30326
|
-
} catch (err) {
|
|
30327
|
-
console.error("[agent] Error:", err.message);
|
|
30328
|
-
deps.sendChat(`Error: ${err.message}`);
|
|
30329
|
-
}
|
|
30330
|
-
}
|
|
30331
|
-
|
|
30332
|
-
// mcp-server/index.ts
|
|
30333
30117
|
var figmaSocket = null;
|
|
30334
30118
|
var currentSelection = [];
|
|
30335
30119
|
var currentPage = null;
|
|
30336
30120
|
var pages = [];
|
|
30337
30121
|
var pendingRequests = /* @__PURE__ */ new Map();
|
|
30338
30122
|
var requestCounter = 0;
|
|
30339
|
-
var chatQueue = [];
|
|
30340
30123
|
function genRequestId() {
|
|
30341
30124
|
return `req_${++requestCounter}_${Date.now()}`;
|
|
30342
30125
|
}
|
|
30126
|
+
function sendActivity(text, level = "status") {
|
|
30127
|
+
if (figmaSocket && figmaSocket.readyState === import_ws.WebSocket.OPEN) {
|
|
30128
|
+
figmaSocket.send(JSON.stringify({ type: "activity", text, level }));
|
|
30129
|
+
}
|
|
30130
|
+
console.error(`[with-figma] [${level}] ${text}`);
|
|
30131
|
+
}
|
|
30343
30132
|
var wss = new import_ws.WebSocketServer({ port: 3055 });
|
|
30344
30133
|
console.error("[with-figma] WebSocket server listening on ws://127.0.0.1:3055");
|
|
30345
30134
|
wss.on("connection", (socket) => {
|
|
30346
30135
|
console.error("[with-figma] Figma plugin connected");
|
|
30347
30136
|
figmaSocket = socket;
|
|
30348
|
-
|
|
30137
|
+
sendActivity("MCP server connected. Waiting for agent commands.");
|
|
30349
30138
|
socket.on("message", (raw) => {
|
|
30350
30139
|
try {
|
|
30351
30140
|
const msg = JSON.parse(raw.toString());
|
|
@@ -30368,24 +30157,6 @@ function handleFigmaMessage(msg) {
|
|
|
30368
30157
|
case "pages-list":
|
|
30369
30158
|
pages = msg.pages || [];
|
|
30370
30159
|
break;
|
|
30371
|
-
case "chat-message":
|
|
30372
|
-
currentSelection = msg.selection || currentSelection;
|
|
30373
|
-
currentPage = msg.page || currentPage;
|
|
30374
|
-
console.error(`[with-figma] Chat: "${msg.text}"`);
|
|
30375
|
-
handleChatMessage(
|
|
30376
|
-
msg.text,
|
|
30377
|
-
msg.selection || currentSelection,
|
|
30378
|
-
msg.page || currentPage,
|
|
30379
|
-
{
|
|
30380
|
-
sendToFigma,
|
|
30381
|
-
sendChat: (text) => {
|
|
30382
|
-
if (figmaSocket && figmaSocket.readyState === import_ws.WebSocket.OPEN) {
|
|
30383
|
-
figmaSocket.send(JSON.stringify({ type: "chat-response", text }));
|
|
30384
|
-
}
|
|
30385
|
-
}
|
|
30386
|
-
}
|
|
30387
|
-
);
|
|
30388
|
-
break;
|
|
30389
30160
|
// Results from Figma plugin operations
|
|
30390
30161
|
case "node-data":
|
|
30391
30162
|
case "node-created":
|
|
@@ -30422,7 +30193,7 @@ function sendToFigma(command, timeoutMs = 1e4) {
|
|
|
30422
30193
|
}
|
|
30423
30194
|
var server = new McpServer({
|
|
30424
30195
|
name: "with-figma",
|
|
30425
|
-
version: "0.
|
|
30196
|
+
version: "0.2.0"
|
|
30426
30197
|
});
|
|
30427
30198
|
server.resource("selection", "figma://selection", async (uri) => ({
|
|
30428
30199
|
contents: [
|
|
@@ -30446,24 +30217,25 @@ server.tool(
|
|
|
30446
30217
|
"get_selection",
|
|
30447
30218
|
"Get the currently selected elements in Figma. Returns node details including type, size, position, and properties.",
|
|
30448
30219
|
{},
|
|
30449
|
-
async () =>
|
|
30450
|
-
|
|
30451
|
-
|
|
30452
|
-
|
|
30453
|
-
|
|
30454
|
-
|
|
30455
|
-
|
|
30456
|
-
|
|
30220
|
+
async () => {
|
|
30221
|
+
sendActivity("Inspecting current selection...", "action");
|
|
30222
|
+
const result = currentSelection.length > 0 ? JSON.stringify({ page: currentPage, nodes: currentSelection }, null, 2) : "No elements are currently selected in Figma.";
|
|
30223
|
+
sendActivity(
|
|
30224
|
+
currentSelection.length > 0 ? `Found ${currentSelection.length} selected element(s)` : "No elements selected",
|
|
30225
|
+
currentSelection.length > 0 ? "success" : "status"
|
|
30226
|
+
);
|
|
30227
|
+
return { content: [{ type: "text", text: result }] };
|
|
30228
|
+
}
|
|
30457
30229
|
);
|
|
30458
30230
|
server.tool(
|
|
30459
30231
|
"get_node",
|
|
30460
30232
|
"Get detailed information about a specific Figma node by ID.",
|
|
30461
30233
|
{ nodeId: external_exports3.string().describe("The Figma node ID") },
|
|
30462
30234
|
async ({ nodeId }) => {
|
|
30235
|
+
sendActivity(`Inspecting node ${nodeId}...`, "action");
|
|
30463
30236
|
const result = await sendToFigma({ type: "get-node", nodeId });
|
|
30464
|
-
|
|
30465
|
-
|
|
30466
|
-
};
|
|
30237
|
+
sendActivity(`Inspected: ${result.node?.name || nodeId}`, "success");
|
|
30238
|
+
return { content: [{ type: "text", text: JSON.stringify(result.node, null, 2) }] };
|
|
30467
30239
|
}
|
|
30468
30240
|
);
|
|
30469
30241
|
server.tool(
|
|
@@ -30477,10 +30249,10 @@ server.tool(
|
|
|
30477
30249
|
y: external_exports3.number().default(0).describe("Y position")
|
|
30478
30250
|
},
|
|
30479
30251
|
async (params) => {
|
|
30252
|
+
sendActivity(`Creating frame "${params.name}" (${params.width}\xD7${params.height})...`, "action");
|
|
30480
30253
|
const result = await sendToFigma({ type: "create-frame", ...params });
|
|
30481
|
-
|
|
30482
|
-
|
|
30483
|
-
};
|
|
30254
|
+
sendActivity(`Created frame "${params.name}"`, "success");
|
|
30255
|
+
return { content: [{ type: "text", text: `Frame created: ${JSON.stringify(result.node, null, 2)}` }] };
|
|
30484
30256
|
}
|
|
30485
30257
|
);
|
|
30486
30258
|
server.tool(
|
|
@@ -30497,15 +30269,15 @@ server.tool(
|
|
|
30497
30269
|
fillColor: external_exports3.object({ r: external_exports3.number(), g: external_exports3.number(), b: external_exports3.number(), a: external_exports3.number().default(1) }).optional().describe("Fill color (RGBA 0-1)")
|
|
30498
30270
|
},
|
|
30499
30271
|
async (params) => {
|
|
30272
|
+
sendActivity(`Creating rectangle "${params.name}" (${params.width}\xD7${params.height})...`, "action");
|
|
30500
30273
|
const command = { type: "create-rectangle", ...params };
|
|
30501
30274
|
if (params.fillColor) {
|
|
30502
30275
|
command.fills = [{ type: "SOLID", color: params.fillColor }];
|
|
30503
30276
|
delete command.fillColor;
|
|
30504
30277
|
}
|
|
30505
30278
|
const result = await sendToFigma(command);
|
|
30506
|
-
|
|
30507
|
-
|
|
30508
|
-
};
|
|
30279
|
+
sendActivity(`Created rectangle "${params.name}"`, "success");
|
|
30280
|
+
return { content: [{ type: "text", text: `Rectangle created: ${JSON.stringify(result.node, null, 2)}` }] };
|
|
30509
30281
|
}
|
|
30510
30282
|
);
|
|
30511
30283
|
server.tool(
|
|
@@ -30521,15 +30293,15 @@ server.tool(
|
|
|
30521
30293
|
fillColor: external_exports3.object({ r: external_exports3.number(), g: external_exports3.number(), b: external_exports3.number(), a: external_exports3.number().default(1) }).optional().describe("Text color (RGBA 0-1)")
|
|
30522
30294
|
},
|
|
30523
30295
|
async (params) => {
|
|
30296
|
+
sendActivity(`Creating text "${params.characters.slice(0, 30)}${params.characters.length > 30 ? "..." : ""}"...`, "action");
|
|
30524
30297
|
const command = { type: "create-text", ...params };
|
|
30525
30298
|
if (params.fillColor) {
|
|
30526
30299
|
command.fills = [{ type: "SOLID", color: params.fillColor }];
|
|
30527
30300
|
delete command.fillColor;
|
|
30528
30301
|
}
|
|
30529
30302
|
const result = await sendToFigma(command);
|
|
30530
|
-
|
|
30531
|
-
|
|
30532
|
-
};
|
|
30303
|
+
sendActivity(`Created text "${params.name}"`, "success");
|
|
30304
|
+
return { content: [{ type: "text", text: `Text created: ${JSON.stringify(result.node, null, 2)}` }] };
|
|
30533
30305
|
}
|
|
30534
30306
|
);
|
|
30535
30307
|
server.tool(
|
|
@@ -30540,10 +30312,11 @@ server.tool(
|
|
|
30540
30312
|
properties: external_exports3.record(external_exports3.unknown()).describe("Object of properties to set (e.g. { x: 10, name: 'New Name', visible: false })")
|
|
30541
30313
|
},
|
|
30542
30314
|
async ({ nodeId, properties }) => {
|
|
30315
|
+
const propKeys = Object.keys(properties).join(", ");
|
|
30316
|
+
sendActivity(`Modifying node ${nodeId} (${propKeys})...`, "action");
|
|
30543
30317
|
const result = await sendToFigma({ type: "modify-node", nodeId, properties });
|
|
30544
|
-
|
|
30545
|
-
|
|
30546
|
-
};
|
|
30318
|
+
sendActivity(`Modified: ${result.node?.name || nodeId}`, "success");
|
|
30319
|
+
return { content: [{ type: "text", text: `Node modified: ${JSON.stringify(result.node, null, 2)}` }] };
|
|
30547
30320
|
}
|
|
30548
30321
|
);
|
|
30549
30322
|
server.tool(
|
|
@@ -30551,10 +30324,10 @@ server.tool(
|
|
|
30551
30324
|
"Delete a node from the Figma document.",
|
|
30552
30325
|
{ nodeId: external_exports3.string().describe("The node ID to delete") },
|
|
30553
30326
|
async ({ nodeId }) => {
|
|
30327
|
+
sendActivity(`Deleting node ${nodeId}...`, "action");
|
|
30554
30328
|
const result = await sendToFigma({ type: "delete-node", nodeId });
|
|
30555
|
-
|
|
30556
|
-
|
|
30557
|
-
};
|
|
30329
|
+
sendActivity(`Deleted node ${result.nodeId}`, "success");
|
|
30330
|
+
return { content: [{ type: "text", text: `Deleted node: ${result.nodeId}` }] };
|
|
30558
30331
|
}
|
|
30559
30332
|
);
|
|
30560
30333
|
server.tool(
|
|
@@ -30566,7 +30339,9 @@ server.tool(
|
|
|
30566
30339
|
scale: external_exports3.number().default(2).describe("Export scale factor")
|
|
30567
30340
|
},
|
|
30568
30341
|
async ({ nodeId, format, scale }) => {
|
|
30342
|
+
sendActivity(`Exporting node ${nodeId} as ${format}...`, "action");
|
|
30569
30343
|
const result = await sendToFigma({ type: "export-node", nodeId, format, scale });
|
|
30344
|
+
sendActivity(`Exported as ${format}`, "success");
|
|
30570
30345
|
if (format === "SVG") {
|
|
30571
30346
|
const text = new TextDecoder().decode(new Uint8Array(result.data));
|
|
30572
30347
|
return { content: [{ type: "text", text }] };
|
|
@@ -30584,37 +30359,15 @@ server.tool(
|
|
|
30584
30359
|
}
|
|
30585
30360
|
);
|
|
30586
30361
|
server.tool(
|
|
30587
|
-
"
|
|
30588
|
-
"
|
|
30589
|
-
{
|
|
30590
|
-
|
|
30591
|
-
|
|
30592
|
-
|
|
30593
|
-
|
|
30594
|
-
|
|
30595
|
-
}
|
|
30596
|
-
const messages = [...chatQueue];
|
|
30597
|
-
chatQueue.length = 0;
|
|
30598
|
-
return {
|
|
30599
|
-
content: [
|
|
30600
|
-
{
|
|
30601
|
-
type: "text",
|
|
30602
|
-
text: JSON.stringify(messages, null, 2)
|
|
30603
|
-
}
|
|
30604
|
-
]
|
|
30605
|
-
};
|
|
30606
|
-
}
|
|
30607
|
-
);
|
|
30608
|
-
server.tool(
|
|
30609
|
-
"send_chat_message",
|
|
30610
|
-
"Send a message back to the Figma plugin chat UI to communicate with the user.",
|
|
30611
|
-
{ text: external_exports3.string().describe("Message text to display in chat") },
|
|
30612
|
-
async ({ text }) => {
|
|
30613
|
-
if (figmaSocket && figmaSocket.readyState === import_ws.WebSocket.OPEN) {
|
|
30614
|
-
figmaSocket.send(JSON.stringify({ type: "chat-response", text }));
|
|
30615
|
-
return { content: [{ type: "text", text: `Message sent to Figma chat: "${text}"` }] };
|
|
30616
|
-
}
|
|
30617
|
-
return { content: [{ type: "text", text: "Error: Figma plugin is not connected" }] };
|
|
30362
|
+
"send_activity",
|
|
30363
|
+
"Send an activity/status message to the Figma plugin log panel.",
|
|
30364
|
+
{
|
|
30365
|
+
text: external_exports3.string().describe("Activity message to display"),
|
|
30366
|
+
level: external_exports3.enum(["status", "action", "success", "error"]).default("status").describe("Log level")
|
|
30367
|
+
},
|
|
30368
|
+
async ({ text, level }) => {
|
|
30369
|
+
sendActivity(text, level);
|
|
30370
|
+
return { content: [{ type: "text", text: `Activity logged: "${text}"` }] };
|
|
30618
30371
|
}
|
|
30619
30372
|
);
|
|
30620
30373
|
async function main() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "with-figma",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "MCP server that connects AI agents to Figma via WebSocket",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -15,12 +15,11 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build:plugin": "esbuild figma-plugin/code.ts --bundle --outfile=figma-plugin/dist/code.js --target=es2020",
|
|
18
|
-
"build:server": "esbuild mcp-server/index.ts --bundle --outfile=mcp-server/dist/index.js --platform=node --target=node18 --format=cjs --external:ws
|
|
18
|
+
"build:server": "esbuild mcp-server/index.ts --bundle --outfile=mcp-server/dist/index.js --platform=node --target=node18 --format=cjs --external:ws",
|
|
19
19
|
"build": "npm run build:plugin && npm run build:server",
|
|
20
20
|
"prepublishOnly": "npm run build"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"openai": "^6.34.0",
|
|
24
23
|
"ws": "^8.18.0"
|
|
25
24
|
},
|
|
26
25
|
"devDependencies": {
|