UNPKG

spex-mcp

Version:

MCP server for Figma SpeX plugin and Cursor AI integration

687 lines (609 loc) • 23.3 kB
/** * MCPToolsManager handles all MCP tools for Cursor AI integration * Supports image injection as per Cursor MCP documentation */ export class MCPToolsManager { constructor(figmaManager) { this.figmaManager = figmaManager; } /** * Get list of all available MCP tools for Cursor AI */ getToolsList() { return [ { name: "hello-world", description: "Returns a simple hello world message", inputSchema: { type: "object", properties: {}, required: [], }, }, { name: "get-specs-readme", description: "Fetches the README.md file from the SpeX plugin that contains documentation about the exported design specs", inputSchema: { type: "object", properties: {}, required: [], }, }, { name: "get-spec-files-manifest", description: "Fetches the manifest.yaml file from the SpeX plugin to confirm design specs are ready and get the list of available spec files", inputSchema: { type: "object", properties: {}, required: [], }, }, { name: "get-a-spec-file", description: "Fetches a specific design specification file from the SpeX plugin by filename", inputSchema: { type: "object", properties: { file_name: { type: "string", description: "The name of the spec file to retrieve (e.g., 'screen.yaml', 'components/button.yaml')" } }, required: ["file_name"], }, }, { name: "get-page-thumbnail", description: "Fetches a thumbnail image of the current page or screen from the Figma design", inputSchema: { type: "object", properties: {}, required: [], }, }, { name: "view_image_file_in_base64", description: "View a physical image file in base64 format from the specified file path", inputSchema: { type: "object", properties: { absolute_file_path: { type: "string", description: "The absolute file path of the image file to view" } }, required: ["absolute_file_path"], }, }, { name: "convert_svg_to_vector_drawable", description: "Converts SVG content to Android Vector Drawable XML format for use in Android applications", inputSchema: { type: "object", properties: { svgContent: { type: "string", description: "The SVG content to convert" }, tint: { type: "string", description: "Optional tint color to apply to the vector drawable (e.g., '#FF0000')" } }, required: ["svgContent"], }, }, { name: "generate-compose-ui", description: "šŸŽÆ Generate Jetpack Compose UI code from Figma design specs served via spex-mcp. Follows a structured 6-step workflow: 1) Understand design context & structure, 2) Parse screen specification, 3) Identify code component definitions, 4) Generate Compose UI code, 5) Adjustments with design screenshot, 6) Summary report. This tool orchestrates the entire design-to-code workflow.", inputSchema: { type: "object", properties: { source_code_file: { type: "string", description: "The target file path where the generated Compose UI code should be written (e.g., 'app/src/main/java/com/example/ui/screens/HomeScreen.kt')" } }, required: ["source_code_file"], }, }, ]; } /** * Handle MCP tool calls from Cursor AI */ async handleToolCall(name, args = {}) { switch (name) { case "hello-world": return this.handleHelloWorld(); case "get-specs-readme": return this.handleGetSpecsReadme(); case "get-spec-files-manifest": return this.handleGetSpecFilesManifest(); case "get-a-spec-file": return this.handleGetASpecFile(args); case "get-page-thumbnail": return this.handleGetPageThumbnail(args); case "view_image_file_in_base64": return this.handleViewImageFileInBase64(args); case "convert_svg_to_vector_drawable": return this.handleConvertSvgToVectorDrawable(args); case "generate-compose-ui": return this.handleGenerateComposeUI(args); default: throw new Error(`Unknown tool: ${name}`); } } /** * Handle hello world tool call */ handleHelloWorld() { return { content: [ { type: "text", text: "Hello World from SpeX MCP Server! šŸŽØ", }, ], }; } /** * Handle get specs readme tool call */ async handleGetSpecsReadme() { try { const result = await this.figmaManager.requestSpecFile('get-specs-readme'); if (result.success && result.data) { return { content: [ { type: "text", text: result.data.content, }, ], }; } else { return { content: [ { type: "text", text: `Error fetching README: ${result.error || 'Unknown error'}`, }, ], }; } } catch (error) { return { content: [ { type: "text", text: `Error requesting README: ${error.message}`, }, ], }; } } /** * Handle get spec files manifest tool call */ async handleGetSpecFilesManifest() { try { const result = await this.figmaManager.requestSpecFile('get-spec-files-manifest'); if (result.success && result.data) { return { content: [ { type: "text", text: result.data.content, }, ], }; } else { return { content: [ { type: "text", text: `Error fetching manifest: ${result.error || 'Unknown error'}`, }, ], }; } } catch (error) { return { content: [ { type: "text", text: `Error requesting manifest: ${error.message}`, }, ], }; } } /** * Handle get a spec file tool call */ async handleGetASpecFile(args) { try { const { file_name } = args; if (!file_name) { return { content: [ { type: "text", text: "Error: file_name parameter is required", }, ], }; } const result = await this.figmaManager.requestSpecFile('get-a-spec-file', file_name); if (result.success && result.data) { return { content: [ { type: "text", text: result.data.content, }, ], }; } else { return { content: [ { type: "text", text: `Error fetching file '${file_name}': ${result.error || 'Unknown error'}`, }, ], }; } } catch (error) { return { content: [ { type: "text", text: `Error requesting file: ${error.message}`, }, ], }; } } /** * Handle get component screenshot tool call * Returns visual screenshot of a component for reference */ /** * Handle get page thumbnail tool call * Returns thumbnail image of the current page/screen from plugin's preview files */ async handleGetPageThumbnail(args) { try { const result = await this.figmaManager.requestSpecFile('get-page-thumbnail'); if (result.success && result.data) { // Check if response contains image data if (result.data.image_data && result.data.mime_type) { return { content: [ { type: "text", text: "Page thumbnail from Figma design:", }, { type: "image", data: result.data.image_data, mimeType: result.data.mime_type, }, ], }; } else { return { content: [ { type: "text", text: result.data.content || "Page thumbnail is not available as image data", }, ], }; } } else { return { content: [ { type: "text", text: `Error fetching page thumbnail: ${result.error || 'Unknown error'}`, }, ], }; } } catch (error) { return { content: [ { type: "text", text: `Error requesting page thumbnail: ${error.message}`, }, ], }; } } /** * Handle view image file in base64 tool call * Reads an existing image file from disk and returns it in base64 format * Supports PNG and JPEG formats */ async handleViewImageFileInBase64(args) { try { const { absolute_file_path } = args; if (!absolute_file_path) { return { content: [ { type: "text", text: "Error: file_path parameter is required", }, ], }; } // Read the image file from disk const result = await this.readImageFile(absolute_file_path); if (result.success && result.image_data) { return { content: [ { type: "image", data: result.image_data, mimeType: result.mime_type, }, ], }; } else { return { content: [ { type: "text", text: `Error reading image file '${absolute_file_path}': ${result.error || 'Unknown error'}`, }, ], }; } } catch (error) { return { content: [ { type: "text", text: `Error reading UI Compose snapshot image: ${error.message}`, }, ], }; } } /** * Read an image file from disk and return it as base64 * Validates that the file is PNG or JPEG format */ async readImageFile(filePath) { try { // Import required modules const fs = await import('fs'); const path = await import('path'); // Check if file exists if (!fs.existsSync(filePath)) { return { success: false, error: `File not found: ${filePath}` }; } // Get file extension to determine MIME type const ext = path.extname(filePath).toLowerCase(); let mimeType; switch (ext) { case '.png': mimeType = 'image/png'; break; case '.jpg': case '.jpeg': mimeType = 'image/jpeg'; break; default: return { success: false, error: `Unsupported image format: ${ext}. Only PNG and JPEG are supported.` }; } // Read file as buffer const imageBuffer = fs.readFileSync(filePath); // Convert to base64 const base64Data = imageBuffer.toString('base64'); return { success: true, image_data: base64Data, mime_type: mimeType, file_info: { path: filePath, size: imageBuffer.length, format: ext.substring(1).toUpperCase() } }; } catch (error) { return { success: false, error: `Failed to read image file: ${error.message}` }; } } /** * Handle generate compose UI tool call * Implements the structured 6-step workflow for generating Jetpack Compose UI from Figma design specs */ async handleGenerateComposeUI(args) { try { const { source_code_file } = args; if (!source_code_file) { return { content: [ { type: "text", text: "Error: source_code_file parameter is required", }, ], }; } // Generate the structured prompt template with the workflow const promptTemplate = `šŸŽÆ Task: Generate Jetpack Compose UI code into \`${source_code_file}\` from Figma design specs served via \`spex-mcp\`. I will provide: - Access to the \`spex-mcp\` server (current design context is already selected). - A screenshot showing the expected visual output of the screen. āœ… **Step 1: Understand the Design Context & Structure** 1. Call \`get-page-thumbnail\` - This retrieves the thumbnail image of the design screen. - Use it to visually understand the expected UI layout and design intent. 2. Call \`get-spec-files-manifest\` - This returns a list of available spec files. - Confirm the existence of README.md and a screen spec file (e.g., screen.yaml). 3. Call \`get-specs-readme\` - This returns the content of README.md. - Use it to understand the file structure, naming conventions, and spec format. - āŒ Do not retrieve or inspect any other files yet — even if referenced in the README. āœ… **Step 2: Parse the Screen Specification** - Use the MCP tool to call \`get-a-spec-file\` to retrieve the screen spec file identified in Step 1 (e.g., screen.yaml). - Read it thoroughly from top to bottom. - āŒ Do not retrieve or read any referenced component files at this step. - **FOR EACH INSTANCE_REF in the specification:** - Retrieve the corresponding component spec file - Cross-reference with existing code components - Note any status/variant mappings šŸ“Œ Construct a **UI hierarchy diagram** based solely on the screen spec. Note: - Diagram must include component's fields. - Diagram must be non-mermaid format āœ… **Step 3: Identify Code Component Definitions in Design Specs** For each component node in the design, check if it has a codeFilePath field. → If yes, locate and load the corresponding source file. - **Mandatory: Examine ALL @Preview functions in detail** - **Pay special attention to @Preview functions that show:** - Status components and their type mappings - Complex content usage patterns - Component composition examples - **Cross-reference Preview patterns with design spec requirements** For components without a codeFilePath, treat them as new components. → Use their corresponding spec file (e.g., components/<name>.yaml) as the sole authoritative source for defining the new component. → Do not guess or invent props, behavior, or layout unless explicitly stated in specs. ### Summary results - A list of all existing components, including (in table format): - Name - Code location - Relevant \`@Preview\` functions — selected based on visual or semantic match with design intent - A list of all new components to be generated - A reflection of each component's design spec parameters vs. actual code āœ… **Step 4: Generate Compose UI Code** Generate Compose UI based on the analyze result of Step 3. Add Preview UI for: - The Screen - Any Newly Created Components āœ… **Step 5: Adjustments with Design Screenshot** - Fetch design screenshot, compare the generated UI against it. - Make necessary adjustments to ensure accuracy and alignment. - Use android-studio mcp for detect code errors. Note: ignore if error is Unresolved reference icon drawable resource. āœ… **Step 6: Summary** Provide a report (in table format) summarizing all identified components from the design. Categorize them into: - Used in generated UI - Not used in generated UI For components not used, include a brief explanation for each (e.g., redundant, incomplete spec, outside screen scope, etc.). ## **Note: If anything is unclear at any step, please pause and ask for clarification.** --- **Target file for code generation:** \`${source_code_file}\` **Now please begin with Step 1: Call \`get-page-thumbnail\` to understand the design context.**`; return { content: [ { type: "text", text: promptTemplate, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error generating Compose UI prompt: ${error.message}`, }, ], }; } } /** * Handle convert SVG to Vector Drawable tool call * Converts SVG content to Android Vector Drawable XML format * * @param {Object} args - Tool arguments * @param {string} args.svgContent - The SVG content to convert * @param {string} [args.tint] - Optional tint color to apply to the vector drawable * @returns {Promise<Object>} - Tool response with Vector Drawable XML or error message */ async handleConvertSvgToVectorDrawable(args) { try { // Import the SVG to Vector Drawable conversion utility const { convertSvgToVectorDrawable, validateSvgContent } = await import('../utils/svg-to-vector-drawable.js'); // Extract parameters const { svgContent, tint } = args; // Validate required parameters if (!svgContent) { return { content: [ { type: "text", text: "Error: svgContent parameter is required", }, ], }; } // Validate SVG content format const validation = validateSvgContent(svgContent); if (!validation.isValid) { return { content: [ { type: "text", text: `Error: Invalid SVG content - ${validation.errors.join(', ')}`, }, ], }; } // Prepare options for conversion const options = {}; if (tint) options.tint = tint; // Convert SVG to Vector Drawable const result = await convertSvgToVectorDrawable(svgContent, options); // Prepare response const response = { content: [ { type: "text", text: "SVG successfully converted to Android Vector Drawable:", }, { type: "text", text: result.vectorDrawable, } ], }; // Add warnings if any if (result.warnings && result.warnings.length > 0) { response.content.push({ type: "text", text: `Warnings: ${result.warnings.join(', ')}`, }); } return response; } catch (error) { return { content: [ { type: "text", text: `Error converting SVG to Vector Drawable: ${error.message}`, }, ], }; } } }