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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/build/index.js +136 -31
  3. 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
  [![npm version](https://img.shields.io/npm/v/zapcode-figma-mcp.svg)](https://www.npmjs.com/package/zapcode-figma-mcp)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ <!-- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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.3", // It's good practice to increment version on significant changes
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 SVG assets to the local 'assets/svg' directory.", {}, async () => {
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
- // Save assets to the current working directory
158
+ // Determine the base path for assets
121
159
  const workspacePath = process.cwd();
122
- const assetResults = await saveSvgAssets(context.assets, workspacePath);
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
- 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\`\`\`\n\nSVG Assets ${assetResults.savedFiles.length > 0
132
- ? "(Saved to " + path.join(workspacePath, "assets", "svg") + "):"
133
- : "(None saved)"}\n${assetResults.savedFiles
134
- .map((p) => `- ${path.basename(p)}`)
135
- .join("\n")}\n\nHTML:\n\`\`\`html\n${context.HTML}\n\`\`\`\n\nCSS:\n\`\`\`css\n${context.CSS}\n\`\`\``;
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 saveSvgAssets(assets, basePath) {
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 !== "svg" || !asset.name || !asset.data)
219
+ if (!asset.type || !asset.name || !asset.data) {
220
+ console.error(`Zapcode: Skipping invalid asset: ${JSON.stringify(asset)}`);
170
221
  continue;
171
- const fileName = asset.name.replace(/[^a-z0-9]/gi, "_").toLowerCase() + ".svg";
172
- const filePath = path.join(assetsDir, fileName);
173
- const svgContent = asset.data.startsWith("data:image/svg+xml;base64,")
174
- ? Buffer.from(asset.data.split(",")[1], "base64").toString()
175
- : asset.data;
176
- fs.writeFileSync(filePath, svgContent);
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
- await startSocketIOServer(figmaPort);
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
- if (figmaHttpServer) {
201
- figmaHttpServer.close(() => {
202
- console.error("Zapcode: Figma HTTP server closed.");
203
- process.exit(0);
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
- else {
207
- process.exit(0);
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",
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
+ }