Aktraiser commited on
Commit
f7dd4c5
·
1 Parent(s): 7be0187

🚀 Serveur MCP Figma avec API REST - Version finale avec vraie intégration Figma

Browse files
Files changed (3) hide show
  1. README.md +148 -26
  2. app.py +348 -343
  3. requirements.txt +3 -2
README.md CHANGED
@@ -9,56 +9,178 @@ app_file: app.py
9
  pinned: false
10
  ---
11
 
12
- # 🎨 Sigma - Serveur MCP Figma
13
 
14
- Serveur MCP (Model Context Protocol) pour contrôler Figma via Claude/Cursor.
15
 
16
- ## 🔗 Utilisation
17
 
18
- **URL du serveur MCP:** `https://aktraiser-sigma.hf.space/gradio_api/mcp/sse`
 
 
 
 
19
 
20
- ### Configuration Claude Desktop
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
  ```json
23
  {
24
  "mcpServers": {
25
  "figma": {
26
- "command": "npx",
27
- "args": [
28
- "mcp-remote",
29
- "https://aktraiser-sigma.hf.space/gradio_api/mcp/sse"
30
- ]
31
  }
32
  }
33
  }
34
  ```
35
 
36
- ### Configuration Cursor
 
 
37
 
38
  ```json
39
  {
40
- "mcpServers": {
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
- - `join_figma_channel` - Rejoindre un canal Figma
52
- - `get_figma_document_info` - Infos du document
53
- - `create_figma_rectangle/frame/text` - Création d'éléments
54
- - `set_figma_fill_color` - Couleurs
55
- - `move_figma_node` / `resize_figma_node` - Modifications
56
- - `delete_figma_node` - Suppression
57
 
58
- ## 🏗️ Architecture
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
  ```
61
- Claude/Cursor ←→ MCP ←→ Gradio Server ←→ WebSocket ←→ Plugin Figma
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  ```
63
 
64
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # Variables globales pour stocker les connexions
21
- connected_clients: List[Dict[str, Any]] = []
22
- channels: Dict[str, List[str]] = {}
23
 
24
- # === COMMUNICATION AVEC LE PLUGIN FIGMA VIA GRADIO ===
25
 
26
- def handle_plugin_connection(channel_name: str, action: str = "join") -> str:
27
- """Gère la connexion du plugin Figma"""
28
- global connected_clients, channels
29
-
30
- if action == "join":
31
- # Ajouter le client
32
- client_info = {
33
- "id": str(uuid.uuid4()),
34
- "channel": channel_name,
35
- "connected_at": str(asyncio.get_event_loop().time()) if asyncio.get_event_loop().is_running() else "0"
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
- elif action == "disconnect":
55
- # Retirer le client
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
- return json.dumps({"type": "error", "message": "Action non reconnue"})
66
-
67
- def execute_figma_command_bridge(command: str, params_json: str) -> str:
68
- """Pont entre les outils MCP et les commandes Figma"""
69
  try:
70
- params = json.loads(params_json) if params_json else {}
 
71
 
72
- if command == "get_document_info":
73
- return json.dumps({"type": "result", "data": {"message": "Document info récupéré", "command": command}})
74
- elif command == "get_selection":
75
- return json.dumps({"type": "result", "data": {"message": "Sélection récupérée", "command": command}})
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 json.dumps({"type": "error", "message": f"Commande inconnue: {command}"})
86
 
87
  except Exception as e:
88
- return json.dumps({"type": "error", "message": str(e)})
89
-
90
- # === OUTILS MCP POUR FIGMA ===
91
 
92
- def join_figma_channel(channel: str) -> str:
93
- """
94
- Rejoint un canal Figma pour commencer à communiquer.
95
 
96
- Args:
97
- channel (str): Nom du canal Figma à rejoindre
98
-
99
- Returns:
100
- str: Message de confirmation ou d'erreur
101
- """
102
- result = handle_plugin_connection(channel, "join")
103
- return f"✅ Canal Figma configuré: {channel}\n📡 Plugin peut se connecter sur: https://aktraiser-sigma.hf.space"
104
-
105
- def get_figma_document_info() -> str:
106
- """
107
- Récupère les informations détaillées du document Figma actuel.
108
-
109
- Returns:
110
- str: Informations du document en format JSON
111
- """
112
- result = execute_figma_command_bridge("get_document_info", "{}")
113
- return f"📄 Commande get_document_info envoyée au plugin Figma\n{result}"
 
 
 
 
114
 
115
- def get_figma_selection() -> str:
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 get_figma_node_info(node_id: str) -> str:
126
- """
127
- Récupère les informations détaillées d'un nœud spécifique dans Figma.
 
128
 
129
- Args:
130
- node_id (str): ID du nœud Figma à analyser
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
- result = execute_figma_command_bridge("create_rectangle", json.dumps(params))
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
- result = execute_figma_command_bridge("create_frame", json.dumps(params))
193
- return f"🖼️ Frame {name} créé ({width}x{height}) à ({x},{y})\n{result}"
194
-
195
- def create_figma_text(x: str, y: str, text: str, font_size: str = "14", name: str = "Text", parent_id: str = "") -> str:
196
- """
197
- Crée un nouvel élément texte dans Figma.
198
-
199
- Args:
200
- x (str): Position X du texte
201
- y (str): Position Y du texte
202
- text (str): Contenu du texte
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
- Returns:
208
- str: Informations du texte créé
209
- """
210
- params = {
211
- "x": float(x),
212
- "y": float(y),
213
- "text": text,
214
- "fontSize": float(font_size),
215
- "name": name
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  }
217
- if parent_id:
218
- params["parentId"] = parent_id
219
 
220
- result = execute_figma_command_bridge("create_text", json.dumps(params))
221
- return f"📝 Texte '{text}' créé (taille {font_size}) à ({x},{y})\n{result}"
222
 
223
- def set_figma_fill_color(node_id: str, r: str, g: str, b: str, a: str = "1.0") -> str:
224
- """
225
- Définit la couleur de remplissage d'un nœud Figma.
226
-
227
- Args:
228
- node_id (str): ID du nœud à modifier
229
- r (str): Composant rouge (0-1)
230
- g (str): Composant vert (0-1)
231
- b (str): Composant bleu (0-1)
232
- a (str): Composant alpha/transparence (0-1, défaut: 1.0)
 
233
 
234
- Returns:
235
- str: Confirmation de la modification
236
- """
237
- params = {
238
- "nodeId": node_id,
239
- "color": {
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
- Returns:
259
- str: Confirmation du déplacement
260
- """
261
- params = {
262
- "nodeId": node_id,
263
- "x": float(x),
264
- "y": float(y)
265
- }
266
- result = execute_figma_command_bridge("move_node", json.dumps(params))
267
- return f"📐 Nœud {node_id} déplacé vers ({x},{y})\n{result}"
268
 
269
- def resize_figma_node(node_id: str, width: str, height: str) -> str:
270
- """
271
- Redimensionne un nœud Figma.
272
-
273
- Args:
274
- node_id (str): ID du nœud à redimensionner
275
- width (str): Nouvelle largeur
276
- height (str): Nouvelle hauteur
277
 
278
- Returns:
279
- str: Confirmation du redimensionnement
280
- """
281
- params = {
282
- "nodeId": node_id,
283
- "width": float(width),
284
- "height": float(height)
285
- }
286
- result = execute_figma_command_bridge("resize_node", json.dumps(params))
287
- return f"📏 Nœud {node_id} redimensionné à {width}x{height}\n{result}"
 
 
 
 
 
 
 
 
 
288
 
289
- def delete_figma_node(node_id: str) -> str:
290
- """
291
- Supprime un nœud dans Figma.
 
292
 
293
- Args:
294
- node_id (str): ID du nœud à supprimer
 
 
 
 
 
 
 
 
 
 
 
 
 
295
 
296
- Returns:
297
- str: Confirmation de la suppression
298
- """
299
- result = execute_figma_command_bridge("delete_node", json.dumps({"nodeId": node_id}))
300
- return f"🗑️ Nœud {node_id} supprimé\n{result}"
 
 
301
 
302
- def get_figma_styles() -> str:
303
- """
304
- Récupère tous les styles du document Figma.
305
-
306
- Returns:
307
- str: Liste des styles en format JSON
308
- """
309
- result = execute_figma_command_bridge("get_styles", "{}")
310
- return f"🎨 Styles du document récupérés\n{result}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
 
312
- def get_figma_components() -> str:
313
- """
314
- Récupère tous les composants locaux du document Figma.
315
-
316
- Returns:
317
- str: Liste des composants en format JSON
318
- """
319
- result = execute_figma_command_bridge("get_local_components", "{}")
320
- return f"🧩 Composants locaux récupérés\n{result}"
 
 
 
 
 
321
 
322
- # === INTERFACE GRADIO ===
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
 
324
- def health_check() -> str:
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
- # Interface Gradio avec communication plugin
336
- with gr.Blocks(title="🎨 Figma MCP Server") as demo:
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.Row():
341
- status_btn = gr.Button("Vérifier l'état", variant="primary")
342
- status_output = gr.Textbox(label="État du serveur", interactive=False)
343
-
344
- with gr.Tab("🔌 Communication Plugin"):
345
- gr.Markdown("### Interface de communication avec le plugin Figma")
346
 
347
- with gr.Row():
348
- channel_input = gr.Textbox(label="Canal", placeholder="nom-du-canal")
349
- join_btn = gr.Button("Rejoindre Canal", variant="primary")
350
-
351
- plugin_output = gr.Textbox(label="Réponse du plugin", interactive=False, lines=3)
 
 
 
 
 
 
 
 
 
352
 
353
- with gr.Row():
354
- command_input = gr.Dropdown(
355
- choices=["get_document_info", "get_selection", "create_rectangle", "create_frame", "create_text"],
356
- label="Commande Test"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- command_output = gr.Textbox(label="Résultat commande", interactive=False, lines=3)
362
-
363
- gr.Markdown("""
364
- ## 🔗 Utilisation
365
-
366
- ### Serveur MCP (Claude/Cursor)
367
- **URL:** `https://aktraiser-sigma.hf.space/gradio_api/mcp/sse`
368
-
369
- ### Plugin Figma
370
- Le plugin peut maintenant utiliser cette interface pour communiquer directement !
371
-
372
- ## 🛠️ Outils MCP disponibles
373
- - `join_figma_channel` - Rejoindre un canal Figma
374
- - `get_figma_document_info` - Infos du document
375
- - `create_figma_rectangle/frame/text` - Création d'éléments
376
- - `set_figma_fill_color` - Couleurs
377
- - `move_figma_node` / `resize_figma_node` - Modifications
378
- - `delete_figma_node` - Suppression
379
-
380
- ## 🏗️ Architecture Simplifiée
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
- # Lance le serveur MCP selon les recommandations Gradio
 
 
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
- websockets==12.0
3
  pydantic==2.5.0
4
- pillow==10.0.0
 
 
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