Files
gh-alekspetrov-navigator/skills/product-design/functions/figma_mcp_client.py
2025-11-29 17:51:59 +08:00

333 lines
11 KiB
Python

#!/usr/bin/env python3
"""
Figma MCP Client - Direct Python interface to Figma Desktop MCP server.
This module provides a simple async interface to Figma's Model Context Protocol
server running locally at http://127.0.0.1:3845/mcp
Usage:
async with FigmaMCPClient() as client:
# Get design tokens
tokens = await client.get_variable_defs()
# Get component metadata
metadata = await client.get_metadata(node_id="1:23")
# Get code mappings
mappings = await client.get_code_connect_map()
Requirements:
- Figma Desktop app must be running
- MCP server enabled in Figma Preferences
- User logged into Figma
- pip install mcp
"""
import json
import logging
from typing import Optional, Dict, Any, List
try:
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
except ImportError as e:
raise ImportError(
"MCP SDK not installed. Install with: pip install mcp"
) from e
logger = logging.getLogger(__name__)
class FigmaMCPError(Exception):
"""Base exception for Figma MCP client errors."""
pass
class FigmaNotRunningError(FigmaMCPError):
"""Raised when Figma Desktop is not running or MCP server not enabled."""
pass
class FigmaMCPClient:
"""
Async client for Figma Desktop MCP server.
Provides direct access to Figma's design data through the Model Context Protocol.
Use as async context manager to ensure proper connection lifecycle.
Example:
async with FigmaMCPClient() as client:
variables = await client.get_variable_defs()
print(f"Found {len(variables)} design tokens")
"""
def __init__(self, mcp_url: str = "http://127.0.0.1:3845/mcp"):
"""
Initialize Figma MCP client.
Args:
mcp_url: URL of Figma Desktop MCP server (default: http://127.0.0.1:3845/mcp)
"""
self.mcp_url = mcp_url
self.session = None
self.transport = None
self.session_context = None
async def __aenter__(self):
"""Async context manager entry - establishes MCP connection."""
try:
# Connect to Figma MCP server
self.transport = streamablehttp_client(self.mcp_url)
self.read_stream, self.write_stream, _ = await self.transport.__aenter__()
# Create MCP session
self.session_context = ClientSession(self.read_stream, self.write_stream)
self.session = await self.session_context.__aenter__()
# Initialize MCP protocol
init_result = await self.session.initialize()
logger.info(
f"Connected to {init_result.serverInfo.name} "
f"v{init_result.serverInfo.version}"
)
return self
except Exception as e:
logger.error(f"Failed to connect to Figma MCP server: {e}")
raise FigmaNotRunningError(
"Could not connect to Figma Desktop MCP server. "
"Please ensure:\n"
" 1. Figma Desktop app is running\n"
" 2. MCP server is enabled in Figma → Preferences\n"
" 3. You are logged into Figma\n"
f"Error: {e}"
) from e
async def __aexit__(self, *args):
"""Async context manager exit - closes MCP connection."""
try:
if self.session_context:
await self.session_context.__aexit__(*args)
if self.transport:
await self.transport.__aexit__(*args)
logger.info("Disconnected from Figma MCP server")
except Exception as e:
logger.warning(f"Error during disconnect: {e}")
async def _call_tool(self, tool_name: str, params: Optional[Dict[str, Any]] = None) -> Any:
"""
Internal method to call MCP tool and extract content.
Args:
tool_name: Name of the MCP tool to call
params: Tool parameters
Returns:
Tool response content (parsed as JSON if possible)
"""
if not self.session:
raise FigmaMCPError("Client not connected. Use 'async with FigmaMCPClient()'")
try:
result = await self.session.call_tool(tool_name, params or {})
# Extract content from MCP response
if result.content and len(result.content) > 0:
content_item = result.content[0]
# Handle different content types
if hasattr(content_item, 'text'):
# Text content (most common)
content = content_item.text
# Try to parse as JSON
try:
return json.loads(content)
except (json.JSONDecodeError, TypeError):
# Return raw text if not JSON
return content
elif hasattr(content_item, 'data'):
# Image or binary content
return content_item.data
else:
# Unknown content type - return as-is
return content_item
return None
except Exception as e:
logger.error(f"Error calling {tool_name}: {e}")
raise FigmaMCPError(f"Failed to call {tool_name}: {e}") from e
async def get_metadata(self, node_id: Optional[str] = None) -> Dict[str, Any]:
"""
Get metadata for a node or page in XML format.
Includes node IDs, layer types, names, positions, and sizes.
Use this to discover component structure before fetching full details.
Args:
node_id: Specific node or page ID (e.g., "1:23" or "0:1")
If None, uses currently selected node in Figma
Returns:
Metadata dictionary with node structure
Example:
metadata = await client.get_metadata(node_id="0:1")
# Parse to find component node IDs
"""
params = {"nodeId": node_id} if node_id else {}
return await self._call_tool("get_metadata", params)
async def get_variable_defs(self, node_id: Optional[str] = None) -> Dict[str, str]:
"""
Get design token variable definitions.
Returns mapping of variable names to values.
Args:
node_id: Specific node ID (if None, uses currently selected)
Returns:
Dictionary mapping variable names to values
Example: {'icon/default/secondary': '#949494', 'spacing/md': '16px'}
Example:
tokens = await client.get_variable_defs()
for name, value in tokens.items():
print(f"{name}: {value}")
"""
params = {"nodeId": node_id} if node_id else {}
return await self._call_tool("get_variable_defs", params)
async def get_code_connect_map(self, node_id: Optional[str] = None) -> Dict[str, Dict[str, str]]:
"""
Get mapping of Figma components to code components.
Requires Figma Enterprise plan with Code Connect configured.
Args:
node_id: Specific node ID (if None, uses currently selected)
Returns:
Dictionary mapping node IDs to code locations
Example: {
'1:2': {
'codeConnectSrc': 'https://github.com/foo/components/Button.tsx',
'codeConnectName': 'Button'
}
}
Example:
mappings = await client.get_code_connect_map()
for node_id, mapping in mappings.items():
print(f"{node_id}{mapping['codeConnectName']}")
"""
params = {"nodeId": node_id} if node_id else {}
return await self._call_tool("get_code_connect_map", params)
async def get_design_context(self, node_id: Optional[str] = None) -> str:
"""
Generate UI code for a component.
Returns React/Vue/HTML implementation code for the selected component.
Use sparingly - can return large responses (50-100k tokens).
Args:
node_id: Specific node ID (if None, uses currently selected)
Returns:
UI code as string (React/Vue/HTML)
Example:
code = await client.get_design_context(node_id="1:23")
# Returns React component code
"""
params = {"nodeId": node_id} if node_id else {}
return await self._call_tool("get_design_context", params)
async def get_screenshot(self, node_id: Optional[str] = None) -> str:
"""
Generate screenshot for a component.
Args:
node_id: Specific node ID (if None, uses currently selected)
Returns:
Screenshot image data (format depends on Figma response)
Example:
screenshot = await client.get_screenshot(node_id="1:23")
# Save or process screenshot data
"""
params = {"nodeId": node_id} if node_id else {}
return await self._call_tool("get_screenshot", params)
async def create_design_system_rules(self) -> str:
"""
Generate design system rules for the repository.
Returns:
Prompt for design system rules generation
Example:
rules = await client.create_design_system_rules()
"""
return await self._call_tool("create_design_system_rules")
async def list_available_tools(self) -> List[str]:
"""
List all available MCP tools.
Useful for debugging or discovering what Figma MCP supports.
Returns:
List of tool names
Example:
tools = await client.list_available_tools()
print(f"Available: {', '.join(tools)}")
"""
if not self.session:
raise FigmaMCPError("Client not connected")
result = await self.session.list_tools()
return [tool.name for tool in result.tools]
# Convenience function for simple use cases
async def get_figma_variables() -> Dict[str, str]:
"""
Quick helper to fetch Figma design tokens.
Returns:
Dictionary of variable name → value mappings
Example:
tokens = await get_figma_variables()
"""
async with FigmaMCPClient() as client:
return await client.get_variable_defs()
async def get_figma_metadata(node_id: Optional[str] = None) -> Dict[str, Any]:
"""
Quick helper to fetch Figma node metadata.
Args:
node_id: Specific node ID (if None, uses currently selected)
Returns:
Metadata dictionary
Example:
metadata = await get_figma_metadata(node_id="0:1")
"""
async with FigmaMCPClient() as client:
return await client.get_metadata(node_id)