Skip to main content
Glama
websocket.js6.29 kB
import { WebSocketServer } from 'ws'; import { EventEmitter } from 'events'; const DEFAULT_PORT = 3055; const REQUEST_TIMEOUT = 30000; // 30 seconds const HEARTBEAT_INTERVAL = 30000; // 30 seconds export class FigmaBridge extends EventEmitter { constructor(port = DEFAULT_PORT) { super(); this.port = port; this.wss = null; this.connection = null; this.connectionState = 'disconnected'; this.documentInfo = null; this.pendingRequests = new Map(); this.requestIdCounter = 0; this.heartbeatInterval = null; } async start() { const maxPort = DEFAULT_PORT + 15; const tryPort = (port) => { return new Promise((resolve, reject) => { const wss = new WebSocketServer({ port }); wss.on('listening', () => { console.error(`[FigmaBridge] WebSocket server listening on port ${port}`); this.wss = wss; this.port = port; resolve(); }); wss.on('error', (error) => { if (error.code === 'EADDRINUSE') { console.error(`[FigmaBridge] Port ${port} in use, trying ${port + 1}`); wss.close(); if (port < maxPort) { tryPort(port + 1).then(resolve, reject); } else { reject(new Error(`Could not find available port (tried ${DEFAULT_PORT}-${maxPort})`)); } } else { reject(error); } }); wss.on('connection', (ws) => this._handleConnection(ws)); }); }; return tryPort(this.port); } _handleConnection(ws) { console.error('[FigmaBridge] Plugin connected'); // Replace existing connection if any if (this.connection) { console.error('[FigmaBridge] Replacing existing connection'); this.connection.close(1000, 'New connection'); } this.connection = ws; this.connectionState = 'connecting'; ws.on('message', (data) => this._handleMessage(data)); ws.on('close', () => this._handleClose()); ws.on('error', (error) => this._handleError(error)); } _handleMessage(data) { let message; try { message = JSON.parse(data.toString()); } catch (error) { console.error('[FigmaBridge] Failed to parse message:', error); return; } // Handle handshake from plugin if (message.type === 'handshake') { this.documentInfo = message.payload; this.connectionState = 'connected'; console.error(`[FigmaBridge] Handshake complete: ${message.payload.fileName}`); // Send handshake acknowledgment this.connection.send(JSON.stringify({ type: 'handshake_ack', payload: { serverVersion: '0.1.0', sessionId: `sess_${Date.now()}` } })); this._startHeartbeat(); this.emit('connected', this.documentInfo); return; } // Handle pong (heartbeat response) if (message.type === 'pong') { return; } // Handle command response if (message.responseTo) { const pending = this.pendingRequests.get(message.responseTo); if (pending) { this.pendingRequests.delete(message.responseTo); clearTimeout(pending.timeout); if (message.payload?.error) { pending.reject(new BridgeError( message.payload.error.code || 'UNKNOWN_ERROR', message.payload.error.message || 'Unknown error', message.payload.error.details )); } else { pending.resolve(message.payload); } } } } _handleClose() { console.error('[FigmaBridge] Connection closed'); this.connectionState = 'disconnected'; this.connection = null; this.documentInfo = null; this._stopHeartbeat(); // Reject all pending requests for (const [id, pending] of this.pendingRequests) { clearTimeout(pending.timeout); pending.reject(new BridgeError('CONNECTION_CLOSED', 'Connection closed')); } this.pendingRequests.clear(); this.emit('disconnected'); } _handleError(error) { console.error('[FigmaBridge] WebSocket error:', error.message); this.emit('error', error); } _startHeartbeat() { this._stopHeartbeat(); this.heartbeatInterval = setInterval(() => { if (this.connection && this.connectionState === 'connected') { this.connection.send(JSON.stringify({ type: 'ping', timestamp: Date.now() })); } }, HEARTBEAT_INTERVAL); } _stopHeartbeat() { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; } } /** * Send a command to the Figma plugin and wait for response * @param {string} command - Command name * @param {object} payload - Command payload * @returns {Promise<object>} Response payload */ async sendCommand(command, payload = {}) { if (this.connectionState !== 'connected') { throw new BridgeError( 'NOT_CONNECTED', 'Figma plugin is not connected. Please open Figma and run the Claude Bridge plugin.' ); } const requestId = `req_${++this.requestIdCounter}`; return new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.pendingRequests.delete(requestId); reject(new BridgeError('TIMEOUT', `Command "${command}" timed out after ${REQUEST_TIMEOUT}ms`)); }, REQUEST_TIMEOUT); this.pendingRequests.set(requestId, { resolve, reject, timeout }); this.connection.send(JSON.stringify({ requestId, command, payload })); }); } isConnected() { return this.connectionState === 'connected'; } getDocumentInfo() { return this.documentInfo; } async stop() { this._stopHeartbeat(); if (this.connection) { this.connection.close(1000, 'Server shutdown'); } if (this.wss) { return new Promise((resolve) => { this.wss.close(() => { console.error('[FigmaBridge] WebSocket server stopped'); resolve(); }); }); } } } export class BridgeError extends Error { constructor(code, message, details = null) { super(message); this.name = 'BridgeError'; this.code = code; this.details = details; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/magic-spells/figma-mcp-bridge'

If you have feedback or need assistance with the MCP directory API, please join our Discord server