new tools

main
Justin Barnett 2025-03-30 15:58:01 -04:00
parent 13508f2e56
commit 8d86cada1c
64 changed files with 4515 additions and 4112 deletions

3
.gitignore vendored
View File

@ -24,4 +24,5 @@ wheels/
# IDE # IDE
.idea/ .idea/
.vscode/ .vscode/
.aider*

View File

@ -1,232 +0,0 @@
using UnityEngine;
using UnityEditor;
using System.IO;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
namespace UnityMCP.Editor.Commands
{
/// <summary>
/// Handles asset-related commands for the MCP Server
/// </summary>
public static class AssetCommandHandler
{
/// <summary>
/// Imports an asset into the project
/// </summary>
public static object ImportAsset(JObject @params)
{
try
{
string sourcePath = (string)@params["source_path"];
string targetPath = (string)@params["target_path"];
if (string.IsNullOrEmpty(sourcePath))
return new { success = false, error = "Source path cannot be empty" };
if (string.IsNullOrEmpty(targetPath))
return new { success = false, error = "Target path cannot be empty" };
if (!File.Exists(sourcePath))
return new { success = false, error = $"Source file not found: {sourcePath}" };
// Ensure target directory exists
string targetDir = Path.GetDirectoryName(targetPath);
if (!Directory.Exists(targetDir))
{
Directory.CreateDirectory(targetDir);
}
// Copy file to target location
File.Copy(sourcePath, targetPath, true);
AssetDatabase.Refresh();
return new
{
success = true,
message = $"Successfully imported asset to {targetPath}",
path = targetPath
};
}
catch (System.Exception e)
{
return new
{
success = false,
error = $"Failed to import asset: {e.Message}",
stackTrace = e.StackTrace
};
}
}
/// <summary>
/// Instantiates a prefab in the current scene
/// </summary>
public static object InstantiatePrefab(JObject @params)
{
try
{
string prefabPath = (string)@params["prefab_path"];
if (string.IsNullOrEmpty(prefabPath))
return new { success = false, error = "Prefab path cannot be empty" };
Vector3 position = new(
(float)@params["position_x"],
(float)@params["position_y"],
(float)@params["position_z"]
);
Vector3 rotation = new(
(float)@params["rotation_x"],
(float)@params["rotation_y"],
(float)@params["rotation_z"]
);
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
if (prefab == null)
{
return new { success = false, error = $"Prefab not found at path: {prefabPath}" };
}
GameObject instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
if (instance == null)
{
return new { success = false, error = $"Failed to instantiate prefab: {prefabPath}" };
}
instance.transform.position = position;
instance.transform.rotation = Quaternion.Euler(rotation);
return new
{
success = true,
message = "Successfully instantiated prefab",
instance_name = instance.name
};
}
catch (System.Exception e)
{
return new
{
success = false,
error = $"Failed to instantiate prefab: {e.Message}",
stackTrace = e.StackTrace
};
}
}
/// <summary>
/// Creates a new prefab from a GameObject in the scene
/// </summary>
public static object CreatePrefab(JObject @params)
{
try
{
string objectName = (string)@params["object_name"];
string prefabPath = (string)@params["prefab_path"];
if (string.IsNullOrEmpty(objectName))
return new { success = false, error = "GameObject name cannot be empty" };
if (string.IsNullOrEmpty(prefabPath))
return new { success = false, error = "Prefab path cannot be empty" };
// Ensure prefab has .prefab extension
if (!prefabPath.ToLower().EndsWith(".prefab"))
prefabPath = $"{prefabPath}.prefab";
GameObject sourceObject = GameObject.Find(objectName);
if (sourceObject == null)
{
return new { success = false, error = $"GameObject not found in scene: {objectName}" };
}
// Ensure target directory exists
string targetDir = Path.GetDirectoryName(prefabPath);
if (!Directory.Exists(targetDir))
{
Directory.CreateDirectory(targetDir);
}
GameObject prefab = PrefabUtility.SaveAsPrefabAsset(sourceObject, prefabPath);
if (prefab == null)
{
return new { success = false, error = "Failed to create prefab. Verify the path is writable." };
}
return new
{
success = true,
message = $"Successfully created prefab at {prefabPath}",
path = prefabPath
};
}
catch (System.Exception e)
{
return new
{
success = false,
error = $"Failed to create prefab: {e.Message}",
stackTrace = e.StackTrace,
sourceInfo = $"Object: {@params["object_name"]}, Path: {@params["prefab_path"]}"
};
}
}
/// <summary>
/// Applies changes from a prefab instance back to the original prefab asset
/// </summary>
public static object ApplyPrefab(JObject @params)
{
string objectName = (string)@params["object_name"];
GameObject instance = GameObject.Find(objectName);
if (instance == null)
{
return new { error = $"GameObject not found in scene: {objectName}" };
}
Object prefabAsset = PrefabUtility.GetCorrespondingObjectFromSource(instance);
if (prefabAsset == null)
{
return new { error = "Selected object is not a prefab instance" };
}
PrefabUtility.ApplyPrefabInstance(instance, InteractionMode.AutomatedAction);
return new { message = "Successfully applied changes to prefab asset" };
}
/// <summary>
/// Gets a list of assets in the project, optionally filtered by type
/// </summary>
public static object GetAssetList(JObject @params)
{
string type = (string)@params["type"];
string searchPattern = (string)@params["search_pattern"] ?? "*";
string folder = (string)@params["folder"] ?? "Assets";
var guids = AssetDatabase.FindAssets(searchPattern, new[] { folder });
var assets = new List<object>();
foreach (var guid in guids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var assetType = AssetDatabase.GetMainAssetTypeAtPath(path);
// Skip if type filter is specified and doesn't match
if (!string.IsNullOrEmpty(type) && assetType?.Name != type)
continue;
assets.Add(new
{
name = Path.GetFileNameWithoutExtension(path),
path,
type = assetType?.Name ?? "Unknown",
guid
});
}
return new { assets };
}
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 72d89b7645a23af4bb8bf1deda8f2b36

View File

@ -1,51 +0,0 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
namespace UnityMCP.Editor.Commands
{
/// <summary>
/// Registry for all MCP command handlers
/// </summary>
public static class CommandRegistry
{
private static readonly Dictionary<string, Func<JObject, object>> _handlers = new()
{
// Scene management commands
{ "GET_SCENE_INFO", _ => SceneCommandHandler.GetSceneInfo() },
{ "OPEN_SCENE", parameters => SceneCommandHandler.OpenScene(parameters) },
{ "SAVE_SCENE", _ => SceneCommandHandler.SaveScene() },
{ "NEW_SCENE", parameters => SceneCommandHandler.NewScene(parameters) },
{ "CHANGE_SCENE", parameters => SceneCommandHandler.ChangeScene(parameters) },
// Asset management commands
{ "IMPORT_ASSET", parameters => AssetCommandHandler.ImportAsset(parameters) },
{ "INSTANTIATE_PREFAB", parameters => AssetCommandHandler.InstantiatePrefab(parameters) },
{ "CREATE_PREFAB", parameters => AssetCommandHandler.CreatePrefab(parameters) },
{ "APPLY_PREFAB", parameters => AssetCommandHandler.ApplyPrefab(parameters) },
{ "GET_ASSET_LIST", parameters => AssetCommandHandler.GetAssetList(parameters) },
// Object management commands
{ "GET_OBJECT_PROPERTIES", parameters => ObjectCommandHandler.GetObjectProperties(parameters) },
{ "GET_COMPONENT_PROPERTIES", parameters => ObjectCommandHandler.GetComponentProperties(parameters) },
{ "FIND_OBJECTS_BY_NAME", parameters => ObjectCommandHandler.FindObjectsByName(parameters) },
{ "FIND_OBJECTS_BY_TAG", parameters => ObjectCommandHandler.FindObjectsByTag(parameters) },
{ "GET_HIERARCHY", _ => ObjectCommandHandler.GetHierarchy() },
{ "SELECT_OBJECT", parameters => ObjectCommandHandler.SelectObject(parameters) },
{ "GET_SELECTED_OBJECT", _ => ObjectCommandHandler.GetSelectedObject() },
// Editor control commands
{ "EDITOR_CONTROL", parameters => EditorControlHandler.HandleEditorControl(parameters) }
};
/// <summary>
/// Gets a command handler by name
/// </summary>
/// <param name="commandName">Name of the command to get</param>
/// <returns>The command handler function if found, null otherwise</returns>
public static Func<JObject, object> GetHandler(string commandName)
{
return _handlers.TryGetValue(commandName, out var handler) ? handler : null;
}
}
}

View File

@ -1,950 +0,0 @@
using UnityEngine;
using UnityEditor;
using UnityEditor.Build.Reporting;
using Newtonsoft.Json.Linq;
using System;
using System.Reflection;
using System.Collections.Generic;
using System.Linq;
namespace UnityMCP.Editor.Commands
{
/// <summary>
/// Handles editor control commands like undo, redo, play, pause, stop, and build operations.
/// </summary>
public static class EditorControlHandler
{
/// <summary>
/// Handles editor control commands
/// </summary>
public static object HandleEditorControl(JObject @params)
{
string command = (string)@params["command"];
JObject commandParams = (JObject)@params["params"];
return command.ToUpper() switch
{
"UNDO" => HandleUndo(),
"REDO" => HandleRedo(),
"PLAY" => HandlePlay(),
"PAUSE" => HandlePause(),
"STOP" => HandleStop(),
"BUILD" => HandleBuild(commandParams),
"EXECUTE_COMMAND" => HandleExecuteCommand(commandParams),
"READ_CONSOLE" => ReadConsole(commandParams),
"GET_AVAILABLE_COMMANDS" => GetAvailableCommands(),
_ => new { error = $"Unknown editor control command: {command}" },
};
}
private static object HandleUndo()
{
Undo.PerformUndo();
return new { message = "Undo performed successfully" };
}
private static object HandleRedo()
{
Undo.PerformRedo();
return new { message = "Redo performed successfully" };
}
private static object HandlePlay()
{
if (!EditorApplication.isPlaying)
{
EditorApplication.isPlaying = true;
return new { message = "Entered play mode" };
}
return new { message = "Already in play mode" };
}
private static object HandlePause()
{
if (EditorApplication.isPlaying)
{
EditorApplication.isPaused = !EditorApplication.isPaused;
return new { message = EditorApplication.isPaused ? "Game paused" : "Game resumed" };
}
return new { message = "Not in play mode" };
}
private static object HandleStop()
{
if (EditorApplication.isPlaying)
{
EditorApplication.isPlaying = false;
return new { message = "Exited play mode" };
}
return new { message = "Not in play mode" };
}
private static object HandleBuild(JObject @params)
{
string platform = (string)@params["platform"];
string buildPath = (string)@params["buildPath"];
try
{
BuildTarget target = GetBuildTarget(platform);
if ((int)target == -1)
{
return new { error = $"Unsupported platform: {platform}" };
}
BuildPlayerOptions buildPlayerOptions = new()
{
scenes = GetEnabledScenes(),
target = target,
locationPathName = buildPath
};
BuildReport report = BuildPipeline.BuildPlayer(buildPlayerOptions);
return new
{
message = "Build completed successfully",
report.summary
};
}
catch (Exception e)
{
return new { error = $"Build failed: {e.Message}" };
}
}
private static object HandleExecuteCommand(JObject @params)
{
string commandName = (string)@params["commandName"];
try
{
EditorApplication.ExecuteMenuItem(commandName);
return new { message = $"Executed command: {commandName}" };
}
catch (Exception e)
{
return new { error = $"Failed to execute command: {e.Message}" };
}
}
/// <summary>
/// Reads log messages from the Unity Console
/// </summary>
/// <param name="params">Parameters containing filtering options</param>
/// <returns>Object containing console messages filtered by type</returns>
public static object ReadConsole(JObject @params)
{
// Default values for show flags
bool showLogs = true;
bool showWarnings = true;
bool showErrors = true;
string searchTerm = string.Empty;
// Get filter parameters if provided
if (@params != null)
{
if (@params["show_logs"] != null) showLogs = (bool)@params["show_logs"];
if (@params["show_warnings"] != null) showWarnings = (bool)@params["show_warnings"];
if (@params["show_errors"] != null) showErrors = (bool)@params["show_errors"];
if (@params["search_term"] != null) searchTerm = (string)@params["search_term"];
}
try
{
// Get required types and methods via reflection
Type logEntriesType = Type.GetType("UnityEditor.LogEntries,UnityEditor");
Type logEntryType = Type.GetType("UnityEditor.LogEntry,UnityEditor");
if (logEntriesType == null || logEntryType == null)
return new { error = "Could not find required Unity logging types", entries = new List<object>() };
// Get essential methods
MethodInfo getCountMethod = logEntriesType.GetMethod("GetCount", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
MethodInfo getEntryMethod = logEntriesType.GetMethod("GetEntryAt", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) ??
logEntriesType.GetMethod("GetEntryInternal", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
if (getCountMethod == null || getEntryMethod == null)
return new { error = "Could not find required Unity logging methods", entries = new List<object>() };
// Get stack trace method if available
MethodInfo getStackTraceMethod = logEntriesType.GetMethod("GetEntryStackTrace", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
null, new[] { typeof(int) }, null) ?? logEntriesType.GetMethod("GetStackTrace", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
null, new[] { typeof(int) }, null);
// Get entry count and prepare result list
int count = (int)getCountMethod.Invoke(null, null);
var entries = new List<object>();
// Create LogEntry instance to populate
object logEntryInstance = Activator.CreateInstance(logEntryType);
// Find properties on LogEntry type
PropertyInfo modeProperty = logEntryType.GetProperty("mode") ?? logEntryType.GetProperty("Mode");
PropertyInfo messageProperty = logEntryType.GetProperty("message") ?? logEntryType.GetProperty("Message");
// Parse search terms if provided
string[] searchWords = !string.IsNullOrWhiteSpace(searchTerm) ?
searchTerm.ToLower().Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries) : null;
// Process each log entry
for (int i = 0; i < count; i++)
{
try
{
// Get log entry at index i
var methodParams = getEntryMethod.GetParameters();
if (methodParams.Length == 2 && methodParams[1].ParameterType == logEntryType)
{
getEntryMethod.Invoke(null, new object[] { i, logEntryInstance });
}
else if (methodParams.Length >= 1 && methodParams[0].ParameterType == typeof(int))
{
var parameters = new object[methodParams.Length];
parameters[0] = i;
for (int p = 1; p < parameters.Length; p++)
{
parameters[p] = methodParams[p].ParameterType.IsValueType ?
Activator.CreateInstance(methodParams[p].ParameterType) : null;
}
getEntryMethod.Invoke(null, parameters);
}
else continue;
// Extract log data
int logType = modeProperty != null ?
Convert.ToInt32(modeProperty.GetValue(logEntryInstance) ?? 0) : 0;
string message = messageProperty != null ?
(messageProperty.GetValue(logEntryInstance)?.ToString() ?? "") : "";
// If message is empty, try to get it via a field
if (string.IsNullOrEmpty(message))
{
var msgField = logEntryType.GetField("message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (msgField != null)
{
object msgValue = msgField.GetValue(logEntryInstance);
message = msgValue != null ? msgValue.ToString() : "";
}
// If still empty, try alternate approach with Console window
if (string.IsNullOrEmpty(message))
{
// Access ConsoleWindow and its data
Type consoleWindowType = Type.GetType("UnityEditor.ConsoleWindow,UnityEditor");
if (consoleWindowType != null)
{
try
{
// Get Console window instance
var getWindowMethod = consoleWindowType.GetMethod("GetWindow",
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
null, new[] { typeof(bool) }, null) ??
consoleWindowType.GetMethod("GetConsoleWindow",
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
if (getWindowMethod != null)
{
object consoleWindow = getWindowMethod.Invoke(null,
getWindowMethod.GetParameters().Length > 0 ? new object[] { false } : null);
if (consoleWindow != null)
{
// Try to find log entries collection
foreach (var prop in consoleWindowType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
if (prop.PropertyType.IsArray ||
(prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>)))
{
try
{
var logItems = prop.GetValue(consoleWindow);
if (logItems != null)
{
if (logItems.GetType().IsArray && i < ((Array)logItems).Length)
{
var entry = ((Array)logItems).GetValue(i);
if (entry != null)
{
var entryType = entry.GetType();
var entryMessageProp = entryType.GetProperty("message") ??
entryType.GetProperty("Message");
if (entryMessageProp != null)
{
object value = entryMessageProp.GetValue(entry);
if (value != null)
{
message = value.ToString();
break;
}
}
}
}
}
}
catch
{
// Ignore errors in this fallback approach
}
}
}
}
}
}
catch
{
// Ignore errors in this fallback approach
}
}
}
// If still empty, try one more approach with log files
if (string.IsNullOrEmpty(message))
{
// This is our last resort - try to get log messages from the most recent Unity log file
try
{
string logPath = string.Empty;
// Determine the log file path based on the platform
if (Application.platform == RuntimePlatform.WindowsEditor)
{
logPath = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Unity", "Editor", "Editor.log");
}
else if (Application.platform == RuntimePlatform.OSXEditor)
{
logPath = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.Personal),
"Library", "Logs", "Unity", "Editor.log");
}
else if (Application.platform == RuntimePlatform.LinuxEditor)
{
logPath = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.Personal),
".config", "unity3d", "logs", "Editor.log");
}
if (!string.IsNullOrEmpty(logPath) && System.IO.File.Exists(logPath))
{
// Read last few lines from the log file
var logLines = ReadLastLines(logPath, 100);
if (logLines.Count > i)
{
message = logLines[logLines.Count - 1 - i];
}
}
}
catch
{
// Ignore errors in this fallback approach
}
}
}
// Get stack trace if method available
string stackTrace = "";
if (getStackTraceMethod != null)
{
stackTrace = getStackTraceMethod.Invoke(null, new object[] { i })?.ToString() ?? "";
}
// Filter by type
bool typeMatch = (logType == 0 && showLogs) ||
(logType == 1 && showWarnings) ||
(logType == 2 && showErrors);
if (!typeMatch) continue;
// Filter by search term
bool searchMatch = true;
if (searchWords != null && searchWords.Length > 0)
{
string lowerMessage = message.ToLower();
string lowerStackTrace = stackTrace.ToLower();
foreach (string word in searchWords)
{
if (!lowerMessage.Contains(word) && !lowerStackTrace.Contains(word))
{
searchMatch = false;
break;
}
}
}
if (!searchMatch) continue;
// Add matching entry to results
string typeStr = logType == 0 ? "Log" : logType == 1 ? "Warning" : "Error";
entries.Add(new
{
type = typeStr,
message,
stackTrace
});
}
catch (Exception)
{
// Skip entries that cause errors
continue;
}
}
// Return filtered results
return new
{
message = "Console logs retrieved successfully",
entries,
total_entries = count,
filtered_count = entries.Count,
show_logs = showLogs,
show_warnings = showWarnings,
show_errors = showErrors
};
}
catch (Exception e)
{
return new
{
error = $"Failed to read console logs: {e.Message}",
entries = new List<object>()
};
}
}
private static MethodInfo FindMethod(Type type, string[] methodNames)
{
foreach (var methodName in methodNames)
{
var method = type.GetMethod(methodName,
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
if (method != null)
return method;
}
return null;
}
private static BuildTarget GetBuildTarget(string platform)
{
BuildTarget target;
switch (platform.ToLower())
{
case "windows": target = BuildTarget.StandaloneWindows64; break;
case "mac": target = BuildTarget.StandaloneOSX; break;
case "linux": target = BuildTarget.StandaloneLinux64; break;
case "android": target = BuildTarget.Android; break;
case "ios": target = BuildTarget.iOS; break;
case "webgl": target = BuildTarget.WebGL; break;
default: target = (BuildTarget)(-1); break; // Invalid target
}
return target;
}
private static string[] GetEnabledScenes()
{
var scenes = new List<string>();
for (int i = 0; i < EditorBuildSettings.scenes.Length; i++)
{
if (EditorBuildSettings.scenes[i].enabled)
{
scenes.Add(EditorBuildSettings.scenes[i].path);
}
}
return scenes.ToArray();
}
/// <summary>
/// Helper method to get information about available properties and fields in a type
/// </summary>
private static Dictionary<string, object> GetTypeInfo(Type type)
{
var result = new Dictionary<string, object>();
// Get all public and non-public properties
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Static | BindingFlags.Instance);
var propList = new List<string>();
foreach (var prop in properties)
{
propList.Add($"{prop.PropertyType.Name} {prop.Name}");
}
result["Properties"] = propList;
// Get all public and non-public fields
var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Static | BindingFlags.Instance);
var fieldList = new List<string>();
foreach (var field in fields)
{
fieldList.Add($"{field.FieldType.Name} {field.Name}");
}
result["Fields"] = fieldList;
// Get all public and non-public methods
var methods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Static | BindingFlags.Instance);
var methodList = new List<string>();
foreach (var method in methods)
{
if (!method.Name.StartsWith("get_") && !method.Name.StartsWith("set_"))
{
var parameters = string.Join(", ", method.GetParameters()
.Select(p => $"{p.ParameterType.Name} {p.Name}"));
methodList.Add($"{method.ReturnType.Name} {method.Name}({parameters})");
}
}
result["Methods"] = methodList;
return result;
}
/// <summary>
/// Helper method to get all property and field values from an object
/// </summary>
private static Dictionary<string, string> GetObjectValues(object obj)
{
if (obj == null) return new Dictionary<string, string>();
var result = new Dictionary<string, string>();
var type = obj.GetType();
// Get all property values
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
foreach (var prop in properties)
{
try
{
var value = prop.GetValue(obj);
result[$"Property:{prop.Name}"] = value?.ToString() ?? "null";
}
catch (Exception)
{
result[$"Property:{prop.Name}"] = "ERROR";
}
}
// Get all field values
var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
foreach (var field in fields)
{
try
{
var value = field.GetValue(obj);
result[$"Field:{field.Name}"] = value?.ToString() ?? "null";
}
catch (Exception)
{
result[$"Field:{field.Name}"] = "ERROR";
}
}
return result;
}
/// <summary>
/// Reads the last N lines from a file
/// </summary>
private static List<string> ReadLastLines(string filePath, int lineCount)
{
var result = new List<string>();
using (var stream = new System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite))
using (var reader = new System.IO.StreamReader(stream))
{
string line;
var circularBuffer = new List<string>(lineCount);
int currentIndex = 0;
// Read all lines keeping only the last N in a circular buffer
while ((line = reader.ReadLine()) != null)
{
if (circularBuffer.Count < lineCount)
{
circularBuffer.Add(line);
}
else
{
circularBuffer[currentIndex] = line;
currentIndex = (currentIndex + 1) % lineCount;
}
}
// Reorder the circular buffer so that lines are returned in order
if (circularBuffer.Count == lineCount)
{
for (int i = 0; i < lineCount; i++)
{
result.Add(circularBuffer[(currentIndex + i) % lineCount]);
}
}
else
{
result.AddRange(circularBuffer);
}
}
return result;
}
/// <summary>
/// Gets a comprehensive list of available Unity commands, including editor menu items,
/// internal commands, utility methods, and other actionable operations that can be executed.
/// </summary>
/// <returns>Object containing categorized lists of available command paths</returns>
private static object GetAvailableCommands()
{
var menuCommands = new HashSet<string>();
var utilityCommands = new HashSet<string>();
var assetCommands = new HashSet<string>();
var sceneCommands = new HashSet<string>();
var gameObjectCommands = new HashSet<string>();
var prefabCommands = new HashSet<string>();
var shortcutCommands = new HashSet<string>();
var otherCommands = new HashSet<string>();
// Add a simple command that we know will work for testing
menuCommands.Add("Window/Unity MCP");
Debug.Log("Starting command collection...");
try
{
// Add all EditorApplication static methods - these are guaranteed to work
Debug.Log("Adding EditorApplication methods...");
foreach (MethodInfo method in typeof(EditorApplication).GetMethods(BindingFlags.Public | BindingFlags.Static))
{
utilityCommands.Add($"EditorApplication.{method.Name}");
}
Debug.Log($"Added {utilityCommands.Count} EditorApplication methods");
// Add built-in menu commands directly - these are common ones that should always be available
Debug.Log("Adding built-in menu commands...");
string[] builtInMenus = new[] {
"File/New Scene",
"File/Open Scene",
"File/Save",
"File/Save As...",
"Edit/Undo",
"Edit/Redo",
"Edit/Cut",
"Edit/Copy",
"Edit/Paste",
"Edit/Duplicate",
"Edit/Delete",
"GameObject/Create Empty",
"GameObject/3D Object/Cube",
"GameObject/3D Object/Sphere",
"GameObject/3D Object/Capsule",
"GameObject/3D Object/Cylinder",
"GameObject/3D Object/Plane",
"GameObject/Light/Directional Light",
"GameObject/Light/Point Light",
"GameObject/Light/Spotlight",
"GameObject/Light/Area Light",
"Component/Mesh/Mesh Filter",
"Component/Mesh/Mesh Renderer",
"Component/Physics/Rigidbody",
"Component/Physics/Box Collider",
"Component/Physics/Sphere Collider",
"Component/Physics/Capsule Collider",
"Component/Audio/Audio Source",
"Component/Audio/Audio Listener",
"Window/General/Scene",
"Window/General/Game",
"Window/General/Inspector",
"Window/General/Hierarchy",
"Window/General/Project",
"Window/General/Console",
"Window/Analysis/Profiler",
"Window/Package Manager",
"Assets/Create/Material",
"Assets/Create/C# Script",
"Assets/Create/Prefab",
"Assets/Create/Scene",
"Assets/Create/Folder",
};
foreach (string menuItem in builtInMenus)
{
menuCommands.Add(menuItem);
}
Debug.Log($"Added {builtInMenus.Length} built-in menu commands");
// Get menu commands from MenuItem attributes - wrapped in separate try block
Debug.Log("Searching for MenuItem attributes...");
try
{
int itemCount = 0;
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
if (assembly.IsDynamic) continue;
try
{
foreach (Type type in assembly.GetExportedTypes())
{
try
{
foreach (MethodInfo method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
{
try
{
object[] attributes = method.GetCustomAttributes(typeof(UnityEditor.MenuItem), false);
if (attributes != null && attributes.Length > 0)
{
foreach (var attr in attributes)
{
var menuItem = attr as UnityEditor.MenuItem;
if (menuItem != null && !string.IsNullOrEmpty(menuItem.menuItem))
{
menuCommands.Add(menuItem.menuItem);
itemCount++;
}
}
}
}
catch (Exception methodEx)
{
Debug.LogWarning($"Error getting menu items for method {method.Name}: {methodEx.Message}");
continue;
}
}
}
catch (Exception typeEx)
{
Debug.LogWarning($"Error processing type: {typeEx.Message}");
continue;
}
}
}
catch (Exception assemblyEx)
{
Debug.LogWarning($"Error examining assembly {assembly.GetName().Name}: {assemblyEx.Message}");
continue;
}
}
Debug.Log($"Found {itemCount} menu items from attributes");
}
catch (Exception menuItemEx)
{
Debug.LogError($"Failed to get menu items: {menuItemEx.Message}");
}
// Add EditorUtility methods as commands
Debug.Log("Adding EditorUtility methods...");
foreach (MethodInfo method in typeof(EditorUtility).GetMethods(BindingFlags.Public | BindingFlags.Static))
{
utilityCommands.Add($"EditorUtility.{method.Name}");
}
Debug.Log($"Added {typeof(EditorUtility).GetMethods(BindingFlags.Public | BindingFlags.Static).Length} EditorUtility methods");
// Add AssetDatabase methods as commands
Debug.Log("Adding AssetDatabase methods...");
foreach (MethodInfo method in typeof(AssetDatabase).GetMethods(BindingFlags.Public | BindingFlags.Static))
{
assetCommands.Add($"AssetDatabase.{method.Name}");
}
Debug.Log($"Added {typeof(AssetDatabase).GetMethods(BindingFlags.Public | BindingFlags.Static).Length} AssetDatabase methods");
// Add EditorSceneManager methods as commands
Debug.Log("Adding EditorSceneManager methods...");
Type sceneManagerType = typeof(UnityEditor.SceneManagement.EditorSceneManager);
if (sceneManagerType != null)
{
foreach (MethodInfo method in sceneManagerType.GetMethods(BindingFlags.Public | BindingFlags.Static))
{
sceneCommands.Add($"EditorSceneManager.{method.Name}");
}
Debug.Log($"Added {sceneManagerType.GetMethods(BindingFlags.Public | BindingFlags.Static).Length} EditorSceneManager methods");
}
// Add GameObject manipulation commands
Debug.Log("Adding GameObject methods...");
foreach (MethodInfo method in typeof(GameObject).GetMethods(BindingFlags.Public | BindingFlags.Static))
{
gameObjectCommands.Add($"GameObject.{method.Name}");
}
Debug.Log($"Added {typeof(GameObject).GetMethods(BindingFlags.Public | BindingFlags.Static).Length} GameObject methods");
// Add Selection-related commands
Debug.Log("Adding Selection methods...");
foreach (MethodInfo method in typeof(Selection).GetMethods(BindingFlags.Public | BindingFlags.Static))
{
gameObjectCommands.Add($"Selection.{method.Name}");
}
Debug.Log($"Added {typeof(Selection).GetMethods(BindingFlags.Public | BindingFlags.Static).Length} Selection methods");
// Add PrefabUtility methods as commands
Debug.Log("Adding PrefabUtility methods...");
Type prefabUtilityType = typeof(UnityEditor.PrefabUtility);
if (prefabUtilityType != null)
{
foreach (MethodInfo method in prefabUtilityType.GetMethods(BindingFlags.Public | BindingFlags.Static))
{
prefabCommands.Add($"PrefabUtility.{method.Name}");
}
Debug.Log($"Added {prefabUtilityType.GetMethods(BindingFlags.Public | BindingFlags.Static).Length} PrefabUtility methods");
}
// Add Undo related methods
Debug.Log("Adding Undo methods...");
foreach (MethodInfo method in typeof(Undo).GetMethods(BindingFlags.Public | BindingFlags.Static))
{
utilityCommands.Add($"Undo.{method.Name}");
}
Debug.Log($"Added {typeof(Undo).GetMethods(BindingFlags.Public | BindingFlags.Static).Length} Undo methods");
// The rest of the command gathering can be attempted but might not be critical
try
{
// Get commands from Unity's internal command system
Debug.Log("Trying to get internal CommandService commands...");
Type commandServiceType = typeof(UnityEditor.EditorWindow).Assembly.GetType("UnityEditor.CommandService");
if (commandServiceType != null)
{
Debug.Log("Found CommandService type");
PropertyInfo instanceProperty = commandServiceType.GetProperty("Instance",
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
if (instanceProperty != null)
{
Debug.Log("Found Instance property");
object commandService = instanceProperty.GetValue(null);
if (commandService != null)
{
Debug.Log("Got CommandService instance");
MethodInfo findAllCommandsMethod = commandServiceType.GetMethod("FindAllCommands",
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (findAllCommandsMethod != null)
{
Debug.Log("Found FindAllCommands method");
var commandsResult = findAllCommandsMethod.Invoke(commandService, null);
if (commandsResult != null)
{
Debug.Log("Got commands result");
var commandsList = commandsResult as System.Collections.IEnumerable;
if (commandsList != null)
{
int commandCount = 0;
foreach (var cmd in commandsList)
{
try
{
PropertyInfo nameProperty = cmd.GetType().GetProperty("name") ??
cmd.GetType().GetProperty("path") ??
cmd.GetType().GetProperty("commandName");
if (nameProperty != null)
{
string commandName = nameProperty.GetValue(cmd)?.ToString();
if (!string.IsNullOrEmpty(commandName))
{
otherCommands.Add(commandName);
commandCount++;
}
}
}
catch (Exception cmdEx)
{
Debug.LogWarning($"Error processing command: {cmdEx.Message}");
continue;
}
}
Debug.Log($"Added {commandCount} internal commands");
}
}
else
{
Debug.LogWarning("FindAllCommands returned null");
}
}
else
{
Debug.LogWarning("FindAllCommands method not found");
}
}
else
{
Debug.LogWarning("CommandService instance is null");
}
}
else
{
Debug.LogWarning("Instance property not found on CommandService");
}
}
else
{
Debug.LogWarning("CommandService type not found");
}
}
catch (Exception e)
{
Debug.LogWarning($"Failed to get internal Unity commands: {e.Message}");
}
// Other additional command sources can be tried
// ... other commands ...
}
catch (Exception e)
{
Debug.LogError($"Error getting Unity commands: {e.Message}\n{e.StackTrace}");
}
// Create command categories dictionary for the result
var commandCategories = new Dictionary<string, List<string>>
{
{ "MenuCommands", menuCommands.OrderBy(x => x).ToList() },
{ "UtilityCommands", utilityCommands.OrderBy(x => x).ToList() },
{ "AssetCommands", assetCommands.OrderBy(x => x).ToList() },
{ "SceneCommands", sceneCommands.OrderBy(x => x).ToList() },
{ "GameObjectCommands", gameObjectCommands.OrderBy(x => x).ToList() },
{ "PrefabCommands", prefabCommands.OrderBy(x => x).ToList() },
{ "ShortcutCommands", shortcutCommands.OrderBy(x => x).ToList() },
{ "OtherCommands", otherCommands.OrderBy(x => x).ToList() }
};
// Calculate total command count
int totalCount = commandCategories.Values.Sum(list => list.Count);
Debug.Log($"Command retrieval complete. Found {totalCount} total commands.");
// Create a simplified response with just the essential data
// The complex object structure might be causing serialization issues
var allCommandsList = commandCategories.Values.SelectMany(x => x).OrderBy(x => x).ToList();
// Use simple string array instead of JArray for better serialization
string[] commandsArray = allCommandsList.ToArray();
// Log the array size for verification
Debug.Log($"Final commands array contains {commandsArray.Length} items");
try
{
// Return a simple object with just the commands array and count
var result = new
{
commands = commandsArray,
count = commandsArray.Length
};
// Verify the result can be serialized properly
var jsonTest = JsonUtility.ToJson(new { test = "This is a test" });
Debug.Log($"JSON serialization test successful: {jsonTest}");
return result;
}
catch (Exception ex)
{
Debug.LogError($"Error creating response: {ex.Message}");
// Ultimate fallback - don't use any JObject/JArray
return new
{
message = $"Found {commandsArray.Length} commands",
firstTen = commandsArray.Take(10).ToArray(),
count = commandsArray.Length
};
}
}
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: c38b437302cc6a846a955fe2cfe746c5

View File

@ -1,95 +0,0 @@
using UnityEngine;
using Newtonsoft.Json.Linq;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering;
using UnityEditor;
using System.IO;
namespace UnityMCP.Editor.Commands
{
/// <summary>
/// Handles material-related commands
/// </summary>
public static class MaterialCommandHandler
{
/// <summary>
/// Sets or modifies a material on an object
/// </summary>
public static object SetMaterial(JObject @params)
{
string objectName = (string)@params["object_name"] ?? throw new System.Exception("Parameter 'object_name' is required.");
var obj = GameObject.Find(objectName) ?? throw new System.Exception($"Object '{objectName}' not found.");
var renderer = obj.GetComponent<Renderer>() ?? throw new System.Exception($"Object '{objectName}' has no renderer.");
// Check if URP is being used
bool isURP = GraphicsSettings.currentRenderPipeline is UniversalRenderPipelineAsset;
Material material = null;
string materialName = (string)@params["material_name"];
bool createIfMissing = (bool)(@params["create_if_missing"] ?? true);
string materialPath = null;
// If material name is specified, try to find or create it
if (!string.IsNullOrEmpty(materialName))
{
// Ensure Materials folder exists
const string materialsFolder = "Assets/Materials";
if (!Directory.Exists(materialsFolder))
{
Directory.CreateDirectory(materialsFolder);
}
materialPath = $"{materialsFolder}/{materialName}.mat";
material = AssetDatabase.LoadAssetAtPath<Material>(materialPath);
if (material == null && createIfMissing)
{
// Create new material with appropriate shader
material = new Material(isURP ? Shader.Find("Universal Render Pipeline/Lit") : Shader.Find("Standard"));
material.name = materialName;
// Save the material asset
AssetDatabase.CreateAsset(material, materialPath);
AssetDatabase.SaveAssets();
}
else if (material == null)
{
throw new System.Exception($"Material '{materialName}' not found and create_if_missing is false.");
}
}
else
{
// Create a temporary material if no name specified
material = new Material(isURP ? Shader.Find("Universal Render Pipeline/Lit") : Shader.Find("Standard"));
}
// Apply color if specified
if (@params.ContainsKey("color"))
{
var colorArray = (JArray)@params["color"];
if (colorArray.Count < 3 || colorArray.Count > 4)
throw new System.Exception("Color must be an array of 3 (RGB) or 4 (RGBA) floats.");
Color color = new(
(float)colorArray[0],
(float)colorArray[1],
(float)colorArray[2],
colorArray.Count > 3 ? (float)colorArray[3] : 1.0f
);
material.color = color;
// If this is a saved material, make sure to save the color change
if (!string.IsNullOrEmpty(materialPath))
{
EditorUtility.SetDirty(material);
AssetDatabase.SaveAssets();
}
}
// Apply the material to the renderer
renderer.material = material;
return new { material_name = material.name, path = materialPath };
}
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 50ed709388e81a741ac984de1c78427c

View File

@ -1,505 +0,0 @@
using UnityEngine;
using Newtonsoft.Json.Linq;
using System;
using System.Linq;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine.SceneManagement;
using UnityEditor.SceneManagement;
using UnityMCP.Editor.Helpers;
using System.Reflection;
namespace UnityMCP.Editor.Commands
{
/// <summary>
/// Handles object-related commands
/// </summary>
public static class ObjectCommandHandler
{
/// <summary>
/// Gets information about a specific object
/// </summary>
public static object GetObjectInfo(JObject @params)
{
string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required.");
var obj = GameObject.Find(name) ?? throw new Exception($"Object '{name}' not found.");
return new
{
obj.name,
position = new[] { obj.transform.position.x, obj.transform.position.y, obj.transform.position.z },
rotation = new[] { obj.transform.eulerAngles.x, obj.transform.eulerAngles.y, obj.transform.eulerAngles.z },
scale = new[] { obj.transform.localScale.x, obj.transform.localScale.y, obj.transform.localScale.z }
};
}
/// <summary>
/// Creates a new object in the scene
/// </summary>
public static object CreateObject(JObject @params)
{
string type = (string)@params["type"] ?? throw new Exception("Parameter 'type' is required.");
GameObject obj = type.ToUpper() switch
{
"CUBE" => GameObject.CreatePrimitive(PrimitiveType.Cube),
"SPHERE" => GameObject.CreatePrimitive(PrimitiveType.Sphere),
"CYLINDER" => GameObject.CreatePrimitive(PrimitiveType.Cylinder),
"CAPSULE" => GameObject.CreatePrimitive(PrimitiveType.Capsule),
"PLANE" => GameObject.CreatePrimitive(PrimitiveType.Plane),
"EMPTY" => new GameObject(),
"CAMERA" => new GameObject("Camera") { }.AddComponent<Camera>().gameObject,
"LIGHT" => new GameObject("Light") { }.AddComponent<Light>().gameObject,
"DIRECTIONAL_LIGHT" => CreateDirectionalLight(),
_ => throw new Exception($"Unsupported object type: {type}")
};
if (@params.ContainsKey("name")) obj.name = (string)@params["name"];
if (@params.ContainsKey("location")) obj.transform.position = Vector3Helper.ParseVector3((JArray)@params["location"]);
if (@params.ContainsKey("rotation")) obj.transform.eulerAngles = Vector3Helper.ParseVector3((JArray)@params["rotation"]);
if (@params.ContainsKey("scale")) obj.transform.localScale = Vector3Helper.ParseVector3((JArray)@params["scale"]);
return new { obj.name };
}
/// <summary>
/// Modifies an existing object's properties
/// </summary>
public static object ModifyObject(JObject @params)
{
string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required.");
var obj = GameObject.Find(name) ?? throw new Exception($"Object '{name}' not found.");
// Handle basic transform properties
if (@params.ContainsKey("location")) obj.transform.position = Vector3Helper.ParseVector3((JArray)@params["location"]);
if (@params.ContainsKey("rotation")) obj.transform.eulerAngles = Vector3Helper.ParseVector3((JArray)@params["rotation"]);
if (@params.ContainsKey("scale")) obj.transform.localScale = Vector3Helper.ParseVector3((JArray)@params["scale"]);
if (@params.ContainsKey("visible")) obj.SetActive((bool)@params["visible"]);
// Handle parent setting
if (@params.ContainsKey("set_parent"))
{
string parentName = (string)@params["set_parent"];
var parent = GameObject.Find(parentName) ?? throw new Exception($"Parent object '{parentName}' not found.");
obj.transform.SetParent(parent.transform);
}
// Handle component operations
if (@params.ContainsKey("add_component"))
{
string componentType = (string)@params["add_component"];
Type type = componentType switch
{
"Rigidbody" => typeof(Rigidbody),
"BoxCollider" => typeof(BoxCollider),
"SphereCollider" => typeof(SphereCollider),
"CapsuleCollider" => typeof(CapsuleCollider),
"MeshCollider" => typeof(MeshCollider),
"Camera" => typeof(Camera),
"Light" => typeof(Light),
"Renderer" => typeof(Renderer),
"MeshRenderer" => typeof(MeshRenderer),
"SkinnedMeshRenderer" => typeof(SkinnedMeshRenderer),
"Animator" => typeof(Animator),
"AudioSource" => typeof(AudioSource),
"AudioListener" => typeof(AudioListener),
"ParticleSystem" => typeof(ParticleSystem),
"ParticleSystemRenderer" => typeof(ParticleSystemRenderer),
"TrailRenderer" => typeof(TrailRenderer),
"LineRenderer" => typeof(LineRenderer),
"TextMesh" => typeof(TextMesh),
"TextMeshPro" => typeof(TMPro.TextMeshPro),
"TextMeshProUGUI" => typeof(TMPro.TextMeshProUGUI),
_ => Type.GetType($"UnityEngine.{componentType}") ??
Type.GetType(componentType) ??
throw new Exception($"Component type '{componentType}' not found.")
};
obj.AddComponent(type);
}
if (@params.ContainsKey("remove_component"))
{
string componentType = (string)@params["remove_component"];
Type type = Type.GetType($"UnityEngine.{componentType}") ??
Type.GetType(componentType) ??
throw new Exception($"Component type '{componentType}' not found.");
var component = obj.GetComponent(type);
if (component != null)
UnityEngine.Object.DestroyImmediate(component);
}
// Handle property setting
if (@params.ContainsKey("set_property"))
{
var propertyData = (JObject)@params["set_property"];
string componentType = (string)propertyData["component"];
string propertyName = (string)propertyData["property"];
var value = propertyData["value"];
// Handle GameObject properties separately
if (componentType == "GameObject")
{
var gameObjectProperty = typeof(GameObject).GetProperty(propertyName) ??
throw new Exception($"Property '{propertyName}' not found on GameObject.");
// Convert value based on property type
object gameObjectValue = Convert.ChangeType(value, gameObjectProperty.PropertyType);
gameObjectProperty.SetValue(obj, gameObjectValue);
return new { obj.name };
}
// Handle component properties
Type type = componentType switch
{
"Rigidbody" => typeof(Rigidbody),
"BoxCollider" => typeof(BoxCollider),
"SphereCollider" => typeof(SphereCollider),
"CapsuleCollider" => typeof(CapsuleCollider),
"MeshCollider" => typeof(MeshCollider),
"Camera" => typeof(Camera),
"Light" => typeof(Light),
"Renderer" => typeof(Renderer),
"MeshRenderer" => typeof(MeshRenderer),
"SkinnedMeshRenderer" => typeof(SkinnedMeshRenderer),
"Animator" => typeof(Animator),
"AudioSource" => typeof(AudioSource),
"AudioListener" => typeof(AudioListener),
"ParticleSystem" => typeof(ParticleSystem),
"ParticleSystemRenderer" => typeof(ParticleSystemRenderer),
"TrailRenderer" => typeof(TrailRenderer),
"LineRenderer" => typeof(LineRenderer),
"TextMesh" => typeof(TextMesh),
"TextMeshPro" => typeof(TMPro.TextMeshPro),
"TextMeshProUGUI" => typeof(TMPro.TextMeshProUGUI),
_ => Type.GetType($"UnityEngine.{componentType}") ??
Type.GetType(componentType) ??
throw new Exception($"Component type '{componentType}' not found.")
};
var component = obj.GetComponent(type) ??
throw new Exception($"Component '{componentType}' not found on object '{name}'.");
var property = type.GetProperty(propertyName) ??
throw new Exception($"Property '{propertyName}' not found on component '{componentType}'.");
// Convert value based on property type
object propertyValue = Convert.ChangeType(value, property.PropertyType);
property.SetValue(component, propertyValue);
}
return new { obj.name };
}
/// <summary>
/// Deletes an object from the scene
/// </summary>
public static object DeleteObject(JObject @params)
{
string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required.");
var obj = GameObject.Find(name) ?? throw new Exception($"Object '{name}' not found.");
UnityEngine.Object.DestroyImmediate(obj);
return new { name };
}
/// <summary>
/// Gets all properties of a specified game object
/// </summary>
public static object GetObjectProperties(JObject @params)
{
string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required.");
var obj = GameObject.Find(name) ?? throw new Exception($"Object '{name}' not found.");
var components = obj.GetComponents<Component>()
.Select(c => new
{
type = c.GetType().Name,
properties = GetComponentProperties(c)
})
.ToList();
return new
{
obj.name,
obj.tag,
obj.layer,
active = obj.activeSelf,
transform = new
{
position = new[] { obj.transform.position.x, obj.transform.position.y, obj.transform.position.z },
rotation = new[] { obj.transform.eulerAngles.x, obj.transform.eulerAngles.y, obj.transform.eulerAngles.z },
scale = new[] { obj.transform.localScale.x, obj.transform.localScale.y, obj.transform.localScale.z }
},
components
};
}
/// <summary>
/// Gets properties of a specific component
/// </summary>
public static object GetComponentProperties(JObject @params)
{
string objectName = (string)@params["object_name"] ?? throw new Exception("Parameter 'object_name' is required.");
string componentType = (string)@params["component_type"] ?? throw new Exception("Parameter 'component_type' is required.");
var obj = GameObject.Find(objectName) ?? throw new Exception($"Object '{objectName}' not found.");
var component = obj.GetComponent(componentType) ?? throw new Exception($"Component '{componentType}' not found on object '{objectName}'.");
return GetComponentProperties(component);
}
/// <summary>
/// Finds objects by name in the scene
/// </summary>
public static object FindObjectsByName(JObject @params)
{
string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required.");
var objects = GameObject.FindObjectsByType<GameObject>(FindObjectsSortMode.None)
.Where(o => o.name.Contains(name))
.Select(o => new
{
o.name,
path = GetGameObjectPath(o)
})
.ToList();
return new { objects };
}
/// <summary>
/// Finds objects by tag in the scene
/// </summary>
public static object FindObjectsByTag(JObject @params)
{
string tag = (string)@params["tag"] ?? throw new Exception("Parameter 'tag' is required.");
var objects = GameObject.FindGameObjectsWithTag(tag)
.Select(o => new
{
o.name,
path = GetGameObjectPath(o)
})
.ToList();
return new { objects };
}
/// <summary>
/// Gets the current hierarchy of game objects in the scene
/// </summary>
public static object GetHierarchy()
{
var rootObjects = SceneManager.GetActiveScene().GetRootGameObjects();
var hierarchy = rootObjects.Select(o => BuildHierarchyNode(o)).ToList();
return new { hierarchy };
}
/// <summary>
/// Selects a specified game object in the editor
/// </summary>
public static object SelectObject(JObject @params)
{
string name = (string)@params["name"] ?? throw new Exception("Parameter 'name' is required.");
var obj = GameObject.Find(name) ?? throw new Exception($"Object '{name}' not found.");
Selection.activeGameObject = obj;
return new { obj.name };
}
/// <summary>
/// Gets the currently selected game object in the editor
/// </summary>
public static object GetSelectedObject()
{
var selected = Selection.activeGameObject;
if (selected == null)
return new { selected = (object)null };
return new
{
selected = new
{
selected.name,
path = GetGameObjectPath(selected)
}
};
}
// Helper methods
private static Dictionary<string, object> GetComponentProperties(Component component)
{
var properties = new Dictionary<string, object>();
var serializedObject = new SerializedObject(component);
var property = serializedObject.GetIterator();
while (property.Next(true))
{
properties[property.name] = GetPropertyValue(property);
}
return properties;
}
private static object GetPropertyValue(SerializedProperty property)
{
switch (property.propertyType)
{
case SerializedPropertyType.Integer:
return property.intValue;
case SerializedPropertyType.Float:
return property.floatValue;
case SerializedPropertyType.Boolean:
return property.boolValue;
case SerializedPropertyType.String:
return property.stringValue;
case SerializedPropertyType.Vector3:
return new[] { property.vector3Value.x, property.vector3Value.y, property.vector3Value.z };
case SerializedPropertyType.Vector2:
return new[] { property.vector2Value.x, property.vector2Value.y };
case SerializedPropertyType.Color:
return new[] { property.colorValue.r, property.colorValue.g, property.colorValue.b, property.colorValue.a };
case SerializedPropertyType.ObjectReference:
return property.objectReferenceValue ? property.objectReferenceValue.name : null;
default:
return property.propertyType.ToString();
}
}
private static string GetGameObjectPath(GameObject obj)
{
var path = obj.name;
var parent = obj.transform.parent;
while (parent != null)
{
path = parent.name + "/" + path;
parent = parent.parent;
}
return path;
}
private static object BuildHierarchyNode(GameObject obj)
{
return new
{
obj.name,
children = Enumerable.Range(0, obj.transform.childCount)
.Select(i => BuildHierarchyNode(obj.transform.GetChild(i).gameObject))
.ToList()
};
}
/// <summary>
/// Creates a directional light game object
/// </summary>
private static GameObject CreateDirectionalLight()
{
var obj = new GameObject("DirectionalLight");
var light = obj.AddComponent<Light>();
light.type = LightType.Directional;
light.intensity = 1.0f;
light.shadows = LightShadows.Soft;
return obj;
}
/// <summary>
/// Executes a context menu method on a component of a game object
/// </summary>
public static object ExecuteContextMenuItem(JObject @params)
{
string objectName = (string)@params["object_name"] ?? throw new Exception("Parameter 'object_name' is required.");
string componentName = (string)@params["component"] ?? throw new Exception("Parameter 'component' is required.");
string contextMenuItemName = (string)@params["context_menu_item"] ?? throw new Exception("Parameter 'context_menu_item' is required.");
// Find the game object
var obj = GameObject.Find(objectName) ?? throw new Exception($"Object '{objectName}' not found.");
// Find the component type
Type componentType = FindTypeInLoadedAssemblies(componentName) ??
throw new Exception($"Component type '{componentName}' not found.");
// Get the component from the game object
var component = obj.GetComponent(componentType) ??
throw new Exception($"Component '{componentName}' not found on object '{objectName}'.");
// Find methods with ContextMenu attribute matching the context menu item name
var methods = componentType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.Where(m => m.GetCustomAttributes(typeof(ContextMenuItemAttribute), true).Any() ||
m.GetCustomAttributes(typeof(ContextMenu), true)
.Cast<ContextMenu>()
.Any(attr => attr.menuItem == contextMenuItemName))
.ToList();
// If no methods with ContextMenuItemAttribute are found, look for methods with name matching the context menu item
if (methods.Count == 0)
{
methods = componentType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.Where(m => m.Name == contextMenuItemName)
.ToList();
}
if (methods.Count == 0)
throw new Exception($"No context menu method '{contextMenuItemName}' found on component '{componentName}'.");
// If multiple methods match, use the first one and log a warning
if (methods.Count > 1)
{
Debug.LogWarning($"Found multiple methods for context menu item '{contextMenuItemName}' on component '{componentName}'. Using the first one.");
}
var method = methods[0];
// Execute the method
try
{
method.Invoke(component, null);
return new
{
success = true,
message = $"Successfully executed context menu item '{contextMenuItemName}' on component '{componentName}' of object '{objectName}'."
};
}
catch (Exception ex)
{
throw new Exception($"Error executing context menu item: {ex.Message}");
}
}
// Add this helper method to find types across all loaded assemblies
private static Type FindTypeInLoadedAssemblies(string typeName)
{
// First try standard approach
Type type = Type.GetType(typeName);
if (type != null)
return type;
type = Type.GetType($"UnityEngine.{typeName}");
if (type != null)
return type;
// Then search all loaded assemblies
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
// Try with the simple name
type = assembly.GetType(typeName);
if (type != null)
return type;
// Try with the fully qualified name (assembly.GetTypes() can be expensive, so we do this last)
var types = assembly.GetTypes().Where(t => t.Name == typeName).ToArray();
if (types.Length > 0)
{
// If we found multiple types with the same name, log a warning
if (types.Length > 1)
{
Debug.LogWarning(
$"Found multiple types named '{typeName}'. Using the first one: {types[0].FullName}"
);
}
return types[0];
}
}
return null;
}
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: fd293dd9be195b94e926aa1c1f44ba72

View File

@ -1,140 +0,0 @@
using UnityEngine.SceneManagement;
using System.Linq;
using System;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
namespace UnityMCP.Editor.Commands
{
/// <summary>
/// Handles scene-related commands for the MCP Server
/// </summary>
public static class SceneCommandHandler
{
/// <summary>
/// Gets information about the current scene
/// </summary>
/// <returns>Scene information including name and root objects</returns>
public static object GetSceneInfo()
{
var scene = SceneManager.GetActiveScene();
var rootObjects = scene.GetRootGameObjects().Select(o => o.name).ToArray();
return new { sceneName = scene.name, rootObjects };
}
/// <summary>
/// Opens a specified scene in the Unity editor
/// </summary>
/// <param name="params">Parameters containing the scene path</param>
/// <returns>Result of the operation</returns>
public static object OpenScene(JObject @params)
{
try
{
string scenePath = (string)@params["scene_path"];
if (string.IsNullOrEmpty(scenePath))
return new { success = false, error = "Scene path cannot be empty" };
if (!System.IO.File.Exists(scenePath))
return new { success = false, error = $"Scene file not found: {scenePath}" };
EditorSceneManager.OpenScene(scenePath);
return new { success = true, message = $"Opened scene: {scenePath}" };
}
catch (Exception e)
{
return new { success = false, error = $"Failed to open scene: {e.Message}", stackTrace = e.StackTrace };
}
}
/// <summary>
/// Saves the current scene
/// </summary>
/// <returns>Result of the operation</returns>
public static object SaveScene()
{
try
{
var scene = SceneManager.GetActiveScene();
EditorSceneManager.SaveScene(scene);
return new { success = true, message = $"Saved scene: {scene.path}" };
}
catch (Exception e)
{
return new { success = false, error = $"Failed to save scene: {e.Message}", stackTrace = e.StackTrace };
}
}
/// <summary>
/// Creates a new empty scene
/// </summary>
/// <param name="params">Parameters containing the new scene path</param>
/// <returns>Result of the operation</returns>
public static object NewScene(JObject @params)
{
try
{
string scenePath = (string)@params["scene_path"];
if (string.IsNullOrEmpty(scenePath))
return new { success = false, error = "Scene path cannot be empty" };
// Create new scene
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene);
// Ensure the scene is loaded and active
if (!scene.isLoaded)
{
EditorSceneManager.LoadScene(scenePath);
}
// Save the scene
EditorSceneManager.SaveScene(scene, scenePath);
// Force a refresh of the scene view
EditorApplication.ExecuteMenuItem("Window/General/Scene");
return new { success = true, message = $"Created new scene at: {scenePath}" };
}
catch (Exception e)
{
return new { success = false, error = $"Failed to create new scene: {e.Message}", stackTrace = e.StackTrace };
}
}
/// <summary>
/// Changes to a different scene, optionally saving the current one
/// </summary>
/// <param name="params">Parameters containing the target scene path and save option</param>
/// <returns>Result of the operation</returns>
public static object ChangeScene(JObject @params)
{
try
{
string scenePath = (string)@params["scene_path"];
bool saveCurrent = @params["save_current"]?.Value<bool>() ?? false;
if (string.IsNullOrEmpty(scenePath))
return new { success = false, error = "Scene path cannot be empty" };
if (!System.IO.File.Exists(scenePath))
return new { success = false, error = $"Scene file not found: {scenePath}" };
// Save current scene if requested
if (saveCurrent)
{
var currentScene = SceneManager.GetActiveScene();
EditorSceneManager.SaveScene(currentScene);
}
// Open the new scene
EditorSceneManager.OpenScene(scenePath);
return new { success = true, message = $"Changed to scene: {scenePath}" };
}
catch (Exception e)
{
return new { success = false, error = $"Failed to change scene: {e.Message}", stackTrace = e.StackTrace };
}
}
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: cef46a6ad7d43744ab874553102f032a

View File

@ -1,496 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
namespace UnityMCP.Editor.Commands
{
/// <summary>
/// Handles script-related commands for Unity
/// </summary>
public static class ScriptCommandHandler
{
/// <summary>
/// Views the contents of a Unity script file
/// </summary>
public static object ViewScript(JObject @params)
{
string scriptPath =
(string)@params["script_path"]
?? throw new Exception("Parameter 'script_path' is required.");
bool requireExists = (bool?)@params["require_exists"] ?? true;
// Handle path correctly to avoid double "Assets" folder issue
string relativePath;
if (scriptPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
// If path already starts with Assets/, remove it for local path operations
relativePath = scriptPath.Substring(7);
}
else
{
relativePath = scriptPath;
}
string fullPath = Path.Combine(Application.dataPath, relativePath);
if (!File.Exists(fullPath))
{
if (requireExists)
{
throw new Exception($"Script file not found: {scriptPath}");
}
else
{
return new { exists = false, message = $"Script file not found: {scriptPath}" };
}
}
string content = File.ReadAllText(fullPath);
byte[] contentBytes = System.Text.Encoding.UTF8.GetBytes(content);
string base64Content = Convert.ToBase64String(contentBytes);
return new
{
exists = true,
content = base64Content,
encoding = "base64"
};
}
/// <summary>
/// Ensures the Scripts folder exists in the project
/// </summary>
private static void EnsureScriptsFolderExists()
{
// Never create an "Assets" folder as it's the project root
// Instead create "Scripts" within the existing Assets folder
string scriptsFolderPath = Path.Combine(Application.dataPath, "Scripts");
if (!Directory.Exists(scriptsFolderPath))
{
Directory.CreateDirectory(scriptsFolderPath);
AssetDatabase.Refresh();
}
}
/// <summary>
/// Creates a new Unity script file in the specified folder
/// </summary>
public static object CreateScript(JObject @params)
{
string scriptName =
(string)@params["script_name"]
?? throw new Exception("Parameter 'script_name' is required.");
string scriptType = (string)@params["script_type"] ?? "MonoBehaviour";
string namespaceName = (string)@params["namespace"];
string template = (string)@params["template"];
string scriptFolder = (string)@params["script_folder"];
string content = (string)@params["content"];
bool overwrite = (bool?)@params["overwrite"] ?? false;
// Ensure script name ends with .cs
if (!scriptName.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
scriptName += ".cs";
// Make sure scriptName doesn't contain path separators - extract base name
scriptName = Path.GetFileName(scriptName);
// Determine the script path
string scriptPath;
// Handle the script folder parameter
if (string.IsNullOrEmpty(scriptFolder))
{
// Default to Scripts folder within Assets
scriptPath = "Scripts";
EnsureScriptsFolderExists();
}
else
{
// Use provided folder path
scriptPath = scriptFolder;
// If scriptFolder starts with "Assets/", remove it for local path operations
if (scriptPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
scriptPath = scriptPath.Substring(7);
}
}
// Create the full directory path, avoiding Assets/Assets issue
string folderPath = Path.Combine(Application.dataPath, scriptPath);
// Create directory if it doesn't exist
if (!Directory.Exists(folderPath))
{
try
{
Directory.CreateDirectory(folderPath);
AssetDatabase.Refresh();
}
catch (Exception ex)
{
throw new Exception($"Failed to create directory '{scriptPath}': {ex.Message}");
}
}
// Check if script already exists
string fullFilePath = Path.Combine(folderPath, scriptName);
if (File.Exists(fullFilePath) && !overwrite)
{
throw new Exception(
$"Script file '{scriptName}' already exists in '{scriptPath}' and overwrite is not enabled."
);
}
try
{
// If content is provided, use it directly
if (!string.IsNullOrEmpty(content))
{
// Create the script file with provided content
File.WriteAllText(fullFilePath, content);
}
else
{
// Otherwise generate content based on template and parameters
StringBuilder contentBuilder = new();
// Add using directives
contentBuilder.AppendLine("using UnityEngine;");
contentBuilder.AppendLine();
// Add namespace if specified
if (!string.IsNullOrEmpty(namespaceName))
{
contentBuilder.AppendLine($"namespace {namespaceName}");
contentBuilder.AppendLine("{");
}
// Add class definition with indent based on namespace
string indent = string.IsNullOrEmpty(namespaceName) ? "" : " ";
contentBuilder.AppendLine(
$"{indent}public class {Path.GetFileNameWithoutExtension(scriptName)} : {scriptType}"
);
contentBuilder.AppendLine($"{indent}{{");
// Add default Unity methods based on script type
if (scriptType == "MonoBehaviour")
{
contentBuilder.AppendLine($"{indent} private void Start()");
contentBuilder.AppendLine($"{indent} {{");
contentBuilder.AppendLine(
$"{indent} // Initialize your component here"
);
contentBuilder.AppendLine($"{indent} }}");
contentBuilder.AppendLine();
contentBuilder.AppendLine($"{indent} private void Update()");
contentBuilder.AppendLine($"{indent} {{");
contentBuilder.AppendLine($"{indent} // Update your component here");
contentBuilder.AppendLine($"{indent} }}");
}
else if (scriptType == "ScriptableObject")
{
contentBuilder.AppendLine($"{indent} private void OnEnable()");
contentBuilder.AppendLine($"{indent} {{");
contentBuilder.AppendLine(
$"{indent} // Initialize your ScriptableObject here"
);
contentBuilder.AppendLine($"{indent} }}");
}
// Close class
contentBuilder.AppendLine($"{indent}}}");
// Close namespace if specified
if (!string.IsNullOrEmpty(namespaceName))
{
contentBuilder.AppendLine("}");
}
// Write the generated content to file
File.WriteAllText(fullFilePath, contentBuilder.ToString());
}
// Refresh the AssetDatabase to recognize the new script
AssetDatabase.Refresh();
// Return the relative path for easier reference
string relativePath = scriptPath.Replace('\\', '/');
if (!relativePath.StartsWith("Assets/"))
{
relativePath = $"Assets/{relativePath}";
}
return new
{
message = $"Created script: {Path.Combine(relativePath, scriptName).Replace('\\', '/')}",
script_path = Path.Combine(relativePath, scriptName).Replace('\\', '/')
};
}
catch (Exception ex)
{
Debug.LogError($"Failed to create script: {ex.Message}\n{ex.StackTrace}");
throw new Exception($"Failed to create script '{scriptName}': {ex.Message}");
}
}
/// <summary>
/// Updates the contents of an existing Unity script
/// </summary>
public static object UpdateScript(JObject @params)
{
string scriptPath =
(string)@params["script_path"]
?? throw new Exception("Parameter 'script_path' is required.");
string content =
(string)@params["content"]
?? throw new Exception("Parameter 'content' is required.");
bool createIfMissing = (bool?)@params["create_if_missing"] ?? false;
bool createFolderIfMissing = (bool?)@params["create_folder_if_missing"] ?? false;
// Handle path correctly to avoid double "Assets" folder
string relativePath;
if (scriptPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
// If path already starts with Assets/, remove it for local path operations
relativePath = scriptPath.Substring(7);
}
else
{
relativePath = scriptPath;
}
string fullPath = Path.Combine(Application.dataPath, relativePath);
string directory = Path.GetDirectoryName(fullPath);
// Debug the paths to help diagnose issues
// Check if file exists, create if requested
if (!File.Exists(fullPath))
{
if (createIfMissing)
{
// Create the directory if requested and needed
if (!Directory.Exists(directory) && createFolderIfMissing)
{
Directory.CreateDirectory(directory);
}
else if (!Directory.Exists(directory))
{
throw new Exception(
$"Directory does not exist: {Path.GetDirectoryName(scriptPath)}"
);
}
// Create the file with content
File.WriteAllText(fullPath, content);
AssetDatabase.Refresh();
return new { message = $"Created script: {scriptPath}" };
}
else
{
throw new Exception($"Script file not found: {scriptPath}");
}
}
// Update existing script
File.WriteAllText(fullPath, content);
// Refresh the AssetDatabase
AssetDatabase.Refresh();
return new { message = $"Updated script: {scriptPath}" };
}
/// <summary>
/// Lists all script files in a specified folder
/// </summary>
public static object ListScripts(JObject @params)
{
string folderPath = (string)@params["folder_path"] ?? "Assets";
// Special handling for "Assets" path since it's already the root
string fullPath;
if (folderPath.Equals("Assets", StringComparison.OrdinalIgnoreCase))
{
fullPath = Application.dataPath;
}
else if (folderPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
// Remove "Assets/" from the path since Application.dataPath already points to it
string relativePath = folderPath.Substring(7);
fullPath = Path.Combine(Application.dataPath, relativePath);
}
else
{
// Assume it's a relative path from Assets
fullPath = Path.Combine(Application.dataPath, folderPath);
}
if (!Directory.Exists(fullPath))
throw new Exception($"Folder not found: {folderPath}");
string[] scripts = Directory
.GetFiles(fullPath, "*.cs", SearchOption.AllDirectories)
.Select(path => path.Replace(Application.dataPath, "Assets"))
.ToArray();
return new { scripts };
}
/// <summary>
/// Attaches a script component to a GameObject
/// </summary>
public static object AttachScript(JObject @params)
{
string objectName =
(string)@params["object_name"]
?? throw new Exception("Parameter 'object_name' is required.");
string scriptName =
(string)@params["script_name"]
?? throw new Exception("Parameter 'script_name' is required.");
string scriptPath = (string)@params["script_path"]; // Optional
// Find the target object
GameObject targetObject = GameObject.Find(objectName);
if (targetObject == null)
throw new Exception($"Object '{objectName}' not found in scene.");
// Ensure script name ends with .cs
if (!scriptName.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
scriptName += ".cs";
// Remove the path from the scriptName if it contains path separators
string scriptFileName = Path.GetFileName(scriptName);
string scriptNameWithoutExtension = Path.GetFileNameWithoutExtension(scriptFileName);
// Find the script asset
string[] guids;
if (!string.IsNullOrEmpty(scriptPath))
{
// If a specific path is provided, try that first
if (
File.Exists(
Path.Combine(Application.dataPath, scriptPath.Replace("Assets/", ""))
)
)
{
// Use the direct path if it exists
MonoScript scriptAsset = AssetDatabase.LoadAssetAtPath<MonoScript>(scriptPath);
if (scriptAsset != null)
{
Type scriptType = scriptAsset.GetClass();
if (scriptType != null)
{
try
{
// Try to add the component
Component component = targetObject.AddComponent(scriptType);
if (component != null)
{
return new
{
message = $"Successfully attached script '{scriptFileName}' to object '{objectName}'",
component_type = scriptType.Name
};
}
}
catch (Exception ex)
{
Debug.LogError($"Error attaching script component: {ex.Message}");
throw new Exception($"Failed to add component: {ex.Message}");
}
}
}
}
}
// Use the file name for searching if direct path didn't work
guids = AssetDatabase.FindAssets(scriptNameWithoutExtension + " t:script");
if (guids.Length == 0)
{
// Try a broader search if exact match fails
guids = AssetDatabase.FindAssets(scriptNameWithoutExtension);
if (guids.Length == 0)
throw new Exception($"Script '{scriptFileName}' not found in project.");
}
// Check each potential script until we find one that can be attached
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
// Filter to only consider .cs files
if (!path.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
continue;
// Double check the file name to avoid false matches
string foundFileName = Path.GetFileName(path);
if (
!string.Equals(
foundFileName,
scriptFileName,
StringComparison.OrdinalIgnoreCase
)
&& !string.Equals(
Path.GetFileNameWithoutExtension(foundFileName),
scriptNameWithoutExtension,
StringComparison.OrdinalIgnoreCase
)
)
continue;
MonoScript scriptAsset = AssetDatabase.LoadAssetAtPath<MonoScript>(path);
if (scriptAsset == null)
continue;
Type scriptType = scriptAsset.GetClass();
if (scriptType == null || !typeof(MonoBehaviour).IsAssignableFrom(scriptType))
continue;
try
{
// Check if component is already attached
if (targetObject.GetComponent(scriptType) != null)
{
return new
{
message = $"Script '{scriptNameWithoutExtension}' is already attached to object '{objectName}'",
component_type = scriptType.Name
};
}
// Add the component
Component component = targetObject.AddComponent(scriptType);
if (component != null)
{
return new
{
message = $"Successfully attached script '{scriptFileName}' to object '{objectName}'",
component_type = scriptType.Name,
script_path = path
};
}
}
catch (Exception ex)
{
Debug.LogError($"Error attaching script '{path}': {ex.Message}");
// Continue trying other matches instead of failing immediately
}
}
// If we've tried all possibilities and nothing worked
throw new Exception(
$"Could not attach script '{scriptFileName}' to object '{objectName}'. No valid script found or component creation failed."
);
}
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 300f7736385f85e41bf90d820ff46645

View File

@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
namespace UnityMCP.Editor.Helpers
{
/// <summary>
/// Provides static methods for creating standardized success and error response objects.
/// Ensures consistent JSON structure for communication back to the Python server.
/// </summary>
public static class Response
{
/// <summary>
/// Creates a standardized success response object.
/// </summary>
/// <param name="message">A message describing the successful operation.</param>
/// <param name="data">Optional additional data to include in the response.</param>
/// <returns>An object representing the success response.</returns>
public static object Success(string message, object data = null)
{
if (data != null)
{
return new { success = true, message = message, data = data };
}
else
{
return new { success = true, message = message };
}
}
/// <summary>
/// Creates a standardized error response object.
/// </summary>
/// <param name="errorMessage">A message describing the error.</param>
/// <param name="data">Optional additional data (e.g., error details) to include.</param>
/// <returns>An object representing the error response.</returns>
public static object Error(string errorMessage, object data = null)
{
if (data != null)
{
// Note: The key is "error" for error messages, not "message"
return new { success = false, error = errorMessage, data = data };
}
else
{
return new { success = false, error = errorMessage };
}
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 80c09a76b944f8c4691e06c4d76c4be8

View File

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
namespace UnityMCP.Editor.Tools
{
/// <summary>
/// Registry for all MCP command handlers (Refactored Version)
/// </summary>
public static class CommandRegistry
{
// Maps command names (matching those called from Python via ctx.bridge.unity_editor.HandlerName)
// to the corresponding static HandleCommand method in the appropriate tool class.
private static readonly Dictionary<string, Func<JObject, object>> _handlers = new()
{
{ "HandleManageScript", ManageScript.HandleCommand },
{ "HandleManageScene", ManageScene.HandleCommand },
{ "HandleManageEditor", ManageEditor.HandleCommand },
{ "HandleManageGameObject", ManageGameObject.HandleCommand },
{ "HandleManageAsset", ManageAsset.HandleCommand },
{ "HandleReadConsole", ReadConsole.HandleCommand },
{ "HandleExecuteMenuItem", ExecuteMenuItem.HandleCommand }
};
/// <summary>
/// Gets a command handler by name.
/// </summary>
/// <param name="commandName">Name of the command handler (e.g., "HandleManageAsset").</param>
/// <returns>The command handler function if found, null otherwise.</returns>
public static Func<JObject, object> GetHandler(string commandName)
{
// Use case-insensitive comparison for flexibility, although Python side should be consistent
return _handlers.TryGetValue(commandName, out var handler) ? handler : null;
// Consider adding logging here if a handler is not found
/*
if (_handlers.TryGetValue(commandName, out var handler)) {
return handler;
} else {
UnityEngine.Debug.LogError($\"[CommandRegistry] No handler found for command: {commandName}\");
return null;
}
*/
}
}
}

View File

@ -0,0 +1,111 @@
using UnityEngine;
using UnityEditor;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic; // Added for HashSet
using UnityMCP.Editor.Helpers; // For Response class
namespace UnityMCP.Editor.Tools
{
/// <summary>
/// Handles executing Unity Editor menu items by path.
/// </summary>
public static class ExecuteMenuItem
{
// Basic blacklist to prevent accidental execution of potentially disruptive menu items.
// This can be expanded based on needs.
private static readonly HashSet<string> _menuPathBlacklist = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"File/Quit",
// Add other potentially dangerous items like "Edit/Preferences...", "File/Build Settings..." if needed
};
/// <summary>
/// Main handler for executing menu items or getting available ones.
/// </summary>
public static object HandleCommand(JObject @params)
{
string action = @params["action"]?.ToString().ToLower() ?? "execute"; // Default action
try
{
switch (action)
{
case "execute":
return ExecuteItem(@params);
case "get_available_menus":
// Getting a comprehensive list of *all* menu items dynamically is very difficult
// and often requires complex reflection or maintaining a manual list.
// Returning a placeholder/acknowledgement for now.
Debug.LogWarning("[ExecuteMenuItem] 'get_available_menus' action is not fully implemented. Dynamically listing all menu items is complex.");
// Returning an empty list as per the refactor plan's requirements.
return Response.Success("'get_available_menus' action is not fully implemented. Returning empty list.", new List<string>());
// TODO: Consider implementing a basic list of common/known menu items or exploring reflection techniques if this feature becomes critical.
default:
return Response.Error($"Unknown action: '{action}'. Valid actions are 'execute', 'get_available_menus'.");
}
}
catch (Exception e)
{
Debug.LogError($"[ExecuteMenuItem] Action '{action}' failed: {e}");
return Response.Error($"Internal error processing action '{action}': {e.Message}");
}
}
/// <summary>
/// Executes a specific menu item.
/// </summary>
private static object ExecuteItem(JObject @params)
{
string menuPath = @params["menu_path"]?.ToString();
// string alias = @params["alias"]?.ToString(); // TODO: Implement alias mapping based on refactor plan requirements.
// JObject parameters = @params["parameters"] as JObject; // TODO: Investigate parameter passing (often not directly supported by ExecuteMenuItem).
if (string.IsNullOrWhiteSpace(menuPath))
{
return Response.Error("Required parameter 'menu_path' is missing or empty.");
}
// Validate against blacklist
if (_menuPathBlacklist.Contains(menuPath))
{
return Response.Error($"Execution of menu item '{menuPath}' is blocked for safety reasons.");
}
// TODO: Implement alias lookup here if needed (Map alias to actual menuPath).
// if (!string.IsNullOrEmpty(alias)) { menuPath = LookupAlias(alias); if(menuPath == null) return Response.Error(...); }
// TODO: Handle parameters ('parameters' object) if a viable method is found.
// This is complex as EditorApplication.ExecuteMenuItem doesn't take arguments directly.
// It might require finding the underlying EditorWindow or command if parameters are needed.
try
{
// Attempt to execute the menu item on the main thread using delayCall for safety.
EditorApplication.delayCall += () => {
try {
bool executed = EditorApplication.ExecuteMenuItem(menuPath);
// Log potential failure inside the delayed call.
if (!executed) {
Debug.LogError($"[ExecuteMenuItem] Failed to find or execute menu item via delayCall: '{menuPath}'. It might be invalid, disabled, or context-dependent.");
}
} catch (Exception delayEx) {
Debug.LogError($"[ExecuteMenuItem] Exception during delayed execution of '{menuPath}': {delayEx}");
}
};
// Report attempt immediately, as execution is delayed.
return Response.Success($"Attempted to execute menu item: '{menuPath}'. Check Unity logs for confirmation or errors.");
}
catch (Exception e)
{
// Catch errors during setup phase.
Debug.LogError($"[ExecuteMenuItem] Failed to setup execution for '{menuPath}': {e}");
return Response.Error($"Error setting up execution for menu item '{menuPath}': {e.Message}");
}
}
// TODO: Add helper for alias lookup if implementing aliases.
// private static string LookupAlias(string alias) { ... return actualMenuPath or null ... }
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 896e8045986eb0d449ee68395479f1d6

828
Editor/Tools/ManageAsset.cs Normal file
View File

@ -0,0 +1,828 @@
using UnityEngine;
using UnityEditor;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using UnityMCP.Editor.Helpers; // For Response class
using System.Globalization;
namespace UnityMCP.Editor.Tools
{
/// <summary>
/// Handles asset management operations within the Unity project.
/// </summary>
public static class ManageAsset
{
// --- Main Handler ---
public static object HandleCommand(JObject @params)
{
string action = @params["action"]?.ToString().ToLower();
if (string.IsNullOrEmpty(action))
{
return Response.Error("Action parameter is required.");
}
// Common parameters
string path = @params["path"]?.ToString();
try
{
switch (action)
{
case "import":
// Note: Unity typically auto-imports. This might re-import or configure import settings.
return ReimportAsset(path, @params["properties"] as JObject);
case "create":
return CreateAsset(@params);
case "modify":
return ModifyAsset(path, @params["properties"] as JObject);
case "delete":
return DeleteAsset(path);
case "duplicate":
return DuplicateAsset(path, @params["destination"]?.ToString());
case "move": // Often same as rename if within Assets/
case "rename":
return MoveOrRenameAsset(path, @params["destination"]?.ToString());
case "search":
return SearchAssets(@params);
case "get_info":
return GetAssetInfo(path, @params["generatePreview"]?.ToObject<bool>() ?? false);
case "create_folder": // Added specific action for clarity
return CreateFolder(path);
default:
return Response.Error($"Unknown action: '{action}'.");
}
}
catch (Exception e)
{
Debug.LogError($"[ManageAsset] Action '{action}' failed for path '{path}': {e}");
return Response.Error($"Internal error processing action '{action}' on '{path}': {e.Message}");
}
}
// --- Action Implementations ---
private static object ReimportAsset(string path, JObject properties)
{
if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for reimport.");
string fullPath = SanitizeAssetPath(path);
if (!AssetExists(fullPath)) return Response.Error($"Asset not found at path: {fullPath}");
try
{
// TODO: Apply importer properties before reimporting?
// This is complex as it requires getting the AssetImporter, casting it,
// applying properties via reflection or specific methods, saving, then reimporting.
if (properties != null && properties.HasValues)
{
Debug.LogWarning("[ManageAsset.Reimport] Modifying importer properties before reimport is not fully implemented yet.");
// AssetImporter importer = AssetImporter.GetAtPath(fullPath);
// if (importer != null) { /* Apply properties */ AssetDatabase.WriteImportSettingsIfDirty(fullPath); }
}
AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate);
// AssetDatabase.Refresh(); // Usually ImportAsset handles refresh
return Response.Success($"Asset '{fullPath}' reimported.", GetAssetData(fullPath));
}
catch (Exception e)
{
return Response.Error($"Failed to reimport asset '{fullPath}': {e.Message}");
}
}
private static object CreateAsset(JObject @params)
{
string path = @params["path"]?.ToString();
string assetType = @params["assetType"]?.ToString();
JObject properties = @params["properties"] as JObject;
if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for create.");
if (string.IsNullOrEmpty(assetType)) return Response.Error("'assetType' is required for create.");
string fullPath = SanitizeAssetPath(path);
string directory = Path.GetDirectoryName(fullPath);
// Ensure directory exists
if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), directory)))
{
Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), directory));
AssetDatabase.Refresh(); // Make sure Unity knows about the new folder
}
if (AssetExists(fullPath)) return Response.Error($"Asset already exists at path: {fullPath}");
try
{
UnityEngine.Object newAsset = null;
string lowerAssetType = assetType.ToLowerInvariant();
// Handle common asset types
if (lowerAssetType == "folder")
{
return CreateFolder(path); // Use dedicated method
}
else if (lowerAssetType == "material")
{
Material mat = new Material(Shader.Find("Standard")); // Default shader
// TODO: Apply properties from JObject (e.g., shader name, color, texture assignments)
if(properties != null) ApplyMaterialProperties(mat, properties);
AssetDatabase.CreateAsset(mat, fullPath);
newAsset = mat;
}
else if (lowerAssetType == "scriptableobject")
{
string scriptClassName = properties?["scriptClass"]?.ToString();
if (string.IsNullOrEmpty(scriptClassName)) return Response.Error("'scriptClass' property required when creating ScriptableObject asset.");
Type scriptType = FindType(scriptClassName);
if (scriptType == null || !typeof(ScriptableObject).IsAssignableFrom(scriptType))
{
return Response.Error($"Script class '{scriptClassName}' not found or does not inherit from ScriptableObject.");
}
ScriptableObject so = ScriptableObject.CreateInstance(scriptType);
// TODO: Apply properties from JObject to the ScriptableObject instance?
AssetDatabase.CreateAsset(so, fullPath);
newAsset = so;
}
else if (lowerAssetType == "prefab")
{
// Creating prefabs usually involves saving an existing GameObject hierarchy.
// A common pattern is to create an empty GameObject, configure it, and then save it.
return Response.Error("Creating prefabs programmatically usually requires a source GameObject. Use manage_gameobject to create/configure, then save as prefab via a separate mechanism or future enhancement.");
// Example (conceptual):
// GameObject source = GameObject.Find(properties["sourceGameObject"].ToString());
// if(source != null) PrefabUtility.SaveAsPrefabAsset(source, fullPath);
}
// TODO: Add more asset types (Animation Controller, Scene, etc.)
else
{
// Generic creation attempt (might fail or create empty files)
// For some types, just creating the file might be enough if Unity imports it.
// File.Create(Path.Combine(Directory.GetCurrentDirectory(), fullPath)).Close();
// AssetDatabase.ImportAsset(fullPath); // Let Unity try to import it
// newAsset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(fullPath);
return Response.Error($"Creation for asset type '{assetType}' is not explicitly supported yet. Supported: Folder, Material, ScriptableObject.");
}
if (newAsset == null && !Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), fullPath))) // Check if it wasn't a folder and asset wasn't created
{
return Response.Error($"Failed to create asset '{assetType}' at '{fullPath}'. See logs for details.");
}
AssetDatabase.SaveAssets();
// AssetDatabase.Refresh(); // CreateAsset often handles refresh
return Response.Success($"Asset '{fullPath}' created successfully.", GetAssetData(fullPath));
}
catch (Exception e)
{
return Response.Error($"Failed to create asset at '{fullPath}': {e.Message}");
}
}
private static object CreateFolder(string path)
{
if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for create_folder.");
string fullPath = SanitizeAssetPath(path);
string parentDir = Path.GetDirectoryName(fullPath);
string folderName = Path.GetFileName(fullPath);
if (AssetExists(fullPath))
{
// Check if it's actually a folder already
if (AssetDatabase.IsValidFolder(fullPath))
{
return Response.Success($"Folder already exists at path: {fullPath}", GetAssetData(fullPath));
}
else
{
return Response.Error($"An asset (not a folder) already exists at path: {fullPath}");
}
}
try
{
// Ensure parent exists
if (!string.IsNullOrEmpty(parentDir) && !AssetDatabase.IsValidFolder(parentDir)) {
// Recursively create parent folders if needed (AssetDatabase handles this internally)
// Or we can do it manually: Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), parentDir)); AssetDatabase.Refresh();
}
string guid = AssetDatabase.CreateFolder(parentDir, folderName);
if (string.IsNullOrEmpty(guid)) {
return Response.Error($"Failed to create folder '{fullPath}'. Check logs and permissions.");
}
// AssetDatabase.Refresh(); // CreateFolder usually handles refresh
return Response.Success($"Folder '{fullPath}' created successfully.", GetAssetData(fullPath));
}
catch (Exception e)
{
return Response.Error($"Failed to create folder '{fullPath}': {e.Message}");
}
}
private static object ModifyAsset(string path, JObject properties)
{
if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for modify.");
if (properties == null || !properties.HasValues) return Response.Error("'properties' are required for modify.");
string fullPath = SanitizeAssetPath(path);
if (!AssetExists(fullPath)) return Response.Error($"Asset not found at path: {fullPath}");
try
{
UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(fullPath);
if (asset == null) return Response.Error($"Failed to load asset at path: {fullPath}");
bool modified = false;
// Example: Modifying a Material
if (asset is Material material)
{
modified = ApplyMaterialProperties(material, properties);
}
// Example: Modifying a ScriptableObject (more complex, needs reflection or specific interface)
else if (asset is ScriptableObject so)
{
modified = ApplyObjectProperties(so, properties); // General helper
}
// Example: Modifying TextureImporter settings
else if (asset is Texture) {
AssetImporter importer = AssetImporter.GetAtPath(fullPath);
if (importer is TextureImporter textureImporter)
{
modified = ApplyObjectProperties(textureImporter, properties);
if (modified) {
// Importer settings need saving
AssetDatabase.WriteImportSettingsIfDirty(fullPath);
AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); // Reimport to apply changes
}
}
else {
Debug.LogWarning($"Could not get TextureImporter for {fullPath}.");
}
}
// TODO: Add modification logic for other common asset types (Models, AudioClips importers, etc.)
else
{
Debug.LogWarning($"Modification for asset type '{asset.GetType().Name}' at '{fullPath}' is not fully implemented. Attempting generic property setting.");
modified = ApplyObjectProperties(asset, properties);
}
if (modified)
{
EditorUtility.SetDirty(asset); // Mark the asset itself as dirty
AssetDatabase.SaveAssets(); // Save changes to disk
// AssetDatabase.Refresh(); // SaveAssets usually handles refresh
return Response.Success($"Asset '{fullPath}' modified successfully.", GetAssetData(fullPath));
} else {
return Response.Success($"No applicable properties found to modify for asset '{fullPath}'.", GetAssetData(fullPath));
}
}
catch (Exception e)
{
return Response.Error($"Failed to modify asset '{fullPath}': {e.Message}");
}
}
private static object DeleteAsset(string path)
{
if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for delete.");
string fullPath = SanitizeAssetPath(path);
if (!AssetExists(fullPath)) return Response.Error($"Asset not found at path: {fullPath}");
try
{
bool success = AssetDatabase.DeleteAsset(fullPath);
if (success)
{
// AssetDatabase.Refresh(); // DeleteAsset usually handles refresh
return Response.Success($"Asset '{fullPath}' deleted successfully.");
}
else
{
// This might happen if the file couldn't be deleted (e.g., locked)
return Response.Error($"Failed to delete asset '{fullPath}'. Check logs or if the file is locked.");
}
}
catch (Exception e)
{
return Response.Error($"Error deleting asset '{fullPath}': {e.Message}");
}
}
private static object DuplicateAsset(string path, string destinationPath)
{
if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for duplicate.");
string sourcePath = SanitizeAssetPath(path);
if (!AssetExists(sourcePath)) return Response.Error($"Source asset not found at path: {sourcePath}");
string destPath;
if (string.IsNullOrEmpty(destinationPath))
{
// Generate a unique path if destination is not provided
destPath = AssetDatabase.GenerateUniqueAssetPath(sourcePath);
}
else
{
destPath = SanitizeAssetPath(destinationPath);
if (AssetExists(destPath)) return Response.Error($"Asset already exists at destination path: {destPath}");
// Ensure destination directory exists
EnsureDirectoryExists(Path.GetDirectoryName(destPath));
}
try
{
bool success = AssetDatabase.CopyAsset(sourcePath, destPath);
if (success)
{
// AssetDatabase.Refresh();
return Response.Success($"Asset '{sourcePath}' duplicated to '{destPath}'.", GetAssetData(destPath));
}
else
{
return Response.Error($"Failed to duplicate asset from '{sourcePath}' to '{destPath}'.");
}
}
catch (Exception e)
{
return Response.Error($"Error duplicating asset '{sourcePath}': {e.Message}");
}
}
private static object MoveOrRenameAsset(string path, string destinationPath)
{
if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for move/rename.");
if (string.IsNullOrEmpty(destinationPath)) return Response.Error("'destination' path is required for move/rename.");
string sourcePath = SanitizeAssetPath(path);
string destPath = SanitizeAssetPath(destinationPath);
if (!AssetExists(sourcePath)) return Response.Error($"Source asset not found at path: {sourcePath}");
if (AssetExists(destPath)) return Response.Error($"An asset already exists at the destination path: {destPath}");
// Ensure destination directory exists
EnsureDirectoryExists(Path.GetDirectoryName(destPath));
try
{
// Validate will return an error string if failed, null if successful
string error = AssetDatabase.ValidateMoveAsset(sourcePath, destPath);
if (!string.IsNullOrEmpty(error))
{
return Response.Error($"Failed to move/rename asset from '{sourcePath}' to '{destPath}': {error}");
}
string guid = AssetDatabase.MoveAsset(sourcePath, destPath);
if (!string.IsNullOrEmpty(guid)) // MoveAsset returns the new GUID on success
{
// AssetDatabase.Refresh(); // MoveAsset usually handles refresh
return Response.Success($"Asset moved/renamed from '{sourcePath}' to '{destPath}'.", GetAssetData(destPath));
}
else
{
// This case might not be reachable if ValidateMoveAsset passes, but good to have
return Response.Error($"MoveAsset call failed unexpectedly for '{sourcePath}' to '{destPath}'.");
}
}
catch (Exception e)
{
return Response.Error($"Error moving/renaming asset '{sourcePath}': {e.Message}");
}
}
private static object SearchAssets(JObject @params)
{
string searchPattern = @params["searchPattern"]?.ToString();
string filterType = @params["filterType"]?.ToString();
string pathScope = @params["path"]?.ToString(); // Use path as folder scope
string filterDateAfterStr = @params["filterDateAfter"]?.ToString();
int pageSize = @params["pageSize"]?.ToObject<int?>() ?? 50; // Default page size
int pageNumber = @params["pageNumber"]?.ToObject<int?>() ?? 1; // Default page number (1-based)
bool generatePreview = @params["generatePreview"]?.ToObject<bool>() ?? false;
List<string> searchFilters = new List<string>();
if (!string.IsNullOrEmpty(searchPattern)) searchFilters.Add(searchPattern);
if (!string.IsNullOrEmpty(filterType)) searchFilters.Add($"t:{filterType}");
string[] folderScope = null;
if (!string.IsNullOrEmpty(pathScope))
{
folderScope = new string[] { SanitizeAssetPath(pathScope) };
if (!AssetDatabase.IsValidFolder(folderScope[0])) {
// Maybe the user provided a file path instead of a folder?
// We could search in the containing folder, or return an error.
Debug.LogWarning($"Search path '{folderScope[0]}' is not a valid folder. Searching entire project.");
folderScope = null; // Search everywhere if path isn't a folder
}
}
DateTime? filterDateAfter = null;
if (!string.IsNullOrEmpty(filterDateAfterStr)) {
if (DateTime.TryParse(filterDateAfterStr, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime parsedDate)) {
filterDateAfter = parsedDate;
} else {
Debug.LogWarning($"Could not parse filterDateAfter: '{filterDateAfterStr}'. Expected ISO 8601 format.");
}
}
try
{
string[] guids = AssetDatabase.FindAssets(string.Join(" ", searchFilters), folderScope);
List<object> results = new List<object>();
int totalFound = 0;
foreach (string guid in guids)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
if (string.IsNullOrEmpty(assetPath)) continue;
// Apply date filter if present
if (filterDateAfter.HasValue) {
DateTime lastWriteTime = File.GetLastWriteTimeUtc(Path.Combine(Directory.GetCurrentDirectory(), assetPath));
if (lastWriteTime <= filterDateAfter.Value) {
continue; // Skip assets older than or equal to the filter date
}
}
totalFound++; // Count matching assets before pagination
results.Add(GetAssetData(assetPath, generatePreview));
}
// Apply pagination
int startIndex = (pageNumber - 1) * pageSize;
var pagedResults = results.Skip(startIndex).Take(pageSize).ToList();
return Response.Success($"Found {totalFound} asset(s). Returning page {pageNumber} ({pagedResults.Count} assets).", new {
totalAssets = totalFound,
pageSize = pageSize,
pageNumber = pageNumber,
assets = pagedResults
});
}
catch (Exception e)
{
return Response.Error($"Error searching assets: {e.Message}");
}
}
private static object GetAssetInfo(string path, bool generatePreview)
{
if (string.IsNullOrEmpty(path)) return Response.Error("'path' is required for get_info.");
string fullPath = SanitizeAssetPath(path);
if (!AssetExists(fullPath)) return Response.Error($"Asset not found at path: {fullPath}");
try
{
return Response.Success("Asset info retrieved.", GetAssetData(fullPath, generatePreview));
}
catch (Exception e)
{
return Response.Error($"Error getting info for asset '{fullPath}': {e.Message}");
}
}
// --- Internal Helpers ---
/// <summary>
/// Ensures the asset path starts with "Assets/".
/// </summary>
private static string SanitizeAssetPath(string path)
{
if (string.IsNullOrEmpty(path)) return path;
path = path.Replace('\\', '/'); // Normalize separators
if (!path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
return "Assets/" + path.TrimStart('/');
}
return path;
}
/// <summary>
/// Checks if an asset exists at the given path (file or folder).
/// </summary>
private static bool AssetExists(string sanitizedPath)
{
// AssetDatabase APIs are generally preferred over raw File/Directory checks for assets.
// Check if it's a known asset GUID.
if (!string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(sanitizedPath)))
{
return true;
}
// AssetPathToGUID might not work for newly created folders not yet refreshed.
// Check directory explicitly for folders.
if(Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), sanitizedPath))) {
// Check if it's considered a *valid* folder by Unity
return AssetDatabase.IsValidFolder(sanitizedPath);
}
// Check file existence for non-folder assets.
if(File.Exists(Path.Combine(Directory.GetCurrentDirectory(), sanitizedPath))){
return true; // Assume if file exists, it's an asset or will be imported
}
return false;
// Alternative: return !string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(sanitizedPath));
}
/// <summary>
/// Ensures the directory for a given asset path exists, creating it if necessary.
/// </summary>
private static void EnsureDirectoryExists(string directoryPath)
{
if (string.IsNullOrEmpty(directoryPath)) return;
string fullDirPath = Path.Combine(Directory.GetCurrentDirectory(), directoryPath);
if (!Directory.Exists(fullDirPath))
{
Directory.CreateDirectory(fullDirPath);
AssetDatabase.Refresh(); // Let Unity know about the new folder
}
}
/// <summary>
/// Applies properties from JObject to a Material.
/// </summary>
private static bool ApplyMaterialProperties(Material mat, JObject properties)
{
if (mat == null || properties == null) return false;
bool modified = false;
// Example: Set shader
if (properties["shader"]?.Type == JTokenType.String) {
Shader newShader = Shader.Find(properties["shader"].ToString());
if (newShader != null && mat.shader != newShader) {
mat.shader = newShader;
modified = true;
}
}
// Example: Set color property
if (properties["color"] is JObject colorProps) {
string propName = colorProps["name"]?.ToString() ?? "_Color"; // Default main color
if (colorProps["value"] is JArray colArr && colArr.Count >= 3) {
try {
Color newColor = new Color(
colArr[0].ToObject<float>(),
colArr[1].ToObject<float>(),
colArr[2].ToObject<float>(),
colArr.Count > 3 ? colArr[3].ToObject<float>() : 1.0f
);
if(mat.HasProperty(propName) && mat.GetColor(propName) != newColor) {
mat.SetColor(propName, newColor);
modified = true;
}
} catch (Exception ex) { Debug.LogWarning($"Error parsing color property '{propName}': {ex.Message}"); }
}
}
// Example: Set float property
if (properties["float"] is JObject floatProps) {
string propName = floatProps["name"]?.ToString();
if (!string.IsNullOrEmpty(propName) && floatProps["value"]?.Type == JTokenType.Float || floatProps["value"]?.Type == JTokenType.Integer) {
try {
float newVal = floatProps["value"].ToObject<float>();
if (mat.HasProperty(propName) && mat.GetFloat(propName) != newVal) {
mat.SetFloat(propName, newVal);
modified = true;
}
} catch (Exception ex) { Debug.LogWarning($"Error parsing float property '{propName}': {ex.Message}"); }
}
}
// Example: Set texture property
if (properties["texture"] is JObject texProps) {
string propName = texProps["name"]?.ToString() ?? "_MainTex"; // Default main texture
string texPath = texProps["path"]?.ToString();
if (!string.IsNullOrEmpty(texPath)) {
Texture newTex = AssetDatabase.LoadAssetAtPath<Texture>(SanitizeAssetPath(texPath));
if (newTex != null && mat.HasProperty(propName) && mat.GetTexture(propName) != newTex) {
mat.SetTexture(propName, newTex);
modified = true;
}
else if(newTex == null) {
Debug.LogWarning($"Texture not found at path: {texPath}");
}
}
}
// TODO: Add handlers for other property types (Vectors, Ints, Keywords, RenderQueue, etc.)
return modified;
}
/// <summary>
/// Generic helper to set properties on any UnityEngine.Object using reflection.
/// </summary>
private static bool ApplyObjectProperties(UnityEngine.Object target, JObject properties)
{
if (target == null || properties == null) return false;
bool modified = false;
Type type = target.GetType();
foreach (var prop in properties.Properties())
{
string propName = prop.Name;
JToken propValue = prop.Value;
if (SetPropertyOrField(target, propName, propValue, type))
{
modified = true;
}
}
return modified;
}
/// <summary>
/// Helper to set a property or field via reflection, handling basic types and Unity objects.
/// </summary>
private static bool SetPropertyOrField(object target, string memberName, JToken value, Type type = null)
{
type = type ?? target.GetType();
System.Reflection.BindingFlags flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.IgnoreCase;
try
{
System.Reflection.PropertyInfo propInfo = type.GetProperty(memberName, flags);
if (propInfo != null && propInfo.CanWrite)
{
object convertedValue = ConvertJTokenToType(value, propInfo.PropertyType);
if (convertedValue != null && !object.Equals(propInfo.GetValue(target), convertedValue))
{
propInfo.SetValue(target, convertedValue);
return true;
}
}
else
{
System.Reflection.FieldInfo fieldInfo = type.GetField(memberName, flags);
if (fieldInfo != null)
{
object convertedValue = ConvertJTokenToType(value, fieldInfo.FieldType);
if (convertedValue != null && !object.Equals(fieldInfo.GetValue(target), convertedValue))
{
fieldInfo.SetValue(target, convertedValue);
return true;
}
}
}
}
catch (Exception ex)
{
Debug.LogWarning($"[SetPropertyOrField] Failed to set '{memberName}' on {type.Name}: {ex.Message}");
}
return false;
}
/// <summary>
/// Simple JToken to Type conversion for common Unity types and primitives.
/// </summary>
private static object ConvertJTokenToType(JToken token, Type targetType)
{
try
{
if (token == null || token.Type == JTokenType.Null) return null;
if (targetType == typeof(string)) return token.ToObject<string>();
if (targetType == typeof(int)) return token.ToObject<int>();
if (targetType == typeof(float)) return token.ToObject<float>();
if (targetType == typeof(bool)) return token.ToObject<bool>();
if (targetType == typeof(Vector2) && token is JArray arrV2 && arrV2.Count == 2)
return new Vector2(arrV2[0].ToObject<float>(), arrV2[1].ToObject<float>());
if (targetType == typeof(Vector3) && token is JArray arrV3 && arrV3.Count == 3)
return new Vector3(arrV3[0].ToObject<float>(), arrV3[1].ToObject<float>(), arrV3[2].ToObject<float>());
if (targetType == typeof(Vector4) && token is JArray arrV4 && arrV4.Count == 4)
return new Vector4(arrV4[0].ToObject<float>(), arrV4[1].ToObject<float>(), arrV4[2].ToObject<float>(), arrV4[3].ToObject<float>());
if (targetType == typeof(Quaternion) && token is JArray arrQ && arrQ.Count == 4)
return new Quaternion(arrQ[0].ToObject<float>(), arrQ[1].ToObject<float>(), arrQ[2].ToObject<float>(), arrQ[3].ToObject<float>());
if (targetType == typeof(Color) && token is JArray arrC && arrC.Count >= 3) // Allow RGB or RGBA
return new Color(arrC[0].ToObject<float>(), arrC[1].ToObject<float>(), arrC[2].ToObject<float>(), arrC.Count > 3 ? arrC[3].ToObject<float>() : 1.0f);
if (targetType.IsEnum)
return Enum.Parse(targetType, token.ToString(), true); // Case-insensitive enum parsing
// Handle loading Unity Objects (Materials, Textures, etc.) by path
if (typeof(UnityEngine.Object).IsAssignableFrom(targetType) && token.Type == JTokenType.String)
{
string assetPath = SanitizeAssetPath(token.ToString());
UnityEngine.Object loadedAsset = AssetDatabase.LoadAssetAtPath(assetPath, targetType);
if (loadedAsset == null) {
Debug.LogWarning($"[ConvertJTokenToType] Could not load asset of type {targetType.Name} from path: {assetPath}");
}
return loadedAsset;
}
// Fallback: Try direct conversion (might work for other simple value types)
return token.ToObject(targetType);
}
catch (Exception ex)
{
Debug.LogWarning($"[ConvertJTokenToType] Could not convert JToken '{token}' (type {token.Type}) to type '{targetType.Name}': {ex.Message}");
return null;
}
}
/// <summary>
/// Helper to find a Type by name, searching relevant assemblies.
/// Needed for creating ScriptableObjects or finding component types by name.
/// </summary>
private static Type FindType(string typeName)
{
if (string.IsNullOrEmpty(typeName)) return null;
// Try direct lookup first (common Unity types often don't need assembly qualified name)
var type = Type.GetType(typeName) ??
Type.GetType($"UnityEngine.{typeName}, UnityEngine.CoreModule") ??
Type.GetType($"UnityEngine.UI.{typeName}, UnityEngine.UI") ??
Type.GetType($"UnityEditor.{typeName}, UnityEditor.CoreModule");
if (type != null) return type;
// If not found, search loaded assemblies (slower but more robust for user scripts)
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
// Look for non-namespaced first
type = assembly.GetType(typeName, false, true); // throwOnError=false, ignoreCase=true
if (type != null) return type;
// Check common namespaces if simple name given
type = assembly.GetType("UnityEngine." + typeName, false, true);
if (type != null) return type;
type = assembly.GetType("UnityEditor." + typeName, false, true);
if (type != null) return type;
// Add other likely namespaces if needed (e.g., specific plugins)
}
Debug.LogWarning($"[FindType] Type '{typeName}' not found in any loaded assembly.");
return null; // Not found
}
// --- Data Serialization ---
/// <summary>
/// Creates a serializable representation of an asset.
/// </summary>
private static object GetAssetData(string path, bool generatePreview = false)
{
if (string.IsNullOrEmpty(path) || !AssetExists(path)) return null;
string guid = AssetDatabase.AssetPathToGUID(path);
Type assetType = AssetDatabase.GetMainAssetTypeAtPath(path);
UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
string previewBase64 = null;
int previewWidth = 0;
int previewHeight = 0;
if (generatePreview && asset != null)
{
Texture2D preview = AssetPreview.GetAssetPreview(asset);
if (preview != null)
{
try {
// Ensure texture is readable for EncodeToPNG
// Creating a temporary readable copy is safer
RenderTexture rt = RenderTexture.GetTemporary(preview.width, preview.height);
Graphics.Blit(preview, rt);
RenderTexture previous = RenderTexture.active;
RenderTexture.active = rt;
Texture2D readablePreview = new Texture2D(preview.width, preview.height);
readablePreview.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
readablePreview.Apply();
RenderTexture.active = previous;
RenderTexture.ReleaseTemporary(rt);
byte[] pngData = readablePreview.EncodeToPNG();
previewBase64 = Convert.ToBase64String(pngData);
previewWidth = readablePreview.width;
previewHeight = readablePreview.height;
UnityEngine.Object.DestroyImmediate(readablePreview); // Clean up temp texture
} catch (Exception ex) {
Debug.LogWarning($"Failed to generate readable preview for '{path}': {ex.Message}. Preview might not be readable.");
// Fallback: Try getting static preview if available?
// Texture2D staticPreview = AssetPreview.GetMiniThumbnail(asset);
}
}
else
{
Debug.LogWarning($"Could not get asset preview for {path} (Type: {assetType?.Name}). Is it supported?");
}
}
return new
{
path = path,
guid = guid,
assetType = assetType?.FullName ?? "Unknown",
name = Path.GetFileNameWithoutExtension(path),
fileName = Path.GetFileName(path),
isFolder = AssetDatabase.IsValidFolder(path),
instanceID = asset?.GetInstanceID() ?? 0,
lastWriteTimeUtc = File.GetLastWriteTimeUtc(Path.Combine(Directory.GetCurrentDirectory(), path)).ToString("o"), // ISO 8601
// --- Preview Data ---
previewBase64 = previewBase64, // PNG data as Base64 string
previewWidth = previewWidth,
previewHeight = previewHeight
// TODO: Add more metadata? Importer settings? Dependencies?
};
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: de90a1d9743a2874cb235cf0b83444b1

View File

@ -0,0 +1,532 @@
using UnityEngine;
using UnityEditor;
using Newtonsoft.Json.Linq;
using System;
using System.Linq;
using System.Collections.Generic;
using UnityMCP.Editor.Helpers; // For Response class
using UnityEditor.ShortcutManagement;
using UnityEditorInternal; // Required for tag management
using System.Reflection; // Required for layer management
namespace UnityMCP.Editor.Tools
{
/// <summary>
/// Handles operations related to controlling and querying the Unity Editor state,
/// including managing Tags and Layers.
/// </summary>
public static class ManageEditor
{
// Constant for starting user layer index
private const int FirstUserLayerIndex = 8;
// Constant for total layer count
private const int TotalLayerCount = 32;
/// <summary>
/// Main handler for editor management actions.
/// </summary>
public static object HandleCommand(JObject @params)
{
string action = @params["action"]?.ToString().ToLower();
// Parameters for specific actions
string tagName = @params["tagName"]?.ToString();
string layerName = @params["layerName"]?.ToString();
bool waitForCompletion = @params["waitForCompletion"]?.ToObject<bool>() ?? false; // Example - not used everywhere
if (string.IsNullOrEmpty(action))
{
return Response.Error("Action parameter is required.");
}
// Route action
switch (action)
{
// Play Mode Control
case "play":
try
{
if (!EditorApplication.isPlaying)
{
EditorApplication.isPlaying = true;
return Response.Success("Entered play mode.");
}
return Response.Success("Already in play mode.");
}
catch (Exception e)
{
return Response.Error($"Error entering play mode: {e.Message}");
}
case "pause":
try
{
if (EditorApplication.isPlaying)
{
EditorApplication.isPaused = !EditorApplication.isPaused;
return Response.Success(EditorApplication.isPaused ? "Game paused." : "Game resumed.");
}
return Response.Error("Cannot pause/resume: Not in play mode.");
}
catch (Exception e)
{
return Response.Error($"Error pausing/resuming game: {e.Message}");
}
case "stop":
try
{
if (EditorApplication.isPlaying)
{
EditorApplication.isPlaying = false;
return Response.Success("Exited play mode.");
}
return Response.Success("Already stopped (not in play mode).");
}
catch (Exception e)
{
return Response.Error($"Error stopping play mode: {e.Message}");
}
// Editor State/Info
case "get_state":
return GetEditorState();
case "get_windows":
return GetEditorWindows();
case "get_active_tool":
return GetActiveTool();
case "get_selection":
return GetSelection();
case "set_active_tool":
string toolName = @params["toolName"]?.ToString();
if (string.IsNullOrEmpty(toolName)) return Response.Error("'toolName' parameter required for set_active_tool.");
return SetActiveTool(toolName);
// Tag Management
case "add_tag":
if (string.IsNullOrEmpty(tagName)) return Response.Error("'tagName' parameter required for add_tag.");
return AddTag(tagName);
case "remove_tag":
if (string.IsNullOrEmpty(tagName)) return Response.Error("'tagName' parameter required for remove_tag.");
return RemoveTag(tagName);
case "get_tags":
return GetTags(); // Helper to list current tags
// Layer Management
case "add_layer":
if (string.IsNullOrEmpty(layerName)) return Response.Error("'layerName' parameter required for add_layer.");
return AddLayer(layerName);
case "remove_layer":
if (string.IsNullOrEmpty(layerName)) return Response.Error("'layerName' parameter required for remove_layer.");
return RemoveLayer(layerName);
case "get_layers":
return GetLayers(); // Helper to list current layers
// --- Settings (Example) ---
// case "set_resolution":
// int? width = @params["width"]?.ToObject<int?>();
// int? height = @params["height"]?.ToObject<int?>();
// if (!width.HasValue || !height.HasValue) return Response.Error("'width' and 'height' parameters required.");
// return SetGameViewResolution(width.Value, height.Value);
// case "set_quality":
// // Handle string name or int index
// return SetQualityLevel(@params["qualityLevel"]);
default:
return Response.Error($"Unknown action: '{action}'. Supported actions include play, pause, stop, get_state, get_windows, get_active_tool, get_selection, set_active_tool, add_tag, remove_tag, get_tags, add_layer, remove_layer, get_layers.");
}
}
// --- Editor State/Info Methods ---
private static object GetEditorState()
{
try
{
var state = new
{
isPlaying = EditorApplication.isPlaying,
isPaused = EditorApplication.isPaused,
isCompiling = EditorApplication.isCompiling,
isUpdating = EditorApplication.isUpdating,
applicationPath = EditorApplication.applicationPath,
applicationContentsPath = EditorApplication.applicationContentsPath,
timeSinceStartup = EditorApplication.timeSinceStartup
};
return Response.Success("Retrieved editor state.", state);
}
catch (Exception e)
{
return Response.Error($"Error getting editor state: {e.Message}");
}
}
private static object GetEditorWindows()
{
try
{
// Get all types deriving from EditorWindow
var windowTypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(assembly => assembly.GetTypes())
.Where(type => type.IsSubclassOf(typeof(EditorWindow)))
.ToList();
var openWindows = new List<object>();
// Find currently open instances
// Resources.FindObjectsOfTypeAll seems more reliable than GetWindow for finding *all* open windows
EditorWindow[] allWindows = Resources.FindObjectsOfTypeAll<EditorWindow>();
foreach (EditorWindow window in allWindows)
{
if (window == null) continue; // Skip potentially destroyed windows
try
{
openWindows.Add(new
{
title = window.titleContent.text,
typeName = window.GetType().FullName,
isFocused = EditorWindow.focusedWindow == window,
position = new { x = window.position.x, y = window.position.y, width = window.position.width, height = window.position.height },
instanceID = window.GetInstanceID()
});
}
catch (Exception ex)
{
Debug.LogWarning($"Could not get info for window {window.GetType().Name}: {ex.Message}");
}
}
return Response.Success("Retrieved list of open editor windows.", openWindows);
}
catch (Exception e)
{
return Response.Error($"Error getting editor windows: {e.Message}");
}
}
private static object GetActiveTool()
{
try
{
Tool currentTool = UnityEditor.Tools.current;
string toolName = currentTool.ToString(); // Enum to string
bool customToolActive = UnityEditor.Tools.current == Tool.Custom; // Check if a custom tool is active
string activeToolName = customToolActive ? EditorTools.GetActiveToolName() : toolName; // Get custom name if needed
var toolInfo = new {
activeTool = activeToolName,
isCustom = customToolActive,
pivotMode = UnityEditor.Tools.pivotMode.ToString(),
pivotRotation = UnityEditor.Tools.pivotRotation.ToString(),
handleRotation = UnityEditor.Tools.handleRotation.eulerAngles, // Euler for simplicity
handlePosition = UnityEditor.Tools.handlePosition
};
return Response.Success("Retrieved active tool information.", toolInfo);
}
catch (Exception e)
{
return Response.Error($"Error getting active tool: {e.Message}");
}
}
private static object SetActiveTool(string toolName)
{
try
{
Tool targetTool;
if (Enum.TryParse<Tool>(toolName, true, out targetTool)) // Case-insensitive parse
{
// Check if it's a valid built-in tool
if (targetTool != Tool.None && targetTool <= Tool.Custom) // Tool.Custom is the last standard tool
{
UnityEditor.Tools.current = targetTool;
return Response.Success($"Set active tool to '{targetTool}'.");
}
else
{
return Response.Error($"Cannot directly set tool to '{toolName}'. It might be None, Custom, or invalid.");
}
}
else
{
// Potentially try activating a custom tool by name here if needed
// This often requires specific editor scripting knowledge for that tool.
return Response.Error($"Could not parse '{toolName}' as a standard Unity Tool (View, Move, Rotate, Scale, Rect, Transform, Custom).");
}
}
catch (Exception e)
{
return Response.Error($"Error setting active tool: {e.Message}");
}
}
private static object GetSelection()
{
try
{
var selectionInfo = new
{
activeObject = Selection.activeObject?.name,
activeGameObject = Selection.activeGameObject?.name,
activeTransform = Selection.activeTransform?.name,
activeInstanceID = Selection.activeInstanceID,
count = Selection.count,
objects = Selection.objects.Select(obj => new { name = obj?.name, type = obj?.GetType().FullName, instanceID = obj?.GetInstanceID() }).ToList(),
gameObjects = Selection.gameObjects.Select(go => new { name = go?.name, instanceID = go?.GetInstanceID() }).ToList(),
assetGUIDs = Selection.assetGUIDs // GUIDs for selected assets in Project view
};
return Response.Success("Retrieved current selection details.", selectionInfo);
}
catch (Exception e)
{
return Response.Error($"Error getting selection: {e.Message}");
}
}
// --- Tag Management Methods ---
private static object AddTag(string tagName)
{
if (string.IsNullOrWhiteSpace(tagName))
return Response.Error("Tag name cannot be empty or whitespace.");
// Check if tag already exists
if (InternalEditorUtility.tags.Contains(tagName))
{
return Response.Error($"Tag '{tagName}' already exists.");
}
try
{
// Add the tag using the internal utility
InternalEditorUtility.AddTag(tagName);
// Force save assets to ensure the change persists in the TagManager asset
AssetDatabase.SaveAssets();
return Response.Success($"Tag '{tagName}' added successfully.");
}
catch (Exception e)
{
return Response.Error($"Failed to add tag '{tagName}': {e.Message}");
}
}
private static object RemoveTag(string tagName)
{
if (string.IsNullOrWhiteSpace(tagName))
return Response.Error("Tag name cannot be empty or whitespace.");
if (tagName.Equals("Untagged", StringComparison.OrdinalIgnoreCase))
return Response.Error("Cannot remove the built-in 'Untagged' tag.");
// Check if tag exists before attempting removal
if (!InternalEditorUtility.tags.Contains(tagName))
{
return Response.Error($"Tag '{tagName}' does not exist.");
}
try
{
// Remove the tag using the internal utility
InternalEditorUtility.RemoveTag(tagName);
// Force save assets
AssetDatabase.SaveAssets();
return Response.Success($"Tag '{tagName}' removed successfully.");
}
catch (Exception e)
{
// Catch potential issues if the tag is somehow in use or removal fails
return Response.Error($"Failed to remove tag '{tagName}': {e.Message}");
}
}
private static object GetTags()
{
try
{
string[] tags = InternalEditorUtility.tags;
return Response.Success("Retrieved current tags.", tags);
}
catch (Exception e)
{
return Response.Error($"Failed to retrieve tags: {e.Message}");
}
}
// --- Layer Management Methods ---
private static object AddLayer(string layerName)
{
if (string.IsNullOrWhiteSpace(layerName))
return Response.Error("Layer name cannot be empty or whitespace.");
// Access the TagManager asset
SerializedObject tagManager = GetTagManager();
if (tagManager == null) return Response.Error("Could not access TagManager asset.");
SerializedProperty layersProp = tagManager.FindProperty("layers");
if (layersProp == null || !layersProp.isArray)
return Response.Error("Could not find 'layers' property in TagManager.");
// Check if layer name already exists (case-insensitive check recommended)
for (int i = 0; i < TotalLayerCount; i++)
{
SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i);
if (layerSP != null && layerName.Equals(layerSP.stringValue, StringComparison.OrdinalIgnoreCase))
{
return Response.Error($"Layer '{layerName}' already exists at index {i}.");
}
}
// Find the first empty user layer slot (indices 8 to 31)
int firstEmptyUserLayer = -1;
for (int i = FirstUserLayerIndex; i < TotalLayerCount; i++)
{
SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i);
if (layerSP != null && string.IsNullOrEmpty(layerSP.stringValue))
{
firstEmptyUserLayer = i;
break;
}
}
if (firstEmptyUserLayer == -1)
{
return Response.Error("No empty User Layer slots available (8-31 are full).");
}
// Assign the name to the found slot
try
{
SerializedProperty targetLayerSP = layersProp.GetArrayElementAtIndex(firstEmptyUserLayer);
targetLayerSP.stringValue = layerName;
// Apply the changes to the TagManager asset
tagManager.ApplyModifiedProperties();
// Save assets to make sure it's written to disk
AssetDatabase.SaveAssets();
return Response.Success($"Layer '{layerName}' added successfully to slot {firstEmptyUserLayer}.");
}
catch (Exception e)
{
return Response.Error($"Failed to add layer '{layerName}': {e.Message}");
}
}
private static object RemoveLayer(string layerName)
{
if (string.IsNullOrWhiteSpace(layerName))
return Response.Error("Layer name cannot be empty or whitespace.");
// Access the TagManager asset
SerializedObject tagManager = GetTagManager();
if (tagManager == null) return Response.Error("Could not access TagManager asset.");
SerializedProperty layersProp = tagManager.FindProperty("layers");
if (layersProp == null || !layersProp.isArray)
return Response.Error("Could not find 'layers' property in TagManager.");
// Find the layer by name (must be user layer)
int layerIndexToRemove = -1;
for (int i = FirstUserLayerIndex; i < TotalLayerCount; i++) // Start from user layers
{
SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i);
// Case-insensitive comparison is safer
if (layerSP != null && layerName.Equals(layerSP.stringValue, StringComparison.OrdinalIgnoreCase))
{
layerIndexToRemove = i;
break;
}
}
if (layerIndexToRemove == -1)
{
return Response.Error($"User layer '{layerName}' not found.");
}
// Clear the name for that index
try
{
SerializedProperty targetLayerSP = layersProp.GetArrayElementAtIndex(layerIndexToRemove);
targetLayerSP.stringValue = string.Empty; // Set to empty string to remove
// Apply the changes
tagManager.ApplyModifiedProperties();
// Save assets
AssetDatabase.SaveAssets();
return Response.Success($"Layer '{layerName}' (slot {layerIndexToRemove}) removed successfully.");
}
catch (Exception e)
{
return Response.Error($"Failed to remove layer '{layerName}': {e.Message}");
}
}
private static object GetLayers()
{
try
{
var layers = new Dictionary<int, string>();
for (int i = 0; i < TotalLayerCount; i++)
{
string layerName = LayerMask.LayerToName(i);
if (!string.IsNullOrEmpty(layerName)) // Only include layers that have names
{
layers.Add(i, layerName);
}
}
return Response.Success("Retrieved current named layers.", layers);
}
catch (Exception e)
{
return Response.Error($"Failed to retrieve layers: {e.Message}");
}
}
// --- Helper Methods ---
/// <summary>
/// Gets the SerializedObject for the TagManager asset.
/// </summary>
private static SerializedObject GetTagManager()
{
try
{
// Load the TagManager asset from the ProjectSettings folder
UnityEngine.Object[] tagManagerAssets = AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/TagManager.asset");
if (tagManagerAssets == null || tagManagerAssets.Length == 0)
{
Debug.LogError("[ManageEditor] TagManager.asset not found in ProjectSettings.");
return null;
}
// The first object in the asset file should be the TagManager
return new SerializedObject(tagManagerAssets[0]);
}
catch (Exception e)
{
Debug.LogError($"[ManageEditor] Error accessing TagManager.asset: {e.Message}");
return null;
}
}
// --- Example Implementations for Settings ---
/*
private static object SetGameViewResolution(int width, int height) { ... }
private static object SetQualityLevel(JToken qualityLevelToken) { ... }
*/
}
// Helper class to get custom tool names (remains the same)
internal static class EditorTools {
public static string GetActiveToolName() {
// This is a placeholder. Real implementation depends on how custom tools
// are registered and tracked in the specific Unity project setup.
// It might involve checking static variables, calling methods on specific tool managers, etc.
if (UnityEditor.Tools.current == Tool.Custom)
{
// Example: Check a known custom tool manager
// if (MyCustomToolManager.IsActive) return MyCustomToolManager.ActiveToolName;
return "Unknown Custom Tool";
}
return UnityEditor.Tools.current.ToString();
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 43ac60aa36b361b4dbe4a038ae9f35c8

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7641d7388f0f6634b9d83d34de87b2ee

344
Editor/Tools/ManageScene.cs Normal file
View File

@ -0,0 +1,344 @@
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEditor;
using UnityEditor.SceneManagement;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using UnityMCP.Editor.Helpers; // For Response class
namespace UnityMCP.Editor.Tools
{
/// <summary>
/// Handles scene management operations like loading, saving, creating, and querying hierarchy.
/// </summary>
public static class ManageScene
{
/// <summary>
/// Main handler for scene management actions.
/// </summary>
public static object HandleCommand(JObject @params)
{
string action = @params["action"]?.ToString().ToLower();
string name = @params["name"]?.ToString();
string path = @params["path"]?.ToString(); // Relative to Assets/
int? buildIndex = @params["buildIndex"]?.ToObject<int?>();
// bool loadAdditive = @params["loadAdditive"]?.ToObject<bool>() ?? false; // Example for future extension
// Ensure path is relative to Assets/, removing any leading "Assets/"
string relativeDir = path ?? string.Empty;
if (!string.IsNullOrEmpty(relativeDir)) {
relativeDir = relativeDir.Replace('\\', '/').Trim('/');
if (relativeDir.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
relativeDir = relativeDir.Substring("Assets/".Length).TrimStart('/');
}
}
// Apply default *after* sanitizing, using the original path variable for the check
if (string.IsNullOrEmpty(path) && action == "create") // Check original path for emptiness
{
relativeDir = "Scenes"; // Default relative directory
}
if (string.IsNullOrEmpty(action))
{
return Response.Error("Action parameter is required.");
}
string sceneFileName = string.IsNullOrEmpty(name) ? null : $"{name}.unity";
// Construct full system path correctly: ProjectRoot/Assets/relativeDir/sceneFileName
string fullPathDir = Path.Combine(Application.dataPath, relativeDir); // Combine with Assets path (Application.dataPath ends in Assets)
string fullPath = string.IsNullOrEmpty(sceneFileName) ? null : Path.Combine(fullPathDir, sceneFileName);
// Ensure relativePath always starts with "Assets/" and uses forward slashes
string relativePath = string.IsNullOrEmpty(sceneFileName) ? null : Path.Combine("Assets", relativeDir, sceneFileName).Replace('\\', '/');
// Ensure directory exists for 'create'
if (action == "create" && !string.IsNullOrEmpty(fullPathDir))
{
try
{
Directory.CreateDirectory(fullPathDir);
}
catch (Exception e)
{
return Response.Error($"Could not create directory '{fullPathDir}': {e.Message}");
}
}
// Route action
switch (action)
{
case "create":
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(relativePath))
return Response.Error("'name' and 'path' parameters are required for 'create' action.");
return CreateScene(fullPath, relativePath);
case "load":
// Loading can be done by path/name or build index
if (!string.IsNullOrEmpty(relativePath))
return LoadScene(relativePath);
else if (buildIndex.HasValue)
return LoadScene(buildIndex.Value);
else
return Response.Error("Either 'name'/'path' or 'buildIndex' must be provided for 'load' action.");
case "save":
// Save current scene, optionally to a new path
return SaveScene(fullPath, relativePath);
case "get_hierarchy":
return GetSceneHierarchy();
case "get_active":
return GetActiveSceneInfo();
case "get_build_settings":
return GetBuildSettingsScenes();
// Add cases for modifying build settings, additive loading, unloading etc.
default:
return Response.Error($"Unknown action: '{action}'. Valid actions: create, load, save, get_hierarchy, get_active, get_build_settings.");
}
}
private static object CreateScene(string fullPath, string relativePath)
{
if (File.Exists(fullPath))
{
return Response.Error($"Scene already exists at '{relativePath}'.");
}
try
{
// Create a new empty scene
Scene newScene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
// Save it to the specified path
bool saved = EditorSceneManager.SaveScene(newScene, relativePath);
if (saved)
{
AssetDatabase.Refresh(); // Ensure Unity sees the new scene file
return Response.Success($"Scene '{Path.GetFileName(relativePath)}' created successfully at '{relativePath}'.", new { path = relativePath });
}
else
{
// If SaveScene fails, it might leave an untitled scene open.
// Optionally try to close it, but be cautious.
return Response.Error($"Failed to save new scene to '{relativePath}'.");
}
}
catch (Exception e)
{
return Response.Error($"Error creating scene '{relativePath}': {e.Message}");
}
}
private static object LoadScene(string relativePath)
{
if (!File.Exists(Path.Combine(Application.dataPath.Substring(0, Application.dataPath.Length - "Assets".Length), relativePath)))
{
return Response.Error($"Scene file not found at '{relativePath}'.");
}
// Check for unsaved changes in the current scene
if (EditorSceneManager.GetActiveScene().isDirty)
{
// Optionally prompt the user or save automatically before loading
return Response.Error("Current scene has unsaved changes. Please save or discard changes before loading a new scene.");
// Example: bool saveOK = EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo();
// if (!saveOK) return Response.Error("Load cancelled by user.");
}
try
{
EditorSceneManager.OpenScene(relativePath, OpenSceneMode.Single);
return Response.Success($"Scene '{relativePath}' loaded successfully.", new { path = relativePath, name = Path.GetFileNameWithoutExtension(relativePath) });
}
catch (Exception e)
{
return Response.Error($"Error loading scene '{relativePath}': {e.Message}");
}
}
private static object LoadScene(int buildIndex)
{
if (buildIndex < 0 || buildIndex >= SceneManager.sceneCountInBuildSettings)
{
return Response.Error($"Invalid build index: {buildIndex}. Must be between 0 and {SceneManager.sceneCountInBuildSettings - 1}.");
}
// Check for unsaved changes
if (EditorSceneManager.GetActiveScene().isDirty)
{
return Response.Error("Current scene has unsaved changes. Please save or discard changes before loading a new scene.");
}
try
{
string scenePath = SceneUtility.GetScenePathByBuildIndex(buildIndex);
EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
return Response.Success($"Scene at build index {buildIndex} ('{scenePath}') loaded successfully.", new { path = scenePath, name = Path.GetFileNameWithoutExtension(scenePath), buildIndex = buildIndex });
}
catch (Exception e)
{
return Response.Error($"Error loading scene with build index {buildIndex}: {e.Message}");
}
}
private static object SaveScene(string fullPath, string relativePath)
{
try
{
Scene currentScene = EditorSceneManager.GetActiveScene();
if (!currentScene.IsValid())
{
return Response.Error("No valid scene is currently active to save.");
}
bool saved;
string finalPath = currentScene.path; // Path where it was last saved or will be saved
if (!string.IsNullOrEmpty(relativePath) && currentScene.path != relativePath)
{
// Save As...
// Ensure directory exists
string dir = Path.GetDirectoryName(fullPath);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
saved = EditorSceneManager.SaveScene(currentScene, relativePath);
finalPath = relativePath;
}
else
{
// Save (overwrite existing or save untitled)
if (string.IsNullOrEmpty(currentScene.path))
{
// Scene is untitled, needs a path
return Response.Error("Cannot save an untitled scene without providing a 'name' and 'path'. Use Save As functionality.");
}
saved = EditorSceneManager.SaveScene(currentScene);
}
if (saved)
{
AssetDatabase.Refresh();
return Response.Success($"Scene '{currentScene.name}' saved successfully to '{finalPath}'.", new { path = finalPath, name = currentScene.name });
}
else
{
return Response.Error($"Failed to save scene '{currentScene.name}'.");
}
}
catch (Exception e)
{
return Response.Error($"Error saving scene: {e.Message}");
}
}
private static object GetActiveSceneInfo()
{
try
{
Scene activeScene = EditorSceneManager.GetActiveScene();
if (!activeScene.IsValid())
{
return Response.Error("No active scene found.");
}
var sceneInfo = new
{
name = activeScene.name,
path = activeScene.path,
buildIndex = activeScene.buildIndex, // -1 if not in build settings
isDirty = activeScene.isDirty,
isLoaded = activeScene.isLoaded,
rootCount = activeScene.rootCount
};
return Response.Success("Retrieved active scene information.", sceneInfo);
}
catch (Exception e)
{
return Response.Error($"Error getting active scene info: {e.Message}");
}
}
private static object GetBuildSettingsScenes()
{
try
{
var scenes = new List<object>();
for (int i = 0; i < EditorBuildSettings.scenes.Length; i++)
{
var scene = EditorBuildSettings.scenes[i];
scenes.Add(new {
path = scene.path,
guid = scene.guid.ToString(),
enabled = scene.enabled,
buildIndex = i // Actual build index considering only enabled scenes might differ
});
}
return Response.Success("Retrieved scenes from Build Settings.", scenes);
}
catch (Exception e)
{
return Response.Error($"Error getting scenes from Build Settings: {e.Message}");
}
}
private static object GetSceneHierarchy()
{
try
{
Scene activeScene = EditorSceneManager.GetActiveScene();
if (!activeScene.IsValid() || !activeScene.isLoaded)
{
return Response.Error("No valid and loaded scene is active to get hierarchy from.");
}
GameObject[] rootObjects = activeScene.GetRootGameObjects();
var hierarchy = rootObjects.Select(go => GetGameObjectDataRecursive(go)).ToList();
return Response.Success($"Retrieved hierarchy for scene '{activeScene.name}'.", hierarchy);
}
catch (Exception e)
{
return Response.Error($"Error getting scene hierarchy: {e.Message}");
}
}
/// <summary>
/// Recursively builds a data representation of a GameObject and its children.
/// </summary>
private static object GetGameObjectDataRecursive(GameObject go)
{
if (go == null) return null;
var childrenData = new List<object>();
foreach (Transform child in go.transform)
{
childrenData.Add(GetGameObjectDataRecursive(child.gameObject));
}
// Basic info
var gameObjectData = new Dictionary<string, object>
{
{ "name", go.name },
{ "activeSelf", go.activeSelf },
{ "activeInHierarchy", go.activeInHierarchy },
{ "tag", go.tag },
{ "layer", go.layer },
{ "isStatic", go.isStatic },
{ "instanceID", go.GetInstanceID() }, // Useful unique identifier
{ "transform", new {
position = go.transform.localPosition,
rotation = go.transform.localRotation.eulerAngles, // Euler for simplicity
scale = go.transform.localScale
}
},
{ "children", childrenData }
// Add components if needed - potentially large data
// { "components", go.GetComponents<Component>().Select(c => c.GetType().FullName).ToList() }
};
return gameObjectData;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b6ddda47f4077e74fbb5092388cefcc2

View File

@ -0,0 +1,277 @@
using UnityEngine;
using UnityEditor;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Linq;
using UnityMCP.Editor.Helpers;
namespace UnityMCP.Editor.Tools
{
/// <summary>
/// Handles CRUD operations for C# scripts within the Unity project.
/// </summary>
public static class ManageScript
{
/// <summary>
/// Main handler for script management actions.
/// </summary>
public static object HandleCommand(JObject @params)
{
// Extract parameters
string action = @params["action"]?.ToString().ToLower();
string name = @params["name"]?.ToString();
string path = @params["path"]?.ToString(); // Relative to Assets/
string contents = @params["contents"]?.ToString();
string scriptType = @params["scriptType"]?.ToString(); // For templates/validation
string namespaceName = @params["namespace"]?.ToString(); // For organizing code
// Validate required parameters
if (string.IsNullOrEmpty(action))
{
return Response.Error("Action parameter is required.");
}
if (string.IsNullOrEmpty(name))
{
return Response.Error("Name parameter is required.");
}
// Basic name validation (alphanumeric, underscores, cannot start with number)
if (!Regex.IsMatch(name, @"^[a-zA-Z_][a-zA-Z0-9_]*$"))
{
return Response.Error($"Invalid script name: '{name}'. Use only letters, numbers, underscores, and don't start with a number.");
}
// Ensure path is relative to Assets/, removing any leading "Assets/"
string relativeDir = path ?? string.Empty;
if (!string.IsNullOrEmpty(relativeDir))
{
relativeDir = relativeDir.Replace('\\', '/').Trim('/');
if (relativeDir.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
relativeDir = relativeDir.Substring("Assets/".Length).TrimStart('/');
}
}
// Construct paths
string scriptFileName = $"{name}.cs";
string fullPathDir = Path.Combine(Application.dataPath, relativeDir); // Application.dataPath ends in "Assets"
string fullPath = Path.Combine(fullPathDir, scriptFileName);
string relativePath = Path.Combine("Assets", relativeDir, scriptFileName).Replace('\\', '/'); // Ensure "Assets/" prefix and forward slashes
// Ensure the target directory exists for create/update
if (action == "create" || action == "update")
{
try
{
Directory.CreateDirectory(fullPathDir);
}
catch (Exception e)
{
return Response.Error($"Could not create directory '{fullPathDir}': {e.Message}");
}
}
// Route to specific action handlers
switch (action)
{
case "create":
return CreateScript(fullPath, relativePath, name, contents, scriptType, namespaceName);
case "read":
return ReadScript(fullPath, relativePath);
case "update":
return UpdateScript(fullPath, relativePath, name, contents);
case "delete":
return DeleteScript(fullPath, relativePath);
default:
return Response.Error($"Unknown action: '{action}'. Valid actions are: create, read, update, delete.");
}
}
private static object CreateScript(string fullPath, string relativePath, string name, string contents, string scriptType, string namespaceName)
{
// Check if script already exists
if (File.Exists(fullPath))
{
return Response.Error($"Script already exists at '{relativePath}'. Use 'update' action to modify.");
}
// Generate default content if none provided
if (string.IsNullOrEmpty(contents))
{
contents = GenerateDefaultScriptContent(name, scriptType, namespaceName);
}
// Validate syntax (basic check)
if (!ValidateScriptSyntax(contents))
{
// Optionally return a specific error or warning about syntax
// return Response.Error("Provided script content has potential syntax errors.");
Debug.LogWarning($"Potential syntax error in script being created: {name}");
}
try
{
File.WriteAllText(fullPath, contents);
AssetDatabase.ImportAsset(relativePath);
AssetDatabase.Refresh(); // Ensure Unity recognizes the new script
return Response.Success($"Script '{name}.cs' created successfully at '{relativePath}'.", new { path = relativePath });
}
catch (Exception e)
{
return Response.Error($"Failed to create script '{relativePath}': {e.Message}");
}
}
private static object ReadScript(string fullPath, string relativePath)
{
if (!File.Exists(fullPath))
{
return Response.Error($"Script not found at '{relativePath}'.");
}
try
{
string contents = File.ReadAllText(fullPath);
return Response.Success($"Script '{Path.GetFileName(relativePath)}' read successfully.", new { path = relativePath, contents = contents });
}
catch (Exception e)
{
return Response.Error($"Failed to read script '{relativePath}': {e.Message}");
}
}
private static object UpdateScript(string fullPath, string relativePath, string name, string contents)
{
if (!File.Exists(fullPath))
{
return Response.Error($"Script not found at '{relativePath}'. Use 'create' action to add a new script.");
}
if (string.IsNullOrEmpty(contents))
{
return Response.Error("Content is required for the 'update' action.");
}
// Validate syntax (basic check)
if (!ValidateScriptSyntax(contents))
{
Debug.LogWarning($"Potential syntax error in script being updated: {name}");
// Consider if this should be a hard error or just a warning
}
try
{
File.WriteAllText(fullPath, contents);
AssetDatabase.ImportAsset(relativePath); // Re-import to reflect changes
AssetDatabase.Refresh();
return Response.Success($"Script '{name}.cs' updated successfully at '{relativePath}'.", new { path = relativePath });
}
catch (Exception e)
{
return Response.Error($"Failed to update script '{relativePath}': {e.Message}");
}
}
private static object DeleteScript(string fullPath, string relativePath)
{
if (!File.Exists(fullPath))
{
return Response.Error($"Script not found at '{relativePath}'. Cannot delete.");
}
try
{
// Use AssetDatabase.MoveAssetToTrash for safer deletion (allows undo)
bool deleted = AssetDatabase.MoveAssetToTrash(relativePath);
if (deleted)
{
AssetDatabase.Refresh();
return Response.Success($"Script '{Path.GetFileName(relativePath)}' moved to trash successfully.");
}
else
{
// Fallback or error if MoveAssetToTrash fails
return Response.Error($"Failed to move script '{relativePath}' to trash. It might be locked or in use.");
}
}
catch (Exception e)
{
return Response.Error($"Error deleting script '{relativePath}': {e.Message}");
}
}
/// <summary>
/// Generates basic C# script content based on name and type.
/// </summary>
private static string GenerateDefaultScriptContent(string name, string scriptType, string namespaceName)
{
string usingStatements = "using UnityEngine;\nusing System.Collections;\n";
string classDeclaration;
string body = "\n // Use this for initialization\n void Start() {\n\n }\n\n // Update is called once per frame\n void Update() {\n\n }\n";
string baseClass = "";
if (!string.IsNullOrEmpty(scriptType))
{
if (scriptType.Equals("MonoBehaviour", StringComparison.OrdinalIgnoreCase))
baseClass = " : MonoBehaviour";
else if (scriptType.Equals("ScriptableObject", StringComparison.OrdinalIgnoreCase))
{
baseClass = " : ScriptableObject";
body = ""; // ScriptableObjects don't usually need Start/Update
}
else if (scriptType.Equals("Editor", StringComparison.OrdinalIgnoreCase) || scriptType.Equals("EditorWindow", StringComparison.OrdinalIgnoreCase))
{
usingStatements += "using UnityEditor;\n";
if (scriptType.Equals("Editor", StringComparison.OrdinalIgnoreCase))
baseClass = " : Editor";
else
baseClass = " : EditorWindow";
body = ""; // Editor scripts have different structures
}
// Add more types as needed
}
classDeclaration = $"public class {name}{baseClass}";
string fullContent = $"{usingStatements}\n";
bool useNamespace = !string.IsNullOrEmpty(namespaceName);
if (useNamespace)
{
fullContent += $"namespace {namespaceName}\n{{\n";
// Indent class and body if using namespace
classDeclaration = " " + classDeclaration;
body = string.Join("\n", body.Split('\n').Select(line => " " + line));
}
fullContent += $"{classDeclaration}\n{{\n{body}\n}}";
if (useNamespace)
{
fullContent += "\n}"; // Close namespace
}
return fullContent.Trim() + "\n"; // Ensure a trailing newline
}
/// <summary>
/// Performs a very basic syntax validation (checks for balanced braces).
/// TODO: Implement more robust syntax checking if possible.
/// </summary>
private static bool ValidateScriptSyntax(string contents)
{
if (string.IsNullOrEmpty(contents)) return true; // Empty is technically valid?
int braceBalance = 0;
foreach (char c in contents)
{
if (c == '{') braceBalance++;
else if (c == '}') braceBalance--;
}
return braceBalance == 0;
// This is extremely basic. A real C# parser/compiler check would be ideal
// but is complex to implement directly here.
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 626d2d44668019a45ae52e9ee066b7ec

366
Editor/Tools/ReadConsole.cs Normal file
View File

@ -0,0 +1,366 @@
using UnityEngine;
using UnityEditor;
using UnityEditorInternal;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityMCP.Editor.Helpers; // For Response class
using System.Globalization;
namespace UnityMCP.Editor.Tools
{
/// <summary>
/// Handles reading and clearing Unity Editor console log entries.
/// Uses reflection to access internal LogEntry methods/properties.
/// </summary>
public static class ReadConsole
{
// Reflection members for accessing internal LogEntry data
private static MethodInfo _getEntriesMethod;
private static MethodInfo _startGettingEntriesMethod;
private static MethodInfo _stopGettingEntriesMethod;
private static MethodInfo _clearMethod;
private static MethodInfo _getCountMethod;
private static MethodInfo _getEntryMethod;
private static FieldInfo _modeField;
private static FieldInfo _messageField;
private static FieldInfo _fileField;
private static FieldInfo _lineField;
private static FieldInfo _instanceIdField;
// Note: Timestamp is not directly available in LogEntry; need to parse message or find alternative?
// Static constructor for reflection setup
static ReadConsole()
{
try {
Type logEntriesType = typeof(EditorApplication).Assembly.GetType("UnityEditor.LogEntries");
if (logEntriesType == null) throw new Exception("Could not find internal type UnityEditor.LogEntries");
_getEntriesMethod = logEntriesType.GetMethod("GetEntries", BindingFlags.Static | BindingFlags.Public);
_startGettingEntriesMethod = logEntriesType.GetMethod("StartGettingEntries", BindingFlags.Static | BindingFlags.Public);
_stopGettingEntriesMethod = logEntriesType.GetMethod("StopGettingEntries", BindingFlags.Static | BindingFlags.Public);
_clearMethod = logEntriesType.GetMethod("Clear", BindingFlags.Static | BindingFlags.Public);
_getCountMethod = logEntriesType.GetMethod("GetCount", BindingFlags.Static | BindingFlags.Public);
_getEntryMethod = logEntriesType.GetMethod("GetEntryInternal", BindingFlags.Static | BindingFlags.Public);
Type logEntryType = typeof(EditorApplication).Assembly.GetType("UnityEditor.LogEntry");
if (logEntryType == null) throw new Exception("Could not find internal type UnityEditor.LogEntry");
_modeField = logEntryType.GetField("mode", BindingFlags.Instance | BindingFlags.Public);
_messageField = logEntryType.GetField("message", BindingFlags.Instance | BindingFlags.Public);
_fileField = logEntryType.GetField("file", BindingFlags.Instance | BindingFlags.Public);
_lineField = logEntryType.GetField("line", BindingFlags.Instance | BindingFlags.Public);
_instanceIdField = logEntryType.GetField("instanceID", BindingFlags.Instance | BindingFlags.Public);
// Basic check if reflection worked
if (_getEntriesMethod == null || _clearMethod == null || _modeField == null || _messageField == null)
{
throw new Exception("Failed to get required reflection members for LogEntries/LogEntry.");
}
}
catch (Exception e)
{
Debug.LogError($"[ReadConsole] Static Initialization Failed: Could not setup reflection for LogEntries. Console reading/clearing will likely fail. Error: {e}");
// Set members to null to prevent NullReferenceExceptions later, HandleCommand should check this.
_getEntriesMethod = _startGettingEntriesMethod = _stopGettingEntriesMethod = _clearMethod = _getCountMethod = _getEntryMethod = null;
_modeField = _messageField = _fileField = _lineField = _instanceIdField = null;
}
}
// --- Main Handler ---
public static object HandleCommand(JObject @params)
{
// Check if reflection setup failed in static constructor
if (_clearMethod == null || _getEntriesMethod == null || _startGettingEntriesMethod == null || _stopGettingEntriesMethod == null || _getCountMethod == null || _getEntryMethod == null || _modeField == null || _messageField == null)
{
return Response.Error("ReadConsole handler failed to initialize due to reflection errors. Cannot access console logs.");
}
string action = @params["action"]?.ToString().ToLower() ?? "get";
try
{
if (action == "clear")
{
return ClearConsole();
}
else if (action == "get")
{
// Extract parameters for 'get'
var types = (@params["types"] as JArray)?.Select(t => t.ToString().ToLower()).ToList() ?? new List<string> { "error", "warning", "log" };
int? count = @params["count"]?.ToObject<int?>();
string filterText = @params["filterText"]?.ToString();
string sinceTimestampStr = @params["sinceTimestamp"]?.ToString(); // TODO: Implement timestamp filtering
string format = (@params["format"]?.ToString() ?? "detailed").ToLower();
bool includeStacktrace = @params["includeStacktrace"]?.ToObject<bool?>() ?? true;
if (types.Contains("all")) {
types = new List<string> { "error", "warning", "log" }; // Expand 'all'
}
if (!string.IsNullOrEmpty(sinceTimestampStr))
{
Debug.LogWarning("[ReadConsole] Filtering by 'since_timestamp' is not currently implemented.");
// Need a way to get timestamp per log entry.
}
return GetConsoleEntries(types, count, filterText, format, includeStacktrace);
}
else
{
return Response.Error($"Unknown action: '{action}'. Valid actions are 'get' or 'clear'.");
}
}
catch (Exception e)
{
Debug.LogError($"[ReadConsole] Action '{action}' failed: {e}");
return Response.Error($"Internal error processing action '{action}': {e.Message}");
}
}
// --- Action Implementations ---
private static object ClearConsole()
{
try
{
_clearMethod.Invoke(null, null); // Static method, no instance, no parameters
return Response.Success("Console cleared successfully.");
}
catch (Exception e)
{
Debug.LogError($"[ReadConsole] Failed to clear console: {e}");
return Response.Error($"Failed to clear console: {e.Message}");
}
}
private static object GetConsoleEntries(List<string> types, int? count, string filterText, string format, bool includeStacktrace)
{
List<object> formattedEntries = new List<object>();
int retrievedCount = 0;
try
{
// LogEntries requires calling Start/Stop around GetEntries/GetEntryInternal
_startGettingEntriesMethod.Invoke(null, null);
int totalEntries = (int)_getCountMethod.Invoke(null, null);
// Create instance to pass to GetEntryInternal - Ensure the type is correct
Type logEntryType = typeof(EditorApplication).Assembly.GetType("UnityEditor.LogEntry");
if (logEntryType == null) throw new Exception("Could not find internal type UnityEditor.LogEntry during GetConsoleEntries.");
object logEntryInstance = Activator.CreateInstance(logEntryType);
for (int i = 0; i < totalEntries; i++)
{
// Get the entry data into our instance using reflection
_getEntryMethod.Invoke(null, new object[] { i, logEntryInstance });
// Extract data using reflection
int mode = (int)_modeField.GetValue(logEntryInstance);
string message = (string)_messageField.GetValue(logEntryInstance);
string file = (string)_fileField.GetValue(logEntryInstance);
int line = (int)_lineField.GetValue(logEntryInstance);
// int instanceId = (int)_instanceIdField.GetValue(logEntryInstance);
if (string.IsNullOrEmpty(message)) continue; // Skip empty messages
// --- Filtering ---
// Filter by type
LogType currentType = GetLogTypeFromMode(mode);
if (!types.Contains(currentType.ToString().ToLowerInvariant()))
{
continue;
}
// Filter by text (case-insensitive)
if (!string.IsNullOrEmpty(filterText) && message.IndexOf(filterText, StringComparison.OrdinalIgnoreCase) < 0)
{
continue;
}
// TODO: Filter by timestamp (requires timestamp data)
// --- Formatting ---
string stackTrace = includeStacktrace ? ExtractStackTrace(message) : null;
// Get first line if stack is present and requested, otherwise use full message
string messageOnly = (includeStacktrace && !string.IsNullOrEmpty(stackTrace)) ? message.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)[0] : message;
object formattedEntry = null;
switch (format)
{
case "plain":
formattedEntry = messageOnly;
break;
case "json":
case "detailed": // Treat detailed as json for structured return
default:
formattedEntry = new {
type = currentType.ToString(),
message = messageOnly,
file = file,
line = line,
// timestamp = "", // TODO
stackTrace = stackTrace // Will be null if includeStacktrace is false or no stack found
};
break;
}
formattedEntries.Add(formattedEntry);
retrievedCount++;
// Apply count limit (after filtering)
if (count.HasValue && retrievedCount >= count.Value)
{
break;
}
}
}
catch (Exception e) {
Debug.LogError($"[ReadConsole] Error while retrieving log entries: {e}");
// Ensure StopGettingEntries is called even if there's an error during iteration
try { _stopGettingEntriesMethod.Invoke(null, null); } catch { /* Ignore nested exception */ }
return Response.Error($"Error retrieving log entries: {e.Message}");
}
finally
{
// Ensure we always call StopGettingEntries
try { _stopGettingEntriesMethod.Invoke(null, null); } catch (Exception e) {
Debug.LogError($"[ReadConsole] Failed to call StopGettingEntries: {e}");
// Don't return error here as we might have valid data, but log it.
}
}
// Return the filtered and formatted list (might be empty)
return Response.Success($"Retrieved {formattedEntries.Count} log entries.", formattedEntries);
}
// --- Internal Helpers ---
// Mapping from LogEntry.mode bits to LogType enum
// Based on decompiled UnityEditor code or common patterns. Precise bits might change between Unity versions.
// See comments below for LogEntry mode bits exploration.
// Note: This mapping is simplified and might not cover all edge cases or future Unity versions perfectly.
private const int ModeBitError = 1 << 0;
private const int ModeBitAssert = 1 << 1;
private const int ModeBitWarning = 1 << 2;
private const int ModeBitLog = 1 << 3;
private const int ModeBitException = 1 << 4; // Often combined with Error bits
private const int ModeBitScriptingError = 1 << 9;
private const int ModeBitScriptingWarning = 1 << 10;
private const int ModeBitScriptingLog = 1 << 11;
private const int ModeBitScriptingException = 1 << 18;
private const int ModeBitScriptingAssertion = 1 << 22;
private static LogType GetLogTypeFromMode(int mode)
{
// Check for specific error/exception/assert types first
// Combine general and scripting-specific bits for broader matching.
if ((mode & (ModeBitError | ModeBitScriptingError | ModeBitException | ModeBitScriptingException)) != 0) {
return LogType.Error;
}
if ((mode & (ModeBitAssert | ModeBitScriptingAssertion)) != 0) {
return LogType.Assert;
}
if ((mode & (ModeBitWarning | ModeBitScriptingWarning)) != 0) {
return LogType.Warning;
}
// If none of the above, assume it's a standard log message.
// This covers ModeBitLog and ModeBitScriptingLog.
return LogType.Log;
}
/// <summary>
/// Attempts to extract the stack trace part from a log message.
/// Unity log messages often have the stack trace appended after the main message,
/// starting on a new line and typically indented or beginning with "at ".
/// </summary>
/// <param name="fullMessage">The complete log message including potential stack trace.</param>
/// <returns>The extracted stack trace string, or null if none is found.</returns>
private static string ExtractStackTrace(string fullMessage)
{
if (string.IsNullOrEmpty(fullMessage)) return null;
// Split into lines, removing empty ones to handle different line endings gracefully.
// Using StringSplitOptions.None might be better if empty lines matter within stack trace, but RemoveEmptyEntries is usually safer here.
string[] lines = fullMessage.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
// If there's only one line or less, there's no separate stack trace.
if (lines.Length <= 1) return null;
int stackStartIndex = -1;
// Start checking from the second line onwards.
for(int i = 1; i < lines.Length; ++i)
{
// Performance: TrimStart creates a new string. Consider using IsWhiteSpace check if performance critical.
string trimmedLine = lines[i].TrimStart();
// Check for common stack trace patterns.
if (trimmedLine.StartsWith("at ") ||
trimmedLine.StartsWith("UnityEngine.") ||
trimmedLine.StartsWith("UnityEditor.") ||
trimmedLine.Contains("(at ") || // Covers "(at Assets/..." pattern
// Heuristic: Check if line starts with likely namespace/class pattern (Uppercase.Something)
(trimmedLine.Length > 0 && char.IsUpper(trimmedLine[0]) && trimmedLine.Contains('.'))
)
{
stackStartIndex = i;
break; // Found the likely start of the stack trace
}
}
// If a potential start index was found...
if (stackStartIndex > 0)
{
// Join the lines from the stack start index onwards using standard newline characters.
// This reconstructs the stack trace part of the message.
return string.Join("\n", lines.Skip(stackStartIndex));
}
// No clear stack trace found based on the patterns.
return null;
}
/* LogEntry.mode bits exploration (based on Unity decompilation/observation):
May change between versions.
Basic Types:
kError = 1 << 0 (1)
kAssert = 1 << 1 (2)
kWarning = 1 << 2 (4)
kLog = 1 << 3 (8)
kFatal = 1 << 4 (16) - Often treated as Exception/Error
Modifiers/Context:
kAssetImportError = 1 << 7 (128)
kAssetImportWarning = 1 << 8 (256)
kScriptingError = 1 << 9 (512)
kScriptingWarning = 1 << 10 (1024)
kScriptingLog = 1 << 11 (2048)
kScriptCompileError = 1 << 12 (4096)
kScriptCompileWarning = 1 << 13 (8192)
kStickyError = 1 << 14 (16384) - Stays visible even after Clear On Play
kMayIgnoreLineNumber = 1 << 15 (32768)
kReportBug = 1 << 16 (65536) - Shows the "Report Bug" button
kDisplayPreviousErrorInStatusBar = 1 << 17 (131072)
kScriptingException = 1 << 18 (262144)
kDontExtractStacktrace = 1 << 19 (524288) - Hint to the console UI
kShouldClearOnPlay = 1 << 20 (1048576) - Default behavior
kGraphCompileError = 1 << 21 (2097152)
kScriptingAssertion = 1 << 22 (4194304)
kVisualScriptingError = 1 << 23 (8388608)
Example observed values:
Log: 2048 (ScriptingLog) or 8 (Log)
Warning: 1028 (ScriptingWarning | Warning) or 4 (Warning)
Error: 513 (ScriptingError | Error) or 1 (Error)
Exception: 262161 (ScriptingException | Error | kFatal?) - Complex combination
Assertion: 4194306 (ScriptingAssertion | Assert) or 2 (Assert)
*/
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 46c4f3614ed61f547ba823f0b2790267

View File

@ -10,7 +10,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using System.IO; using System.IO;
using UnityMCP.Editor.Models; using UnityMCP.Editor.Models;
using UnityMCP.Editor.Commands; using UnityMCP.Editor.Tools;
namespace UnityMCP.Editor namespace UnityMCP.Editor
{ {
@ -269,59 +269,47 @@ namespace UnityMCP.Editor
} }
// Handle ping command for connection verification // Handle ping command for connection verification
if (command.type == "ping") if (command.type.Equals("ping", StringComparison.OrdinalIgnoreCase))
{ {
var pingResponse = new { status = "success", result = new { message = "pong" } }; var pingResponse = new { status = "success", result = new { message = "pong" } };
return JsonConvert.SerializeObject(pingResponse); return JsonConvert.SerializeObject(pingResponse);
} }
// Use JObject for parameters as the new handlers likely expect this
JObject paramsObject = command.@params ?? new JObject();
// Route command based on the new tool structure from the refactor plan
object result = command.type switch object result = command.type switch
{ {
"GET_SCENE_INFO" => SceneCommandHandler.GetSceneInfo(), // Maps the command type (tool name) to the corresponding handler's static HandleCommand method
"OPEN_SCENE" => SceneCommandHandler.OpenScene(command.@params), // Assumes each handler class has a static method named 'HandleCommand' that takes JObject parameters
"SAVE_SCENE" => SceneCommandHandler.SaveScene(), "manage_script" => ManageScript.HandleCommand(paramsObject),
"NEW_SCENE" => SceneCommandHandler.NewScene(command.@params), "manage_scene" => ManageScene.HandleCommand(paramsObject),
"CHANGE_SCENE" => SceneCommandHandler.ChangeScene(command.@params), "manage_editor" => ManageEditor.HandleCommand(paramsObject),
"GET_OBJECT_INFO" => ObjectCommandHandler.GetObjectInfo(command.@params), "manage_gameobject" => ManageGameObject.HandleCommand(paramsObject),
"CREATE_OBJECT" => ObjectCommandHandler.CreateObject(command.@params), "manage_asset" => ManageAsset.HandleCommand(paramsObject),
"MODIFY_OBJECT" => ObjectCommandHandler.ModifyObject(command.@params), "read_console" => ReadConsole.HandleCommand(paramsObject),
"DELETE_OBJECT" => ObjectCommandHandler.DeleteObject(command.@params), "execute_menu_item" => ExecuteMenuItem.HandleCommand(paramsObject),
"EXECUTE_CONTEXT_MENU_ITEM" => ObjectCommandHandler.ExecuteContextMenuItem(command.@params), _ => throw new ArgumentException($"Unknown or unsupported command type: {command.type}")
"GET_OBJECT_PROPERTIES" => ObjectCommandHandler.GetObjectProperties(command.@params),
"GET_COMPONENT_PROPERTIES" => ObjectCommandHandler.GetComponentProperties(command.@params),
"FIND_OBJECTS_BY_NAME" => ObjectCommandHandler.FindObjectsByName(command.@params),
"FIND_OBJECTS_BY_TAG" => ObjectCommandHandler.FindObjectsByTag(command.@params),
"GET_HIERARCHY" => ObjectCommandHandler.GetHierarchy(),
"SELECT_OBJECT" => ObjectCommandHandler.SelectObject(command.@params),
"GET_SELECTED_OBJECT" => ObjectCommandHandler.GetSelectedObject(),
"SET_MATERIAL" => MaterialCommandHandler.SetMaterial(command.@params),
"VIEW_SCRIPT" => ScriptCommandHandler.ViewScript(command.@params),
"CREATE_SCRIPT" => ScriptCommandHandler.CreateScript(command.@params),
"UPDATE_SCRIPT" => ScriptCommandHandler.UpdateScript(command.@params),
"LIST_SCRIPTS" => ScriptCommandHandler.ListScripts(command.@params),
"ATTACH_SCRIPT" => ScriptCommandHandler.AttachScript(command.@params),
"IMPORT_ASSET" => AssetCommandHandler.ImportAsset(command.@params),
"INSTANTIATE_PREFAB" => AssetCommandHandler.InstantiatePrefab(command.@params),
"CREATE_PREFAB" => AssetCommandHandler.CreatePrefab(command.@params),
"APPLY_PREFAB" => AssetCommandHandler.ApplyPrefab(command.@params),
"GET_ASSET_LIST" => AssetCommandHandler.GetAssetList(command.@params),
"EDITOR_CONTROL" => EditorControlHandler.HandleEditorControl(command.@params),
_ => throw new Exception($"Unknown command type: {command.type}")
}; };
// Standard success response format
var response = new { status = "success", result }; var response = new { status = "success", result };
return JsonConvert.SerializeObject(response); return JsonConvert.SerializeObject(response);
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.LogError($"Error executing command {command.type}: {ex.Message}\n{ex.StackTrace}"); // Log the detailed error in Unity for debugging
Debug.LogError($"Error executing command '{command?.type ?? "Unknown"}': {ex.Message}\n{ex.StackTrace}");
// Standard error response format
var response = new var response = new
{ {
status = "error", status = "error",
error = ex.Message, error = ex.Message, // Provide the specific error message
command = command.type, command = command?.type ?? "Unknown", // Include the command type if available
stackTrace = ex.StackTrace, stackTrace = ex.StackTrace, // Include stack trace for detailed debugging
paramsSummary = command.@params != null ? GetParamsSummary(command.@params) : "No parameters" paramsSummary = command?.@params != null ? GetParamsSummary(command.@params) : "No parameters" // Summarize parameters for context
}; };
return JsonConvert.SerializeObject(response); return JsonConvert.SerializeObject(response);
} }
@ -343,4 +331,4 @@ namespace UnityMCP.Editor
} }
} }
} }
} }

View File

@ -29,7 +29,9 @@ async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
logger.warning(f"Could not connect to Unity on startup: {str(e)}") logger.warning(f"Could not connect to Unity on startup: {str(e)}")
_unity_connection = None _unity_connection = None
try: try:
yield {} # Yield the connection object so it can be attached to the context
# The key 'bridge' matches how tools like read_console expect to access it (ctx.bridge)
yield {"bridge": _unity_connection}
finally: finally:
if _unity_connection: if _unity_connection:
_unity_connection.disconnect() _unity_connection.disconnect()
@ -50,56 +52,17 @@ register_all_tools(mcp)
@mcp.prompt() @mcp.prompt()
def asset_creation_strategy() -> str: def asset_creation_strategy() -> str:
"""Guide for creating and managing assets in Unity.""" """Guide for discovering and using Unity MCP tools effectively."""
return ( return (
"Unity MCP Server Tools and Best Practices:\n\n" "Available Unity MCP Server Tools:\\n\\n"
"1. **Editor Control**\n" "For detailed usage, please refer to the specific tool's documentation.\\n\\n"
" - `editor_action` - Performs editor-wide actions such as `PLAY`, `PAUSE`, `STOP`, `BUILD`, `SAVE`\n" "- `manage_editor`: Controls editor state (play/pause/stop) and queries info (state, selection).\\n"
" - `read_console(show_logs=True, show_warnings=True, show_errors=True, search_term=None)` - Read and filter Unity Console logs\n" "- `execute_menu_item`: Executes Unity Editor menu items by path (e.g., 'File/Save Project').\\n"
"2. **Scene Management**\n" "- `read_console`: Reads or clears Unity console messages, with filtering options.\\n"
" - `get_current_scene()`, `get_scene_list()` - Get scene details\n" "- `manage_scene`: Manages scenes (load, save, create, get hierarchy).\\n"
" - `open_scene(path)`, `save_scene(path)` - Open/save scenes\n" "- `manage_gameobject`: Manages GameObjects in the scene (CRUD, find, components, assign properties).\\n"
" - `new_scene(path)`, `change_scene(path, save_current)` - Create/switch scenes\n\n" "- `manage_script`: Manages C# script files (CRUD).\\n"
"3. **Object Management**\n" "- `manage_asset`: Manages project assets (import, create, modify, delete, search).\\n\\n"
" - ALWAYS use `find_objects_by_name(name)` to check if an object exists before creating or modifying it\n"
" - `create_object(name, type)` - Create objects (e.g. `CUBE`, `SPHERE`, `EMPTY`, `CAMERA`)\n"
" - `delete_object(name)` - Remove objects\n"
" - `set_object_transform(name, location, rotation, scale)` - Modify object position, rotation, and scale\n"
" - `add_component(name, component_type)` - Add components to objects (e.g. `Rigidbody`, `BoxCollider`)\n"
" - `remove_component(name, component_type)` - Remove components from objects\n"
" - `get_object_properties(name)` - Get object properties\n"
" - `find_objects_by_name(name)` - Find objects by name\n"
" - `get_hierarchy()` - Get object hierarchy\n"
"4. **Script Management**\n"
" - ALWAYS use `list_scripts(folder_path)` or `view_script(path)` to check if a script exists before creating or updating it\n"
" - `create_script(name, type, namespace, template)` - Create scripts\n"
" - `view_script(path)`, `update_script(path, content)` - View/modify scripts\n"
" - `attach_script(object_name, script_name)` - Add scripts to objects\n"
" - `list_scripts(folder_path)` - List scripts in folder\n\n"
"5. **Asset Management**\n"
" - ALWAYS use `get_asset_list(type, search_pattern, folder)` to check if an asset exists before creating or importing it\n"
" - `import_asset(source_path, target_path)` - Import external assets\n"
" - `instantiate_prefab(path, pos_x, pos_y, pos_z, rot_x, rot_y, rot_z)` - Create prefab instances\n"
" - `create_prefab(object_name, path)`, `apply_prefab(object_name, path)` - Manage prefabs\n"
" - `get_asset_list(type, search_pattern, folder)` - List project assets\n"
" - Use relative paths for Unity assets (e.g., 'Assets/Models/MyModel.fbx')\n"
" - Use absolute paths for external files\n\n"
"6. **Material Management**\n"
" - ALWAYS check if a material exists before creating or modifying it\n"
" - `set_material(object_name, material_name, color)` - Apply/create materials\n"
" - Use RGB colors (0.0-1.0 range)\n\n"
"7. **Best Practices**\n"
" - ALWAYS verify existence before creating or updating any objects, scripts, assets, or materials\n"
" - Use meaningful names for objects and scripts\n"
" - Keep scripts organized in folders with namespaces\n"
" - Verify changes after modifications\n"
" - Save scenes before major changes\n"
" - Use full component names (e.g., 'Rigidbody', 'BoxCollider')\n"
" - Provide correct value types for properties\n"
" - Keep prefabs in dedicated folders\n"
" - Regularly apply prefab changes\n"
" - Monitor console logs for errors and warnings\n"
" - Use search terms to filter console output when debugging\n"
) )
# Run the server # Run the server

View File

@ -1,15 +1,19 @@
from .scene_tools import register_scene_tools from .manage_script import register_manage_script_tools
from .script_tools import register_script_tools from .manage_scene import register_manage_scene_tools
from .material_tools import register_material_tools from .manage_editor import register_manage_editor_tools
from .editor_tools import register_editor_tools from .manage_gameobject import register_manage_gameobject_tools
from .asset_tools import register_asset_tools from .manage_asset import register_manage_asset_tools
from .object_tools import register_object_tools from .read_console import register_read_console_tools
from .execute_menu_item import register_execute_menu_item_tools
def register_all_tools(mcp): def register_all_tools(mcp):
"""Register all tools with the MCP server.""" """Register all refactored tools with the MCP server."""
register_scene_tools(mcp) print("Registering UnityMCP refactored tools...")
register_script_tools(mcp) register_manage_script_tools(mcp)
register_material_tools(mcp) register_manage_scene_tools(mcp)
register_editor_tools(mcp) register_manage_editor_tools(mcp)
register_asset_tools(mcp) register_manage_gameobject_tools(mcp)
register_object_tools(mcp) register_manage_asset_tools(mcp)
register_read_console_tools(mcp)
register_execute_menu_item_tools(mcp)
print("UnityMCP tool registration complete.")

View File

@ -1,259 +0,0 @@
from typing import Optional
from mcp.server.fastmcp import FastMCP, Context
from unity_connection import get_unity_connection
def register_asset_tools(mcp: FastMCP):
"""Register all asset management tools with the MCP server."""
@mcp.tool()
def import_asset(
ctx: Context,
source_path: str,
target_path: str,
overwrite: bool = False
) -> str:
"""Import an asset (e.g., 3D model, texture) into the Unity project.
Args:
ctx: The MCP context
source_path: Path to the source file on disk
target_path: Path where the asset should be imported in the Unity project (relative to Assets folder)
overwrite: Whether to overwrite if an asset already exists at the target path (default: False)
Returns:
str: Success message or error details
"""
try:
unity = get_unity_connection()
# Parameter validation
if not source_path or not isinstance(source_path, str):
return f"Error importing asset: source_path must be a valid string"
if not target_path or not isinstance(target_path, str):
return f"Error importing asset: target_path must be a valid string"
# Check if the source file exists (on local disk)
import os
if not os.path.exists(source_path):
return f"Error importing asset: Source file '{source_path}' does not exist"
# Extract the target directory and filename
target_dir = '/'.join(target_path.split('/')[:-1])
target_filename = target_path.split('/')[-1]
# Check if an asset already exists at the target path
existing_assets = unity.send_command("GET_ASSET_LIST", {
"search_pattern": target_filename,
"folder": target_dir or "Assets"
}).get("assets", [])
# Check if any asset matches the exact path
asset_exists = any(asset.get("path") == target_path for asset in existing_assets)
if asset_exists and not overwrite:
return f"Asset already exists at '{target_path}'. Use overwrite=True to replace it."
response = unity.send_command("IMPORT_ASSET", {
"source_path": source_path,
"target_path": target_path,
"overwrite": overwrite
})
if not response.get("success", False):
return f"Error importing asset: {response.get('error', 'Unknown error')} (Source: {source_path}, Target: {target_path})"
return response.get("message", "Asset imported successfully")
except Exception as e:
return f"Error importing asset: {str(e)} (Source: {source_path}, Target: {target_path})"
@mcp.tool()
def instantiate_prefab(
ctx: Context,
prefab_path: str,
position_x: float = 0.0,
position_y: float = 0.0,
position_z: float = 0.0,
rotation_x: float = 0.0,
rotation_y: float = 0.0,
rotation_z: float = 0.0
) -> str:
"""Instantiate a prefab into the current scene at a specified location.
Args:
ctx: The MCP context
prefab_path: Path to the prefab asset (relative to Assets folder)
position_x: X position in world space (default: 0.0)
position_y: Y position in world space (default: 0.0)
position_z: Z position in world space (default: 0.0)
rotation_x: X rotation in degrees (default: 0.0)
rotation_y: Y rotation in degrees (default: 0.0)
rotation_z: Z rotation in degrees (default: 0.0)
Returns:
str: Success message or error details
"""
try:
unity = get_unity_connection()
# Parameter validation
if not prefab_path or not isinstance(prefab_path, str):
return f"Error instantiating prefab: prefab_path must be a valid string"
# Validate numeric parameters
position_params = {
"position_x": position_x,
"position_y": position_y,
"position_z": position_z,
"rotation_x": rotation_x,
"rotation_y": rotation_y,
"rotation_z": rotation_z
}
for param_name, param_value in position_params.items():
if not isinstance(param_value, (int, float)):
return f"Error instantiating prefab: {param_name} must be a number"
# Check if the prefab exists
prefab_dir = '/'.join(prefab_path.split('/')[:-1]) or "Assets"
prefab_name = prefab_path.split('/')[-1]
# Ensure prefab has .prefab extension for searching
if not prefab_name.lower().endswith('.prefab'):
prefab_name = f"{prefab_name}.prefab"
prefab_path = f"{prefab_path}.prefab"
prefab_assets = unity.send_command("GET_ASSET_LIST", {
"type": "Prefab",
"search_pattern": prefab_name,
"folder": prefab_dir
}).get("assets", [])
prefab_exists = any(asset.get("path") == prefab_path for asset in prefab_assets)
if not prefab_exists:
return f"Prefab '{prefab_path}' not found in the project."
response = unity.send_command("INSTANTIATE_PREFAB", {
"prefab_path": prefab_path,
"position_x": position_x,
"position_y": position_y,
"position_z": position_z,
"rotation_x": rotation_x,
"rotation_y": rotation_y,
"rotation_z": rotation_z
})
if not response.get("success", False):
return f"Error instantiating prefab: {response.get('error', 'Unknown error')} (Path: {prefab_path})"
return f"Prefab instantiated successfully as '{response.get('instance_name', 'unknown')}'"
except Exception as e:
return f"Error instantiating prefab: {str(e)} (Path: {prefab_path})"
@mcp.tool()
def create_prefab(
ctx: Context,
object_name: str,
prefab_path: str,
overwrite: bool = False
) -> str:
"""Create a new prefab asset from a GameObject in the scene.
Args:
ctx: The MCP context
object_name: Name of the GameObject in the scene to create prefab from
prefab_path: Path where the prefab should be saved (relative to Assets folder)
overwrite: Whether to overwrite if a prefab already exists at the path (default: False)
Returns:
str: Success message or error details
"""
try:
unity = get_unity_connection()
# Parameter validation
if not object_name or not isinstance(object_name, str):
return f"Error creating prefab: object_name must be a valid string"
if not prefab_path or not isinstance(prefab_path, str):
return f"Error creating prefab: prefab_path must be a valid string"
# Check if the GameObject exists
found_objects = unity.send_command("FIND_OBJECTS_BY_NAME", {
"name": object_name
}).get("objects", [])
if not found_objects:
return f"GameObject '{object_name}' not found in the scene."
# Verify prefab path has proper extension
if not prefab_path.lower().endswith('.prefab'):
prefab_path = f"{prefab_path}.prefab"
# Check if a prefab already exists at this path
prefab_dir = '/'.join(prefab_path.split('/')[:-1]) or "Assets"
prefab_name = prefab_path.split('/')[-1]
prefab_assets = unity.send_command("GET_ASSET_LIST", {
"type": "Prefab",
"search_pattern": prefab_name,
"folder": prefab_dir
}).get("assets", [])
prefab_exists = any(asset.get("path") == prefab_path for asset in prefab_assets)
if prefab_exists and not overwrite:
return f"Prefab already exists at '{prefab_path}'. Use overwrite=True to replace it."
response = unity.send_command("CREATE_PREFAB", {
"object_name": object_name,
"prefab_path": prefab_path,
"overwrite": overwrite
})
if not response.get("success", False):
return f"Error creating prefab: {response.get('error', 'Unknown error')} (Object: {object_name}, Path: {prefab_path})"
return f"Prefab created successfully at {response.get('path', prefab_path)}"
except Exception as e:
return f"Error creating prefab: {str(e)} (Object: {object_name}, Path: {prefab_path})"
@mcp.tool()
def apply_prefab(
ctx: Context,
object_name: str
) -> str:
"""Apply changes made to a prefab instance back to the original prefab asset.
Args:
ctx: The MCP context
object_name: Name of the prefab instance in the scene
Returns:
str: Success message or error details
"""
try:
unity = get_unity_connection()
# Check if the GameObject exists
found_objects = unity.send_command("FIND_OBJECTS_BY_NAME", {
"name": object_name
}).get("objects", [])
if not found_objects:
return f"GameObject '{object_name}' not found in the scene."
# Check if the object is a prefab instance
object_props = unity.send_command("GET_OBJECT_PROPERTIES", {
"name": object_name
})
# Try to extract prefab status from properties
is_prefab_instance = object_props.get("isPrefabInstance", False)
if not is_prefab_instance:
return f"GameObject '{object_name}' is not a prefab instance."
response = unity.send_command("APPLY_PREFAB", {
"object_name": object_name
})
return response.get("message", "Prefab changes applied successfully")
except Exception as e:
return f"Error applying prefab changes: {str(e)}"

View File

@ -1,295 +0,0 @@
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)}"]

View File

@ -0,0 +1,53 @@
"""
Defines the execute_menu_item tool for running Unity Editor menu commands.
"""
from typing import Optional, Dict, Any
from mcp.server.fastmcp import FastMCP, Context
def register_execute_menu_item_tools(mcp: FastMCP):
"""Registers the execute_menu_item tool with the MCP server."""
@mcp.tool()
async def execute_menu_item(
ctx: Context,
menu_path: str,
action: Optional[str] = 'execute', # Allows extending later (e.g., 'validate', 'get_available')
parameters: Optional[Dict[str, Any]] = None, # For menu items that might accept parameters (less common)
# alias: Optional[str] = None, # Potential future addition for common commands
# context: Optional[Dict[str, Any]] = None # Potential future addition for context-specific menus
) -> Dict[str, Any]:
"""Executes a Unity Editor menu item via its path (e.g., "File/Save Project").
Args:
ctx: The MCP context.
menu_path: The full path of the menu item to execute.
action: The operation to perform (default: 'execute').
parameters: Optional parameters for the menu item (rarely used).
Returns:
A dictionary indicating success or failure, with optional message/error.
"""
action = action.lower() if action else 'execute'
# Prepare parameters for the C# handler
params_dict = {
"action": action,
"menuPath": menu_path,
"parameters": parameters if parameters else {},
# "alias": alias,
# "context": context
}
# Remove None values
params_dict = {k: v for k, v in params_dict.items() if v is not None}
if "parameters" not in params_dict:
params_dict["parameters"] = {} # Ensure parameters dict exists
# Forward the command to the Unity editor handler
# The C# handler is the static method HandleCommand in the ExecuteMenuItem class.
# We assume ctx.call is the correct way to invoke it via FastMCP.
# Note: The exact target string might need adjustment based on FastMCP's specifics.
csharp_handler_target = "UnityMCP.Editor.Tools.ExecuteMenuItem.HandleCommand"
return await ctx.call(csharp_handler_target, params_dict)

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 44d6968eea5de444880d425390b19ff4 guid: 67b82e49c36517040b7cfea8e421764e
DefaultImporter: DefaultImporter:
externalObjects: {} externalObjects: {}
userData: userData:

View File

@ -0,0 +1,66 @@
"""
Defines the manage_asset tool for interacting with Unity assets.
"""
from typing import Optional, Dict, Any, List
from mcp.server.fastmcp import FastMCP, Context
def register_manage_asset_tools(mcp: FastMCP):
"""Registers the manage_asset tool with the MCP server."""
@mcp.tool()
async def manage_asset(
ctx: Context,
action: str,
path: str,
asset_type: Optional[str] = None,
properties: Optional[Dict[str, Any]] = None,
destination: Optional[str] = None, # Used for move/duplicate
generate_preview: Optional[bool] = False,
# Search specific parameters
search_pattern: Optional[str] = None, # Replaces path for search action? Or use path as pattern?
filter_type: Optional[str] = None, # Redundant with asset_type?
filter_date_after: Optional[str] = None, # ISO 8601 format
page_size: Optional[int] = None,
page_number: Optional[int] = None
) -> Dict[str, Any]:
"""Performs asset operations (import, create, modify, delete, etc.) in Unity.
Args:
ctx: The MCP context.
action: Operation to perform (e.g., 'import', 'create', 'search').
path: Asset path (e.g., "Materials/MyMaterial.mat") or search scope.
asset_type: Asset type (e.g., 'Material', 'Folder') - required for 'create'.
properties: Dictionary of properties for 'create'/'modify'.
destination: Target path for 'duplicate'/'move'.
search_pattern: Search pattern (e.g., '*.prefab').
filter_*: Filters for search (type, date).
page_*: Pagination for search.
Returns:
A dictionary with operation results ('success', 'data', 'error').
"""
# Ensure properties is a dict if None
if properties is None:
properties = {}
# Prepare parameters for the C# handler
params_dict = {
"action": action.lower(),
"path": path,
"assetType": asset_type,
"properties": properties,
"destination": destination,
"generatePreview": generate_preview,
"searchPattern": search_pattern,
"filterType": filter_type,
"filterDateAfter": filter_date_after,
"pageSize": page_size,
"pageNumber": page_number
}
# Remove None values to avoid sending unnecessary nulls
params_dict = {k: v for k, v in params_dict.items() if v is not None}
# Forward the command to the Unity editor handler using the send_command method
# The C# side expects a command type and parameters.
return await ctx.send_command("manage_asset", params_dict)

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 9ac5db8cf38041644a81e7d655d879a9 guid: 27ffc6de0e9253e4f980ae545f07731a
DefaultImporter: DefaultImporter:
externalObjects: {} externalObjects: {}
userData: userData:

View File

@ -0,0 +1,69 @@
from mcp.server.fastmcp import FastMCP, Context
from typing import Optional, Dict, Any, Union
from unity_connection import get_unity_connection
def register_manage_editor_tools(mcp: FastMCP):
"""Register all editor management tools with the MCP server."""
@mcp.tool()
def manage_editor(
ctx: Context,
action: str,
wait_for_completion: Optional[bool] = None,
# --- Parameters for specific actions ---
# For 'set_active_tool'
tool_name: Optional[str] = None,
# For 'add_tag', 'remove_tag'
tag_name: Optional[str] = None,
# For 'add_layer', 'remove_layer'
layer_name: Optional[str] = None,
# Example: width: Optional[int] = None, height: Optional[int] = None
# Example: window_name: Optional[str] = None
# context: Optional[Dict[str, Any]] = None # Additional context
) -> Dict[str, Any]:
"""Controls and queries the Unity editor's state and settings.
Args:
action: Operation (e.g., 'play', 'pause', 'get_state', 'set_active_tool', 'add_tag').
wait_for_completion: Optional. If True, waits for certain actions.
Action-specific arguments (e.g., tool_name, tag_name, layer_name).
Returns:
Dictionary with operation results ('success', 'message', 'data').
"""
try:
# Prepare parameters, removing None values
params = {
"action": action,
"waitForCompletion": wait_for_completion,
"toolName": tool_name, # Corrected parameter name to match C#
"tagName": tag_name, # Pass tag name
"layerName": layer_name, # Pass layer name
# Add other parameters based on the action being performed
# "width": width,
# "height": height,
# etc.
}
params = {k: v for k, v in params.items() if v is not None}
# Send command to Unity
response = get_unity_connection().send_command("manage_editor", params)
# Process response
if response.get("success"):
return {"success": True, "message": response.get("message", "Editor operation successful."), "data": response.get("data")}
else:
return {"success": False, "message": response.get("error", "An unknown error occurred during editor management.")}
except Exception as e:
return {"success": False, "message": f"Python error managing editor: {str(e)}"}
# Example of potentially splitting into more specific tools:
# @mcp.tool()
# def get_editor_state(ctx: Context) -> Dict[str, Any]: ...
# @mcp.tool()
# def set_editor_playmode(ctx: Context, state: str) -> Dict[str, Any]: ... # state='play'/'pause'/'stop'
# @mcp.tool()
# def add_editor_tag(ctx: Context, tag_name: str) -> Dict[str, Any]: ...
# @mcp.tool()
# def add_editor_layer(ctx: Context, layer_name: str) -> Dict[str, Any]: ...

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 53b3a554a0ffeb04fb41b71ca78fda29 guid: a2f972b61922666418f99fa8f8ba817e
DefaultImporter: DefaultImporter:
externalObjects: {} externalObjects: {}
userData: userData:

View File

@ -0,0 +1,116 @@
from mcp.server.fastmcp import FastMCP, Context
from typing import Optional, Dict, Any, List, Union
from unity_connection import get_unity_connection
def register_manage_gameobject_tools(mcp: FastMCP):
"""Register all GameObject management tools with the MCP server."""
@mcp.tool()
def manage_gameobject(
ctx: Context,
action: str,
target: Optional[Union[str, int]] = None, # Name, path, or instance ID
search_method: Optional[str] = None, # by_name, by_tag, by_layer, by_component, by_id
# --- Parameters for 'create' ---
name: Optional[str] = None, # Required for 'create'
tag: Optional[str] = None, # Tag to assign during creation
parent: Optional[Union[str, int]] = None, # Name or ID of parent
position: Optional[List[float]] = None, # [x, y, z]
rotation: Optional[List[float]] = None, # [x, y, z] Euler angles
scale: Optional[List[float]] = None, # [x, y, z]
components_to_add: Optional[List[Union[str, Dict[str, Any]]]] = None, # List of component names or dicts with properties
primitive_type: Optional[str] = None, # Optional: create primitive (Cube, Sphere, etc.) instead of empty
save_as_prefab: Optional[bool] = False, # If True, save the created object as a prefab
prefab_path: Optional[str] = None, # Full path to save prefab (e.g., "Assets/Prefabs/MyObject.prefab"). Overrides prefab_folder.
prefab_folder: Optional[str] = "Assets/Prefabs", # Default folder if prefab_path not set (e.g., "Assets/Prefabs")
# --- Parameters for 'modify' ---
new_name: Optional[str] = None,
new_parent: Optional[Union[str, int]] = None,
set_active: Optional[bool] = None,
new_tag: Optional[str] = None,
new_layer: Optional[Union[str, int]] = None, # Layer name or number
components_to_remove: Optional[List[str]] = None,
component_properties: Optional[Dict[str, Dict[str, Any]]] = None, # { "ComponentName": { "propName": value } }
# --- Parameters for 'find' ---
search_term: Optional[str] = None, # Used with search_method (e.g., name, tag value, component type)
find_all: Optional[bool] = False, # Find all matches or just the first?
search_in_children: Optional[bool] = False, # Limit search scope
search_inactive: Optional[bool] = False, # Include inactive GameObjects?
# -- Component Management Arguments --
component_name: Optional[str] = None, # Target component for component actions
) -> Dict[str, Any]:
"""Manages GameObjects: create, modify, delete, find, and component operations.
Args:
action: Operation (e.g., 'create', 'modify', 'find', 'add_component').
target: GameObject identifier (name, path, ID) for modify/delete/component actions.
search_method: How to find objects ('by_name', 'by_id', 'by_path', etc.). Used with 'find'.
Action-specific arguments (e.g., name, parent, position for 'create';
component_name, component_properties for component actions;
search_term, find_all for 'find').
Returns:
Dictionary with operation results ('success', 'message', 'data').
"""
try:
# Prepare parameters, removing None values
params = {
"action": action,
"target": target,
"searchMethod": search_method,
"name": name,
"tag": tag,
"parent": parent,
"position": position,
"rotation": rotation,
"scale": scale,
"componentsToAdd": components_to_add,
"primitiveType": primitive_type,
"saveAsPrefab": save_as_prefab,
"prefabPath": prefab_path,
"prefabFolder": prefab_folder,
"newName": new_name,
"newParent": new_parent,
"setActive": set_active,
"newTag": new_tag,
"newLayer": new_layer,
"componentsToRemove": components_to_remove,
"componentProperties": component_properties,
"searchTerm": search_term,
"findAll": find_all,
"searchInChildren": search_in_children,
"searchInactive": search_inactive,
"componentName": component_name
}
params = {k: v for k, v in params.items() if v is not None}
# --- Handle Prefab Path Logic ---
if action == "create" and params.get("saveAsPrefab"): # Check if 'saveAsPrefab' is explicitly True in params
if "prefabPath" not in params:
if "name" not in params or not params["name"]:
return {"success": False, "message": "Cannot create default prefab path: 'name' parameter is missing."}
# Use the provided prefab_folder (which has a default) and the name to construct the path
constructed_path = f"{prefab_folder}/{params['name']}.prefab"
# Ensure clean path separators (Unity prefers '/')
params["prefabPath"] = constructed_path.replace("\\", "/")
elif not params["prefabPath"].lower().endswith(".prefab"):
return {"success": False, "message": f"Invalid prefab_path: '{params['prefabPath']}' must end with .prefab"}
# Ensure prefab_folder itself isn't sent if prefabPath was constructed or provided
# The C# side only needs the final prefabPath
params.pop("prefab_folder", None)
# --------------------------------
# Send the command to Unity via the established connection
# Use the get_unity_connection function to retrieve the active connection instance
# Changed "MANAGE_GAMEOBJECT" to "manage_gameobject" to potentially match Unity expectation
response = get_unity_connection().send_command("manage_gameobject", params)
# Check if the response indicates success
# If the response is not successful, raise an exception with the error message
if response.get("success"):
return {"success": True, "message": response.get("message", "GameObject operation successful."), "data": response.get("data")}
else:
return {"success": False, "message": response.get("error", "An unknown error occurred during GameObject management.")}
except Exception as e:
return {"success": False, "message": f"Python error managing GameObject: {str(e)}"}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 0b8eb3f808238b040a4b41766228664f guid: b34907e09ab90854fa849302b96c6247
DefaultImporter: DefaultImporter:
externalObjects: {} externalObjects: {}
userData: userData:

View File

@ -0,0 +1,56 @@
from mcp.server.fastmcp import FastMCP, Context
from typing import Optional, Dict, Any
from unity_connection import get_unity_connection
def register_manage_scene_tools(mcp: FastMCP):
"""Register all scene management tools with the MCP server."""
@mcp.tool()
def manage_scene(
ctx: Context,
action: str,
name: Optional[str] = None,
path: Optional[str] = None,
build_index: Optional[int] = None,
# Add other potential parameters like load_additive, etc. if needed
# context: Optional[Dict[str, Any]] = None # Future: Contextual info (e.g., current project settings)
) -> Dict[str, Any]:
"""Manages Unity scenes (load, save, create, get hierarchy, etc.).
Args:
action: Operation (e.g., 'load', 'save', 'create', 'get_hierarchy').
name: Scene name (no extension) for create/load/save.
path: Asset path for scene operations (default: "Assets/").
build_index: Build index for load/build settings actions.
# Add other action-specific args as needed (e.g., for hierarchy depth)
Returns:
Dictionary with results ('success', 'message', 'data').
"""
try:
# Prepare parameters, removing None values
params = {
"action": action,
"name": name,
"path": path,
"buildIndex": build_index
}
params = {k: v for k, v in params.items() if v is not None}
# Send command to Unity
response = get_unity_connection().send_command("manage_scene", params)
# Process response
if response.get("success"):
return {"success": True, "message": response.get("message", "Scene operation successful."), "data": response.get("data")}
else:
return {"success": False, "message": response.get("error", "An unknown error occurred during scene management.")}
except Exception as e:
return {"success": False, "message": f"Python error managing scene: {str(e)}"}
# Consider adding specific tools if the single 'manage_scene' becomes too complex:
# @mcp.tool()
# def load_scene(ctx: Context, name: str, path: Optional[str] = None, build_index: Optional[int] = None) -> Dict[str, Any]: ...
# @mcp.tool()
# def get_scene_hierarchy(ctx: Context) -> Dict[str, Any]: ...

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: be712c04494a1874593719eeb2a882ac
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,63 @@
from mcp.server.fastmcp import FastMCP, Context
from typing import Optional, Dict, Any
from unity_connection import get_unity_connection
import os
def register_manage_script_tools(mcp: FastMCP):
"""Register all script management tools with the MCP server."""
@mcp.tool()
def manage_script(
ctx: Context,
action: str,
name: str,
path: Optional[str] = None,
contents: Optional[str] = None,
script_type: Optional[str] = None,
namespace: Optional[str] = None
) -> Dict[str, Any]:
"""Manages C# scripts in Unity (create, read, update, delete).
Args:
action: Operation ('create', 'read', 'update', 'delete').
name: Script name (no .cs extension).
path: Asset path (optional, default: "Assets/").
contents: C# code for 'create'/'update'.
script_type: Type hint (e.g., 'MonoBehaviour', optional).
namespace: Script namespace (optional).
Returns:
Dictionary with results ('success', 'message', 'data').
"""
try:
# Prepare parameters for Unity
params = {
"action": action,
"name": name,
"path": path,
"contents": contents,
"scriptType": script_type,
"namespace": namespace
}
# Remove None values so they don't get sent as null
params = {k: v for k, v in params.items() if v is not None}
# Send command to Unity
response = get_unity_connection().send_command("manage_script", params)
# Process response from Unity
if response.get("success"):
return {"success": True, "message": response.get("message", "Operation successful."), "data": response.get("data")}
else:
return {"success": False, "message": response.get("error", "An unknown error occurred.")}
except Exception as e:
# Handle Python-side errors (e.g., connection issues)
return {"success": False, "message": f"Python error managing script: {str(e)}"}
# Potentially add more specific helper tools if needed later, e.g.:
# @mcp.tool()
# def create_script(...): ...
# @mcp.tool()
# def read_script(...): ...
# etc.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: beb93a353b9140c44b7ac22d2bb8481a
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,89 +0,0 @@
from mcp.server.fastmcp import FastMCP, Context
from typing import List, Optional
from unity_connection import get_unity_connection
def register_material_tools(mcp: FastMCP):
"""Register all material-related tools with the MCP server."""
@mcp.tool()
def set_material(
ctx: Context,
object_name: str,
material_name: Optional[str] = None,
color: Optional[List[float]] = None,
create_if_missing: bool = True
) -> str:
"""
Apply or create a material for a game object. If material_name is provided,
the material will be saved as a shared asset in the Materials folder.
Args:
object_name: Target game object.
material_name: Optional material name. If provided, creates/uses a shared material asset.
color: Optional [R, G, B] or [R, G, B, A] values (0.0-1.0).
create_if_missing: Whether to create the material if it doesn't exist (default: True).
Returns:
str: Status message indicating success or failure.
"""
try:
unity = get_unity_connection()
# Check if the object exists
object_response = unity.send_command("FIND_OBJECTS_BY_NAME", {
"name": object_name
})
objects = object_response.get("objects", [])
if not objects:
return f"GameObject '{object_name}' not found in the scene."
# If a material name is specified, check if it exists
if material_name:
material_assets = unity.send_command("GET_ASSET_LIST", {
"type": "Material",
"search_pattern": material_name,
"folder": "Assets/Materials"
}).get("assets", [])
material_exists = any(asset.get("name") == material_name for asset in material_assets)
if not material_exists and not create_if_missing:
return f"Material '{material_name}' not found. Use create_if_missing=True to create it."
# Validate color values if provided
if color:
# Check if color has the right number of components (RGB or RGBA)
if not (len(color) == 3 or len(color) == 4):
return f"Error: Color must have 3 (RGB) or 4 (RGBA) components, but got {len(color)}."
# Check if all color values are in the 0-1 range
for i, value in enumerate(color):
if not isinstance(value, (int, float)):
return f"Error: Color component at index {i} is not a number."
if value < 0.0 or value > 1.0:
channel = "RGBA"[i] if i < 4 else f"component {i}"
return f"Error: Color {channel} value must be in the range 0.0-1.0, but got {value}."
# Set up parameters for the command
params = {
"object_name": object_name,
"create_if_missing": create_if_missing
}
if material_name:
params["material_name"] = material_name
if color:
params["color"] = color
result = unity.send_command("SET_MATERIAL", params)
material_name = result.get("material_name", "unknown")
material_path = result.get("path")
if material_path:
return f"Applied shared material '{material_name}' to {object_name} (saved at {material_path})"
else:
return f"Applied instance material '{material_name}' to {object_name}"
except Exception as e:
return f"Error setting material: {str(e)}"

View File

@ -1,250 +0,0 @@
"""Tools for inspecting and manipulating Unity objects."""
from typing import Optional, List, Dict, Any
from mcp.server.fastmcp import FastMCP, Context
from unity_connection import get_unity_connection
def register_object_tools(mcp: FastMCP):
"""Register all object inspection and manipulation tools with the MCP server."""
@mcp.tool()
def get_object_properties(
ctx: Context,
name: str
) -> Dict[str, Any]:
"""Get all properties of a specified game object.
Args:
ctx: The MCP context
name: Name of the game object to inspect
Returns:
Dict containing the object's properties, components, and their values
"""
try:
response = get_unity_connection().send_command("GET_OBJECT_PROPERTIES", {
"name": name
})
return response
except Exception as e:
return {"error": f"Failed to get object properties: {str(e)}"}
@mcp.tool()
def get_component_properties(
ctx: Context,
object_name: str,
component_type: str
) -> Dict[str, Any]:
"""Get properties of a specific component on a game object.
Args:
ctx: The MCP context
object_name: Name of the game object
component_type: Type of the component to inspect
Returns:
Dict containing the component's properties and their values
"""
try:
response = get_unity_connection().send_command("GET_COMPONENT_PROPERTIES", {
"object_name": object_name,
"component_type": component_type
})
return response
except Exception as e:
return {"error": f"Failed to get component properties: {str(e)}"}
@mcp.tool()
def find_objects_by_name(
ctx: Context,
name: str
) -> List[Dict[str, str]]:
"""Find game objects in the scene by name.
Args:
ctx: The MCP context
name: Name to search for (partial matches are supported)
Returns:
List of dicts containing object names and their paths
"""
try:
response = get_unity_connection().send_command("FIND_OBJECTS_BY_NAME", {
"name": name
})
return response.get("objects", [])
except Exception as e:
return [{"error": f"Failed to find objects: {str(e)}"}]
@mcp.tool()
def find_objects_by_tag(
ctx: Context,
tag: str
) -> List[Dict[str, str]]:
"""Find game objects in the scene by tag.
Args:
ctx: The MCP context
tag: Tag to search for
Returns:
List of dicts containing object names and their paths
"""
try:
response = get_unity_connection().send_command("FIND_OBJECTS_BY_TAG", {
"tag": tag
})
return response.get("objects", [])
except Exception as e:
return [{"error": f"Failed to find objects: {str(e)}"}]
@mcp.tool()
def get_scene_info(ctx: Context) -> Dict[str, Any]:
"""Get information about the current scene.
Args:
ctx: The MCP context
Returns:
Dict containing scene information including name and root objects
"""
try:
response = get_unity_connection().send_command("GET_SCENE_INFO")
return response
except Exception as e:
return {"error": f"Failed to get scene info: {str(e)}"}
@mcp.tool()
def get_hierarchy(ctx: Context) -> Dict[str, Any]:
"""Get the current hierarchy of game objects in the scene.
Args:
ctx: The MCP context
Returns:
Dict containing the scene hierarchy as a tree structure
"""
try:
response = get_unity_connection().send_command("GET_HIERARCHY")
return response
except Exception as e:
return {"error": f"Failed to get hierarchy: {str(e)}"}
@mcp.tool()
def select_object(
ctx: Context,
name: str
) -> Dict[str, str]:
"""Select a game object in the Unity Editor.
Args:
ctx: The MCP context
name: Name of the object to select
Returns:
Dict containing the name of the selected object
"""
try:
response = get_unity_connection().send_command("SELECT_OBJECT", {
"name": name
})
return response
except Exception as e:
return {"error": f"Failed to select object: {str(e)}"}
@mcp.tool()
def get_selected_object(ctx: Context) -> Optional[Dict[str, str]]:
"""Get the currently selected game object in the Unity Editor.
Args:
ctx: The MCP context
Returns:
Dict containing the selected object's name and path, or None if no object is selected
"""
try:
response = get_unity_connection().send_command("GET_SELECTED_OBJECT")
return response.get("selected")
except Exception as e:
return {"error": f"Failed to get selected object: {str(e)}"}
@mcp.tool()
def get_asset_list(
ctx: Context,
type: Optional[str] = None,
search_pattern: str = "*",
folder: str = "Assets"
) -> List[Dict[str, str]]:
"""Get a list of assets in the project.
Args:
ctx: The MCP context
type: Optional asset type to filter by
search_pattern: Pattern to search for in asset names
folder: Folder to search in (default: "Assets")
Returns:
List of dicts containing asset information
"""
try:
response = get_unity_connection().send_command("GET_ASSET_LIST", {
"type": type,
"search_pattern": search_pattern,
"folder": folder
})
return response.get("assets", [])
except Exception as e:
return [{"error": f"Failed to get asset list: {str(e)}"}]
@mcp.tool()
def execute_context_menu_item(
ctx: Context,
object_name: str,
component: str,
context_menu_item: str
) -> Dict[str, Any]:
"""Execute a specific [ContextMenu] method on a component of a given game object.
Args:
ctx: The MCP context
object_name: Name of the game object to call
component: Name of the component type
context_menu_item: Name of the context menu item to execute
Returns:
Dict containing the result of the operation
"""
try:
unity = get_unity_connection()
# Check if the object exists
found_objects = unity.send_command("FIND_OBJECTS_BY_NAME", {
"name": object_name
}).get("objects", [])
if not found_objects:
return {"error": f"Object with name '{object_name}' not found in the scene."}
# Check if the component exists on the object
object_props = unity.send_command("GET_OBJECT_PROPERTIES", {
"name": object_name
})
if "error" in object_props:
return {"error": f"Failed to get object properties: {object_props['error']}"}
components = object_props.get("components", [])
component_exists = any(comp.get("type") == component for comp in components)
if not component_exists:
return {"error": f"Component '{component}' is not attached to object '{object_name}'."}
# Now execute the context menu item
response = unity.send_command("EXECUTE_CONTEXT_MENU_ITEM", {
"object_name": object_name,
"component": component,
"context_menu_item": context_menu_item
})
return response
except Exception as e:
return {"error": f"Failed to execute context menu item: {str(e)}"}

View File

@ -0,0 +1,60 @@
"""
Defines the read_console tool for accessing Unity Editor console messages.
"""
from typing import Optional, List, Dict, Any
from mcp.server.fastmcp import FastMCP, Context
def register_read_console_tools(mcp: FastMCP):
"""Registers the read_console tool with the MCP server."""
@mcp.tool()
async def read_console(
ctx: Context,
action: Optional[str] = 'get', # Default action is to get messages
types: Optional[List[str]] = ['error', 'warning', 'log'], # Default types to retrieve
count: Optional[int] = None, # Max number of messages to return (null for all matching)
filter_text: Optional[str] = None, # Text to filter messages by
since_timestamp: Optional[str] = None, # ISO 8601 timestamp to get messages since
format: Optional[str] = 'detailed', # 'plain', 'detailed', 'json'
include_stacktrace: Optional[bool] = True, # Whether to include stack traces in detailed/json formats
# context: Optional[Dict[str, Any]] = None # Future context
) -> Dict[str, Any]:
"""Gets messages from or clears the Unity Editor console.
Args:
action: Operation ('get' or 'clear').
types: Message types to get ('error', 'warning', 'log', 'all').
count: Max messages to return.
filter_text: Text filter for messages.
since_timestamp: Get messages after this timestamp (ISO 8601).
format: Output format ('plain', 'detailed', 'json').
include_stacktrace: Include stack traces in output.
Returns:
Dictionary with results. For 'get', includes 'data' (messages).
"""
# Normalize action
action = action.lower() if action else 'get'
# Prepare parameters for the C# handler
params_dict = {
"action": action,
"types": types if types else ['error', 'warning', 'log'], # Ensure types is not None
"count": count,
"filterText": filter_text,
"sinceTimestamp": since_timestamp,
"format": format.lower() if format else 'detailed',
"includeStacktrace": include_stacktrace
}
# Remove None values unless it's 'count' (as None might mean 'all')
params_dict = {k: v for k, v in params_dict.items() if v is not None or k == 'count'}
# Add count back if it was None, explicitly sending null might be important for C# logic
if 'count' not in params_dict:
params_dict['count'] = None
# Forward the command to the Unity editor handler
# The C# handler name might need adjustment (e.g., CommandRegistry)
return await ctx.bridge.unity_editor.HandleReadConsole(params_dict)

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c94ba17ca2284764f99d61356c5feded
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,338 +0,0 @@
from mcp.server.fastmcp import FastMCP, Context
from typing import List, Dict, Any, Optional
import json
from unity_connection import get_unity_connection
def register_scene_tools(mcp: FastMCP):
"""Register all scene-related tools with the MCP server."""
@mcp.tool()
def get_scene_info(ctx: Context) -> str:
"""Retrieve detailed info about the current Unity scene.
Returns:
str: JSON string containing scene information including:
- sceneName: Name of the current scene
- rootObjects: List of root GameObject names in the scene
"""
try:
unity = get_unity_connection()
result = unity.send_command("GET_SCENE_INFO")
return json.dumps(result, indent=2)
except Exception as e:
return f"Error getting scene info: {str(e)}"
@mcp.tool()
def open_scene(ctx: Context, scene_path: str) -> str:
"""Open a specified scene in the Unity editor.
Args:
scene_path: Full path to the scene file (e.g., "Assets/Scenes/MyScene.unity")
Returns:
str: Success message or error details
"""
try:
unity = get_unity_connection()
# Check if the scene exists in the project
scenes = unity.send_command("GET_ASSET_LIST", {
"type": "Scene",
"search_pattern": scene_path.split('/')[-1],
"folder": '/'.join(scene_path.split('/')[:-1]) or "Assets"
}).get("assets", [])
# Check if any scene matches the exact path
scene_exists = any(scene.get("path") == scene_path for scene in scenes)
if not scene_exists:
return f"Scene at '{scene_path}' not found in the project."
result = unity.send_command("OPEN_SCENE", {"scene_path": scene_path})
return result.get("message", "Scene opened successfully")
except Exception as e:
return f"Error opening scene: {str(e)}"
@mcp.tool()
def save_scene(ctx: Context) -> str:
"""Save the current scene to its file.
Returns:
str: Success message or error details
"""
try:
unity = get_unity_connection()
result = unity.send_command("SAVE_SCENE")
return result.get("message", "Scene saved successfully")
except Exception as e:
return f"Error saving scene: {str(e)}"
@mcp.tool()
def new_scene(ctx: Context, scene_path: str, overwrite: bool = False) -> str:
"""Create a new empty scene in the Unity editor.
Args:
scene_path: Full path where the new scene should be saved (e.g., "Assets/Scenes/NewScene.unity")
overwrite: Whether to overwrite if scene already exists (default: False)
Returns:
str: Success message or error details
"""
try:
unity = get_unity_connection()
# Check if a scene with this path already exists
scenes = unity.send_command("GET_ASSET_LIST", {
"type": "Scene",
"search_pattern": scene_path.split('/')[-1],
"folder": '/'.join(scene_path.split('/')[:-1]) or "Assets"
}).get("assets", [])
# Check if any scene matches the exact path
scene_exists = any(scene.get("path") == scene_path for scene in scenes)
if scene_exists and not overwrite:
return f"Scene at '{scene_path}' already exists. Use overwrite=True to replace it."
# Create new scene
result = unity.send_command("NEW_SCENE", {
"scene_path": scene_path,
"overwrite": overwrite
})
# Save the scene to ensure it's properly created
unity.send_command("SAVE_SCENE")
# Get scene info to verify it's loaded
scene_info = unity.send_command("GET_SCENE_INFO")
return result.get("message", "New scene created successfully")
except Exception as e:
return f"Error creating new scene: {str(e)}"
@mcp.tool()
def change_scene(ctx: Context, scene_path: str, save_current: bool = False) -> str:
"""Change to a different scene, optionally saving the current one.
Args:
scene_path: Full path to the target scene file (e.g., "Assets/Scenes/TargetScene.unity")
save_current: Whether to save the current scene before changing (default: False)
Returns:
str: Success message or error details
"""
try:
unity = get_unity_connection()
result = unity.send_command("CHANGE_SCENE", {
"scene_path": scene_path,
"save_current": save_current
})
return result.get("message", "Scene changed successfully")
except Exception as e:
return f"Error changing scene: {str(e)}"
@mcp.tool()
def get_object_info(ctx: Context, object_name: str) -> str:
"""
Get info about a specific game object.
Args:
object_name: Name of the game object.
"""
try:
unity = get_unity_connection()
result = unity.send_command("GET_OBJECT_INFO", {"name": object_name})
return json.dumps(result, indent=2)
except Exception as e:
return f"Error getting object info: {str(e)}"
@mcp.tool()
def create_object(
ctx: Context,
type: str = "CUBE",
name: str = None,
location: List[float] = None,
rotation: List[float] = None,
scale: List[float] = None,
replace_if_exists: bool = False
) -> str:
"""
Create a game object in the Unity scene.
Args:
type: Object type (CUBE, SPHERE, CYLINDER, CAPSULE, PLANE, EMPTY, CAMERA, LIGHT).
name: Optional name for the game object.
location: [x, y, z] position (defaults to [0, 0, 0]).
rotation: [x, y, z] rotation in degrees (defaults to [0, 0, 0]).
scale: [x, y, z] scale factors (defaults to [1, 1, 1]).
replace_if_exists: Whether to replace if an object with the same name exists (default: False)
Returns:
Confirmation message with the created object's name.
"""
try:
unity = get_unity_connection()
# Check if an object with the specified name already exists (if name is provided)
if name:
found_objects = unity.send_command("FIND_OBJECTS_BY_NAME", {
"name": name
}).get("objects", [])
if found_objects and not replace_if_exists:
return f"Object with name '{name}' already exists. Use replace_if_exists=True to replace it."
elif found_objects and replace_if_exists:
# Delete the existing object
unity.send_command("DELETE_OBJECT", {"name": name})
# Create the new object
params = {
"type": type.upper(),
"location": location or [0, 0, 0],
"rotation": rotation or [0, 0, 0],
"scale": scale or [1, 1, 1]
}
if name:
params["name"] = name
result = unity.send_command("CREATE_OBJECT", params)
return f"Created {type} game object: {result['name']}"
except Exception as e:
return f"Error creating game object: {str(e)}"
@mcp.tool()
def modify_object(
ctx: Context,
name: str,
location: Optional[List[float]] = None,
rotation: Optional[List[float]] = None,
scale: Optional[List[float]] = None,
visible: Optional[bool] = None,
set_parent: Optional[str] = None,
add_component: Optional[str] = None,
remove_component: Optional[str] = None,
set_property: Optional[Dict[str, Any]] = None
) -> str:
"""
Modify a game object's properties and components.
Args:
name: Name of the game object to modify.
location: Optional [x, y, z] position.
rotation: Optional [x, y, z] rotation in degrees.
scale: Optional [x, y, z] scale factors.
visible: Optional visibility toggle.
set_parent: Optional name of the parent object to set.
add_component: Optional name of the component type to add (e.g., "Rigidbody", "BoxCollider").
remove_component: Optional name of the component type to remove.
set_property: Optional dict with keys:
- component: Name of the component type
- property: Name of the property to set
- value: Value to set the property to
Returns:
str: Success message or error details
"""
try:
unity = get_unity_connection()
# Check if the object exists
found_objects = unity.send_command("FIND_OBJECTS_BY_NAME", {
"name": name
}).get("objects", [])
if not found_objects:
return f"Object with name '{name}' not found in the scene."
# If set_parent is provided, check if parent object exists
if set_parent is not None:
parent_objects = unity.send_command("FIND_OBJECTS_BY_NAME", {
"name": set_parent
}).get("objects", [])
if not parent_objects:
return f"Parent object '{set_parent}' not found in the scene."
# If we're adding a component, we could also check if it's already attached
if add_component is not None:
object_props = unity.send_command("GET_OBJECT_PROPERTIES", {
"name": name
})
components = object_props.get("components", [])
component_exists = any(comp.get("type") == add_component for comp in components)
if component_exists:
return f"Component '{add_component}' is already attached to '{name}'."
# If we're removing a component, check if it exists
if remove_component is not None:
object_props = unity.send_command("GET_OBJECT_PROPERTIES", {
"name": name
})
components = object_props.get("components", [])
component_exists = any(comp.get("type") == remove_component for comp in components)
if not component_exists:
return f"Component '{remove_component}' is not attached to '{name}'."
params = {"name": name}
# Add basic transform properties
if location is not None:
params["location"] = location
if rotation is not None:
params["rotation"] = rotation
if scale is not None:
params["scale"] = scale
if visible is not None:
params["visible"] = visible
# Add parent setting
if set_parent is not None:
params["set_parent"] = set_parent
# Add component operations
if add_component is not None:
params["add_component"] = add_component
if remove_component is not None:
params["remove_component"] = remove_component
# Add property setting
if set_property is not None:
params["set_property"] = set_property
result = unity.send_command("MODIFY_OBJECT", params)
return f"Modified game object: {result['name']}"
except Exception as e:
return f"Error modifying game object: {str(e)}"
@mcp.tool()
def delete_object(ctx: Context, name: str, ignore_missing: bool = False) -> str:
"""
Remove a game object from the scene.
Args:
name: Name of the game object to delete.
ignore_missing: Whether to silently ignore if the object doesn't exist (default: False)
Returns:
str: Success message or error details
"""
try:
unity = get_unity_connection()
# Check if the object exists
found_objects = unity.send_command("FIND_OBJECTS_BY_NAME", {
"name": name
}).get("objects", [])
if not found_objects:
if ignore_missing:
return f"No object named '{name}' found to delete. Ignoring."
else:
return f"Error: Object '{name}' not found in the scene."
result = unity.send_command("DELETE_OBJECT", {"name": name})
return f"Deleted game object: {name}"
except Exception as e:
return f"Error deleting game object: {str(e)}"

View File

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: ed5ef5bed9e67a34297b908a0e15a8dc
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,280 +0,0 @@
from mcp.server.fastmcp import FastMCP, Context
from typing import List
from unity_connection import get_unity_connection
import base64
def register_script_tools(mcp: FastMCP):
"""Register all script-related tools with the MCP server."""
@mcp.tool()
def view_script(ctx: Context, script_path: str, require_exists: bool = True) -> str:
"""View the contents of a Unity script file.
Args:
ctx: The MCP context
script_path: Path to the script file relative to the Assets folder
require_exists: Whether to raise an error if the file doesn't exist (default: True)
Returns:
str: The contents of the script file or error message
"""
try:
# Normalize script path to ensure it has the correct format
if not script_path.startswith("Assets/"):
script_path = f"Assets/{script_path}"
# Debug to help diagnose issues
print(f"ViewScript - Using normalized script path: {script_path}")
# Send command to Unity to read the script file
response = get_unity_connection().send_command(
"VIEW_SCRIPT",
{"script_path": script_path, "require_exists": require_exists},
)
if response.get("exists", True):
if response.get("encoding", "base64") == "base64":
decoded_content = base64.b64decode(response.get("content")).decode(
"utf-8"
)
return decoded_content
return response.get("content", "Script contents not available")
else:
return response.get("message", "Script not found")
except Exception as e:
return f"Error viewing script: {str(e)}"
@mcp.tool()
def create_script(
ctx: Context,
script_name: str,
script_type: str = "MonoBehaviour",
namespace: str = None,
template: str = None,
script_folder: str = None,
overwrite: bool = False,
content: str = None,
) -> str:
"""Create a new Unity script file.
Args:
ctx: The MCP context
script_name: Name of the script (without .cs extension)
script_type: Type of script (e.g., MonoBehaviour, ScriptableObject)
namespace: Optional namespace for the script
template: Optional custom template to use
script_folder: Optional folder path within Assets to create the script
overwrite: Whether to overwrite if script already exists (default: False)
content: Optional custom content for the script
Returns:
str: Success message or error details
"""
try:
unity = get_unity_connection()
# Determine script path based on script_folder parameter
if script_folder:
# Use provided folder path
# Normalize the folder path first
if script_folder.startswith("Assets/"):
normalized_folder = script_folder
else:
normalized_folder = f"Assets/{script_folder}"
# Create the full path
if normalized_folder.endswith("/"):
script_path = f"{normalized_folder}{script_name}.cs"
else:
script_path = f"{normalized_folder}/{script_name}.cs"
# Debug to help diagnose issues
print(f"CreateScript - Folder: {script_folder}")
print(f"CreateScript - Normalized folder: {normalized_folder}")
print(f"CreateScript - Script path: {script_path}")
else:
# Default to Scripts folder when no folder is provided
script_path = f"Assets/Scripts/{script_name}.cs"
print(f"CreateScript - Using default script path: {script_path}")
# Send command to Unity to create the script directly
# The C# handler will handle the file existence check
params = {
"script_name": script_name,
"script_type": script_type,
"namespace": namespace,
"template": template,
"overwrite": overwrite,
}
# Add script_folder if provided
if script_folder:
params["script_folder"] = script_folder
# Add content if provided
if content:
params["content"] = content
response = unity.send_command("CREATE_SCRIPT", params)
return response.get("message", "Script created successfully")
except Exception as e:
return f"Error creating script: {str(e)}"
@mcp.tool()
def update_script(
ctx: Context,
script_path: str,
content: str,
create_if_missing: bool = False,
create_folder_if_missing: bool = False,
) -> str:
"""Update the contents of an existing Unity script.
Args:
ctx: The MCP context
script_path: Path to the script file relative to the Assets folder
content: New content for the script
create_if_missing: Whether to create the script if it doesn't exist (default: False)
create_folder_if_missing: Whether to create the parent directory if it doesn't exist (default: False)
Returns:
str: Success message or error details
"""
try:
unity = get_unity_connection()
# Normalize script path to ensure it has the correct format
# Make sure the path starts with Assets/ but not Assets/Assets/
if not script_path.startswith("Assets/"):
script_path = f"Assets/{script_path}"
# Debug to help diagnose issues
print(f"UpdateScript - Original path: {script_path}")
# Parse script path (for potential creation)
script_name = script_path.split("/")[-1]
if not script_name.endswith(".cs"):
script_name += ".cs"
script_path = f"{script_path}.cs"
if create_if_missing:
# When create_if_missing is true, we'll just try to update directly,
# and let Unity handle the creation if needed
params = {
"script_path": script_path,
"content": content,
"create_if_missing": True,
}
# Add folder creation flag if requested
if create_folder_if_missing:
params["create_folder_if_missing"] = True
# Send command to Unity to update/create the script
response = unity.send_command("UPDATE_SCRIPT", params)
return response.get("message", "Script updated successfully")
else:
# Standard update without creation flags
response = unity.send_command(
"UPDATE_SCRIPT", {"script_path": script_path, "content": content}
)
return response.get("message", "Script updated successfully")
except Exception as e:
return f"Error updating script: {str(e)}"
@mcp.tool()
def list_scripts(ctx: Context, folder_path: str = "Assets") -> str:
"""List all script files in a specified folder.
Args:
ctx: The MCP context
folder_path: Path to the folder to search (default: Assets)
Returns:
str: List of script files or error message
"""
try:
# Send command to Unity to list scripts
response = get_unity_connection().send_command(
"LIST_SCRIPTS", {"folder_path": folder_path}
)
scripts = response.get("scripts", [])
if not scripts:
return "No scripts found in the specified folder"
return "\n".join(scripts)
except Exception as e:
return f"Error listing scripts: {str(e)}"
@mcp.tool()
def attach_script(
ctx: Context, object_name: str, script_name: str, script_path: str = None
) -> str:
"""Attach a script component to a GameObject.
Args:
ctx: The MCP context
object_name: Name of the target GameObject in the scene
script_name: Name of the script to attach (with or without .cs extension)
script_path: Optional full path to the script (if not in the default Scripts folder)
Returns:
str: Success message or error details
"""
try:
unity = get_unity_connection()
# Check if the object exists
object_response = unity.send_command(
"FIND_OBJECTS_BY_NAME", {"name": object_name}
)
objects = object_response.get("objects", [])
if not objects:
return f"GameObject '{object_name}' not found in the scene."
# Ensure script_name has .cs extension
if not script_name.lower().endswith(".cs"):
script_name = f"{script_name}.cs"
# Remove any path information from script_name if it contains slashes
script_basename = script_name.split("/")[-1]
# Determine the full script path if provided
if script_path is not None:
# Ensure script_path starts with Assets/
if not script_path.startswith("Assets/"):
script_path = f"Assets/{script_path}"
# If path is just a directory, append the script name
if not script_path.endswith(script_basename):
if script_path.endswith("/"):
script_path = f"{script_path}{script_basename}"
else:
script_path = f"{script_path}/{script_basename}"
# Check if the script is already attached
object_props = unity.send_command(
"GET_OBJECT_PROPERTIES", {"name": object_name}
)
# Extract script name without .cs and without path for component type checking
script_class_name = script_basename.replace(".cs", "")
# Check if component is already attached
components = object_props.get("components", [])
for component in components:
if component.get("type") == script_class_name:
return f"Script '{script_class_name}' is already attached to '{object_name}'."
# Send command to Unity to attach the script
params = {"object_name": object_name, "script_name": script_basename}
# Add script_path if provided
if script_path:
params["script_path"] = script_path
response = unity.send_command("ATTACH_SCRIPT", params)
return response.get("message", "Script attached successfully")
except Exception as e:
return f"Error attaching script: {str(e)}"

View File

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: bce1a207771693f4ba78d880688360d4
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

20
example-prompt-v2.md Normal file
View File

@ -0,0 +1,20 @@
# Create a "Collect the Cubes" game
Objective: The player controls a simple 3D character (like a sphere or capsule) that moves around a flat 3D environment to collect floating cubes before a timer runs out.
Win Condition: Collect all the cubes (e.g., 510) to win.
Lose Condition: Timer runs out before all cubes are collected.
## Steps
Create a 3D plane in the scene and position it as the ground.
Add a 3D sphere to the scene as the player object.
Attach a Rigidbody component to the sphere.
Create a new script called "PlayerMovement" and attach it to the sphere.
Add five 3D cubes to the scene, positioning them at different spots above the ground.
Add a Collider component to each cube and set it as a trigger.
Create a new script called "Collectible" and attach it to each cube.
Create an empty GameObject called "GameManager" in the scene.
Create a new script called "GameController" and attach it to the GameManager.
Add a UI Text element to the scene for displaying the score.
Add a second UI Text element to the scene for displaying the timer.
Create a UI Text element for a win message and set it to be invisible by default.
Create a UI Text element for a lose message and set it to be invisible by default.
Save the scene.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 870319fa77a9f444ea4b604112bc761e
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

83
example-prompt.md Normal file
View File

@ -0,0 +1,83 @@
Create an endless runner game based on the Google Dinosaur game concept.
Scene Setup:
Create a new 2D scene named "EndlessRunner".
Configure the Main Camera for 2D orthographic view.
Player Character:
Create a GameObject for the Player (e.g., a simple sprite or 2D shape like a square).
Position the Player towards the left side of the screen, slightly above the ground level.
Add appropriate 2D physics components (Rigidbody2D, Collider2D) to the Player. Configure gravity.
Create a PlayerController script and attach it to the Player.
Implement jump functionality triggered by player input (e.g., Spacebar, mouse click, or screen tap). This should apply an upward force.
Prevent double-jumping unless intended (check if grounded).
Detect collisions, specifically with objects tagged as "Obstacle".
Ground:
Create at least two Ground GameObjects (e.g., long thin sprites or shapes) that can be placed end-to-end.
Add Collider2D components to the Ground GameObjects so the player can stand on them.
Create a script (e.g., GroundScroller) to manage ground movement.
Implement continuous scrolling movement from right to left for the ground segments.
Implement logic to reposition ground segments that move off-screen to the left back to the right side, creating an infinite loop.
Obstacles:
Create at least one Obstacle prefab (e.g., a different sprite or shape representing a cactus).
Add a Collider2D component to the Obstacle prefab.
Assign a specific tag (e.g., "Obstacle") to the Obstacle prefab.
Create an empty GameObject named ObstacleSpawner.
Create an ObstacleSpawner script and attach it.
Implement logic to periodically spawn Obstacle prefabs at a set position off-screen to the right.
Introduce random variation in the time between spawns.
(Optional) Implement logic to choose randomly between different obstacle types if more than one prefab is created.
Create an ObstacleMover script and attach it to the Obstacle prefab(s).
Implement movement for spawned obstacles from right to left at the game's current speed.
Implement logic to destroy obstacles once they move off-screen to the left.
Game Management:
Create an empty GameObject named GameManager.
Create a GameManager script and attach it.
Manage the overall game state (e.g., Initializing, Playing, GameOver).
Track the player's score, increasing it over time while the game state is "Playing".
Control the game's speed, gradually increasing the scrolling speed of the ground and obstacles over time.
Implement Game Over logic: triggered when the Player collides with an "Obstacle". This should stop all movement (player, ground, obstacles) and change the game state.
Implement Restart logic: allow the player to restart the game (e.g., by pressing a key or button) after a Game Over, resetting the score, speed, and scene elements.
User Interface (UI):
Create a UI Canvas.
Add a UI Text element to display the current score, updated by the GameManager.
Add UI elements for the Game Over screen (e.g., "Game Over" text, final score display, restart instructions). These should be hidden initially and shown when the game state changes to "GameOver".

7
example-prompt.md.meta Normal file
View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 22513ccfdc5b6134f8d582d3d8869c5c
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

98
tool-refactor-plan.md Normal file
View File

@ -0,0 +1,98 @@
# Tool Refactor Plan
The purpose of this refactor is to minimize the amount of tools in use. Right now we have around 35 tool available to the LLM. Most research I've seen says the ideal amount of tools is 10-30 total. This includes when using multiple MCP servers. So to help the LLM make the best tool choice for the job, we're going to narrow down the number of tools we are using from 35 to 8-ish.
## Project Structure
We are building a Unity plugin under the folder and name UnityMCP. Within this folder are two projects. One is the MCP server under Python/ and the other is the Unity bridge and tool implementations under Editor/
## Steps
1. Remove all existing tools except for execute_command under editor_tools.py and for HandleExecuteCommand in EditorControlHandler.cs. This will be the only tool reused. All other files should be deleted. Rename editor_tools.py to execute_command.py. Rename EditorControllerHandler.cs to ExecuteCommand.cs.
2. Create Python/tools/manage_script.py and Editor/Tools/ManageScript.cs
- Implement all CRUD operations. Specify the action with an 'action' parameter.
- Add required parameter 'name'
- Add optional parameters 'path', 'contents', and 'script_type' (MonoBehaviour, ScriptableObject, Editor, etc.)
- Include validation for script syntax
- Add optional 'namespace' parameter for organizing scripts
3. Create Python/tools/manage_scene.py and Editor/Tools/ManageScene.cs
- Implement scene operations like loading, saving, creating new scenes.
- Add required parameter 'action' to specify operation (load, save, create, get_hierarchy, etc.)
- Add optional parameters 'name', 'path', and 'build_index'
- Handle scene hierarchy queries with 'get_hierarchy' action
4. Create Python/tools/manage_editor.py and Editor/Tools/ManageEditor.cs
- Control editor state (play mode, pause, stop). Query editor state
- Add required parameter 'action' to specify the operation ('play', 'pause', 'stop', 'get_state', etc.)
- Add optional parameters for specific settings ('resolution', 'quality', 'target_framerate')
- Include operations for managing editor windows and layouts
- Add optional 'wait_for_completion' boolean parameter for operations that take time
- Support querying current active tool and selection
5. Create Python/tools/manage_gameobject.py and Editor/Tools/ManageGameObject.cs
- Handle GameObject creation, modification, deletion
- Add required parameters 'action' ('create', 'modify', 'delete', 'find', 'get_components', etc.)
- Add required parameter 'target' for operations on existing objects (path, name, or ID)
- Add optional parameters 'parent', 'position', 'rotation', 'scale', 'components'
- Support component-specific operations with 'component_name' and 'component_properties'
- Add 'search_method' parameter ('by_name', 'by_tag', 'by_layer', 'by_component')
- Return standardized GameObject data structure with transforms and components
6. Create Python/tools/manage_asset.py and Editor/Tools/ManageAsset.cs
- Implement asset operations ('import', 'create', 'modify', 'delete', 'duplicate', 'search')
- Add required parameters 'action' and 'path'
- Add optional parameters 'asset_type', 'properties', 'destination' (for duplicate/move)
- Support asset-specific parameters based on asset_type
- Include preview generation with optional 'generate_preview' parameter
- Add pagination support with 'page_size' and 'page_number' for search results
- Support filtering assets by type, name pattern, or creation date
7. Create Python/tools/read_console.py and Editor/Tools/ReadConsole.cs
- Retrieve Unity console output (errors, warnings, logs)
- Add optional parameters 'type' (array of 'error', 'warning', 'log', 'all')
- Add optional 'count', 'filter_text', 'since_timestamp' parameters
- Support 'clear' action to clear console
- Add 'format' parameter ('plain', 'detailed', 'json') for different output formats
- Include stack trace toggle with 'include_stacktrace' boolean
8. Create Python/tools/execute_menu_item.py and Editor/Tools/ExecuteMenuItem.cs
- Execute Unity editor menu commands through script
- Add required parameter 'menu_path' for the menu item to execute
- Add optional 'parameters' object for menu items that accept parameters
- Support common menu operations with 'alias' parameter for simplified access
- Include validation to prevent execution of dangerous operations
- Add 'get_available_menus' action to list accessible menu items
- Support context-specific menu items with optional 'context' parameter
## Implementation Guidelines
1. Ensure consistent parameter naming and structure across all tools:
- Use 'action' parameter consistently for all operation-based tools
- Return standardized response format with 'success', 'data', and 'error' fields
- Use consistent error codes and messages
2. Implement proper error handling and validation:
- Validate parameters before execution
- Provide detailed error messages with suggestions for resolution
- Add timeout handling for long-running operations
- Include parameter type checking in both Python and C#
3. Use JSON for structured data exchange:
- Define clear schema for each tool's input and output
- Handle serialization edge cases (e.g., circular references)
- Optimize for large data transfers when necessary
4. Minimize dependencies between tools:
- Design each tool to function independently
- Use common utility functions for shared functionality
- Document any required dependencies clearly
5. Add performance considerations:
- Implement batching for multiple related operations
- Add optional asynchronous execution for long-running tasks
- Include optional progress reporting for time-consuming operations
6. Improve documentation:
- Add detailed XML/JSDoc comments for all public methods
- Include example usage for common scenarios
- Document potential side effects of operations

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c3ccc86868276bd4bba01c840f14e5bb
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: