with-figma 0.1.4 → 0.1.6

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,6 +24,9 @@ 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
+ },
27
30
  };
28
31
  fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2) + "\n");
29
32
  console.log("✓ .vscode/mcp.json updated");
@@ -41,6 +44,9 @@ if (command === "init") {
41
44
  claudeJson.mcpServers["with-figma"] = {
42
45
  command: "npx",
43
46
  args: ["-y", "with-figma", "serve"],
47
+ env: {
48
+ OPENAI_API_KEY: "${env:OPENAI_API_KEY}",
49
+ },
44
50
  };
45
51
  fs.writeFileSync(claudePath, JSON.stringify(claudeJson, null, 2) + "\n");
46
52
  console.log("✓ .claude/settings.json updated");
@@ -30114,12 +30114,229 @@ 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
30117
30333
  var figmaSocket = null;
30118
30334
  var currentSelection = [];
30119
30335
  var currentPage = null;
30120
30336
  var pages = [];
30121
30337
  var pendingRequests = /* @__PURE__ */ new Map();
30122
30338
  var requestCounter = 0;
30339
+ var chatQueue = [];
30123
30340
  function genRequestId() {
30124
30341
  return `req_${++requestCounter}_${Date.now()}`;
30125
30342
  }
@@ -30154,12 +30371,20 @@ function handleFigmaMessage(msg) {
30154
30371
  case "chat-message":
30155
30372
  currentSelection = msg.selection || currentSelection;
30156
30373
  currentPage = msg.page || currentPage;
30157
- if (figmaSocket) {
30158
- figmaSocket.send(JSON.stringify({
30159
- type: "chat-response",
30160
- text: `Received: "${msg.text}". Processing via AI agent...`
30161
- }));
30162
- }
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
+ );
30163
30388
  break;
30164
30389
  // Results from Figma plugin operations
30165
30390
  case "node-data":
@@ -30358,6 +30583,28 @@ server.tool(
30358
30583
  };
30359
30584
  }
30360
30585
  );
30586
+ 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
+ );
30361
30608
  server.tool(
30362
30609
  "send_chat_message",
30363
30610
  "Send a message back to the Figma plugin chat UI to communicate with the user.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "with-figma",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "MCP server that connects AI agents to Figma via WebSocket",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -15,11 +15,12 @@
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 --external:openai",
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",
23
24
  "ws": "^8.18.0"
24
25
  },
25
26
  "devDependencies": {