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

🔗 Ajout serveur WebSocket pour communication directe avec plugin Figma - Serveur WebSocket sur port 8765 - Plugin modifié pour se connecter à HF Spaces - Architecture Claude/Cursor ←→ MCP ←→ WebSocket ←→ Plugin Figma

Browse files
Files changed (1) hide show
  1. app.py +196 -206
app.py CHANGED
@@ -13,104 +13,144 @@ 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
  """
@@ -122,13 +162,8 @@ def join_figma_channel(channel: str) -> str:
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
  """
@@ -137,11 +172,7 @@ def get_figma_document_info() -> str:
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
  """
@@ -150,11 +181,7 @@ def get_figma_selection() -> str:
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
  """
@@ -166,11 +193,7 @@ def get_figma_node_info(node_id: str) -> str:
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
  """
@@ -187,21 +210,16 @@ def create_figma_rectangle(x: str, y: str, width: str, height: str, name: str =
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
  """
@@ -218,21 +236,16 @@ def create_figma_frame(x: str, y: str, width: str, height: str, name: str = "Fra
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
  """
@@ -249,21 +262,16 @@ def create_figma_text(x: str, y: str, text: str, font_size: str = "14", name: st
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
  """
@@ -279,20 +287,16 @@ def set_figma_fill_color(node_id: str, r: str, g: str, b: str, a: str = "1.0") -
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
  """
@@ -306,16 +310,12 @@ def move_figma_node(node_id: str, x: str, y: str) -> str:
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
  """
@@ -329,16 +329,12 @@ def resize_figma_node(node_id: str, width: str, height: str) -> str:
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
  """
@@ -350,11 +346,7 @@ def delete_figma_node(node_id: str) -> str:
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
  """
@@ -363,11 +355,7 @@ def get_figma_styles() -> str:
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
  """
@@ -376,11 +364,7 @@ def get_figma_components() -> str:
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
 
@@ -391,11 +375,9 @@ def health_check() -> str:
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:
@@ -409,24 +391,32 @@ with gr.Blocks(title="🎨 Figma MCP Server") as demo:
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
 
13
  from PIL import Image
14
  import base64
15
  import io
16
+ import threading
17
+ from websockets.exceptions import ConnectionClosed
18
 
19
  # Configuration du logging
20
  logging.basicConfig(level=logging.INFO)
21
  logger = logging.getLogger(__name__)
22
 
23
  # Variables globales pour la connexion WebSocket
24
+ active_connections: Dict[str, websockets.WebSocketServerProtocol] = {}
25
+ channels: Dict[str, List[websockets.WebSocketServerProtocol]] = {}
26
  pending_requests: Dict[str, asyncio.Future] = {}
 
 
27
 
28
+ # === SERVEUR WEBSOCKET POUR LE PLUGIN FIGMA ===
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
+ async def handle_client(websocket, path):
31
+ """Gère les connexions WebSocket du plugin Figma"""
32
+ client_id = str(uuid.uuid4())
33
+ active_connections[client_id] = websocket
34
+ logger.info(f"Client {client_id} connecté")
35
+
36
  try:
37
+ async for message in websocket:
38
+ await handle_websocket_message(websocket, client_id, message)
39
+ except ConnectionClosed:
40
+ logger.info(f"Client {client_id} déconnecté")
 
41
  except Exception as e:
42
+ logger.error(f"Erreur avec client {client_id}: {e}")
43
+ finally:
44
+ # Nettoyage
45
+ if client_id in active_connections:
46
+ del active_connections[client_id]
47
+ # Retirer des canaux
48
+ for channel_clients in channels.values():
49
+ if websocket in channel_clients:
50
+ channel_clients.remove(websocket)
51
 
52
+ async def handle_websocket_message(websocket, client_id, message):
53
+ """Traite les messages WebSocket du plugin"""
54
  try:
55
  data = json.loads(message)
56
+ message_type = data.get("type")
57
 
58
+ if message_type == "join":
59
+ # Rejoindre un canal
60
+ channel = data.get("channel")
61
+ if channel:
62
+ if channel not in channels:
63
+ channels[channel] = []
64
+ channels[channel].append(websocket)
65
+
66
+ # Confirmer la jointure
67
+ await websocket.send(json.dumps({
68
+ "type": "system",
69
+ "channel": channel,
70
+ "message": {
71
+ "result": f"Joined channel {channel}",
72
+ "channel": channel
73
+ }
74
+ }))
75
+ logger.info(f"Client {client_id} a rejoint le canal {channel}")
76
+
77
+ elif message_type == "message":
78
+ # Message de commande
79
+ channel = data.get("channel")
80
+ message_data = data.get("message", {})
81
+ command = message_data.get("command")
82
+ params = message_data.get("params", {})
83
+ request_id = message_data.get("id")
84
+
85
+ # Exécuter la commande MCP correspondante
86
+ try:
87
+ result = await execute_figma_command(command, params)
88
+
89
+ # Renvoyer le résultat
90
+ response = {
91
+ "type": "message",
92
+ "channel": channel,
93
+ "message": {
94
+ "id": request_id,
95
+ "result": result
96
+ }
97
+ }
98
+ await websocket.send(json.dumps(response))
99
+
100
+ except Exception as e:
101
+ # Renvoyer l'erreur
102
+ error_response = {
103
+ "type": "message",
104
+ "channel": channel,
105
+ "message": {
106
+ "id": request_id,
107
+ "error": str(e)
108
+ }
109
+ }
110
+ await websocket.send(json.dumps(error_response))
111
+
112
  except Exception as e:
113
  logger.error(f"Erreur lors du traitement du message: {e}")
114
 
115
+ async def execute_figma_command(command: str, params: Dict[str, Any]) -> Any:
116
+ """Exécute une commande Figma et retourne le résultat"""
117
+ # Utilise les mêmes fonctions que les outils MCP
118
+ if command == "get_document_info":
119
+ return {"message": "get_document_info non implémenté via WebSocket"}
120
+ elif command == "get_selection":
121
+ return {"message": "get_selection non implémenté via WebSocket"}
122
+ elif command == "create_rectangle":
123
+ return {"message": "create_rectangle exécuté", "params": params}
124
+ elif command == "create_frame":
125
+ return {"message": "create_frame exécuté", "params": params}
126
+ elif command == "create_text":
127
+ return {"message": "create_text exécuté", "params": params}
128
+ elif command == "set_fill_color":
129
+ return {"message": "set_fill_color exécuté", "params": params}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  else:
131
+ raise Exception(f"Commande inconnue: {command}")
132
+
133
+ def start_websocket_server():
134
+ """Lance le serveur WebSocket en arrière-plan"""
135
+ async def run_server():
136
+ try:
137
+ # Démarre le serveur WebSocket sur le port 8765
138
+ server = await websockets.serve(handle_client, "0.0.0.0", 8765)
139
+ logger.info("🚀 Serveur WebSocket démarré sur port 8765")
140
+ await server.wait_closed()
141
+ except Exception as e:
142
+ logger.error(f"Erreur serveur WebSocket: {e}")
143
 
144
+ # Lance dans un thread séparé pour ne pas bloquer Gradio
145
+ def run_in_thread():
146
+ loop = asyncio.new_event_loop()
147
+ asyncio.set_event_loop(loop)
148
+ loop.run_until_complete(run_server())
149
 
150
+ thread = threading.Thread(target=run_in_thread, daemon=True)
151
+ thread.start()
 
 
 
 
152
 
153
+ # === OUTILS MCP POUR FIGMA (simplifiés) ===
154
 
155
  def join_figma_channel(channel: str) -> str:
156
  """
 
162
  Returns:
163
  str: Message de confirmation ou d'erreur
164
  """
165
+ # Note: Avec la nouvelle architecture, le plugin se connecte directement
166
+ return f"✅ Le plugin Figma peut maintenant rejoindre le canal: {channel} via WebSocket"
 
 
 
 
 
167
 
168
  def get_figma_document_info() -> str:
169
  """
 
172
  Returns:
173
  str: Informations du document en format JSON
174
  """
175
+ return "ℹ️ Commande transmise au plugin Figma via WebSocket"
 
 
 
 
176
 
177
  def get_figma_selection() -> str:
178
  """
 
181
  Returns:
182
  str: Informations de la sélection en format JSON
183
  """
184
+ return "ℹ️ Commande transmise au plugin Figma via WebSocket"
 
 
 
 
185
 
186
  def get_figma_node_info(node_id: str) -> str:
187
  """
 
193
  Returns:
194
  str: Informations du nœud en format JSON
195
  """
196
+ return f"ℹ️ Commande get_node_info pour {node_id} transmise au plugin Figma via WebSocket"
 
 
 
 
197
 
198
  def create_figma_rectangle(x: str, y: str, width: str, height: str, name: str = "Rectangle", parent_id: str = "") -> str:
199
  """
 
210
  Returns:
211
  str: Informations du rectangle créé
212
  """
213
+ params = {
214
+ "x": float(x),
215
+ "y": float(y),
216
+ "width": float(width),
217
+ "height": float(height),
218
+ "name": name
219
+ }
220
+ if parent_id:
221
+ params["parentId"] = parent_id
222
+ return f" Commande create_rectangle transmise: {params}"
 
 
 
 
 
223
 
224
  def create_figma_frame(x: str, y: str, width: str, height: str, name: str = "Frame", parent_id: str = "") -> str:
225
  """
 
236
  Returns:
237
  str: Informations du frame créé avec son ID
238
  """
239
+ params = {
240
+ "x": float(x),
241
+ "y": float(y),
242
+ "width": float(width),
243
+ "height": float(height),
244
+ "name": name
245
+ }
246
+ if parent_id:
247
+ params["parentId"] = parent_id
248
+ return f" Commande create_frame transmise: {params}"
 
 
 
 
 
249
 
250
  def create_figma_text(x: str, y: str, text: str, font_size: str = "14", name: str = "Text", parent_id: str = "") -> str:
251
  """
 
262
  Returns:
263
  str: Informations du texte créé
264
  """
265
+ params = {
266
+ "x": float(x),
267
+ "y": float(y),
268
+ "text": text,
269
+ "fontSize": float(font_size),
270
+ "name": name
271
+ }
272
+ if parent_id:
273
+ params["parentId"] = parent_id
274
+ return f" Commande create_text transmise: {params}"
 
 
 
 
 
275
 
276
  def set_figma_fill_color(node_id: str, r: str, g: str, b: str, a: str = "1.0") -> str:
277
  """
 
287
  Returns:
288
  str: Confirmation de la modification
289
  """
290
+ params = {
291
+ "nodeId": node_id,
292
+ "color": {
293
+ "r": float(r),
294
+ "g": float(g),
295
+ "b": float(b),
296
+ "a": float(a)
 
 
297
  }
298
+ }
299
+ return f"✅ Commande set_fill_color transmise: {params}"
 
 
300
 
301
  def move_figma_node(node_id: str, x: str, y: str) -> str:
302
  """
 
310
  Returns:
311
  str: Confirmation du déplacement
312
  """
313
+ params = {
314
+ "nodeId": node_id,
315
+ "x": float(x),
316
+ "y": float(y)
317
+ }
318
+ return f"✅ Commande move_node transmise: {params}"
 
 
 
 
319
 
320
  def resize_figma_node(node_id: str, width: str, height: str) -> str:
321
  """
 
329
  Returns:
330
  str: Confirmation du redimensionnement
331
  """
332
+ params = {
333
+ "nodeId": node_id,
334
+ "width": float(width),
335
+ "height": float(height)
336
+ }
337
+ return f"✅ Commande resize_node transmise: {params}"
 
 
 
 
338
 
339
  def delete_figma_node(node_id: str) -> str:
340
  """
 
346
  Returns:
347
  str: Confirmation de la suppression
348
  """
349
+ return f"✅ Commande delete_node transmise pour nœud: {node_id}"
 
 
 
 
350
 
351
  def get_figma_styles() -> str:
352
  """
 
355
  Returns:
356
  str: Liste des styles en format JSON
357
  """
358
+ return "ℹ️ Commande get_styles transmise au plugin Figma via WebSocket"
 
 
 
 
359
 
360
  def get_figma_components() -> str:
361
  """
 
364
  Returns:
365
  str: Liste des composants en format JSON
366
  """
367
+ return "ℹ️ Commande get_local_components transmise au plugin Figma via WebSocket"
 
 
 
 
368
 
369
  # === INTERFACE GRADIO (SIMPLE) ===
370
 
 
375
  Returns:
376
  str: État de la connexion
377
  """
378
+ connection_count = len(active_connections)
379
+ channel_count = len(channels)
380
+ return f"✅ Serveur MCP Figma actif - {connection_count} clients connectés, {channel_count} canaux"
 
 
381
 
382
  # Interface Gradio simple pour le monitoring
383
  with gr.Blocks(title="🎨 Figma MCP Server") as demo:
 
391
  gr.Markdown("""
392
  ## 🔗 Utilisation
393
 
394
+ ### Serveur MCP (Claude/Cursor)
395
+ **URL:** `https://aktraiser-sigma.hf.space/gradio_api/mcp/sse`
396
+
397
+ ### Serveur WebSocket (Plugin Figma)
398
+ **URL:** `wss://aktraiser-sigma.hf.space:8765`
399
+
400
+ ## 🛠️ Outils disponibles
401
  - `join_figma_channel` - Rejoindre un canal Figma
402
  - `get_figma_document_info` - Infos du document
403
+ - `create_figma_rectangle/frame/text` - Création d'éléments
404
+ - `set_figma_fill_color` - Couleurs
405
+ - `move_figma_node` / `resize_figma_node` - Modifications
406
+ - `delete_figma_node` - Suppression
 
 
 
 
407
 
408
+ ## 🏗️ Architecture
409
+ ```
410
+ Claude/Cursor ←→ MCP ←→ Gradio Server ←→ WebSocket:8765 ←→ Plugin Figma
411
+ ```
412
  """)
413
 
414
  status_btn.click(health_check, outputs=[status_output])
415
 
416
  if __name__ == "__main__":
417
+ # Lance le serveur WebSocket en arrière-plan
418
+ start_websocket_server()
419
+
420
  # Lance le serveur MCP selon les recommandations Gradio
421
  demo.launch(
422
  mcp_server=True, # Active le serveur MCP