zapcode-figma-mcp 1.0.3 → 1.1.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/README.md +1 -1
- package/build/index.js +136 -31
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@ Generated markdown
|
|
|
3
3
|
# Zapcode Figma MCP Server
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/zapcode-figma-mcp)
|
|
6
|
-
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
<!-- [](https://opensource.org/licenses/MIT) -->
|
|
7
7
|
|
|
8
8
|
This package provides a Model Context Protocol (MCP) server that acts as a bridge between your **Figma designs** and any **MCP-compatible AI client** (like Claude for Desktop, Open WebUI, etc.).
|
|
9
9
|
|
package/build/index.js
CHANGED
|
@@ -24,6 +24,39 @@ function parseArgs() {
|
|
|
24
24
|
}
|
|
25
25
|
return { figmaPort };
|
|
26
26
|
}
|
|
27
|
+
// --- Helper: Find Available Port ---
|
|
28
|
+
async function findAvailablePort(startPort, maxAttempts = 4) {
|
|
29
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
30
|
+
const port = startPort + i;
|
|
31
|
+
try {
|
|
32
|
+
const testServer = createHttpServer();
|
|
33
|
+
await new Promise((resolve, reject) => {
|
|
34
|
+
testServer.once('error', (err) => {
|
|
35
|
+
testServer.close();
|
|
36
|
+
if (err.code === 'EADDRINUSE') {
|
|
37
|
+
console.error(`Zapcode: Port ${port} is already in use, trying next port...`);
|
|
38
|
+
reject(err);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
reject(err);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
testServer.listen(port, () => {
|
|
45
|
+
testServer.close();
|
|
46
|
+
resolve();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
return port;
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
if (err.code !== 'EADDRINUSE' || i === maxAttempts - 1) {
|
|
53
|
+
throw new Error(`No available ports in range ${startPort}-${startPort + maxAttempts - 1}`);
|
|
54
|
+
}
|
|
55
|
+
// Continue to next port
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
throw new Error('Unexpected error in port detection');
|
|
59
|
+
}
|
|
27
60
|
// --- Socket.IO Server Logic ---
|
|
28
61
|
function startSocketIOServer(port) {
|
|
29
62
|
return new Promise((resolve, reject) => {
|
|
@@ -84,12 +117,17 @@ async function startMcpServer() {
|
|
|
84
117
|
const serverInfo = {
|
|
85
118
|
name: "zapcode-figma-mcp-server",
|
|
86
119
|
title: "Zapcode Figma MCP",
|
|
87
|
-
version: "1.0
|
|
120
|
+
version: "1.1.0",
|
|
88
121
|
};
|
|
89
122
|
const mcpServerInstance = new McpServer(serverInfo, {
|
|
90
123
|
capabilities: { tools: {} },
|
|
91
124
|
});
|
|
92
|
-
mcpServerInstance.tool("get_figma_context", "Retrieves the selected Figma frame context (HTML, CSS, prompt, image, assets). Saves any provided
|
|
125
|
+
mcpServerInstance.tool("get_figma_context", "Retrieves the selected Figma frame context (HTML, CSS, prompt, image, assets). Saves any provided assets to the local directory. Accepts an optional 'assets_path' parameter to specify where assets should be saved.", {
|
|
126
|
+
assets_path: {
|
|
127
|
+
type: "string",
|
|
128
|
+
description: "Optional absolute or relative path where assets should be saved. Defaults to 'assets' in the current working directory.",
|
|
129
|
+
},
|
|
130
|
+
}, async (params) => {
|
|
93
131
|
console.error("Zapcode: MCP Tool 'get_figma_context' invoked.");
|
|
94
132
|
if (!connectedFigmaClient?.connected) {
|
|
95
133
|
console.error("Zapcode: Error - Figma plugin not connected.");
|
|
@@ -117,9 +155,14 @@ async function startMcpServer() {
|
|
|
117
155
|
}, 30000);
|
|
118
156
|
});
|
|
119
157
|
console.error("Zapcode: Successfully received context from Figma.");
|
|
120
|
-
//
|
|
158
|
+
// Determine the base path for assets
|
|
121
159
|
const workspacePath = process.cwd();
|
|
122
|
-
const
|
|
160
|
+
const assetsBasePath = params.assets_path
|
|
161
|
+
? path.isAbsolute(params.assets_path)
|
|
162
|
+
? params.assets_path
|
|
163
|
+
: path.join(workspacePath, params.assets_path)
|
|
164
|
+
: path.join(workspacePath, "assets");
|
|
165
|
+
const assetResults = await saveAssets(context.assets, assetsBasePath);
|
|
123
166
|
const mcpContent = [];
|
|
124
167
|
if (context.image) {
|
|
125
168
|
mcpContent.push({
|
|
@@ -128,11 +171,21 @@ async function startMcpServer() {
|
|
|
128
171
|
mimeType: "image/png",
|
|
129
172
|
});
|
|
130
173
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
.
|
|
174
|
+
// Format assets information by type
|
|
175
|
+
let assetsInfo = "";
|
|
176
|
+
if (assetResults.savedFiles.length > 0) {
|
|
177
|
+
assetsInfo = `\n\nAssets (Saved to ${assetsBasePath}):\n`;
|
|
178
|
+
assetResults.assetsByType.forEach((files, type) => {
|
|
179
|
+
assetsInfo += `\n${type.toUpperCase()} Files:\n`;
|
|
180
|
+
files.forEach((filePath) => {
|
|
181
|
+
assetsInfo += `- ${path.basename(filePath)}\n`;
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
assetsInfo = "\n\nAssets: (None saved)";
|
|
187
|
+
}
|
|
188
|
+
const textDetails = `Figma Context Details:\n\nPrompt Suggestion:\n${context.prompt}\n\nTechnology Configuration:\n\`\`\`json\n${JSON.stringify(context.tech_config, null, 2)}\n\`\`\`${assetsInfo}\n\nHTML:\n\`\`\`html\n${context.HTML}\n\`\`\`\n\nCSS:\n\`\`\`css\n${context.CSS}\n\`\`\``;
|
|
136
189
|
mcpContent.push({ type: "text", text: textDetails });
|
|
137
190
|
return { content: mcpContent };
|
|
138
191
|
}
|
|
@@ -155,38 +208,64 @@ async function startMcpServer() {
|
|
|
155
208
|
console.error("Zapcode: ✅ MCP server connected and listening on stdio.");
|
|
156
209
|
}
|
|
157
210
|
// --- Utility Functions ---
|
|
158
|
-
async function
|
|
211
|
+
async function saveAssets(assets, basePath) {
|
|
159
212
|
const savedFiles = [];
|
|
160
213
|
const errors = [];
|
|
214
|
+
const assetsByType = new Map();
|
|
161
215
|
if (!Array.isArray(assets))
|
|
162
|
-
return { savedFiles, errors: ["No assets provided"] };
|
|
163
|
-
const assetsDir = path.join(basePath, "assets", "svg");
|
|
216
|
+
return { savedFiles, errors: ["No assets provided"], assetsByType };
|
|
164
217
|
try {
|
|
165
|
-
if (!fs.existsSync(assetsDir)) {
|
|
166
|
-
fs.mkdirSync(assetsDir, { recursive: true });
|
|
167
|
-
}
|
|
168
218
|
for (const asset of assets) {
|
|
169
|
-
if (asset.type
|
|
219
|
+
if (!asset.type || !asset.name || !asset.data) {
|
|
220
|
+
console.error(`Zapcode: Skipping invalid asset: ${JSON.stringify(asset)}`);
|
|
170
221
|
continue;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
:
|
|
176
|
-
|
|
222
|
+
}
|
|
223
|
+
// Create subdirectory for this asset type
|
|
224
|
+
const typeDir = path.join(basePath, asset.type);
|
|
225
|
+
if (!fs.existsSync(typeDir)) {
|
|
226
|
+
fs.mkdirSync(typeDir, { recursive: true });
|
|
227
|
+
}
|
|
228
|
+
// Sanitize filename and add appropriate extension
|
|
229
|
+
const sanitizedName = asset.name.replace(/[^a-z0-9]/gi, "_").toLowerCase();
|
|
230
|
+
const fileName = `${sanitizedName}.${asset.type}`;
|
|
231
|
+
const filePath = path.join(typeDir, fileName);
|
|
232
|
+
// Handle base64-encoded data
|
|
233
|
+
let fileContent;
|
|
234
|
+
if (asset.data.startsWith("data:")) {
|
|
235
|
+
// Extract base64 content from data URL
|
|
236
|
+
const base64Data = asset.data.split(",")[1];
|
|
237
|
+
fileContent = Buffer.from(base64Data, "base64");
|
|
238
|
+
}
|
|
239
|
+
else if (asset.type === "svg") {
|
|
240
|
+
// SVG might be plain text
|
|
241
|
+
fileContent = asset.data;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
// Assume it's already base64 string
|
|
245
|
+
fileContent = Buffer.from(asset.data, "base64");
|
|
246
|
+
}
|
|
247
|
+
fs.writeFileSync(filePath, fileContent);
|
|
177
248
|
savedFiles.push(filePath);
|
|
249
|
+
// Track assets by type
|
|
250
|
+
if (!assetsByType.has(asset.type)) {
|
|
251
|
+
assetsByType.set(asset.type, []);
|
|
252
|
+
}
|
|
253
|
+
assetsByType.get(asset.type).push(filePath);
|
|
178
254
|
}
|
|
179
255
|
}
|
|
180
256
|
catch (err) {
|
|
181
257
|
errors.push(`Failed to save assets: ${err.message}`);
|
|
182
258
|
}
|
|
183
|
-
return { savedFiles, errors };
|
|
259
|
+
return { savedFiles, errors, assetsByType };
|
|
184
260
|
}
|
|
185
261
|
// --- Main Execution Logic ---
|
|
186
262
|
async function main() {
|
|
187
263
|
const { figmaPort } = parseArgs();
|
|
188
264
|
try {
|
|
189
|
-
|
|
265
|
+
// Find an available port starting from the specified port
|
|
266
|
+
const availablePort = await findAvailablePort(figmaPort);
|
|
267
|
+
console.error(`Zapcode: ✅ Found available port ${availablePort}`);
|
|
268
|
+
await startSocketIOServer(availablePort);
|
|
190
269
|
await startMcpServer();
|
|
191
270
|
}
|
|
192
271
|
catch (error) {
|
|
@@ -195,17 +274,43 @@ async function main() {
|
|
|
195
274
|
}
|
|
196
275
|
}
|
|
197
276
|
// --- Graceful Shutdown ---
|
|
277
|
+
let isShuttingDown = false;
|
|
198
278
|
async function shutdown() {
|
|
279
|
+
if (isShuttingDown) {
|
|
280
|
+
// Second Ctrl+C forces immediate exit
|
|
281
|
+
process.exit(0);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
isShuttingDown = true;
|
|
199
285
|
console.error("\nZapcode: Gracefully shutting down...");
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
286
|
+
// Cleanup pending Figma context requests
|
|
287
|
+
figmaContextRequests.forEach((request) => {
|
|
288
|
+
request.reject(new Error("Server shutting down"));
|
|
289
|
+
});
|
|
290
|
+
figmaContextRequests.clear();
|
|
291
|
+
// Forcefully disconnect all Socket.IO clients
|
|
292
|
+
if (io) {
|
|
293
|
+
io.disconnectSockets(true);
|
|
205
294
|
}
|
|
206
|
-
|
|
207
|
-
|
|
295
|
+
if (connectedFigmaClient) {
|
|
296
|
+
connectedFigmaClient.disconnect(true);
|
|
297
|
+
connectedFigmaClient = null;
|
|
208
298
|
}
|
|
299
|
+
// Force immediate shutdown - don't wait for callbacks
|
|
300
|
+
setImmediate(() => {
|
|
301
|
+
if (io) {
|
|
302
|
+
io.close();
|
|
303
|
+
}
|
|
304
|
+
if (figmaHttpServer) {
|
|
305
|
+
figmaHttpServer.close();
|
|
306
|
+
}
|
|
307
|
+
console.error("Zapcode: Server shutdown complete.");
|
|
308
|
+
process.exit(0);
|
|
309
|
+
});
|
|
310
|
+
// Safety timeout - exit after 500ms no matter what
|
|
311
|
+
setTimeout(() => {
|
|
312
|
+
process.exit(0);
|
|
313
|
+
}, 500);
|
|
209
314
|
}
|
|
210
315
|
process.on("SIGINT", shutdown);
|
|
211
316
|
process.on("SIGTERM", shutdown);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zapcode-figma-mcp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "An MCP server that exposes Figma context via a plugin and saves assets, from Zapcode.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,4 +32,4 @@
|
|
|
32
32
|
"@types/node": "^20.12.12",
|
|
33
33
|
"typescript": "^5.4.5"
|
|
34
34
|
}
|
|
35
|
-
}
|
|
35
|
+
}
|