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
|