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