spex-mcp
Version:
MCP server for Figma SpeX plugin and Cursor AI integration
250 lines (223 loc) • 10 kB
JavaScript
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);
});