Aktraiser
commited on
Commit
·
f7dd4c5
1
Parent(s):
7be0187
🚀 Serveur MCP Figma avec API REST - Version finale avec vraie intégration Figma
Browse files- README.md +148 -26
- app.py +348 -343
- requirements.txt +3 -2
README.md
CHANGED
|
@@ -9,56 +9,178 @@ app_file: app.py
|
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
-
# 🎨
|
| 13 |
|
| 14 |
-
Serveur MCP
|
| 15 |
|
| 16 |
-
##
|
| 17 |
|
| 18 |
-
**
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
```json
|
| 23 |
{
|
| 24 |
"mcpServers": {
|
| 25 |
"figma": {
|
| 26 |
-
"command": "
|
| 27 |
-
"args": [
|
| 28 |
-
"mcp-remote",
|
| 29 |
-
"https://aktraiser-sigma.hf.space/gradio_api/mcp/sse"
|
| 30 |
-
]
|
| 31 |
}
|
| 32 |
}
|
| 33 |
}
|
| 34 |
```
|
| 35 |
|
| 36 |
-
###
|
|
|
|
|
|
|
| 37 |
|
| 38 |
```json
|
| 39 |
{
|
| 40 |
-
"
|
| 41 |
-
"figma":
|
| 42 |
-
"transport": "sse",
|
| 43 |
-
"url": "https://aktraiser-sigma.hf.space/gradio_api/mcp/sse"
|
| 44 |
-
}
|
| 45 |
}
|
| 46 |
}
|
| 47 |
```
|
| 48 |
|
| 49 |
-
## 🛠️ Outils disponibles
|
| 50 |
|
| 51 |
-
|
| 52 |
-
- `
|
| 53 |
-
- `
|
| 54 |
-
- `set_figma_fill_color` - Couleurs
|
| 55 |
-
- `move_figma_node` / `resize_figma_node` - Modifications
|
| 56 |
-
- `delete_figma_node` - Suppression
|
| 57 |
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
```
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
```
|
| 63 |
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# 🎨 Figma MCP Server
|
| 13 |
|
| 14 |
+
**Serveur MCP pour contrôler Figma via Claude/Cursor avec l'API REST officielle**
|
| 15 |
|
| 16 |
+
## 🚀 Fonctionnalités
|
| 17 |
|
| 18 |
+
- ✅ **Authentification Figma** avec Personal Access Token
|
| 19 |
+
- ✅ **Vraie API REST Figma** pour toutes les opérations
|
| 20 |
+
- ✅ **Serveur MCP** compatible Claude Desktop et Cursor
|
| 21 |
+
- ✅ **Hébergé sur HF Spaces** - pas d'installation locale
|
| 22 |
+
- ✅ **Interface de test** intégrée
|
| 23 |
|
| 24 |
+
## 🏗️ Architecture
|
| 25 |
+
|
| 26 |
+
```
|
| 27 |
+
Claude/Cursor ←→ MCP (SSE) ←→ Gradio Server ←→ API REST Figma
|
| 28 |
+
https://...sse HF Spaces api.figma.com
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
## 🔧 Configuration
|
| 32 |
+
|
| 33 |
+
### 1. **Obtenir un Personal Access Token Figma**
|
| 34 |
+
|
| 35 |
+
1. Aller sur [Figma Settings > Personal Access Tokens](https://www.figma.com/settings)
|
| 36 |
+
2. Cliquer sur "Generate new token"
|
| 37 |
+
3. Donner un nom à votre token
|
| 38 |
+
4. Copier le token (commence par `figd_` ou `figc_`)
|
| 39 |
+
|
| 40 |
+
### 2. **Obtenir l'ID de votre fichier Figma**
|
| 41 |
+
|
| 42 |
+
1. Ouvrir votre fichier Figma dans le navigateur
|
| 43 |
+
2. Copier l'ID depuis l'URL : `https://www.figma.com/file/FILE_ID/nom-du-fichier`
|
| 44 |
+
3. L'ID est la partie entre `/file/` et `/`
|
| 45 |
+
|
| 46 |
+
### 3. **Configurer Claude Desktop**
|
| 47 |
+
|
| 48 |
+
Ajouter dans `claude_desktop_config.json` :
|
| 49 |
|
| 50 |
```json
|
| 51 |
{
|
| 52 |
"mcpServers": {
|
| 53 |
"figma": {
|
| 54 |
+
"command": "sse",
|
| 55 |
+
"args": ["https://aktraiser-sigma.hf.space/gradio_api/mcp/sse"]
|
|
|
|
|
|
|
|
|
|
| 56 |
}
|
| 57 |
}
|
| 58 |
}
|
| 59 |
```
|
| 60 |
|
| 61 |
+
### 4. **Configurer Cursor**
|
| 62 |
+
|
| 63 |
+
Ajouter dans `.cursorrules` ou les paramètres MCP :
|
| 64 |
|
| 65 |
```json
|
| 66 |
{
|
| 67 |
+
"mcp": {
|
| 68 |
+
"figma": "https://aktraiser-sigma.hf.space/gradio_api/mcp/sse"
|
|
|
|
|
|
|
|
|
|
| 69 |
}
|
| 70 |
}
|
| 71 |
```
|
| 72 |
|
| 73 |
+
## 🛠️ Outils MCP disponibles
|
| 74 |
|
| 75 |
+
### Configuration
|
| 76 |
+
- `configure_figma_token(token)` - Configure le token d'accès Figma
|
| 77 |
+
- `configure_figma_file_id(file_id)` - Configure l'ID du fichier à utiliser
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
+
### Informations
|
| 80 |
+
- `get_figma_file_info(file_id?)` - Récupère les infos d'un fichier
|
| 81 |
+
- `get_figma_user_info()` - Info de l'utilisateur connecté
|
| 82 |
+
- `get_figma_comments(file_id?)` - Liste les commentaires
|
| 83 |
+
|
| 84 |
+
### Création d'éléments (via commentaires)
|
| 85 |
+
- `create_figma_rectangle(x, y, width, height, name?, color?)` - Crée un rectangle
|
| 86 |
+
- `create_figma_frame(x, y, width, height, name?)` - Crée un frame
|
| 87 |
+
- `create_figma_text(x, y, text, name?, font_size?)` - Crée un texte
|
| 88 |
+
|
| 89 |
+
### Projets et équipes
|
| 90 |
+
- `list_figma_team_projects(team_id?)` - Liste les projets d'une équipe
|
| 91 |
+
|
| 92 |
+
## 📝 Utilisation
|
| 93 |
+
|
| 94 |
+
### Exemple avec Claude
|
| 95 |
|
| 96 |
```
|
| 97 |
+
User: Configure mon token Figma et commence à créer un design
|
| 98 |
+
|
| 99 |
+
Claude: Je vais d'abord configurer votre token Figma. Pouvez-vous me donner votre Personal Access Token ?
|
| 100 |
+
|
| 101 |
+
[Après avoir reçu le token]
|
| 102 |
+
|
| 103 |
+
configure_figma_token("figd_your_token_here")
|
| 104 |
+
|
| 105 |
+
Maintenant, donnez-moi l'ID de votre fichier Figma...
|
| 106 |
+
|
| 107 |
+
configure_figma_file_id("YOUR_FILE_ID")
|
| 108 |
+
|
| 109 |
+
Parfait ! Je peux maintenant créer des éléments. Créons un rectangle rouge :
|
| 110 |
+
|
| 111 |
+
create_figma_rectangle("100", "100", "200", "150", "Mon Rectangle", "#FF0000")
|
| 112 |
```
|
| 113 |
|
| 114 |
+
## 🧪 Interface de test
|
| 115 |
+
|
| 116 |
+
L'application inclut une interface de test accessible sur :
|
| 117 |
+
https://aktraiser-sigma.hf.space
|
| 118 |
+
|
| 119 |
+
Cette interface permet de :
|
| 120 |
+
- Tester la configuration du token
|
| 121 |
+
- Vérifier l'accès aux fichiers
|
| 122 |
+
- Afficher les informations du fichier
|
| 123 |
+
- Lister les commentaires
|
| 124 |
+
|
| 125 |
+
## ⚠️ Limitations actuelles
|
| 126 |
+
|
| 127 |
+
### Création d'éléments
|
| 128 |
+
Pour l'instant, la création d'éléments (rectangles, frames, texte) se fait via **commentaires** car l'API REST Figma ne permet pas de créer directement des nœuds. Les commentaires apparaîtront dans votre fichier Figma avec les instructions de création.
|
| 129 |
+
|
| 130 |
+
### Permissions
|
| 131 |
+
- Vous devez avoir accès en écriture au fichier
|
| 132 |
+
- Le token doit avoir les bonnes permissions (défini lors de la création)
|
| 133 |
+
|
| 134 |
+
## 🔄 Fonctionnement
|
| 135 |
+
|
| 136 |
+
1. **Claude/Cursor** appelle les outils MCP
|
| 137 |
+
2. **Serveur MCP** reçoit la requête via SSE
|
| 138 |
+
3. **Gradio** traite la requête et appelle l'**API REST Figma**
|
| 139 |
+
4. **Figma** renvoie la réponse
|
| 140 |
+
5. **Serveur MCP** retourne le résultat à **Claude/Cursor**
|
| 141 |
+
|
| 142 |
+
## 🚀 Déploiement
|
| 143 |
+
|
| 144 |
+
Le serveur est automatiquement déployé sur Hugging Face Spaces :
|
| 145 |
+
- **URL MCP :** `https://aktraiser-sigma.hf.space/gradio_api/mcp/sse`
|
| 146 |
+
- **Interface web :** `https://aktraiser-sigma.hf.space`
|
| 147 |
+
|
| 148 |
+
## 🔒 Sécurité
|
| 149 |
+
|
| 150 |
+
- Les tokens ne sont **jamais stockés** de façon persistante
|
| 151 |
+
- Ils ne sont conservés qu'en mémoire pendant la session
|
| 152 |
+
- Utilisez toujours des tokens avec des permissions limitées
|
| 153 |
+
|
| 154 |
+
## 📚 API Figma
|
| 155 |
+
|
| 156 |
+
Documentation officielle : https://www.figma.com/developers/api
|
| 157 |
+
|
| 158 |
+
## 🐛 Dépannage
|
| 159 |
+
|
| 160 |
+
### Erreurs courantes
|
| 161 |
+
|
| 162 |
+
**Token invalide**
|
| 163 |
+
```
|
| 164 |
+
❌ Token invalide. Le token doit commencer par 'figd_' ou 'figc_'
|
| 165 |
+
```
|
| 166 |
+
→ Vérifiez que votre token est correct
|
| 167 |
+
|
| 168 |
+
**Fichier inaccessible**
|
| 169 |
+
```
|
| 170 |
+
❌ Impossible d'accéder au fichier : 403
|
| 171 |
+
```
|
| 172 |
+
→ Vérifiez que vous avez les permissions sur le fichier
|
| 173 |
+
|
| 174 |
+
**Token non configuré**
|
| 175 |
+
```
|
| 176 |
+
❌ Token Figma non configuré
|
| 177 |
+
```
|
| 178 |
+
→ Appelez `configure_figma_token()` d'abord
|
| 179 |
+
|
| 180 |
+
## 🤝 Contribution
|
| 181 |
+
|
| 182 |
+
Ce projet est open source ! N'hésitez pas à contribuer.
|
| 183 |
+
|
| 184 |
+
## 📄 Licence
|
| 185 |
+
|
| 186 |
+
MIT License
|
app.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
🎨 Figma MCP Server - Hébergé sur Hugging Face Spaces
|
| 4 |
-
Serveur MCP pour contrôler Figma via Claude/Cursor
|
| 5 |
"""
|
| 6 |
import gradio as gr
|
| 7 |
import asyncio
|
| 8 |
import json
|
| 9 |
-
import uuid
|
| 10 |
import logging
|
|
|
|
| 11 |
from typing import Dict, Any, Optional, List
|
| 12 |
from PIL import Image
|
| 13 |
import base64
|
|
@@ -17,389 +17,394 @@ import io
|
|
| 17 |
logging.basicConfig(level=logging.INFO)
|
| 18 |
logger = logging.getLogger(__name__)
|
| 19 |
|
| 20 |
-
#
|
| 21 |
-
|
| 22 |
-
channels: Dict[str, List[str]] = {}
|
| 23 |
|
| 24 |
-
# ===
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
}
|
| 37 |
-
connected_clients.append(client_info)
|
| 38 |
-
|
| 39 |
-
# Ajouter au canal
|
| 40 |
-
if channel_name not in channels:
|
| 41 |
-
channels[channel_name] = []
|
| 42 |
-
channels[channel_name].append(client_info["id"])
|
| 43 |
-
|
| 44 |
-
logger.info(f"Plugin rejoint le canal {channel_name}")
|
| 45 |
-
return json.dumps({
|
| 46 |
-
"type": "system",
|
| 47 |
-
"channel": channel_name,
|
| 48 |
-
"message": {
|
| 49 |
-
"result": f"Joined channel {channel_name}",
|
| 50 |
-
"channel": channel_name
|
| 51 |
-
}
|
| 52 |
-
})
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
connected_clients[:] = [c for c in connected_clients if c.get("channel") != channel_name]
|
| 57 |
-
if channel_name in channels:
|
| 58 |
-
del channels[channel_name]
|
| 59 |
-
|
| 60 |
-
return json.dumps({
|
| 61 |
-
"type": "system",
|
| 62 |
-
"message": {"result": f"Disconnected from channel {channel_name}"}
|
| 63 |
-
})
|
| 64 |
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
"""Pont entre les outils MCP et les commandes Figma"""
|
| 69 |
try:
|
| 70 |
-
|
|
|
|
| 71 |
|
| 72 |
-
if
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
return
|
| 76 |
-
elif command == "create_rectangle":
|
| 77 |
-
return json.dumps({"type": "result", "data": {"message": "Rectangle créé", "params": params}})
|
| 78 |
-
elif command == "create_frame":
|
| 79 |
-
return json.dumps({"type": "result", "data": {"message": "Frame créé", "params": params}})
|
| 80 |
-
elif command == "create_text":
|
| 81 |
-
return json.dumps({"type": "result", "data": {"message": "Texte créé", "params": params}})
|
| 82 |
-
elif command == "set_fill_color":
|
| 83 |
-
return json.dumps({"type": "result", "data": {"message": "Couleur définie", "params": params}})
|
| 84 |
else:
|
| 85 |
-
return
|
| 86 |
|
| 87 |
except Exception as e:
|
| 88 |
-
return
|
| 89 |
-
|
| 90 |
-
# === OUTILS MCP POUR FIGMA ===
|
| 91 |
|
| 92 |
-
def
|
| 93 |
-
"""
|
| 94 |
-
|
| 95 |
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
-
|
| 116 |
-
"""
|
| 117 |
-
Récupère les informations sur la sélection actuelle dans Figma.
|
| 118 |
-
|
| 119 |
-
Returns:
|
| 120 |
-
str: Informations de la sélection en format JSON
|
| 121 |
-
"""
|
| 122 |
-
result = execute_figma_command_bridge("get_selection", "{}")
|
| 123 |
-
return f"🎯 Commande get_selection envoyée au plugin Figma\n{result}"
|
| 124 |
|
| 125 |
-
def
|
| 126 |
-
"""
|
| 127 |
-
|
|
|
|
| 128 |
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
Returns:
|
| 133 |
-
str: Informations du nœud en format JSON
|
| 134 |
-
"""
|
| 135 |
-
params = json.dumps({"nodeId": node_id})
|
| 136 |
-
result = execute_figma_command_bridge("get_node_info", params)
|
| 137 |
-
return f"🔍 Commande get_node_info pour {node_id} envoyée au plugin Figma\n{result}"
|
| 138 |
-
|
| 139 |
-
def create_figma_rectangle(x: str, y: str, width: str, height: str, name: str = "Rectangle", parent_id: str = "") -> str:
|
| 140 |
-
"""
|
| 141 |
-
Crée un nouveau rectangle dans Figma.
|
| 142 |
-
|
| 143 |
-
Args:
|
| 144 |
-
x (str): Position X du rectangle
|
| 145 |
-
y (str): Position Y du rectangle
|
| 146 |
-
width (str): Largeur du rectangle
|
| 147 |
-
height (str): Hauteur du rectangle
|
| 148 |
-
name (str): Nom optionnel du rectangle
|
| 149 |
-
parent_id (str): ID du nœud parent (optionnel)
|
| 150 |
-
|
| 151 |
-
Returns:
|
| 152 |
-
str: Informations du rectangle créé
|
| 153 |
-
"""
|
| 154 |
-
params = {
|
| 155 |
-
"x": float(x),
|
| 156 |
-
"y": float(y),
|
| 157 |
-
"width": float(width),
|
| 158 |
-
"height": float(height),
|
| 159 |
-
"name": name
|
| 160 |
}
|
| 161 |
-
if parent_id:
|
| 162 |
-
params["parentId"] = parent_id
|
| 163 |
|
| 164 |
-
|
| 165 |
-
return f"🟦 Rectangle {name} créé ({width}x{height}) à ({x},{y})\n{result}"
|
| 166 |
-
|
| 167 |
-
def create_figma_frame(x: str, y: str, width: str, height: str, name: str = "Frame", parent_id: str = "") -> str:
|
| 168 |
-
"""
|
| 169 |
-
Crée un nouveau frame (conteneur) dans Figma.
|
| 170 |
-
|
| 171 |
-
Args:
|
| 172 |
-
x (str): Position X du frame
|
| 173 |
-
y (str): Position Y du frame
|
| 174 |
-
width (str): Largeur du frame
|
| 175 |
-
height (str): Hauteur du frame
|
| 176 |
-
name (str): Nom optionnel du frame
|
| 177 |
-
parent_id (str): ID du nœud parent (optionnel)
|
| 178 |
-
|
| 179 |
-
Returns:
|
| 180 |
-
str: Informations du frame créé avec son ID
|
| 181 |
-
"""
|
| 182 |
-
params = {
|
| 183 |
-
"x": float(x),
|
| 184 |
-
"y": float(y),
|
| 185 |
-
"width": float(width),
|
| 186 |
-
"height": float(height),
|
| 187 |
-
"name": name
|
| 188 |
-
}
|
| 189 |
-
if parent_id:
|
| 190 |
-
params["parentId"] = parent_id
|
| 191 |
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
font_size (str): Taille de la police (défaut: 14)
|
| 204 |
-
name (str): Nom optionnel de l'élément texte
|
| 205 |
-
parent_id (str): ID du nœud parent (optionnel)
|
| 206 |
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
"
|
| 214 |
-
|
| 215 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
}
|
| 217 |
-
if parent_id:
|
| 218 |
-
params["parentId"] = parent_id
|
| 219 |
|
| 220 |
-
|
| 221 |
-
return f"📝 Texte '{text}' créé (taille {font_size}) à ({x},{y})\n{result}"
|
| 222 |
|
| 223 |
-
def
|
| 224 |
-
"""
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
|
|
|
| 233 |
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
"r": float(r),
|
| 241 |
-
"g": float(g),
|
| 242 |
-
"b": float(b),
|
| 243 |
-
"a": float(a)
|
| 244 |
}
|
| 245 |
-
}
|
| 246 |
-
result = execute_figma_command_bridge("set_fill_color", json.dumps(params))
|
| 247 |
-
return f"🎨 Couleur RGBA({r},{g},{b},{a}) appliquée au nœud {node_id}\n{result}"
|
| 248 |
-
|
| 249 |
-
def move_figma_node(node_id: str, x: str, y: str) -> str:
|
| 250 |
-
"""
|
| 251 |
-
Déplace un nœud Figma vers une nouvelle position.
|
| 252 |
-
|
| 253 |
-
Args:
|
| 254 |
-
node_id (str): ID du nœud à déplacer
|
| 255 |
-
x (str): Nouvelle position X
|
| 256 |
-
y (str): Nouvelle position Y
|
| 257 |
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
"
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
return f"📐 Nœud {node_id} déplacé vers ({x},{y})\n{result}"
|
| 268 |
|
| 269 |
-
def
|
| 270 |
-
"""
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
|
| 289 |
-
def
|
| 290 |
-
"""
|
| 291 |
-
|
|
|
|
| 292 |
|
| 293 |
-
|
| 294 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
|
|
|
|
|
|
| 301 |
|
| 302 |
-
def
|
| 303 |
-
"""
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
result =
|
| 310 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
|
| 312 |
-
def
|
| 313 |
-
"""
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
|
| 322 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
|
| 324 |
-
|
| 325 |
-
"""
|
| 326 |
-
Vérifie l'état de santé du serveur MCP Figma.
|
| 327 |
-
|
| 328 |
-
Returns:
|
| 329 |
-
str: État de la connexion
|
| 330 |
-
"""
|
| 331 |
-
client_count = len(connected_clients)
|
| 332 |
-
channel_count = len(channels)
|
| 333 |
-
return f"✅ Serveur MCP Figma actif\n📱 {client_count} clients connectés\n📡 {channel_count} canaux actifs"
|
| 334 |
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
gr.Markdown("# 🎨 Serveur MCP Figma")
|
| 338 |
-
gr.Markdown("Serveur MCP hébergé sur Hugging Face Spaces pour contrôler Figma via Claude/Cursor")
|
| 339 |
|
| 340 |
-
with gr.
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
with gr.Tab("🔌 Communication Plugin"):
|
| 345 |
-
gr.Markdown("### Interface de communication avec le plugin Figma")
|
| 346 |
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
)
|
| 358 |
-
params_input = gr.Textbox(label="Paramètres JSON", placeholder='{"x": 10, "y": 20}')
|
| 359 |
-
execute_btn = gr.Button("Exécuter Commande")
|
| 360 |
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
Claude/Cursor ←→ MCP ←→ Gradio Server ←→ Interface Plugin ←→ Plugin Figma
|
| 383 |
-
```
|
| 384 |
-
""")
|
| 385 |
-
|
| 386 |
-
# Event handlers
|
| 387 |
-
status_btn.click(health_check, outputs=[status_output])
|
| 388 |
-
join_btn.click(
|
| 389 |
-
lambda channel: handle_plugin_connection(channel, "join"),
|
| 390 |
-
inputs=[channel_input],
|
| 391 |
-
outputs=[plugin_output]
|
| 392 |
-
)
|
| 393 |
-
execute_btn.click(
|
| 394 |
-
execute_figma_command_bridge,
|
| 395 |
-
inputs=[command_input, params_input],
|
| 396 |
-
outputs=[command_output]
|
| 397 |
-
)
|
| 398 |
|
| 399 |
if __name__ == "__main__":
|
| 400 |
-
|
|
|
|
|
|
|
| 401 |
demo.launch(
|
| 402 |
-
mcp_server=True, # Active le serveur MCP
|
| 403 |
server_name="0.0.0.0",
|
| 404 |
server_port=7860,
|
| 405 |
share=False,
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
🎨 Figma MCP Server - Hébergé sur Hugging Face Spaces
|
| 4 |
+
Serveur MCP pour contrôler Figma via Claude/Cursor avec la vraie API REST
|
| 5 |
"""
|
| 6 |
import gradio as gr
|
| 7 |
import asyncio
|
| 8 |
import json
|
|
|
|
| 9 |
import logging
|
| 10 |
+
import requests
|
| 11 |
from typing import Dict, Any, Optional, List
|
| 12 |
from PIL import Image
|
| 13 |
import base64
|
|
|
|
| 17 |
logging.basicConfig(level=logging.INFO)
|
| 18 |
logger = logging.getLogger(__name__)
|
| 19 |
|
| 20 |
+
# Configuration Figma API
|
| 21 |
+
FIGMA_API_BASE = "https://api.figma.com/v1"
|
|
|
|
| 22 |
|
| 23 |
+
# === CONFIGURATION ET ÉTAT ===
|
| 24 |
|
| 25 |
+
# Variables globales pour stocker la configuration
|
| 26 |
+
figma_config = {
|
| 27 |
+
"token": None,
|
| 28 |
+
"file_id": None,
|
| 29 |
+
"team_id": None
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
def configure_figma_token(token: str) -> str:
|
| 33 |
+
"""Configure le token d'accès Figma"""
|
| 34 |
+
global figma_config
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
if not token or not token.startswith(('figd_', 'figc_')):
|
| 37 |
+
return "❌ Token invalide. Le token doit commencer par 'figd_' ou 'figc_'"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
+
figma_config["token"] = token
|
| 40 |
+
|
| 41 |
+
# Test de connexion
|
|
|
|
| 42 |
try:
|
| 43 |
+
headers = {"X-Figma-Token": token}
|
| 44 |
+
response = requests.get(f"{FIGMA_API_BASE}/me", headers=headers, timeout=10)
|
| 45 |
|
| 46 |
+
if response.status_code == 200:
|
| 47 |
+
user_data = response.json()
|
| 48 |
+
username = user_data.get("handle", "Utilisateur inconnu")
|
| 49 |
+
return f"✅ Token configuré avec succès ! Connecté en tant que : {username}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
else:
|
| 51 |
+
return f"❌ Erreur lors de la vérification du token : {response.status_code}"
|
| 52 |
|
| 53 |
except Exception as e:
|
| 54 |
+
return f"❌ Erreur de connexion à l'API Figma : {str(e)}"
|
|
|
|
|
|
|
| 55 |
|
| 56 |
+
def configure_figma_file_id(file_id: str) -> str:
|
| 57 |
+
"""Configure l'ID du fichier Figma à utiliser"""
|
| 58 |
+
global figma_config
|
| 59 |
|
| 60 |
+
if not file_id:
|
| 61 |
+
return "❌ L'ID du fichier est requis"
|
| 62 |
+
|
| 63 |
+
figma_config["file_id"] = file_id
|
| 64 |
+
|
| 65 |
+
# Test d'accès au fichier
|
| 66 |
+
if figma_config["token"]:
|
| 67 |
+
try:
|
| 68 |
+
headers = {"X-Figma-Token": figma_config["token"]}
|
| 69 |
+
response = requests.get(f"{FIGMA_API_BASE}/files/{file_id}", headers=headers, timeout=10)
|
| 70 |
+
|
| 71 |
+
if response.status_code == 200:
|
| 72 |
+
file_data = response.json()
|
| 73 |
+
file_name = file_data.get("name", "Fichier inconnu")
|
| 74 |
+
return f"✅ Fichier configuré avec succès : {file_name}"
|
| 75 |
+
else:
|
| 76 |
+
return f"❌ Impossible d'accéder au fichier : {response.status_code}"
|
| 77 |
+
|
| 78 |
+
except Exception as e:
|
| 79 |
+
return f"❌ Erreur lors de l'accès au fichier : {str(e)}"
|
| 80 |
+
else:
|
| 81 |
+
return "⚠️ ID du fichier configuré, mais token manquant"
|
| 82 |
|
| 83 |
+
# === FONCTIONS API FIGMA ===
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
+
def make_figma_request(endpoint: str, method: str = "GET", data: Dict = None) -> Dict[str, Any]:
|
| 86 |
+
"""Effectue une requête à l'API Figma"""
|
| 87 |
+
if not figma_config["token"]:
|
| 88 |
+
return {"error": "Token Figma non configuré"}
|
| 89 |
|
| 90 |
+
headers = {
|
| 91 |
+
"X-Figma-Token": figma_config["token"],
|
| 92 |
+
"Content-Type": "application/json"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
}
|
|
|
|
|
|
|
| 94 |
|
| 95 |
+
url = f"{FIGMA_API_BASE}/{endpoint}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
+
try:
|
| 98 |
+
if method == "GET":
|
| 99 |
+
response = requests.get(url, headers=headers, timeout=30)
|
| 100 |
+
elif method == "POST":
|
| 101 |
+
response = requests.post(url, headers=headers, json=data, timeout=30)
|
| 102 |
+
elif method == "PUT":
|
| 103 |
+
response = requests.put(url, headers=headers, json=data, timeout=30)
|
| 104 |
+
elif method == "DELETE":
|
| 105 |
+
response = requests.delete(url, headers=headers, timeout=30)
|
| 106 |
+
else:
|
| 107 |
+
return {"error": f"Méthode HTTP non supportée : {method}"}
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
+
if response.status_code in [200, 201]:
|
| 110 |
+
return response.json()
|
| 111 |
+
else:
|
| 112 |
+
return {"error": f"Erreur API {response.status_code}: {response.text}"}
|
| 113 |
+
|
| 114 |
+
except Exception as e:
|
| 115 |
+
return {"error": f"Erreur de requête : {str(e)}"}
|
| 116 |
+
|
| 117 |
+
# === OUTILS MCP FIGMA ===
|
| 118 |
+
|
| 119 |
+
def get_figma_file_info(file_id: str = "") -> str:
|
| 120 |
+
"""Récupère les informations d'un fichier Figma"""
|
| 121 |
+
file_id = file_id or figma_config["file_id"]
|
| 122 |
+
|
| 123 |
+
if not file_id:
|
| 124 |
+
return "❌ ID du fichier requis. Utilisez configure_figma_file_id() d'abord."
|
| 125 |
+
|
| 126 |
+
result = make_figma_request(f"files/{file_id}")
|
| 127 |
+
|
| 128 |
+
if "error" in result:
|
| 129 |
+
return f"❌ Erreur : {result['error']}"
|
| 130 |
+
|
| 131 |
+
file_info = {
|
| 132 |
+
"nom": result.get("name", ""),
|
| 133 |
+
"derniere_modification": result.get("lastModified", ""),
|
| 134 |
+
"version": result.get("version", ""),
|
| 135 |
+
"pages": [page.get("name", "") for page in result.get("document", {}).get("children", [])]
|
| 136 |
}
|
|
|
|
|
|
|
| 137 |
|
| 138 |
+
return f"📄 **Fichier Figma :**\n{json.dumps(file_info, indent=2, ensure_ascii=False)}"
|
|
|
|
| 139 |
|
| 140 |
+
def create_figma_rectangle(x: str, y: str, width: str, height: str, name: str = "Rectangle", color: str = "#FF0000") -> str:
|
| 141 |
+
"""Crée un rectangle dans Figma (via commentaire pour notification)"""
|
| 142 |
+
if not figma_config["file_id"]:
|
| 143 |
+
return "❌ ID du fichier requis. Utilisez configure_figma_file_id() d'abord."
|
| 144 |
+
|
| 145 |
+
try:
|
| 146 |
+
x_pos, y_pos = float(x), float(y)
|
| 147 |
+
w, h = float(width), float(height)
|
| 148 |
+
|
| 149 |
+
# Créer un commentaire avec les instructions de création
|
| 150 |
+
comment_text = f"🟦 **Rectangle à créer :**\n- Nom: {name}\n- Position: ({x_pos}, {y_pos})\n- Taille: {w}x{h}\n- Couleur: {color}"
|
| 151 |
|
| 152 |
+
comment_data = {
|
| 153 |
+
"message": comment_text,
|
| 154 |
+
"client_meta": {
|
| 155 |
+
"x": x_pos,
|
| 156 |
+
"y": y_pos
|
| 157 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
+
result = make_figma_request(f"files/{figma_config['file_id']}/comments", "POST", comment_data)
|
| 161 |
+
|
| 162 |
+
if "error" in result:
|
| 163 |
+
return f"❌ Erreur lors de la création du commentaire : {result['error']}"
|
| 164 |
+
|
| 165 |
+
return f"✅ Rectangle {name} créé (via commentaire) à ({x_pos}, {y_pos}) avec la taille {w}x{h}"
|
| 166 |
+
|
| 167 |
+
except ValueError:
|
| 168 |
+
return "❌ Les coordonnées et dimensions doivent être des nombres"
|
|
|
|
| 169 |
|
| 170 |
+
def create_figma_frame(x: str, y: str, width: str, height: str, name: str = "Frame") -> str:
|
| 171 |
+
"""Crée un frame dans Figma (via commentaire pour notification)"""
|
| 172 |
+
if not figma_config["file_id"]:
|
| 173 |
+
return "❌ ID du fichier requis. Utilisez configure_figma_file_id() d'abord."
|
| 174 |
+
|
| 175 |
+
try:
|
| 176 |
+
x_pos, y_pos = float(x), float(y)
|
| 177 |
+
w, h = float(width), float(height)
|
| 178 |
|
| 179 |
+
comment_text = f"🖼️ **Frame à créer :**\n- Nom: {name}\n- Position: ({x_pos}, {y_pos})\n- Taille: {w}x{h}"
|
| 180 |
+
|
| 181 |
+
comment_data = {
|
| 182 |
+
"message": comment_text,
|
| 183 |
+
"client_meta": {
|
| 184 |
+
"x": x_pos,
|
| 185 |
+
"y": y_pos
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
result = make_figma_request(f"files/{figma_config['file_id']}/comments", "POST", comment_data)
|
| 190 |
+
|
| 191 |
+
if "error" in result:
|
| 192 |
+
return f"❌ Erreur lors de la création du commentaire : {result['error']}"
|
| 193 |
+
|
| 194 |
+
return f"✅ Frame {name} créé (via commentaire) à ({x_pos}, {y_pos}) avec la taille {w}x{h}"
|
| 195 |
+
|
| 196 |
+
except ValueError:
|
| 197 |
+
return "❌ Les coordonnées et dimensions doivent être des nombres"
|
| 198 |
|
| 199 |
+
def create_figma_text(x: str, y: str, text: str, name: str = "Text", font_size: str = "16") -> str:
|
| 200 |
+
"""Crée un élément texte dans Figma (via commentaire pour notification)"""
|
| 201 |
+
if not figma_config["file_id"]:
|
| 202 |
+
return "❌ ID du fichier requis. Utilisez configure_figma_file_id() d'abord."
|
| 203 |
|
| 204 |
+
try:
|
| 205 |
+
x_pos, y_pos = float(x), float(y)
|
| 206 |
+
size = float(font_size)
|
| 207 |
+
|
| 208 |
+
comment_text = f"📝 **Texte à créer :**\n- Nom: {name}\n- Position: ({x_pos}, {y_pos})\n- Texte: \"{text}\"\n- Taille: {size}px"
|
| 209 |
+
|
| 210 |
+
comment_data = {
|
| 211 |
+
"message": comment_text,
|
| 212 |
+
"client_meta": {
|
| 213 |
+
"x": x_pos,
|
| 214 |
+
"y": y_pos
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
result = make_figma_request(f"files/{figma_config['file_id']}/comments", "POST", comment_data)
|
| 219 |
|
| 220 |
+
if "error" in result:
|
| 221 |
+
return f"❌ Erreur lors de la création du commentaire : {result['error']}"
|
| 222 |
+
|
| 223 |
+
return f"✅ Texte \"{text}\" créé (via commentaire) à ({x_pos}, {y_pos})"
|
| 224 |
+
|
| 225 |
+
except ValueError:
|
| 226 |
+
return "❌ Les coordonnées et la taille doivent être des nombres"
|
| 227 |
|
| 228 |
+
def get_figma_comments(file_id: str = "") -> str:
|
| 229 |
+
"""Récupère tous les commentaires d'un fichier Figma"""
|
| 230 |
+
file_id = file_id or figma_config["file_id"]
|
| 231 |
+
|
| 232 |
+
if not file_id:
|
| 233 |
+
return "❌ ID du fichier requis"
|
| 234 |
+
|
| 235 |
+
result = make_figma_request(f"files/{file_id}/comments")
|
| 236 |
+
|
| 237 |
+
if "error" in result:
|
| 238 |
+
return f"❌ Erreur : {result['error']}"
|
| 239 |
+
|
| 240 |
+
comments = result.get("comments", [])
|
| 241 |
+
|
| 242 |
+
if not comments:
|
| 243 |
+
return "📝 Aucun commentaire trouvé dans ce fichier"
|
| 244 |
+
|
| 245 |
+
comment_list = []
|
| 246 |
+
for comment in comments[:10]: # Limiter à 10 commentaires
|
| 247 |
+
user = comment.get("user", {}).get("handle", "Anonyme")
|
| 248 |
+
message = comment.get("message", "")
|
| 249 |
+
created_at = comment.get("created_at", "")
|
| 250 |
+
comment_list.append(f"👤 {user} ({created_at}): {message}")
|
| 251 |
+
|
| 252 |
+
return f"📝 **Commentaires récents :**\n" + "\n\n".join(comment_list)
|
| 253 |
|
| 254 |
+
def get_figma_user_info() -> str:
|
| 255 |
+
"""Récupère les informations de l'utilisateur connecté"""
|
| 256 |
+
result = make_figma_request("me")
|
| 257 |
+
|
| 258 |
+
if "error" in result:
|
| 259 |
+
return f"❌ Erreur : {result['error']}"
|
| 260 |
+
|
| 261 |
+
user_info = {
|
| 262 |
+
"nom": result.get("handle", ""),
|
| 263 |
+
"email": result.get("email", ""),
|
| 264 |
+
"id": result.get("id", "")
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
return f"👤 **Utilisateur connecté :**\n{json.dumps(user_info, indent=2, ensure_ascii=False)}"
|
| 268 |
|
| 269 |
+
def list_figma_team_projects(team_id: str = "") -> str:
|
| 270 |
+
"""Liste les projets d'une équipe Figma"""
|
| 271 |
+
team_id = team_id or figma_config["team_id"]
|
| 272 |
+
|
| 273 |
+
if not team_id:
|
| 274 |
+
return "❌ ID de l'équipe requis. Configurez-le avec figma_config['team_id'] = 'VOTRE_TEAM_ID'"
|
| 275 |
+
|
| 276 |
+
result = make_figma_request(f"teams/{team_id}/projects")
|
| 277 |
+
|
| 278 |
+
if "error" in result:
|
| 279 |
+
return f"❌ Erreur : {result['error']}"
|
| 280 |
+
|
| 281 |
+
projects = result.get("projects", [])
|
| 282 |
+
|
| 283 |
+
if not projects:
|
| 284 |
+
return "📁 Aucun projet trouvé dans cette équipe"
|
| 285 |
+
|
| 286 |
+
project_list = []
|
| 287 |
+
for project in projects[:10]: # Limiter à 10 projets
|
| 288 |
+
name = project.get("name", "Sans nom")
|
| 289 |
+
project_id = project.get("id", "")
|
| 290 |
+
project_list.append(f"📁 {name} (ID: {project_id})")
|
| 291 |
+
|
| 292 |
+
return f"📁 **Projets de l'équipe :**\n" + "\n".join(project_list)
|
| 293 |
|
| 294 |
+
# === CONFIGURATION DE L'APPLICATION GRADIO ===
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
|
| 296 |
+
def setup_demo():
|
| 297 |
+
"""Configure l'interface Gradio pour le serveur MCP"""
|
|
|
|
|
|
|
| 298 |
|
| 299 |
+
with gr.Blocks(
|
| 300 |
+
title="🎨 Figma MCP Server",
|
| 301 |
+
theme=gr.themes.Soft(),
|
| 302 |
+
) as demo:
|
|
|
|
|
|
|
| 303 |
|
| 304 |
+
gr.Markdown("""
|
| 305 |
+
# 🎨 Figma MCP Server
|
| 306 |
+
**Serveur MCP pour contrôler Figma via Claude/Cursor avec l'API REST**
|
| 307 |
+
|
| 308 |
+
## 📋 **Instructions de configuration :**
|
| 309 |
+
|
| 310 |
+
### 1. **Obtenir un token Figma :**
|
| 311 |
+
- Aller sur [Figma Settings > Personal Access Tokens](https://www.figma.com/settings)
|
| 312 |
+
- Créer un nouveau token
|
| 313 |
+
- Copier le token (commence par `figd_` ou `figc_`)
|
| 314 |
+
|
| 315 |
+
### 2. **Obtenir l'ID d'un fichier :**
|
| 316 |
+
- Ouvrir votre fichier Figma
|
| 317 |
+
- Copier l'ID depuis l'URL : `https://www.figma.com/file/FILE_ID/nom-du-fichier`
|
| 318 |
|
| 319 |
+
### 3. **Configurer Claude/Cursor :**
|
| 320 |
+
```json
|
| 321 |
+
{
|
| 322 |
+
"mcpServers": {
|
| 323 |
+
"figma": {
|
| 324 |
+
"command": "sse",
|
| 325 |
+
"args": ["https://aktraiser-sigma.hf.space/gradio_api/mcp/sse"]
|
| 326 |
+
}
|
| 327 |
+
}
|
| 328 |
+
}
|
| 329 |
+
```
|
| 330 |
+
""")
|
| 331 |
+
|
| 332 |
+
# Interface de test (optionnelle)
|
| 333 |
+
with gr.Tab("🧪 Test"):
|
| 334 |
+
with gr.Row():
|
| 335 |
+
token_input = gr.Textbox(
|
| 336 |
+
placeholder="figd_...",
|
| 337 |
+
label="Token Figma",
|
| 338 |
+
type="password"
|
| 339 |
+
)
|
| 340 |
+
token_btn = gr.Button("Configurer Token")
|
| 341 |
+
|
| 342 |
+
with gr.Row():
|
| 343 |
+
file_input = gr.Textbox(
|
| 344 |
+
placeholder="ID du fichier",
|
| 345 |
+
label="ID du fichier Figma"
|
| 346 |
+
)
|
| 347 |
+
file_btn = gr.Button("Configurer Fichier")
|
| 348 |
+
|
| 349 |
+
status_output = gr.Textbox(label="Status", lines=3)
|
| 350 |
+
|
| 351 |
+
# Actions de test
|
| 352 |
+
with gr.Row():
|
| 353 |
+
test_info_btn = gr.Button("📄 Info Fichier")
|
| 354 |
+
test_comments_btn = gr.Button("📝 Commentaires")
|
| 355 |
+
test_user_btn = gr.Button("👤 Info Utilisateur")
|
| 356 |
+
|
| 357 |
+
# Connexions des événements
|
| 358 |
+
token_btn.click(
|
| 359 |
+
configure_figma_token,
|
| 360 |
+
inputs=[token_input],
|
| 361 |
+
outputs=[status_output]
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
file_btn.click(
|
| 365 |
+
configure_figma_file_id,
|
| 366 |
+
inputs=[file_input],
|
| 367 |
+
outputs=[status_output]
|
| 368 |
+
)
|
| 369 |
+
|
| 370 |
+
test_info_btn.click(
|
| 371 |
+
lambda: get_figma_file_info(),
|
| 372 |
+
outputs=[status_output]
|
| 373 |
+
)
|
| 374 |
+
|
| 375 |
+
test_comments_btn.click(
|
| 376 |
+
lambda: get_figma_comments(),
|
| 377 |
+
outputs=[status_output]
|
| 378 |
)
|
|
|
|
|
|
|
| 379 |
|
| 380 |
+
test_user_btn.click(
|
| 381 |
+
get_figma_user_info,
|
| 382 |
+
outputs=[status_output]
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
gr.Markdown("""
|
| 386 |
+
---
|
| 387 |
+
### 🛠️ **Outils MCP disponibles :**
|
| 388 |
+
- `configure_figma_token(token)` - Configure le token d'accès
|
| 389 |
+
- `configure_figma_file_id(file_id)` - Configure l'ID du fichier
|
| 390 |
+
- `get_figma_file_info()` - Récupère les infos du fichier
|
| 391 |
+
- `create_figma_rectangle(x, y, width, height, name, color)` - Crée un rectangle
|
| 392 |
+
- `create_figma_frame(x, y, width, height, name)` - Crée un frame
|
| 393 |
+
- `create_figma_text(x, y, text, name, font_size)` - Crée un texte
|
| 394 |
+
- `get_figma_comments()` - Récupère les commentaires
|
| 395 |
+
- `get_figma_user_info()` - Info utilisateur connecté
|
| 396 |
+
""")
|
| 397 |
+
|
| 398 |
+
return demo
|
| 399 |
+
|
| 400 |
+
# === LANCEMENT DE L'APPLICATION ===
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
|
| 402 |
if __name__ == "__main__":
|
| 403 |
+
demo = setup_demo()
|
| 404 |
+
|
| 405 |
+
# Configuration pour Hugging Face Spaces avec MCP
|
| 406 |
demo.launch(
|
| 407 |
+
mcp_server=True, # 🔑 Active le serveur MCP !
|
| 408 |
server_name="0.0.0.0",
|
| 409 |
server_port=7860,
|
| 410 |
share=False,
|
requirements.txt
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
gradio[mcp]>=5.3.0
|
| 2 |
-
|
| 3 |
pydantic==2.5.0
|
| 4 |
-
pillow
|
|
|
|
|
|
| 1 |
gradio[mcp]>=5.3.0
|
| 2 |
+
requests>=2.31.0
|
| 3 |
pydantic==2.5.0
|
| 4 |
+
pillow>=10.0.0
|
| 5 |
+
typing-extensions>=4.8.0
|