spex-mcp
Version:
MCP server for Figma SpeX plugin and Cursor AI integration
687 lines (609 loc) ⢠23.3 kB
JavaScript
/**
* 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}`,
},
],
};
}
}
}