Unity MCP Server

by justinpbarnett
Verified
from mcp.server.fastmcp import FastMCP, Context from typing import Optional, List, Dict, Any from unity_connection import get_unity_connection def register_editor_tools(mcp: FastMCP): """Register all editor control tools with the MCP server.""" @mcp.tool() def undo(ctx: Context) -> str: """Undo the last action performed in the Unity editor. Returns: str: Success message or error details """ try: response = get_unity_connection().send_command("EDITOR_CONTROL", { "command": "UNDO" }) return response.get("message", "Undo performed successfully") except Exception as e: return f"Error performing undo: {str(e)}" @mcp.tool() def redo(ctx: Context) -> str: """Redo the last undone action in the Unity editor. Returns: str: Success message or error details """ try: response = get_unity_connection().send_command("EDITOR_CONTROL", { "command": "REDO" }) return response.get("message", "Redo performed successfully") except Exception as e: return f"Error performing redo: {str(e)}" @mcp.tool() def play(ctx: Context) -> str: """Start the game in play mode within the Unity editor. Returns: str: Success message or error details """ try: response = get_unity_connection().send_command("EDITOR_CONTROL", { "command": "PLAY" }) return response.get("message", "Entered play mode") except Exception as e: return f"Error entering play mode: {str(e)}" @mcp.tool() def pause(ctx: Context) -> str: """Pause the game while in play mode. Returns: str: Success message or error details """ try: response = get_unity_connection().send_command("EDITOR_CONTROL", { "command": "PAUSE" }) return response.get("message", "Game paused") except Exception as e: return f"Error pausing game: {str(e)}" @mcp.tool() def stop(ctx: Context) -> str: """Stop the game and exit play mode. Returns: str: Success message or error details """ try: response = get_unity_connection().send_command("EDITOR_CONTROL", { "command": "STOP" }) return response.get("message", "Exited play mode") except Exception as e: return f"Error stopping game: {str(e)}" @mcp.tool() def build(ctx: Context, platform: str, build_path: str) -> str: """Build the project for a specified platform. Args: platform: Target platform (windows, mac, linux, android, ios, webgl) build_path: Path where the build should be saved Returns: str: Success message or error details """ try: # Validate platform valid_platforms = ["windows", "mac", "linux", "android", "ios", "webgl"] if platform.lower() not in valid_platforms: return f"Error: '{platform}' is not a valid platform. Valid platforms are: {', '.join(valid_platforms)}" # Check if build_path exists and is writable import os # Check if the directory exists build_dir = os.path.dirname(build_path) if not os.path.exists(build_dir): return f"Error: Build directory '{build_dir}' does not exist. Please create it first." # Check if the directory is writable if not os.access(build_dir, os.W_OK): return f"Error: Build directory '{build_dir}' is not writable." # If the build path itself exists, check if it's a file or directory if os.path.exists(build_path): if os.path.isfile(build_path): # If it's a file, check if it's writable if not os.access(build_path, os.W_OK): return f"Error: Existing build file '{build_path}' is not writable." elif os.path.isdir(build_path): # If it's a directory, check if it's writable if not os.access(build_path, os.W_OK): return f"Error: Existing build directory '{build_path}' is not writable." response = get_unity_connection().send_command("EDITOR_CONTROL", { "command": "BUILD", "params": { "platform": platform, "buildPath": build_path } }) return response.get("message", "Build completed successfully") except Exception as e: return f"Error building project: {str(e)}" @mcp.tool() def execute_command(ctx: Context, command_name: str, validate_command: bool = True) -> str: """Execute a specific editor command or custom script within the Unity editor. Args: command_name: Name of the editor command to execute (e.g., "Edit/Preferences") validate_command: Whether to validate the command existence before executing (default: True) Returns: str: Success message or error details """ try: unity = get_unity_connection() # Optionally validate if the command exists if validate_command: # Get a list of available commands from Unity available_commands = unity.send_command("EDITOR_CONTROL", { "command": "GET_AVAILABLE_COMMANDS" }).get("commands", []) # Check if the command exists in the list if available_commands and command_name not in available_commands: # If command doesn't exist, try to find similar commands as suggestions similar_commands = [cmd for cmd in available_commands if command_name.lower() in cmd.lower()] suggestion_msg = "" if similar_commands: suggestion_msg = f" Did you mean one of these: {', '.join(similar_commands[:5])}" if len(similar_commands) > 5: suggestion_msg += " or others?" else: suggestion_msg += "?" return f"Error: Command '{command_name}' not found.{suggestion_msg}" response = unity.send_command("EDITOR_CONTROL", { "command": "EXECUTE_COMMAND", "params": { "commandName": command_name } }) return response.get("message", f"Executed command: {command_name}") except Exception as e: return f"Error executing command: {str(e)}" @mcp.tool() def read_console( ctx: Context, show_logs: bool = True, show_warnings: bool = True, show_errors: bool = True, search_term: Optional[str] = None ) -> List[Dict[str, Any]]: """Read log messages from the Unity Console. Args: ctx: The MCP context show_logs: Whether to include regular log messages (default: True) show_warnings: Whether to include warning messages (default: True) show_errors: Whether to include error messages (default: True) search_term: Optional text to filter logs by content. If multiple words are provided, entries must contain all words (not necessarily in order) to be included. (default: None) Returns: List[Dict[str, Any]]: A list of console log entries, each containing 'type', 'message', and 'stackTrace' fields """ try: # Prepare params with only the provided values params = { "show_logs": show_logs, "show_warnings": show_warnings, "show_errors": show_errors } # Only add search_term if it's provided if search_term is not None: params["search_term"] = search_term response = get_unity_connection().send_command("EDITOR_CONTROL", { "command": "READ_CONSOLE", "params": params }) if "error" in response: return [{ "type": "Error", "message": f"Failed to read console: {response['error']}", "stackTrace": response.get("stackTrace", "") }] entries = response.get("entries", []) total_entries = response.get("total_entries", 0) filtered_count = response.get("filtered_count", 0) filter_applied = response.get("filter_applied", False) # Add summary info summary = [] if total_entries > 0: summary.append(f"Total console entries: {total_entries}") if filter_applied: summary.append(f"Filtered entries: {filtered_count}") if filtered_count == 0: summary.append(f"No entries matched the search term: '{search_term}'") else: summary.append(f"Showing all entries") else: summary.append("No entries in console") # Add filter info filter_types = [] if show_logs: filter_types.append("logs") if show_warnings: filter_types.append("warnings") if show_errors: filter_types.append("errors") if filter_types: summary.append(f"Showing: {', '.join(filter_types)}") # Add summary as first entry if summary: entries.insert(0, { "type": "Info", "message": " | ".join(summary), "stackTrace": "" }) return entries if entries else [{ "type": "Info", "message": "No logs found in console", "stackTrace": "" }] except Exception as e: return [{ "type": "Error", "message": f"Error reading console: {str(e)}", "stackTrace": "" }] @mcp.tool() def get_available_commands(ctx: Context) -> List[str]: """Get a list of all available editor commands that can be executed. This tool provides direct access to the list of commands that can be executed in the Unity Editor through the MCP system. Returns: List[str]: List of available command paths """ try: unity = get_unity_connection() # Send request for available commands response = unity.send_command("EDITOR_CONTROL", { "command": "GET_AVAILABLE_COMMANDS" }) # Extract commands list commands = response.get("commands", []) # Return the commands list return commands except Exception as e: return [f"Error fetching commands: {str(e)}"]