using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;
#endif
namespace LocalMcp.UnityServer
{
/// <summary>
/// Provides scene management operations for Unity MCP Server.
/// Handles scene structure retrieval, initialization, and GameObject creation.
/// </summary>
public static class SceneSetupCommand
{
/// <summary>
/// Lists the current scene structure with all GameObjects and their components.
/// </summary>
/// <param name="paramsJson">JSON parameters containing optional maxDepth (default: 2)</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response containing scene structure</returns>
public static string ListScene(string paramsJson, object id)
{
try
{
int maxDepth = 2; // Default depth limit
if (!string.IsNullOrEmpty(paramsJson))
{
try
{
var parameters = JsonUtility.FromJson<SceneListParams>(paramsJson);
if (parameters.maxDepth > 0)
{
maxDepth = parameters.maxDepth;
}
}
catch
{
// If parsing fails, use default
}
}
Scene scene = SceneManager.GetActiveScene();
GameObject[] rootObjects = scene.GetRootGameObjects();
List<GameObjectData> rootObjectsData = new List<GameObjectData>();
foreach (GameObject rootObj in rootObjects)
{
rootObjectsData.Add(SerializeGameObject(rootObj, rootObj.name, 0, maxDepth));
}
var result = new SceneListResult
{
sceneName = scene.name,
rootObjects = rootObjectsData.ToArray(),
maxDepth = maxDepth
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] ListScene error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to list scene: {e.Message}", id);
}
}
/// <summary>
/// Overload for backward compatibility (no parameters)
/// </summary>
public static string ListScene(object id)
{
return ListScene(null, id);
}
/// <summary>
/// Sets up a scene with optional Camera and Light creation.
/// </summary>
/// <param name="paramsJson">JSON parameters containing sceneName, createCamera, createLight</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response with setup result</returns>
public static string SetupScene(string paramsJson, object id)
{
#if UNITY_EDITOR
try
{
var parameters = JsonUtility.FromJson<SceneSetupParams>(paramsJson);
Scene scene;
List<string> created = new List<string>();
// Get current active scene (don't create new scene to avoid GUI changes)
scene = SceneManager.GetActiveScene();
// If sceneName is specified but doesn't match, return error
if (!string.IsNullOrEmpty(parameters.sceneName) && scene.name != parameters.sceneName)
{
return JsonRpcResponseHelper.ErrorMessage(
$"Scene name mismatch: Active scene is '{scene.name}', but requested '{parameters.sceneName}'. " +
"Please open the desired scene first, or omit sceneName to use the active scene.",
id);
}
// Create Camera if requested
if (parameters.createCamera)
{
GameObject camera = new GameObject("Main Camera");
Camera cam = camera.AddComponent<Camera>();
camera.AddComponent<AudioListener>();
camera.tag = "MainCamera";
camera.transform.position = new Vector3(0, 1, -10);
created.Add("Main Camera");
}
// Create Light if requested
if (parameters.createLight)
{
GameObject light = new GameObject("Directional Light");
Light lightComp = light.AddComponent<Light>();
lightComp.type = LightType.Directional;
light.transform.rotation = Quaternion.Euler(50, -30, 0);
created.Add("Directional Light");
}
var result = new SceneSetupResult
{
status = "success",
message = $"Scene '{scene.name}' setup completed",
created = created.ToArray()
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] SetupScene error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to setup scene: {e.Message}", id);
}
#else
return JsonRpcResponseHelper.ErrorMessage("SetupScene requires Unity Editor", id);
#endif
}
/// <summary>
/// Creates a new GameObject in the scene.
/// </summary>
/// <param name="paramsJson">JSON parameters containing name, parent, primitiveType, position</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response with creation result</returns>
public static string CreateGameObject(string paramsJson, object id)
{
try
{
var parameters = JsonUtility.FromJson<CreateGameObjectParams>(paramsJson);
GameObject go;
// Create primitive or empty GameObject
if (!string.IsNullOrEmpty(parameters.primitiveType))
{
PrimitiveType primitiveTypeEnum;
if (Enum.TryParse(parameters.primitiveType, true, out primitiveTypeEnum))
{
go = GameObject.CreatePrimitive(primitiveTypeEnum);
}
else
{
return JsonRpcResponseHelper.InvalidParams(
$"Invalid primitiveType: {parameters.primitiveType}. Valid types: Cube, Sphere, Capsule, Cylinder, Plane, Quad",
id
);
}
}
else
{
go = new GameObject();
}
// Set name
go.name = string.IsNullOrEmpty(parameters.name) ? "GameObject" : parameters.name;
// Set position
if (parameters.position != null)
{
go.transform.position = new Vector3(
parameters.position.x,
parameters.position.y,
parameters.position.z
);
}
// Set parent
if (!string.IsNullOrEmpty(parameters.parent))
{
GameObject parentObj = GameObject.Find(parameters.parent);
if (parentObj != null)
{
go.transform.SetParent(parentObj.transform);
}
else
{
Debug.LogWarning($"[MCP] Parent GameObject '{parameters.parent}' not found, creating at root");
}
}
// Build full path
string path = GetGameObjectPath(go);
var result = new CreateGameObjectResult
{
status = "success",
path = path,
instanceId = go.GetInstanceID()
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] CreateGameObject error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to create GameObject: {e.Message}", id);
}
}
/// <summary>
/// Deletes a GameObject from the scene.
/// </summary>
/// <param name="paramsJson">JSON parameters containing gameObjectPath</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response</returns>
public static string DeleteGameObject(string paramsJson, object id)
{
try
{
var parameters = JsonUtility.FromJson<GameObjectPathParams>(paramsJson);
if (string.IsNullOrEmpty(parameters.gameObjectPath))
{
return JsonRpcResponseHelper.InvalidParams("gameObjectPath is required", id);
}
GameObject go = GameObject.Find(parameters.gameObjectPath);
if (go == null)
{
return JsonRpcResponseHelper.ErrorMessage($"GameObject '{parameters.gameObjectPath}' not found", id);
}
string name = go.name;
UnityEngine.Object.DestroyImmediate(go);
var result = new GameObjectOperationResult
{
success = true,
message = $"GameObject '{name}' deleted successfully"
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] DeleteGameObject error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to delete GameObject: {e.Message}", id);
}
}
/// <summary>
/// Sets the active state of a GameObject.
/// </summary>
/// <param name="paramsJson">JSON parameters containing gameObjectPath and active state</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response</returns>
public static string SetGameObjectActive(string paramsJson, object id)
{
try
{
var parameters = JsonUtility.FromJson<SetGameObjectActiveParams>(paramsJson);
if (string.IsNullOrEmpty(parameters.gameObjectPath))
{
return JsonRpcResponseHelper.InvalidParams("gameObjectPath is required", id);
}
GameObject go = GameObject.Find(parameters.gameObjectPath);
if (go == null)
{
return JsonRpcResponseHelper.ErrorMessage($"GameObject '{parameters.gameObjectPath}' not found", id);
}
go.SetActive(parameters.active);
var result = new GameObjectOperationResult
{
success = true,
message = $"GameObject '{go.name}' active state set to {parameters.active}"
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] SetGameObjectActive error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to set GameObject active state: {e.Message}", id);
}
}
/// <summary>
/// Sets the parent of a GameObject.
/// </summary>
/// <param name="paramsJson">JSON parameters containing gameObjectPath and parentPath</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response</returns>
public static string SetGameObjectParent(string paramsJson, object id)
{
try
{
var parameters = JsonUtility.FromJson<SetGameObjectParentParams>(paramsJson);
if (string.IsNullOrEmpty(parameters.gameObjectPath))
{
return JsonRpcResponseHelper.InvalidParams("gameObjectPath is required", id);
}
GameObject go = GameObject.Find(parameters.gameObjectPath);
if (go == null)
{
return JsonRpcResponseHelper.ErrorMessage($"GameObject '{parameters.gameObjectPath}' not found", id);
}
Transform newParent = null;
if (!string.IsNullOrEmpty(parameters.parentPath))
{
GameObject parentGo = GameObject.Find(parameters.parentPath);
if (parentGo == null)
{
return JsonRpcResponseHelper.ErrorMessage($"Parent GameObject '{parameters.parentPath}' not found", id);
}
newParent = parentGo.transform;
}
go.transform.SetParent(newParent, parameters.worldPositionStays);
var result = new GameObjectOperationResult
{
success = true,
message = $"GameObject '{go.name}' parent set successfully"
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] SetGameObjectParent error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to set GameObject parent: {e.Message}", id);
}
}
/// <summary>
/// Sets the world position of a GameObject.
/// </summary>
/// <param name="paramsJson">JSON parameters containing gameObjectPath and position</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response</returns>
public static string SetTransformPosition(string paramsJson, object id)
{
try
{
var parameters = JsonUtility.FromJson<SetTransformPositionParams>(paramsJson);
if (string.IsNullOrEmpty(parameters.gameObjectPath))
{
return JsonRpcResponseHelper.InvalidParams("gameObjectPath is required", id);
}
GameObject go = GameObject.Find(parameters.gameObjectPath);
if (go == null)
{
return JsonRpcResponseHelper.ErrorMessage($"GameObject '{parameters.gameObjectPath}' not found", id);
}
if (parameters.local)
{
go.transform.localPosition = new Vector3(parameters.x, parameters.y, parameters.z);
}
else
{
go.transform.position = new Vector3(parameters.x, parameters.y, parameters.z);
}
var result = new GameObjectOperationResult
{
success = true,
message = $"GameObject '{go.name}' position set to ({parameters.x}, {parameters.y}, {parameters.z})"
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] SetTransformPosition error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to set transform position: {e.Message}", id);
}
}
/// <summary>
/// Sets the rotation of a GameObject.
/// </summary>
/// <param name="paramsJson">JSON parameters containing gameObjectPath and rotation</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response</returns>
public static string SetTransformRotation(string paramsJson, object id)
{
try
{
var parameters = JsonUtility.FromJson<SetTransformRotationParams>(paramsJson);
if (string.IsNullOrEmpty(parameters.gameObjectPath))
{
return JsonRpcResponseHelper.InvalidParams("gameObjectPath is required", id);
}
GameObject go = GameObject.Find(parameters.gameObjectPath);
if (go == null)
{
return JsonRpcResponseHelper.ErrorMessage($"GameObject '{parameters.gameObjectPath}' not found", id);
}
Quaternion rotation = Quaternion.Euler(parameters.x, parameters.y, parameters.z);
if (parameters.local)
{
go.transform.localRotation = rotation;
}
else
{
go.transform.rotation = rotation;
}
var result = new GameObjectOperationResult
{
success = true,
message = $"GameObject '{go.name}' rotation set to ({parameters.x}, {parameters.y}, {parameters.z})"
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] SetTransformRotation error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to set transform rotation: {e.Message}", id);
}
}
/// <summary>
/// Sets the scale of a GameObject.
/// </summary>
/// <param name="paramsJson">JSON parameters containing gameObjectPath and scale</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response</returns>
public static string SetTransformScale(string paramsJson, object id)
{
try
{
var parameters = JsonUtility.FromJson<SetTransformScaleParams>(paramsJson);
if (string.IsNullOrEmpty(parameters.gameObjectPath))
{
return JsonRpcResponseHelper.InvalidParams("gameObjectPath is required", id);
}
GameObject go = GameObject.Find(parameters.gameObjectPath);
if (go == null)
{
return JsonRpcResponseHelper.ErrorMessage($"GameObject '{parameters.gameObjectPath}' not found", id);
}
go.transform.localScale = new Vector3(parameters.x, parameters.y, parameters.z);
var result = new GameObjectOperationResult
{
success = true,
message = $"GameObject '{go.name}' scale set to ({parameters.x}, {parameters.y}, {parameters.z})"
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] SetTransformScale error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to set transform scale: {e.Message}", id);
}
}
/// <summary>
/// Destroys a GameObject from the scene.
/// API: unity.gameobject.destroy
/// </summary>
/// <param name="paramsJson">JSON parameters containing gameObjectPath or instanceId</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response</returns>
public static string DestroyGameObject(string paramsJson, object id)
{
#if UNITY_EDITOR
try
{
var parameters = JsonUtility.FromJson<DestroyGameObjectParams>(paramsJson);
GameObject go = null;
// Try to find by instanceId first
if (parameters.instanceId != 0)
{
go = EditorUtility.InstanceIDToObject(parameters.instanceId) as GameObject;
}
// If not found by instanceId, try by path
if (go == null && !string.IsNullOrEmpty(parameters.gameObjectPath))
{
go = GameObject.Find(parameters.gameObjectPath);
}
if (go == null)
{
string identifier = parameters.instanceId != 0
? $"instanceId:{parameters.instanceId}"
: parameters.gameObjectPath;
return JsonRpcResponseHelper.ErrorMessage($"GameObject not found: {identifier}", id);
}
string destroyedName = go.name;
string destroyedPath = GetGameObjectPath(go);
// Register undo before destroying
Undo.DestroyObjectImmediate(go);
var result = new DestroyGameObjectResult
{
success = true,
message = $"GameObject '{destroyedName}' destroyed",
destroyedPath = destroyedPath,
destroyedName = destroyedName
};
Debug.Log($"[MCP] GameObject destroyed: {destroyedPath}");
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] DestroyGameObject error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to destroy GameObject: {e.Message}", id);
}
#else
return JsonRpcResponseHelper.ErrorMessage("DestroyGameObject requires Unity Editor", id);
#endif
}
/// <summary>
/// Recursively serializes a GameObject and its children.
/// </summary>
/// <param name="go">GameObject to serialize</param>
/// <param name="path">Hierarchical path</param>
/// <param name="currentDepth">Current depth level</param>
/// <param name="maxDepth">Maximum depth to serialize</param>
private static GameObjectData SerializeGameObject(GameObject go, string path, int currentDepth, int maxDepth)
{
var data = new GameObjectData
{
name = go.name,
path = path,
active = go.activeInHierarchy,
transform = new TransformData
{
position = new Vector3Data
{
x = go.transform.position.x,
y = go.transform.position.y,
z = go.transform.position.z
},
rotation = new QuaternionData
{
x = go.transform.rotation.x,
y = go.transform.rotation.y,
z = go.transform.rotation.z,
w = go.transform.rotation.w
},
scale = new Vector3Data
{
x = go.transform.localScale.x,
y = go.transform.localScale.y,
z = go.transform.localScale.z
}
}
};
// Get component names
Component[] components = go.GetComponents<Component>();
List<string> componentNames = new List<string>();
foreach (Component comp in components)
{
if (comp != null)
{
componentNames.Add(comp.GetType().Name);
}
}
data.components = componentNames.ToArray();
// Serialize children recursively only if within depth limit
List<GameObjectData> children = new List<GameObjectData>();
if (currentDepth < maxDepth)
{
for (int i = 0; i < go.transform.childCount; i++)
{
GameObject child = go.transform.GetChild(i).gameObject;
string childPath = path + "/" + child.name;
children.Add(SerializeGameObject(child, childPath, currentDepth + 1, maxDepth));
}
}
data.children = children.ToArray();
data.childCount = go.transform.childCount; // Store total child count even if not serialized
return data;
}
/// <summary>
/// Gets the full hierarchical path of a GameObject.
/// </summary>
private static string GetGameObjectPath(GameObject go)
{
string path = go.name;
Transform current = go.transform.parent;
while (current != null)
{
path = current.name + "/" + path;
current = current.parent;
}
return path;
}
/// <summary>
/// Opens an existing scene file in Unity Editor.
/// </summary>
/// <param name="paramsJson">JSON parameters containing scenePath and optional mode</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response</returns>
public static string OpenScene(string paramsJson, object id)
{
#if UNITY_EDITOR
try
{
var parameters = JsonUtility.FromJson<OpenSceneParams>(paramsJson);
if (string.IsNullOrEmpty(parameters.scenePath))
{
return JsonRpcResponseHelper.InvalidParams("scenePath is required", id);
}
// Check if scene file exists
if (!File.Exists(parameters.scenePath))
{
return JsonRpcResponseHelper.ErrorMessage($"Scene file not found: {parameters.scenePath}", id);
}
// Determine open mode
OpenSceneMode mode = OpenSceneMode.Single;
if (!string.IsNullOrEmpty(parameters.mode))
{
switch (parameters.mode.ToLower())
{
case "additive":
mode = OpenSceneMode.Additive;
break;
case "single":
default:
mode = OpenSceneMode.Single;
break;
}
}
// Save current scene(s) if modified to avoid save dialog
if (mode == OpenSceneMode.Single)
{
// Save all modified scenes before opening new scene
for (int i = 0; i < SceneManager.sceneCount; i++)
{
Scene currentScene = SceneManager.GetSceneAt(i);
if (currentScene.isDirty)
{
// If scene has never been saved (path is empty), skip saving
if (!string.IsNullOrEmpty(currentScene.path))
{
EditorSceneManager.SaveScene(currentScene);
}
}
}
}
// Open the scene
Scene openedScene = EditorSceneManager.OpenScene(parameters.scenePath, mode);
if (!openedScene.IsValid())
{
return JsonRpcResponseHelper.ErrorMessage($"Failed to open scene: {parameters.scenePath}", id);
}
var result = new OpenSceneResult
{
success = true,
scenePath = openedScene.path,
sceneName = openedScene.name,
isLoaded = openedScene.isLoaded,
mode = mode.ToString()
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] OpenScene error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to open scene: {e.Message}", id);
}
#else
return JsonRpcResponseHelper.ErrorMessage("OpenScene requires Unity Editor", id);
#endif
}
/// <summary>
/// Saves the current scene or a specific scene by path.
/// </summary>
/// <param name="paramsJson">JSON parameters containing optional scenePath</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response</returns>
public static string SaveScene(string paramsJson, object id)
{
#if UNITY_EDITOR
try
{
var parameters = JsonUtility.FromJson<SaveSceneParams>(paramsJson);
Scene scene;
if (!string.IsNullOrEmpty(parameters.scenePath))
{
scene = SceneManager.GetSceneByPath(parameters.scenePath);
if (!scene.IsValid())
{
return JsonRpcResponseHelper.ErrorMessage($"Scene '{parameters.scenePath}' not found", id);
}
}
else
{
scene = SceneManager.GetActiveScene();
}
bool success = EditorSceneManager.SaveScene(scene);
if (!success)
{
return JsonRpcResponseHelper.ErrorMessage("Failed to save scene", id);
}
var result = new SaveSceneResult
{
success = true,
scenePath = scene.path,
sceneName = scene.name
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] SaveScene error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to save scene: {e.Message}", id);
}
#else
return JsonRpcResponseHelper.ErrorMessage("SaveScene requires Unity Editor", id);
#endif
}
/// <summary>
/// Saves all modified assets in the project to disk.
/// Equivalent to File > Save Project (Ctrl+S on assets).
/// This saves .controller, .overrideController, .asset files etc.
/// </summary>
/// <param name="paramsJson">JSON parameters (none required)</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response</returns>
public static string SaveProject(string paramsJson, object id)
{
#if UNITY_EDITOR
try
{
// Save all modified assets to disk
AssetDatabase.SaveAssets();
// Refresh the AssetDatabase to ensure changes are recognized
AssetDatabase.Refresh();
var result = new SaveProjectResult
{
success = true,
message = "All modified assets have been saved to disk"
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] SaveProject error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to save project: {e.Message}", id);
}
#else
return JsonRpcResponseHelper.ErrorMessage("SaveProject requires Unity Editor", id);
#endif
}
/// <summary>
/// Executes a Unity Editor menu item.
/// </summary>
/// <param name="paramsJson">JSON parameters containing menuPath</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response</returns>
public static string ExecuteMenuItem(string paramsJson, object id)
{
#if UNITY_EDITOR
try
{
var parameters = JsonUtility.FromJson<ExecuteMenuItemParams>(paramsJson);
if (string.IsNullOrEmpty(parameters.menuPath))
{
return JsonRpcResponseHelper.InvalidParams("menuPath is required", id);
}
bool success = EditorApplication.ExecuteMenuItem(parameters.menuPath);
if (!success)
{
return JsonRpcResponseHelper.ErrorMessage($"MenuItem '{parameters.menuPath}' not found or failed to execute", id);
}
var result = new ExecuteMenuItemResult
{
success = true,
menuPath = parameters.menuPath
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] ExecuteMenuItem error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to execute MenuItem: {e.Message}", id);
}
#else
return JsonRpcResponseHelper.ErrorMessage("ExecuteMenuItem requires Unity Editor", id);
#endif
}
/// <summary>
/// Finds an object of a specific type in the scene (equivalent to Object.FindObjectOfType).
/// </summary>
/// <param name="paramsJson">JSON parameters containing componentType and optional includeInactive</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response containing GameObject path and component info</returns>
public static string FindObjectOfType(string paramsJson, object id)
{
try
{
var parameters = JsonUtility.FromJson<FindObjectOfTypeParams>(paramsJson);
if (string.IsNullOrEmpty(parameters.componentType))
{
return JsonRpcResponseHelper.InvalidParams("componentType is required", id);
}
// Find the component type across all loaded assemblies
Type componentType = ComponentReflection.FindComponentType(parameters.componentType);
if (componentType == null)
{
return JsonRpcResponseHelper.ErrorMessage($"Component type '{parameters.componentType}' not found", id);
}
// Use Resources.FindObjectsOfTypeAll for inactive objects, or Object.FindObjectOfType for active only
UnityEngine.Object foundObject;
if (parameters.includeInactive)
{
// FindObjectsOfTypeAll includes inactive objects
var allObjects = Resources.FindObjectsOfTypeAll(componentType);
// Filter out objects that are part of assets (not in scene)
foundObject = null;
foreach (var obj in allObjects)
{
if (obj is Component comp && comp.gameObject.scene.IsValid())
{
foundObject = obj;
break;
}
}
}
else
{
// Find only active objects in scene
foundObject = UnityEngine.Object.FindObjectOfType(componentType);
}
if (foundObject == null)
{
var result = new FindObjectOfTypeResult
{
found = false,
componentType = parameters.componentType
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
// Get GameObject and path
GameObject go = null;
if (foundObject is Component comp2)
{
go = comp2.gameObject;
}
else if (foundObject is GameObject gameObj)
{
go = gameObj;
}
if (go == null)
{
return JsonRpcResponseHelper.ErrorMessage("Found object is not a Component or GameObject", id);
}
string path = GetGameObjectPath(go);
var successResult = new FindObjectOfTypeResult
{
found = true,
gameObjectPath = path,
gameObjectName = go.name,
componentType = componentType.FullName,
instanceId = go.GetInstanceID(),
isActive = go.activeInHierarchy
};
var successResponse = JsonRpcResponse.Success(successResult, id);
return successResponse.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] FindObjectOfType error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to find object of type: {e.Message}", id);
}
}
/// <summary>
/// Outputs a debug log message to Unity Console.
/// </summary>
/// <param name="paramsJson">JSON parameters containing message and logType</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response</returns>
public static string DebugLog(string paramsJson, object id)
{
try
{
var parameters = JsonUtility.FromJson<DebugLogParams>(paramsJson);
if (string.IsNullOrEmpty(parameters.message))
{
return JsonRpcResponseHelper.InvalidParams("message is required", id);
}
// Output log based on type
switch (parameters.logType?.ToLower())
{
case "warning":
Debug.LogWarning($"[MCP Client] {parameters.message}");
break;
case "error":
Debug.LogError($"[MCP Client] {parameters.message}");
break;
default:
Debug.Log($"[MCP Client] {parameters.message}");
break;
}
var result = new DebugLogResult
{
success = true,
message = parameters.message,
logType = parameters.logType ?? "log"
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] DebugLog error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to output debug log: {e.Message}", id);
}
}
/// <summary>
/// Sets up complete scene architecture for Avatar system.
/// Creates GameObjects, components, ScriptableObjects, and configures all references.
/// </summary>
/// <param name="paramsJson">JSON parameters containing optional configuration</param>
/// <param name="id">JSON-RPC request ID</param>
/// <returns>JSON-RPC response</returns>
public static string SetupSceneArchitecture(string paramsJson, object id)
{
#if UNITY_EDITOR
try
{
var parameters = JsonUtility.FromJson<SetupSceneArchitectureParams>(paramsJson);
List<string> steps = new List<string>();
// 1. Create AvatarPlacementSettings ScriptableObject
steps.Add("Creating AvatarPlacementSettings ScriptableObject...");
string settingsPath = "Assets/Settings/DefaultAvatarPlacementSettings.asset";
var settingsResult = UnityAssetCreator.CreateScriptableObject(
JsonUtility.ToJson(new UnityAssetCreator.CreateScriptableObjectParams
{
path = settingsPath,
typeName = "AICam.VRM.AvatarPlacementSettings"
}),
null
);
// 2. Create GameObjects
steps.Add("Creating AvatarInstanceManager GameObject...");
CreateGameObject(JsonUtility.ToJson(new CreateGameObjectParams { name = "AvatarInstanceManager" }), null);
steps.Add("Creating AvatarFollowController GameObject...");
CreateGameObject(JsonUtility.ToJson(new CreateGameObjectParams { name = "AvatarFollowController" }), null);
steps.Add("Creating AvatarSwipeController GameObject...");
CreateGameObject(JsonUtility.ToJson(new CreateGameObjectParams { name = "AvatarSwipeController" }), null);
// 3. Add Components
steps.Add("Adding AvatarInstanceManager component...");
ComponentReflection.AddComponent(
JsonUtility.ToJson(new ComponentReflection.ComponentAddParams
{
gameObjectPath = "AvatarInstanceManager",
componentType = "AICam.VRM.AvatarInstanceManager"
}),
null
);
steps.Add("Adding AvatarFollowController component...");
ComponentReflection.AddComponent(
JsonUtility.ToJson(new ComponentReflection.ComponentAddParams
{
gameObjectPath = "AvatarFollowController",
componentType = "AICam.VRM.AvatarFollowController"
}),
null
);
steps.Add("Adding AvatarSwipeController component...");
ComponentReflection.AddComponent(
JsonUtility.ToJson(new ComponentReflection.ComponentAddParams
{
gameObjectPath = "AvatarSwipeController",
componentType = "AICam.VRM.AvatarSwipeController"
}),
null
);
// 4. Setup AvatarInstanceManager references
steps.Add("Setting up AvatarInstanceManager references...");
// Set placementSettings
ComponentReflection.SetReference(
JsonUtility.ToJson(new ComponentReflection.ComponentSetReferenceParams
{
gameObjectPath = "AvatarInstanceManager",
componentType = "AICam.VRM.AvatarInstanceManager",
fieldName = "placementSettings",
referenceType = "asset",
referencePath = settingsPath
}),
null
);
// Find and set AR Camera
var arCameraPath = "XR Origin/Camera Offset/AR Camera"; // Standard AR Foundation path
ComponentReflection.SetReference(
JsonUtility.ToJson(new ComponentReflection.ComponentSetReferenceParams
{
gameObjectPath = "AvatarInstanceManager",
componentType = "AICam.VRM.AvatarInstanceManager",
fieldName = "arCamera",
referenceType = "component",
referenceGameObjectPath = arCameraPath,
referenceComponentType = "UnityEngine.Camera"
}),
null
);
// Find ARAnchorManager using FindObjectOfType
steps.Add("Finding ARAnchorManager in scene...");
string findResult = FindObjectOfType(
JsonUtility.ToJson(new FindObjectOfTypeParams
{
componentType = "UnityEngine.XR.ARFoundation.ARAnchorManager",
includeInactive = false
}),
null
);
var findResponse = JsonUtility.FromJson<JsonRpcResponse>(findResult);
if (findResponse.result != null)
{
var findData = JsonUtility.FromJson<FindObjectOfTypeResult>(JsonUtility.ToJson(findResponse.result));
if (findData.found)
{
steps.Add($"Found ARAnchorManager at: {findData.gameObjectPath}");
ComponentReflection.SetReference(
JsonUtility.ToJson(new ComponentReflection.ComponentSetReferenceParams
{
gameObjectPath = "AvatarInstanceManager",
componentType = "AICam.VRM.AvatarInstanceManager",
fieldName = "anchorManager",
referenceType = "component",
referenceGameObjectPath = findData.gameObjectPath,
referenceComponentType = "UnityEngine.XR.ARFoundation.ARAnchorManager"
}),
null
);
}
else
{
steps.Add("Warning: ARAnchorManager not found in scene");
}
}
// 5. Setup AvatarFollowController
steps.Add("Setting up AvatarFollowController...");
// Set arCamera reference
ComponentReflection.SetReference(
JsonUtility.ToJson(new ComponentReflection.ComponentSetReferenceParams
{
gameObjectPath = "AvatarFollowController",
componentType = "AICam.VRM.AvatarFollowController",
fieldName = "arCamera",
referenceType = "component",
referenceGameObjectPath = arCameraPath,
referenceComponentType = "UnityEngine.Camera"
}),
null
);
// Set initial parameters
ComponentReflection.SetField(
JsonUtility.ToJson(new ComponentReflection.ComponentSetParams
{
gameObjectPath = "AvatarFollowController",
componentType = "AICam.VRM.AvatarFollowController",
fieldName = "followDistance",
value = 1.5f
}),
null
);
// 6. Setup AvatarSwipeController
steps.Add("Setting up AvatarSwipeController...");
// Set followController reference
ComponentReflection.SetReference(
JsonUtility.ToJson(new ComponentReflection.ComponentSetReferenceParams
{
gameObjectPath = "AvatarSwipeController",
componentType = "AICam.VRM.AvatarSwipeController",
fieldName = "followController",
referenceType = "component",
referenceGameObjectPath = "AvatarFollowController",
referenceComponentType = "AICam.VRM.AvatarFollowController"
}),
null
);
// 7. Find and setup PlaceAvatarOnPlaneOnly if exists
steps.Add("Looking for PlaceAvatarOnPlaneOnly component...");
string placeAvatarFind = FindObjectOfType(
JsonUtility.ToJson(new FindObjectOfTypeParams
{
componentType = "AICam.VRM.PlaceAvatarOnPlaneOnly",
includeInactive = false
}),
null
);
var placeAvatarResponse = JsonUtility.FromJson<JsonRpcResponse>(placeAvatarFind);
if (placeAvatarResponse.result != null)
{
var placeAvatarData = JsonUtility.FromJson<FindObjectOfTypeResult>(JsonUtility.ToJson(placeAvatarResponse.result));
if (placeAvatarData.found)
{
steps.Add($"Found PlaceAvatarOnPlaneOnly at: {placeAvatarData.gameObjectPath}");
// Set avatarInstanceManager
ComponentReflection.SetReference(
JsonUtility.ToJson(new ComponentReflection.ComponentSetReferenceParams
{
gameObjectPath = placeAvatarData.gameObjectPath,
componentType = "AICam.VRM.PlaceAvatarOnPlaneOnly",
fieldName = "avatarInstanceManager",
referenceType = "component",
referenceGameObjectPath = "AvatarInstanceManager",
referenceComponentType = "AICam.VRM.AvatarInstanceManager"
}),
null
);
// Set avatarFollowController
ComponentReflection.SetReference(
JsonUtility.ToJson(new ComponentReflection.ComponentSetReferenceParams
{
gameObjectPath = placeAvatarData.gameObjectPath,
componentType = "AICam.VRM.PlaceAvatarOnPlaneOnly",
fieldName = "avatarFollowController",
referenceType = "component",
referenceGameObjectPath = "AvatarFollowController",
referenceComponentType = "AICam.VRM.AvatarFollowController"
}),
null
);
// Set avatarSwipeController
ComponentReflection.SetReference(
JsonUtility.ToJson(new ComponentReflection.ComponentSetReferenceParams
{
gameObjectPath = placeAvatarData.gameObjectPath,
componentType = "AICam.VRM.PlaceAvatarOnPlaneOnly",
fieldName = "avatarSwipeController",
referenceType = "component",
referenceGameObjectPath = "AvatarSwipeController",
referenceComponentType = "AICam.VRM.AvatarSwipeController"
}),
null
);
}
}
// 8. Find and setup CameraCaptureController if exists
steps.Add("Looking for CameraCaptureController component...");
string cameraCaptureFind = FindObjectOfType(
JsonUtility.ToJson(new FindObjectOfTypeParams
{
componentType = "AICam.VRM.CameraCaptureController",
includeInactive = false
}),
null
);
var cameraCaptureResponse = JsonUtility.FromJson<JsonRpcResponse>(cameraCaptureFind);
if (cameraCaptureResponse.result != null)
{
var cameraCaptureData = JsonUtility.FromJson<FindObjectOfTypeResult>(JsonUtility.ToJson(cameraCaptureResponse.result));
if (cameraCaptureData.found)
{
steps.Add($"Found CameraCaptureController at: {cameraCaptureData.gameObjectPath}");
// Set avatarInstanceManager
ComponentReflection.SetReference(
JsonUtility.ToJson(new ComponentReflection.ComponentSetReferenceParams
{
gameObjectPath = cameraCaptureData.gameObjectPath,
componentType = "AICam.VRM.CameraCaptureController",
fieldName = "avatarInstanceManager",
referenceType = "component",
referenceGameObjectPath = "AvatarInstanceManager",
referenceComponentType = "AICam.VRM.AvatarInstanceManager"
}),
null
);
}
}
// 9. Mark scene as dirty
steps.Add("Marking scene as dirty...");
Scene scene = SceneManager.GetActiveScene();
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(scene);
steps.Add("Scene architecture setup complete!");
var result = new SetupSceneArchitectureResult
{
success = true,
steps = steps.ToArray(),
sceneName = scene.name
};
var response = JsonRpcResponse.Success(result, id);
return response.ToJson();
}
catch (Exception e)
{
Debug.LogError($"[MCP] SetupSceneArchitecture error: {e.Message}\n{e.StackTrace}");
return JsonRpcResponseHelper.ErrorMessage($"Failed to setup scene architecture: {e.Message}", id);
}
#else
return JsonRpcResponseHelper.ErrorMessage("SetupSceneArchitecture requires Unity Editor", id);
#endif
}
}
#region Data Structures
/// <summary>
/// Parameters for scene setup.
/// </summary>
[Serializable]
public class SceneSetupParams
{
public string sceneName;
public bool createCamera = true;
public bool createLight = true;
}
/// <summary>
/// Parameters for GameObject creation.
/// </summary>
[Serializable]
public class CreateGameObjectParams
{
public string name;
public string parent;
public string primitiveType;
public Vector3Data position;
}
/// <summary>
/// Parameters for listing scene.
/// </summary>
[Serializable]
public class SceneListParams
{
public int maxDepth = 2; // Maximum depth to serialize (default: 2)
}
/// <summary>
/// Result of scene listing.
/// </summary>
[Serializable]
public class SceneListResult
{
public string sceneName;
public GameObjectData[] rootObjects;
public int maxDepth; // The depth limit used
}
/// <summary>
/// Result of scene setup.
/// </summary>
[Serializable]
public class SceneSetupResult
{
public string status;
public string message;
public string[] created;
}
/// <summary>
/// Result of GameObject creation.
/// </summary>
[Serializable]
public class CreateGameObjectResult
{
public string status;
public string path;
public int instanceId;
}
/// <summary>
/// Serializable GameObject data.
/// </summary>
[Serializable]
public class GameObjectData
{
public string name;
public string path;
public bool active;
public TransformData transform;
public string[] components;
public GameObjectData[] children;
public int childCount; // Total number of children (even if not all serialized)
}
/// <summary>
/// Serializable Transform data.
/// </summary>
[Serializable]
public class TransformData
{
public Vector3Data position;
public QuaternionData rotation;
public Vector3Data scale;
}
/// <summary>
/// Serializable Vector2 data.
/// </summary>
[Serializable]
public class Vector2Data
{
public float x;
public float y;
}
/// <summary>
/// Serializable Vector3 data.
/// </summary>
[Serializable]
public class Vector3Data
{
public float x;
public float y;
public float z;
}
/// <summary>
/// Serializable Vector4 data.
/// </summary>
[Serializable]
public class Vector4Data
{
public float x;
public float y;
public float z;
public float w;
}
/// <summary>
/// Serializable Quaternion data.
/// </summary>
[Serializable]
public class QuaternionData
{
public float x;
public float y;
public float z;
public float w;
}
/// <summary>
/// Parameters for debug log output.
/// </summary>
[Serializable]
public class DebugLogParams
{
public string message;
public string logType; // "log", "warning", "error"
}
/// <summary>
/// Result of debug log output.
/// </summary>
[Serializable]
public class DebugLogResult
{
public bool success;
public string message;
public string logType;
}
/// <summary>
/// Parameters for opening a scene.
/// </summary>
[Serializable]
public class OpenSceneParams
{
public string scenePath; // Required: path to the scene file
public string mode; // Optional: "single" (default) or "additive"
}
/// <summary>
/// Result of opening a scene.
/// </summary>
[Serializable]
public class OpenSceneResult
{
public bool success;
public string scenePath;
public string sceneName;
public bool isLoaded;
public string mode;
}
/// <summary>
/// Parameters for saving a scene.
/// </summary>
[Serializable]
public class SaveSceneParams
{
public string scenePath; // Optional, null = save active scene
}
/// <summary>
/// Result of saving a scene.
/// </summary>
[Serializable]
public class SaveSceneResult
{
public bool success;
public string scenePath;
public string sceneName;
}
/// <summary>
/// Result of saving the project (all modified assets).
/// </summary>
[Serializable]
public class SaveProjectResult
{
public bool success;
public string message;
}
/// <summary>
/// Parameters for executing a menu item.
/// </summary>
[Serializable]
public class ExecuteMenuItemParams
{
public string menuPath;
}
/// <summary>
/// Result of executing a menu item.
/// </summary>
[Serializable]
public class ExecuteMenuItemResult
{
public bool success;
public string menuPath;
}
/// <summary>
/// Parameters for finding an object of a specific type.
/// </summary>
[Serializable]
public class FindObjectOfTypeParams
{
public string componentType;
public bool includeInactive = false; // Optional, default: false (active only)
}
/// <summary>
/// Result of finding an object of a specific type.
/// </summary>
[Serializable]
public class FindObjectOfTypeResult
{
public bool found;
public string gameObjectPath;
public string gameObjectName;
public string componentType;
public int instanceId;
public bool isActive;
}
/// <summary>
/// Parameters for scene architecture setup.
/// </summary>
[Serializable]
public class SetupSceneArchitectureParams
{
// Currently no parameters needed, but structure for future expansion
}
/// <summary>
/// Result of scene architecture setup.
/// </summary>
[Serializable]
public class SetupSceneArchitectureResult
{
public bool success;
public string[] steps;
public string sceneName;
}
/// <summary>
/// Parameters for GameObject path reference.
/// </summary>
[Serializable]
public class GameObjectPathParams
{
public string gameObjectPath;
}
/// <summary>
/// Parameters for setting GameObject active state.
/// </summary>
[Serializable]
public class SetGameObjectActiveParams
{
public string gameObjectPath;
public bool active;
}
/// <summary>
/// Parameters for setting GameObject parent.
/// </summary>
[Serializable]
public class SetGameObjectParentParams
{
public string gameObjectPath;
public string parentPath; // null or empty to set no parent (root)
public bool worldPositionStays = true;
}
/// <summary>
/// Parameters for setting transform position.
/// </summary>
[Serializable]
public class SetTransformPositionParams
{
public string gameObjectPath;
public float x;
public float y;
public float z;
public bool local = false;
}
/// <summary>
/// Parameters for setting transform rotation (Euler angles).
/// </summary>
[Serializable]
public class SetTransformRotationParams
{
public string gameObjectPath;
public float x;
public float y;
public float z;
public bool local = false;
}
/// <summary>
/// Parameters for setting transform scale.
/// </summary>
[Serializable]
public class SetTransformScaleParams
{
public string gameObjectPath;
public float x;
public float y;
public float z;
}
/// <summary>
/// Result of GameObject operation.
/// </summary>
[Serializable]
public class GameObjectOperationResult
{
public bool success;
public string message;
}
/// <summary>
/// Parameters for destroying a GameObject.
/// </summary>
[Serializable]
public class DestroyGameObjectParams
{
public string gameObjectPath; // Path to find the GameObject
public int instanceId; // Alternative: instanceId
}
/// <summary>
/// Result of destroying a GameObject.
/// </summary>
[Serializable]
public class DestroyGameObjectResult
{
public bool success;
public string message;
public string destroyedPath;
public string destroyedName;
}
#endregion
}