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 +6 -0
- package/mcp-server/dist/index.js +253 -6
- package/package.json +3 -2
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");
|
package/mcp-server/dist/index.js
CHANGED
|
@@ -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
|
-
|
|
30158
|
-
|
|
30159
|
-
|
|
30160
|
-
|
|
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.
|
|
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": {
|