Skip to main content
Glama
socket.ts11.6 kB
import { WebSocketServer, WebSocket } from "ws"; import { createServer, IncomingMessage, ServerResponse } from "http"; // Enhanced logging system const logger = { info: (message: string, ...args: any[]) => { console.log(`[INFO] ${message}`, ...args); }, debug: (message: string, ...args: any[]) => { console.log(`[DEBUG] ${message}`, ...args); }, warn: (message: string, ...args: any[]) => { console.warn(`[WARN] ${message}`, ...args); }, error: (message: string, ...args: any[]) => { console.error(`[ERROR] ${message}`, ...args); } }; // Extended WebSocket type with custom data interface ExtendedWebSocket extends WebSocket { data?: { clientId: string; }; isAlive?: boolean; } // Store clients by channel const channels = new Map<string, Set<ExtendedWebSocket>>(); // Keep track of channel statistics const stats = { totalConnections: 0, activeConnections: 0, messagesSent: 0, messagesReceived: 0, errors: 0 }; const PORT = 3055; // Create HTTP server for status endpoint const httpServer = createServer((req: IncomingMessage, res: ServerResponse) => { const url = new URL(req.url || "/", `http://localhost:${PORT}`); // CORS headers res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); // Handle CORS preflight if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; } // Handle status endpoint if (url.pathname === "/status") { res.setHeader("Content-Type", "application/json"); res.writeHead(200); res.end(JSON.stringify({ status: "running", uptime: process.uptime(), stats })); return; } // Default response res.setHeader("Content-Type", "text/plain"); res.writeHead(200); res.end("Claude to Figma WebSocket server running. Try connecting with a WebSocket client."); }); // Create WebSocket server const wss = new WebSocketServer({ server: httpServer }); function handleConnection(ws: ExtendedWebSocket) { // Track connection statistics stats.totalConnections++; stats.activeConnections++; // Assign a unique client ID for better tracking const clientId = `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; ws.data = { clientId }; ws.isAlive = true; logger.info(`New client connected: ${clientId}`); // Send welcome message to the new client try { ws.send(JSON.stringify({ type: "system", message: "Please join a channel to start communicating with Figma", })); } catch (error) { logger.error(`Failed to send welcome message to client ${clientId}:`, error); stats.errors++; } // Handle pong for heartbeat ws.on("pong", () => { ws.isAlive = true; }); // Handle messages ws.on("message", (message: Buffer | string) => { try { stats.messagesReceived++; const clientId = ws.data?.clientId || "unknown"; const messageStr = typeof message === 'string' ? message : message.toString(); logger.debug(`Received message from client ${clientId}:`, messageStr.substring(0, 200)); const data = JSON.parse(messageStr); if (data.type === "join") { const channelName = data.channel; if (!channelName || typeof channelName !== "string") { logger.warn(`Client ${clientId} attempted to join without a valid channel name`); ws.send(JSON.stringify({ type: "error", message: "Channel name is required" })); stats.messagesSent++; return; } // Create channel if it doesn't exist if (!channels.has(channelName)) { logger.info(`Creating new channel: ${channelName}`); channels.set(channelName, new Set()); } // Add client to channel const channelClients = channels.get(channelName)!; channelClients.add(ws); logger.info(`Client ${clientId} joined channel: ${channelName}`); // Notify client they joined successfully try { ws.send(JSON.stringify({ type: "system", message: `Joined channel: ${channelName}`, channel: channelName })); stats.messagesSent++; ws.send(JSON.stringify({ type: "system", message: { id: data.id, result: "Connected to channel: " + channelName, }, channel: channelName })); stats.messagesSent++; logger.debug(`Connection confirmation sent to client ${clientId} for channel ${channelName}`); } catch (error) { logger.error(`Failed to send join confirmation to client ${clientId}:`, error); stats.errors++; } // Notify other clients in channel try { let notificationCount = 0; channelClients.forEach((client) => { if (client !== ws && client.readyState === WebSocket.OPEN) { client.send(JSON.stringify({ type: "system", message: "A new client has joined the channel", channel: channelName })); stats.messagesSent++; notificationCount++; } }); if (notificationCount > 0) { logger.debug(`Notified ${notificationCount} other clients in channel ${channelName}`); } } catch (error) { logger.error(`Error notifying channel about new client:`, error); stats.errors++; } return; } // Handle regular messages if (data.type === "message") { const channelName = data.channel; if (!channelName || typeof channelName !== "string") { logger.warn(`Client ${clientId} sent message without a valid channel name`); ws.send(JSON.stringify({ type: "error", message: "Channel name is required" })); stats.messagesSent++; return; } const channelClients = channels.get(channelName); if (!channelClients || !channelClients.has(ws)) { logger.warn(`Client ${clientId} attempted to send to channel ${channelName} without joining first`); ws.send(JSON.stringify({ type: "error", message: "You must join the channel first" })); stats.messagesSent++; return; } // Broadcast to all OTHER clients in the channel (not the sender) // This ensures commands go to Figma and responses go back to MCP try { let broadcastCount = 0; channelClients.forEach((client) => { if (client !== ws && client.readyState === WebSocket.OPEN) { logger.debug(`Forwarding message to client in channel ${channelName}`); // Forward the original message structure to preserve format client.send(JSON.stringify({ type: "broadcast", channel: channelName, message: data.message })); stats.messagesSent++; broadcastCount++; } }); logger.info(`Forwarded message to ${broadcastCount} clients in channel ${channelName}`); } catch (error) { logger.error(`Error forwarding message to channel ${channelName}:`, error); stats.errors++; } } // Handle progress updates if (data.type === "progress_update") { const channelName = data.channel; if (!channelName || typeof channelName !== "string") { logger.warn(`Client ${clientId} sent progress update without a valid channel name`); return; } const channelClients = channels.get(channelName); if (!channelClients) { logger.warn(`Progress update for non-existent channel: ${channelName}`); return; } logger.debug(`Progress update for command ${data.id} in channel ${channelName}: ${data.message?.data?.status || 'unknown'} - ${data.message?.data?.progress || 0}%`); // Broadcast progress update to all clients in the channel try { channelClients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(data)); stats.messagesSent++; } }); } catch (error) { logger.error(`Error broadcasting progress update:`, error); stats.errors++; } } } catch (err) { stats.errors++; logger.error("Error handling message:", err); try { ws.send(JSON.stringify({ type: "error", message: "Error processing your message: " + (err instanceof Error ? err.message : String(err)) })); stats.messagesSent++; } catch (sendError) { logger.error("Failed to send error message to client:", sendError); } } }); // Handle close ws.on("close", (code: number, reason: Buffer) => { const clientId = ws.data?.clientId || "unknown"; logger.info(`WebSocket closed for client ${clientId}: Code ${code}, Reason: ${reason?.toString() || 'No reason provided'}`); // Remove client from their channel channels.forEach((clients, channelName) => { if (clients.delete(ws)) { logger.debug(`Removed client ${clientId} from channel ${channelName} due to connection close`); // Notify other clients in channel clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { try { client.send(JSON.stringify({ type: "system", message: "A client has left the channel", channel: channelName })); stats.messagesSent++; } catch (error) { logger.error(`Error notifying channel about client disconnect:`, error); } } }); } }); stats.activeConnections--; }); // Handle errors ws.on("error", (error: Error) => { const clientId = ws.data?.clientId || "unknown"; logger.error(`WebSocket error for client ${clientId}:`, error); stats.errors++; }); } // Set up connection handler wss.on("connection", handleConnection); // Heartbeat to detect dead connections const heartbeatInterval = setInterval(() => { wss.clients.forEach((ws) => { const extWs = ws as ExtendedWebSocket; if (extWs.isAlive === false) { logger.debug(`Terminating dead connection: ${extWs.data?.clientId}`); return extWs.terminate(); } extWs.isAlive = false; extWs.ping(); }); }, 30000); wss.on("close", () => { clearInterval(heartbeatInterval); }); // Start server httpServer.listen(PORT, "0.0.0.0", () => { logger.info(`Claude to Figma WebSocket server running on port ${PORT}`); logger.info(`Status endpoint available at http://localhost:${PORT}/status`); }); // Print server stats every 5 minutes setInterval(() => { logger.info("Server stats:", { channels: channels.size, ...stats }); }, 5 * 60 * 1000); // Graceful shutdown process.on("SIGTERM", () => { logger.info("SIGTERM received, shutting down gracefully"); wss.close(); httpServer.close(); process.exit(0); }); process.on("SIGINT", () => { logger.info("SIGINT received, shutting down gracefully"); wss.close(); httpServer.close(); process.exit(0); });

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/agenisea/cc-fig-mcp'

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