Initial commit
This commit is contained in:
332
skills/product-design/functions/figma_mcp_client.py
Normal file
332
skills/product-design/functions/figma_mcp_client.py
Normal file
@@ -0,0 +1,332 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user