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"
|
||||
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
|
||||
env:
|
||||
NEW_VERSION: ${{ steps.compute.outputs.new_version }}
|
||||
|
|
@ -78,7 +81,7 @@ jobs:
|
|||
set -euo pipefail
|
||||
git config user.name "GitHub Actions"
|
||||
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
|
||||
echo "No version changes to commit."
|
||||
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.Tools;
|
||||
using MCPForUnity.Editor.Tools.MenuItems;
|
||||
using MCPForUnity.Editor.Tools.Prefabs;
|
||||
|
||||
namespace MCPForUnity.Editor
|
||||
{
|
||||
|
|
@ -1040,7 +1041,26 @@ namespace MCPForUnity.Editor
|
|||
// Use JObject for parameters as the new handlers likely expect this
|
||||
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
|
||||
var response = new { status = "success", result };
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ namespace MCPForUnity.Editor.Tools
|
|||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return Response.Error("'path' is required for reimport.");
|
||||
string fullPath = SanitizeAssetPath(path);
|
||||
string fullPath = AssetPathUtility.SanitizeAssetPath(path);
|
||||
if (!AssetExists(fullPath))
|
||||
return Response.Error($"Asset not found at path: {fullPath}");
|
||||
|
||||
|
|
@ -154,7 +154,7 @@ namespace MCPForUnity.Editor.Tools
|
|||
if (string.IsNullOrEmpty(assetType))
|
||||
return Response.Error("'assetType' is required for create.");
|
||||
|
||||
string fullPath = SanitizeAssetPath(path);
|
||||
string fullPath = AssetPathUtility.SanitizeAssetPath(path);
|
||||
string directory = Path.GetDirectoryName(fullPath);
|
||||
|
||||
// Ensure directory exists
|
||||
|
|
@ -280,7 +280,7 @@ namespace MCPForUnity.Editor.Tools
|
|||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return Response.Error("'path' is required for create_folder.");
|
||||
string fullPath = SanitizeAssetPath(path);
|
||||
string fullPath = AssetPathUtility.SanitizeAssetPath(path);
|
||||
string parentDir = Path.GetDirectoryName(fullPath);
|
||||
string folderName = Path.GetFileName(fullPath);
|
||||
|
||||
|
|
@ -338,7 +338,7 @@ namespace MCPForUnity.Editor.Tools
|
|||
if (properties == null || !properties.HasValues)
|
||||
return Response.Error("'properties' are required for modify.");
|
||||
|
||||
string fullPath = SanitizeAssetPath(path);
|
||||
string fullPath = AssetPathUtility.SanitizeAssetPath(path);
|
||||
if (!AssetExists(fullPath))
|
||||
return Response.Error($"Asset not found at path: {fullPath}");
|
||||
|
||||
|
|
@ -495,7 +495,7 @@ namespace MCPForUnity.Editor.Tools
|
|||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return Response.Error("'path' is required for delete.");
|
||||
string fullPath = SanitizeAssetPath(path);
|
||||
string fullPath = AssetPathUtility.SanitizeAssetPath(path);
|
||||
if (!AssetExists(fullPath))
|
||||
return Response.Error($"Asset not found at path: {fullPath}");
|
||||
|
||||
|
|
@ -526,7 +526,7 @@ namespace MCPForUnity.Editor.Tools
|
|||
if (string.IsNullOrEmpty(path))
|
||||
return Response.Error("'path' is required for duplicate.");
|
||||
|
||||
string sourcePath = SanitizeAssetPath(path);
|
||||
string sourcePath = AssetPathUtility.SanitizeAssetPath(path);
|
||||
if (!AssetExists(sourcePath))
|
||||
return Response.Error($"Source asset not found at path: {sourcePath}");
|
||||
|
||||
|
|
@ -538,7 +538,7 @@ namespace MCPForUnity.Editor.Tools
|
|||
}
|
||||
else
|
||||
{
|
||||
destPath = SanitizeAssetPath(destinationPath);
|
||||
destPath = AssetPathUtility.SanitizeAssetPath(destinationPath);
|
||||
if (AssetExists(destPath))
|
||||
return Response.Error($"Asset already exists at destination path: {destPath}");
|
||||
// Ensure destination directory exists
|
||||
|
|
@ -576,8 +576,8 @@ namespace MCPForUnity.Editor.Tools
|
|||
if (string.IsNullOrEmpty(destinationPath))
|
||||
return Response.Error("'destination' path is required for move/rename.");
|
||||
|
||||
string sourcePath = SanitizeAssetPath(path);
|
||||
string destPath = SanitizeAssetPath(destinationPath);
|
||||
string sourcePath = AssetPathUtility.SanitizeAssetPath(path);
|
||||
string destPath = AssetPathUtility.SanitizeAssetPath(destinationPath);
|
||||
|
||||
if (!AssetExists(sourcePath))
|
||||
return Response.Error($"Source asset not found at path: {sourcePath}");
|
||||
|
|
@ -642,7 +642,7 @@ namespace MCPForUnity.Editor.Tools
|
|||
string[] folderScope = null;
|
||||
if (!string.IsNullOrEmpty(pathScope))
|
||||
{
|
||||
folderScope = new string[] { SanitizeAssetPath(pathScope) };
|
||||
folderScope = new string[] { AssetPathUtility.SanitizeAssetPath(pathScope) };
|
||||
if (!AssetDatabase.IsValidFolder(folderScope[0]))
|
||||
{
|
||||
// Maybe the user provided a file path instead of a folder?
|
||||
|
|
@ -732,7 +732,7 @@ namespace MCPForUnity.Editor.Tools
|
|||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return Response.Error("'path' is required for get_info.");
|
||||
string fullPath = SanitizeAssetPath(path);
|
||||
string fullPath = AssetPathUtility.SanitizeAssetPath(path);
|
||||
if (!AssetExists(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.");
|
||||
|
||||
// 2. Sanitize and check existence
|
||||
string fullPath = SanitizeAssetPath(path);
|
||||
string fullPath = AssetPathUtility.SanitizeAssetPath(path);
|
||||
if (!AssetExists(fullPath))
|
||||
return Response.Error($"Asset not found at path: {fullPath}");
|
||||
|
||||
|
|
@ -829,18 +829,6 @@ namespace MCPForUnity.Editor.Tools
|
|||
/// <summary>
|
||||
/// Ensures the asset path starts with "Assets/".
|
||||
/// </summary>
|
||||
private static string SanitizeAssetPath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return path;
|
||||
path = path.Replace('\\', '/'); // Normalize separators
|
||||
if (!path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "Assets/" + path.TrimStart('/');
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an asset exists at the given path (file or folder).
|
||||
/// </summary>
|
||||
|
|
@ -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";
|
||||
try {
|
||||
try
|
||||
{
|
||||
if (colorArr.Count >= 3)
|
||||
{
|
||||
Color newColor = new Color(
|
||||
|
|
@ -949,7 +939,8 @@ namespace MCPForUnity.Editor.Tools
|
|||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"Error parsing color property '{propName}': {ex.Message}"
|
||||
);
|
||||
|
|
@ -989,7 +980,7 @@ namespace MCPForUnity.Editor.Tools
|
|||
if (!string.IsNullOrEmpty(texPath))
|
||||
{
|
||||
Texture newTex = AssetDatabase.LoadAssetAtPath<Texture>(
|
||||
SanitizeAssetPath(texPath)
|
||||
AssetPathUtility.SanitizeAssetPath(texPath)
|
||||
);
|
||||
if (
|
||||
newTex != null
|
||||
|
|
@ -1217,7 +1208,7 @@ namespace MCPForUnity.Editor.Tools
|
|||
&& token.Type == JTokenType.String
|
||||
)
|
||||
{
|
||||
string assetPath = SanitizeAssetPath(token.ToString());
|
||||
string assetPath = AssetPathUtility.SanitizeAssetPath(token.ToString());
|
||||
UnityEngine.Object loadedAsset = AssetDatabase.LoadAssetAtPath(
|
||||
assetPath,
|
||||
targetType
|
||||
|
|
@ -1337,4 +1328,3 @@ namespace MCPForUnity.Editor.Tools
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@ using System.IO;
|
|||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal; // Required for tag management
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Helpers; // For Response class
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools
|
||||
{
|
||||
|
|
@ -98,6 +99,8 @@ namespace MCPForUnity.Editor.Tools
|
|||
return GetActiveTool();
|
||||
case "get_selection":
|
||||
return GetSelection();
|
||||
case "get_prefab_stage":
|
||||
return GetPrefabStageInfo();
|
||||
case "set_active_tool":
|
||||
string toolName = @params["toolName"]?.ToString();
|
||||
if (string.IsNullOrEmpty(toolName))
|
||||
|
|
@ -140,7 +143,7 @@ namespace MCPForUnity.Editor.Tools
|
|||
|
||||
default:
|
||||
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()
|
||||
{
|
||||
try
|
||||
|
|
@ -610,4 +642,3 @@ namespace MCPForUnity.Editor.Tools
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,9 @@
|
|||
using System;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.MenuItems
|
||||
|
|
@ -35,25 +34,14 @@ namespace MCPForUnity.Editor.Tools.MenuItems
|
|||
return Response.Error($"Execution of menu item '{menuPath}' is blocked for safety reasons.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Execute on main thread using delayCall
|
||||
EditorApplication.delayCall += () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
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.");
|
||||
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.");
|
||||
}
|
||||
}
|
||||
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.");
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
|
||||
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_gameobject import register_manage_gameobject_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 .read_console import register_read_console_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_gameobject_tools(mcp)
|
||||
register_manage_asset_tools(mcp)
|
||||
register_manage_prefabs_tools(mcp)
|
||||
register_manage_shader_tools(mcp)
|
||||
register_read_console_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):
|
||||
"""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")
|
||||
async def manage_menu_item(
|
||||
ctx: Context,
|
||||
|
|
@ -25,18 +25,6 @@ def register_manage_menu_item_tools(mcp: FastMCP):
|
|||
refresh: Annotated[bool | None,
|
||||
"Optional flag to force refresh of the menu cache when listing"] = None,
|
||||
) -> 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
|
||||
params_dict: dict[str, Any] = {
|
||||
"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