rhinomcp 0.1.0__tar.gz
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.
- rhinomcp-0.1.0/PKG-INFO +14 -0
- rhinomcp-0.1.0/pyproject.toml +32 -0
- rhinomcp-0.1.0/setup.cfg +4 -0
- rhinomcp-0.1.0/src/__init__.py +6 -0
- rhinomcp-0.1.0/src/rhinomcp.egg-info/PKG-INFO +14 -0
- rhinomcp-0.1.0/src/rhinomcp.egg-info/SOURCES.txt +9 -0
- rhinomcp-0.1.0/src/rhinomcp.egg-info/dependency_links.txt +1 -0
- rhinomcp-0.1.0/src/rhinomcp.egg-info/entry_points.txt +2 -0
- rhinomcp-0.1.0/src/rhinomcp.egg-info/requires.txt +1 -0
- rhinomcp-0.1.0/src/rhinomcp.egg-info/top_level.txt +2 -0
- rhinomcp-0.1.0/src/server.py +435 -0
rhinomcp-0.1.0/PKG-INFO
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: rhinomcp
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: Rhino integration through the Model Context Protocol
|
5
|
+
Author-email: Jingcheng Chen <mail@jchen.ch>
|
6
|
+
License: MIT
|
7
|
+
Project-URL: Homepage, https://github.com/jingcheng-chen/rhinomcp
|
8
|
+
Project-URL: Bug Tracker, https://github.com/jingcheng-chen/rhinomcp/issues
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
11
|
+
Classifier: Operating System :: OS Independent
|
12
|
+
Requires-Python: >=3.10
|
13
|
+
Description-Content-Type: text/markdown
|
14
|
+
Requires-Dist: mcp[cli]>=1.3.0
|
@@ -0,0 +1,32 @@
|
|
1
|
+
[project]
|
2
|
+
name = "rhinomcp"
|
3
|
+
version = "0.1.0"
|
4
|
+
description = "Rhino integration through the Model Context Protocol"
|
5
|
+
readme = "README.md"
|
6
|
+
requires-python = ">=3.10"
|
7
|
+
authors = [
|
8
|
+
{name = "Jingcheng Chen", email = "mail@jchen.ch"}
|
9
|
+
]
|
10
|
+
license = {text = "MIT"}
|
11
|
+
classifiers = [
|
12
|
+
"Programming Language :: Python :: 3",
|
13
|
+
"License :: OSI Approved :: MIT License",
|
14
|
+
"Operating System :: OS Independent",
|
15
|
+
]
|
16
|
+
dependencies = [
|
17
|
+
"mcp[cli]>=1.3.0",
|
18
|
+
]
|
19
|
+
|
20
|
+
[project.scripts]
|
21
|
+
rhinomcp = "src.server:main"
|
22
|
+
|
23
|
+
[build-system]
|
24
|
+
requires = ["setuptools>=61.0", "wheel"]
|
25
|
+
build-backend = "setuptools.build_meta"
|
26
|
+
|
27
|
+
[tool.setuptools]
|
28
|
+
package-dir = {"" = "src"}
|
29
|
+
|
30
|
+
[project.urls]
|
31
|
+
"Homepage" = "https://github.com/jingcheng-chen/rhinomcp"
|
32
|
+
"Bug Tracker" = "https://github.com/jingcheng-chen/rhinomcp/issues"
|
rhinomcp-0.1.0/setup.cfg
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: rhinomcp
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: Rhino integration through the Model Context Protocol
|
5
|
+
Author-email: Jingcheng Chen <mail@jchen.ch>
|
6
|
+
License: MIT
|
7
|
+
Project-URL: Homepage, https://github.com/jingcheng-chen/rhinomcp
|
8
|
+
Project-URL: Bug Tracker, https://github.com/jingcheng-chen/rhinomcp/issues
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
11
|
+
Classifier: Operating System :: OS Independent
|
12
|
+
Requires-Python: >=3.10
|
13
|
+
Description-Content-Type: text/markdown
|
14
|
+
Requires-Dist: mcp[cli]>=1.3.0
|
@@ -0,0 +1,9 @@
|
|
1
|
+
pyproject.toml
|
2
|
+
src/__init__.py
|
3
|
+
src/server.py
|
4
|
+
src/rhinomcp.egg-info/PKG-INFO
|
5
|
+
src/rhinomcp.egg-info/SOURCES.txt
|
6
|
+
src/rhinomcp.egg-info/dependency_links.txt
|
7
|
+
src/rhinomcp.egg-info/entry_points.txt
|
8
|
+
src/rhinomcp.egg-info/requires.txt
|
9
|
+
src/rhinomcp.egg-info/top_level.txt
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
mcp[cli]>=1.3.0
|
@@ -0,0 +1,435 @@
|
|
1
|
+
# rhino_mcp_server.py
|
2
|
+
from mcp.server.fastmcp import FastMCP, Context, Image
|
3
|
+
import socket
|
4
|
+
import json
|
5
|
+
import asyncio
|
6
|
+
import logging
|
7
|
+
from dataclasses import dataclass
|
8
|
+
from contextlib import asynccontextmanager
|
9
|
+
from typing import AsyncIterator, Dict, Any, List
|
10
|
+
import os
|
11
|
+
from pathlib import Path
|
12
|
+
import base64
|
13
|
+
from urllib.parse import urlparse
|
14
|
+
|
15
|
+
# Configure logging
|
16
|
+
logging.basicConfig(level=logging.INFO,
|
17
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
18
|
+
logger = logging.getLogger("RhinoMCPServer")
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class RhinoConnection:
|
22
|
+
host: str
|
23
|
+
port: int
|
24
|
+
sock: socket.socket = None # Changed from 'socket' to 'sock' to avoid naming conflict
|
25
|
+
|
26
|
+
def connect(self) -> bool:
|
27
|
+
"""Connect to the Rhino addon socket server"""
|
28
|
+
if self.sock:
|
29
|
+
return True
|
30
|
+
|
31
|
+
try:
|
32
|
+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
33
|
+
self.sock.connect((self.host, self.port))
|
34
|
+
logger.info(f"Connected to Rhino at {self.host}:{self.port}")
|
35
|
+
return True
|
36
|
+
except Exception as e:
|
37
|
+
logger.error(f"Failed to connect to Rhino: {str(e)}")
|
38
|
+
self.sock = None
|
39
|
+
return False
|
40
|
+
|
41
|
+
def disconnect(self):
|
42
|
+
"""Disconnect from the Rhino addon"""
|
43
|
+
if self.sock:
|
44
|
+
try:
|
45
|
+
self.sock.close()
|
46
|
+
except Exception as e:
|
47
|
+
logger.error(f"Error disconnecting from Rhino: {str(e)}")
|
48
|
+
finally:
|
49
|
+
self.sock = None
|
50
|
+
|
51
|
+
def receive_full_response(self, sock, buffer_size=8192):
|
52
|
+
"""Receive the complete response, potentially in multiple chunks"""
|
53
|
+
chunks = []
|
54
|
+
# Use a consistent timeout value that matches the addon's timeout
|
55
|
+
sock.settimeout(15.0) # Match the addon's timeout
|
56
|
+
|
57
|
+
try:
|
58
|
+
while True:
|
59
|
+
try:
|
60
|
+
chunk = sock.recv(buffer_size)
|
61
|
+
if not chunk:
|
62
|
+
# If we get an empty chunk, the connection might be closed
|
63
|
+
if not chunks: # If we haven't received anything yet, this is an error
|
64
|
+
raise Exception("Connection closed before receiving any data")
|
65
|
+
break
|
66
|
+
|
67
|
+
chunks.append(chunk)
|
68
|
+
|
69
|
+
# Check if we've received a complete JSON object
|
70
|
+
try:
|
71
|
+
data = b''.join(chunks)
|
72
|
+
json.loads(data.decode('utf-8'))
|
73
|
+
# If we get here, it parsed successfully
|
74
|
+
logger.info(f"Received complete response ({len(data)} bytes)")
|
75
|
+
return data
|
76
|
+
except json.JSONDecodeError:
|
77
|
+
# Incomplete JSON, continue receiving
|
78
|
+
continue
|
79
|
+
except socket.timeout:
|
80
|
+
# If we hit a timeout during receiving, break the loop and try to use what we have
|
81
|
+
logger.warning("Socket timeout during chunked receive")
|
82
|
+
break
|
83
|
+
except (ConnectionError, BrokenPipeError, ConnectionResetError) as e:
|
84
|
+
logger.error(f"Socket connection error during receive: {str(e)}")
|
85
|
+
raise # Re-raise to be handled by the caller
|
86
|
+
except socket.timeout:
|
87
|
+
logger.warning("Socket timeout during chunked receive")
|
88
|
+
except Exception as e:
|
89
|
+
logger.error(f"Error during receive: {str(e)}")
|
90
|
+
raise
|
91
|
+
|
92
|
+
# If we get here, we either timed out or broke out of the loop
|
93
|
+
# Try to use what we have
|
94
|
+
if chunks:
|
95
|
+
data = b''.join(chunks)
|
96
|
+
logger.info(f"Returning data after receive completion ({len(data)} bytes)")
|
97
|
+
try:
|
98
|
+
# Try to parse what we have
|
99
|
+
json.loads(data.decode('utf-8'))
|
100
|
+
return data
|
101
|
+
except json.JSONDecodeError:
|
102
|
+
# If we can't parse it, it's incomplete
|
103
|
+
raise Exception("Incomplete JSON response received")
|
104
|
+
else:
|
105
|
+
raise Exception("No data received")
|
106
|
+
|
107
|
+
def send_command(self, command_type: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
|
108
|
+
"""Send a command to Rhino and return the response"""
|
109
|
+
if not self.sock and not self.connect():
|
110
|
+
raise ConnectionError("Not connected to Rhino")
|
111
|
+
|
112
|
+
command = {
|
113
|
+
"type": command_type,
|
114
|
+
"params": params or {}
|
115
|
+
}
|
116
|
+
|
117
|
+
try:
|
118
|
+
# Log the command being sent
|
119
|
+
logger.info(f"Sending command: {command_type} with params: {params}")
|
120
|
+
|
121
|
+
# Send the command
|
122
|
+
self.sock.sendall(json.dumps(command).encode('utf-8'))
|
123
|
+
logger.info(f"Command sent, waiting for response...")
|
124
|
+
|
125
|
+
# Set a timeout for receiving - use the same timeout as in receive_full_response
|
126
|
+
self.sock.settimeout(15.0) # Match the addon's timeout
|
127
|
+
|
128
|
+
# Receive the response using the improved receive_full_response method
|
129
|
+
response_data = self.receive_full_response(self.sock)
|
130
|
+
logger.info(f"Received {len(response_data)} bytes of data")
|
131
|
+
|
132
|
+
response = json.loads(response_data.decode('utf-8'))
|
133
|
+
logger.info(f"Response parsed, status: {response.get('status', 'unknown')}")
|
134
|
+
|
135
|
+
if response.get("status") == "error":
|
136
|
+
logger.error(f"Rhino error: {response.get('message')}")
|
137
|
+
raise Exception(response.get("message", "Unknown error from Rhino"))
|
138
|
+
|
139
|
+
return response.get("result", {})
|
140
|
+
except socket.timeout:
|
141
|
+
logger.error("Socket timeout while waiting for response from Rhino")
|
142
|
+
# Don't try to reconnect here - let the get_rhino_connection handle reconnection
|
143
|
+
# Just invalidate the current socket so it will be recreated next time
|
144
|
+
self.sock = None
|
145
|
+
raise Exception("Timeout waiting for Rhino response - try simplifying your request")
|
146
|
+
except (ConnectionError, BrokenPipeError, ConnectionResetError) as e:
|
147
|
+
logger.error(f"Socket connection error: {str(e)}")
|
148
|
+
self.sock = None
|
149
|
+
raise Exception(f"Connection to Rhino lost: {str(e)}")
|
150
|
+
except json.JSONDecodeError as e:
|
151
|
+
logger.error(f"Invalid JSON response from Rhino: {str(e)}")
|
152
|
+
# Try to log what was received
|
153
|
+
if 'response_data' in locals() and response_data:
|
154
|
+
logger.error(f"Raw response (first 200 bytes): {response_data[:200]}")
|
155
|
+
raise Exception(f"Invalid response from Rhino: {str(e)}")
|
156
|
+
except Exception as e:
|
157
|
+
logger.error(f"Error communicating with Rhino: {str(e)}")
|
158
|
+
# Don't try to reconnect here - let the get_rhino_connection handle reconnection
|
159
|
+
self.sock = None
|
160
|
+
raise Exception(f"Communication error with Rhino: {str(e)}")
|
161
|
+
|
162
|
+
@asynccontextmanager
|
163
|
+
async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
|
164
|
+
"""Manage server startup and shutdown lifecycle"""
|
165
|
+
# We don't need to create a connection here since we're using the global connection
|
166
|
+
# for resources and tools
|
167
|
+
|
168
|
+
try:
|
169
|
+
# Just log that we're starting up
|
170
|
+
logger.info("RhinoMCP server starting up")
|
171
|
+
|
172
|
+
# Try to connect to Rhino on startup to verify it's available
|
173
|
+
try:
|
174
|
+
# This will initialize the global connection if needed
|
175
|
+
rhino = get_rhino_connection()
|
176
|
+
logger.info("Successfully connected to Rhino on startup")
|
177
|
+
except Exception as e:
|
178
|
+
logger.warning(f"Could not connect to Rhino on startup: {str(e)}")
|
179
|
+
logger.warning("Make sure the Rhino addon is running before using Rhino resources or tools")
|
180
|
+
|
181
|
+
# Return an empty context - we're using the global connection
|
182
|
+
yield {}
|
183
|
+
finally:
|
184
|
+
# Clean up the global connection on shutdown
|
185
|
+
global _rhino_connection
|
186
|
+
if _rhino_connection:
|
187
|
+
logger.info("Disconnecting from Rhino on shutdown")
|
188
|
+
_rhino_connection.disconnect()
|
189
|
+
_rhino_connection = None
|
190
|
+
logger.info("RhinoMCP server shut down")
|
191
|
+
|
192
|
+
# Create the MCP server with lifespan support
|
193
|
+
mcp = FastMCP(
|
194
|
+
"RhinoMCP",
|
195
|
+
description="Rhino integration through the Model Context Protocol",
|
196
|
+
lifespan=server_lifespan
|
197
|
+
)
|
198
|
+
|
199
|
+
# Resource endpoints
|
200
|
+
|
201
|
+
# Global connection for resources (since resources can't access context)
|
202
|
+
_rhino_connection = None
|
203
|
+
_polyhaven_enabled = False # Add this global variable
|
204
|
+
|
205
|
+
def get_rhino_connection():
|
206
|
+
"""Get or create a persistent Rhino connection"""
|
207
|
+
global _rhino_connection, _polyhaven_enabled # Add _polyhaven_enabled to globals
|
208
|
+
|
209
|
+
# If we have an existing connection, check if it's still valid
|
210
|
+
if _rhino_connection is not None:
|
211
|
+
try:
|
212
|
+
# First check if PolyHaven is enabled by sending a ping command
|
213
|
+
result = _rhino_connection.send_command("get_polyhaven_status")
|
214
|
+
# Store the PolyHaven status globally
|
215
|
+
_polyhaven_enabled = result.get("enabled", False)
|
216
|
+
return _rhino_connection
|
217
|
+
except Exception as e:
|
218
|
+
# Connection is dead, close it and create a new one
|
219
|
+
logger.warning(f"Existing connection is no longer valid: {str(e)}")
|
220
|
+
try:
|
221
|
+
_rhino_connection.disconnect()
|
222
|
+
except:
|
223
|
+
pass
|
224
|
+
_rhino_connection = None
|
225
|
+
|
226
|
+
# Create a new connection if needed
|
227
|
+
if _rhino_connection is None:
|
228
|
+
_rhino_connection = RhinoConnection(host="127.0.0.1", port=1999)
|
229
|
+
if not _rhino_connection.connect():
|
230
|
+
logger.error("Failed to connect to Rhino")
|
231
|
+
_rhino_connection = None
|
232
|
+
raise Exception("Could not connect to Rhino. Make sure the Rhino addon is running.")
|
233
|
+
logger.info("Created new persistent connection to Rhino")
|
234
|
+
|
235
|
+
return _rhino_connection
|
236
|
+
|
237
|
+
|
238
|
+
@mcp.tool()
|
239
|
+
def get_scene_info(ctx: Context) -> str:
|
240
|
+
"""Get detailed information about the current Rhino scene"""
|
241
|
+
try:
|
242
|
+
rhino = get_rhino_connection()
|
243
|
+
result = rhino.send_command("get_scene_info")
|
244
|
+
|
245
|
+
# Just return the JSON representation of what Rhino sent us
|
246
|
+
return json.dumps(result, indent=2)
|
247
|
+
except Exception as e:
|
248
|
+
logger.error(f"Error getting scene info from Rhino: {str(e)}")
|
249
|
+
return f"Error getting scene info: {str(e)}"
|
250
|
+
|
251
|
+
@mcp.tool()
|
252
|
+
def get_object_info(ctx: Context, object_name: str) -> str:
|
253
|
+
"""
|
254
|
+
Get detailed information about a specific object in the Rhino scene.
|
255
|
+
|
256
|
+
Parameters:
|
257
|
+
- object_name: The name of the object to get information about
|
258
|
+
"""
|
259
|
+
try:
|
260
|
+
rhino = get_rhino_connection()
|
261
|
+
result = rhino.send_command("get_object_info", {"name": object_name})
|
262
|
+
|
263
|
+
# Just return the JSON representation of what Rhino sent us
|
264
|
+
return json.dumps(result, indent=2)
|
265
|
+
except Exception as e:
|
266
|
+
logger.error(f"Error getting object info from Rhino: {str(e)}")
|
267
|
+
return f"Error getting object info: {str(e)}"
|
268
|
+
|
269
|
+
|
270
|
+
|
271
|
+
@mcp.tool()
|
272
|
+
def create_object(
|
273
|
+
ctx: Context,
|
274
|
+
type: str = "CUBE",
|
275
|
+
name: str = None,
|
276
|
+
location: List[float] = None,
|
277
|
+
rotation: List[float] = None,
|
278
|
+
scale: List[float] = None,
|
279
|
+
# Torus-specific parameters
|
280
|
+
align: str = "WORLD",
|
281
|
+
major_segments: int = 48,
|
282
|
+
minor_segments: int = 12,
|
283
|
+
mode: str = "MAJOR_MINOR",
|
284
|
+
major_radius: float = 1.0,
|
285
|
+
minor_radius: float = 0.25,
|
286
|
+
abso_major_rad: float = 1.25,
|
287
|
+
abso_minor_rad: float = 0.75,
|
288
|
+
generate_uvs: bool = True
|
289
|
+
) -> str:
|
290
|
+
"""
|
291
|
+
Create a new object in the Rhino scene.
|
292
|
+
|
293
|
+
Parameters:
|
294
|
+
- type: Object type (CUBE, SPHERE, CYLINDER, PLANE, CONE, TORUS, EMPTY, CAMERA, LIGHT)
|
295
|
+
- name: Optional name for the object
|
296
|
+
- location: Optional [x, y, z] location coordinates
|
297
|
+
- rotation: Optional [x, y, z] rotation in radians
|
298
|
+
- scale: Optional [x, y, z] scale factors (not used for TORUS)
|
299
|
+
|
300
|
+
Torus-specific parameters (only used when type == "TORUS"):
|
301
|
+
- align: How to align the torus ('WORLD', 'VIEW', or 'CURSOR')
|
302
|
+
- major_segments: Number of segments for the main ring
|
303
|
+
- minor_segments: Number of segments for the cross-section
|
304
|
+
- mode: Dimension mode ('MAJOR_MINOR' or 'EXT_INT')
|
305
|
+
- major_radius: Radius from the origin to the center of the cross sections
|
306
|
+
- minor_radius: Radius of the torus' cross section
|
307
|
+
- abso_major_rad: Total exterior radius of the torus
|
308
|
+
- abso_minor_rad: Total interior radius of the torus
|
309
|
+
- generate_uvs: Whether to generate a default UV map
|
310
|
+
|
311
|
+
Returns:
|
312
|
+
A message indicating the created object name.
|
313
|
+
"""
|
314
|
+
try:
|
315
|
+
# Get the global connection
|
316
|
+
rhino = get_rhino_connection()
|
317
|
+
|
318
|
+
# Set default values for missing parameters
|
319
|
+
loc = location or [0, 0, 0]
|
320
|
+
rot = rotation or [0, 0, 0]
|
321
|
+
sc = scale or [1, 1, 1]
|
322
|
+
|
323
|
+
params = {
|
324
|
+
"type": type,
|
325
|
+
"location": loc,
|
326
|
+
"rotation": rot,
|
327
|
+
}
|
328
|
+
|
329
|
+
if name:
|
330
|
+
params["name"] = name
|
331
|
+
|
332
|
+
if type == "TORUS":
|
333
|
+
# For torus, the scale is not used.
|
334
|
+
params.update({
|
335
|
+
"align": align,
|
336
|
+
"major_segments": major_segments,
|
337
|
+
"minor_segments": minor_segments,
|
338
|
+
"mode": mode,
|
339
|
+
"major_radius": major_radius,
|
340
|
+
"minor_radius": minor_radius,
|
341
|
+
"abso_major_rad": abso_major_rad,
|
342
|
+
"abso_minor_rad": abso_minor_rad,
|
343
|
+
"generate_uvs": generate_uvs
|
344
|
+
})
|
345
|
+
result = rhino.send_command("create_object", params)
|
346
|
+
return f"Created {type} object: {result['name']}"
|
347
|
+
else:
|
348
|
+
# For non-torus objects, include scale
|
349
|
+
params["scale"] = sc
|
350
|
+
result = rhino.send_command("create_object", params)
|
351
|
+
return f"Created {type} object: {result['name']}"
|
352
|
+
except Exception as e:
|
353
|
+
logger.error(f"Error creating object: {str(e)}")
|
354
|
+
return f"Error creating object: {str(e)}"
|
355
|
+
|
356
|
+
@mcp.tool()
|
357
|
+
def modify_object(
|
358
|
+
ctx: Context,
|
359
|
+
name: str,
|
360
|
+
location: List[float] = None,
|
361
|
+
rotation: List[float] = None,
|
362
|
+
scale: List[float] = None,
|
363
|
+
visible: bool = None
|
364
|
+
) -> str:
|
365
|
+
"""
|
366
|
+
Modify an existing object in the Rhino scene.
|
367
|
+
|
368
|
+
Parameters:
|
369
|
+
- name: Name of the object to modify
|
370
|
+
- location: Optional [x, y, z] location coordinates
|
371
|
+
- rotation: Optional [x, y, z] rotation in radians
|
372
|
+
- scale: Optional [x, y, z] scale factors
|
373
|
+
- visible: Optional boolean to set visibility
|
374
|
+
"""
|
375
|
+
try:
|
376
|
+
# Get the global connection
|
377
|
+
rhino = get_rhino_connection()
|
378
|
+
|
379
|
+
params = {"name": name}
|
380
|
+
|
381
|
+
if location is not None:
|
382
|
+
params["location"] = location
|
383
|
+
if rotation is not None:
|
384
|
+
params["rotation"] = rotation
|
385
|
+
if scale is not None:
|
386
|
+
params["scale"] = scale
|
387
|
+
if visible is not None:
|
388
|
+
params["visible"] = visible
|
389
|
+
|
390
|
+
result = rhino.send_command("modify_object", params)
|
391
|
+
return f"Modified object: {result['name']}"
|
392
|
+
except Exception as e:
|
393
|
+
logger.error(f"Error modifying object: {str(e)}")
|
394
|
+
return f"Error modifying object: {str(e)}"
|
395
|
+
|
396
|
+
@mcp.tool()
|
397
|
+
def delete_object(ctx: Context, name: str) -> str:
|
398
|
+
"""
|
399
|
+
Delete an object from the Rhino scene.
|
400
|
+
|
401
|
+
Parameters:
|
402
|
+
- name: Name of the object to delete
|
403
|
+
"""
|
404
|
+
try:
|
405
|
+
# Get the global connection
|
406
|
+
rhino = get_rhino_connection()
|
407
|
+
|
408
|
+
result = rhino.send_command("delete_object", {"name": name})
|
409
|
+
return f"Deleted object: {name}"
|
410
|
+
except Exception as e:
|
411
|
+
logger.error(f"Error deleting object: {str(e)}")
|
412
|
+
return f"Error deleting object: {str(e)}"
|
413
|
+
|
414
|
+
@mcp.prompt()
|
415
|
+
def asset_creation_strategy() -> str:
|
416
|
+
"""Defines the preferred strategy for creating assets in Rhino"""
|
417
|
+
return """When creating 3D content in Rhino, always start by checking if integrations are available:
|
418
|
+
|
419
|
+
0. Before anything, always check the scene from get_scene_info()
|
420
|
+
1. Use the method create_object() for basic primitives (CUBE, SPHERE, etc.)
|
421
|
+
2. When including an object into scene, ALWAYS make sure that the name of the object is meanful.
|
422
|
+
3. After giving the tool location/scale/rotation information (via create_object() and modify_object()),
|
423
|
+
double check the related object's location, scale, rotation, and world_bounding_box using get_object_info(),
|
424
|
+
so that the object is in the desired location.
|
425
|
+
"""
|
426
|
+
|
427
|
+
# Main execution
|
428
|
+
|
429
|
+
def main():
|
430
|
+
"""Run the MCP server"""
|
431
|
+
mcp.run()
|
432
|
+
|
433
|
+
|
434
|
+
if __name__ == "__main__":
|
435
|
+
main()
|