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);
});