Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8b10c6f
feat: Add prefab read operations (get_info, get_hierarchy, list_prefabs)
whatevertogo Jan 22, 2026
d0cb1ba
fix: Use correct API to save prefab stage changes
whatevertogo Jan 22, 2026
94a78ec
refactor: improve code quality and error handling
whatevertogo Jan 22, 2026
8493558
feat: Remove list_prefabs action and update related documentation
whatevertogo Jan 22, 2026
5859fec
feat: Enhance prefab management with detailed parameter descriptions …
whatevertogo Jan 22, 2026
6a59050
feat: Simplify prefab creation logic and unify logging for asset repl…
whatevertogo Jan 22, 2026
943e7ad
feat: Update SaveStagePrefab method to use SetDirty and SaveAssets fo…
whatevertogo Jan 22, 2026
228f9fb
feat: Add PrefabUtilityHelper class with utility methods for prefab a…
whatevertogo Jan 22, 2026
2e61b21
feat: Refactor action constants and enhance parameter validation in p…
whatevertogo Jan 22, 2026
aac6ec8
feat: Update ValidateSourceObjectForPrefab method to remove replaceEx…
whatevertogo Jan 22, 2026
f44e1b3
fix: Fix searchInactive parameter and improve prefab management
whatevertogo Jan 22, 2026
14e3847
feat: Add path validation and security checks for prefab operations
whatevertogo Jan 23, 2026
c21c860
feat: Remove pagination from GetHierarchy method and simplify prefab …
whatevertogo Jan 23, 2026
40de0d0
feat: Remove mode parameter from prefab management functions to simpl…
whatevertogo Jan 23, 2026
72de29d
fix: Improve path validation and replace logic in prefab management
whatevertogo Jan 23, 2026
d0c7135
feat: Enhance prefab management by adding nesting depth and parent pr…
whatevertogo Jan 23, 2026
aa8d358
Merge branch 'CoplayDev:beta' into enhancement/Prefab-management
whatevertogo Jan 23, 2026
b74cc00
fix: resolve Unknown pseudo class last-child USS warnings
dsarno Jan 25, 2026
f0a2cc5
Merge branch 'enhancement/Prefab-management' of https://github.com/wh…
dsarno Jan 25, 2026
7c8d478
fix: improve prefab stage save for automated workflows
dsarno Jan 25, 2026
b1471de
Merge branch 'CoplayDev:beta' into enhancement/Prefab-management
whatevertogo Jan 25, 2026
56a8d23
Merge pull request #1 from dsarno/fix/prefab-save-dialogs-and-force-p…
whatevertogo Jan 25, 2026
2ccf8f1
Update prefab.py
whatevertogo Jan 25, 2026
e4d3c8c
refactor: remove unnecessary blank line before create function
whatevertogo Jan 25, 2026
f752290
feat: add info and hierarchy commands to prefab CLI for enhanced pref…
whatevertogo Jan 25, 2026
1bcc6e1
feat: enhance prefab management with comprehensive CRUD tests and ens…
whatevertogo Jan 25, 2026
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
49 changes: 49 additions & 0 deletions MCPForUnity/Editor/Helpers/AssetPathUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public static string NormalizeSeparators(string path)

/// <summary>
/// Normalizes a Unity asset path by ensuring forward slashes are used and that it is rooted under "Assets/".
/// Also protects against path traversal attacks using "../" sequences.
/// </summary>
public static string SanitizeAssetPath(string path)
{
Expand All @@ -36,6 +37,15 @@ public static string SanitizeAssetPath(string path)
}

path = NormalizeSeparators(path);

// Check for path traversal sequences
if (path.Contains(".."))
{
McpLog.Warn($"[AssetPathUtility] Path contains potential traversal sequence: '{path}'");
return null;
}

// Ensure path starts with Assets/
if (!path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
return "Assets/" + path.TrimStart('/');
Expand All @@ -44,6 +54,45 @@ public static string SanitizeAssetPath(string path)
return path;
}

/// <summary>
/// Checks if a given asset path is valid and safe (no traversal, within Assets folder).
/// </summary>
/// <returns>True if the path is valid, false otherwise.</returns>
public static bool IsValidAssetPath(string path)
{
if (string.IsNullOrEmpty(path))
{
return false;
}

// Normalize for comparison
string normalized = NormalizeSeparators(path);

// Must start with Assets/
if (!normalized.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
return false;
}

// Must not contain traversal sequences
if (normalized.Contains(".."))
{
return false;
}

// Must not contain invalid path characters
char[] invalidChars = { ':', '*', '?', '"', '<', '>', '|' };
foreach (char c in invalidChars)
{
if (normalized.IndexOf(c) >= 0)
{
return false;
}
}

return true;
}

/// <summary>
/// Gets the MCP for Unity package root path.
/// Works for registry Package Manager, local Package Manager, and Asset Store installations.
Expand Down
228 changes: 228 additions & 0 deletions MCPForUnity/Editor/Helpers/PrefabUtilityHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Provides common utility methods for working with Unity Prefab assets.
/// </summary>
public static class PrefabUtilityHelper
{
/// <summary>
/// Gets the GUID for a prefab asset path.
/// </summary>
/// <param name="assetPath">The Unity asset path (e.g., "Assets/Prefabs/MyPrefab.prefab")</param>
/// <returns>The GUID string, or null if the path is invalid.</returns>
public static string GetPrefabGUID(string assetPath)
{
if (string.IsNullOrEmpty(assetPath))
{
return null;
}

try
{
return AssetDatabase.AssetPathToGUID(assetPath);
}
catch (Exception ex)
{
McpLog.Warn($"Failed to get GUID for asset path '{assetPath}': {ex.Message}");
return null;
}
}

/// <summary>
/// Gets variant information if the prefab is a variant.
/// </summary>
/// <param name="prefabAsset">The prefab GameObject to check.</param>
/// <returns>A tuple containing (isVariant, parentPath, parentGuid).</returns>
public static (bool isVariant, string parentPath, string parentGuid) GetVariantInfo(GameObject prefabAsset)
{
if (prefabAsset == null)
{
return (false, null, null);
}

try
{
PrefabAssetType assetType = PrefabUtility.GetPrefabAssetType(prefabAsset);
if (assetType != PrefabAssetType.Variant)
{
return (false, null, null);
}

GameObject parentAsset = PrefabUtility.GetCorrespondingObjectFromSource(prefabAsset);
if (parentAsset == null)
{
return (true, null, null);
}

string parentPath = AssetDatabase.GetAssetPath(parentAsset);
string parentGuid = GetPrefabGUID(parentPath);

return (true, parentPath, parentGuid);
}
catch (Exception ex)
{
McpLog.Warn($"Failed to get variant info for '{prefabAsset.name}': {ex.Message}");
return (false, null, null);
}
}

/// <summary>
/// Gets the list of component type names on a GameObject.
/// </summary>
/// <param name="obj">The GameObject to inspect.</param>
/// <returns>A list of component type full names.</returns>
public static List<string> GetComponentTypeNames(GameObject obj)
{
var typeNames = new List<string>();

if (obj == null)
{
return typeNames;
}

try
{
var components = obj.GetComponents<Component>();
foreach (var component in components)
{
if (component != null)
{
typeNames.Add(component.GetType().FullName);
}
}
}
catch (Exception ex)
{
McpLog.Warn($"Failed to get component types for '{obj.name}': {ex.Message}");
}

return typeNames;
}

/// <summary>
/// Recursively counts all children in the hierarchy.
/// </summary>
/// <param name="transform">The root transform to count from.</param>
/// <returns>Total number of children in the hierarchy.</returns>
public static int CountChildrenRecursive(Transform transform)
{
if (transform == null)
{
return 0;
}

int count = transform.childCount;
for (int i = 0; i < transform.childCount; i++)
{
count += CountChildrenRecursive(transform.GetChild(i));
}
return count;
}

/// <summary>
/// Gets the source prefab path for a nested prefab instance.
/// </summary>
/// <param name="gameObject">The GameObject to check.</param>
/// <returns>The asset path of the source prefab, or null if not a nested prefab.</returns>
public static string GetNestedPrefabPath(GameObject gameObject)
{
if (gameObject == null || !PrefabUtility.IsAnyPrefabInstanceRoot(gameObject))
{
return null;
}

try
{
var sourcePrefab = PrefabUtility.GetCorrespondingObjectFromSource(gameObject);
if (sourcePrefab != null)
{
return AssetDatabase.GetAssetPath(sourcePrefab);
}
}
catch (Exception ex)
{
McpLog.Warn($"Failed to get nested prefab path for '{gameObject.name}': {ex.Message}");
}

return null;
}

/// <summary>
/// Gets the nesting depth of a prefab instance within the prefab hierarchy.
/// Returns 0 for main prefab root, 1 for first-level nested, 2 for second-level, etc.
/// Returns -1 for non-prefab-root objects.
/// </summary>
/// <param name="gameObject">The GameObject to analyze.</param>
/// <param name="mainPrefabRoot">The root transform of the main prefab asset.</param>
/// <returns>Nesting depth (0=main root, 1+=nested), or -1 if not a prefab root.</returns>
public static int GetPrefabNestingDepth(GameObject gameObject, Transform mainPrefabRoot)
{
if (gameObject == null)
return -1;

// Main prefab root
if (gameObject.transform == mainPrefabRoot)
return 0;

// Not a prefab instance root
if (!PrefabUtility.IsAnyPrefabInstanceRoot(gameObject))
return -1;

// Calculate depth by walking up the hierarchy
int depth = 0;
Transform current = gameObject.transform;

while (current != null && current != mainPrefabRoot)
{
if (PrefabUtility.IsAnyPrefabInstanceRoot(current.gameObject))
{
depth++;
}
current = current.parent;
}

return depth;
}

/// <summary>
/// Gets the parent prefab path for a nested prefab instance.
/// Returns null for main prefab root or non-prefab objects.
/// </summary>
/// <param name="gameObject">The GameObject to analyze.</param>
/// <param name="mainPrefabRoot">The root transform of the main prefab asset.</param>
/// <returns>The asset path of the parent prefab, or null if none.</returns>
public static string GetParentPrefabPath(GameObject gameObject, Transform mainPrefabRoot)
{
if (gameObject == null || gameObject.transform == mainPrefabRoot)
return null;

if (!PrefabUtility.IsAnyPrefabInstanceRoot(gameObject))
return null;

// Walk up the hierarchy to find the parent prefab instance
Transform current = gameObject.transform.parent;

while (current != null && current != mainPrefabRoot)
{
if (PrefabUtility.IsAnyPrefabInstanceRoot(current.gameObject))
{
return GetNestedPrefabPath(current.gameObject);
}
current = current.parent;
}

// Parent is the main prefab root - get its asset path
if (mainPrefabRoot != null)
{
return AssetDatabase.GetAssetPath(mainPrefabRoot.gameObject);
}

return null;
}
}
}
13 changes: 13 additions & 0 deletions MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEditorInternal;
using UnityEngine;

Expand Down Expand Up @@ -239,6 +240,18 @@ internal static object Handle(JObject @params, JToken targetToken, string search
}

EditorUtility.SetDirty(targetGo);

// Mark the appropriate scene as dirty (handles both regular scenes and prefab stages)
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
if (prefabStage != null)
{
EditorSceneManager.MarkSceneDirty(prefabStage.scene);
}
else
{
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
}
Comment on lines 242 to +253
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs | head -300

Repository: CoplayDev/unity-mcp

Length of output: 12509


🏁 Script executed:

find . -name "*.cs" -type f | xargs grep -l "ManageGameObjectCommon" | head -5

Repository: CoplayDev/unity-mcp

Length of output: 9166


🏁 Script executed:

cat -n MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs

Repository: CoplayDev/unity-mcp

Length of output: 11621


🏁 Script executed:

find . -name "GameObjectLookup.cs" -type f

Repository: CoplayDev/unity-mcp

Length of output: 111


🏁 Script executed:

cat -n MCPForUnity/Editor/Helpers/GameObjectLookup.cs

Repository: CoplayDev/unity-mcp

Length of output: 16010


Mark the GameObject's actual scene dirty, not always the active scene.

When searching by component (by_component), the target object can be found in any loaded scene via FindObjectsByType/FindObjectsOfType. Marking only the active scene leaves modified GameObjects in additive scenes unmarked, risking lost changes. Use targetGo.scene when valid.

🐛 Proposed fix
-            EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
+            var sceneToDirty = targetGo.scene.IsValid()
+                ? targetGo.scene
+                : EditorSceneManager.GetActiveScene();
+            EditorSceneManager.MarkSceneDirty(sceneToDirty);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
EditorUtility.SetDirty(targetGo);
// Mark the appropriate scene as dirty (handles both regular scenes and prefab stages)
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
if (prefabStage != null)
{
EditorSceneManager.MarkSceneDirty(prefabStage.scene);
}
else
{
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
}
EditorUtility.SetDirty(targetGo);
// Mark the appropriate scene as dirty (handles both regular scenes and prefab stages)
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
if (prefabStage != null)
{
EditorSceneManager.MarkSceneDirty(prefabStage.scene);
}
else
{
var sceneToDirty = targetGo.scene.IsValid()
? targetGo.scene
: EditorSceneManager.GetActiveScene();
EditorSceneManager.MarkSceneDirty(sceneToDirty);
}
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs` around lines 242 -
253, The code always marks the active scene dirty which misses modified
GameObjects in non-active (additive) scenes; update the logic after
EditorUtility.SetDirty(targetGo) to, when not in a prefab stage, use the target
GameObject's scene (targetGo.scene) if it is valid and loaded instead of
unconditionally calling EditorSceneManager.GetActiveScene(); call
EditorSceneManager.MarkSceneDirty(targetGo.scene) when targetGo.scene.IsValid()
(fall back to GetActiveScene() only if the target scene is invalid).


return new SuccessResponse(
$"GameObject '{targetGo.name}' modified successfully.",
Helpers.GameObjectSerializer.GetGameObjectData(targetGo)
Expand Down
Loading