grab / grab/TalkToFigmaDesktop

Fix: Support ephemeral MCP clients (Perplexity, ChatGPT) — race condition on startup

Open
#6 2 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
51
Forks
10
PR merge metrics
No merged PRs in 30d

Description

## 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": ""
}
}
```

## 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.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.