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.
- {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/METADATA +222 -223
- amd_gaia-0.15.2.dist-info/RECORD +182 -0
- {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/WHEEL +1 -1
- {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/entry_points.txt +1 -0
- {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/licenses/LICENSE.md +20 -20
- gaia/__init__.py +29 -29
- gaia/agents/__init__.py +19 -19
- gaia/agents/base/__init__.py +9 -9
- gaia/agents/base/agent.py +2132 -2177
- gaia/agents/base/api_agent.py +119 -120
- gaia/agents/base/console.py +1967 -1841
- gaia/agents/base/errors.py +237 -237
- gaia/agents/base/mcp_agent.py +86 -86
- gaia/agents/base/tools.py +88 -83
- gaia/agents/blender/__init__.py +7 -0
- gaia/agents/blender/agent.py +553 -556
- gaia/agents/blender/agent_simple.py +133 -135
- gaia/agents/blender/app.py +211 -211
- gaia/agents/blender/app_simple.py +41 -41
- gaia/agents/blender/core/__init__.py +16 -16
- gaia/agents/blender/core/materials.py +506 -506
- gaia/agents/blender/core/objects.py +316 -316
- gaia/agents/blender/core/rendering.py +225 -225
- gaia/agents/blender/core/scene.py +220 -220
- gaia/agents/blender/core/view.py +146 -146
- gaia/agents/chat/__init__.py +9 -9
- gaia/agents/chat/agent.py +809 -835
- gaia/agents/chat/app.py +1065 -1058
- gaia/agents/chat/session.py +508 -508
- gaia/agents/chat/tools/__init__.py +15 -15
- gaia/agents/chat/tools/file_tools.py +96 -96
- gaia/agents/chat/tools/rag_tools.py +1744 -1729
- gaia/agents/chat/tools/shell_tools.py +437 -436
- gaia/agents/code/__init__.py +7 -7
- gaia/agents/code/agent.py +549 -549
- gaia/agents/code/cli.py +377 -0
- gaia/agents/code/models.py +135 -135
- gaia/agents/code/orchestration/__init__.py +24 -24
- gaia/agents/code/orchestration/checklist_executor.py +1763 -1763
- gaia/agents/code/orchestration/checklist_generator.py +713 -713
- gaia/agents/code/orchestration/factories/__init__.py +9 -9
- gaia/agents/code/orchestration/factories/base.py +63 -63
- gaia/agents/code/orchestration/factories/nextjs_factory.py +118 -118
- gaia/agents/code/orchestration/factories/python_factory.py +106 -106
- gaia/agents/code/orchestration/orchestrator.py +841 -841
- gaia/agents/code/orchestration/project_analyzer.py +391 -391
- gaia/agents/code/orchestration/steps/__init__.py +67 -67
- gaia/agents/code/orchestration/steps/base.py +188 -188
- gaia/agents/code/orchestration/steps/error_handler.py +314 -314
- gaia/agents/code/orchestration/steps/nextjs.py +828 -828
- gaia/agents/code/orchestration/steps/python.py +307 -307
- gaia/agents/code/orchestration/template_catalog.py +469 -469
- gaia/agents/code/orchestration/workflows/__init__.py +14 -14
- gaia/agents/code/orchestration/workflows/base.py +80 -80
- gaia/agents/code/orchestration/workflows/nextjs.py +186 -186
- gaia/agents/code/orchestration/workflows/python.py +94 -94
- gaia/agents/code/prompts/__init__.py +11 -11
- gaia/agents/code/prompts/base_prompt.py +77 -77
- gaia/agents/code/prompts/code_patterns.py +2034 -2036
- gaia/agents/code/prompts/nextjs_prompt.py +40 -40
- gaia/agents/code/prompts/python_prompt.py +109 -109
- gaia/agents/code/schema_inference.py +365 -365
- gaia/agents/code/system_prompt.py +41 -41
- gaia/agents/code/tools/__init__.py +42 -42
- gaia/agents/code/tools/cli_tools.py +1138 -1138
- gaia/agents/code/tools/code_formatting.py +319 -319
- gaia/agents/code/tools/code_tools.py +769 -769
- gaia/agents/code/tools/error_fixing.py +1347 -1347
- gaia/agents/code/tools/external_tools.py +180 -180
- gaia/agents/code/tools/file_io.py +845 -845
- gaia/agents/code/tools/prisma_tools.py +190 -190
- gaia/agents/code/tools/project_management.py +1016 -1016
- gaia/agents/code/tools/testing.py +321 -321
- gaia/agents/code/tools/typescript_tools.py +122 -122
- gaia/agents/code/tools/validation_parsing.py +461 -461
- gaia/agents/code/tools/validation_tools.py +806 -806
- gaia/agents/code/tools/web_dev_tools.py +1758 -1758
- gaia/agents/code/validators/__init__.py +16 -16
- gaia/agents/code/validators/antipattern_checker.py +241 -241
- gaia/agents/code/validators/ast_analyzer.py +197 -197
- gaia/agents/code/validators/requirements_validator.py +145 -145
- gaia/agents/code/validators/syntax_validator.py +171 -171
- gaia/agents/docker/__init__.py +7 -7
- gaia/agents/docker/agent.py +643 -642
- gaia/agents/emr/__init__.py +8 -8
- gaia/agents/emr/agent.py +1504 -1506
- gaia/agents/emr/cli.py +1322 -1322
- gaia/agents/emr/constants.py +475 -475
- gaia/agents/emr/dashboard/__init__.py +4 -4
- gaia/agents/emr/dashboard/server.py +1972 -1974
- gaia/agents/jira/__init__.py +11 -11
- gaia/agents/jira/agent.py +894 -894
- gaia/agents/jira/jql_templates.py +299 -299
- gaia/agents/routing/__init__.py +7 -7
- gaia/agents/routing/agent.py +567 -570
- gaia/agents/routing/system_prompt.py +75 -75
- gaia/agents/summarize/__init__.py +11 -0
- gaia/agents/summarize/agent.py +885 -0
- gaia/agents/summarize/prompts.py +129 -0
- gaia/api/__init__.py +23 -23
- gaia/api/agent_registry.py +238 -238
- gaia/api/app.py +305 -305
- gaia/api/openai_server.py +575 -575
- gaia/api/schemas.py +186 -186
- gaia/api/sse_handler.py +373 -373
- gaia/apps/__init__.py +4 -4
- gaia/apps/llm/__init__.py +6 -6
- gaia/apps/llm/app.py +184 -169
- gaia/apps/summarize/app.py +116 -633
- gaia/apps/summarize/html_viewer.py +133 -133
- gaia/apps/summarize/pdf_formatter.py +284 -284
- gaia/audio/__init__.py +2 -2
- gaia/audio/audio_client.py +439 -439
- gaia/audio/audio_recorder.py +269 -269
- gaia/audio/kokoro_tts.py +599 -599
- gaia/audio/whisper_asr.py +432 -432
- gaia/chat/__init__.py +16 -16
- gaia/chat/app.py +428 -430
- gaia/chat/prompts.py +522 -522
- gaia/chat/sdk.py +1228 -1225
- gaia/cli.py +5659 -5632
- gaia/database/__init__.py +10 -10
- gaia/database/agent.py +176 -176
- gaia/database/mixin.py +290 -290
- gaia/database/testing.py +64 -64
- gaia/eval/batch_experiment.py +2332 -2332
- gaia/eval/claude.py +542 -542
- gaia/eval/config.py +37 -37
- gaia/eval/email_generator.py +512 -512
- gaia/eval/eval.py +3179 -3179
- gaia/eval/groundtruth.py +1130 -1130
- gaia/eval/transcript_generator.py +582 -582
- gaia/eval/webapp/README.md +167 -167
- gaia/eval/webapp/package-lock.json +875 -875
- gaia/eval/webapp/package.json +20 -20
- gaia/eval/webapp/public/app.js +3402 -3402
- gaia/eval/webapp/public/index.html +87 -87
- gaia/eval/webapp/public/styles.css +3661 -3661
- gaia/eval/webapp/server.js +415 -415
- gaia/eval/webapp/test-setup.js +72 -72
- gaia/installer/__init__.py +23 -0
- gaia/installer/init_command.py +1275 -0
- gaia/installer/lemonade_installer.py +619 -0
- gaia/llm/__init__.py +10 -2
- gaia/llm/base_client.py +60 -0
- gaia/llm/exceptions.py +12 -0
- gaia/llm/factory.py +70 -0
- gaia/llm/lemonade_client.py +3421 -3221
- gaia/llm/lemonade_manager.py +294 -294
- gaia/llm/providers/__init__.py +9 -0
- gaia/llm/providers/claude.py +108 -0
- gaia/llm/providers/lemonade.py +118 -0
- gaia/llm/providers/openai_provider.py +79 -0
- gaia/llm/vlm_client.py +382 -382
- gaia/logger.py +189 -189
- gaia/mcp/agent_mcp_server.py +245 -245
- gaia/mcp/blender_mcp_client.py +138 -138
- gaia/mcp/blender_mcp_server.py +648 -648
- gaia/mcp/context7_cache.py +332 -332
- gaia/mcp/external_services.py +518 -518
- gaia/mcp/mcp_bridge.py +811 -550
- gaia/mcp/servers/__init__.py +6 -6
- gaia/mcp/servers/docker_mcp.py +83 -83
- gaia/perf_analysis.py +361 -0
- gaia/rag/__init__.py +10 -10
- gaia/rag/app.py +293 -293
- gaia/rag/demo.py +304 -304
- gaia/rag/pdf_utils.py +235 -235
- gaia/rag/sdk.py +2194 -2194
- gaia/security.py +183 -163
- gaia/talk/app.py +287 -289
- gaia/talk/sdk.py +538 -538
- gaia/testing/__init__.py +87 -87
- gaia/testing/assertions.py +330 -330
- gaia/testing/fixtures.py +333 -333
- gaia/testing/mocks.py +493 -493
- gaia/util.py +46 -46
- gaia/utils/__init__.py +33 -33
- gaia/utils/file_watcher.py +675 -675
- gaia/utils/parsing.py +223 -223
- gaia/version.py +100 -100
- amd_gaia-0.15.0.dist-info/RECORD +0 -168
- gaia/agents/code/app.py +0 -266
- gaia/llm/llm_client.py +0 -723
- {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/top_level.txt +0 -0
gaia/mcp/blender_mcp_server.py
CHANGED
|
@@ -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()
|