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

🔧 Fix: Architecture simplifiée sans WebSocket - utilise l'API Gradio directement

Browse files
Files changed (1) hide show
  1. app.py +131 -151
app.py CHANGED
@@ -7,150 +7,87 @@ 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
- 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,8 +99,8 @@ def join_figma_channel(channel: str) -> str:
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,7 +109,8 @@ def get_figma_document_info() -> str:
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,7 +119,8 @@ def get_figma_selection() -> str:
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,7 +132,9 @@ def get_figma_node_info(node_id: str) -> str:
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
  """
@@ -219,7 +160,9 @@ def create_figma_rectangle(x: str, y: str, width: str, height: str, name: str =
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
  """
@@ -245,7 +188,9 @@ def create_figma_frame(x: str, y: str, width: str, height: str, name: str = "Fra
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
  """
@@ -271,7 +216,9 @@ def create_figma_text(x: str, y: str, text: str, font_size: str = "14", name: st
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
  """
@@ -296,7 +243,8 @@ def set_figma_fill_color(node_id: str, r: str, g: str, b: str, a: str = "1.0") -
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
  """
@@ -315,7 +263,8 @@ def move_figma_node(node_id: str, x: str, y: str) -> str:
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
  """
@@ -334,7 +283,8 @@ def resize_figma_node(node_id: str, width: str, height: str) -> str:
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,7 +296,8 @@ def delete_figma_node(node_id: str) -> str:
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,7 +306,8 @@ def get_figma_styles() -> str:
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,9 +316,10 @@ def get_figma_components() -> str:
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
 
371
  def health_check() -> str:
372
  """
@@ -375,11 +328,11 @@ def health_check() -> str:
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:
384
  gr.Markdown("# 🎨 Serveur MCP Figma")
385
  gr.Markdown("Serveur MCP hébergé sur Hugging Face Spaces pour contrôler Figma via Claude/Cursor")
@@ -388,16 +341,35 @@ with gr.Blocks(title="🎨 Figma MCP Server") as demo:
388
  status_btn = gr.Button("Vérifier l'état", variant="primary")
389
  status_output = gr.Textbox(label="État du serveur", interactive=False)
390
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
@@ -405,18 +377,26 @@ with gr.Blocks(title="🎨 Figma MCP Server") as demo:
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
 
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
14
  import io
 
 
15
 
16
  # Configuration du logging
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
  """
 
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
  """
 
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
  """
 
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
  """
 
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
  """
 
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
  """
 
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
  """
 
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
  """
 
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
  """
 
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
  """
 
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
  """
 
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
  """
 
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
  """
 
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
  """
 
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")
 
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
 
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