## Problem MCP clients like **Perplexity** and **ChatGPT** spawn a **fresh MCP server process for each tool call** (unlike Cursor/Claude Code which keep the process alive). This causes the connection to fail almost every time due to race conditions in the WebSocket + channel join flow. ### Error messages users see: - `Error joining channel: Not connected to Figma` - `Not connected to Figma. Connection timed out.` ## Root Cause Three related race conditions: ### 1. `joinChannel()` has a premature connection check `joinChannel()` immediately throws "Not connected to Figma" if `ws.readyState !== OPEN`, **before** `sendCommandToFigma()` can use its built-in connection retry loop. ```js // Current code — fails for ephemeral clients async function joinChannel(channelName) { if (!ws || ws.readyState !== WebSocket.OPEN) { throw new Error("Not connected to Figma"); // Instant failure! } await sendCommandToFigma("join", { channel: channelName }); } ``` ### 2. Polling loop can detect OPEN before `ws.on('open')` fires `sendCommandToFigma()` polls `ws.readyState` every 100ms. When the WebSocket transitions to OPEN, the polling `setInterval` can detect it **before** the `ws.on('open')` event handler runs. This means `channelReadyPromise` is still `null`, so the command is sent without waiting for the channel join. ### 3. No channel persistence across processes Ephemeral clients lose all state (including `currentChannel`) when the process dies after each tool call. There's no way to pre-configure the channel. ## Proposed Fixes ### Fix 1: Remove premature check from `joinChannel()` Let `sendCommandToFigma` handle the connection wait — it already has a retry loop. ```js async function joinChannel(channelName) { await sendCommandToFigma("join", { channel: channelName }); } ``` ### Fix 2: Add `FIGMA_CHANNEL` environment variable Allow pre-configuring the channel so each fresh process auto-joins on startup: ```js let currentChannel = process.env.FIGMA_CHANNEL || null; ``` ### Fix 3: Auto-join safety net in `sendCommandToFigma()` After confirming the WebSocket is open, check if a channel join is still needed: ```js // After WS is confirmed open and channelReadyPromise is awaited: if (requiresChannel && channel && !currentChannel) { await joinChannel(channel); } ``` ### Fix 4: Increase connection timeout 6 seconds is tight for ephemeral processes. 10 seconds gives more breathing room: ```js const maxWait = 10000; // was 6000 ``` ## Configuration for Ephemeral Clients With these fixes, Perplexity/ChatGPT users configure their MCP like this: ```json { "command": "node", "args": ["path/to/dist/server.js"], "env": { "FIGMA_CHANNEL": "<channel-code-from-figma-plugin>" } } ``` ## Why Cursor works but Perplexity doesn't | Behavior | Cursor / Claude Code | Perplexity / ChatGPT | |----------|---------------------|---------------------| | Process lifecycle | Persistent | Ephemeral (new per call) | | WebSocket | Connects once | Must reconnect every call | | Channel state | Joined once, remembered | Lost after each call | | Time before first tool call | Seconds to minutes | Milliseconds | | Race condition impact | None | Breaks every time | ## Tested - Perplexity with GPT-5.4 Thinking model — confirmed working after fixes - Claude Code — still works (no regression) ## Additional Ideas for Future Improvement 1. **`execute_figma_code` tool** — A single tool that accepts arbitrary Plugin API JavaScript and executes it inside Figma (like MCP Magic's `use_figma`). Would make the 40+ fixed tools extensible without code changes. 2. **Figma REST API tools** — Add tools that call `api.figma.com` directly for comments, reactions, and file metadata (no plugin needed). 3. **Connection diagnostics tool** — Help users troubleshoot connection issues from within the AI chat. 4. **Promise-based connection wait** — Replace the polling `setInterval` with a proper Promise that resolves from the `ws.on('open')` handler directly.