├── resources ├── MCPIcon │ ├── 16x16-normal.png │ └── 32x32-normal.png └── placeholder.txt ├── .gitignore ├── LICENSE ├── README.md ├── client.py ├── server.py └── fusion360_mcp_addin.py /resources/MCPIcon/16x16-normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Joelalbon/Fusion-MCP-Server/HEAD/resources/MCPIcon/16x16-normal.png -------------------------------------------------------------------------------- /resources/MCPIcon/32x32-normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Joelalbon/Fusion-MCP-Server/HEAD/resources/MCPIcon/32x32-normal.png -------------------------------------------------------------------------------- /resources/placeholder.txt: -------------------------------------------------------------------------------- 1 | This is a placeholder file for the resources directory. 2 | You should replace this with actual icon files for the Fusion 360 add-in. 3 | 4 | Place the MCP icons in the `MCPIcon` subdirectory. Include at least 5 | `32x32-normal.png` and `16x16-normal.png` for the add-in to load correctly. 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Distribution / packaging 7 | dist/ 8 | build/ 9 | *.egg-info/ 10 | 11 | # Virtual environments 12 | venv/ 13 | env/ 14 | ENV/ 15 | 16 | # IDE specific files 17 | .idea/ 18 | .vscode/ 19 | *.swp 20 | *.swo 21 | 22 | # Logs 23 | *.log 24 | 25 | # Local configuration 26 | .env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fusion 360 MCP Server 2 | 3 | Master Control Program (MCP) server for Autodesk Fusion 360 that enables remote control and automation of Fusion 360 operations. 4 | 5 | ## Overview 6 | 7 | This project implements a client-server architecture that allows external applications to interact with Fusion 360. The server acts as a bridge between clients and the Fusion 360 API, enabling remote execution of commands and retrieval of model information. 8 | 9 | ## Components 10 | 11 | The project consists of three main components: 12 | 13 | 1. **MCP Server** (`server.py`): A standalone Python server that listens for connections from clients and communicates with Fusion 360. 14 | 15 | 2. **MCP Client** (`client.py`): A Python client library that connects to the MCP server and provides methods for sending commands and receiving responses. 16 | 17 | 3. **Fusion 360 Add-in** (`fusion360_mcp_addin.py`): A Fusion 360 add-in that connects to the MCP server and provides the actual integration with the Fusion 360 API. 18 | 19 | ## Installation 20 | 21 | ### Server and Client 22 | 23 | 1. Clone this repository or copy the files to your desired location. 24 | 2. Make sure you have Python 3.6+ installed. 25 | 3. No additional Python packages are required for basic functionality as the implementation uses only standard library modules. 26 | 4. To enable LLM features, install the optional `openai` package: 27 | 28 | ```bash 29 | pip install openai 30 | ``` 31 | 32 | ### Fusion 360 Add-in 33 | 34 | 1. Copy the entire folder containing `fusion360_mcp_addin.py`, `client.py`, and the `resources` directory to your Fusion 360 **AddIns** directory. Ensure the `resources/MCPIcon` folder contains the required icon files (`32x32-normal.png` and `16x16-normal.png`). 35 | 2. In Fusion 360, open the "Scripts and Add-ins" dialog (press `Shift+S` or find it in the "Design" workspace under "Utilities"). 36 | 3. On the "Add-ins" tab choose **Load from my computer** ("Load from Device" on some versions) and select this folder. Selecting the folder starts the add-in. 37 | 4. Click "Run" or enable "Run on Startup" to have it automatically load when Fusion 360 starts. 38 | 39 | ## Usage 40 | 41 | ### Starting the Server 42 | 43 | 1. Open a command prompt or terminal. 44 | 2. Navigate to the directory containing the server files. 45 | 3. Run the server: 46 | 47 | ``` 48 | python server.py 49 | ``` 50 | 51 | By default, the server will listen on `127.0.0.1:8080`. You can modify the host 52 | and port in the code if needed. Set the `OPENAI_API_KEY` environment variable if 53 | you plan to use the LLM integration. 54 | 55 | ### Connecting Fusion 360 to the Server 56 | 57 | 1. Start Fusion 360 and make sure the MCP add-in is running. 58 | 2. In Fusion 360, look for the "MCP Controls" panel. 59 | 3. Click the "Connect to MCP Server" button. 60 | 4. Enter the server host and port, then click OK. 61 | 5. If successful, you will see a confirmation message. 62 | 63 | ### Using the Client 64 | 65 | You can use the provided client implementation to connect to the server and interact with Fusion 360: 66 | 67 | ```python 68 | from client import MCPClient 69 | 70 | # Create and connect the client 71 | client = MCPClient('127.0.0.1', 8080) 72 | if client.connect(): 73 | # Get model information 74 | client.get_model_info() 75 | 76 | # Execute a Fusion 360 command 77 | client.execute_fusion_command('create_circle', { 78 | 'center': [0, 0, 0], 79 | 'radius': 10 80 | }) 81 | 82 | # Disconnect when done 83 | client.disconnect() 84 | ``` 85 | 86 | ## Protocol 87 | 88 | The server and clients communicate using a simple JSON-based protocol over TCP sockets. Each message is a JSON object with at least a `type` field indicating the message type. 89 | 90 | ### Message Types 91 | 92 | - `fusion_command`: Execute a command in Fusion 360 93 | - `get_model_info`: Request information about the current model 94 | - `command_result`: Response containing the result of a command execution 95 | - `model_info`: Response containing model information 96 | - `llm_request`: Request text generation from the configured LLM 97 | - `llm_result`: Response containing LLM output 98 | 99 | ## Extension 100 | 101 | You can extend the server and add-in to support additional functionality: 102 | 103 | 1. Add new message types to the protocol 104 | 2. Implement handlers for these message types in the server 105 | 3. Add corresponding methods to the client 106 | 4. Implement the actual functionality in the Fusion 360 add-in 107 | 108 | ## License 109 | 110 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 111 | 112 | ## Contributing 113 | 114 | Contributions are welcome! Please feel free to submit a Pull Request. 115 | 116 | ## Notes 117 | - Fusion 360 (2601.1.34)MCP Add-in refuses to install with errors indicating more than one file. 118 | - Create a new add-in (python), edit, copy the fusion360_mcp_addin.py source to the new file, save, open the new file location and copy the client.py and resource folder to that location, restart Fusion 360. 119 | -------------------------------------------------------------------------------- /client.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import json 3 | import threading 4 | import logging 5 | import time 6 | from typing import Dict, Any, Callable, Optional 7 | 8 | # Configure logging 9 | logging.basicConfig( 10 | level=logging.INFO, 11 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 12 | handlers=[logging.StreamHandler()] 13 | ) 14 | logger = logging.getLogger("Fusion360_MCP_Client") 15 | 16 | class MCPClient: 17 | """ 18 | Client for interacting with the Master Control Program server for Fusion 360 19 | """ 20 | 21 | def __init__(self, host: str = '127.0.0.1', port: int = 8080): 22 | self.host = host 23 | self.port = port 24 | self.socket = None 25 | self.connected = False 26 | self.running = False 27 | self.response_handlers: Dict[str, Callable] = {} 28 | self.receive_thread = None 29 | 30 | def connect(self) -> bool: 31 | """Connect to the MCP server""" 32 | try: 33 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 34 | self.socket.connect((self.host, self.port)) 35 | self.connected = True 36 | self.running = True 37 | 38 | # Start the receive thread 39 | self.receive_thread = threading.Thread(target=self.receive_messages) 40 | self.receive_thread.daemon = True 41 | self.receive_thread.start() 42 | 43 | logger.info(f"Connected to MCP server at {self.host}:{self.port}") 44 | return True 45 | 46 | except Exception as e: 47 | logger.error(f"Failed to connect to MCP server: {e}") 48 | return False 49 | 50 | def disconnect(self): 51 | """Disconnect from the MCP server""" 52 | self.running = False 53 | self.connected = False 54 | 55 | if self.socket: 56 | try: 57 | self.socket.close() 58 | except: 59 | pass 60 | 61 | logger.info("Disconnected from MCP server") 62 | 63 | def receive_messages(self): 64 | """Receive and process messages from the server""" 65 | buffer = "" 66 | while self.running and self.connected: 67 | try: 68 | data = self.socket.recv(4096) 69 | if not data: 70 | logger.warning("Connection to MCP server closed") 71 | self.connected = False 72 | break 73 | 74 | buffer += data.decode('utf-8') 75 | while "\n" in buffer: 76 | line, buffer = buffer.split("\n", 1) 77 | if not line.strip(): 78 | continue 79 | response = json.loads(line) 80 | self.handle_response(response) 81 | 82 | except Exception as e: 83 | if self.running: 84 | logger.error(f"Error receiving message: {e}") 85 | self.connected = False 86 | break 87 | 88 | def handle_response(self, response: Dict[str, Any]): 89 | """Handle a response from the server""" 90 | if 'type' not in response: 91 | logger.warning(f"Received response without type: {response}") 92 | return 93 | 94 | response_type = response['type'] 95 | logger.info(f"Received {response_type} response") 96 | 97 | # Call the appropriate handler if registered 98 | if response_type in self.response_handlers: 99 | try: 100 | self.response_handlers[response_type](response) 101 | except Exception as e: 102 | logger.error(f"Error in response handler for {response_type}: {e}") 103 | 104 | def register_handler(self, response_type: str, handler: Callable): 105 | """Register a handler for a specific response type""" 106 | self.response_handlers[response_type] = handler 107 | logger.info(f"Registered handler for {response_type} responses") 108 | 109 | def send_message(self, message: Dict[str, Any]) -> bool: 110 | """Send a message to the MCP server""" 111 | if not self.connected: 112 | logger.error("Cannot send message: Not connected to server") 113 | return False 114 | 115 | try: 116 | data = json.dumps(message) + "\n" 117 | self.socket.sendall(data.encode('utf-8')) 118 | return True 119 | 120 | except Exception as e: 121 | logger.error(f"Error sending message: {e}") 122 | self.connected = False 123 | return False 124 | 125 | def execute_fusion_command(self, command: str, params: Dict[str, Any] = None) -> bool: 126 | """Execute a command in Fusion 360 via the MCP server""" 127 | if params is None: 128 | params = {} 129 | 130 | message = { 131 | 'type': 'fusion_command', 132 | 'command': command, 133 | 'params': params 134 | } 135 | 136 | return self.send_message(message) 137 | 138 | def get_model_info(self) -> bool: 139 | """Request model information from the MCP server""" 140 | message = { 141 | 'type': 'get_model_info' 142 | } 143 | 144 | return self.send_message(message) 145 | 146 | def send_llm_request(self, prompt: str, model: str = 'gpt-3.5-turbo') -> bool: 147 | """Send a prompt to the server to be processed by an LLM""" 148 | message = { 149 | 'type': 'llm_request', 150 | 'prompt': prompt, 151 | 'model': model 152 | } 153 | 154 | return self.send_message(message) 155 | 156 | 157 | # Example usage 158 | if __name__ == "__main__": 159 | # Create and connect the client 160 | client = MCPClient() 161 | 162 | if client.connect(): 163 | try: 164 | # Register response handlers 165 | def command_result_handler(response): 166 | result = response.get('result', {}) 167 | print(f"Command result: {result}") 168 | 169 | def model_info_handler(response): 170 | model_info = response.get('data', {}) 171 | print(f"Model info: {model_info}") 172 | 173 | client.register_handler('command_result', command_result_handler) 174 | client.register_handler('model_info', model_info_handler) 175 | 176 | # Example: Get model info 177 | client.get_model_info() 178 | 179 | # Example: Execute a command 180 | client.execute_fusion_command('create_circle', { 181 | 'center': [0, 0, 0], 182 | 'radius': 10 183 | }) 184 | 185 | # Example: LLM request 186 | client.send_llm_request('Summarize the Fusion 360 model') 187 | 188 | # Keep the client running for a while to receive responses 189 | time.sleep(5) 190 | 191 | finally: 192 | client.disconnect() 193 | else: 194 | print("Failed to connect to MCP server. Make sure it's running.") 195 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import threading 3 | import json 4 | import logging 5 | import os 6 | import time 7 | from typing import Dict, Any, List, Optional 8 | 9 | try: 10 | import openai 11 | except ImportError: # pragma: no cover - optional dependency 12 | openai = None 13 | 14 | # Configure logging 15 | logging.basicConfig( 16 | level=logging.INFO, 17 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 18 | handlers=[logging.StreamHandler()] 19 | ) 20 | logger = logging.getLogger("Fusion360_MCP") 21 | 22 | class MCPServer: 23 | """ 24 | Master Control Program server for Fusion 360 25 | Handles communication between clients and Fusion 360 26 | """ 27 | 28 | def __init__(self, host: str = '127.0.0.1', port: int = 8080): 29 | self.host = host 30 | self.port = port 31 | self.server_socket = None 32 | self.clients: Dict[str, socket.socket] = {} 33 | self.fusion_data: Dict[str, Any] = {} 34 | self.running = False 35 | self.openai_api_key = os.getenv("OPENAI_API_KEY") 36 | 37 | def start(self): 38 | """Start the MCP server""" 39 | self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 40 | self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 41 | 42 | try: 43 | self.server_socket.bind((self.host, self.port)) 44 | self.server_socket.listen(5) 45 | self.running = True 46 | logger.info(f"MCP Server started on {self.host}:{self.port}") 47 | 48 | # Start accepting client connections 49 | accept_thread = threading.Thread(target=self.accept_connections) 50 | accept_thread.daemon = True 51 | accept_thread.start() 52 | 53 | # Main server loop 54 | try: 55 | while self.running: 56 | time.sleep(0.1) 57 | except KeyboardInterrupt: 58 | self.stop() 59 | 60 | except Exception as e: 61 | logger.error(f"Error starting MCP server: {e}") 62 | self.stop() 63 | 64 | def accept_connections(self): 65 | """Accept incoming client connections""" 66 | while self.running: 67 | try: 68 | client_socket, addr = self.server_socket.accept() 69 | logger.info(f"New connection from {addr}") 70 | 71 | # Start a new thread to handle the client 72 | client_thread = threading.Thread( 73 | target=self.handle_client, 74 | args=(client_socket, addr) 75 | ) 76 | client_thread.daemon = True 77 | client_thread.start() 78 | 79 | except Exception as e: 80 | if self.running: 81 | logger.error(f"Error accepting connection: {e}") 82 | 83 | def handle_client(self, client_socket: socket.socket, addr): 84 | """Handle communication with a connected client""" 85 | client_id = f"{addr[0]}:{addr[1]}" 86 | self.clients[client_id] = client_socket 87 | 88 | try: 89 | buffer = "" 90 | while self.running: 91 | data = client_socket.recv(4096) 92 | if not data: 93 | break 94 | 95 | buffer += data.decode('utf-8') 96 | while "\n" in buffer: 97 | line, buffer = buffer.split("\n", 1) 98 | if not line.strip(): 99 | continue 100 | message = json.loads(line) 101 | self.process_message(client_id, message) 102 | 103 | except Exception as e: 104 | logger.error(f"Error handling client {client_id}: {e}") 105 | 106 | finally: 107 | # Clean up when client disconnects 108 | if client_id in self.clients: 109 | del self.clients[client_id] 110 | try: 111 | client_socket.close() 112 | except: 113 | pass 114 | logger.info(f"Client {client_id} disconnected") 115 | 116 | def process_message(self, client_id: str, message: Dict[str, Any]): 117 | """Process a message from a client""" 118 | if 'type' not in message: 119 | self.send_response(client_id, {'status': 'error', 'message': 'Missing message type'}) 120 | return 121 | 122 | msg_type = message['type'] 123 | logger.info(f"Received {msg_type} message from {client_id}") 124 | 125 | if msg_type == 'fusion_command': 126 | # Handle Fusion 360 command 127 | command = message.get('command') 128 | params = message.get('params', {}) 129 | 130 | # TODO: Implement actual Fusion 360 API integration 131 | result = self.execute_fusion_command(command, params) 132 | self.send_response(client_id, { 133 | 'status': 'success', 134 | 'type': 'command_result', 135 | 'command': command, 136 | 'result': result 137 | }) 138 | 139 | elif msg_type == 'get_model_info': 140 | # Return information about the current model 141 | model_info = self.get_model_info() 142 | self.send_response(client_id, { 143 | 'status': 'success', 144 | 'type': 'model_info', 145 | 'data': model_info 146 | }) 147 | 148 | elif msg_type == 'llm_request': 149 | prompt = message.get('prompt', '') 150 | model = message.get('model', 'gpt-3.5-turbo') 151 | result = self.handle_llm_request(prompt, model) 152 | self.send_response(client_id, { 153 | 'status': 'success' if 'error' not in result else 'error', 154 | 'type': 'llm_result', 155 | 'data': result 156 | }) 157 | 158 | else: 159 | self.send_response(client_id, { 160 | 'status': 'error', 161 | 'message': f'Unknown message type: {msg_type}' 162 | }) 163 | 164 | def send_response(self, client_id: str, response: Dict[str, Any]): 165 | """Send a response to a client""" 166 | if client_id not in self.clients: 167 | logger.warning(f"Attempted to send response to unknown client {client_id}") 168 | return 169 | 170 | try: 171 | data = json.dumps(response) + "\n" 172 | self.clients[client_id].sendall(data.encode('utf-8')) 173 | except Exception as e: 174 | logger.error(f"Error sending response to {client_id}: {e}") 175 | 176 | def execute_fusion_command(self, command: str, params: Dict[str, Any]) -> Dict[str, Any]: 177 | """Execute a command in Fusion 360""" 178 | # This is a placeholder. In a real implementation, this would 179 | # interact with the Fusion 360 API to execute commands 180 | logger.info(f"Executing Fusion command: {command} with params: {params}") 181 | 182 | # Mock response for demonstration 183 | return { 184 | 'command': command, 185 | 'executed': True, 186 | 'message': f"Command {command} executed successfully" 187 | } 188 | 189 | def get_model_info(self) -> Dict[str, Any]: 190 | """Get information about the current model in Fusion 360""" 191 | # This is a placeholder. In a real implementation, this would 192 | # retrieve actual model data from Fusion 360 193 | return { 194 | 'name': 'Example Model', 195 | 'version': '1.0', 196 | 'components': [ 197 | {'id': 'comp1', 'name': 'Component 1'}, 198 | {'id': 'comp2', 'name': 'Component 2'} 199 | ] 200 | } 201 | 202 | def handle_llm_request(self, prompt: str, model: str) -> Dict[str, Any]: 203 | """Send a prompt to an LLM and return the response""" 204 | if openai is None: 205 | return {'error': 'openai package not installed'} 206 | 207 | if not self.openai_api_key: 208 | return {'error': 'OPENAI_API_KEY not configured'} 209 | 210 | try: 211 | openai.api_key = self.openai_api_key 212 | response = openai.ChatCompletion.create( 213 | model=model, 214 | messages=[{'role': 'user', 'content': prompt}] 215 | ) 216 | content = response['choices'][0]['message']['content'] 217 | return {'response': content} 218 | except Exception as exc: 219 | logger.error(f"LLM request failed: {exc}") 220 | return {'error': str(exc)} 221 | 222 | def stop(self): 223 | """Stop the MCP server""" 224 | self.running = False 225 | 226 | # Close all client connections 227 | for client_id, client_socket in self.clients.items(): 228 | try: 229 | client_socket.close() 230 | except: 231 | pass 232 | self.clients.clear() 233 | 234 | # Close server socket 235 | if self.server_socket: 236 | try: 237 | self.server_socket.close() 238 | except: 239 | pass 240 | 241 | logger.info("MCP Server stopped") 242 | 243 | 244 | if __name__ == "__main__": 245 | # Create and start the MCP server 246 | server = MCPServer() 247 | server.start() -------------------------------------------------------------------------------- /fusion360_mcp_addin.py: -------------------------------------------------------------------------------- 1 | import adsk.core 2 | import adsk.fusion 3 | import adsk.cam 4 | import traceback 5 | import json 6 | import threading 7 | import time 8 | import os 9 | import sys 10 | 11 | # Add client module directory to path 12 | script_dir = os.path.dirname(os.path.abspath(__file__)) 13 | sys.path.append(script_dir) 14 | 15 | # Import our MCP client 16 | from client import MCPClient 17 | 18 | # Global variables used to maintain the references to various event handlers 19 | handlers = [] 20 | app = None 21 | ui = None 22 | client = None 23 | stop_flag = False 24 | 25 | # Event handler for the commandExecuted event 26 | class CommandExecutedHandler(adsk.core.CommandEventHandler): 27 | def __init__(self): 28 | super().__init__() 29 | 30 | def notify(self, args): 31 | try: 32 | # Get the command that was executed 33 | eventArgs = adsk.core.CommandEventArgs.cast(args) 34 | command = eventArgs.command 35 | 36 | if client and client.connected: 37 | # Send the command info to the MCP server 38 | client.execute_fusion_command('command_executed', { 39 | 'command_id': command.id, 40 | 'command_name': command.commandDefinition.name 41 | }) 42 | 43 | except: 44 | if ui: 45 | ui.messageBox('Command executed event failed:\n{}'.format(traceback.format_exc())) 46 | 47 | # Event handler for the documentOpened event 48 | class DocumentOpenedHandler(adsk.core.DocumentEventHandler): 49 | def __init__(self): 50 | super().__init__() 51 | 52 | def notify(self, args): 53 | try: 54 | # Get the document that was opened 55 | eventArgs = adsk.core.DocumentEventArgs.cast(args) 56 | doc = eventArgs.document 57 | 58 | if client and client.connected: 59 | # Send document info to the MCP server 60 | client.execute_fusion_command('document_opened', { 61 | 'document_name': doc.name, 62 | 'document_path': doc.path 63 | }) 64 | 65 | except: 66 | if ui: 67 | ui.messageBox('Document opened event failed:\n{}'.format(traceback.format_exc())) 68 | 69 | # Command handler for the MCP connection command 70 | class MCPConnectionCommandCreatedHandler(adsk.core.CommandCreatedEventHandler): 71 | def __init__(self): 72 | super().__init__() 73 | 74 | def notify(self, args): 75 | try: 76 | global client 77 | 78 | # Get the command 79 | cmd = adsk.core.Command.cast(args.command) 80 | 81 | # Get the CommandInputs collection 82 | inputs = cmd.commandInputs 83 | 84 | # Create server settings inputs 85 | hostInput = inputs.addStringValueInput('hostInput', 'Server Host', '127.0.0.1') 86 | portInput = inputs.addStringValueInput('portInput', 'Server Port', '8080') 87 | 88 | # Connect to server button 89 | connectBtn = inputs.addBoolValueInput('connectBtn', 'Connect to Server', False) 90 | 91 | # Add handlers for the execute and destroy events 92 | onExecute = MCPConnectionCommandExecuteHandler() 93 | cmd.execute.add(onExecute) 94 | handlers.append(onExecute) 95 | 96 | onDestroy = MCPConnectionCommandDestroyHandler() 97 | cmd.destroy.add(onDestroy) 98 | handlers.append(onDestroy) 99 | 100 | except: 101 | if ui: 102 | ui.messageBox('Command created failed:\n{}'.format(traceback.format_exc())) 103 | 104 | # Command executed handler for the MCP connection command 105 | class MCPConnectionCommandExecuteHandler(adsk.core.CommandEventHandler): 106 | def __init__(self): 107 | super().__init__() 108 | 109 | def notify(self, args): 110 | try: 111 | global client 112 | 113 | # Get the command 114 | cmd = args.command 115 | 116 | # Get the inputs 117 | inputs = cmd.commandInputs 118 | hostInput = inputs.itemById('hostInput') 119 | portInput = inputs.itemById('portInput') 120 | 121 | # Get the values 122 | host = hostInput.value 123 | port = int(portInput.value) 124 | 125 | # Create and connect the client 126 | client = MCPClient(host, port) 127 | if client.connect(): 128 | ui.messageBox(f'Successfully connected to MCP server at {host}:{port}') 129 | 130 | # Register response handlers 131 | def command_result_handler(response): 132 | result = response.get('result', {}) 133 | app.log(f"MCP Command result: {result}") 134 | 135 | client.register_handler('command_result', command_result_handler) 136 | 137 | # Test the connection 138 | client.get_model_info() 139 | 140 | else: 141 | ui.messageBox(f'Failed to connect to MCP server at {host}:{port}') 142 | client = None 143 | 144 | except: 145 | if ui: 146 | ui.messageBox('Command execute failed:\n{}'.format(traceback.format_exc())) 147 | 148 | # Command destroy handler for the MCP connection command 149 | class MCPConnectionCommandDestroyHandler(adsk.core.CommandEventHandler): 150 | def __init__(self): 151 | super().__init__() 152 | 153 | def notify(self, args): 154 | try: 155 | # Clean up any resources 156 | pass 157 | except: 158 | if ui: 159 | ui.messageBox('Command destroy failed:\n{}'.format(traceback.format_exc())) 160 | 161 | # Worker function for the client communication thread 162 | def client_worker(): 163 | global stop_flag, client 164 | 165 | while not stop_flag: 166 | # Keep the thread alive but don't consume too much CPU 167 | time.sleep(0.1) 168 | 169 | # Disconnect the client when stopping 170 | if client and client.connected: 171 | client.disconnect() 172 | client = None 173 | 174 | def run(context): 175 | global handlers 176 | global app 177 | global ui 178 | global client 179 | global stop_flag 180 | 181 | try: 182 | # Initialize Fusion 360 API 183 | app = adsk.core.Application.get() 184 | ui = app.userInterface 185 | 186 | # Create a new command 187 | mcpWorkspace = ui.workspaces.itemById('FusionSolidEnvironment') 188 | tbPanels = mcpWorkspace.toolbarPanels 189 | mcpPanel = tbPanels.add('MCPPanel', 'MCP Controls') 190 | 191 | ### './resources/MCPIcon' is optional and unless commented out 192 | ### in original code, will cause errors 193 | # Add the command 194 | controls = mcpPanel.controls 195 | mcpCommandDef = ui.commandDefinitions.addButtonDefinition( 196 | 'MCPConnectionBtn', 197 | 'Connect to MCP Server', 198 | 'Connect to the Fusion 360 Master Control Program server', 199 | './resources/MCPIcon' 200 | ) 201 | 202 | # Add handlers for the command 203 | onCommandCreated = MCPConnectionCommandCreatedHandler() 204 | mcpCommandDef.commandCreated.add(onCommandCreated) 205 | handlers.append(onCommandCreated) 206 | 207 | # Add the button to the panel 208 | mcpButton = controls.addCommand(mcpCommandDef) 209 | mcpButton.isPromoted = True 210 | mcpButton.isPromotedByDefault = True 211 | 212 | ### commandExecuted.add causes errors (C++ only ?) 213 | # Add event handlers for Fusion 360 events 214 | # cmdExecutedHandler = CommandExecutedHandler() 215 | # app.userInterface.commandExecuted.add(cmdExecutedHandler) 216 | # mcpCommandDef. .execute. .add(cmdExecutedHandler) 217 | # handlers.append(cmdExecutedHandler) 218 | 219 | docOpenedHandler = DocumentOpenedHandler() 220 | app.documentOpened.add(docOpenedHandler) 221 | handlers.append(docOpenedHandler) 222 | 223 | # Start client worker thread 224 | stop_flag = False 225 | client_thread = threading.Thread(target=client_worker) 226 | client_thread.daemon = True 227 | client_thread.start() 228 | 229 | ui.messageBox('MCP Add-in Started') 230 | 231 | except: 232 | if ui: 233 | ui.messageBox('Failed:\n{}'.format(traceback.format_exc())) 234 | 235 | def stop(context): 236 | global handlers 237 | global app 238 | global ui 239 | global stop_flag 240 | 241 | try: 242 | # Signal the client thread to stop 243 | stop_flag = True 244 | 245 | # Clean up command definitions 246 | ui.commandDefinitions.itemById('MCPConnectionBtn').deleteMe() 247 | 248 | # Clean up the panel 249 | mcpWorkspace = ui.workspaces.itemById('FusionSolidEnvironment') 250 | panel = mcpWorkspace.toolbarPanels.itemById('MCPPanel') 251 | if panel: 252 | panel.deleteMe() 253 | 254 | # Clean up event handlers 255 | handlers = [] 256 | 257 | ui.messageBox('MCP Add-in Stopped') 258 | 259 | except: 260 | if ui: 261 | ui.messageBox('Failed:\n{}'.format(traceback.format_exc())) --------------------------------------------------------------------------------