amd-gaia 0.15.0__py3-none-any.whl → 0.15.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.1.dist-info}/METADATA +223 -223
  2. amd_gaia-0.15.1.dist-info/RECORD +178 -0
  3. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.1.dist-info}/entry_points.txt +1 -0
  4. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.1.dist-info}/licenses/LICENSE.md +20 -20
  5. gaia/__init__.py +29 -29
  6. gaia/agents/__init__.py +19 -19
  7. gaia/agents/base/__init__.py +9 -9
  8. gaia/agents/base/agent.py +2177 -2177
  9. gaia/agents/base/api_agent.py +120 -120
  10. gaia/agents/base/console.py +1841 -1841
  11. gaia/agents/base/errors.py +237 -237
  12. gaia/agents/base/mcp_agent.py +86 -86
  13. gaia/agents/base/tools.py +83 -83
  14. gaia/agents/blender/agent.py +556 -556
  15. gaia/agents/blender/agent_simple.py +133 -135
  16. gaia/agents/blender/app.py +211 -211
  17. gaia/agents/blender/app_simple.py +41 -41
  18. gaia/agents/blender/core/__init__.py +16 -16
  19. gaia/agents/blender/core/materials.py +506 -506
  20. gaia/agents/blender/core/objects.py +316 -316
  21. gaia/agents/blender/core/rendering.py +225 -225
  22. gaia/agents/blender/core/scene.py +220 -220
  23. gaia/agents/blender/core/view.py +146 -146
  24. gaia/agents/chat/__init__.py +9 -9
  25. gaia/agents/chat/agent.py +835 -835
  26. gaia/agents/chat/app.py +1058 -1058
  27. gaia/agents/chat/session.py +508 -508
  28. gaia/agents/chat/tools/__init__.py +15 -15
  29. gaia/agents/chat/tools/file_tools.py +96 -96
  30. gaia/agents/chat/tools/rag_tools.py +1729 -1729
  31. gaia/agents/chat/tools/shell_tools.py +436 -436
  32. gaia/agents/code/__init__.py +7 -7
  33. gaia/agents/code/agent.py +549 -549
  34. gaia/agents/code/cli.py +377 -0
  35. gaia/agents/code/models.py +135 -135
  36. gaia/agents/code/orchestration/__init__.py +24 -24
  37. gaia/agents/code/orchestration/checklist_executor.py +1763 -1763
  38. gaia/agents/code/orchestration/checklist_generator.py +713 -713
  39. gaia/agents/code/orchestration/factories/__init__.py +9 -9
  40. gaia/agents/code/orchestration/factories/base.py +63 -63
  41. gaia/agents/code/orchestration/factories/nextjs_factory.py +118 -118
  42. gaia/agents/code/orchestration/factories/python_factory.py +106 -106
  43. gaia/agents/code/orchestration/orchestrator.py +841 -841
  44. gaia/agents/code/orchestration/project_analyzer.py +391 -391
  45. gaia/agents/code/orchestration/steps/__init__.py +67 -67
  46. gaia/agents/code/orchestration/steps/base.py +188 -188
  47. gaia/agents/code/orchestration/steps/error_handler.py +314 -314
  48. gaia/agents/code/orchestration/steps/nextjs.py +828 -828
  49. gaia/agents/code/orchestration/steps/python.py +307 -307
  50. gaia/agents/code/orchestration/template_catalog.py +469 -469
  51. gaia/agents/code/orchestration/workflows/__init__.py +14 -14
  52. gaia/agents/code/orchestration/workflows/base.py +80 -80
  53. gaia/agents/code/orchestration/workflows/nextjs.py +186 -186
  54. gaia/agents/code/orchestration/workflows/python.py +94 -94
  55. gaia/agents/code/prompts/__init__.py +11 -11
  56. gaia/agents/code/prompts/base_prompt.py +77 -77
  57. gaia/agents/code/prompts/code_patterns.py +2036 -2036
  58. gaia/agents/code/prompts/nextjs_prompt.py +40 -40
  59. gaia/agents/code/prompts/python_prompt.py +109 -109
  60. gaia/agents/code/schema_inference.py +365 -365
  61. gaia/agents/code/system_prompt.py +41 -41
  62. gaia/agents/code/tools/__init__.py +42 -42
  63. gaia/agents/code/tools/cli_tools.py +1138 -1138
  64. gaia/agents/code/tools/code_formatting.py +319 -319
  65. gaia/agents/code/tools/code_tools.py +769 -769
  66. gaia/agents/code/tools/error_fixing.py +1347 -1347
  67. gaia/agents/code/tools/external_tools.py +180 -180
  68. gaia/agents/code/tools/file_io.py +845 -845
  69. gaia/agents/code/tools/prisma_tools.py +190 -190
  70. gaia/agents/code/tools/project_management.py +1016 -1016
  71. gaia/agents/code/tools/testing.py +321 -321
  72. gaia/agents/code/tools/typescript_tools.py +122 -122
  73. gaia/agents/code/tools/validation_parsing.py +461 -461
  74. gaia/agents/code/tools/validation_tools.py +806 -806
  75. gaia/agents/code/tools/web_dev_tools.py +1758 -1758
  76. gaia/agents/code/validators/__init__.py +16 -16
  77. gaia/agents/code/validators/antipattern_checker.py +241 -241
  78. gaia/agents/code/validators/ast_analyzer.py +197 -197
  79. gaia/agents/code/validators/requirements_validator.py +145 -145
  80. gaia/agents/code/validators/syntax_validator.py +171 -171
  81. gaia/agents/docker/__init__.py +7 -7
  82. gaia/agents/docker/agent.py +642 -642
  83. gaia/agents/emr/__init__.py +8 -8
  84. gaia/agents/emr/agent.py +1506 -1506
  85. gaia/agents/emr/cli.py +1322 -1322
  86. gaia/agents/emr/constants.py +475 -475
  87. gaia/agents/emr/dashboard/__init__.py +4 -4
  88. gaia/agents/emr/dashboard/server.py +1974 -1974
  89. gaia/agents/jira/__init__.py +11 -11
  90. gaia/agents/jira/agent.py +894 -894
  91. gaia/agents/jira/jql_templates.py +299 -299
  92. gaia/agents/routing/__init__.py +7 -7
  93. gaia/agents/routing/agent.py +567 -570
  94. gaia/agents/routing/system_prompt.py +75 -75
  95. gaia/agents/summarize/__init__.py +11 -0
  96. gaia/agents/summarize/agent.py +885 -0
  97. gaia/agents/summarize/prompts.py +129 -0
  98. gaia/api/__init__.py +23 -23
  99. gaia/api/agent_registry.py +238 -238
  100. gaia/api/app.py +305 -305
  101. gaia/api/openai_server.py +575 -575
  102. gaia/api/schemas.py +186 -186
  103. gaia/api/sse_handler.py +373 -373
  104. gaia/apps/__init__.py +4 -4
  105. gaia/apps/llm/__init__.py +6 -6
  106. gaia/apps/llm/app.py +173 -169
  107. gaia/apps/summarize/app.py +116 -633
  108. gaia/apps/summarize/html_viewer.py +133 -133
  109. gaia/apps/summarize/pdf_formatter.py +284 -284
  110. gaia/audio/__init__.py +2 -2
  111. gaia/audio/audio_client.py +439 -439
  112. gaia/audio/audio_recorder.py +269 -269
  113. gaia/audio/kokoro_tts.py +599 -599
  114. gaia/audio/whisper_asr.py +432 -432
  115. gaia/chat/__init__.py +16 -16
  116. gaia/chat/app.py +430 -430
  117. gaia/chat/prompts.py +522 -522
  118. gaia/chat/sdk.py +1228 -1225
  119. gaia/cli.py +5481 -5632
  120. gaia/database/__init__.py +10 -10
  121. gaia/database/agent.py +176 -176
  122. gaia/database/mixin.py +290 -290
  123. gaia/database/testing.py +64 -64
  124. gaia/eval/batch_experiment.py +2332 -2332
  125. gaia/eval/claude.py +542 -542
  126. gaia/eval/config.py +37 -37
  127. gaia/eval/email_generator.py +512 -512
  128. gaia/eval/eval.py +3179 -3179
  129. gaia/eval/groundtruth.py +1130 -1130
  130. gaia/eval/transcript_generator.py +582 -582
  131. gaia/eval/webapp/README.md +167 -167
  132. gaia/eval/webapp/package-lock.json +875 -875
  133. gaia/eval/webapp/package.json +20 -20
  134. gaia/eval/webapp/public/app.js +3402 -3402
  135. gaia/eval/webapp/public/index.html +87 -87
  136. gaia/eval/webapp/public/styles.css +3661 -3661
  137. gaia/eval/webapp/server.js +415 -415
  138. gaia/eval/webapp/test-setup.js +72 -72
  139. gaia/llm/__init__.py +9 -2
  140. gaia/llm/base_client.py +60 -0
  141. gaia/llm/exceptions.py +12 -0
  142. gaia/llm/factory.py +70 -0
  143. gaia/llm/lemonade_client.py +3236 -3221
  144. gaia/llm/lemonade_manager.py +294 -294
  145. gaia/llm/providers/__init__.py +9 -0
  146. gaia/llm/providers/claude.py +108 -0
  147. gaia/llm/providers/lemonade.py +120 -0
  148. gaia/llm/providers/openai_provider.py +79 -0
  149. gaia/llm/vlm_client.py +382 -382
  150. gaia/logger.py +189 -189
  151. gaia/mcp/agent_mcp_server.py +245 -245
  152. gaia/mcp/blender_mcp_client.py +138 -138
  153. gaia/mcp/blender_mcp_server.py +648 -648
  154. gaia/mcp/context7_cache.py +332 -332
  155. gaia/mcp/external_services.py +518 -518
  156. gaia/mcp/mcp_bridge.py +811 -550
  157. gaia/mcp/servers/__init__.py +6 -6
  158. gaia/mcp/servers/docker_mcp.py +83 -83
  159. gaia/perf_analysis.py +361 -0
  160. gaia/rag/__init__.py +10 -10
  161. gaia/rag/app.py +293 -293
  162. gaia/rag/demo.py +304 -304
  163. gaia/rag/pdf_utils.py +235 -235
  164. gaia/rag/sdk.py +2194 -2194
  165. gaia/security.py +163 -163
  166. gaia/talk/app.py +289 -289
  167. gaia/talk/sdk.py +538 -538
  168. gaia/testing/__init__.py +87 -87
  169. gaia/testing/assertions.py +330 -330
  170. gaia/testing/fixtures.py +333 -333
  171. gaia/testing/mocks.py +493 -493
  172. gaia/util.py +46 -46
  173. gaia/utils/__init__.py +33 -33
  174. gaia/utils/file_watcher.py +675 -675
  175. gaia/utils/parsing.py +223 -223
  176. gaia/version.py +100 -100
  177. amd_gaia-0.15.0.dist-info/RECORD +0 -168
  178. gaia/agents/code/app.py +0 -266
  179. gaia/llm/llm_client.py +0 -723
  180. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.1.dist-info}/WHEEL +0 -0
  181. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.1.dist-info}/top_level.txt +0 -0
@@ -1,648 +1,648 @@
1
- # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
- # SPDX-License-Identifier: MIT
3
-
4
- # This Blender MCP client is a simplified and modified version of the BlenderMCP project from https://github.com/BlenderMCP/blender-mcp
5
-
6
- import json
7
- import socket
8
- import threading
9
- import time
10
- import traceback
11
-
12
- import bpy
13
- import mathutils
14
- from bpy.props import BoolProperty, IntProperty
15
-
16
- bl_info = {
17
- "name": "Simple Blender MCP",
18
- "author": "BlenderMCP",
19
- "version": (0, 3),
20
- "blender": (3, 0, 0),
21
- "location": "View3D > Sidebar > BlenderMCP",
22
- "description": "Connect Blender via MCP",
23
- "category": "Interface",
24
- }
25
-
26
-
27
- class SimpleBlenderMCPServer:
28
- def __init__(self, host="localhost", port=9876):
29
- self.host = host
30
- self.port = port
31
- self.running = False
32
- self.socket = None
33
- self.server_thread = None
34
-
35
- def start(self):
36
- if self.running:
37
- print("Server is already running")
38
- return
39
-
40
- self.running = True
41
-
42
- try:
43
- # Create socket
44
- self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
45
- self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
46
- self.socket.bind((self.host, self.port))
47
- self.socket.listen(1)
48
-
49
- # Start server thread
50
- self.server_thread = threading.Thread(target=self._server_loop)
51
- self.server_thread.daemon = True
52
- self.server_thread.start()
53
-
54
- print(f"SimpleMCP server started on {self.host}:{self.port}")
55
- except Exception as e:
56
- print(f"Failed to start server: {str(e)}")
57
- self.stop()
58
-
59
- def stop(self):
60
- self.running = False
61
-
62
- # Close socket
63
- if self.socket:
64
- try:
65
- self.socket.close()
66
- except Exception as e:
67
- print(f"Error closing socket: {e}")
68
- self.socket = None
69
-
70
- # Wait for thread to finish
71
- if self.server_thread:
72
- try:
73
- if self.server_thread.is_alive():
74
- self.server_thread.join(timeout=1.0)
75
- except Exception as e:
76
- print(f"Error joining server thread: {e}")
77
- self.server_thread = None
78
-
79
- print("SimpleMCP server stopped")
80
-
81
- def _server_loop(self):
82
- """Main server loop in a separate thread"""
83
- print("Server thread started")
84
- self.socket.settimeout(1.0) # Timeout to allow for stopping
85
-
86
- while self.running:
87
- try:
88
- # Accept new connection
89
- try:
90
- client, address = self.socket.accept()
91
- print(f"Connected to client: {address}")
92
-
93
- # Handle client in a separate thread
94
- client_thread = threading.Thread(
95
- target=self._handle_client, args=(client,)
96
- )
97
- client_thread.daemon = True
98
- client_thread.start()
99
- except socket.timeout:
100
- # Just check running condition
101
- continue
102
- except Exception as e:
103
- print(f"Error accepting connection: {str(e)}")
104
- time.sleep(0.5)
105
- except Exception as e:
106
- print(f"Error in server loop: {str(e)}")
107
- if not self.running:
108
- break
109
- time.sleep(0.5)
110
-
111
- print("Server thread stopped")
112
-
113
- def _handle_client(self, client):
114
- """Handle connected client"""
115
- print("Client handler started")
116
- client.settimeout(None) # No timeout
117
- buffer = b""
118
-
119
- try:
120
- while self.running:
121
- # Receive data
122
- try:
123
- data = client.recv(8192)
124
- if not data:
125
- print("Client disconnected")
126
- break
127
-
128
- buffer += data
129
- try:
130
- # Try to parse command
131
- command = json.loads(buffer.decode("utf-8"))
132
- buffer = b""
133
-
134
- # Execute command in Blender's main thread
135
- def execute_wrapper():
136
- try:
137
- response = self.execute_command(command)
138
- response_json = json.dumps(response)
139
- try:
140
- client.sendall(response_json.encode("utf-8"))
141
- except Exception as e:
142
- print(
143
- f"Failed to send response - client disconnected: {e}"
144
- )
145
- except Exception as e:
146
- print(f"Error executing command: {str(e)}")
147
- traceback.print_exc()
148
- try:
149
- error_response = {
150
- "status": "error",
151
- "message": str(e),
152
- }
153
- client.sendall(
154
- json.dumps(error_response).encode("utf-8")
155
- )
156
- except Exception as send_err:
157
- print(
158
- f"Failed to send error response - client disconnected: {send_err}"
159
- )
160
- return None
161
-
162
- # Schedule execution in main thread
163
- bpy.app.timers.register(execute_wrapper, first_interval=0.0)
164
- except json.JSONDecodeError:
165
- # Incomplete JSON data received, continue buffering
166
- continue
167
- except Exception as e:
168
- print(f"Error receiving data: {str(e)}")
169
- break
170
- except Exception as e:
171
- print(f"Error in client handler: {str(e)}")
172
- finally:
173
- try:
174
- client.close()
175
- except Exception as e:
176
- print(f"Error closing client connection: {e}")
177
- print("Client handler stopped")
178
-
179
- def execute_command(self, command):
180
- """Execute a command in the main Blender thread"""
181
- try:
182
- cmd_type = command.get("type")
183
- params = command.get("params", {})
184
-
185
- # Ensure we're in the right context
186
- if cmd_type in ["create_object", "modify_object", "delete_object"]:
187
- override = bpy.context.copy()
188
- override["area"] = [
189
- area for area in bpy.context.screen.areas if area.type == "VIEW_3D"
190
- ][0]
191
- with bpy.context.temp_override(**override):
192
- return self._execute_command_internal(command)
193
- else:
194
- return self._execute_command_internal(command)
195
-
196
- except Exception as e:
197
- print(f"Error executing command: {str(e)}")
198
- traceback.print_exc()
199
- return {"status": "error", "message": str(e)}
200
-
201
- def _execute_command_internal(self, command):
202
- """Internal command execution with proper context"""
203
- cmd_type = command.get("type")
204
- params = command.get("params", {})
205
-
206
- # Define available command handlers
207
- handlers = {
208
- "get_scene_info": self.get_scene_info,
209
- "create_object": self.create_object,
210
- "modify_object": self.modify_object,
211
- "delete_object": self.delete_object,
212
- "get_object_info": self.get_object_info,
213
- "execute_code": self.execute_code,
214
- }
215
-
216
- handler = handlers.get(cmd_type)
217
- if handler:
218
- try:
219
- print(f"Executing handler for {cmd_type}")
220
- result = handler(**params)
221
- print(f"Handler execution complete")
222
- return {"status": "success", "result": result}
223
- except Exception as e:
224
- print(f"Error in handler: {str(e)}")
225
- traceback.print_exc()
226
- return {"status": "error", "message": str(e)}
227
- else:
228
- return {"status": "error", "message": f"Unknown command type: {cmd_type}"}
229
-
230
- def get_scene_info(self):
231
- """Get information about the current Blender scene"""
232
- try:
233
- print("Getting scene info...")
234
- # Simplify the scene info to reduce data size
235
- scene_info = {
236
- "name": bpy.context.scene.name,
237
- "object_count": len(bpy.context.scene.objects),
238
- "objects": [],
239
- }
240
-
241
- # Collect minimal object information (limit to first 10 objects)
242
- for i, obj in enumerate(bpy.context.scene.objects):
243
- if i >= 10:
244
- break
245
-
246
- obj_info = {
247
- "name": obj.name,
248
- "type": obj.type,
249
- # Only include basic location data
250
- "location": [
251
- round(float(obj.location.x), 2),
252
- round(float(obj.location.y), 2),
253
- round(float(obj.location.z), 2),
254
- ],
255
- }
256
- scene_info["objects"].append(obj_info)
257
-
258
- print(f"Scene info collected: {len(scene_info['objects'])} objects")
259
- return scene_info
260
- except Exception as e:
261
- print(f"Error in get_scene_info: {str(e)}")
262
- traceback.print_exc()
263
- return {"error": str(e)}
264
-
265
- @staticmethod
266
- def _get_aabb(obj):
267
- """Returns the world-space axis-aligned bounding box (AABB) of an object."""
268
- if obj.type != "MESH":
269
- raise TypeError("Object must be a mesh")
270
-
271
- # Get the bounding box corners in local space
272
- local_bbox_corners = [mathutils.Vector(corner) for corner in obj.bound_box]
273
-
274
- # Convert to world coordinates
275
- world_bbox_corners = [
276
- obj.matrix_world @ corner for corner in local_bbox_corners
277
- ]
278
-
279
- # Compute axis-aligned min/max coordinates
280
- min_corner = mathutils.Vector(map(min, zip(*world_bbox_corners)))
281
- max_corner = mathutils.Vector(map(max, zip(*world_bbox_corners)))
282
-
283
- return [[*min_corner], [*max_corner]]
284
-
285
- def create_object(
286
- self,
287
- type="CUBE",
288
- name=None,
289
- location=(0, 0, 0),
290
- rotation=(0, 0, 0),
291
- scale=(1, 1, 1),
292
- ):
293
- """Create a new object in the scene"""
294
- try:
295
- # Deselect all objects first
296
- bpy.ops.object.select_all(action="DESELECT")
297
-
298
- # Create the object based on type
299
- if type == "CUBE":
300
- bpy.ops.mesh.primitive_cube_add(
301
- location=location, rotation=rotation, scale=scale
302
- )
303
- elif type == "SPHERE":
304
- bpy.ops.mesh.primitive_uv_sphere_add(
305
- location=location, rotation=rotation, scale=scale
306
- )
307
- elif type == "CYLINDER":
308
- bpy.ops.mesh.primitive_cylinder_add(
309
- location=location, rotation=rotation, scale=scale
310
- )
311
- elif type == "PLANE":
312
- bpy.ops.mesh.primitive_plane_add(
313
- location=location, rotation=rotation, scale=scale
314
- )
315
- elif type == "CONE":
316
- bpy.ops.mesh.primitive_cone_add(
317
- location=location, rotation=rotation, scale=scale
318
- )
319
- elif type == "EMPTY":
320
- bpy.ops.object.empty_add(
321
- location=location, rotation=rotation, scale=scale
322
- )
323
- elif type == "CAMERA":
324
- bpy.ops.object.camera_add(location=location, rotation=rotation)
325
- elif type == "LIGHT":
326
- bpy.ops.object.light_add(
327
- type="POINT", location=location, rotation=rotation, scale=scale
328
- )
329
- else:
330
- raise ValueError(f"Unsupported object type: {type}")
331
-
332
- # Force update the view layer
333
- bpy.context.view_layer.update()
334
-
335
- # Get the active object (which should be our newly created object)
336
- obj = bpy.context.view_layer.objects.active
337
-
338
- # If we don't have an active object, something went wrong
339
- if obj is None:
340
- raise RuntimeError("Failed to create object - no active object")
341
-
342
- # Make sure it's selected
343
- obj.select_set(True)
344
-
345
- # Rename if name is provided
346
- if name:
347
- obj.name = name
348
- if obj.data:
349
- obj.data.name = name
350
-
351
- # Return the object info
352
- result = {
353
- "name": obj.name,
354
- "type": obj.type,
355
- "location": [obj.location.x, obj.location.y, obj.location.z],
356
- "rotation": [
357
- obj.rotation_euler.x,
358
- obj.rotation_euler.y,
359
- obj.rotation_euler.z,
360
- ],
361
- "scale": [obj.scale.x, obj.scale.y, obj.scale.z],
362
- }
363
-
364
- if obj.type == "MESH":
365
- bounding_box = self._get_aabb(obj)
366
- result["world_bounding_box"] = bounding_box
367
-
368
- return result
369
- except Exception as e:
370
- print(f"Error in create_object: {str(e)}")
371
- traceback.print_exc()
372
- return {"error": str(e)}
373
-
374
- def modify_object(
375
- self, name, location=None, rotation=None, scale=None, visible=None
376
- ):
377
- """Modify an existing object in the scene"""
378
- # Find the object by name
379
- obj = bpy.data.objects.get(name)
380
- if not obj:
381
- raise ValueError(f"Object not found: {name}")
382
-
383
- # Modify properties as requested
384
- if location is not None:
385
- obj.location = location
386
-
387
- if rotation is not None:
388
- obj.rotation_euler = rotation
389
-
390
- if scale is not None:
391
- obj.scale = scale
392
-
393
- if visible is not None:
394
- obj.hide_viewport = not visible
395
- obj.hide_render = not visible
396
-
397
- result = {
398
- "name": obj.name,
399
- "type": obj.type,
400
- "location": [obj.location.x, obj.location.y, obj.location.z],
401
- "rotation": [
402
- obj.rotation_euler.x,
403
- obj.rotation_euler.y,
404
- obj.rotation_euler.z,
405
- ],
406
- "scale": [obj.scale.x, obj.scale.y, obj.scale.z],
407
- "visible": obj.visible_get(),
408
- }
409
-
410
- if obj.type == "MESH":
411
- bounding_box = self._get_aabb(obj)
412
- result["world_bounding_box"] = bounding_box
413
-
414
- return result
415
-
416
- def delete_object(self, name):
417
- """Delete an object from the scene"""
418
- obj = bpy.data.objects.get(name)
419
- if not obj:
420
- raise ValueError(f"Object not found: {name}")
421
-
422
- # Store the name to return
423
- obj_name = obj.name
424
-
425
- # Select and delete the object
426
- if obj:
427
- bpy.data.objects.remove(obj, do_unlink=True)
428
-
429
- return {"deleted": obj_name}
430
-
431
- def get_object_info(self, name):
432
- """Get detailed information about a specific object"""
433
- obj = bpy.data.objects.get(name)
434
- if not obj:
435
- raise ValueError(f"Object not found: {name}")
436
-
437
- # Basic object info
438
- obj_info = {
439
- "name": obj.name,
440
- "type": obj.type,
441
- "location": [obj.location.x, obj.location.y, obj.location.z],
442
- "rotation": [
443
- obj.rotation_euler.x,
444
- obj.rotation_euler.y,
445
- obj.rotation_euler.z,
446
- ],
447
- "scale": [obj.scale.x, obj.scale.y, obj.scale.z],
448
- "visible": obj.visible_get(),
449
- }
450
-
451
- if obj.type == "MESH":
452
- bounding_box = self._get_aabb(obj)
453
- obj_info["world_bounding_box"] = bounding_box
454
-
455
- # Add mesh data if applicable
456
- mesh = obj.data
457
- obj_info["mesh"] = {
458
- "vertices": len(mesh.vertices),
459
- "edges": len(mesh.edges),
460
- "polygons": len(mesh.polygons),
461
- }
462
-
463
- return obj_info
464
-
465
- def execute_code(self, code):
466
- """Execute arbitrary Blender Python code"""
467
- try:
468
- # Create a namespace for execution and a buffer to capture output
469
- import io
470
- import sys
471
- from contextlib import redirect_stderr, redirect_stdout
472
-
473
- namespace = {"bpy": bpy}
474
- stdout_buffer = io.StringIO()
475
- stderr_buffer = io.StringIO()
476
- result_value = None
477
-
478
- # Print to Blender console that we're executing code
479
- print("\n----- EXECUTING CODE IN BLENDER -----")
480
- print(code)
481
- print("----- CODE EXECUTION OUTPUT -----")
482
-
483
- # Class to split output between buffer and console
484
- class TeeOutput:
485
- def __init__(self, buffer, original):
486
- self.buffer = buffer
487
- self.original = original
488
-
489
- def write(self, text):
490
- self.buffer.write(text)
491
- self.original.write(text)
492
-
493
- def flush(self):
494
- self.original.flush()
495
-
496
- # Setup tee for both stdout and stderr
497
- stdout_tee = TeeOutput(stdout_buffer, sys.__stdout__)
498
- stderr_tee = TeeOutput(stderr_buffer, sys.__stderr__)
499
-
500
- # Execute the code and capture output and return value
501
- with redirect_stdout(stdout_tee), redirect_stderr(stderr_tee):
502
- exec_result = exec(code, namespace)
503
- if "result" in namespace:
504
- result_value = namespace["result"]
505
-
506
- # Get the captured output
507
- stdout_output = stdout_buffer.getvalue()
508
- stderr_output = stderr_buffer.getvalue()
509
-
510
- # Print execution completion to console
511
- print("----- CODE EXECUTION COMPLETE -----")
512
- if result_value:
513
- print("----- RETURNED RESULT -----")
514
- print(str(result_value))
515
- print("\n")
516
-
517
- # Return a more detailed response
518
- return {
519
- "executed": True,
520
- "stdout": stdout_output,
521
- "stderr": stderr_output,
522
- "result": result_value,
523
- }
524
- except Exception as e:
525
- import traceback
526
-
527
- tb_str = traceback.format_exc()
528
- # Print error to console
529
- print("----- CODE EXECUTION ERROR -----")
530
- print(str(e))
531
- print(tb_str)
532
- print("--------------------------------")
533
- raise Exception(f"Code execution error: {str(e)}\n{tb_str}")
534
-
535
-
536
- # Blender UI Panel
537
- class SIMPLEMCP_PT_Panel(bpy.types.Panel):
538
- bl_label = "Blender MCP"
539
- bl_idname = "SIMPLEMCP_PT_Panel"
540
- bl_space_type = "VIEW_3D"
541
- bl_region_type = "UI"
542
- bl_category = "BlenderMCP"
543
-
544
- def draw(self, context):
545
- layout = self.layout
546
- scene = context.scene
547
-
548
- layout.prop(scene, "simplemcp_port")
549
-
550
- if not scene.simplemcp_server_running:
551
- layout.operator("simplemcp.start_server", text="Start MCP Server")
552
- else:
553
- layout.operator("simplemcp.stop_server", text="Stop MCP Server")
554
- layout.label(text=f"Running on port {scene.simplemcp_port}")
555
-
556
-
557
- # Operator to start the server
558
- class SIMPLEMCP_OT_StartServer(bpy.types.Operator):
559
- bl_idname = "simplemcp.start_server"
560
- bl_label = "Connect to GAIA"
561
- bl_description = "Start the BlenderMCP server to connect to GAIA"
562
-
563
- def execute(self, context):
564
- scene = context.scene
565
-
566
- # Create a new server instance
567
- if not hasattr(bpy.types, "simplemcp_server") or not bpy.types.simplemcp_server:
568
- bpy.types.simplemcp_server = SimpleBlenderMCPServer(
569
- port=scene.simplemcp_port
570
- )
571
-
572
- # Start the server
573
- bpy.types.simplemcp_server.start()
574
- scene.simplemcp_server_running = True
575
-
576
- return {"FINISHED"}
577
-
578
-
579
- # Operator to stop the server
580
- class SIMPLEMCP_OT_StopServer(bpy.types.Operator):
581
- bl_idname = "simplemcp.stop_server"
582
- bl_label = "Stop the connection"
583
- bl_description = "Stop the connection"
584
-
585
- def execute(self, context):
586
- scene = context.scene
587
-
588
- # Stop the server if it exists
589
- if hasattr(bpy.types, "simplemcp_server") and bpy.types.simplemcp_server:
590
- bpy.types.simplemcp_server.stop()
591
- del bpy.types.simplemcp_server
592
-
593
- scene.simplemcp_server_running = False
594
-
595
- return {"FINISHED"}
596
-
597
-
598
- # Registration functions
599
- def register():
600
- bpy.types.Scene.simplemcp_port = IntProperty(
601
- name="Port",
602
- description="Port for the BlenderMCP server",
603
- default=9876,
604
- min=1024,
605
- max=65535,
606
- )
607
-
608
- bpy.types.Scene.simplemcp_server_running = BoolProperty(
609
- name="Server Running", default=False
610
- )
611
-
612
- bpy.utils.register_class(SIMPLEMCP_PT_Panel)
613
- bpy.utils.register_class(SIMPLEMCP_OT_StartServer)
614
- bpy.utils.register_class(SIMPLEMCP_OT_StopServer)
615
-
616
- print("BlenderMCP addon registered")
617
-
618
-
619
- def unregister():
620
- # Stop the server if it's running
621
- if hasattr(bpy.types, "simplemcp_server") and bpy.types.simplemcp_server:
622
- try:
623
- bpy.types.simplemcp_server.stop()
624
- del bpy.types.simplemcp_server
625
- except Exception as e:
626
- print(f"Error stopping server: {str(e)}")
627
- traceback.print_exc()
628
-
629
- try:
630
- bpy.utils.unregister_class(SIMPLEMCP_PT_Panel)
631
- bpy.utils.unregister_class(SIMPLEMCP_OT_StartServer)
632
- bpy.utils.unregister_class(SIMPLEMCP_OT_StopServer)
633
- except Exception as e:
634
- print(f"Error unregistering classes: {str(e)}")
635
- traceback.print_exc()
636
-
637
- try:
638
- del bpy.types.Scene.simplemcp_port
639
- del bpy.types.Scene.simplemcp_server_running
640
- except Exception as e:
641
- print(f"Error removing properties: {str(e)}")
642
- traceback.print_exc()
643
-
644
- print("BlenderMCP addon unregistered")
645
-
646
-
647
- if __name__ == "__main__":
648
- register()
1
+ # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ # This Blender MCP client is a simplified and modified version of the BlenderMCP project from https://github.com/BlenderMCP/blender-mcp
5
+
6
+ import json
7
+ import socket
8
+ import threading
9
+ import time
10
+ import traceback
11
+
12
+ import bpy
13
+ import mathutils
14
+ from bpy.props import BoolProperty, IntProperty
15
+
16
+ bl_info = {
17
+ "name": "Simple Blender MCP",
18
+ "author": "BlenderMCP",
19
+ "version": (0, 3),
20
+ "blender": (3, 0, 0),
21
+ "location": "View3D > Sidebar > BlenderMCP",
22
+ "description": "Connect Blender via MCP",
23
+ "category": "Interface",
24
+ }
25
+
26
+
27
+ class SimpleBlenderMCPServer:
28
+ def __init__(self, host="localhost", port=9876):
29
+ self.host = host
30
+ self.port = port
31
+ self.running = False
32
+ self.socket = None
33
+ self.server_thread = None
34
+
35
+ def start(self):
36
+ if self.running:
37
+ print("Server is already running")
38
+ return
39
+
40
+ self.running = True
41
+
42
+ try:
43
+ # Create socket
44
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
45
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
46
+ self.socket.bind((self.host, self.port))
47
+ self.socket.listen(1)
48
+
49
+ # Start server thread
50
+ self.server_thread = threading.Thread(target=self._server_loop)
51
+ self.server_thread.daemon = True
52
+ self.server_thread.start()
53
+
54
+ print(f"SimpleMCP server started on {self.host}:{self.port}")
55
+ except Exception as e:
56
+ print(f"Failed to start server: {str(e)}")
57
+ self.stop()
58
+
59
+ def stop(self):
60
+ self.running = False
61
+
62
+ # Close socket
63
+ if self.socket:
64
+ try:
65
+ self.socket.close()
66
+ except Exception as e:
67
+ print(f"Error closing socket: {e}")
68
+ self.socket = None
69
+
70
+ # Wait for thread to finish
71
+ if self.server_thread:
72
+ try:
73
+ if self.server_thread.is_alive():
74
+ self.server_thread.join(timeout=1.0)
75
+ except Exception as e:
76
+ print(f"Error joining server thread: {e}")
77
+ self.server_thread = None
78
+
79
+ print("SimpleMCP server stopped")
80
+
81
+ def _server_loop(self):
82
+ """Main server loop in a separate thread"""
83
+ print("Server thread started")
84
+ self.socket.settimeout(1.0) # Timeout to allow for stopping
85
+
86
+ while self.running:
87
+ try:
88
+ # Accept new connection
89
+ try:
90
+ client, address = self.socket.accept()
91
+ print(f"Connected to client: {address}")
92
+
93
+ # Handle client in a separate thread
94
+ client_thread = threading.Thread(
95
+ target=self._handle_client, args=(client,)
96
+ )
97
+ client_thread.daemon = True
98
+ client_thread.start()
99
+ except socket.timeout:
100
+ # Just check running condition
101
+ continue
102
+ except Exception as e:
103
+ print(f"Error accepting connection: {str(e)}")
104
+ time.sleep(0.5)
105
+ except Exception as e:
106
+ print(f"Error in server loop: {str(e)}")
107
+ if not self.running:
108
+ break
109
+ time.sleep(0.5)
110
+
111
+ print("Server thread stopped")
112
+
113
+ def _handle_client(self, client):
114
+ """Handle connected client"""
115
+ print("Client handler started")
116
+ client.settimeout(None) # No timeout
117
+ buffer = b""
118
+
119
+ try:
120
+ while self.running:
121
+ # Receive data
122
+ try:
123
+ data = client.recv(8192)
124
+ if not data:
125
+ print("Client disconnected")
126
+ break
127
+
128
+ buffer += data
129
+ try:
130
+ # Try to parse command
131
+ command = json.loads(buffer.decode("utf-8"))
132
+ buffer = b""
133
+
134
+ # Execute command in Blender's main thread
135
+ def execute_wrapper():
136
+ try:
137
+ response = self.execute_command(command)
138
+ response_json = json.dumps(response)
139
+ try:
140
+ client.sendall(response_json.encode("utf-8"))
141
+ except Exception as e:
142
+ print(
143
+ f"Failed to send response - client disconnected: {e}"
144
+ )
145
+ except Exception as e:
146
+ print(f"Error executing command: {str(e)}")
147
+ traceback.print_exc()
148
+ try:
149
+ error_response = {
150
+ "status": "error",
151
+ "message": str(e),
152
+ }
153
+ client.sendall(
154
+ json.dumps(error_response).encode("utf-8")
155
+ )
156
+ except Exception as send_err:
157
+ print(
158
+ f"Failed to send error response - client disconnected: {send_err}"
159
+ )
160
+ return None
161
+
162
+ # Schedule execution in main thread
163
+ bpy.app.timers.register(execute_wrapper, first_interval=0.0)
164
+ except json.JSONDecodeError:
165
+ # Incomplete JSON data received, continue buffering
166
+ continue
167
+ except Exception as e:
168
+ print(f"Error receiving data: {str(e)}")
169
+ break
170
+ except Exception as e:
171
+ print(f"Error in client handler: {str(e)}")
172
+ finally:
173
+ try:
174
+ client.close()
175
+ except Exception as e:
176
+ print(f"Error closing client connection: {e}")
177
+ print("Client handler stopped")
178
+
179
+ def execute_command(self, command):
180
+ """Execute a command in the main Blender thread"""
181
+ try:
182
+ cmd_type = command.get("type")
183
+ params = command.get("params", {})
184
+
185
+ # Ensure we're in the right context
186
+ if cmd_type in ["create_object", "modify_object", "delete_object"]:
187
+ override = bpy.context.copy()
188
+ override["area"] = [
189
+ area for area in bpy.context.screen.areas if area.type == "VIEW_3D"
190
+ ][0]
191
+ with bpy.context.temp_override(**override):
192
+ return self._execute_command_internal(command)
193
+ else:
194
+ return self._execute_command_internal(command)
195
+
196
+ except Exception as e:
197
+ print(f"Error executing command: {str(e)}")
198
+ traceback.print_exc()
199
+ return {"status": "error", "message": str(e)}
200
+
201
+ def _execute_command_internal(self, command):
202
+ """Internal command execution with proper context"""
203
+ cmd_type = command.get("type")
204
+ params = command.get("params", {})
205
+
206
+ # Define available command handlers
207
+ handlers = {
208
+ "get_scene_info": self.get_scene_info,
209
+ "create_object": self.create_object,
210
+ "modify_object": self.modify_object,
211
+ "delete_object": self.delete_object,
212
+ "get_object_info": self.get_object_info,
213
+ "execute_code": self.execute_code,
214
+ }
215
+
216
+ handler = handlers.get(cmd_type)
217
+ if handler:
218
+ try:
219
+ print(f"Executing handler for {cmd_type}")
220
+ result = handler(**params)
221
+ print(f"Handler execution complete")
222
+ return {"status": "success", "result": result}
223
+ except Exception as e:
224
+ print(f"Error in handler: {str(e)}")
225
+ traceback.print_exc()
226
+ return {"status": "error", "message": str(e)}
227
+ else:
228
+ return {"status": "error", "message": f"Unknown command type: {cmd_type}"}
229
+
230
+ def get_scene_info(self):
231
+ """Get information about the current Blender scene"""
232
+ try:
233
+ print("Getting scene info...")
234
+ # Simplify the scene info to reduce data size
235
+ scene_info = {
236
+ "name": bpy.context.scene.name,
237
+ "object_count": len(bpy.context.scene.objects),
238
+ "objects": [],
239
+ }
240
+
241
+ # Collect minimal object information (limit to first 10 objects)
242
+ for i, obj in enumerate(bpy.context.scene.objects):
243
+ if i >= 10:
244
+ break
245
+
246
+ obj_info = {
247
+ "name": obj.name,
248
+ "type": obj.type,
249
+ # Only include basic location data
250
+ "location": [
251
+ round(float(obj.location.x), 2),
252
+ round(float(obj.location.y), 2),
253
+ round(float(obj.location.z), 2),
254
+ ],
255
+ }
256
+ scene_info["objects"].append(obj_info)
257
+
258
+ print(f"Scene info collected: {len(scene_info['objects'])} objects")
259
+ return scene_info
260
+ except Exception as e:
261
+ print(f"Error in get_scene_info: {str(e)}")
262
+ traceback.print_exc()
263
+ return {"error": str(e)}
264
+
265
+ @staticmethod
266
+ def _get_aabb(obj):
267
+ """Returns the world-space axis-aligned bounding box (AABB) of an object."""
268
+ if obj.type != "MESH":
269
+ raise TypeError("Object must be a mesh")
270
+
271
+ # Get the bounding box corners in local space
272
+ local_bbox_corners = [mathutils.Vector(corner) for corner in obj.bound_box]
273
+
274
+ # Convert to world coordinates
275
+ world_bbox_corners = [
276
+ obj.matrix_world @ corner for corner in local_bbox_corners
277
+ ]
278
+
279
+ # Compute axis-aligned min/max coordinates
280
+ min_corner = mathutils.Vector(map(min, zip(*world_bbox_corners)))
281
+ max_corner = mathutils.Vector(map(max, zip(*world_bbox_corners)))
282
+
283
+ return [[*min_corner], [*max_corner]]
284
+
285
+ def create_object(
286
+ self,
287
+ type="CUBE",
288
+ name=None,
289
+ location=(0, 0, 0),
290
+ rotation=(0, 0, 0),
291
+ scale=(1, 1, 1),
292
+ ):
293
+ """Create a new object in the scene"""
294
+ try:
295
+ # Deselect all objects first
296
+ bpy.ops.object.select_all(action="DESELECT")
297
+
298
+ # Create the object based on type
299
+ if type == "CUBE":
300
+ bpy.ops.mesh.primitive_cube_add(
301
+ location=location, rotation=rotation, scale=scale
302
+ )
303
+ elif type == "SPHERE":
304
+ bpy.ops.mesh.primitive_uv_sphere_add(
305
+ location=location, rotation=rotation, scale=scale
306
+ )
307
+ elif type == "CYLINDER":
308
+ bpy.ops.mesh.primitive_cylinder_add(
309
+ location=location, rotation=rotation, scale=scale
310
+ )
311
+ elif type == "PLANE":
312
+ bpy.ops.mesh.primitive_plane_add(
313
+ location=location, rotation=rotation, scale=scale
314
+ )
315
+ elif type == "CONE":
316
+ bpy.ops.mesh.primitive_cone_add(
317
+ location=location, rotation=rotation, scale=scale
318
+ )
319
+ elif type == "EMPTY":
320
+ bpy.ops.object.empty_add(
321
+ location=location, rotation=rotation, scale=scale
322
+ )
323
+ elif type == "CAMERA":
324
+ bpy.ops.object.camera_add(location=location, rotation=rotation)
325
+ elif type == "LIGHT":
326
+ bpy.ops.object.light_add(
327
+ type="POINT", location=location, rotation=rotation, scale=scale
328
+ )
329
+ else:
330
+ raise ValueError(f"Unsupported object type: {type}")
331
+
332
+ # Force update the view layer
333
+ bpy.context.view_layer.update()
334
+
335
+ # Get the active object (which should be our newly created object)
336
+ obj = bpy.context.view_layer.objects.active
337
+
338
+ # If we don't have an active object, something went wrong
339
+ if obj is None:
340
+ raise RuntimeError("Failed to create object - no active object")
341
+
342
+ # Make sure it's selected
343
+ obj.select_set(True)
344
+
345
+ # Rename if name is provided
346
+ if name:
347
+ obj.name = name
348
+ if obj.data:
349
+ obj.data.name = name
350
+
351
+ # Return the object info
352
+ result = {
353
+ "name": obj.name,
354
+ "type": obj.type,
355
+ "location": [obj.location.x, obj.location.y, obj.location.z],
356
+ "rotation": [
357
+ obj.rotation_euler.x,
358
+ obj.rotation_euler.y,
359
+ obj.rotation_euler.z,
360
+ ],
361
+ "scale": [obj.scale.x, obj.scale.y, obj.scale.z],
362
+ }
363
+
364
+ if obj.type == "MESH":
365
+ bounding_box = self._get_aabb(obj)
366
+ result["world_bounding_box"] = bounding_box
367
+
368
+ return result
369
+ except Exception as e:
370
+ print(f"Error in create_object: {str(e)}")
371
+ traceback.print_exc()
372
+ return {"error": str(e)}
373
+
374
+ def modify_object(
375
+ self, name, location=None, rotation=None, scale=None, visible=None
376
+ ):
377
+ """Modify an existing object in the scene"""
378
+ # Find the object by name
379
+ obj = bpy.data.objects.get(name)
380
+ if not obj:
381
+ raise ValueError(f"Object not found: {name}")
382
+
383
+ # Modify properties as requested
384
+ if location is not None:
385
+ obj.location = location
386
+
387
+ if rotation is not None:
388
+ obj.rotation_euler = rotation
389
+
390
+ if scale is not None:
391
+ obj.scale = scale
392
+
393
+ if visible is not None:
394
+ obj.hide_viewport = not visible
395
+ obj.hide_render = not visible
396
+
397
+ result = {
398
+ "name": obj.name,
399
+ "type": obj.type,
400
+ "location": [obj.location.x, obj.location.y, obj.location.z],
401
+ "rotation": [
402
+ obj.rotation_euler.x,
403
+ obj.rotation_euler.y,
404
+ obj.rotation_euler.z,
405
+ ],
406
+ "scale": [obj.scale.x, obj.scale.y, obj.scale.z],
407
+ "visible": obj.visible_get(),
408
+ }
409
+
410
+ if obj.type == "MESH":
411
+ bounding_box = self._get_aabb(obj)
412
+ result["world_bounding_box"] = bounding_box
413
+
414
+ return result
415
+
416
+ def delete_object(self, name):
417
+ """Delete an object from the scene"""
418
+ obj = bpy.data.objects.get(name)
419
+ if not obj:
420
+ raise ValueError(f"Object not found: {name}")
421
+
422
+ # Store the name to return
423
+ obj_name = obj.name
424
+
425
+ # Select and delete the object
426
+ if obj:
427
+ bpy.data.objects.remove(obj, do_unlink=True)
428
+
429
+ return {"deleted": obj_name}
430
+
431
+ def get_object_info(self, name):
432
+ """Get detailed information about a specific object"""
433
+ obj = bpy.data.objects.get(name)
434
+ if not obj:
435
+ raise ValueError(f"Object not found: {name}")
436
+
437
+ # Basic object info
438
+ obj_info = {
439
+ "name": obj.name,
440
+ "type": obj.type,
441
+ "location": [obj.location.x, obj.location.y, obj.location.z],
442
+ "rotation": [
443
+ obj.rotation_euler.x,
444
+ obj.rotation_euler.y,
445
+ obj.rotation_euler.z,
446
+ ],
447
+ "scale": [obj.scale.x, obj.scale.y, obj.scale.z],
448
+ "visible": obj.visible_get(),
449
+ }
450
+
451
+ if obj.type == "MESH":
452
+ bounding_box = self._get_aabb(obj)
453
+ obj_info["world_bounding_box"] = bounding_box
454
+
455
+ # Add mesh data if applicable
456
+ mesh = obj.data
457
+ obj_info["mesh"] = {
458
+ "vertices": len(mesh.vertices),
459
+ "edges": len(mesh.edges),
460
+ "polygons": len(mesh.polygons),
461
+ }
462
+
463
+ return obj_info
464
+
465
+ def execute_code(self, code):
466
+ """Execute arbitrary Blender Python code"""
467
+ try:
468
+ # Create a namespace for execution and a buffer to capture output
469
+ import io
470
+ import sys
471
+ from contextlib import redirect_stderr, redirect_stdout
472
+
473
+ namespace = {"bpy": bpy}
474
+ stdout_buffer = io.StringIO()
475
+ stderr_buffer = io.StringIO()
476
+ result_value = None
477
+
478
+ # Print to Blender console that we're executing code
479
+ print("\n----- EXECUTING CODE IN BLENDER -----")
480
+ print(code)
481
+ print("----- CODE EXECUTION OUTPUT -----")
482
+
483
+ # Class to split output between buffer and console
484
+ class TeeOutput:
485
+ def __init__(self, buffer, original):
486
+ self.buffer = buffer
487
+ self.original = original
488
+
489
+ def write(self, text):
490
+ self.buffer.write(text)
491
+ self.original.write(text)
492
+
493
+ def flush(self):
494
+ self.original.flush()
495
+
496
+ # Setup tee for both stdout and stderr
497
+ stdout_tee = TeeOutput(stdout_buffer, sys.__stdout__)
498
+ stderr_tee = TeeOutput(stderr_buffer, sys.__stderr__)
499
+
500
+ # Execute the code and capture output and return value
501
+ with redirect_stdout(stdout_tee), redirect_stderr(stderr_tee):
502
+ exec_result = exec(code, namespace)
503
+ if "result" in namespace:
504
+ result_value = namespace["result"]
505
+
506
+ # Get the captured output
507
+ stdout_output = stdout_buffer.getvalue()
508
+ stderr_output = stderr_buffer.getvalue()
509
+
510
+ # Print execution completion to console
511
+ print("----- CODE EXECUTION COMPLETE -----")
512
+ if result_value:
513
+ print("----- RETURNED RESULT -----")
514
+ print(str(result_value))
515
+ print("\n")
516
+
517
+ # Return a more detailed response
518
+ return {
519
+ "executed": True,
520
+ "stdout": stdout_output,
521
+ "stderr": stderr_output,
522
+ "result": result_value,
523
+ }
524
+ except Exception as e:
525
+ import traceback
526
+
527
+ tb_str = traceback.format_exc()
528
+ # Print error to console
529
+ print("----- CODE EXECUTION ERROR -----")
530
+ print(str(e))
531
+ print(tb_str)
532
+ print("--------------------------------")
533
+ raise Exception(f"Code execution error: {str(e)}\n{tb_str}")
534
+
535
+
536
+ # Blender UI Panel
537
+ class SIMPLEMCP_PT_Panel(bpy.types.Panel):
538
+ bl_label = "Blender MCP"
539
+ bl_idname = "SIMPLEMCP_PT_Panel"
540
+ bl_space_type = "VIEW_3D"
541
+ bl_region_type = "UI"
542
+ bl_category = "BlenderMCP"
543
+
544
+ def draw(self, context):
545
+ layout = self.layout
546
+ scene = context.scene
547
+
548
+ layout.prop(scene, "simplemcp_port")
549
+
550
+ if not scene.simplemcp_server_running:
551
+ layout.operator("simplemcp.start_server", text="Start MCP Server")
552
+ else:
553
+ layout.operator("simplemcp.stop_server", text="Stop MCP Server")
554
+ layout.label(text=f"Running on port {scene.simplemcp_port}")
555
+
556
+
557
+ # Operator to start the server
558
+ class SIMPLEMCP_OT_StartServer(bpy.types.Operator):
559
+ bl_idname = "simplemcp.start_server"
560
+ bl_label = "Connect to GAIA"
561
+ bl_description = "Start the BlenderMCP server to connect to GAIA"
562
+
563
+ def execute(self, context):
564
+ scene = context.scene
565
+
566
+ # Create a new server instance
567
+ if not hasattr(bpy.types, "simplemcp_server") or not bpy.types.simplemcp_server:
568
+ bpy.types.simplemcp_server = SimpleBlenderMCPServer(
569
+ port=scene.simplemcp_port
570
+ )
571
+
572
+ # Start the server
573
+ bpy.types.simplemcp_server.start()
574
+ scene.simplemcp_server_running = True
575
+
576
+ return {"FINISHED"}
577
+
578
+
579
+ # Operator to stop the server
580
+ class SIMPLEMCP_OT_StopServer(bpy.types.Operator):
581
+ bl_idname = "simplemcp.stop_server"
582
+ bl_label = "Stop the connection"
583
+ bl_description = "Stop the connection"
584
+
585
+ def execute(self, context):
586
+ scene = context.scene
587
+
588
+ # Stop the server if it exists
589
+ if hasattr(bpy.types, "simplemcp_server") and bpy.types.simplemcp_server:
590
+ bpy.types.simplemcp_server.stop()
591
+ del bpy.types.simplemcp_server
592
+
593
+ scene.simplemcp_server_running = False
594
+
595
+ return {"FINISHED"}
596
+
597
+
598
+ # Registration functions
599
+ def register():
600
+ bpy.types.Scene.simplemcp_port = IntProperty(
601
+ name="Port",
602
+ description="Port for the BlenderMCP server",
603
+ default=9876,
604
+ min=1024,
605
+ max=65535,
606
+ )
607
+
608
+ bpy.types.Scene.simplemcp_server_running = BoolProperty(
609
+ name="Server Running", default=False
610
+ )
611
+
612
+ bpy.utils.register_class(SIMPLEMCP_PT_Panel)
613
+ bpy.utils.register_class(SIMPLEMCP_OT_StartServer)
614
+ bpy.utils.register_class(SIMPLEMCP_OT_StopServer)
615
+
616
+ print("BlenderMCP addon registered")
617
+
618
+
619
+ def unregister():
620
+ # Stop the server if it's running
621
+ if hasattr(bpy.types, "simplemcp_server") and bpy.types.simplemcp_server:
622
+ try:
623
+ bpy.types.simplemcp_server.stop()
624
+ del bpy.types.simplemcp_server
625
+ except Exception as e:
626
+ print(f"Error stopping server: {str(e)}")
627
+ traceback.print_exc()
628
+
629
+ try:
630
+ bpy.utils.unregister_class(SIMPLEMCP_PT_Panel)
631
+ bpy.utils.unregister_class(SIMPLEMCP_OT_StartServer)
632
+ bpy.utils.unregister_class(SIMPLEMCP_OT_StopServer)
633
+ except Exception as e:
634
+ print(f"Error unregistering classes: {str(e)}")
635
+ traceback.print_exc()
636
+
637
+ try:
638
+ del bpy.types.Scene.simplemcp_port
639
+ del bpy.types.Scene.simplemcp_server_running
640
+ except Exception as e:
641
+ print(f"Error removing properties: {str(e)}")
642
+ traceback.print_exc()
643
+
644
+ print("BlenderMCP addon unregistered")
645
+
646
+
647
+ if __name__ == "__main__":
648
+ register()