amd-gaia 0.15.0__py3-none-any.whl → 0.15.2__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 (185) hide show
  1. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/METADATA +222 -223
  2. amd_gaia-0.15.2.dist-info/RECORD +182 -0
  3. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/WHEEL +1 -1
  4. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/entry_points.txt +1 -0
  5. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/licenses/LICENSE.md +20 -20
  6. gaia/__init__.py +29 -29
  7. gaia/agents/__init__.py +19 -19
  8. gaia/agents/base/__init__.py +9 -9
  9. gaia/agents/base/agent.py +2132 -2177
  10. gaia/agents/base/api_agent.py +119 -120
  11. gaia/agents/base/console.py +1967 -1841
  12. gaia/agents/base/errors.py +237 -237
  13. gaia/agents/base/mcp_agent.py +86 -86
  14. gaia/agents/base/tools.py +88 -83
  15. gaia/agents/blender/__init__.py +7 -0
  16. gaia/agents/blender/agent.py +553 -556
  17. gaia/agents/blender/agent_simple.py +133 -135
  18. gaia/agents/blender/app.py +211 -211
  19. gaia/agents/blender/app_simple.py +41 -41
  20. gaia/agents/blender/core/__init__.py +16 -16
  21. gaia/agents/blender/core/materials.py +506 -506
  22. gaia/agents/blender/core/objects.py +316 -316
  23. gaia/agents/blender/core/rendering.py +225 -225
  24. gaia/agents/blender/core/scene.py +220 -220
  25. gaia/agents/blender/core/view.py +146 -146
  26. gaia/agents/chat/__init__.py +9 -9
  27. gaia/agents/chat/agent.py +809 -835
  28. gaia/agents/chat/app.py +1065 -1058
  29. gaia/agents/chat/session.py +508 -508
  30. gaia/agents/chat/tools/__init__.py +15 -15
  31. gaia/agents/chat/tools/file_tools.py +96 -96
  32. gaia/agents/chat/tools/rag_tools.py +1744 -1729
  33. gaia/agents/chat/tools/shell_tools.py +437 -436
  34. gaia/agents/code/__init__.py +7 -7
  35. gaia/agents/code/agent.py +549 -549
  36. gaia/agents/code/cli.py +377 -0
  37. gaia/agents/code/models.py +135 -135
  38. gaia/agents/code/orchestration/__init__.py +24 -24
  39. gaia/agents/code/orchestration/checklist_executor.py +1763 -1763
  40. gaia/agents/code/orchestration/checklist_generator.py +713 -713
  41. gaia/agents/code/orchestration/factories/__init__.py +9 -9
  42. gaia/agents/code/orchestration/factories/base.py +63 -63
  43. gaia/agents/code/orchestration/factories/nextjs_factory.py +118 -118
  44. gaia/agents/code/orchestration/factories/python_factory.py +106 -106
  45. gaia/agents/code/orchestration/orchestrator.py +841 -841
  46. gaia/agents/code/orchestration/project_analyzer.py +391 -391
  47. gaia/agents/code/orchestration/steps/__init__.py +67 -67
  48. gaia/agents/code/orchestration/steps/base.py +188 -188
  49. gaia/agents/code/orchestration/steps/error_handler.py +314 -314
  50. gaia/agents/code/orchestration/steps/nextjs.py +828 -828
  51. gaia/agents/code/orchestration/steps/python.py +307 -307
  52. gaia/agents/code/orchestration/template_catalog.py +469 -469
  53. gaia/agents/code/orchestration/workflows/__init__.py +14 -14
  54. gaia/agents/code/orchestration/workflows/base.py +80 -80
  55. gaia/agents/code/orchestration/workflows/nextjs.py +186 -186
  56. gaia/agents/code/orchestration/workflows/python.py +94 -94
  57. gaia/agents/code/prompts/__init__.py +11 -11
  58. gaia/agents/code/prompts/base_prompt.py +77 -77
  59. gaia/agents/code/prompts/code_patterns.py +2034 -2036
  60. gaia/agents/code/prompts/nextjs_prompt.py +40 -40
  61. gaia/agents/code/prompts/python_prompt.py +109 -109
  62. gaia/agents/code/schema_inference.py +365 -365
  63. gaia/agents/code/system_prompt.py +41 -41
  64. gaia/agents/code/tools/__init__.py +42 -42
  65. gaia/agents/code/tools/cli_tools.py +1138 -1138
  66. gaia/agents/code/tools/code_formatting.py +319 -319
  67. gaia/agents/code/tools/code_tools.py +769 -769
  68. gaia/agents/code/tools/error_fixing.py +1347 -1347
  69. gaia/agents/code/tools/external_tools.py +180 -180
  70. gaia/agents/code/tools/file_io.py +845 -845
  71. gaia/agents/code/tools/prisma_tools.py +190 -190
  72. gaia/agents/code/tools/project_management.py +1016 -1016
  73. gaia/agents/code/tools/testing.py +321 -321
  74. gaia/agents/code/tools/typescript_tools.py +122 -122
  75. gaia/agents/code/tools/validation_parsing.py +461 -461
  76. gaia/agents/code/tools/validation_tools.py +806 -806
  77. gaia/agents/code/tools/web_dev_tools.py +1758 -1758
  78. gaia/agents/code/validators/__init__.py +16 -16
  79. gaia/agents/code/validators/antipattern_checker.py +241 -241
  80. gaia/agents/code/validators/ast_analyzer.py +197 -197
  81. gaia/agents/code/validators/requirements_validator.py +145 -145
  82. gaia/agents/code/validators/syntax_validator.py +171 -171
  83. gaia/agents/docker/__init__.py +7 -7
  84. gaia/agents/docker/agent.py +643 -642
  85. gaia/agents/emr/__init__.py +8 -8
  86. gaia/agents/emr/agent.py +1504 -1506
  87. gaia/agents/emr/cli.py +1322 -1322
  88. gaia/agents/emr/constants.py +475 -475
  89. gaia/agents/emr/dashboard/__init__.py +4 -4
  90. gaia/agents/emr/dashboard/server.py +1972 -1974
  91. gaia/agents/jira/__init__.py +11 -11
  92. gaia/agents/jira/agent.py +894 -894
  93. gaia/agents/jira/jql_templates.py +299 -299
  94. gaia/agents/routing/__init__.py +7 -7
  95. gaia/agents/routing/agent.py +567 -570
  96. gaia/agents/routing/system_prompt.py +75 -75
  97. gaia/agents/summarize/__init__.py +11 -0
  98. gaia/agents/summarize/agent.py +885 -0
  99. gaia/agents/summarize/prompts.py +129 -0
  100. gaia/api/__init__.py +23 -23
  101. gaia/api/agent_registry.py +238 -238
  102. gaia/api/app.py +305 -305
  103. gaia/api/openai_server.py +575 -575
  104. gaia/api/schemas.py +186 -186
  105. gaia/api/sse_handler.py +373 -373
  106. gaia/apps/__init__.py +4 -4
  107. gaia/apps/llm/__init__.py +6 -6
  108. gaia/apps/llm/app.py +184 -169
  109. gaia/apps/summarize/app.py +116 -633
  110. gaia/apps/summarize/html_viewer.py +133 -133
  111. gaia/apps/summarize/pdf_formatter.py +284 -284
  112. gaia/audio/__init__.py +2 -2
  113. gaia/audio/audio_client.py +439 -439
  114. gaia/audio/audio_recorder.py +269 -269
  115. gaia/audio/kokoro_tts.py +599 -599
  116. gaia/audio/whisper_asr.py +432 -432
  117. gaia/chat/__init__.py +16 -16
  118. gaia/chat/app.py +428 -430
  119. gaia/chat/prompts.py +522 -522
  120. gaia/chat/sdk.py +1228 -1225
  121. gaia/cli.py +5659 -5632
  122. gaia/database/__init__.py +10 -10
  123. gaia/database/agent.py +176 -176
  124. gaia/database/mixin.py +290 -290
  125. gaia/database/testing.py +64 -64
  126. gaia/eval/batch_experiment.py +2332 -2332
  127. gaia/eval/claude.py +542 -542
  128. gaia/eval/config.py +37 -37
  129. gaia/eval/email_generator.py +512 -512
  130. gaia/eval/eval.py +3179 -3179
  131. gaia/eval/groundtruth.py +1130 -1130
  132. gaia/eval/transcript_generator.py +582 -582
  133. gaia/eval/webapp/README.md +167 -167
  134. gaia/eval/webapp/package-lock.json +875 -875
  135. gaia/eval/webapp/package.json +20 -20
  136. gaia/eval/webapp/public/app.js +3402 -3402
  137. gaia/eval/webapp/public/index.html +87 -87
  138. gaia/eval/webapp/public/styles.css +3661 -3661
  139. gaia/eval/webapp/server.js +415 -415
  140. gaia/eval/webapp/test-setup.js +72 -72
  141. gaia/installer/__init__.py +23 -0
  142. gaia/installer/init_command.py +1275 -0
  143. gaia/installer/lemonade_installer.py +619 -0
  144. gaia/llm/__init__.py +10 -2
  145. gaia/llm/base_client.py +60 -0
  146. gaia/llm/exceptions.py +12 -0
  147. gaia/llm/factory.py +70 -0
  148. gaia/llm/lemonade_client.py +3421 -3221
  149. gaia/llm/lemonade_manager.py +294 -294
  150. gaia/llm/providers/__init__.py +9 -0
  151. gaia/llm/providers/claude.py +108 -0
  152. gaia/llm/providers/lemonade.py +118 -0
  153. gaia/llm/providers/openai_provider.py +79 -0
  154. gaia/llm/vlm_client.py +382 -382
  155. gaia/logger.py +189 -189
  156. gaia/mcp/agent_mcp_server.py +245 -245
  157. gaia/mcp/blender_mcp_client.py +138 -138
  158. gaia/mcp/blender_mcp_server.py +648 -648
  159. gaia/mcp/context7_cache.py +332 -332
  160. gaia/mcp/external_services.py +518 -518
  161. gaia/mcp/mcp_bridge.py +811 -550
  162. gaia/mcp/servers/__init__.py +6 -6
  163. gaia/mcp/servers/docker_mcp.py +83 -83
  164. gaia/perf_analysis.py +361 -0
  165. gaia/rag/__init__.py +10 -10
  166. gaia/rag/app.py +293 -293
  167. gaia/rag/demo.py +304 -304
  168. gaia/rag/pdf_utils.py +235 -235
  169. gaia/rag/sdk.py +2194 -2194
  170. gaia/security.py +183 -163
  171. gaia/talk/app.py +287 -289
  172. gaia/talk/sdk.py +538 -538
  173. gaia/testing/__init__.py +87 -87
  174. gaia/testing/assertions.py +330 -330
  175. gaia/testing/fixtures.py +333 -333
  176. gaia/testing/mocks.py +493 -493
  177. gaia/util.py +46 -46
  178. gaia/utils/__init__.py +33 -33
  179. gaia/utils/file_watcher.py +675 -675
  180. gaia/utils/parsing.py +223 -223
  181. gaia/version.py +100 -100
  182. amd_gaia-0.15.0.dist-info/RECORD +0 -168
  183. gaia/agents/code/app.py +0 -266
  184. gaia/llm/llm_client.py +0 -723
  185. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.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()