unity-mcp/Python/tools/editor_tools.py

269 lines
11 KiB
Python

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": ""
}]