meshwire 0.5.0a1__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.
- mcp_blender/__init__.py +3 -0
- mcp_blender/blender_client.py +248 -0
- mcp_blender/py.typed +0 -0
- mcp_blender/server.py +5766 -0
- mcp_blender/tools/__init__.py +21 -0
- mcp_blender/tools/animation.py +75 -0
- mcp_blender/tools/export.py +80 -0
- mcp_blender/tools/external.py +550 -0
- mcp_blender/tools/materials.py +96 -0
- mcp_blender/tools/modifiers.py +66 -0
- mcp_blender/tools/objects.py +136 -0
- mcp_blender/tools/render.py +74 -0
- mcp_blender/tools/scene.py +56 -0
- mcp_blender/types.py +106 -0
- meshwire-0.5.0a1.dist-info/METADATA +379 -0
- meshwire-0.5.0a1.dist-info/RECORD +19 -0
- meshwire-0.5.0a1.dist-info/WHEEL +4 -0
- meshwire-0.5.0a1.dist-info/entry_points.txt +3 -0
- meshwire-0.5.0a1.dist-info/licenses/LICENSE +661 -0
mcp_blender/__init__.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Async TCP client for communicating with the Meshwire addon."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("meshwire.client")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BlenderConnectionError(Exception):
|
|
13
|
+
"""Raised when connection to Blender fails."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BlenderCommandError(Exception):
|
|
19
|
+
"""Raised when a Blender command fails."""
|
|
20
|
+
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BlenderClient:
|
|
25
|
+
"""
|
|
26
|
+
Async TCP client for communicating with the Meshwire addon.
|
|
27
|
+
|
|
28
|
+
Uses JSON-RPC 2.0 protocol over TCP with newline-delimited messages.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, host: str = "127.0.0.1", port: int = 9876):
|
|
32
|
+
self.host = host
|
|
33
|
+
self.port = port
|
|
34
|
+
self._reader: asyncio.StreamReader | None = None
|
|
35
|
+
self._writer: asyncio.StreamWriter | None = None
|
|
36
|
+
self._lock = asyncio.Lock()
|
|
37
|
+
self._connected = False
|
|
38
|
+
self._pending_requests: dict[str, asyncio.Future] = {}
|
|
39
|
+
self._read_task: asyncio.Task | None = None
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def connected(self) -> bool:
|
|
43
|
+
"""Check if client is connected."""
|
|
44
|
+
return self._connected and self._writer is not None
|
|
45
|
+
|
|
46
|
+
async def connect(self, timeout: float = 5.0) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Connect to the Blender addon socket server.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
timeout: Connection timeout in seconds
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
BlenderConnectionError: If connection fails
|
|
55
|
+
"""
|
|
56
|
+
if self._connected:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
self._reader, self._writer = await asyncio.wait_for(
|
|
61
|
+
asyncio.open_connection(self.host, self.port),
|
|
62
|
+
timeout=timeout,
|
|
63
|
+
)
|
|
64
|
+
self._connected = True
|
|
65
|
+
logger.info(f"Connected to Blender at {self.host}:{self.port}")
|
|
66
|
+
|
|
67
|
+
# Start background reader task
|
|
68
|
+
self._read_task = asyncio.create_task(self._read_loop())
|
|
69
|
+
|
|
70
|
+
except asyncio.TimeoutError:
|
|
71
|
+
raise BlenderConnectionError(
|
|
72
|
+
f"Connection to Blender timed out ({self.host}:{self.port})"
|
|
73
|
+
)
|
|
74
|
+
except ConnectionRefusedError:
|
|
75
|
+
raise BlenderConnectionError(
|
|
76
|
+
f"Connection refused. Is Blender running with MCP addon enabled? ({self.host}:{self.port})"
|
|
77
|
+
)
|
|
78
|
+
except OSError as e:
|
|
79
|
+
raise BlenderConnectionError(f"Connection failed: {e}")
|
|
80
|
+
|
|
81
|
+
async def disconnect(self) -> None:
|
|
82
|
+
"""Disconnect from Blender."""
|
|
83
|
+
self._connected = False
|
|
84
|
+
|
|
85
|
+
if self._read_task:
|
|
86
|
+
self._read_task.cancel()
|
|
87
|
+
try:
|
|
88
|
+
await self._read_task
|
|
89
|
+
except asyncio.CancelledError:
|
|
90
|
+
pass
|
|
91
|
+
self._read_task = None
|
|
92
|
+
|
|
93
|
+
if self._writer:
|
|
94
|
+
self._writer.close()
|
|
95
|
+
try:
|
|
96
|
+
await self._writer.wait_closed()
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
self._writer = None
|
|
100
|
+
self._reader = None
|
|
101
|
+
|
|
102
|
+
# Cancel any pending requests
|
|
103
|
+
for future in self._pending_requests.values():
|
|
104
|
+
if not future.done():
|
|
105
|
+
future.set_exception(BlenderConnectionError("Disconnected"))
|
|
106
|
+
self._pending_requests.clear()
|
|
107
|
+
|
|
108
|
+
logger.info("Disconnected from Blender")
|
|
109
|
+
|
|
110
|
+
async def send_command(
|
|
111
|
+
self,
|
|
112
|
+
method: str,
|
|
113
|
+
params: dict[str, Any] | None = None,
|
|
114
|
+
timeout: float = 30.0,
|
|
115
|
+
) -> Any:
|
|
116
|
+
"""
|
|
117
|
+
Send a command to Blender and wait for the response.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
method: The command method name
|
|
121
|
+
params: Command parameters
|
|
122
|
+
timeout: Response timeout in seconds
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
The result from Blender
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
BlenderConnectionError: If not connected
|
|
129
|
+
BlenderCommandError: If the command fails
|
|
130
|
+
asyncio.TimeoutError: If response times out
|
|
131
|
+
"""
|
|
132
|
+
if not self._connected:
|
|
133
|
+
await self.connect()
|
|
134
|
+
|
|
135
|
+
if not self._writer:
|
|
136
|
+
raise BlenderConnectionError("Not connected to Blender")
|
|
137
|
+
|
|
138
|
+
# Generate unique request ID
|
|
139
|
+
request_id = str(uuid.uuid4())
|
|
140
|
+
|
|
141
|
+
# Build JSON-RPC request
|
|
142
|
+
request = {
|
|
143
|
+
"jsonrpc": "2.0",
|
|
144
|
+
"method": method,
|
|
145
|
+
"params": params or {},
|
|
146
|
+
"id": request_id,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# Create future for response
|
|
150
|
+
future: asyncio.Future = asyncio.get_running_loop().create_future()
|
|
151
|
+
self._pending_requests[request_id] = future
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
# Send request
|
|
155
|
+
async with self._lock:
|
|
156
|
+
message = json.dumps(request) + "\n"
|
|
157
|
+
self._writer.write(message.encode("utf-8"))
|
|
158
|
+
await self._writer.drain()
|
|
159
|
+
|
|
160
|
+
logger.debug(f"Sent command: {method}")
|
|
161
|
+
|
|
162
|
+
# Wait for response
|
|
163
|
+
result = await asyncio.wait_for(future, timeout=timeout)
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
except asyncio.TimeoutError:
|
|
167
|
+
self._pending_requests.pop(request_id, None)
|
|
168
|
+
raise asyncio.TimeoutError(f"Command '{method}' timed out after {timeout}s")
|
|
169
|
+
|
|
170
|
+
except Exception:
|
|
171
|
+
self._pending_requests.pop(request_id, None)
|
|
172
|
+
raise
|
|
173
|
+
|
|
174
|
+
async def _read_loop(self) -> None:
|
|
175
|
+
"""Background task to read responses from Blender."""
|
|
176
|
+
buffer = b""
|
|
177
|
+
|
|
178
|
+
while self._connected and self._reader:
|
|
179
|
+
try:
|
|
180
|
+
# Read data
|
|
181
|
+
data = await self._reader.read(65536)
|
|
182
|
+
if not data:
|
|
183
|
+
# Connection closed
|
|
184
|
+
logger.warning("Connection closed by Blender")
|
|
185
|
+
await self.disconnect()
|
|
186
|
+
break
|
|
187
|
+
|
|
188
|
+
buffer += data
|
|
189
|
+
|
|
190
|
+
# Process complete messages
|
|
191
|
+
while b"\n" in buffer:
|
|
192
|
+
line, buffer = buffer.split(b"\n", 1)
|
|
193
|
+
if line.strip():
|
|
194
|
+
await self._handle_response(line)
|
|
195
|
+
|
|
196
|
+
except asyncio.CancelledError:
|
|
197
|
+
break
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.error(f"Read error: {e}")
|
|
200
|
+
await self.disconnect()
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
async def _handle_response(self, data: bytes) -> None:
|
|
204
|
+
"""Handle a JSON-RPC response from Blender."""
|
|
205
|
+
try:
|
|
206
|
+
response = json.loads(data.decode("utf-8"))
|
|
207
|
+
except json.JSONDecodeError as e:
|
|
208
|
+
logger.error(f"Invalid JSON response: {e}")
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
request_id = response.get("id")
|
|
212
|
+
if request_id is None:
|
|
213
|
+
logger.warning("Received response without ID")
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
future = self._pending_requests.pop(request_id, None)
|
|
217
|
+
if future is None:
|
|
218
|
+
logger.warning(f"Received response for unknown request: {request_id}")
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
if "error" in response:
|
|
222
|
+
error = response["error"]
|
|
223
|
+
error_msg = error.get("message", str(error))
|
|
224
|
+
future.set_exception(BlenderCommandError(error_msg))
|
|
225
|
+
else:
|
|
226
|
+
future.set_result(response.get("result"))
|
|
227
|
+
|
|
228
|
+
async def ping(self, timeout: float = 5.0) -> bool:
|
|
229
|
+
"""
|
|
230
|
+
Test connectivity to Blender.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
True if connected and responsive
|
|
234
|
+
"""
|
|
235
|
+
try:
|
|
236
|
+
result = await self.send_command("ping", timeout=timeout)
|
|
237
|
+
return result.get("pong", False)
|
|
238
|
+
except Exception:
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
async def __aenter__(self) -> "BlenderClient":
|
|
242
|
+
"""Async context manager entry."""
|
|
243
|
+
await self.connect()
|
|
244
|
+
return self
|
|
245
|
+
|
|
246
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
247
|
+
"""Async context manager exit."""
|
|
248
|
+
await self.disconnect()
|
mcp_blender/py.typed
ADDED
|
File without changes
|