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 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");
@@ -13,9 +13,8 @@
13
13
  height: 100vh;
14
14
  }
15
15
 
16
- /* ─── Header ─── */
17
16
  .header {
18
- padding: 12px 16px;
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
- /* ─── Chat Area ─── */
48
- .chat-area {
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: 12px;
54
+ gap: 6px;
55
55
  }
56
- .message {
57
- max-width: 85%;
58
- padding: 10px 14px;
59
- border-radius: 12px;
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
- word-break: break-word;
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
- .message.system {
75
- align-self: center;
64
+ .log-entry .log-time {
65
+ color: var(--figma-color-text-tertiary, #aaa);
76
66
  font-size: 11px;
77
- color: var(--figma-color-text-tertiary, #999);
78
- background: none;
79
- padding: 4px;
67
+ flex-shrink: 0;
68
+ font-variant-numeric: tabular-nums;
80
69
  }
81
-
82
- /* ─── Input Area ─── */
83
- .input-area {
84
- padding: 12px 16px;
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
- .input-area textarea {
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
- min-height: 40px;
101
- max-height: 120px;
77
+ word-break: break-word;
102
78
  }
103
- .input-area textarea:focus { border-color: #0d99ff; }
104
- .input-area button {
105
- background: #0d99ff;
106
- color: #fff;
107
- border: none;
108
- border-radius: 8px;
109
- padding: 8px 16px;
110
- font-size: 13px;
111
- font-weight: 600;
112
- cursor: pointer;
113
- align-self: flex-end;
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 AI</span>
123
- <span id="statusText" style="font-size:11px; color:#999">Disconnected</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="chat-area" id="chatArea">
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="input-area">
135
- <textarea id="input" placeholder="이 요소를 어떻게 작업할까요?" rows="1"></textarea>
136
- <button id="sendBtn" disabled>Send</button>
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 chatArea = document.getElementById("chatArea");
148
- const input = document.getElementById("input");
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
- addMessage("system", "Connected to AI server.");
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 message handling ──────────────────────────────────
166
+ // ─── Server messages ──────────────────────────────────────────
206
167
 
207
168
  function handleServerMessage(msg) {
208
169
  switch (msg.type) {
209
- case "chat-response":
210
- addMessage("assistant", msg.text);
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
- addMessage("system", msg.text);
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 Helpers ───────────────────────────────────────────────
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 = `<span class="node-name">${n.name}</span> (${n.type}) — ${Math.round(n.width)}×${Math.round(n.height)}`;
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 = `<span class="node-name">${currentSelection.length} elements</span> selected`;
228
+ selectionBar.innerHTML = '<span class="node-name">' + currentSelection.length + ' elements</span> selected';
278
229
  }
279
230
  }
280
231
 
281
- function addMessage(role, text) {
282
- const div = document.createElement("div");
283
- div.className = "message " + role;
284
- div.textContent = text;
285
- chatArea.appendChild(div);
286
- chatArea.scrollTop = chatArea.scrollHeight;
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
- // ─── User Input ───────────────────────────────────────────────
290
-
291
- function sendMessage() {
292
- const text = input.value.trim();
293
- if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
294
-
295
- addMessage("user", text);
296
- ws.send(JSON.stringify({
297
- type: "chat-message",
298
- text,
299
- selection: currentSelection,
300
- page: currentPage,
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
- sendBtn.addEventListener("click", sendMessage);
307
- input.addEventListener("keydown", (e) => {
308
- if (e.key === "Enter" && !e.shiftKey) {
309
- e.preventDefault();
310
- sendMessage();
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>
@@ -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
- socket.send(JSON.stringify({ type: "status", text: "MCP server connected. Ready for commands." }));
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.1.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
- content: [
30451
- {
30452
- type: "text",
30453
- text: currentSelection.length > 0 ? JSON.stringify({ page: currentPage, nodes: currentSelection }, null, 2) : "No elements are currently selected in Figma."
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
- return {
30465
- content: [{ type: "text", text: JSON.stringify(result.node, null, 2) }]
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
- return {
30482
- content: [{ type: "text", text: `Frame created: ${JSON.stringify(result.node, null, 2)}` }]
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
- return {
30507
- content: [{ type: "text", text: `Rectangle created: ${JSON.stringify(result.node, null, 2)}` }]
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
- return {
30531
- content: [{ type: "text", text: `Text created: ${JSON.stringify(result.node, null, 2)}` }]
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
- return {
30545
- content: [{ type: "text", text: `Node modified: ${JSON.stringify(result.node, null, 2)}` }]
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
- return {
30556
- content: [{ type: "text", text: `Deleted node: ${result.nodeId}` }]
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
- "get_chat_messages",
30588
- "Get pending chat messages from the Figma plugin. Users send design requests via the Figma chat UI. Call this tool to check for new messages, then use other Figma tools to fulfill the requests. After processing, the queue is cleared.",
30589
- {},
30590
- async () => {
30591
- if (chatQueue.length === 0) {
30592
- return {
30593
- content: [{ type: "text", text: "No pending messages from Figma." }]
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.1.6",
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 --external:openai",
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": {