claude-cad 0.1.0__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.
- claude_cad/__init__.py +5 -0
- claude_cad/mock_server.py +543 -0
- claude_cad/model_generator.py +471 -0
- claude_cad/server.py +505 -0
- claude_cad/utils.py +86 -0
- claude_cad-0.1.0.dist-info/METADATA +147 -0
- claude_cad-0.1.0.dist-info/RECORD +11 -0
- claude_cad-0.1.0.dist-info/WHEEL +5 -0
- claude_cad-0.1.0.dist-info/entry_points.txt +2 -0
- claude_cad-0.1.0.dist-info/licenses/LICENSE +201 -0
- claude_cad-0.1.0.dist-info/top_level.txt +1 -0
claude_cad/__init__.py
ADDED
@@ -0,0 +1,543 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Mock MCP Server for Claude CAD
|
4
|
+
|
5
|
+
This file provides a simplified version of the MCP server that doesn't rely on
|
6
|
+
the actual MCP package. It allows for testing the core functionality without
|
7
|
+
dealing with MCP-specific implementation details.
|
8
|
+
"""
|
9
|
+
|
10
|
+
import os
|
11
|
+
import json
|
12
|
+
import tempfile
|
13
|
+
import uuid
|
14
|
+
import shutil
|
15
|
+
import asyncio
|
16
|
+
from pathlib import Path
|
17
|
+
from typing import Dict, List, Optional, Any, Union, Callable
|
18
|
+
|
19
|
+
import cadquery as cq
|
20
|
+
|
21
|
+
from . import model_generator
|
22
|
+
from . import utils
|
23
|
+
|
24
|
+
|
25
|
+
class MockMcpError(Exception):
|
26
|
+
"""Mock implementation of McpError."""
|
27
|
+
|
28
|
+
def __init__(self, code: int, message: str):
|
29
|
+
self.code = code
|
30
|
+
self.message = message
|
31
|
+
super().__init__(f"MCP Error {code}: {message}")
|
32
|
+
|
33
|
+
|
34
|
+
class MockServer:
|
35
|
+
"""A simplified mock implementation of the MCP Server."""
|
36
|
+
|
37
|
+
def __init__(self, name: str, version: str):
|
38
|
+
self.name = name
|
39
|
+
self.version = version
|
40
|
+
self.handlers = {}
|
41
|
+
self.onerror = None
|
42
|
+
|
43
|
+
def setRequestHandler(self, schema: str, handler: Callable) -> None:
|
44
|
+
"""Register a request handler."""
|
45
|
+
self.handlers[schema] = handler
|
46
|
+
|
47
|
+
async def connect(self, transport: Any) -> None:
|
48
|
+
"""Mock connection to a transport."""
|
49
|
+
print(f"Mock server {self.name} v{self.version} connected")
|
50
|
+
|
51
|
+
async def close(self) -> None:
|
52
|
+
"""Mock close the server connection."""
|
53
|
+
print(f"Mock server {self.name} closed")
|
54
|
+
|
55
|
+
|
56
|
+
class MockStdioTransport:
|
57
|
+
"""A mock implementation of the stdio transport."""
|
58
|
+
|
59
|
+
def __init__(self):
|
60
|
+
pass
|
61
|
+
|
62
|
+
|
63
|
+
class MockRequest:
|
64
|
+
"""A mock request object."""
|
65
|
+
|
66
|
+
def __init__(self, params: Any):
|
67
|
+
self.params = params
|
68
|
+
|
69
|
+
|
70
|
+
# Mock schema constants
|
71
|
+
ListResourcesRequestSchema = "resources/list"
|
72
|
+
ReadResourceRequestSchema = "resources/read"
|
73
|
+
ListToolsRequestSchema = "tools/list"
|
74
|
+
CallToolRequestSchema = "tools/call"
|
75
|
+
|
76
|
+
|
77
|
+
# Mock error codes
|
78
|
+
class MockErrorCode:
|
79
|
+
"""Mock error codes."""
|
80
|
+
InvalidRequest = 400
|
81
|
+
MethodNotFound = 404
|
82
|
+
InternalError = 500
|
83
|
+
InvalidParams = 422
|
84
|
+
|
85
|
+
|
86
|
+
class ClaudeCADServer:
|
87
|
+
"""Mock MCP Server implementation for 3D modeling with CadQuery."""
|
88
|
+
|
89
|
+
def __init__(self):
|
90
|
+
"""Initialize the CAD MCP server."""
|
91
|
+
self.server = MockServer("claude_cad", "0.1.0")
|
92
|
+
|
93
|
+
# Create a temp directory for storing generated models
|
94
|
+
self.temp_dir = Path(tempfile.mkdtemp())
|
95
|
+
|
96
|
+
# Dictionary to store created models
|
97
|
+
self.models: Dict[str, Dict[str, Any]] = {}
|
98
|
+
|
99
|
+
# Set up request handlers
|
100
|
+
self.setup_handlers()
|
101
|
+
self.setup_error_handling()
|
102
|
+
|
103
|
+
def setup_error_handling(self) -> None:
|
104
|
+
"""Configure error handling for the server."""
|
105
|
+
self.server.onerror = lambda error: print(f"[MCP Error] {error}", flush=True)
|
106
|
+
|
107
|
+
def setup_handlers(self) -> None:
|
108
|
+
"""Register all request handlers for the server."""
|
109
|
+
self.setup_resource_handlers()
|
110
|
+
self.setup_tool_handlers()
|
111
|
+
|
112
|
+
def setup_resource_handlers(self) -> None:
|
113
|
+
"""Set up handlers for resource-related requests."""
|
114
|
+
|
115
|
+
# Handler for listing available models as resources
|
116
|
+
self.server.setRequestHandler(
|
117
|
+
ListResourcesRequestSchema,
|
118
|
+
self.handle_list_resources
|
119
|
+
)
|
120
|
+
|
121
|
+
# Handler for reading model content
|
122
|
+
self.server.setRequestHandler(
|
123
|
+
ReadResourceRequestSchema,
|
124
|
+
self.handle_read_resource
|
125
|
+
)
|
126
|
+
|
127
|
+
def setup_tool_handlers(self) -> None:
|
128
|
+
"""Set up handlers for tool-related requests."""
|
129
|
+
|
130
|
+
# Handler for listing available CAD tools
|
131
|
+
self.server.setRequestHandler(
|
132
|
+
ListToolsRequestSchema,
|
133
|
+
self.handle_list_tools
|
134
|
+
)
|
135
|
+
|
136
|
+
# Handler for calling CAD tools
|
137
|
+
self.server.setRequestHandler(
|
138
|
+
CallToolRequestSchema,
|
139
|
+
self.handle_call_tool
|
140
|
+
)
|
141
|
+
|
142
|
+
async def handle_list_resources(self, request: Any) -> Dict[str, List[Dict[str, str]]]:
|
143
|
+
"""Handle the resources/list request to provide available 3D models.
|
144
|
+
|
145
|
+
Returns:
|
146
|
+
A dictionary containing the list of available models as resources.
|
147
|
+
"""
|
148
|
+
return {
|
149
|
+
"resources": [
|
150
|
+
{
|
151
|
+
"uri": f"model://{model_id}",
|
152
|
+
"name": model_info.get("name", f"Model {model_id}"),
|
153
|
+
"mimeType": "model/step",
|
154
|
+
"description": model_info.get("description", "A 3D model created with CadQuery")
|
155
|
+
}
|
156
|
+
for model_id, model_info in self.models.items()
|
157
|
+
]
|
158
|
+
}
|
159
|
+
|
160
|
+
async def handle_read_resource(self, request: Any) -> Dict[str, List[Dict[str, str]]]:
|
161
|
+
"""Handle the resources/read request to provide model data.
|
162
|
+
|
163
|
+
Args:
|
164
|
+
request: The MCP request object containing the resource URI.
|
165
|
+
|
166
|
+
Returns:
|
167
|
+
A dictionary containing the model data.
|
168
|
+
|
169
|
+
Raises:
|
170
|
+
McpError: If the requested model is not found.
|
171
|
+
"""
|
172
|
+
url = utils.parse_resource_uri(request.params.uri)
|
173
|
+
model_id = url.path.lstrip('/')
|
174
|
+
|
175
|
+
if model_id not in self.models:
|
176
|
+
raise MockMcpError(
|
177
|
+
MockErrorCode.InvalidRequest,
|
178
|
+
f"Model {model_id} not found"
|
179
|
+
)
|
180
|
+
|
181
|
+
model_info = self.models[model_id]
|
182
|
+
|
183
|
+
# Return metadata about the model
|
184
|
+
return {
|
185
|
+
"contents": [
|
186
|
+
{
|
187
|
+
"uri": request.params.uri,
|
188
|
+
"mimeType": "application/json",
|
189
|
+
"text": json.dumps(model_info, indent=2)
|
190
|
+
}
|
191
|
+
]
|
192
|
+
}
|
193
|
+
|
194
|
+
async def handle_list_tools(self, request: Any) -> Dict[str, List[Dict[str, Any]]]:
|
195
|
+
"""Handle the tools/list request to provide available CAD tools.
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
A dictionary containing the list of available CAD tools.
|
199
|
+
"""
|
200
|
+
return {
|
201
|
+
"tools": [
|
202
|
+
{
|
203
|
+
"name": "create_primitive",
|
204
|
+
"description": "Create a primitive 3D shape",
|
205
|
+
"inputSchema": {
|
206
|
+
"type": "object",
|
207
|
+
"properties": {
|
208
|
+
"shape_type": {
|
209
|
+
"type": "string",
|
210
|
+
"description": "Type of primitive shape (box, sphere, cylinder, cone)",
|
211
|
+
"enum": ["box", "sphere", "cylinder", "cone"]
|
212
|
+
},
|
213
|
+
"parameters": {
|
214
|
+
"type": "object",
|
215
|
+
"description": "Parameters for the primitive shape"
|
216
|
+
},
|
217
|
+
"name": {
|
218
|
+
"type": "string",
|
219
|
+
"description": "Name of the model"
|
220
|
+
},
|
221
|
+
"description": {
|
222
|
+
"type": "string",
|
223
|
+
"description": "Description of the model"
|
224
|
+
}
|
225
|
+
},
|
226
|
+
"required": ["shape_type", "parameters"]
|
227
|
+
}
|
228
|
+
},
|
229
|
+
{
|
230
|
+
"name": "create_model_from_text",
|
231
|
+
"description": "Create a 3D model from a text description using CadQuery",
|
232
|
+
"inputSchema": {
|
233
|
+
"type": "object",
|
234
|
+
"properties": {
|
235
|
+
"description": {
|
236
|
+
"type": "string",
|
237
|
+
"description": "Natural language description of the 3D model to create"
|
238
|
+
},
|
239
|
+
"name": {
|
240
|
+
"type": "string",
|
241
|
+
"description": "Name of the model"
|
242
|
+
},
|
243
|
+
"format": {
|
244
|
+
"type": "string",
|
245
|
+
"description": "Export format (step, stl)",
|
246
|
+
"enum": ["step", "stl"],
|
247
|
+
"default": "step"
|
248
|
+
}
|
249
|
+
},
|
250
|
+
"required": ["description"]
|
251
|
+
}
|
252
|
+
},
|
253
|
+
{
|
254
|
+
"name": "execute_cadquery_script",
|
255
|
+
"description": "Execute custom CadQuery Python code to create a 3D model",
|
256
|
+
"inputSchema": {
|
257
|
+
"type": "object",
|
258
|
+
"properties": {
|
259
|
+
"code": {
|
260
|
+
"type": "string",
|
261
|
+
"description": "Python code using CadQuery to create a model"
|
262
|
+
},
|
263
|
+
"name": {
|
264
|
+
"type": "string",
|
265
|
+
"description": "Name of the model"
|
266
|
+
},
|
267
|
+
"description": {
|
268
|
+
"type": "string",
|
269
|
+
"description": "Description of the model"
|
270
|
+
},
|
271
|
+
"format": {
|
272
|
+
"type": "string",
|
273
|
+
"description": "Export format (step, stl)",
|
274
|
+
"enum": ["step", "stl"],
|
275
|
+
"default": "step"
|
276
|
+
}
|
277
|
+
},
|
278
|
+
"required": ["code"]
|
279
|
+
}
|
280
|
+
}
|
281
|
+
]
|
282
|
+
}
|
283
|
+
|
284
|
+
async def handle_call_tool(self, request: Any) -> Dict[str, List[Dict[str, str]]]:
|
285
|
+
"""Handle the tools/call request to execute a CAD tool.
|
286
|
+
|
287
|
+
Args:
|
288
|
+
request: The MCP request object containing the tool name and arguments.
|
289
|
+
|
290
|
+
Returns:
|
291
|
+
A dictionary containing the result of the tool execution.
|
292
|
+
|
293
|
+
Raises:
|
294
|
+
McpError: If the requested tool is not found or if there's an error executing it.
|
295
|
+
"""
|
296
|
+
tool_name = request.params.name
|
297
|
+
args = request.params.arguments or {}
|
298
|
+
|
299
|
+
try:
|
300
|
+
if tool_name == "create_primitive":
|
301
|
+
return await self._handle_create_primitive(args)
|
302
|
+
elif tool_name == "create_model_from_text":
|
303
|
+
return await self._handle_create_model_from_text(args)
|
304
|
+
elif tool_name == "execute_cadquery_script":
|
305
|
+
return await self._handle_execute_cadquery_script(args)
|
306
|
+
else:
|
307
|
+
raise MockMcpError(
|
308
|
+
MockErrorCode.MethodNotFound,
|
309
|
+
f"Unknown tool: {tool_name}"
|
310
|
+
)
|
311
|
+
except Exception as e:
|
312
|
+
if isinstance(e, MockMcpError):
|
313
|
+
raise
|
314
|
+
raise MockMcpError(
|
315
|
+
MockErrorCode.InternalError,
|
316
|
+
f"Error executing {tool_name}: {str(e)}"
|
317
|
+
)
|
318
|
+
|
319
|
+
async def _handle_create_primitive(self, args: Dict[str, Any]) -> Dict[str, List[Dict[str, str]]]:
|
320
|
+
"""Handle the create_primitive tool request.
|
321
|
+
|
322
|
+
Args:
|
323
|
+
args: The tool arguments containing the primitive shape details.
|
324
|
+
|
325
|
+
Returns:
|
326
|
+
A dictionary containing the result of the primitive creation.
|
327
|
+
"""
|
328
|
+
shape_type = args.get("shape_type")
|
329
|
+
parameters = args.get("parameters", {})
|
330
|
+
name = args.get("name", f"Primitive {shape_type.capitalize()}")
|
331
|
+
description = args.get("description", f"A {shape_type} created with CadQuery")
|
332
|
+
|
333
|
+
# Generate the model using CadQuery
|
334
|
+
try:
|
335
|
+
model = model_generator.create_primitive(shape_type, parameters)
|
336
|
+
|
337
|
+
# Save the model and store metadata
|
338
|
+
model_id = self._save_model(model, name, description)
|
339
|
+
|
340
|
+
# Return success message with model information
|
341
|
+
return {
|
342
|
+
"content": [
|
343
|
+
{
|
344
|
+
"type": "text",
|
345
|
+
"text": f"Created {shape_type} model with ID: {model_id}\n"
|
346
|
+
f"You can access this model as a resource with URI: model://{model_id}"
|
347
|
+
}
|
348
|
+
]
|
349
|
+
}
|
350
|
+
except Exception as e:
|
351
|
+
raise MockMcpError(
|
352
|
+
MockErrorCode.InternalError,
|
353
|
+
f"Error creating primitive: {str(e)}"
|
354
|
+
)
|
355
|
+
|
356
|
+
async def _handle_create_model_from_text(self, args: Dict[str, Any]) -> Dict[str, List[Dict[str, str]]]:
|
357
|
+
"""Handle the create_model_from_text tool request.
|
358
|
+
|
359
|
+
Args:
|
360
|
+
args: The tool arguments containing the text description of the model.
|
361
|
+
|
362
|
+
Returns:
|
363
|
+
A dictionary containing the result of the model creation.
|
364
|
+
"""
|
365
|
+
description = args.get("description")
|
366
|
+
name = args.get("name", "Text-generated Model")
|
367
|
+
format_type = args.get("format", "step")
|
368
|
+
|
369
|
+
if not description:
|
370
|
+
raise MockMcpError(
|
371
|
+
MockErrorCode.InvalidParams,
|
372
|
+
"Model description is required"
|
373
|
+
)
|
374
|
+
|
375
|
+
try:
|
376
|
+
# Generate model from text description
|
377
|
+
model, code = model_generator.create_from_text(description)
|
378
|
+
|
379
|
+
# Save the model
|
380
|
+
model_id = self._save_model(model, name, description, code, format_type)
|
381
|
+
|
382
|
+
# Return success message with model information and generated code
|
383
|
+
return {
|
384
|
+
"content": [
|
385
|
+
{
|
386
|
+
"type": "text",
|
387
|
+
"text": f"Created model from description with ID: {model_id}\n"
|
388
|
+
f"You can access this model as a resource with URI: model://{model_id}\n\n"
|
389
|
+
f"Generated CadQuery code:\n```python\n{code}\n```"
|
390
|
+
}
|
391
|
+
]
|
392
|
+
}
|
393
|
+
except Exception as e:
|
394
|
+
raise MockMcpError(
|
395
|
+
MockErrorCode.InternalError,
|
396
|
+
f"Error creating model from text: {str(e)}"
|
397
|
+
)
|
398
|
+
|
399
|
+
async def _handle_execute_cadquery_script(self, args: Dict[str, Any]) -> Dict[str, List[Dict[str, str]]]:
|
400
|
+
"""Handle the execute_cadquery_script tool request.
|
401
|
+
|
402
|
+
Args:
|
403
|
+
args: The tool arguments containing the CadQuery Python code.
|
404
|
+
|
405
|
+
Returns:
|
406
|
+
A dictionary containing the result of the script execution.
|
407
|
+
"""
|
408
|
+
code = args.get("code")
|
409
|
+
name = args.get("name", "Custom CadQuery Model")
|
410
|
+
description = args.get("description", "A model created with custom CadQuery code")
|
411
|
+
format_type = args.get("format", "step")
|
412
|
+
|
413
|
+
if not code:
|
414
|
+
raise MockMcpError(
|
415
|
+
MockErrorCode.InvalidParams,
|
416
|
+
"CadQuery code is required"
|
417
|
+
)
|
418
|
+
|
419
|
+
try:
|
420
|
+
# Execute the CadQuery script
|
421
|
+
model = model_generator.execute_script(code)
|
422
|
+
|
423
|
+
# Save the model
|
424
|
+
model_id = self._save_model(model, name, description, code, format_type)
|
425
|
+
|
426
|
+
# Return success message with model information
|
427
|
+
return {
|
428
|
+
"content": [
|
429
|
+
{
|
430
|
+
"type": "text",
|
431
|
+
"text": f"Successfully executed CadQuery code and created model with ID: {model_id}\n"
|
432
|
+
f"You can access this model as a resource with URI: model://{model_id}"
|
433
|
+
}
|
434
|
+
]
|
435
|
+
}
|
436
|
+
except Exception as e:
|
437
|
+
raise MockMcpError(
|
438
|
+
MockErrorCode.InternalError,
|
439
|
+
f"Error executing CadQuery script: {str(e)}"
|
440
|
+
)
|
441
|
+
|
442
|
+
def _save_model(self,
|
443
|
+
model: cq.Workplane,
|
444
|
+
name: str,
|
445
|
+
description: str,
|
446
|
+
code: Optional[str] = None,
|
447
|
+
format_type: str = "step") -> str:
|
448
|
+
"""Save a CadQuery model to disk and store its metadata.
|
449
|
+
|
450
|
+
Args:
|
451
|
+
model: The CadQuery Workplane object.
|
452
|
+
name: The name of the model.
|
453
|
+
description: The description of the model.
|
454
|
+
code: The CadQuery code used to generate the model (optional).
|
455
|
+
format_type: The export format (step, stl).
|
456
|
+
|
457
|
+
Returns:
|
458
|
+
The generated model ID.
|
459
|
+
"""
|
460
|
+
model_id = str(uuid.uuid4())
|
461
|
+
model_dir = self.temp_dir / model_id
|
462
|
+
model_dir.mkdir(exist_ok=True)
|
463
|
+
|
464
|
+
# Export the model in the requested format
|
465
|
+
if format_type.lower() == "stl":
|
466
|
+
file_path = model_dir / f"{model_id}.stl"
|
467
|
+
model.export(str(file_path))
|
468
|
+
mime_type = "model/stl"
|
469
|
+
else: # Default to STEP
|
470
|
+
file_path = model_dir / f"{model_id}.step"
|
471
|
+
model.export(str(file_path))
|
472
|
+
mime_type = "model/step"
|
473
|
+
|
474
|
+
# Store model metadata
|
475
|
+
self.models[model_id] = {
|
476
|
+
"id": model_id,
|
477
|
+
"name": name,
|
478
|
+
"description": description,
|
479
|
+
"file_path": str(file_path),
|
480
|
+
"mime_type": mime_type,
|
481
|
+
"format": format_type,
|
482
|
+
"code": code
|
483
|
+
}
|
484
|
+
|
485
|
+
return model_id
|
486
|
+
|
487
|
+
async def run(self) -> None:
|
488
|
+
"""Start the CAD MCP server."""
|
489
|
+
# Use MockStdioTransport instead of the real transport
|
490
|
+
transport = MockStdioTransport()
|
491
|
+
await self.server.connect(transport)
|
492
|
+
print("Mock Claude CAD server running", flush=True)
|
493
|
+
|
494
|
+
async def cleanup(self) -> None:
|
495
|
+
"""Clean up resources used by the server."""
|
496
|
+
try:
|
497
|
+
# Remove temporary directory
|
498
|
+
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
499
|
+
except Exception as e:
|
500
|
+
print(f"Error during cleanup: {e}", flush=True)
|
501
|
+
|
502
|
+
# Close the server connection
|
503
|
+
await self.server.close()
|
504
|
+
|
505
|
+
|
506
|
+
def main() -> None:
|
507
|
+
"""Entry point for the Claude CAD mock server."""
|
508
|
+
import asyncio
|
509
|
+
import signal
|
510
|
+
import sys
|
511
|
+
|
512
|
+
# Create and run the server
|
513
|
+
server = ClaudeCADServer()
|
514
|
+
|
515
|
+
# Set up signal handlers
|
516
|
+
loop = asyncio.get_event_loop()
|
517
|
+
|
518
|
+
for sig in [signal.SIGINT, signal.SIGTERM]:
|
519
|
+
loop.add_signal_handler(sig, lambda: asyncio.create_task(shutdown(server, loop)))
|
520
|
+
|
521
|
+
try:
|
522
|
+
loop.run_until_complete(server.run())
|
523
|
+
loop.run_forever()
|
524
|
+
except Exception as e:
|
525
|
+
print(f"Server error: {e}", flush=True)
|
526
|
+
sys.exit(1)
|
527
|
+
|
528
|
+
|
529
|
+
async def shutdown(server: ClaudeCADServer, loop: asyncio.AbstractEventLoop) -> None:
|
530
|
+
"""Gracefully shut down the server."""
|
531
|
+
print("Shutting down server...", flush=True)
|
532
|
+
await server.cleanup()
|
533
|
+
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
534
|
+
|
535
|
+
for task in tasks:
|
536
|
+
task.cancel()
|
537
|
+
|
538
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
539
|
+
loop.stop()
|
540
|
+
|
541
|
+
|
542
|
+
if __name__ == "__main__":
|
543
|
+
main()
|