├── requirements.txt ├── LiveCube.manifest ├── LiveCube ├── LiveCube.manifest └── LiveCube.py ├── fusion_server.py ├── LiveCube.py ├── README.md └── fusion_mcp.py /requirements.txt: -------------------------------------------------------------------------------- 1 | mcp 2 | httpx 3 | python-dotenv 4 | -------------------------------------------------------------------------------- /LiveCube.manifest: -------------------------------------------------------------------------------- 1 | { 2 | "autodeskProduct": "Fusion360", 3 | "type": "python", 4 | "author": "Your Name", 5 | "description": "Live cube controlled by MCP", 6 | "supportedOS": "mac", 7 | "version": "0.1.0", 8 | "runOnStartup": false, 9 | "entry": "LiveCube.py" 10 | } -------------------------------------------------------------------------------- /LiveCube/LiveCube.manifest: -------------------------------------------------------------------------------- 1 | { 2 | "autodeskProduct": "Fusion360", 3 | "type": "python", 4 | "author": "Your Name", 5 | "description": "Live cube controlled by MCP", 6 | "supportedOS": "mac", 7 | "version": "0.1.0", 8 | "runOnStartup": false, 9 | "entry": "LiveCube.py" 10 | } -------------------------------------------------------------------------------- /fusion_server.py: -------------------------------------------------------------------------------- 1 | """ 2 | fusion_server.py - Intermediary server that communicates between MCP and Fusion 360 3 | """ 4 | from http.server import HTTPServer, BaseHTTPRequestHandler 5 | import json 6 | import urllib.parse 7 | import sys 8 | import logging 9 | 10 | # Configure logging 11 | logging.basicConfig( 12 | level=logging.INFO, 13 | format="%(asctime)s - %(levelname)s - %(message)s", 14 | ) 15 | logger = logging.getLogger(__name__) 16 | 17 | class FusionRequestHandler(BaseHTTPRequestHandler): 18 | """ 19 | HTTP request handler for the Fusion 360 intermediary server. 20 | Receives requests from the MCP server and forwards them to Fusion 360. 21 | """ 22 | def do_GET(self): 23 | """Handle GET requests from the MCP server""" 24 | try: 25 | # Parse the URL and query parameters 26 | parsed_path = urllib.parse.urlparse(self.path) 27 | params = dict(urllib.parse.parse_qsl(parsed_path.query)) 28 | 29 | # Handle different endpoints 30 | if parsed_path.path == "/create_cube": 31 | self._handle_create_cube(params) 32 | else: 33 | self.send_response(404) 34 | self.end_headers() 35 | self.wfile.write(b"Not found") 36 | except Exception as e: 37 | logger.error(f"Error handling request: {e}") 38 | self.send_response(500) 39 | self.end_headers() 40 | self.wfile.write(f"Error: {str(e)}".encode()) 41 | 42 | def _handle_create_cube(self, params): 43 | """Handle cube creation requests""" 44 | try: 45 | # Extract cube dimensions from parameters 46 | width = float(params.get("width", 50.0)) 47 | height = float(params.get("height", 50.0)) 48 | depth = float(params.get("depth", 50.0)) 49 | 50 | logger.info(f"Creating cube with dimensions: W:{width}mm, H:{height}mm, D:{depth}mm") 51 | 52 | # In a real implementation, this would communicate with Fusion 360 via its API 53 | # For this example, we'll just return a success response 54 | 55 | self.send_response(200) 56 | self.send_header("Content-Type", "application/json") 57 | self.end_headers() 58 | self.wfile.write(json.dumps({ 59 | "status": "success", 60 | "message": f"Cube created with dimensions: W:{width}mm, H:{height}mm, D:{depth}mm" 61 | }).encode()) 62 | 63 | except Exception as e: 64 | logger.error(f"Error creating cube: {e}") 65 | self.send_response(500) 66 | self.end_headers() 67 | self.wfile.write(f"Error creating cube: {str(e)}".encode()) 68 | 69 | def run_server(host="localhost", port=8000): 70 | """Run the Fusion 360 intermediary server""" 71 | server_address = (host, port) 72 | httpd = HTTPServer(server_address, FusionRequestHandler) 73 | logger.info(f"Starting Fusion 360 intermediary server on {host}:{port}") 74 | try: 75 | httpd.serve_forever() 76 | except KeyboardInterrupt: 77 | logger.info("Server stopped by user") 78 | except Exception as e: 79 | logger.error(f"Server error: {e}") 80 | finally: 81 | httpd.server_close() 82 | logger.info("Server closed") 83 | 84 | if __name__ == "__main__": 85 | run_server() 86 | -------------------------------------------------------------------------------- /LiveCube.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # LiveCube.py - A Fusion 360 add-in that creates cubes via HTTP endpoint 3 | import adsk.core, adsk.fusion, adsk.cam, traceback 4 | import threading, json, os 5 | from http.server import HTTPServer, BaseHTTPRequestHandler 6 | from urllib.parse import urlparse, parse_qs 7 | 8 | # Global variables 9 | app = None 10 | ui = None 11 | handlers = [] 12 | server_thread = None 13 | DEFAULT_PORT = 18080 14 | 15 | def run(context): 16 | global app, ui, server_thread 17 | 18 | try: 19 | # Get the Fusion application object 20 | app = adsk.core.Application.get() 21 | ui = app.userInterface 22 | 23 | # Start the HTTP server in a separate thread 24 | server_thread = threading.Thread(target=start_server) 25 | server_thread.daemon = True 26 | server_thread.start() 27 | 28 | ui.messageBox('Cube MCP Server started on port 18080.\nSend HTTP GET requests to http://127.0.0.1:18080/cmd?edge=SIZE') 29 | 30 | except: 31 | if ui: 32 | ui.messageBox('Failed:\n{}'.format(traceback.format_exc())) 33 | 34 | def stop(context): 35 | global server_thread 36 | 37 | try: 38 | ui.messageBox('Cube MCP Server stopping...') 39 | # Server will stop when Fusion closes as it's a daemon thread 40 | except: 41 | if ui: 42 | ui.messageBox('Failed to stop:\n{}'.format(traceback.format_exc())) 43 | 44 | def create_cube(edge_mm=20.0): 45 | """Create a cube with the specified edge length in mm""" 46 | try: 47 | app = adsk.core.Application.get() 48 | design = adsk.fusion.Design.cast(app.activeProduct) 49 | if not design: 50 | app.activeDocument.close(False) # Don't save 51 | app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType) 52 | design = app.activeProduct 53 | 54 | root = design.rootComponent 55 | 56 | # Create sketch on XY plane 57 | sketches = root.sketches 58 | xyPlane = root.xYConstructionPlane 59 | sketch = sketches.add(xyPlane) 60 | 61 | # Draw a square centered at origin 62 | lines = sketch.sketchCurves.sketchLines 63 | half_edge = edge_mm / 2 64 | lines.addCenterPointRectangle( 65 | adsk.core.Point3D.create(0, 0, 0), 66 | adsk.core.Point3D.create(half_edge, half_edge, 0) 67 | ) 68 | 69 | # Extrude to create a cube 70 | extrudes = root.features.extrudeFeatures 71 | distance = adsk.core.ValueInput.createByReal(edge_mm) 72 | profile = sketch.profiles.item(0) 73 | extInput = extrudes.createInput( 74 | profile, adsk.fusion.FeatureOperations.NewBodyFeatureOperation 75 | ) 76 | extInput.setDistanceExtent(False, distance) 77 | extrude = extrudes.add(extInput) 78 | 79 | # Set a nice view of the cube 80 | cam = app.activeViewport.camera 81 | cam.viewOrientation = adsk.core.ViewOrientations.IsometricViewOrientation 82 | cam.isFitView = True 83 | app.activeViewport.camera = cam 84 | 85 | # Return success message with cube dimensions 86 | return {"status": "success", "cube": f"{edge_mm}mm edge cube created"} 87 | 88 | except Exception as e: 89 | return {"status": "error", "message": str(e)} 90 | 91 | class CubeRequestHandler(BaseHTTPRequestHandler): 92 | def do_GET(self): 93 | try: 94 | # Parse the URL 95 | url = urlparse(self.path) 96 | 97 | # Basic routing 98 | if url.path == "/cmd": 99 | # Parse query parameters 100 | query = parse_qs(url.query) 101 | 102 | # Get the edge parameter or use default 103 | edge_mm = 20.0 104 | if "edge" in query and query["edge"][0]: 105 | try: 106 | edge_mm = float(query["edge"][0]) 107 | except ValueError: 108 | pass 109 | 110 | # Create the cube in the main thread 111 | result = create_cube(edge_mm) 112 | 113 | # Send response 114 | self.send_response(200) 115 | self.send_header('Content-type', 'application/json') 116 | self.end_headers() 117 | self.wfile.write(json.dumps(result).encode('utf-8')) 118 | else: 119 | # Not found 120 | self.send_response(404) 121 | self.end_headers() 122 | self.wfile.write(b'{"status": "error", "message": "Path not found"}') 123 | 124 | except Exception as e: 125 | # Internal error 126 | self.send_response(500) 127 | self.end_headers() 128 | self.wfile.write(json.dumps({"status": "error", "message": str(e)}).encode('utf-8')) 129 | 130 | def log_message(self, format, *args): 131 | # Silence the HTTP server logs 132 | pass 133 | 134 | def start_server(): 135 | """Start the HTTP server on localhost:18080""" 136 | try: 137 | server = HTTPServer(('127.0.0.1', DEFAULT_PORT), CubeRequestHandler) 138 | print(f"Server started at http://127.0.0.1:{DEFAULT_PORT}") 139 | server.serve_forever() 140 | except Exception as e: 141 | if ui: 142 | ui.messageBox(f"Failed to start server: {str(e)}") 143 | -------------------------------------------------------------------------------- /LiveCube/LiveCube.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # LiveCube.py - A Fusion 360 add-in that creates cubes via HTTP endpoint 3 | import adsk.core, adsk.fusion, adsk.cam, traceback 4 | import threading, json, os 5 | from http.server import HTTPServer, BaseHTTPRequestHandler 6 | from urllib.parse import urlparse, parse_qs 7 | 8 | # Global variables 9 | app = None 10 | ui = None 11 | handlers = [] 12 | server_thread = None 13 | DEFAULT_PORT = 18080 14 | 15 | def run(context): 16 | global app, ui, server_thread 17 | 18 | try: 19 | # Get the Fusion application object 20 | app = adsk.core.Application.get() 21 | ui = app.userInterface 22 | 23 | # Start the HTTP server in a separate thread 24 | server_thread = threading.Thread(target=start_server) 25 | server_thread.daemon = True 26 | server_thread.start() 27 | 28 | ui.messageBox('Cube MCP Server started on port 18080.\nSend HTTP GET requests to http://127.0.0.1:18080/cmd?edge=SIZE') 29 | 30 | except: 31 | if ui: 32 | ui.messageBox('Failed:\n{}'.format(traceback.format_exc())) 33 | 34 | def stop(context): 35 | global server_thread 36 | 37 | try: 38 | ui.messageBox('Cube MCP Server stopping...') 39 | # Server will stop when Fusion closes as it's a daemon thread 40 | except: 41 | if ui: 42 | ui.messageBox('Failed to stop:\n{}'.format(traceback.format_exc())) 43 | 44 | def create_cube(edge_mm=20.0): 45 | """Create a cube with the specified edge length in mm""" 46 | try: 47 | app = adsk.core.Application.get() 48 | design = adsk.fusion.Design.cast(app.activeProduct) 49 | if not design: 50 | app.activeDocument.close(False) # Don't save 51 | app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType) 52 | design = app.activeProduct 53 | 54 | root = design.rootComponent 55 | 56 | # Create sketch on XY plane 57 | sketches = root.sketches 58 | xyPlane = root.xYConstructionPlane 59 | sketch = sketches.add(xyPlane) 60 | 61 | # Draw a square centered at origin 62 | lines = sketch.sketchCurves.sketchLines 63 | half_edge = edge_mm / 2 64 | lines.addCenterPointRectangle( 65 | adsk.core.Point3D.create(0, 0, 0), 66 | adsk.core.Point3D.create(half_edge, half_edge, 0) 67 | ) 68 | 69 | # Extrude to create a cube 70 | extrudes = root.features.extrudeFeatures 71 | distance = adsk.core.ValueInput.createByReal(edge_mm) 72 | profile = sketch.profiles.item(0) 73 | extInput = extrudes.createInput( 74 | profile, adsk.fusion.FeatureOperations.NewBodyFeatureOperation 75 | ) 76 | extInput.setDistanceExtent(False, distance) 77 | extrude = extrudes.add(extInput) 78 | 79 | # Set a nice view of the cube 80 | cam = app.activeViewport.camera 81 | cam.viewOrientation = adsk.core.ViewOrientations.IsometricViewOrientation 82 | cam.isFitView = True 83 | app.activeViewport.camera = cam 84 | 85 | # Return success message with cube dimensions 86 | return {"status": "success", "cube": f"{edge_mm}mm edge cube created"} 87 | 88 | except Exception as e: 89 | return {"status": "error", "message": str(e)} 90 | 91 | class CubeRequestHandler(BaseHTTPRequestHandler): 92 | def do_GET(self): 93 | try: 94 | # Parse the URL 95 | url = urlparse(self.path) 96 | 97 | # Basic routing 98 | if url.path == "/cmd": 99 | # Parse query parameters 100 | query = parse_qs(url.query) 101 | 102 | # Get the edge parameter or use default 103 | edge_mm = 20.0 104 | if "edge" in query and query["edge"][0]: 105 | try: 106 | edge_mm = float(query["edge"][0]) 107 | except ValueError: 108 | pass 109 | 110 | # Create the cube in the main thread 111 | result = create_cube(edge_mm) 112 | 113 | # Send response 114 | self.send_response(200) 115 | self.send_header('Content-type', 'application/json') 116 | self.end_headers() 117 | self.wfile.write(json.dumps(result).encode('utf-8')) 118 | else: 119 | # Not found 120 | self.send_response(404) 121 | self.end_headers() 122 | self.wfile.write(b'{"status": "error", "message": "Path not found"}') 123 | 124 | except Exception as e: 125 | # Internal error 126 | self.send_response(500) 127 | self.end_headers() 128 | self.wfile.write(json.dumps({"status": "error", "message": str(e)}).encode('utf-8')) 129 | 130 | def log_message(self, format, *args): 131 | # Silence the HTTP server logs 132 | pass 133 | 134 | def start_server(): 135 | """Start the HTTP server on localhost:18080""" 136 | try: 137 | server = HTTPServer(('127.0.0.1', DEFAULT_PORT), CubeRequestHandler) 138 | print(f"Server started at http://127.0.0.1:{DEFAULT_PORT}") 139 | server.serve_forever() 140 | except Exception as e: 141 | if ui: 142 | ui.messageBox(f"Failed to start server: {str(e)}") 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Autodesk Fusion 360 MCP Integration 2 | 3 | The Fusion MCP (Model Context Protocol) application is an integration system that enables AI assistants to interact programmatically with Autodesk Fusion 360. This implementation specifically demonstrates how to create parametric 3D models through simple API calls, bridging the gap between conversational AI and CAD software. 4 | 5 | ## Overview 6 | 7 | The Fusion 360 MCP Integration enables AI assistants to control Fusion 360 for 3D modeling tasks. This project is particularly valuable for: 8 | 9 | - AI-assisted CAD design workflows 10 | - Parametric 3D model generation 11 | - Automating repetitive design tasks in Fusion 360 12 | - Creating programmatic interfaces to Fusion 360 13 | 14 | ## Components 15 | 16 | The integration consists of three main components: 17 | 18 | ### 1. LiveCube Script (`LiveCube.py` & `LiveCube.manifest`) 19 | 20 | A Fusion 360 add-in that: 21 | - Runs inside Fusion 360 as a script 22 | - Creates parametric cubes with specified dimensions 23 | - Exposes an HTTP endpoint on port 18080 to receive commands 24 | - Can be triggered via simple HTTP GET requests 25 | 26 | ### 2. Fusion Server (`fusion_server.py`) 27 | 28 | An intermediary server that: 29 | - Acts as a bridge between MCP and Fusion 360 30 | - Listens on port 8000 for MCP requests 31 | - Translates MCP calls into formats Fusion 360 can understand 32 | - Handles communication with the LiveCube script 33 | 34 | ### 3. MCP Server (`fusion_mcp.py`) 35 | 36 | The Model Context Protocol server that: 37 | - Provides tools AI assistants can use 38 | - Integrates with Autodesk Platform Services (APS) for cloud automation 39 | - Offers the `generate_cube` tool for creating parametric cubes 40 | - Uses OAuth authentication for secure access to APS 41 | 42 | ## Features 43 | 44 | - **Cube Creation**: Generate parametric cubes with specified dimensions 45 | - **Autodesk Platform Services Integration**: Use APS Design Automation for complex operations 46 | - **Simple HTTP Interface**: Easy-to-use API for controlling Fusion 360 47 | - **MCP Standard Compliance**: Works with any MCP-compatible AI assistant 48 | 49 | ## Installation 50 | 51 | ### Prerequisites 52 | 53 | - Autodesk Fusion 360 (2023 or newer) 54 | - Python 3.9+ with pip 55 | - Autodesk Platform Services account with API access 56 | - MCP-compatible AI assistant (like Claude in Windsurf environments) 57 | 58 | ### Setup Instructions 59 | 60 | 1. **Install Python Dependencies**: 61 | ```bash 62 | pip install -r requirements.txt 63 | ``` 64 | 65 | 2. **Set Up Environment Variables**: 66 | Create a `keys.env` file with your Autodesk Platform Services credentials: 67 | ``` 68 | APS_CLIENT_ID=your_client_id 69 | APS_CLIENT_SECRET=your_client_secret 70 | FUSION_ACTIVITY_ID=your_activity_id 71 | ``` 72 | 73 | 3. **Install LiveCube Script in Fusion 360**: 74 | - Open Fusion 360 75 | - Navigate to Scripts and Add-Ins (Shift+S) 76 | - Click the green "+" button and select "Add script" 77 | - Browse to and select the `LiveCube` folder in this repository 78 | - The script should now appear in your scripts list 79 | 80 | ## Usage 81 | 82 | ### Starting the Servers 83 | 84 | 1. **Start the Fusion Server**: 85 | ```bash 86 | python fusion_server.py 87 | ``` 88 | This will start listening on http://localhost:8000 89 | 90 | 2. **Run the LiveCube Script**: 91 | - In Fusion 360, go to Scripts and Add-Ins 92 | - Select LiveCube and click "Run" 93 | - This will start the HTTP server inside Fusion 360 on port 18080 94 | 95 | 3. **Start the MCP Server**: 96 | ```bash 97 | python fusion_mcp.py 98 | ``` 99 | This will start the MCP server with stdio transport by default. 100 | 101 | ### Using with AI Assistants 102 | 103 | Configure your MCP-compatible AI assistant to connect to the Fusion MCP server. For example, with Claude Desktop: 104 | 105 | ```json 106 | { 107 | "mcpServers": { 108 | "fusion": { 109 | "command": "python", 110 | "args": ["/path/to/fusion_mcp.py"] 111 | } 112 | } 113 | } 114 | ``` 115 | 116 | The AI can then use the `generate_cube` tool to create cubes in Fusion 360. 117 | 118 | ### Direct API Access 119 | 120 | You can also directly interact with the LiveCube script HTTP endpoint: 121 | 122 | ``` 123 | GET http://127.0.0.1:18080/cmd?edge=50 124 | ``` 125 | 126 | This would create a cube with 50mm edge length in Fusion 360. 127 | 128 | ## Developer Notes 129 | 130 | - The MCP server communicates with Autodesk Platform Services (APS) using OAuth 2.0 authentication 131 | - For advanced use cases, modify `fusion_mcp.py` to add additional tools beyond cube creation 132 | - The system architecture can be extended to support other Fusion 360 operations by adding new handlers in `fusion_server.py` and corresponding Fusion 360 scripts 133 | 134 | ## License 135 | 136 | MIT 137 | 138 | ## Acknowledgments 139 | 140 | - Thanks to @damianengineer and @ydooWoody for the contributions! 141 | - Thanks to YCombinator for hosting the World's Biggest MCP Hackathon: https://lu.ma/t4zeld9m?tk=67ixxD 142 | - Autodesk for the Fusion 360 API and Platform Services 143 | - Model Context Protocol (MCP) creators for enabling AI-tool interoperability 144 | ``` 145 | 146 | ### Or specify stdio explicitly 147 | ```shell 148 | npx @modelcontextprotocol/server-everything stdio 149 | ``` 150 | 151 | ### Run the SSE server 152 | ```shell 153 | npx @modelcontextprotocol/server-everything sse 154 | ``` 155 | 156 | ### Run the streamable HTTP server 157 | ```shell 158 | npx @modelcontextprotocol/server-everything streamableHttp 159 | ``` 160 | 161 | -------------------------------------------------------------------------------- /fusion_mcp.py: -------------------------------------------------------------------------------- 1 | # fusion_mcp.py – bare-bones Fusion Design-Automation MCP server 2 | # 3 | # Rename your folder to fusion_mcp/ and launch with: 4 | # uv run fusion_mcp.py 5 | # 6 | # Environment variables expected (put them in keys.env or .env): 7 | # APS_CLIENT_ID 8 | # APS_CLIENT_SECRET 9 | # FUSION_ACTIVITY_ID # ← activity you registered in APS; see README 10 | # APS_BASE=https://developer.api.autodesk.com # optional override 11 | 12 | from mcp.server.fastmcp import FastMCP 13 | import httpx, os, asyncio, logging, argparse 14 | from dotenv import load_dotenv 15 | from datetime import datetime, timezone 16 | 17 | # ──────────────────────────── Bootstrap ──────────────────────────── 18 | logging.basicConfig( 19 | level=logging.INFO, 20 | format="%(asctime)s - %(levelname)s - %(message)s", 21 | ) 22 | 23 | mcp = FastMCP("fusion") # server name Windsurf will show 24 | load_dotenv("keys.env") # same pattern you had before 25 | 26 | APS_BASE = os.getenv("APS_BASE", "https://developer.api.autodesk.com") 27 | CLIENT_ID = os.environ["APS_CLIENT_ID"] 28 | CLIENT_SECRET = os.environ["APS_CLIENT_SECRET"] 29 | ACTIVITY_ID = os.environ["FUSION_ACTIVITY_ID"] # e.g. .GenerateCube+prod 30 | 31 | TOKEN_URL = f"{APS_BASE}/authentication/v2/token" # [oai_citation:0‡Autodesk Platform Services](https://aps.autodesk.com/en/docs/oauth/v2/reference/http/gettoken-POST?utm_source=chatgpt.com) 32 | WORKITEMS_URL = f"{APS_BASE}/da/us-east/v3/workitems" # " turn0search1 33 | POLL_INTERVAL = 4 # seconds 34 | 35 | # ─────────────────────── APS helper functions ────────────────────── 36 | async def get_oauth_token() -> str: 37 | """ 38 | Fetches a 2-legged OAuth token good for 60 minutes. 39 | APS OAuth v2 expects Basic-auth header with client-credentials. [oai_citation:1‡Autodesk Platform Services](https://aps.autodesk.com/en/docs/oauth/v2/tutorials/get-2-legged-token?utm_source=chatgpt.com) [oai_citation:2‡Autodesk Platform Services](https://aps.autodesk.com/en/docs/oauth/v2/developers_guide/field-guide?utm_source=chatgpt.com) 40 | """ 41 | headers = httpx.BasicAuth(CLIENT_ID, CLIENT_SECRET).auth_header 42 | async with httpx.AsyncClient() as client: 43 | resp = await client.post( 44 | TOKEN_URL, 45 | headers={"Authorization": headers}, 46 | data={"grant_type": "client_credentials", "scope": "data:read data:write code:all"} 47 | ) 48 | resp.raise_for_status() 49 | return resp.json()["access_token"] 50 | 51 | async def submit_cube_workitem(edge_mm: float, token: str) -> str: 52 | """ 53 | Launches a Design-Automation workitem that calls the pre-registered 54 | Fusion activity (ACTIVITY_ID) and returns the workitem ID. 55 | The activity script inside Fusion should read the `edge_mm` param, 56 | create a cube, export STL → OSS. [oai_citation:3‡Autodesk Platform Services](https://aps.autodesk.com/en/docs/design-automation/v3/reference/?utm_source=chatgpt.com) 57 | """ 58 | payload = { 59 | "activityId" : ACTIVITY_ID, 60 | "arguments" : { 61 | "edge_mm": { "value": edge_mm }, 62 | # output 'result' will be uploaded to a temp OSS bucket 63 | "result" : { "verb": "put", "url": "urn:adsk.objects:os.object:destination/result.stl" } 64 | } 65 | } 66 | async with httpx.AsyncClient() as client: 67 | resp = await client.post( 68 | WORKITEMS_URL, 69 | headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, 70 | json=payload 71 | ) 72 | resp.raise_for_status() 73 | return resp.json()["id"] # workitem ID 74 | 75 | async def wait_for_workitem(work_id: str, token: str) -> dict: 76 | """ 77 | Polls GET /workitems/{id} until status ∈ {success,failed}. [oai_citation:4‡Autodesk Platform Services](https://aps.autodesk.com/blog/design-automation-get-workitemsid-will-be-enforced-rate-limit-150-rate-minute-rpm?utm_source=chatgpt.com) 78 | Returns the final JSON (includes output URLs if success). 79 | """ 80 | async with httpx.AsyncClient() as client: 81 | url = f"{WORKITEMS_URL}/{work_id}" 82 | while True: 83 | resp = await client.get(url, headers={"Authorization": f"Bearer {token}"}) 84 | resp.raise_for_status() 85 | data = resp.json() 86 | state = data.get("status") 87 | if state in ("success", "failed"): 88 | return data 89 | await asyncio.sleep(POLL_INTERVAL) 90 | 91 | # ────────────────────────────── MCP Tool ─────────────────────────── 92 | @mcp.tool() 93 | async def generate_cube(edge_mm: float = 20.0) -> str: 94 | """ 95 | Create a parametric cube with the given edge length (mm) via 96 | Autodesk Fusion Design Automation and return a signed STL URL. 97 | """ 98 | try: 99 | token = await get_oauth_token() 100 | work_id = await submit_cube_workitem(edge_mm, token) 101 | logging.info(f"WorkItem {work_id} submitted for {edge_mm} mm cube") 102 | result = await wait_for_workitem(work_id, token) 103 | 104 | if result["status"] != "success": 105 | return f"WorkItem failed: {result.get('reportUrl')}" 106 | stl_url = result["arguments"]["result"]["url"] # pre-signed URL 107 | return f"✅ Cube ready: {stl_url}" 108 | except Exception as e: 109 | logging.exception("generate_cube failed") 110 | return f"❌ Error creating cube: {e}" 111 | 112 | # ────────────────────────────── Main ─────────────────────────────── 113 | if __name__ == "__main__": 114 | parser = argparse.ArgumentParser( 115 | description="Fusion MCP Server – headless Fusion automation via APS" 116 | ) 117 | parser.add_argument( 118 | "--transport", 119 | default="stdio", 120 | choices=["stdio", "http"], 121 | help="Transport mechanism for the MCP server (default: stdio)", 122 | ) 123 | args = parser.parse_args() 124 | logging.info(f"Starting Fusion MCP server with transport: {args.transport}") 125 | mcp.run(transport=args.transport) --------------------------------------------------------------------------------