Open and close prefabs in the stage view + create them (#283)
* refactor: remove unused UnityEngine references from menu item classes * Add new tools to manage a prefab, particularly, making them staged. This might be enough, but it's possible we may have to extract some logic from ManageGameObject * feat: add AssetPathUtility for asset path normalization and update references in ManageAsset and ManagePrefabs * feat: add prefab management tools and register them with the MCP server * feat: update prefab management commands to use 'prefabPath' and add 'create_from_gameobject' action * fix: update parameter references to 'prefabPath' in ManagePrefabs and manage_prefabs tools * fix: clarify error message for missing 'prefabPath' in create_from_gameobject command * fix: ensure pull request triggers for unity tests workflow * Revert "fix: ensure pull request triggers for unity tests workflow" This reverts commit 10bfe54b5b7f3c449852b1bf1bb72f498289a1a0. * Remove delayed execution of executing menu item, fixing #279 This brings the Unity window into focus but that seems to be a better UX for devs. Also streamline manage_menu_item tool info, as FastMCP recommends * docs: clarify menu item tool description with guidance to use list action first * feat: add version update for server_version.txt in bump-version workflow * fix: simplify error message for failed menu item executionmain
parent
549ac1eb0c
commit
ac4eae926e
|
|
@ -70,6 +70,9 @@ jobs:
|
||||||
echo "Updating UnityMcpBridge/UnityMcpServer~/src/pyproject.toml to $NEW_VERSION"
|
echo "Updating UnityMcpBridge/UnityMcpServer~/src/pyproject.toml to $NEW_VERSION"
|
||||||
sed -i '0,/^version = ".*"/s//version = "'"$NEW_VERSION"'"/' "UnityMcpBridge/UnityMcpServer~/src/pyproject.toml"
|
sed -i '0,/^version = ".*"/s//version = "'"$NEW_VERSION"'"/' "UnityMcpBridge/UnityMcpServer~/src/pyproject.toml"
|
||||||
|
|
||||||
|
echo "Updating UnityMcpBridge/UnityMcpServer~/src/server_version.txt to $NEW_VERSION"
|
||||||
|
echo "$NEW_VERSION" > "UnityMcpBridge/UnityMcpServer~/src/server_version.txt"
|
||||||
|
|
||||||
- name: Commit and push changes
|
- name: Commit and push changes
|
||||||
env:
|
env:
|
||||||
NEW_VERSION: ${{ steps.compute.outputs.new_version }}
|
NEW_VERSION: ${{ steps.compute.outputs.new_version }}
|
||||||
|
|
@ -78,7 +81,7 @@ jobs:
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
git config user.name "GitHub Actions"
|
git config user.name "GitHub Actions"
|
||||||
git config user.email "actions@github.com"
|
git config user.email "actions@github.com"
|
||||||
git add UnityMcpBridge/package.json "UnityMcpBridge/UnityMcpServer~/src/pyproject.toml"
|
git add UnityMcpBridge/package.json "UnityMcpBridge/UnityMcpServer~/src/pyproject.toml" "UnityMcpBridge/UnityMcpServer~/src/server_version.txt"
|
||||||
if git diff --cached --quiet; then
|
if git diff --cached --quiet; then
|
||||||
echo "No version changes to commit."
|
echo "No version changes to commit."
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,255 @@
|
||||||
|
using System.IO;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEditor.SceneManagement;
|
||||||
|
using UnityEngine;
|
||||||
|
using MCPForUnity.Editor.Tools.Prefabs;
|
||||||
|
using MCPForUnity.Editor.Tools;
|
||||||
|
|
||||||
|
namespace MCPForUnityTests.Editor.Tools
|
||||||
|
{
|
||||||
|
public class ManagePrefabsTests
|
||||||
|
{
|
||||||
|
private const string TempDirectory = "Assets/Temp/ManagePrefabsTests";
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
StageUtility.GoToMainStage();
|
||||||
|
EnsureTempDirectoryExists();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public void TearDown()
|
||||||
|
{
|
||||||
|
StageUtility.GoToMainStage();
|
||||||
|
}
|
||||||
|
|
||||||
|
[OneTimeTearDown]
|
||||||
|
public void CleanupAll()
|
||||||
|
{
|
||||||
|
StageUtility.GoToMainStage();
|
||||||
|
if (AssetDatabase.IsValidFolder(TempDirectory))
|
||||||
|
{
|
||||||
|
AssetDatabase.DeleteAsset(TempDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void OpenStage_OpensPrefabInIsolation()
|
||||||
|
{
|
||||||
|
string prefabPath = CreateTestPrefab("OpenStageCube");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var openParams = new JObject
|
||||||
|
{
|
||||||
|
["action"] = "open_stage",
|
||||||
|
["prefabPath"] = prefabPath
|
||||||
|
};
|
||||||
|
|
||||||
|
var openResult = ToJObject(ManagePrefabs.HandleCommand(openParams));
|
||||||
|
|
||||||
|
Assert.IsTrue(openResult.Value<bool>("success"), "open_stage should succeed for a valid prefab.");
|
||||||
|
|
||||||
|
PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
|
||||||
|
Assert.IsNotNull(stage, "Prefab stage should be open after open_stage.");
|
||||||
|
Assert.AreEqual(prefabPath, stage.assetPath, "Opened stage should match prefab path.");
|
||||||
|
|
||||||
|
var stageInfo = ToJObject(ManageEditor.HandleCommand(new JObject { ["action"] = "get_prefab_stage" }));
|
||||||
|
Assert.IsTrue(stageInfo.Value<bool>("success"), "get_prefab_stage should succeed when stage is open.");
|
||||||
|
|
||||||
|
var data = stageInfo["data"] as JObject;
|
||||||
|
Assert.IsNotNull(data, "Stage info should include data payload.");
|
||||||
|
Assert.IsTrue(data.Value<bool>("isOpen"));
|
||||||
|
Assert.AreEqual(prefabPath, data.Value<string>("assetPath"));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
StageUtility.GoToMainStage();
|
||||||
|
AssetDatabase.DeleteAsset(prefabPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CloseStage_ReturnsSuccess_WhenNoStageOpen()
|
||||||
|
{
|
||||||
|
StageUtility.GoToMainStage();
|
||||||
|
var closeResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
|
||||||
|
{
|
||||||
|
["action"] = "close_stage"
|
||||||
|
}));
|
||||||
|
|
||||||
|
Assert.IsTrue(closeResult.Value<bool>("success"), "close_stage should succeed even if no stage is open.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CloseStage_ClosesOpenPrefabStage()
|
||||||
|
{
|
||||||
|
string prefabPath = CreateTestPrefab("CloseStageCube");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ManagePrefabs.HandleCommand(new JObject
|
||||||
|
{
|
||||||
|
["action"] = "open_stage",
|
||||||
|
["prefabPath"] = prefabPath
|
||||||
|
});
|
||||||
|
|
||||||
|
var closeResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
|
||||||
|
{
|
||||||
|
["action"] = "close_stage"
|
||||||
|
}));
|
||||||
|
|
||||||
|
Assert.IsTrue(closeResult.Value<bool>("success"), "close_stage should succeed when stage is open.");
|
||||||
|
Assert.IsNull(PrefabStageUtility.GetCurrentPrefabStage(), "Prefab stage should be closed after close_stage.");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
StageUtility.GoToMainStage();
|
||||||
|
AssetDatabase.DeleteAsset(prefabPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SaveOpenStage_SavesDirtyChanges()
|
||||||
|
{
|
||||||
|
string prefabPath = CreateTestPrefab("SaveStageCube");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ManagePrefabs.HandleCommand(new JObject
|
||||||
|
{
|
||||||
|
["action"] = "open_stage",
|
||||||
|
["prefabPath"] = prefabPath
|
||||||
|
});
|
||||||
|
|
||||||
|
PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
|
||||||
|
Assert.IsNotNull(stage, "Stage should be open before modifying.");
|
||||||
|
|
||||||
|
stage.prefabContentsRoot.transform.localScale = new Vector3(2f, 2f, 2f);
|
||||||
|
|
||||||
|
var saveResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
|
||||||
|
{
|
||||||
|
["action"] = "save_open_stage"
|
||||||
|
}));
|
||||||
|
|
||||||
|
Assert.IsTrue(saveResult.Value<bool>("success"), "save_open_stage should succeed when stage is open.");
|
||||||
|
Assert.IsFalse(stage.scene.isDirty, "Stage scene should not be dirty after saving.");
|
||||||
|
|
||||||
|
GameObject reloaded = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||||
|
Assert.AreEqual(new Vector3(2f, 2f, 2f), reloaded.transform.localScale, "Saved prefab asset should include changes from open stage.");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
StageUtility.GoToMainStage();
|
||||||
|
AssetDatabase.DeleteAsset(prefabPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SaveOpenStage_ReturnsError_WhenNoStageOpen()
|
||||||
|
{
|
||||||
|
StageUtility.GoToMainStage();
|
||||||
|
|
||||||
|
var saveResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
|
||||||
|
{
|
||||||
|
["action"] = "save_open_stage"
|
||||||
|
}));
|
||||||
|
|
||||||
|
Assert.IsFalse(saveResult.Value<bool>("success"), "save_open_stage should fail when no stage is open.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CreateFromGameObject_CreatesPrefabAndLinksInstance()
|
||||||
|
{
|
||||||
|
EnsureTempDirectoryExists();
|
||||||
|
StageUtility.GoToMainStage();
|
||||||
|
|
||||||
|
string prefabPath = Path.Combine(TempDirectory, "SceneObjectSaved.prefab").Replace('\\', '/');
|
||||||
|
GameObject sceneObject = new GameObject("ScenePrefabSource");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
|
||||||
|
{
|
||||||
|
["action"] = "create_from_gameobject",
|
||||||
|
["target"] = sceneObject.name,
|
||||||
|
["prefabPath"] = prefabPath
|
||||||
|
}));
|
||||||
|
|
||||||
|
Assert.IsTrue(result.Value<bool>("success"), "create_from_gameobject should succeed for a valid scene object.");
|
||||||
|
|
||||||
|
var data = result["data"] as JObject;
|
||||||
|
Assert.IsNotNull(data, "Response data should include prefab information.");
|
||||||
|
|
||||||
|
string savedPath = data.Value<string>("prefabPath");
|
||||||
|
Assert.AreEqual(prefabPath, savedPath, "Returned prefab path should match the requested path.");
|
||||||
|
|
||||||
|
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(savedPath);
|
||||||
|
Assert.IsNotNull(prefabAsset, "Prefab asset should exist at the saved path.");
|
||||||
|
|
||||||
|
int instanceId = data.Value<int>("instanceId");
|
||||||
|
var linkedInstance = EditorUtility.InstanceIDToObject(instanceId) as GameObject;
|
||||||
|
Assert.IsNotNull(linkedInstance, "Linked instance should resolve from instanceId.");
|
||||||
|
Assert.AreEqual(savedPath, PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(linkedInstance), "Instance should be connected to the new prefab.");
|
||||||
|
|
||||||
|
sceneObject = linkedInstance;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(prefabPath) != null)
|
||||||
|
{
|
||||||
|
AssetDatabase.DeleteAsset(prefabPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sceneObject != null)
|
||||||
|
{
|
||||||
|
if (PrefabUtility.IsPartOfPrefabInstance(sceneObject))
|
||||||
|
{
|
||||||
|
PrefabUtility.UnpackPrefabInstance(
|
||||||
|
sceneObject,
|
||||||
|
PrefabUnpackMode.Completely,
|
||||||
|
InteractionMode.AutomatedAction
|
||||||
|
);
|
||||||
|
}
|
||||||
|
UnityEngine.Object.DestroyImmediate(sceneObject, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateTestPrefab(string name)
|
||||||
|
{
|
||||||
|
EnsureTempDirectoryExists();
|
||||||
|
|
||||||
|
GameObject temp = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||||||
|
temp.name = name;
|
||||||
|
|
||||||
|
string path = Path.Combine(TempDirectory, name + ".prefab").Replace('\\', '/');
|
||||||
|
PrefabUtility.SaveAsPrefabAsset(temp, path, out bool success);
|
||||||
|
UnityEngine.Object.DestroyImmediate(temp);
|
||||||
|
|
||||||
|
Assert.IsTrue(success, "PrefabUtility.SaveAsPrefabAsset should succeed for test prefab.");
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureTempDirectoryExists()
|
||||||
|
{
|
||||||
|
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
|
||||||
|
{
|
||||||
|
AssetDatabase.CreateFolder("Assets", "Temp");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AssetDatabase.IsValidFolder(TempDirectory))
|
||||||
|
{
|
||||||
|
AssetDatabase.CreateFolder("Assets/Temp", "ManagePrefabsTests");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JObject ToJObject(object result)
|
||||||
|
{
|
||||||
|
return result as JObject ?? JObject.FromObject(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8e7a7e542325421ba6de4992ddb3f5db
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MCPForUnity.Editor.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides common utility methods for working with Unity asset paths.
|
||||||
|
/// </summary>
|
||||||
|
public static class AssetPathUtility
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Normalizes a Unity asset path by ensuring forward slashes are used and that it is rooted under "Assets/".
|
||||||
|
/// </summary>
|
||||||
|
public static string SanitizeAssetPath(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
path = path.Replace('\\', '/');
|
||||||
|
if (!path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return "Assets/" + path.TrimStart('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1d42f5b5ea5d4d43ad1a771e14bda2a0
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -15,6 +15,7 @@ using MCPForUnity.Editor.Helpers;
|
||||||
using MCPForUnity.Editor.Models;
|
using MCPForUnity.Editor.Models;
|
||||||
using MCPForUnity.Editor.Tools;
|
using MCPForUnity.Editor.Tools;
|
||||||
using MCPForUnity.Editor.Tools.MenuItems;
|
using MCPForUnity.Editor.Tools.MenuItems;
|
||||||
|
using MCPForUnity.Editor.Tools.Prefabs;
|
||||||
|
|
||||||
namespace MCPForUnity.Editor
|
namespace MCPForUnity.Editor
|
||||||
{
|
{
|
||||||
|
|
@ -1040,7 +1041,26 @@ namespace MCPForUnity.Editor
|
||||||
// Use JObject for parameters as the new handlers likely expect this
|
// Use JObject for parameters as the new handlers likely expect this
|
||||||
JObject paramsObject = command.@params ?? new JObject();
|
JObject paramsObject = command.@params ?? new JObject();
|
||||||
|
|
||||||
object result = CommandRegistry.GetHandler(command.type)(paramsObject);
|
// Route command based on the new tool structure from the refactor plan
|
||||||
|
object result = command.type switch
|
||||||
|
{
|
||||||
|
// Maps the command type (tool name) to the corresponding handler's static HandleCommand method
|
||||||
|
// Assumes each handler class has a static method named 'HandleCommand' that takes JObject parameters
|
||||||
|
"manage_script" => ManageScript.HandleCommand(paramsObject),
|
||||||
|
// Run scene operations on the main thread to avoid deadlocks/hangs (with diagnostics under debug flag)
|
||||||
|
"manage_scene" => HandleManageScene(paramsObject)
|
||||||
|
?? throw new TimeoutException($"manage_scene timed out after {FrameIOTimeoutMs} ms on main thread"),
|
||||||
|
"manage_editor" => ManageEditor.HandleCommand(paramsObject),
|
||||||
|
"manage_gameobject" => ManageGameObject.HandleCommand(paramsObject),
|
||||||
|
"manage_asset" => ManageAsset.HandleCommand(paramsObject),
|
||||||
|
"manage_shader" => ManageShader.HandleCommand(paramsObject),
|
||||||
|
"read_console" => ReadConsole.HandleCommand(paramsObject),
|
||||||
|
"manage_menu_item" => ManageMenuItem.HandleCommand(paramsObject),
|
||||||
|
"manage_prefabs" => ManagePrefabs.HandleCommand(paramsObject),
|
||||||
|
_ => throw new ArgumentException(
|
||||||
|
$"Unknown or unsupported command type: {command.type}"
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
// Standard success response format
|
// Standard success response format
|
||||||
var response = new { status = "success", result };
|
var response = new { status = "success", result };
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(path))
|
if (string.IsNullOrEmpty(path))
|
||||||
return Response.Error("'path' is required for reimport.");
|
return Response.Error("'path' is required for reimport.");
|
||||||
string fullPath = SanitizeAssetPath(path);
|
string fullPath = AssetPathUtility.SanitizeAssetPath(path);
|
||||||
if (!AssetExists(fullPath))
|
if (!AssetExists(fullPath))
|
||||||
return Response.Error($"Asset not found at path: {fullPath}");
|
return Response.Error($"Asset not found at path: {fullPath}");
|
||||||
|
|
||||||
|
|
@ -154,7 +154,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
if (string.IsNullOrEmpty(assetType))
|
if (string.IsNullOrEmpty(assetType))
|
||||||
return Response.Error("'assetType' is required for create.");
|
return Response.Error("'assetType' is required for create.");
|
||||||
|
|
||||||
string fullPath = SanitizeAssetPath(path);
|
string fullPath = AssetPathUtility.SanitizeAssetPath(path);
|
||||||
string directory = Path.GetDirectoryName(fullPath);
|
string directory = Path.GetDirectoryName(fullPath);
|
||||||
|
|
||||||
// Ensure directory exists
|
// Ensure directory exists
|
||||||
|
|
@ -280,7 +280,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(path))
|
if (string.IsNullOrEmpty(path))
|
||||||
return Response.Error("'path' is required for create_folder.");
|
return Response.Error("'path' is required for create_folder.");
|
||||||
string fullPath = SanitizeAssetPath(path);
|
string fullPath = AssetPathUtility.SanitizeAssetPath(path);
|
||||||
string parentDir = Path.GetDirectoryName(fullPath);
|
string parentDir = Path.GetDirectoryName(fullPath);
|
||||||
string folderName = Path.GetFileName(fullPath);
|
string folderName = Path.GetFileName(fullPath);
|
||||||
|
|
||||||
|
|
@ -338,7 +338,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
if (properties == null || !properties.HasValues)
|
if (properties == null || !properties.HasValues)
|
||||||
return Response.Error("'properties' are required for modify.");
|
return Response.Error("'properties' are required for modify.");
|
||||||
|
|
||||||
string fullPath = SanitizeAssetPath(path);
|
string fullPath = AssetPathUtility.SanitizeAssetPath(path);
|
||||||
if (!AssetExists(fullPath))
|
if (!AssetExists(fullPath))
|
||||||
return Response.Error($"Asset not found at path: {fullPath}");
|
return Response.Error($"Asset not found at path: {fullPath}");
|
||||||
|
|
||||||
|
|
@ -495,7 +495,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(path))
|
if (string.IsNullOrEmpty(path))
|
||||||
return Response.Error("'path' is required for delete.");
|
return Response.Error("'path' is required for delete.");
|
||||||
string fullPath = SanitizeAssetPath(path);
|
string fullPath = AssetPathUtility.SanitizeAssetPath(path);
|
||||||
if (!AssetExists(fullPath))
|
if (!AssetExists(fullPath))
|
||||||
return Response.Error($"Asset not found at path: {fullPath}");
|
return Response.Error($"Asset not found at path: {fullPath}");
|
||||||
|
|
||||||
|
|
@ -526,7 +526,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
if (string.IsNullOrEmpty(path))
|
if (string.IsNullOrEmpty(path))
|
||||||
return Response.Error("'path' is required for duplicate.");
|
return Response.Error("'path' is required for duplicate.");
|
||||||
|
|
||||||
string sourcePath = SanitizeAssetPath(path);
|
string sourcePath = AssetPathUtility.SanitizeAssetPath(path);
|
||||||
if (!AssetExists(sourcePath))
|
if (!AssetExists(sourcePath))
|
||||||
return Response.Error($"Source asset not found at path: {sourcePath}");
|
return Response.Error($"Source asset not found at path: {sourcePath}");
|
||||||
|
|
||||||
|
|
@ -538,7 +538,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
destPath = SanitizeAssetPath(destinationPath);
|
destPath = AssetPathUtility.SanitizeAssetPath(destinationPath);
|
||||||
if (AssetExists(destPath))
|
if (AssetExists(destPath))
|
||||||
return Response.Error($"Asset already exists at destination path: {destPath}");
|
return Response.Error($"Asset already exists at destination path: {destPath}");
|
||||||
// Ensure destination directory exists
|
// Ensure destination directory exists
|
||||||
|
|
@ -576,8 +576,8 @@ namespace MCPForUnity.Editor.Tools
|
||||||
if (string.IsNullOrEmpty(destinationPath))
|
if (string.IsNullOrEmpty(destinationPath))
|
||||||
return Response.Error("'destination' path is required for move/rename.");
|
return Response.Error("'destination' path is required for move/rename.");
|
||||||
|
|
||||||
string sourcePath = SanitizeAssetPath(path);
|
string sourcePath = AssetPathUtility.SanitizeAssetPath(path);
|
||||||
string destPath = SanitizeAssetPath(destinationPath);
|
string destPath = AssetPathUtility.SanitizeAssetPath(destinationPath);
|
||||||
|
|
||||||
if (!AssetExists(sourcePath))
|
if (!AssetExists(sourcePath))
|
||||||
return Response.Error($"Source asset not found at path: {sourcePath}");
|
return Response.Error($"Source asset not found at path: {sourcePath}");
|
||||||
|
|
@ -642,7 +642,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
string[] folderScope = null;
|
string[] folderScope = null;
|
||||||
if (!string.IsNullOrEmpty(pathScope))
|
if (!string.IsNullOrEmpty(pathScope))
|
||||||
{
|
{
|
||||||
folderScope = new string[] { SanitizeAssetPath(pathScope) };
|
folderScope = new string[] { AssetPathUtility.SanitizeAssetPath(pathScope) };
|
||||||
if (!AssetDatabase.IsValidFolder(folderScope[0]))
|
if (!AssetDatabase.IsValidFolder(folderScope[0]))
|
||||||
{
|
{
|
||||||
// Maybe the user provided a file path instead of a folder?
|
// Maybe the user provided a file path instead of a folder?
|
||||||
|
|
@ -732,7 +732,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(path))
|
if (string.IsNullOrEmpty(path))
|
||||||
return Response.Error("'path' is required for get_info.");
|
return Response.Error("'path' is required for get_info.");
|
||||||
string fullPath = SanitizeAssetPath(path);
|
string fullPath = AssetPathUtility.SanitizeAssetPath(path);
|
||||||
if (!AssetExists(fullPath))
|
if (!AssetExists(fullPath))
|
||||||
return Response.Error($"Asset not found at path: {fullPath}");
|
return Response.Error($"Asset not found at path: {fullPath}");
|
||||||
|
|
||||||
|
|
@ -761,7 +761,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
return Response.Error("'path' is required for get_components.");
|
return Response.Error("'path' is required for get_components.");
|
||||||
|
|
||||||
// 2. Sanitize and check existence
|
// 2. Sanitize and check existence
|
||||||
string fullPath = SanitizeAssetPath(path);
|
string fullPath = AssetPathUtility.SanitizeAssetPath(path);
|
||||||
if (!AssetExists(fullPath))
|
if (!AssetExists(fullPath))
|
||||||
return Response.Error($"Asset not found at path: {fullPath}");
|
return Response.Error($"Asset not found at path: {fullPath}");
|
||||||
|
|
||||||
|
|
@ -829,18 +829,6 @@ namespace MCPForUnity.Editor.Tools
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ensures the asset path starts with "Assets/".
|
/// Ensures the asset path starts with "Assets/".
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Checks if an asset exists at the given path (file or folder).
|
/// Checks if an asset exists at the given path (file or folder).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -930,10 +918,12 @@ namespace MCPForUnity.Editor.Tools
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (properties["color"] is JArray colorArr) //Use color now with examples set in manage_asset.py
|
}
|
||||||
|
else if (properties["color"] is JArray colorArr) //Use color now with examples set in manage_asset.py
|
||||||
{
|
{
|
||||||
string propName = "_Color";
|
string propName = "_Color";
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
if (colorArr.Count >= 3)
|
if (colorArr.Count >= 3)
|
||||||
{
|
{
|
||||||
Color newColor = new Color(
|
Color newColor = new Color(
|
||||||
|
|
@ -949,7 +939,8 @@ namespace MCPForUnity.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex)
|
||||||
|
{
|
||||||
Debug.LogWarning(
|
Debug.LogWarning(
|
||||||
$"Error parsing color property '{propName}': {ex.Message}"
|
$"Error parsing color property '{propName}': {ex.Message}"
|
||||||
);
|
);
|
||||||
|
|
@ -989,7 +980,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
if (!string.IsNullOrEmpty(texPath))
|
if (!string.IsNullOrEmpty(texPath))
|
||||||
{
|
{
|
||||||
Texture newTex = AssetDatabase.LoadAssetAtPath<Texture>(
|
Texture newTex = AssetDatabase.LoadAssetAtPath<Texture>(
|
||||||
SanitizeAssetPath(texPath)
|
AssetPathUtility.SanitizeAssetPath(texPath)
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
newTex != null
|
newTex != null
|
||||||
|
|
@ -1217,7 +1208,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
&& token.Type == JTokenType.String
|
&& token.Type == JTokenType.String
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
string assetPath = SanitizeAssetPath(token.ToString());
|
string assetPath = AssetPathUtility.SanitizeAssetPath(token.ToString());
|
||||||
UnityEngine.Object loadedAsset = AssetDatabase.LoadAssetAtPath(
|
UnityEngine.Object loadedAsset = AssetDatabase.LoadAssetAtPath(
|
||||||
assetPath,
|
assetPath,
|
||||||
targetType
|
targetType
|
||||||
|
|
@ -1337,4 +1328,3 @@ namespace MCPForUnity.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@ using System.IO;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEditorInternal; // Required for tag management
|
using UnityEditorInternal; // Required for tag management
|
||||||
|
using UnityEditor.SceneManagement;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using MCPForUnity.Editor.Helpers; // For Response class
|
using MCPForUnity.Editor.Helpers;
|
||||||
|
|
||||||
namespace MCPForUnity.Editor.Tools
|
namespace MCPForUnity.Editor.Tools
|
||||||
{
|
{
|
||||||
|
|
@ -98,6 +99,8 @@ namespace MCPForUnity.Editor.Tools
|
||||||
return GetActiveTool();
|
return GetActiveTool();
|
||||||
case "get_selection":
|
case "get_selection":
|
||||||
return GetSelection();
|
return GetSelection();
|
||||||
|
case "get_prefab_stage":
|
||||||
|
return GetPrefabStageInfo();
|
||||||
case "set_active_tool":
|
case "set_active_tool":
|
||||||
string toolName = @params["toolName"]?.ToString();
|
string toolName = @params["toolName"]?.ToString();
|
||||||
if (string.IsNullOrEmpty(toolName))
|
if (string.IsNullOrEmpty(toolName))
|
||||||
|
|
@ -140,7 +143,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return Response.Error(
|
return Response.Error(
|
||||||
$"Unknown action: '{action}'. Supported actions include play, pause, stop, get_state, get_project_root, get_windows, get_active_tool, get_selection, set_active_tool, add_tag, remove_tag, get_tags, add_layer, remove_layer, get_layers."
|
$"Unknown action: '{action}'. Supported actions include play, pause, stop, get_state, get_project_root, get_windows, get_active_tool, get_selection, get_prefab_stage, set_active_tool, add_tag, remove_tag, get_tags, add_layer, remove_layer, get_layers."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -244,6 +247,35 @@ namespace MCPForUnity.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static object GetPrefabStageInfo()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
|
||||||
|
if (stage == null)
|
||||||
|
{
|
||||||
|
return Response.Success
|
||||||
|
("No prefab stage is currently open.", new { isOpen = false });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.Success(
|
||||||
|
"Prefab stage info retrieved.",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
isOpen = true,
|
||||||
|
assetPath = stage.assetPath,
|
||||||
|
prefabRootName = stage.prefabContentsRoot != null ? stage.prefabContentsRoot.name : null,
|
||||||
|
mode = stage.mode.ToString(),
|
||||||
|
isDirty = stage.scene.isDirty
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return Response.Error($"Error getting prefab stage info: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static object GetActiveTool()
|
private static object GetActiveTool()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -610,4 +642,3 @@ namespace MCPForUnity.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using UnityEditor;
|
|
||||||
using UnityEngine;
|
|
||||||
using MCPForUnity.Editor.Helpers;
|
using MCPForUnity.Editor.Helpers;
|
||||||
|
|
||||||
namespace MCPForUnity.Editor.Tools.MenuItems
|
namespace MCPForUnity.Editor.Tools.MenuItems
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Facade handler for managing Unity Editor menu items.
|
|
||||||
/// Routes actions to read or execute implementations.
|
|
||||||
/// </summary>
|
|
||||||
public static class ManageMenuItem
|
public static class ManageMenuItem
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
|
||||||
using MCPForUnity.Editor.Helpers;
|
using MCPForUnity.Editor.Helpers;
|
||||||
|
|
||||||
namespace MCPForUnity.Editor.Tools.MenuItems
|
namespace MCPForUnity.Editor.Tools.MenuItems
|
||||||
|
|
@ -37,23 +36,12 @@ namespace MCPForUnity.Editor.Tools.MenuItems
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Execute on main thread using delayCall
|
bool executed = EditorApplication.ExecuteMenuItem(menuPath);
|
||||||
EditorApplication.delayCall += () =>
|
if (!executed)
|
||||||
{
|
{
|
||||||
try
|
McpLog.Error($"[MenuItemExecutor] Failed to execute menu item '{menuPath}'. It might be invalid, disabled, or context-dependent.");
|
||||||
{
|
return Response.Error($"Failed to execute menu item '{menuPath}'. It might be invalid, disabled, or context-dependent.");
|
||||||
bool executed = EditorApplication.ExecuteMenuItem(menuPath);
|
}
|
||||||
if (!executed)
|
|
||||||
{
|
|
||||||
McpLog.Error($"[MenuItemExecutor] Failed to execute menu item via delayCall: '{menuPath}'. It might be invalid, disabled, or context-dependent.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception delayEx)
|
|
||||||
{
|
|
||||||
McpLog.Error($"[MenuItemExecutor] Exception during delayed execution of '{menuPath}': {delayEx}");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return Response.Success($"Attempted to execute menu item: '{menuPath}'. Check Unity logs for confirmation or errors.");
|
return Response.Success($"Attempted to execute menu item: '{menuPath}'. Check Unity logs for confirmation or errors.");
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
|
||||||
using MCPForUnity.Editor.Helpers;
|
using MCPForUnity.Editor.Helpers;
|
||||||
|
|
||||||
namespace MCPForUnity.Editor.Tools.MenuItems
|
namespace MCPForUnity.Editor.Tools.MenuItems
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1bd48a1b7555c46bba168078ce0291cc
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,274 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using MCPForUnity.Editor.Helpers;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEditor.SceneManagement;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.SceneManagement;
|
||||||
|
|
||||||
|
namespace MCPForUnity.Editor.Tools.Prefabs
|
||||||
|
{
|
||||||
|
public static class ManagePrefabs
|
||||||
|
{
|
||||||
|
private const string SupportedActions = "open_stage, close_stage, save_open_stage, create_from_gameobject";
|
||||||
|
|
||||||
|
public static object HandleCommand(JObject @params)
|
||||||
|
{
|
||||||
|
if (@params == null)
|
||||||
|
{
|
||||||
|
return Response.Error("Parameters cannot be null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string action = @params["action"]?.ToString()?.ToLowerInvariant();
|
||||||
|
if (string.IsNullOrEmpty(action))
|
||||||
|
{
|
||||||
|
return Response.Error($"Action parameter is required. Valid actions are: {SupportedActions}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case "open_stage":
|
||||||
|
return OpenStage(@params);
|
||||||
|
case "close_stage":
|
||||||
|
return CloseStage(@params);
|
||||||
|
case "save_open_stage":
|
||||||
|
return SaveOpenStage();
|
||||||
|
case "create_from_gameobject":
|
||||||
|
return CreatePrefabFromGameObject(@params);
|
||||||
|
default:
|
||||||
|
return Response.Error($"Unknown action: '{action}'. Valid actions are: {SupportedActions}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
McpLog.Error($"[ManagePrefabs] Action '{action}' failed: {e}");
|
||||||
|
return Response.Error($"Internal error: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object OpenStage(JObject @params)
|
||||||
|
{
|
||||||
|
string prefabPath = @params["prefabPath"]?.ToString();
|
||||||
|
if (string.IsNullOrEmpty(prefabPath))
|
||||||
|
{
|
||||||
|
return Response.Error("'prefabPath' parameter is required for open_stage.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string sanitizedPath = AssetPathUtility.SanitizeAssetPath(prefabPath);
|
||||||
|
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(sanitizedPath);
|
||||||
|
if (prefabAsset == null)
|
||||||
|
{
|
||||||
|
return Response.Error($"No prefab asset found at path '{sanitizedPath}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string modeValue = @params["mode"]?.ToString();
|
||||||
|
if (!string.IsNullOrEmpty(modeValue) && !modeValue.Equals(PrefabStage.Mode.InIsolation.ToString(), StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return Response.Error("Only PrefabStage mode 'InIsolation' is supported at this time.");
|
||||||
|
}
|
||||||
|
|
||||||
|
PrefabStage stage = PrefabStageUtility.OpenPrefab(sanitizedPath);
|
||||||
|
if (stage == null)
|
||||||
|
{
|
||||||
|
return Response.Error($"Failed to open prefab stage for '{sanitizedPath}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.Success($"Opened prefab stage for '{sanitizedPath}'.", SerializeStage(stage));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object CloseStage(JObject @params)
|
||||||
|
{
|
||||||
|
PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
|
||||||
|
if (stage == null)
|
||||||
|
{
|
||||||
|
return Response.Success("No prefab stage was open.");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool saveBeforeClose = @params["saveBeforeClose"]?.ToObject<bool>() ?? false;
|
||||||
|
if (saveBeforeClose && stage.scene.isDirty)
|
||||||
|
{
|
||||||
|
SaveStagePrefab(stage);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
}
|
||||||
|
|
||||||
|
StageUtility.GoToMainStage();
|
||||||
|
return Response.Success($"Closed prefab stage for '{stage.assetPath}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object SaveOpenStage()
|
||||||
|
{
|
||||||
|
PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
|
||||||
|
if (stage == null)
|
||||||
|
{
|
||||||
|
return Response.Error("No prefab stage is currently open.");
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveStagePrefab(stage);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
return Response.Success($"Saved prefab stage for '{stage.assetPath}'.", SerializeStage(stage));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SaveStagePrefab(PrefabStage stage)
|
||||||
|
{
|
||||||
|
if (stage?.prefabContentsRoot == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Cannot save prefab stage without a prefab root.");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool saved = PrefabUtility.SaveAsPrefabAsset(stage.prefabContentsRoot, stage.assetPath);
|
||||||
|
if (!saved)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Failed to save prefab asset at '{stage.assetPath}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object CreatePrefabFromGameObject(JObject @params)
|
||||||
|
{
|
||||||
|
string targetName = @params["target"]?.ToString() ?? @params["name"]?.ToString();
|
||||||
|
if (string.IsNullOrEmpty(targetName))
|
||||||
|
{
|
||||||
|
return Response.Error("'target' parameter is required for create_from_gameobject.");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool includeInactive = @params["searchInactive"]?.ToObject<bool>() ?? false;
|
||||||
|
GameObject sourceObject = FindSceneObjectByName(targetName, includeInactive);
|
||||||
|
if (sourceObject == null)
|
||||||
|
{
|
||||||
|
return Response.Error($"GameObject '{targetName}' not found in the active scene.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PrefabUtility.IsPartOfPrefabAsset(sourceObject))
|
||||||
|
{
|
||||||
|
return Response.Error(
|
||||||
|
$"GameObject '{sourceObject.name}' is part of a prefab asset. Open the prefab stage to save changes instead."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PrefabInstanceStatus status = PrefabUtility.GetPrefabInstanceStatus(sourceObject);
|
||||||
|
if (status != PrefabInstanceStatus.NotAPrefab)
|
||||||
|
{
|
||||||
|
return Response.Error(
|
||||||
|
$"GameObject '{sourceObject.name}' is already linked to an existing prefab instance."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
string requestedPath = @params["prefabPath"]?.ToString();
|
||||||
|
if (string.IsNullOrWhiteSpace(requestedPath))
|
||||||
|
{
|
||||||
|
return Response.Error("'prefabPath' parameter is required for create_from_gameobject.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string sanitizedPath = AssetPathUtility.SanitizeAssetPath(requestedPath);
|
||||||
|
if (!sanitizedPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
sanitizedPath += ".prefab";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool allowOverwrite = @params["allowOverwrite"]?.ToObject<bool>() ?? false;
|
||||||
|
string finalPath = sanitizedPath;
|
||||||
|
|
||||||
|
if (!allowOverwrite && AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(finalPath) != null)
|
||||||
|
{
|
||||||
|
finalPath = AssetDatabase.GenerateUniqueAssetPath(finalPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureAssetDirectoryExists(finalPath);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
GameObject connectedInstance = PrefabUtility.SaveAsPrefabAssetAndConnect(
|
||||||
|
sourceObject,
|
||||||
|
finalPath,
|
||||||
|
InteractionMode.AutomatedAction
|
||||||
|
);
|
||||||
|
|
||||||
|
if (connectedInstance == null)
|
||||||
|
{
|
||||||
|
return Response.Error($"Failed to save prefab asset at '{finalPath}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Selection.activeGameObject = connectedInstance;
|
||||||
|
|
||||||
|
return Response.Success(
|
||||||
|
$"Prefab created at '{finalPath}' and instance linked.",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
prefabPath = finalPath,
|
||||||
|
instanceId = connectedInstance.GetInstanceID()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return Response.Error($"Error saving prefab asset at '{finalPath}': {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureAssetDirectoryExists(string assetPath)
|
||||||
|
{
|
||||||
|
string directory = Path.GetDirectoryName(assetPath);
|
||||||
|
if (string.IsNullOrEmpty(directory))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string fullDirectory = Path.Combine(Directory.GetCurrentDirectory(), directory);
|
||||||
|
if (!Directory.Exists(fullDirectory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(fullDirectory);
|
||||||
|
AssetDatabase.Refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GameObject FindSceneObjectByName(string name, bool includeInactive)
|
||||||
|
{
|
||||||
|
PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
|
||||||
|
if (stage?.prefabContentsRoot != null)
|
||||||
|
{
|
||||||
|
foreach (Transform transform in stage.prefabContentsRoot.GetComponentsInChildren<Transform>(includeInactive))
|
||||||
|
{
|
||||||
|
if (transform.name == name)
|
||||||
|
{
|
||||||
|
return transform.gameObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scene activeScene = SceneManager.GetActiveScene();
|
||||||
|
foreach (GameObject root in activeScene.GetRootGameObjects())
|
||||||
|
{
|
||||||
|
foreach (Transform transform in root.GetComponentsInChildren<Transform>(includeInactive))
|
||||||
|
{
|
||||||
|
GameObject candidate = transform.gameObject;
|
||||||
|
if (candidate.name == name)
|
||||||
|
{
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object SerializeStage(PrefabStage stage)
|
||||||
|
{
|
||||||
|
if (stage == null)
|
||||||
|
{
|
||||||
|
return new { isOpen = false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
isOpen = true,
|
||||||
|
assetPath = stage.assetPath,
|
||||||
|
prefabRootName = stage.prefabContentsRoot != null ? stage.prefabContentsRoot.name : null,
|
||||||
|
mode = stage.mode.ToString(),
|
||||||
|
isDirty = stage.scene.isDirty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c14e76b2aa7bb4570a88903b061e946e
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -5,6 +5,7 @@ from .manage_scene import register_manage_scene_tools
|
||||||
from .manage_editor import register_manage_editor_tools
|
from .manage_editor import register_manage_editor_tools
|
||||||
from .manage_gameobject import register_manage_gameobject_tools
|
from .manage_gameobject import register_manage_gameobject_tools
|
||||||
from .manage_asset import register_manage_asset_tools
|
from .manage_asset import register_manage_asset_tools
|
||||||
|
from .manage_prefabs import register_manage_prefabs_tools
|
||||||
from .manage_shader import register_manage_shader_tools
|
from .manage_shader import register_manage_shader_tools
|
||||||
from .read_console import register_read_console_tools
|
from .read_console import register_read_console_tools
|
||||||
from .manage_menu_item import register_manage_menu_item_tools
|
from .manage_menu_item import register_manage_menu_item_tools
|
||||||
|
|
@ -22,6 +23,7 @@ def register_all_tools(mcp):
|
||||||
register_manage_editor_tools(mcp)
|
register_manage_editor_tools(mcp)
|
||||||
register_manage_gameobject_tools(mcp)
|
register_manage_gameobject_tools(mcp)
|
||||||
register_manage_asset_tools(mcp)
|
register_manage_asset_tools(mcp)
|
||||||
|
register_manage_prefabs_tools(mcp)
|
||||||
register_manage_shader_tools(mcp)
|
register_manage_shader_tools(mcp)
|
||||||
register_read_console_tools(mcp)
|
register_read_console_tools(mcp)
|
||||||
register_manage_menu_item_tools(mcp)
|
register_manage_menu_item_tools(mcp)
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ from unity_connection import get_unity_connection, async_send_command_with_retry
|
||||||
def register_manage_menu_item_tools(mcp: FastMCP):
|
def register_manage_menu_item_tools(mcp: FastMCP):
|
||||||
"""Registers the manage_menu_item tool with the MCP server."""
|
"""Registers the manage_menu_item tool with the MCP server."""
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool(description="Manage Unity menu items (execute/list/exists). If you're not sure what menu item to use, use the 'list' action to find it before using 'execute'.")
|
||||||
@telemetry_tool("manage_menu_item")
|
@telemetry_tool("manage_menu_item")
|
||||||
async def manage_menu_item(
|
async def manage_menu_item(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
|
|
@ -25,18 +25,6 @@ def register_manage_menu_item_tools(mcp: FastMCP):
|
||||||
refresh: Annotated[bool | None,
|
refresh: Annotated[bool | None,
|
||||||
"Optional flag to force refresh of the menu cache when listing"] = None,
|
"Optional flag to force refresh of the menu cache when listing"] = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Manage Unity menu items (execute/list/exists).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ctx: The MCP context.
|
|
||||||
action: One of 'execute', 'list', 'exists'.
|
|
||||||
menu_path: Menu path for 'execute' or 'exists' (e.g., "File/Save Project").
|
|
||||||
search: Optional filter string for 'list'.
|
|
||||||
refresh: Optional flag to force refresh of the menu cache when listing.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A dictionary with operation results ('success', 'data', 'error').
|
|
||||||
"""
|
|
||||||
# Prepare parameters for the C# handler
|
# Prepare parameters for the C# handler
|
||||||
params_dict: dict[str, Any] = {
|
params_dict: dict[str, Any] = {
|
||||||
"action": action,
|
"action": action,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
from typing import Annotated, Any, Literal
|
||||||
|
from mcp.server.fastmcp import FastMCP, Context
|
||||||
|
|
||||||
|
from telemetry_decorator import telemetry_tool
|
||||||
|
from unity_connection import send_command_with_retry
|
||||||
|
|
||||||
|
|
||||||
|
def register_manage_prefabs_tools(mcp: FastMCP) -> None:
|
||||||
|
"""Register prefab management tools with the MCP server."""
|
||||||
|
|
||||||
|
@mcp.tool(description="Bridge for prefab management commands (stage control and creation).")
|
||||||
|
@telemetry_tool("manage_prefabs")
|
||||||
|
def manage_prefabs(
|
||||||
|
ctx: Context,
|
||||||
|
action: Annotated[Literal[
|
||||||
|
"open_stage",
|
||||||
|
"close_stage",
|
||||||
|
"save_open_stage",
|
||||||
|
"create_from_gameobject",
|
||||||
|
], "One of open_stage, close_stage, save_open_stage, create_from_gameobject"],
|
||||||
|
prefab_path: Annotated[str | None,
|
||||||
|
"Prefab asset path relative to Assets e.g. Assets/Prefabs/favorite.prefab"] = None,
|
||||||
|
mode: Annotated[str | None,
|
||||||
|
"Optional prefab stage mode (only 'InIsolation' is currently supported)"] = None,
|
||||||
|
save_before_close: Annotated[bool | None,
|
||||||
|
"When true, `close_stage` will save the prefab before exiting the stage."] = None,
|
||||||
|
target: Annotated[str | None,
|
||||||
|
"Scene GameObject name required for create_from_gameobject"] = None,
|
||||||
|
allow_overwrite: Annotated[bool | None,
|
||||||
|
"Allow replacing an existing prefab at the same path"] = None,
|
||||||
|
search_inactive: Annotated[bool | None,
|
||||||
|
"Include inactive objects when resolving the target name"] = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
params: dict[str, Any] = {"action": action}
|
||||||
|
|
||||||
|
if prefab_path:
|
||||||
|
params["prefabPath"] = prefab_path
|
||||||
|
if mode:
|
||||||
|
params["mode"] = mode
|
||||||
|
if save_before_close is not None:
|
||||||
|
params["saveBeforeClose"] = bool(save_before_close)
|
||||||
|
if target:
|
||||||
|
params["target"] = target
|
||||||
|
if allow_overwrite is not None:
|
||||||
|
params["allowOverwrite"] = bool(allow_overwrite)
|
||||||
|
if search_inactive is not None:
|
||||||
|
params["searchInactive"] = bool(search_inactive)
|
||||||
|
response = send_command_with_retry("manage_prefabs", params)
|
||||||
|
|
||||||
|
if isinstance(response, dict) and response.get("success"):
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": response.get("message", "Prefab operation successful."),
|
||||||
|
"data": response.get("data"),
|
||||||
|
}
|
||||||
|
return response if isinstance(response, dict) else {"success": False, "message": str(response)}
|
||||||
|
except Exception as exc:
|
||||||
|
return {"success": False, "message": f"Python error managing prefabs: {exc}"}
|
||||||
Loading…
Reference in New Issue