Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 36 additions & 0 deletions MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,42 @@ internal static object Handle(JObject @params, JToken targetToken, string search
string name = @params["name"]?.ToString();
if (!string.IsNullOrEmpty(name) && targetGo.name != name)
{
// Check if we're renaming the root object of an open prefab stage
var prefabStageForRename = PrefabStageUtility.GetCurrentPrefabStage();
bool isRenamingPrefabRoot = prefabStageForRename != null &&
prefabStageForRename.prefabContentsRoot == targetGo;

if (isRenamingPrefabRoot)
{
// Rename the prefab asset file to match the new name (avoids Unity dialog)
string assetPath = prefabStageForRename.assetPath;
string directory = System.IO.Path.GetDirectoryName(assetPath);
string newAssetPath = System.IO.Path.Combine(directory, name + ".prefab").Replace('\\', '/');

// Only rename if the path actually changes
if (newAssetPath != assetPath)
{
// Check for collision using GUID comparison
string currentGuid = AssetDatabase.AssetPathToGUID(assetPath);
string existingGuid = AssetDatabase.AssetPathToGUID(newAssetPath);

// Collision only if there's a different asset at the new path
if (!string.IsNullOrEmpty(existingGuid) && existingGuid != currentGuid)
{
return new ErrorResponse($"Cannot rename prefab root to '{name}': a prefab already exists at '{newAssetPath}'.");
}

// Rename the asset file
string renameError = AssetDatabase.RenameAsset(assetPath, name);
if (!string.IsNullOrEmpty(renameError))
{
return new ErrorResponse($"Failed to rename prefab asset: {renameError}");
}

McpLog.Info($"[GameObjectModify] Renamed prefab asset from '{assetPath}' to '{newAssetPath}'");
}
}

targetGo.name = name;
modified = true;
}
Expand Down
21 changes: 21 additions & 0 deletions MCPForUnity/Editor/Tools/ManageComponents.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 UnityEngine;

namespace MCPForUnity.Editor.Tools
Expand Down Expand Up @@ -103,6 +104,7 @@ private static object AddComponent(JObject @params, JToken targetToken, string s
}

EditorUtility.SetDirty(targetGo);
MarkOwningSceneDirty(targetGo);

return new
{
Expand Down Expand Up @@ -146,6 +148,7 @@ private static object RemoveComponent(JObject @params, JToken targetToken, strin
}

EditorUtility.SetDirty(targetGo);
MarkOwningSceneDirty(targetGo);

return new
{
Expand Down Expand Up @@ -227,6 +230,7 @@ private static object SetProperty(JObject @params, JToken targetToken, string se
}

EditorUtility.SetDirty(component);
MarkOwningSceneDirty(targetGo);

if (errors.Count > 0)
{
Expand Down Expand Up @@ -262,6 +266,23 @@ private static object SetProperty(JObject @params, JToken targetToken, string se

#region Helpers

/// <summary>
/// Marks the appropriate scene as dirty for the given GameObject.
/// Handles both regular scenes and prefab stages.
/// </summary>
private static void MarkOwningSceneDirty(GameObject targetGo)
{
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
if (prefabStage != null)
{
EditorSceneManager.MarkSceneDirty(prefabStage.scene);
}
else
{
EditorSceneManager.MarkSceneDirty(targetGo.scene);
}
}

private static GameObject FindTarget(JToken targetToken, string searchMethod)
{
if (targetToken == null)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ openupm add com.coplaydev.unity-mcp
`manage_asset` • `manage_editor` • `manage_gameobject` • `manage_components` • `manage_material` • `manage_prefabs` • `manage_scene` • `manage_script` • `manage_scriptable_object` • `manage_shader` • `manage_vfx` • `manage_texture` • `batch_execute` • `find_gameobjects` • `find_in_file` • `read_console` • `refresh_unity` • `run_tests` • `get_test_job` • `execute_menu_item` • `apply_text_edits` • `script_apply_edits` • `validate_script` • `create_script` • `delete_script` • `get_sha`

### Available Resources
`custom_tools` • `unity_instances` • `menu_items` • `get_tests` • `gameobject` • `gameobject_components` • `editor_state` • `editor_selection` • `editor_prefab_stage` • `project_info` • `project_tags` • `project_layers`
`custom_tools` • `unity_instances` • `menu_items` • `get_tests` • `gameobject` • `gameobject_components` • `prefab_api` • `prefab_info` • `prefab_hierarchy` • `editor_state` • `editor_selection` • `editor_prefab_stage` • `project_info` • `project_tags` • `project_layers`

**Performance Tip:** Use `batch_execute` for multiple operations — it's 10-100x faster than individual calls!
</details>
Expand Down
191 changes: 191 additions & 0 deletions Server/src/services/resources/prefab.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
"""
MCP Resources for reading Prefab data from Unity.

These resources provide read-only access to:
- Prefab info by asset path (mcpforunity://prefab/{path})
- Prefab hierarchy by asset path (mcpforunity://prefab/{path}/hierarchy)
- Currently open prefab stage (mcpforunity://editor/prefab-stage - see prefab_stage.py)
"""
from typing import Any
from urllib.parse import unquote
from pydantic import BaseModel
from fastmcp import Context

from models import MCPResponse
from services.registry import mcp_for_unity_resource
from services.tools import get_unity_instance_from_context
from transport.unity_transport import send_with_unity_instance
from transport.legacy.unity_connection import async_send_command_with_retry


def _normalize_response(response: dict | MCPResponse | Any) -> MCPResponse:
"""Normalize Unity transport response to MCPResponse."""
if isinstance(response, dict):
return MCPResponse(**response)
if isinstance(response, MCPResponse):
return response
# Fallback: wrap unexpected types in an error response
return MCPResponse(success=False, error=f"Unexpected response type: {type(response).__name__}")


def _decode_prefab_path(encoded_path: str) -> str:
"""
Decode a URL-encoded prefab path.
Handles paths like 'Assets%2FPrefabs%2FMyPrefab.prefab' -> 'Assets/Prefabs/MyPrefab.prefab'
"""
return unquote(encoded_path)


# =============================================================================
# Static Helper Resource (shows in UI)
# =============================================================================

@mcp_for_unity_resource(
uri="mcpforunity://prefab-api",
name="prefab_api",
description="Documentation for Prefab resources. Use manage_asset action=search filterType=Prefab to find prefabs, then access resources below."
)
async def get_prefab_api_docs(_ctx: Context) -> MCPResponse:
"""
Returns documentation for the Prefab resource API.

This is a helper resource that explains how to use the parameterized
Prefab resources which require an asset path.
"""
docs = {
"overview": "Prefab resources provide read-only access to Unity prefab assets.",
"workflow": [
"1. Use manage_asset action=search filterType=Prefab to find prefabs",
"2. Use the asset path to access detailed data via resources below",
"3. Use manage_prefabs tool for prefab stage operations (open, save, close)"
],
"path_encoding": {
"note": "Prefab paths must be URL-encoded when used in resource URIs",
"example": "Assets/Prefabs/MyPrefab.prefab -> Assets%2FPrefabs%2FMyPrefab.prefab"
},
"resources": {
"mcpforunity://prefab/{encoded_path}": {
"description": "Get prefab asset info (type, root name, components, variant info)",
"example": "mcpforunity://prefab/Assets%2FPrefabs%2FPlayer.prefab",
"returns": ["assetPath", "guid", "prefabType", "rootObjectName", "rootComponentTypes", "childCount", "isVariant", "parentPrefab"]
},
"mcpforunity://prefab/{encoded_path}/hierarchy": {
"description": "Get full prefab hierarchy with nested prefab information",
"example": "mcpforunity://prefab/Assets%2FPrefabs%2FPlayer.prefab/hierarchy",
"returns": ["prefabPath", "total", "items (with name, instanceId, path, componentTypes, prefab nesting info)"]
},
"mcpforunity://editor/prefab-stage": {
"description": "Get info about the currently open prefab stage (if any)",
"returns": ["isOpen", "assetPath", "prefabRootName", "mode", "isDirty"]
}
},
"related_tools": {
"manage_prefabs": "Open/close prefab stages, save changes, create prefabs from GameObjects",
"manage_asset": "Search for prefab assets, get asset info",
"manage_gameobject": "Modify GameObjects in open prefab stage",
"manage_components": "Add/remove/modify components on prefab GameObjects"
}
}
return MCPResponse(success=True, data=docs)


# =============================================================================
# Prefab Info Resource
# =============================================================================

# TODO: Use these typed response classes for better type safety once
# we update the endpoints to validate response structure more strictly.


class PrefabInfoData(BaseModel):
"""Data for a prefab asset."""
assetPath: str
guid: str = ""
prefabType: str = "Regular"
rootObjectName: str = ""
rootComponentTypes: list[str] = []
childCount: int = 0
isVariant: bool = False
parentPrefab: str | None = None


class PrefabInfoResponse(MCPResponse):
"""Response containing prefab info data."""
data: PrefabInfoData | None = None


@mcp_for_unity_resource(
uri="mcpforunity://prefab/{encoded_path}",
name="prefab_info",
description="Get detailed information about a prefab asset by URL-encoded path. Returns prefab type, root object name, component types, child count, and variant info."
)
async def get_prefab_info(ctx: Context, encoded_path: str) -> MCPResponse:
"""Get prefab asset info by path."""
unity_instance = get_unity_instance_from_context(ctx)

# Decode the URL-encoded path
decoded_path = _decode_prefab_path(encoded_path)

response = await send_with_unity_instance(
async_send_command_with_retry,
unity_instance,
"manage_prefabs",
{
"action": "get_info",
"prefabPath": decoded_path
}
)

return _normalize_response(response)


# =============================================================================
# Prefab Hierarchy Resource
# =============================================================================

class PrefabHierarchyItem(BaseModel):
"""Single item in prefab hierarchy."""
name: str
instanceId: int
path: str
activeSelf: bool = True
childCount: int = 0
componentTypes: list[str] = []
prefab: dict[str, Any] = {}


class PrefabHierarchyData(BaseModel):
"""Data for prefab hierarchy."""
prefabPath: str
total: int = 0
items: list[PrefabHierarchyItem] = []


class PrefabHierarchyResponse(MCPResponse):
"""Response containing prefab hierarchy data."""
data: PrefabHierarchyData | None = None


@mcp_for_unity_resource(
uri="mcpforunity://prefab/{encoded_path}/hierarchy",
name="prefab_hierarchy",
description="Get the full hierarchy of a prefab with nested prefab information. Returns all GameObjects with their components and nesting depth."
)
async def get_prefab_hierarchy(ctx: Context, encoded_path: str) -> MCPResponse:
"""Get prefab hierarchy by path."""
unity_instance = get_unity_instance_from_context(ctx)

# Decode the URL-encoded path
decoded_path = _decode_prefab_path(encoded_path)

response = await send_with_unity_instance(
async_send_command_with_retry,
unity_instance,
"manage_prefabs",
{
"action": "get_hierarchy",
"prefabPath": decoded_path
}
)

return _normalize_response(response)
Original file line number Diff line number Diff line change
Expand Up @@ -423,9 +423,6 @@ public void GetHierarchy_IncludesNestingInfo_ForNestedPrefabs()

AssetDatabase.Refresh();

// Expect the nested prefab warning due to test environment
LogAssert.Expect(UnityEngine.LogType.Error, new Regex("Nested Prefab problem"));

var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
{
["action"] = "get_hierarchy",
Expand All @@ -444,10 +441,10 @@ public void GetHierarchy_IncludesNestingInfo_ForNestedPrefabs()
}
finally
{
// Delete nested container first (before deleting prefabs it references)
SafeDeleteAsset(Path.Combine(TempDirectory, "NestedContainer.prefab").Replace('\\', '/'));
SafeDeleteAsset(parentPath);
SafeDeleteAsset(Path.Combine(TempDirectory, "ParentPrefab.prefab").Replace('\\', '/'));
SafeDeleteAsset(Path.Combine(TempDirectory, "ChildPrefab.prefab").Replace('\\', '/'));
SafeDeleteAsset(Path.Combine(TempDirectory, "NestedContainer.prefab").Replace('\\', '/'));
}
}

Expand Down