Aktraiser commited on
Commit
2fee365
·
1 Parent(s): 2ce4102

🎨 Déploiement serveur MCP Figma avec Gradio - Serveur MCP conforme aux standards Gradio - Outils Figma complets - Communication WebSocket - Interface monitoring - URL MCP: /gradio_api/mcp/sse

Browse files
Files changed (3) hide show
  1. README.md +54 -2
  2. app.py +437 -0
  3. requirements.txt +4 -0
README.md CHANGED
@@ -1,6 +1,6 @@
1
  ---
2
- title: Sigma
3
- emoji: 📚
4
  colorFrom: purple
5
  colorTo: purple
6
  sdk: gradio
@@ -9,4 +9,56 @@ app_file: app.py
9
  pinned: false
10
  ---
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: Sigma - Figma MCP Server
3
+ emoji: 🎨
4
  colorFrom: purple
5
  colorTo: purple
6
  sdk: gradio
 
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
app.py ADDED
@@ -0,0 +1,437 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 websockets
11
+ import logging
12
+ from typing import Dict, Any, Optional, List
13
+ from PIL import Image
14
+ import base64
15
+ import io
16
+
17
+ # Configuration du logging
18
+ logging.basicConfig(level=logging.INFO)
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # Variables globales pour la connexion WebSocket
22
+ ws: Optional[websockets.WebSocketClientProtocol] = None
23
+ pending_requests: Dict[str, asyncio.Future] = {}
24
+ current_channel: Optional[str] = None
25
+ is_connected = False
26
+
27
+ async def connect_to_figma(port: int = 3055) -> bool:
28
+ """Connecte au serveur WebSocket Figma"""
29
+ global ws, is_connected
30
+ try:
31
+ uri = f"ws://localhost:{port}"
32
+ ws = await websockets.connect(uri)
33
+ is_connected = True
34
+ logger.info(f"Connecté à Figma sur {uri}")
35
+
36
+ # Démarre l'écoute des messages
37
+ asyncio.create_task(listen_messages())
38
+ return True
39
+ except Exception as e:
40
+ logger.error(f"Erreur de connexion à Figma: {e}")
41
+ is_connected = False
42
+ return False
43
+
44
+ async def listen_messages():
45
+ """Écoute les messages du WebSocket"""
46
+ global ws, is_connected
47
+ try:
48
+ async for message in ws:
49
+ await handle_message(message)
50
+ except websockets.exceptions.ConnectionClosed:
51
+ logger.warning("Connexion WebSocket fermée")
52
+ is_connected = False
53
+ except Exception as e:
54
+ logger.error(f"Erreur lors de l'écoute des messages: {e}")
55
+
56
+ async def handle_message(message: str):
57
+ """Traite un message reçu de Figma"""
58
+ try:
59
+ data = json.loads(message)
60
+ response_data = data.get("message", {})
61
+
62
+ response_id = response_data.get("id")
63
+ if response_id and response_id in pending_requests:
64
+ future = pending_requests.pop(response_id)
65
+ if "error" in response_data:
66
+ future.set_exception(Exception(response_data["error"]))
67
+ else:
68
+ future.set_result(response_data.get("result"))
69
+ except Exception as e:
70
+ logger.error(f"Erreur lors du traitement du message: {e}")
71
+
72
+ async def send_command_to_figma(command: str, params: Dict[str, Any] = None) -> Any:
73
+ """Envoie une commande à Figma et attend la réponse"""
74
+ global ws, current_channel, is_connected
75
+
76
+ if not is_connected or not ws:
77
+ # Tente de se connecter
78
+ await connect_to_figma()
79
+ if not is_connected:
80
+ raise Exception("Impossible de se connecter à Figma")
81
+
82
+ if command != 'join' and not current_channel:
83
+ raise Exception("Doit rejoindre un canal avant d'envoyer des commandes")
84
+
85
+ request_id = str(uuid.uuid4())
86
+ future = asyncio.Future()
87
+ pending_requests[request_id] = future
88
+
89
+ request = {
90
+ "id": request_id,
91
+ "type": "join" if command == 'join' else "message",
92
+ "message": {
93
+ "id": request_id,
94
+ "command": command,
95
+ "params": params or {}
96
+ }
97
+ }
98
+
99
+ if command == 'join':
100
+ request["channel"] = params.get("channel")
101
+ else:
102
+ request["channel"] = current_channel
103
+
104
+ await ws.send(json.dumps(request))
105
+
106
+ try:
107
+ result = await asyncio.wait_for(future, timeout=30.0)
108
+ return result
109
+ except asyncio.TimeoutError:
110
+ pending_requests.pop(request_id, None)
111
+ raise Exception(f"Timeout lors de l'exécution de la commande {command}")
112
+
113
+ # === OUTILS MCP POUR FIGMA ===
114
+
115
+ def join_figma_channel(channel: str) -> str:
116
+ """
117
+ Rejoint un canal Figma pour commencer à communiquer.
118
+
119
+ Args:
120
+ channel (str): Nom du canal Figma à rejoindre
121
+
122
+ Returns:
123
+ str: Message de confirmation ou d'erreur
124
+ """
125
+ global current_channel
126
+ try:
127
+ result = asyncio.run(send_command_to_figma('join', {"channel": channel}))
128
+ current_channel = channel
129
+ return f"✅ Canal Figma rejoint avec succès: {channel}"
130
+ except Exception as e:
131
+ return f"❌ Erreur lors de la jointure du canal: {str(e)}"
132
+
133
+ def get_figma_document_info() -> str:
134
+ """
135
+ Récupère les informations détaillées du document Figma actuel.
136
+
137
+ Returns:
138
+ str: Informations du document en format JSON
139
+ """
140
+ try:
141
+ result = asyncio.run(send_command_to_figma('get_document_info'))
142
+ return json.dumps(result, indent=2, ensure_ascii=False)
143
+ except Exception as e:
144
+ return f"❌ Erreur: {str(e)}"
145
+
146
+ def get_figma_selection() -> str:
147
+ """
148
+ Récupère les informations sur la sélection actuelle dans Figma.
149
+
150
+ Returns:
151
+ str: Informations de la sélection en format JSON
152
+ """
153
+ try:
154
+ result = asyncio.run(send_command_to_figma('get_selection'))
155
+ return json.dumps(result, indent=2, ensure_ascii=False)
156
+ except Exception as e:
157
+ return f"❌ Erreur: {str(e)}"
158
+
159
+ def get_figma_node_info(node_id: str) -> str:
160
+ """
161
+ Récupère les informations détaillées d'un nœud spécifique dans Figma.
162
+
163
+ Args:
164
+ node_id (str): ID du nœud Figma à analyser
165
+
166
+ Returns:
167
+ str: Informations du nœud en format JSON
168
+ """
169
+ try:
170
+ result = asyncio.run(send_command_to_figma('get_node_info', {"nodeId": node_id}))
171
+ return json.dumps(result, indent=2, ensure_ascii=False)
172
+ except Exception as e:
173
+ return f"❌ Erreur: {str(e)}"
174
+
175
+ def create_figma_rectangle(x: str, y: str, width: str, height: str, name: str = "Rectangle", parent_id: str = "") -> str:
176
+ """
177
+ Crée un nouveau rectangle dans Figma.
178
+
179
+ Args:
180
+ x (str): Position X du rectangle
181
+ y (str): Position Y du rectangle
182
+ width (str): Largeur du rectangle
183
+ height (str): Hauteur du rectangle
184
+ name (str): Nom optionnel du rectangle
185
+ parent_id (str): ID du nœud parent (optionnel)
186
+
187
+ Returns:
188
+ str: Informations du rectangle créé
189
+ """
190
+ try:
191
+ params = {
192
+ "x": float(x),
193
+ "y": float(y),
194
+ "width": float(width),
195
+ "height": float(height),
196
+ "name": name
197
+ }
198
+ if parent_id:
199
+ params["parentId"] = parent_id
200
+
201
+ result = asyncio.run(send_command_to_figma('create_rectangle', params))
202
+ return f"✅ Rectangle créé: {json.dumps(result, indent=2)}"
203
+ except Exception as e:
204
+ return f"❌ Erreur: {str(e)}"
205
+
206
+ def create_figma_frame(x: str, y: str, width: str, height: str, name: str = "Frame", parent_id: str = "") -> str:
207
+ """
208
+ Crée un nouveau frame (conteneur) dans Figma.
209
+
210
+ Args:
211
+ x (str): Position X du frame
212
+ y (str): Position Y du frame
213
+ width (str): Largeur du frame
214
+ height (str): Hauteur du frame
215
+ name (str): Nom optionnel du frame
216
+ parent_id (str): ID du nœud parent (optionnel)
217
+
218
+ Returns:
219
+ str: Informations du frame créé avec son ID
220
+ """
221
+ try:
222
+ params = {
223
+ "x": float(x),
224
+ "y": float(y),
225
+ "width": float(width),
226
+ "height": float(height),
227
+ "name": name
228
+ }
229
+ if parent_id:
230
+ params["parentId"] = parent_id
231
+
232
+ result = asyncio.run(send_command_to_figma('create_frame', params))
233
+ return f"✅ Frame créé: {json.dumps(result, indent=2)}"
234
+ except Exception as e:
235
+ return f"❌ Erreur: {str(e)}"
236
+
237
+ def create_figma_text(x: str, y: str, text: str, font_size: str = "14", name: str = "Text", parent_id: str = "") -> str:
238
+ """
239
+ Crée un nouvel élément texte dans Figma.
240
+
241
+ Args:
242
+ x (str): Position X du texte
243
+ y (str): Position Y du texte
244
+ text (str): Contenu du texte
245
+ font_size (str): Taille de la police (défaut: 14)
246
+ name (str): Nom optionnel de l'élément texte
247
+ parent_id (str): ID du nœud parent (optionnel)
248
+
249
+ Returns:
250
+ str: Informations du texte créé
251
+ """
252
+ try:
253
+ params = {
254
+ "x": float(x),
255
+ "y": float(y),
256
+ "text": text,
257
+ "fontSize": float(font_size),
258
+ "name": name
259
+ }
260
+ if parent_id:
261
+ params["parentId"] = parent_id
262
+
263
+ result = asyncio.run(send_command_to_figma('create_text', params))
264
+ return f"✅ Texte créé: {json.dumps(result, indent=2)}"
265
+ except Exception as e:
266
+ return f"❌ Erreur: {str(e)}"
267
+
268
+ def set_figma_fill_color(node_id: str, r: str, g: str, b: str, a: str = "1.0") -> str:
269
+ """
270
+ Définit la couleur de remplissage d'un nœud Figma.
271
+
272
+ Args:
273
+ node_id (str): ID du nœud à modifier
274
+ r (str): Composant rouge (0-1)
275
+ g (str): Composant vert (0-1)
276
+ b (str): Composant bleu (0-1)
277
+ a (str): Composant alpha/transparence (0-1, défaut: 1.0)
278
+
279
+ Returns:
280
+ str: Confirmation de la modification
281
+ """
282
+ try:
283
+ params = {
284
+ "nodeId": node_id,
285
+ "color": {
286
+ "r": float(r),
287
+ "g": float(g),
288
+ "b": float(b),
289
+ "a": float(a)
290
+ }
291
+ }
292
+ result = asyncio.run(send_command_to_figma('set_fill_color', params))
293
+ return f"✅ Couleur de remplissage définie: {json.dumps(result, indent=2)}"
294
+ except Exception as e:
295
+ return f"❌ Erreur: {str(e)}"
296
+
297
+ def move_figma_node(node_id: str, x: str, y: str) -> str:
298
+ """
299
+ Déplace un nœud Figma vers une nouvelle position.
300
+
301
+ Args:
302
+ node_id (str): ID du nœud à déplacer
303
+ x (str): Nouvelle position X
304
+ y (str): Nouvelle position Y
305
+
306
+ Returns:
307
+ str: Confirmation du déplacement
308
+ """
309
+ try:
310
+ params = {
311
+ "nodeId": node_id,
312
+ "x": float(x),
313
+ "y": float(y)
314
+ }
315
+ result = asyncio.run(send_command_to_figma('move_node', params))
316
+ return f"✅ Nœud déplacé: {json.dumps(result, indent=2)}"
317
+ except Exception as e:
318
+ return f"❌ Erreur: {str(e)}"
319
+
320
+ def resize_figma_node(node_id: str, width: str, height: str) -> str:
321
+ """
322
+ Redimensionne un nœud Figma.
323
+
324
+ Args:
325
+ node_id (str): ID du nœud à redimensionner
326
+ width (str): Nouvelle largeur
327
+ height (str): Nouvelle hauteur
328
+
329
+ Returns:
330
+ str: Confirmation du redimensionnement
331
+ """
332
+ try:
333
+ params = {
334
+ "nodeId": node_id,
335
+ "width": float(width),
336
+ "height": float(height)
337
+ }
338
+ result = asyncio.run(send_command_to_figma('resize_node', params))
339
+ return f"✅ Nœud redimensionné: {json.dumps(result, indent=2)}"
340
+ except Exception as e:
341
+ return f"❌ Erreur: {str(e)}"
342
+
343
+ def delete_figma_node(node_id: str) -> str:
344
+ """
345
+ Supprime un nœud dans Figma.
346
+
347
+ Args:
348
+ node_id (str): ID du nœud à supprimer
349
+
350
+ Returns:
351
+ str: Confirmation de la suppression
352
+ """
353
+ try:
354
+ result = asyncio.run(send_command_to_figma('delete_node', {"nodeId": node_id}))
355
+ return f"✅ Nœud supprimé: {node_id}"
356
+ except Exception as e:
357
+ return f"❌ Erreur: {str(e)}"
358
+
359
+ def get_figma_styles() -> str:
360
+ """
361
+ Récupère tous les styles du document Figma.
362
+
363
+ Returns:
364
+ str: Liste des styles en format JSON
365
+ """
366
+ try:
367
+ result = asyncio.run(send_command_to_figma('get_styles'))
368
+ return json.dumps(result, indent=2, ensure_ascii=False)
369
+ except Exception as e:
370
+ return f"❌ Erreur: {str(e)}"
371
+
372
+ def get_figma_components() -> str:
373
+ """
374
+ Récupère tous les composants locaux du document Figma.
375
+
376
+ Returns:
377
+ str: Liste des composants en format JSON
378
+ """
379
+ try:
380
+ result = asyncio.run(send_command_to_figma('get_local_components'))
381
+ return json.dumps(result, indent=2, ensure_ascii=False)
382
+ except Exception as e:
383
+ return f"❌ Erreur: {str(e)}"
384
+
385
+ # === INTERFACE GRADIO (SIMPLE) ===
386
+
387
+ def health_check() -> str:
388
+ """
389
+ Vérifie l'état de santé du serveur MCP Figma.
390
+
391
+ Returns:
392
+ str: État de la connexion
393
+ """
394
+ global is_connected, current_channel
395
+ if is_connected:
396
+ return f"✅ Serveur MCP Figma actif - Canal: {current_channel or 'Aucun'}"
397
+ else:
398
+ return "❌ Serveur MCP Figma non connecté"
399
+
400
+ # Interface Gradio simple pour le monitoring
401
+ with gr.Blocks(title="🎨 Figma MCP Server") as demo:
402
+ gr.Markdown("# 🎨 Serveur MCP Figma")
403
+ gr.Markdown("Serveur MCP hébergé sur Hugging Face Spaces pour contrôler Figma via Claude/Cursor")
404
+
405
+ with gr.Row():
406
+ status_btn = gr.Button("Vérifier l'état", variant="primary")
407
+ status_output = gr.Textbox(label="État du serveur", interactive=False)
408
+
409
+ gr.Markdown("""
410
+ ## 🔗 Utilisation
411
+
412
+ Ce serveur MCP expose les outils Figma suivants :
413
+ - `join_figma_channel` - Rejoindre un canal Figma
414
+ - `get_figma_document_info` - Infos du document
415
+ - `get_figma_selection` - Sélection actuelle
416
+ - `create_figma_rectangle` - Créer un rectangle
417
+ - `create_figma_frame` - Créer un frame
418
+ - `create_figma_text` - Créer du texte
419
+ - `set_figma_fill_color` - Définir une couleur
420
+ - `move_figma_node` - Déplacer un élément
421
+ - `resize_figma_node` - Redimensionner un élément
422
+ - `delete_figma_node` - Supprimer un élément
423
+
424
+ **URL du serveur MCP:** `https://votre-space.hf.space/gradio_api/mcp/sse`
425
+ """)
426
+
427
+ status_btn.click(health_check, outputs=[status_output])
428
+
429
+ if __name__ == "__main__":
430
+ # Lance le serveur MCP selon les recommandations Gradio
431
+ demo.launch(
432
+ mcp_server=True, # Active le serveur MCP
433
+ server_name="0.0.0.0",
434
+ server_port=7860,
435
+ share=False,
436
+ show_error=True
437
+ )
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio[mcp]>=5.3.0
2
+ websockets==12.0
3
+ pydantic==2.5.0
4
+ pillow==10.0.0