Skip to main content

Overview

Talk to Figma MCP uses a three-component pipeline architecture to enable seamless communication between AI agents (like Claude Code or Cursor) and Figma designs. This architecture ensures reliable, real-time interaction with Figma while maintaining the MCP protocol standard.

Architecture Diagram

Claude Code / Cursor ←(stdio)→ MCP Server ←(WebSocket)→ WebSocket Relay ←(WebSocket)→ Figma Plugin
Each component plays a specific role in the pipeline:

MCP Server

Protocol handler that exposes 50+ tools to AI agents

WebSocket Relay

Message router with channel-based isolation

Figma Plugin

In-canvas executor that modifies designs

MCP Server

The MCP server (src/talk_to_figma_mcp/server.ts) is the protocol adapter between AI agents and Figma. It implements the Model Context Protocol using @modelcontextprotocol/sdk.

Key Responsibilities

  • Tool Registration: Exposes 50+ tools for creating shapes, modifying text, managing layouts, exporting images, and more
  • Prompt Strategies: Provides AI prompts for design best practices and workflows
  • Request Management: Tracks all outgoing requests with UUIDs in a pendingRequests Map
  • Timeout Handling: Manages 30-second timeouts with progress update support
  • Protocol Translation: Converts MCP tool calls into WebSocket messages

Communication Flow

1

AI Agent sends tool request

The agent calls a tool like create_frame() via stdio
2

Server validates parameters

Zod schemas validate all input parameters
3

Server generates UUID

A unique request ID is created using uuidv4()
4

Server sends WebSocket message

The request is forwarded to the WebSocket relay
5

Server awaits response

The promise is stored in pendingRequests with timeout callbacks

Code Example: Request Lifecycle

server.ts
function sendCommandToFigma(
  command: FigmaCommand,
  params: unknown = {},
  timeoutMs: number = 30000
): Promise<unknown> {
  return new Promise((resolve, reject) => {
    const id = uuidv4();
    const request = {
      id,
      type: "message",
      channel: currentChannel,
      message: {
        id,
        command,
        params: {
          ...params,
          commandId: id,
        },
      },
    };

    // Set timeout for request
    const timeout = setTimeout(() => {
      if (pendingRequests.has(id)) {
        pendingRequests.delete(id);
        reject(new Error('Request to Figma timed out'));
      }
    }, timeoutMs);

    // Store callbacks for later resolution
    pendingRequests.set(id, {
      resolve,
      reject,
      timeout,
      lastActivity: Date.now()
    });

    ws.send(JSON.stringify(request));
  });
}

Transport Layer

The MCP server uses stdio transport to communicate with AI agents:
server.ts
// All logs go to stderr to avoid interfering with MCP protocol
const logger = {
  info: (message: string) => process.stderr.write(`[INFO] ${message}\n`),
  debug: (message: string) => process.stderr.write(`[DEBUG] ${message}\n`),
  error: (message: string) => process.stderr.write(`[ERROR] ${message}\n`),
};

// Start server with stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);
stdout is reserved for MCP protocol messages. All logging must go to stderr to prevent protocol corruption.

WebSocket Relay

The WebSocket relay (src/socket.ts) is a lightweight Bun-powered router that enables channel-based message isolation between multiple MCP servers and Figma plugins.

Why a Relay?

The relay solves a critical architectural challenge:
  • MCP servers communicate via stdio and need WebSocket connectivity
  • Multiple users may run servers simultaneously on the same machine
  • Channel isolation prevents messages from crossing between different sessions

Channel-Based Routing

Clients must join a channel before sending messages:
socket.ts
if (data.type === "join") {
  const channelName = data.channel;
  
  // Create channel if it doesn't exist
  if (!channels.has(channelName)) {
    channels.set(channelName, new Set());
  }
  
  // Add client to channel
  const channelClients = channels.get(channelName)!;
  channelClients.add(ws);
  
  console.log(`Client joined channel "${channelName}" (${channelClients.size} total clients)`);
}
Messages are broadcast only within the same channel:
socket.ts
if (data.type === "message") {
  const channelClients = channels.get(channelName);
  
  // Broadcast to all OTHER clients (not the sender)
  channelClients.forEach((client) => {
    if (client !== ws && client.readyState === WebSocket.OPEN) {
      client.send(JSON.stringify({
        type: "broadcast",
        message: data.message,
        sender: "peer",
        channel: channelName
      }));
    }
  });
}

Server Configuration

The relay runs on port 3055 by default (configurable via PORT env):
socket.ts
const server = Bun.serve({
  port: 3055,
  websocket: {
    open: handleConnection,
    message: handleMessage,
    close: handleClose
  }
});

console.log(`WebSocket server running on port ${server.port}`);
Start the relay with bun socket before connecting MCP servers or Figma plugins.

Figma Plugin

The Figma plugin (src/cursor_mcp_plugin/) runs inside Figma and executes design modifications in response to commands from the MCP server.

Plugin Components

The plugin main thread handles 30+ commands via a dispatcher pattern. It processes requests like create_frame, set_text_content, export_node_as_image, etc.No build step requiredcode.js is written directly as the runtime artifact.
The plugin UI provides WebSocket connection management, allowing users to:
  • Connect to the relay server
  • Join channels
  • Monitor connection status
Declares required permissions:
  • dynamic-page access for document manipulation
  • localhost network access for WebSocket connections

Command Execution

The plugin receives commands via WebSocket and executes them using Figma’s Plugin API:
code.js
// Example: Creating a frame
function createFrame(params) {
  const frame = figma.createFrame();
  frame.x = params.x;
  frame.y = params.y;
  frame.resize(params.width, params.height);
  frame.name = params.name || "Frame";
  
  if (params.fillColor) {
    frame.fills = [{
      type: 'SOLID',
      color: {
        r: params.fillColor.r,
        g: params.fillColor.g,
        b: params.fillColor.b
      },
      opacity: params.fillColor.a || 1
    }];
  }
  
  return {
    id: frame.id,
    name: frame.name
  };
}

Message Flow Example

Here’s how a complete request flows through the pipeline:
1

AI Agent calls tool

// User asks: "Create a blue frame"
create_frame({
  x: 100,
  y: 100,
  width: 200,
  height: 200,
  fillColor: { r: 0, g: 0.5, b: 1, a: 1 }
})
2

MCP Server receives via stdio

Server validates parameters with Zod schema, generates UUID abc-123
3

MCP Server sends to relay

{
  "id": "abc-123",
  "type": "message",
  "channel": "my-channel",
  "message": {
    "id": "abc-123",
    "command": "create_frame",
    "params": {
      "x": 100,
      "y": 100,
      "width": 200,
      "height": 200,
      "fillColor": { "r": 0, "g": 0.5, "b": 1, "a": 1 }
    }
  }
}
4

Relay broadcasts to channel

Relay forwards message to all clients in my-channel except the sender
5

Figma Plugin receives and executes

Plugin creates the frame using Figma API and sends response
6

Response flows back

{
  "id": "abc-123",
  "result": {
    "id": "frame-456",
    "name": "Frame"
  }
}
7

MCP Server resolves promise

Server finds pending request by ID, clears timeout, resolves promise
8

AI Agent receives result

Created frame "Frame" with ID: frame-456

Key Design Patterns

Request Tracking

All requests are tracked in a Map with timeout and promise callbacks:
server.ts
const pendingRequests = new Map<string, {
  resolve: (value: unknown) => void;
  reject: (reason: unknown) => void;
  timeout: ReturnType<typeof setTimeout>;
  lastActivity: number;
}>();

Progress Updates

For long-running operations (like scanning 100+ nodes), the plugin sends progress updates that reset the inactivity timer:
server.ts
if (json.type === 'progress_update') {
  const request = pendingRequests.get(requestId)!;
  
  // Update last activity timestamp
  request.lastActivity = Date.now();
  
  // Reset the timeout
  clearTimeout(request.timeout);
  request.timeout = setTimeout(() => {
    // 60 second timeout for inactivity
    request.reject(new Error('Request timed out'));
  }, 60000);
}

Auto-Reconnection

The MCP server automatically reconnects to the relay if the connection drops:
server.ts
ws.on('close', () => {
  logger.info('Disconnected from relay');
  
  // Reject all pending requests
  for (const [id, request] of pendingRequests.entries()) {
    clearTimeout(request.timeout);
    request.reject(new Error("Connection closed"));
    pendingRequests.delete(id);
  }
  
  // Reconnect after 2 seconds
  setTimeout(() => connectToFigma(port), 2000);
});

Next Steps

Channel Communication

Learn how channel-based isolation works

WebSocket Relay

Deep dive into relay server details

Build docs developers (and LLMs) love