Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6b6172f
refactor: remove unused UnityEngine references from menu item classes
msanatan Sep 17, 2025
2d6baeb
Merge branch 'main' into improved-prefab-support
msanatan Sep 22, 2025
08bb831
Add new tools to manage a prefab, particularly, making them staged.
msanatan Sep 22, 2025
7f940db
feat: add AssetPathUtility for asset path normalization and update re…
msanatan Sep 22, 2025
863984c
feat: add prefab management tools and register them with the MCP server
msanatan Sep 23, 2025
dfa3272
feat: update prefab management commands to use 'prefabPath' and add '…
msanatan Sep 23, 2025
9002957
fix: update parameter references to 'prefabPath' in ManagePrefabs and…
msanatan Sep 23, 2025
fdd1262
fix: clarify error message for missing 'prefabPath' in create_from_ga…
msanatan Sep 23, 2025
10bfe54
fix: ensure pull request triggers for unity tests workflow
msanatan Sep 25, 2025
63b1a42
Revert "fix: ensure pull request triggers for unity tests workflow"
msanatan Sep 25, 2025
745aa32
Remove delayed execution of executing menu item, fixing #279
msanatan Sep 25, 2025
4d70f93
Merge pull request #1 from msanatan/menu-item-fix
msanatan Sep 25, 2025
7972faa
docs: clarify menu item tool description with guidance to use list ac…
msanatan Sep 25, 2025
72c8983
feat: add version update for server_version.txt in bump-version workflow
msanatan Sep 25, 2025
f685e26
fix: simplify error message for failed menu item execution
msanatan Sep 25, 2025
c72549c
Merge branch 'main' into improved-prefab-support
msanatan Sep 26, 2025
8a7d265
Merge branch 'main' into improved-prefab-support
msanatan Sep 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/unity-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ on:
- TestProjects/UnityMCPTests/**
- UnityMcpBridge/Editor/**
- .github/workflows/unity-tests.yml
pull_request:
types: [opened, synchronize, reopened, edited]
branches: [ main ]
paths:
- TestProjects/UnityMCPTests/**
- UnityMcpBridge/Editor/**
- .github/workflows/unity-tests.yml

jobs:
testAllModes:
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions UnityMcpBridge/Editor/Helpers/AssetPathUtility.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
11 changes: 11 additions & 0 deletions UnityMcpBridge/Editor/Helpers/AssetPathUtility.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions UnityMcpBridge/Editor/MCPForUnityBridge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using MCPForUnity.Editor.Models;
using MCPForUnity.Editor.Tools;
using MCPForUnity.Editor.Tools.MenuItems;
using MCPForUnity.Editor.Tools.Prefabs;

namespace MCPForUnity.Editor
{
Expand Down Expand Up @@ -1055,6 +1056,7 @@ private static string ExecuteCommand(Command command)
"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}"
),
Expand Down
Loading
Loading