UNPKG

spex-mcp

Version:

MCP server for Figma SpeX plugin and Cursor AI integration

250 lines (223 loc) 10 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import { createRequire } from 'module'; import { FigmaPluginManager } from "./figma-functions.js"; import { MCPToolsManager } from "./mcp-tools.js"; // Read version from package.json const require = createRequire(import.meta.url); const packageJson = require('../../package.json'); /** * Main SpeX MCP Server that orchestrates both Figma SpeX plugin communication * and MCP tools for Cursor AI integration */ class SpeXMCPServer { constructor(port = 8080, force = false, sessionId = null, clientType = null, serverUrl = null) { // Initialize managers this.figmaManager = new FigmaPluginManager(port, force); this.mcpToolsManager = new MCPToolsManager(this.figmaManager); this.sessionId = sessionId; this.clientType = clientType; this.serverUrl = serverUrl; // Initialize MCP server this.server = new Server( { name: packageJson.name, version: packageJson.version, }, { capabilities: { tools: {}, }, } ); this.setupMCPHandlers(); } /** * Setup MCP request handlers for Cursor AI integration */ setupMCPHandlers() { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: this.mcpToolsManager.getToolsList(), }; }); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; // Pass session info to tool handler for session matching return await this.mcpToolsManager.handleToolCall(name, args, this.sessionId); }); } /** * Start the server with appropriate components based on client type */ async run() { try { // For MCP client mode, connect to existing WebSocket server and start MCP server if (this.clientType === 'mcp-client') { // Connect to existing WebSocket server as MCP client if (this.sessionId) { await this.connectToWebSocketServer(); } // Start MCP server for Cursor AI communication const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("✅ MCP server started for Cursor AI integration"); } else { // Default mode: Start WebSocket server for Figma SpeX plugins await this.figmaManager.setupWebSocketServer(); console.error(`✅ Figma SpeX WebSocket server started on port ${this.figmaManager.port}`); // Also start MCP server for Cursor AI const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("✅ MCP server started for Cursor AI integration"); } // Setup graceful shutdown this.setupGracefulShutdown(); } catch (error) { console.error("❌ Error starting SpeX MCP Server:", error); process.exit(1); } } /** * Connect to existing WebSocket server as MCP client */ async connectToWebSocketServer() { try { const WebSocket = (await import('ws')).default; // Use provided server URL or default to localhost const baseUrl = this.serverUrl || `ws://localhost:${this.figmaManager.port}`; const wsUrl = `${baseUrl}?session-id=${this.sessionId}&type=mcp-client`; console.error(`🔗 Connecting to WebSocket server: ${wsUrl}`); const ws = new WebSocket(wsUrl); return new Promise((resolve, reject) => { ws.on('open', () => { console.error('✅ Connected to WebSocket server as MCP client'); // Store the connection for the session manager to use this.figmaManager.sessionManager.registerConnection(this.sessionId, ws, 'mcp-client'); // Provide the WebSocket connection to the MCP tools manager this.mcpToolsManager.setWebSocketConnection(ws); resolve(); }); ws.on('error', (error) => { console.error('❌ Failed to connect to WebSocket server:', error.message); reject(error); }); ws.on('message', (data) => { // Handle messages from WebSocket server if needed console.error('📥 MCP Client received WebSocket message:', data.toString()); }); ws.on('close', () => { console.error('🔌 WebSocket connection closed'); }); }); } catch (error) { console.error('❌ Error connecting to WebSocket server:', error); throw error; } } /** * Setup graceful shutdown handlers */ setupGracefulShutdown() { const shutdown = async () => { console.error("\n🔄 Shutting down SpeX MCP Server..."); try { await this.figmaManager.shutdown(); console.error("✅ Server shutdown completed"); } catch (error) { console.error("❌ Error during shutdown:", error); } process.exit(0); }; process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); process.on('uncaughtException', (error) => { console.error('❌ Uncaught Exception:', error); shutdown(); }); process.on('unhandledRejection', (reason, promise) => { console.error('❌ Unhandled Rejection at:', promise, 'reason:', reason); shutdown(); }); } } // Parse command line arguments function parseArgs() { const args = process.argv.slice(2); // Use Render's assigned PORT or default to 8080 const config = { port: process.env.PORT || 8080, force: false, sessionId: null, clientType: null, serverUrl: null }; console.error(`🔧 Parsing arguments: ${JSON.stringify(args)}`); console.error(`🔧 Environment PORT: ${process.env.PORT || 'not set'}`); for (let i = 0; i < args.length; i++) { if (args[i] === '--port' && i + 1 < args.length) { const port = parseInt(args[i + 1]); if (isNaN(port) || port < 1 || port > 65535) { console.error("❌ Invalid port number. Must be between 1 and 65535."); process.exit(1); } config.port = port; i++; // Skip the next argument since it's the port value } else if (args[i] === '--session-id' && i + 1 < args.length) { config.sessionId = args[i + 1]; i++; // Skip the next argument since it's the session ID value } else if (args[i] === '--client-type' && i + 1 < args.length) { const clientType = args[i + 1]; if (!['plugin', 'mcp-client'].includes(clientType)) { console.error("❌ Invalid client type. Must be 'plugin' or 'mcp-client'."); process.exit(1); } config.clientType = clientType; i++; // Skip the next argument since it's the client type value } else if (args[i] === '--server-url' && i + 1 < args.length) { config.serverUrl = args[i + 1]; console.error(`🔧 Server URL set to: ${config.serverUrl}`); i++; // Skip the next argument since it's the server URL value } else if (args[i] === '--force') { config.force = true; console.error(`🔧 Force option enabled`) } else if (args[i] === '--version' || args[i] === '-v') { console.error(`spex-mcp version ${packageJson.version}`); process.exit(0); } else if (args[i] === '--help' || args[i] === '-h') { console.error(` SpeX MCP Server - Figma SpeX plugin integration with Cursor AI (Remote Mode) Usage: node src/remote/index.js [options] Options: --port <number> WebSocket server port (default: 8080, or $PORT env var) --session-id <string> Session ID for MCP client connections --client-type <type> Client type: 'plugin' or 'mcp-client' --server-url <url> Remote server URL for MCP client connections --force Force kill any process using the port before starting --version, -v Show version number --help, -h Show this help message Examples: node src/remote/index.js --port 3000 node src/remote/index.js --session-id session_123_abc --client-type mcp-client node src/remote/index.js --client-type mcp-client --session-id session_123 --server-url wss://your-app.onrender.com node src/remote/index.js --port 3000 --force node src/remote/index.js --version node src/remote/index.js `); process.exit(0); } } console.error(`🔧 Final config: ${JSON.stringify(config)}`); return config; } // Start the server const config = parseArgs(); const server = new SpeXMCPServer(config.port, config.force, config.sessionId, config.clientType, config.serverUrl); server.run().catch((error) => { console.error("❌ Failed to start server:", error); process.exit(1); });