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 ADDED
@@ -0,0 +1,5 @@
1
+ """
2
+ Claude CAD - A Model Context Protocol (MCP) plugin for creating 3D models with Claude AI using CadQuery
3
+ """
4
+
5
+ __version__ = "0.1.0"
@@ -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()