|
|
|
""" |
|
🎨 Figma MCP Server - Hébergé sur Hugging Face Spaces |
|
Serveur MCP pour contrôler Figma via Claude/Cursor avec la vraie API REST |
|
""" |
|
import gradio as gr |
|
import asyncio |
|
import json |
|
import logging |
|
import requests |
|
from typing import Dict, Any, Optional, List |
|
from PIL import Image |
|
import base64 |
|
import io |
|
|
|
|
|
logging.basicConfig(level=logging.INFO) |
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
FIGMA_API_BASE = "https://api.figma.com/v1" |
|
|
|
|
|
|
|
|
|
figma_config = { |
|
"token": None, |
|
"file_id": None, |
|
"team_id": None |
|
} |
|
|
|
def configure_figma_token(token: str) -> str: |
|
"""Configure le token d'accès Figma""" |
|
global figma_config |
|
|
|
if not token or not token.startswith(('figd_', 'figc_')): |
|
return "❌ Token invalide. Le token doit commencer par 'figd_' ou 'figc_'" |
|
|
|
figma_config["token"] = token |
|
|
|
|
|
try: |
|
headers = {"X-Figma-Token": token} |
|
response = requests.get(f"{FIGMA_API_BASE}/me", headers=headers, timeout=10) |
|
|
|
if response.status_code == 200: |
|
user_data = response.json() |
|
username = user_data.get("handle", "Utilisateur inconnu") |
|
return f"✅ Token configuré avec succès ! Connecté en tant que : {username}" |
|
else: |
|
return f"❌ Erreur lors de la vérification du token : {response.status_code}" |
|
|
|
except Exception as e: |
|
return f"❌ Erreur de connexion à l'API Figma : {str(e)}" |
|
|
|
def configure_figma_file_id(file_id: str) -> str: |
|
"""Configure l'ID du fichier Figma à utiliser""" |
|
global figma_config |
|
|
|
if not file_id: |
|
return "❌ L'ID du fichier est requis" |
|
|
|
figma_config["file_id"] = file_id |
|
|
|
|
|
if figma_config["token"]: |
|
try: |
|
headers = {"X-Figma-Token": figma_config["token"]} |
|
response = requests.get(f"{FIGMA_API_BASE}/files/{file_id}", headers=headers, timeout=10) |
|
|
|
if response.status_code == 200: |
|
file_data = response.json() |
|
file_name = file_data.get("name", "Fichier inconnu") |
|
return f"✅ Fichier configuré avec succès : {file_name}" |
|
else: |
|
return f"❌ Impossible d'accéder au fichier : {response.status_code}" |
|
|
|
except Exception as e: |
|
return f"❌ Erreur lors de l'accès au fichier : {str(e)}" |
|
else: |
|
return "⚠️ ID du fichier configuré, mais token manquant" |
|
|
|
|
|
|
|
def make_figma_request(endpoint: str, method: str = "GET", data: Dict = None) -> Dict[str, Any]: |
|
"""Effectue une requête à l'API Figma""" |
|
if not figma_config["token"]: |
|
return {"error": "Token Figma non configuré"} |
|
|
|
headers = { |
|
"X-Figma-Token": figma_config["token"], |
|
"Content-Type": "application/json" |
|
} |
|
|
|
url = f"{FIGMA_API_BASE}/{endpoint}" |
|
|
|
try: |
|
if method == "GET": |
|
response = requests.get(url, headers=headers, timeout=30) |
|
elif method == "POST": |
|
response = requests.post(url, headers=headers, json=data, timeout=30) |
|
elif method == "PUT": |
|
response = requests.put(url, headers=headers, json=data, timeout=30) |
|
elif method == "DELETE": |
|
response = requests.delete(url, headers=headers, timeout=30) |
|
else: |
|
return {"error": f"Méthode HTTP non supportée : {method}"} |
|
|
|
if response.status_code in [200, 201]: |
|
return response.json() |
|
else: |
|
return {"error": f"Erreur API {response.status_code}: {response.text}"} |
|
|
|
except Exception as e: |
|
return {"error": f"Erreur de requête : {str(e)}"} |
|
|
|
|
|
|
|
def get_figma_file_info(file_id: str = "") -> str: |
|
"""Récupère les informations d'un fichier Figma""" |
|
file_id = file_id or figma_config["file_id"] |
|
|
|
if not file_id: |
|
return "❌ ID du fichier requis. Utilisez configure_figma_file_id() d'abord." |
|
|
|
result = make_figma_request(f"files/{file_id}") |
|
|
|
if "error" in result: |
|
return f"❌ Erreur : {result['error']}" |
|
|
|
file_info = { |
|
"nom": result.get("name", ""), |
|
"derniere_modification": result.get("lastModified", ""), |
|
"version": result.get("version", ""), |
|
"pages": [page.get("name", "") for page in result.get("document", {}).get("children", [])] |
|
} |
|
|
|
return f"📄 **Fichier Figma :**\n{json.dumps(file_info, indent=2, ensure_ascii=False)}" |
|
|
|
def create_figma_rectangle(x: str, y: str, width: str, height: str, name: str = "Rectangle", color: str = "#FF0000") -> str: |
|
"""Crée un rectangle dans Figma (via commentaire pour notification)""" |
|
if not figma_config["file_id"]: |
|
return "❌ ID du fichier requis. Utilisez configure_figma_file_id() d'abord." |
|
|
|
try: |
|
x_pos, y_pos = float(x), float(y) |
|
w, h = float(width), float(height) |
|
|
|
|
|
comment_text = f"🟦 **Rectangle à créer :**\n- Nom: {name}\n- Position: ({x_pos}, {y_pos})\n- Taille: {w}x{h}\n- Couleur: {color}" |
|
|
|
comment_data = { |
|
"message": comment_text, |
|
"client_meta": { |
|
"x": x_pos, |
|
"y": y_pos |
|
} |
|
} |
|
|
|
result = make_figma_request(f"files/{figma_config['file_id']}/comments", "POST", comment_data) |
|
|
|
if "error" in result: |
|
return f"❌ Erreur lors de la création du commentaire : {result['error']}" |
|
|
|
return f"✅ Rectangle {name} créé (via commentaire) à ({x_pos}, {y_pos}) avec la taille {w}x{h}" |
|
|
|
except ValueError: |
|
return "❌ Les coordonnées et dimensions doivent être des nombres" |
|
|
|
def create_figma_frame(x: str, y: str, width: str, height: str, name: str = "Frame") -> str: |
|
"""Crée un frame dans Figma (via commentaire pour notification)""" |
|
if not figma_config["file_id"]: |
|
return "❌ ID du fichier requis. Utilisez configure_figma_file_id() d'abord." |
|
|
|
try: |
|
x_pos, y_pos = float(x), float(y) |
|
w, h = float(width), float(height) |
|
|
|
comment_text = f"🖼️ **Frame à créer :**\n- Nom: {name}\n- Position: ({x_pos}, {y_pos})\n- Taille: {w}x{h}" |
|
|
|
comment_data = { |
|
"message": comment_text, |
|
"client_meta": { |
|
"x": x_pos, |
|
"y": y_pos |
|
} |
|
} |
|
|
|
result = make_figma_request(f"files/{figma_config['file_id']}/comments", "POST", comment_data) |
|
|
|
if "error" in result: |
|
return f"❌ Erreur lors de la création du commentaire : {result['error']}" |
|
|
|
return f"✅ Frame {name} créé (via commentaire) à ({x_pos}, {y_pos}) avec la taille {w}x{h}" |
|
|
|
except ValueError: |
|
return "❌ Les coordonnées et dimensions doivent être des nombres" |
|
|
|
def create_figma_text(x: str, y: str, text: str, name: str = "Text", font_size: str = "16") -> str: |
|
"""Crée un élément texte dans Figma (via commentaire pour notification)""" |
|
if not figma_config["file_id"]: |
|
return "❌ ID du fichier requis. Utilisez configure_figma_file_id() d'abord." |
|
|
|
try: |
|
x_pos, y_pos = float(x), float(y) |
|
size = float(font_size) |
|
|
|
comment_text = f"📝 **Texte à créer :**\n- Nom: {name}\n- Position: ({x_pos}, {y_pos})\n- Texte: \"{text}\"\n- Taille: {size}px" |
|
|
|
comment_data = { |
|
"message": comment_text, |
|
"client_meta": { |
|
"x": x_pos, |
|
"y": y_pos |
|
} |
|
} |
|
|
|
result = make_figma_request(f"files/{figma_config['file_id']}/comments", "POST", comment_data) |
|
|
|
if "error" in result: |
|
return f"❌ Erreur lors de la création du commentaire : {result['error']}" |
|
|
|
return f"✅ Texte \"{text}\" créé (via commentaire) à ({x_pos}, {y_pos})" |
|
|
|
except ValueError: |
|
return "❌ Les coordonnées et la taille doivent être des nombres" |
|
|
|
def get_figma_comments(file_id: str = "") -> str: |
|
"""Récupère tous les commentaires d'un fichier Figma""" |
|
file_id = file_id or figma_config["file_id"] |
|
|
|
if not file_id: |
|
return "❌ ID du fichier requis" |
|
|
|
result = make_figma_request(f"files/{file_id}/comments") |
|
|
|
if "error" in result: |
|
return f"❌ Erreur : {result['error']}" |
|
|
|
comments = result.get("comments", []) |
|
|
|
if not comments: |
|
return "📝 Aucun commentaire trouvé dans ce fichier" |
|
|
|
comment_list = [] |
|
for comment in comments[:10]: |
|
user = comment.get("user", {}).get("handle", "Anonyme") |
|
message = comment.get("message", "") |
|
created_at = comment.get("created_at", "") |
|
comment_list.append(f"👤 {user} ({created_at}): {message}") |
|
|
|
return f"📝 **Commentaires récents :**\n" + "\n\n".join(comment_list) |
|
|
|
def get_figma_user_info() -> str: |
|
"""Récupère les informations de l'utilisateur connecté""" |
|
result = make_figma_request("me") |
|
|
|
if "error" in result: |
|
return f"❌ Erreur : {result['error']}" |
|
|
|
user_info = { |
|
"nom": result.get("handle", ""), |
|
"email": result.get("email", ""), |
|
"id": result.get("id", "") |
|
} |
|
|
|
return f"👤 **Utilisateur connecté :**\n{json.dumps(user_info, indent=2, ensure_ascii=False)}" |
|
|
|
def list_figma_team_projects(team_id: str = "") -> str: |
|
"""Liste les projets d'une équipe Figma""" |
|
team_id = team_id or figma_config["team_id"] |
|
|
|
if not team_id: |
|
return "❌ ID de l'équipe requis. Configurez-le avec figma_config['team_id'] = 'VOTRE_TEAM_ID'" |
|
|
|
result = make_figma_request(f"teams/{team_id}/projects") |
|
|
|
if "error" in result: |
|
return f"❌ Erreur : {result['error']}" |
|
|
|
projects = result.get("projects", []) |
|
|
|
if not projects: |
|
return "📁 Aucun projet trouvé dans cette équipe" |
|
|
|
project_list = [] |
|
for project in projects[:10]: |
|
name = project.get("name", "Sans nom") |
|
project_id = project.get("id", "") |
|
project_list.append(f"📁 {name} (ID: {project_id})") |
|
|
|
return f"📁 **Projets de l'équipe :**\n" + "\n".join(project_list) |
|
|
|
|
|
|
|
def setup_demo(): |
|
"""Configure l'interface Gradio pour le serveur MCP""" |
|
|
|
|
|
def test_file_info(): |
|
return get_figma_file_info() |
|
|
|
def test_comments(): |
|
return get_figma_comments() |
|
|
|
def test_user(): |
|
return get_figma_user_info() |
|
|
|
with gr.Blocks( |
|
title="🎨 Figma MCP Server", |
|
theme=gr.themes.Soft(), |
|
) as demo: |
|
|
|
gr.Markdown(""" |
|
# 🎨 Figma MCP Server |
|
**Serveur MCP pour contrôler Figma via Claude/Cursor avec l'API REST** |
|
|
|
## 📋 **Instructions de configuration :** |
|
|
|
### 1. **Obtenir un token Figma :** |
|
- Aller sur [Figma Settings > Personal Access Tokens](https://www.figma.com/settings) |
|
- Créer un nouveau token |
|
- Copier le token (commence par `figd_` ou `figc_`) |
|
|
|
### 2. **Obtenir l'ID d'un fichier :** |
|
- Ouvrir votre fichier Figma |
|
- Copier l'ID depuis l'URL : `https://www.figma.com/file/FILE_ID/nom-du-fichier` |
|
|
|
### 3. **Configurer Claude/Cursor :** |
|
```json |
|
{ |
|
"mcpServers": { |
|
"figma": { |
|
"command": "sse", |
|
"args": ["https://aktraiser-sigma.hf.space/gradio_api/mcp/sse"] |
|
} |
|
} |
|
} |
|
``` |
|
""") |
|
|
|
|
|
with gr.Tab("🧪 Test"): |
|
with gr.Row(): |
|
token_input = gr.Textbox( |
|
placeholder="figd_...", |
|
label="Token Figma", |
|
type="password" |
|
) |
|
token_btn = gr.Button("Configurer Token") |
|
|
|
with gr.Row(): |
|
file_input = gr.Textbox( |
|
placeholder="ID du fichier", |
|
label="ID du fichier Figma" |
|
) |
|
file_btn = gr.Button("Configurer Fichier") |
|
|
|
status_output = gr.Textbox(label="Status", lines=3) |
|
|
|
|
|
with gr.Row(): |
|
test_info_btn = gr.Button("📄 Info Fichier") |
|
test_comments_btn = gr.Button("📝 Commentaires") |
|
test_user_btn = gr.Button("👤 Info Utilisateur") |
|
|
|
|
|
token_btn.click( |
|
configure_figma_token, |
|
inputs=[token_input], |
|
outputs=[status_output] |
|
) |
|
|
|
file_btn.click( |
|
configure_figma_file_id, |
|
inputs=[file_input], |
|
outputs=[status_output] |
|
) |
|
|
|
test_info_btn.click( |
|
test_file_info, |
|
outputs=[status_output] |
|
) |
|
|
|
test_comments_btn.click( |
|
test_comments, |
|
outputs=[status_output] |
|
) |
|
|
|
test_user_btn.click( |
|
test_user, |
|
outputs=[status_output] |
|
) |
|
|
|
gr.Markdown(""" |
|
--- |
|
### 🛠️ **Outils MCP disponibles :** |
|
- `configure_figma_token(token)` - Configure le token d'accès |
|
- `configure_figma_file_id(file_id)` - Configure l'ID du fichier |
|
- `get_figma_file_info()` - Récupère les infos du fichier |
|
- `create_figma_rectangle(x, y, width, height, name, color)` - Crée un rectangle |
|
- `create_figma_frame(x, y, width, height, name)` - Crée un frame |
|
- `create_figma_text(x, y, text, name, font_size)` - Crée un texte |
|
- `get_figma_comments()` - Récupère les commentaires |
|
- `get_figma_user_info()` - Info utilisateur connecté |
|
""") |
|
|
|
return demo |
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
demo = setup_demo() |
|
|
|
|
|
demo.launch( |
|
mcp_server=True, |
|
server_name="0.0.0.0", |
|
server_port=7860, |
|
share=False, |
|
show_error=True |
|
) |