sigma / app.py
Aktraiser
new
9f18da6
raw
history blame
14.8 kB
#!/usr/bin/env python3
"""
🎨 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
# Configuration du logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Configuration Figma API
FIGMA_API_BASE = "https://api.figma.com/v1"
# === CONFIGURATION ET ÉTAT ===
# Variables globales pour stocker la configuration
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
# Test de connexion
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
# Test d'accès au fichier
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"
# === FONCTIONS API FIGMA ===
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)}"}
# === OUTILS MCP FIGMA ===
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)
# Créer un commentaire avec les instructions de création
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]: # Limiter à 10 commentaires
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]: # Limiter à 10 projets
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)
# === CONFIGURATION DE L'APPLICATION GRADIO ===
def setup_demo():
"""Configure l'interface Gradio pour le serveur MCP"""
# Fonctions wrapper pour éviter les problèmes avec les lambdas
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"]
}
}
}
```
""")
# Interface de test (optionnelle)
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)
# Actions de test
with gr.Row():
test_info_btn = gr.Button("📄 Info Fichier")
test_comments_btn = gr.Button("📝 Commentaires")
test_user_btn = gr.Button("👤 Info Utilisateur")
# Connexions des événements
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
# === LANCEMENT DE L'APPLICATION ===
if __name__ == "__main__":
demo = setup_demo()
# Configuration pour Hugging Face Spaces avec MCP
demo.launch(
mcp_server=True, # 🔑 Active le serveur MCP !
server_name="0.0.0.0",
server_port=7860,
share=False,
show_error=True
)