MCP Toolbox
by ai-zerolab
- mcp_toolbox
- figma
import json
import time
from pathlib import Path
from typing import Any
import httpx
from pydantic import BaseModel
from mcp_toolbox.app import mcp
from mcp_toolbox.config import Config
# Type definitions for request/response parameters
class ClientMeta(BaseModel):
x: float
y: float
node_id: str | None = None
node_offset: dict[str, float] | None = None
# API Client
class FigmaApiClient:
BASE_URL = "https://api.figma.com/v1"
def __init__(self):
self.config = Config()
async def get_access_token(self) -> str:
if not self.config.figma_api_key:
raise ValueError("No Figma API key provided. Set the FIGMA_API_KEY environment variable.")
return self.config.figma_api_key
async def make_request(self, path: str, method: str = "GET", data: Any = None) -> dict[str, Any]:
token = await self.get_access_token()
async with httpx.AsyncClient(
transport=httpx.AsyncHTTPTransport(retries=3),
timeout=30,
) as client:
headers = {"X-Figma-Token": token}
url = f"{self.BASE_URL}{path}"
try:
if method == "GET":
response = await client.get(url, headers=headers)
elif method == "POST":
response = await client.post(url, headers=headers, json=data)
elif method == "DELETE":
response = await client.delete(url, headers=headers)
else:
raise ValueError(f"Unsupported HTTP method: {method}")
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as e:
figma_error = (
e.response.json() if e.response.content else {"status": e.response.status_code, "err": str(e)}
)
raise ValueError(
f"Figma API error: {figma_error.get('err', figma_error.get('message', str(e)))}"
) from e
except httpx.RequestError as e:
raise ValueError(f"Request error: {e!s}") from e
def build_query_string(self, params: dict[str, Any]) -> str:
# Filter out None values
filtered_params = {k: v for k, v in params.items() if v is not None}
if not filtered_params:
return ""
# Convert lists to comma-separated strings
for key, value in filtered_params.items():
if isinstance(value, list):
filtered_params[key] = ",".join(map(str, value))
# Build query string
query_parts = [f"{k}={v}" for k, v in filtered_params.items()]
return "?" + "&".join(query_parts)
# Cache Manager
class CacheManager:
def __init__(self):
self.config = Config()
self.cache_dir = Path(self.config.cache_dir)
self.cache_dir.mkdir(parents=True, exist_ok=True)
def save_to_cache(self, filename: str, data: Any) -> str:
file_path = self.cache_dir / filename
with open(file_path, "w") as f:
json.dump(data, f, indent=2)
return str(file_path)
# Initialize API client and cache manager
api_client = FigmaApiClient()
cache_manager = CacheManager()
# Tool implementations
@mcp.tool(
description="Get a Figma file by key. Args: file_key (required, The key of the file to get), version (optional, A specific version ID to get), depth (optional, Depth of nodes to return 1-4), branch_data (optional, Include branch data if true)"
)
async def figma_get_file(
file_key: str,
version: str | None = None,
depth: int | None = None,
branch_data: bool | None = None,
) -> dict[str, Any]:
"""Get a Figma file by key.
Args:
file_key: The key of the file to get
version: Optional. A specific version ID to get
depth: Optional. Depth of nodes to return (1-4)
branch_data: Optional. Include branch data if true
"""
params = {"version": version, "depth": depth, "branch_data": branch_data}
query_string = api_client.build_query_string(params)
result = await api_client.make_request(f"/files/{file_key}{query_string}")
# Save to cache
try:
filename = f"file_{file_key}_{int(time.time() * 1000)}.json"
file_path = cache_manager.save_to_cache(filename, result)
return {
"file_path": file_path,
"message": "File data saved to local cache. Use this file path to access the complete data.",
}
except Exception:
# If saving to cache fails, return original result
return result
@mcp.tool(
description="Get specific nodes from a Figma file. Args: file_key (required, The key of the file to get nodes from), node_ids (required, Array of node IDs to get), depth (optional, Depth of nodes to return 1-4), version (optional, A specific version ID to get)"
)
async def figma_get_file_nodes(
file_key: str,
node_ids: list[str],
depth: int | None = None,
version: str | None = None,
) -> dict[str, Any]:
"""Get specific nodes from a Figma file.
Args:
file_key: The key of the file to get nodes from
node_ids: Array of node IDs to get
depth: Optional. Depth of nodes to return (1-4)
version: Optional. A specific version ID to get
"""
params = {"ids": node_ids, "depth": depth, "version": version}
query_string = api_client.build_query_string(params)
result = await api_client.make_request(f"/files/{file_key}/nodes{query_string}")
# Save to cache
try:
filename = f"file_nodes_{file_key}_{int(time.time() * 1000)}.json"
file_path = cache_manager.save_to_cache(filename, result)
return {
"file_path": file_path,
"message": "File nodes data saved to local cache. Use this file path to access the complete data.",
}
except Exception:
# If saving to cache fails, return original result
return result
@mcp.tool(
description="Get images for nodes in a Figma file. Args: file_key (required, The key of the file to get images from), ids (required, Array of node IDs to render), scale (optional, Scale factor to render at 0.01-4), format_type (optional, Image format jpg/png/svg/pdf), svg_include_id (optional, Include IDs in SVG output), svg_simplify_stroke (optional, Simplify strokes in SVG output), use_absolute_bounds (optional, Use absolute bounds)"
)
async def figma_get_image(
file_key: str,
ids: list[str],
scale: float | None = None,
format_type: str | None = None, # Renamed from 'format' to avoid shadowing builtin
svg_include_id: bool | None = None,
svg_simplify_stroke: bool | None = None,
use_absolute_bounds: bool | None = None,
) -> dict[str, Any]:
"""Get images for nodes in a Figma file.
Args:
file_key: The key of the file to get images from
ids: Array of node IDs to render
scale: Optional. Scale factor to render at (0.01-4)
format_type: Optional. Image format (jpg, png, svg, pdf)
svg_include_id: Optional. Include IDs in SVG output
svg_simplify_stroke: Optional. Simplify strokes in SVG output
use_absolute_bounds: Optional. Use absolute bounds
"""
params = {
"ids": ids,
"scale": scale,
"format": format_type, # Use original API parameter name in the request
"svg_include_id": svg_include_id,
"svg_simplify_stroke": svg_simplify_stroke,
"use_absolute_bounds": use_absolute_bounds,
}
query_string = api_client.build_query_string(params)
return await api_client.make_request(f"/images/{file_key}{query_string}")
@mcp.tool(
description="Get URLs for images used in a Figma file. Args: file_key (required, The key of the file to get image fills from)"
)
async def figma_get_image_fills(file_key: str) -> dict[str, Any]:
"""Get URLs for images used in a Figma file.
Args:
file_key: The key of the file to get image fills from
"""
return await api_client.make_request(f"/files/{file_key}/images")
@mcp.tool(
description="Get comments on a Figma file. Args: file_key (required, The key of the file to get comments from)"
)
async def figma_get_comments(file_key: str) -> dict[str, Any]:
"""Get comments on a Figma file.
Args:
file_key: The key of the file to get comments from
"""
return await api_client.make_request(f"/files/{file_key}/comments")
@mcp.tool(
description="Post a comment on a Figma file. Args: file_key (required, The key of the file to comment on), message (required, Comment message text), client_meta (optional, Position of the comment x/y/node_id/node_offset), comment_id (optional, ID of comment to reply to)"
)
async def figma_post_comment(
file_key: str,
message: str,
client_meta: dict[str, Any] | None = None,
comment_id: str | None = None,
) -> dict[str, Any]:
"""Post a comment on a Figma file.
Args:
file_key: The key of the file to comment on
message: Comment message text
client_meta: Optional. Position of the comment (x, y, node_id, node_offset)
comment_id: Optional. ID of comment to reply to
"""
comment_data = {"message": message}
if client_meta:
comment_data["client_meta"] = client_meta
if comment_id:
comment_data["comment_id"] = comment_id
return await api_client.make_request(f"/files/{file_key}/comments", "POST", comment_data)
@mcp.tool(
description="Delete a comment from a Figma file. Args: file_key (required, The key of the file to delete a comment from), comment_id (required, ID of the comment to delete)"
)
async def figma_delete_comment(file_key: str, comment_id: str) -> dict[str, Any]:
"""Delete a comment from a Figma file.
Args:
file_key: The key of the file to delete a comment from
comment_id: ID of the comment to delete
"""
return await api_client.make_request(f"/files/{file_key}/comments/{comment_id}", "DELETE")
@mcp.tool(
description="Get projects for a team. Args: team_id (required, The team ID), page_size (optional, Number of items per page), cursor (optional, Cursor for pagination)"
)
async def figma_get_team_projects(
team_id: str, page_size: int | None = None, cursor: str | None = None
) -> dict[str, Any]:
"""Get projects for a team.
Args:
team_id: The team ID
page_size: Optional. Number of items per page
cursor: Optional. Cursor for pagination
"""
params = {"page_size": page_size, "cursor": cursor}
query_string = api_client.build_query_string(params)
return await api_client.make_request(f"/teams/{team_id}/projects{query_string}")
@mcp.tool(
description="Get files for a project. Args: project_id (required, The project ID), page_size (optional, Number of items per page), cursor (optional, Cursor for pagination), branch_data (optional, Include branch data if true)"
)
async def figma_get_project_files(
project_id: str,
page_size: int | None = None,
cursor: str | None = None,
branch_data: bool | None = None,
) -> dict[str, Any]:
"""Get files for a project.
Args:
project_id: The project ID
page_size: Optional. Number of items per page
cursor: Optional. Cursor for pagination
branch_data: Optional. Include branch data if true
"""
params = {"page_size": page_size, "cursor": cursor, "branch_data": branch_data}
query_string = api_client.build_query_string(params)
return await api_client.make_request(f"/projects/{project_id}/files{query_string}")
@mcp.tool(
description="Get components for a team. Args: team_id (required, The team ID), page_size (optional, Number of items per page), cursor (optional, Cursor for pagination)"
)
async def figma_get_team_components(
team_id: str, page_size: int | None = None, cursor: str | None = None
) -> dict[str, Any]:
"""Get components for a team.
Args:
team_id: The team ID
page_size: Optional. Number of items per page
cursor: Optional. Cursor for pagination
"""
params = {"page_size": page_size, "cursor": cursor}
query_string = api_client.build_query_string(params)
return await api_client.make_request(f"/teams/{team_id}/components{query_string}")
@mcp.tool(
description="Get components from a file. Args: file_key (required, The key of the file to get components from)"
)
async def figma_get_file_components(file_key: str) -> dict[str, Any]:
"""Get components from a file.
Args:
file_key: The key of the file to get components from
"""
return await api_client.make_request(f"/files/{file_key}/components")
@mcp.tool(description="Get a component by key. Args: key (required, The component key)")
async def figma_get_component(key: str) -> dict[str, Any]:
"""Get a component by key.
Args:
key: The component key
"""
return await api_client.make_request(f"/components/{key}")
@mcp.tool(
description="Get component sets for a team. Args: team_id (required, The team ID), page_size (optional, Number of items per page), cursor (optional, Cursor for pagination)"
)
async def figma_get_team_component_sets(
team_id: str, page_size: int | None = None, cursor: str | None = None
) -> dict[str, Any]:
"""Get component sets for a team.
Args:
team_id: The team ID
page_size: Optional. Number of items per page
cursor: Optional. Cursor for pagination
"""
params = {"page_size": page_size, "cursor": cursor}
query_string = api_client.build_query_string(params)
return await api_client.make_request(f"/teams/{team_id}/component_sets{query_string}")
@mcp.tool(
description="Get styles for a team. Args: team_id (required, The team ID), page_size (optional, Number of items per page), cursor (optional, Cursor for pagination)"
)
async def figma_get_team_styles(
team_id: str, page_size: int | None = None, cursor: str | None = None
) -> dict[str, Any]:
"""Get styles for a team.
Args:
team_id: The team ID
page_size: Optional. Number of items per page
cursor: Optional. Cursor for pagination
"""
params = {"page_size": page_size, "cursor": cursor}
query_string = api_client.build_query_string(params)
return await api_client.make_request(f"/teams/{team_id}/styles{query_string}")
@mcp.tool(description="Get styles from a file. Args: file_key (required, The key of the file to get styles from)")
async def figma_get_file_styles(file_key: str) -> dict[str, Any]:
"""Get styles from a file.
Args:
file_key: The key of the file to get styles from
"""
return await api_client.make_request(f"/files/{file_key}/styles")
@mcp.tool(description="Get a style by key. Args: key (required, The style key)")
async def figma_get_style(key: str) -> dict[str, Any]:
"""Get a style by key.
Args:
key: The style key
"""
return await api_client.make_request(f"/styles/{key}")