Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
76 changes: 48 additions & 28 deletions Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Assets.Copy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
*/

#nullable enable
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using com.IvanMurzak.McpPlugin;
using com.IvanMurzak.ReflectorNet.Utils;
using com.IvanMurzak.Unity.MCP.Runtime.Data;
using UnityEditor;

namespace com.IvanMurzak.Unity.MCP.Editor.API
Expand All @@ -21,46 +22,65 @@ public partial class Tool_Assets
{
[McpPluginTool
(
"Assets_Copy",
Title = "Assets Copy"
"assets-copy",
Title = "Assets / Copy"
)]
[Description(@"Copy the asset at path and stores it at newPath. Does AssetDatabase.Refresh() at the end.")]
public string Copy
public CopyAssetsResponse Copy
(
[Description("The paths of the asset to copy.")]
string[] sourcePaths,
[Description("The paths to store the copied asset.")]
string[] destinationPaths
)
=> MainThread.Instance.Run(() =>
{
if (sourcePaths.Length == 0)
return Error.SourcePathsArrayIsEmpty();

if (sourcePaths.Length != destinationPaths.Length)
return Error.SourceAndDestinationPathsArrayMustBeOfTheSameLength();
return MainThread.Instance.Run(() =>
{
if (sourcePaths.Length == 0)
throw new System.Exception(Error.SourcePathsArrayIsEmpty());

var stringBuilder = new StringBuilder();
if (sourcePaths.Length != destinationPaths.Length)
throw new System.Exception(Error.SourceAndDestinationPathsArrayMustBeOfTheSameLength());

for (var i = 0; i < sourcePaths.Length; i++)
{
var sourcePath = sourcePaths[i];
var destinationPath = destinationPaths[i];
var response = new CopyAssetsResponse();

if (string.IsNullOrEmpty(sourcePath) || string.IsNullOrEmpty(destinationPath))
for (var i = 0; i < sourcePaths.Length; i++)
{
stringBuilder.AppendLine(Error.SourceOrDestinationPathIsEmpty());
continue;
}
if (!AssetDatabase.CopyAsset(sourcePath, destinationPath))
{
stringBuilder.AppendLine($"[Error] Failed to copy asset from {sourcePath} to {destinationPath}.");
continue;
var sourcePath = sourcePaths[i];
var destinationPath = destinationPaths[i];

if (string.IsNullOrEmpty(sourcePath) || string.IsNullOrEmpty(destinationPath))
{
response.errors ??= new();
response.errors.Add(Error.SourceOrDestinationPathIsEmpty());
continue;
}
if (!AssetDatabase.CopyAsset(sourcePath, destinationPath))
{
response.errors ??= new();
response.errors.Add($"[Error] Failed to copy asset from {sourcePath} to {destinationPath}.");
continue;
}
var newAssetType = AssetDatabase.GetMainAssetTypeAtPath(destinationPath);
var newAsset = AssetDatabase.LoadAssetAtPath(destinationPath, newAssetType);

response.copiedAssets ??= new();
response.copiedAssets?.Add(new AssetObjectRef(newAsset));
}
stringBuilder.AppendLine($"[Success] Copied asset from {sourcePath} to {destinationPath}.");
}
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
return stringBuilder.ToString();
});

AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
UnityEditor.EditorApplication.RepaintProjectWindow();
UnityEditor.EditorApplication.RepaintHierarchyWindow();
UnityEditorInternal.InternalEditorUtility.RepaintAllViews();

return response;
});
}

public class CopyAssetsResponse
{
public List<AssetObjectRef>? copiedAssets;
public List<string>? errors;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ public partial class Tool_Assets
{
[McpPluginTool
(
"Assets_CreateFolders",
Title = "Assets Create Folders"
"assets-createfolder",
Title = "Assets / Create Folder"
)]
[Description(@"Create folders at specific locations in the project.
Use it to organize scripts and assets in the project. Does AssetDatabase.Refresh() at the end.")]
Expand All @@ -31,27 +31,29 @@ public string CreateFolders
[Description("The paths for the folders to create.")]
string[] paths
)
=> MainThread.Instance.Run(() =>
{
if (paths.Length == 0)
return Error.SourcePathsArrayIsEmpty();
return MainThread.Instance.Run(() =>
{
if (paths.Length == 0)
return Error.SourcePathsArrayIsEmpty();

var stringBuilder = new StringBuilder();
var stringBuilder = new StringBuilder();

for (var i = 0; i < paths.Length; i++)
{
if (string.IsNullOrEmpty(paths[i]))
for (var i = 0; i < paths.Length; i++)
{
stringBuilder.AppendLine(Error.SourcePathIsEmpty());
continue;
if (string.IsNullOrEmpty(paths[i]))
{
stringBuilder.AppendLine(Error.SourcePathIsEmpty());
continue;
}
}


#nullable enable
}

AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
return stringBuilder.ToString();
});
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
UnityEditor.EditorApplication.RepaintHierarchyWindow();
UnityEditor.EditorApplication.RepaintProjectWindow();
UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
return stringBuilder.ToString();
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,32 +22,38 @@ public partial class Tool_Assets
{
[McpPluginTool
(
"Assets_Delete",
Title = "Assets Delete"
"assets-delete",
Title = "Assets / Delete"
)]
[Description(@"Delete the assets at paths from the project. Does AssetDatabase.Refresh() at the end.")]
public string Delete
(
[Description("The paths of the assets")]
string[] paths
)
=> MainThread.Instance.Run(() =>
{
if (paths.Length == 0)
return Error.SourcePathsArrayIsEmpty();

var outFailedPaths = new List<string>();
var success = AssetDatabase.DeleteAssets(paths, outFailedPaths);
if (!success)
return MainThread.Instance.Run(() =>
{
var stringBuilder = new StringBuilder();
foreach (var failedPath in outFailedPaths)
stringBuilder.AppendLine($"[Error] Failed to delete asset at {failedPath}.");
return stringBuilder.ToString();
}
if (paths.Length == 0)
return Error.SourcePathsArrayIsEmpty();

var outFailedPaths = new List<string>();
var success = AssetDatabase.DeleteAssets(paths, outFailedPaths);
if (!success)
{
var stringBuilder = new StringBuilder();
foreach (var failedPath in outFailedPaths)
stringBuilder.AppendLine($"[Error] Failed to delete asset at {failedPath}.");
return stringBuilder.ToString();
}

AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
UnityEditor.EditorApplication.RepaintProjectWindow();
UnityEditor.EditorApplication.RepaintHierarchyWindow();
UnityEditorInternal.InternalEditorUtility.RepaintAllViews();

AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
return "[Success] Deleted assets at paths:\n" + string.Join("\n", paths);
});
return "[Success] Deleted assets at paths:\n" + string.Join("\n", paths);
});
}
}
}
75 changes: 38 additions & 37 deletions Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Assets.Find.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,36 @@
*/

#nullable enable
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using com.IvanMurzak.McpPlugin;
using com.IvanMurzak.ReflectorNet.Utils;
using com.IvanMurzak.Unity.MCP.Runtime.Data;
using UnityEditor;

namespace com.IvanMurzak.Unity.MCP.Editor.API
{
using Consts = McpPlugin.Common.Consts;
public partial class Tool_Assets
{
[McpPluginTool
(
"Assets_Find",
Title = "Find assets in the project"
"assets-find",
Title = "Assets / Find"
)]
[Description(@"Search the asset database using the search filter string.
allows you to search for Assets. The string argument can provide names, labels or types (classnames).")]
public List<AssetObjectRef> Find
(
// <ref>https://docs.unity3d.com/ScriptReference/AssetDatabase.FindAssets.html</ref>
[Description(@"The filter string can contain search data. Could be empty.
Name: Filter assets by their filename (without extension). Words separated by whitespace are treated as separate name searches. For example, 'test asset' is a name of an Asset which will be searched for. Note that the name can be used to identify an asset. Further, the name used in the filter string can be specified as a subsection. For example, the 'test asset' example above can be matched using 'test'.
Labels (l:): Assets can have labels attached to them. Assets with particular labels can be found using the keyword 'l:' before each label. This indicates that the string is searching for labels.
Types (t:): Find assets based on explicitly identified types. The keyword 't:' is used as a way to specify that typed assets are being looked for. If more than one type is included in the filter string, then assets that match one class will be returned. Types can either be built-in types such as Texture2D or user-created script classes. User-created classes are assets created from a ScriptableObject class in the project. If all assets are wanted, use Object as all assets derive from Object. Specifying one or more folders using the searchInFolders argument will limit the searching to these folders and their child folders. This is faster than searching all assets in all folders.
AssetBundles (b:): Find assets which are part of an Asset bundle. The keyword 'b:' is used to determine that Asset bundle names should be part of the query.
Area (a:): Find assets in a specific area of a project. Valid values are 'all', 'assets', and 'packages'. Use this to make your query more specific using the 'a:' keyword followed by the area name to speed up searching.
Globbing (glob:): Use globbing to match specific rules. The keyword 'glob:' is followed by the query. For example, 'glob:Editor' will find all Editor folders in a project, 'glob:(Editor|Resources)' will find all Editor and Resources folders in a project, 'glob:Editor/*' will return all Assets inside Editor folders in a project, while 'glob:Editor/**' will return all Assets within Editor folders recursively.

The filter string can include
Available types:
t:AnimationClip
t:AudioClip
Expand All @@ -45,50 +58,38 @@ public partial class Tool_Assets
t:Texture
t:VideoClip
t:VisualEffectAsset
t:VisualEffectSubgraph")]
public string Search
(
// <ref>https://docs.unity3d.com/ScriptReference/AssetDatabase.FindAssets.html</ref>
[Description(@"Searching filter. Could be empty.
Name: Filter assets by their filename (without extension). Words separated by whitespace are treated as separate name searches. For example, 'test asset' is a name of an Asset which will be searched for. Note that the name can be used to identify an asset. Further, the name used in the filter string can be specified as a subsection. For example, the 'test asset' example above can be matched using 'test'.
Labels (l:): Assets can have labels attached to them. Assets with particular labels can be found using the keyword 'l:' before each label. This indicates that the string is searching for labels.
Types (t:): Find assets based on explicitly identified types. The keyword 't:' is used as a way to specify that typed assets are being looked for. If more than one type is included in the filter string, then assets that match one class will be returned. Types can either be built-in types such as Texture2D or user-created script classes. User-created classes are assets created from a ScriptableObject class in the project. If all assets are wanted, use Object as all assets derive from Object. Specifying one or more folders using the searchInFolders argument will limit the searching to these folders and their child folders. This is faster than searching all assets in all folders.
AssetBundles (b:): Find assets which are part of an Asset bundle. The keyword 'b:' is used to determine that Asset bundle names should be part of the query.
Area (a:): Find assets in a specific area of a project. Valid values are 'all', 'assets', and 'packages'. Use this to make your query more specific using the 'a:' keyword followed by the area name to speed up searching.
Globbing (glob:): Use globbing to match specific rules. The keyword 'glob:' is followed by the query. For example, 'glob:Editor' will find all Editor folders in a project, 'glob:(Editor|Resources)' will find all Editor and Resources folders in a project, 'glob:Editor/*' will return all Assets inside Editor folders in a project, while 'glob:Editor/**' will return all Assets within Editor folders recursively.
t:VisualEffectSubgraph

Note:
Searching is case insensitive.")]
string? filter = null,
[Description("The folders where the search will start. If null, the search will be performed in all folders.")]
string[]? searchInFolders = null
string[]? searchInFolders = null,
[Description("Maximum number of assets to return. If the number of found assets exceeds this limit, the result will be truncated.")]
int maxResults = 10
)
=> MainThread.Instance.Run(() =>
{
var assetGuids = (searchInFolders?.Length ?? 0) == 0
? AssetDatabase.FindAssets(filter ?? string.Empty)
: AssetDatabase.FindAssets(filter ?? string.Empty, searchInFolders);
return MainThread.Instance.Run(() =>
{
var assetGuids = (searchInFolders?.Length ?? 0) == 0
? AssetDatabase.FindAssets(filter ?? string.Empty)
: AssetDatabase.FindAssets(filter ?? string.Empty, searchInFolders);

var stringBuilder = new StringBuilder();
stringBuilder.AppendLine("instanceID | assetGuid | assetPath");
stringBuilder.AppendLine("-----------+--------------------------------------+---------------------------------");
// " -12345 | 8e09c738-7b14-4d83-9740-2b396bd4cfc9 | Assets/Editor/Image.png");
var response = new List<AssetObjectRef>();

for (var i = 0; i < assetGuids.Length; i++)
{
if (i >= Consts.MCP.Plugin.LinesLimit)
for (var i = 0; i < assetGuids.Length && i < maxResults; i++)
{
stringBuilder.AppendLine($"... and {assetGuids.Length - i} more assets. Use {nameof(searchInFolders)} parameter to specify request.");
break;
var assetPath = AssetDatabase.GUIDToAssetPath(assetGuids[i]);
var assetType = AssetDatabase.GetMainAssetTypeAtPath(assetPath);
var assetObject = AssetDatabase.LoadAssetAtPath(assetPath, assetType);
if (assetObject == null)
continue;

response.Add(new AssetObjectRef(assetObject));
}
var assetPath = AssetDatabase.GUIDToAssetPath(assetGuids[i]);
var assetObject = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath);
if (assetObject == null) continue;
var instanceID = assetObject.GetInstanceID();
stringBuilder.AppendLine($"{instanceID,-10} | {assetGuids[i],-36} | {assetPath}");
}

return $"[Success] Assets found: {assetGuids.Length}.\n{stringBuilder}";
});
return response;
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
┌──────────────────────────────────────────────────────────────────┐
│ Author: Ivan Murzak (https://github.com/IvanMurzak) │
│ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │
│ Copyright (c) 2025 Ivan Murzak │
│ Licensed under the Apache License, Version 2.0. │
│ See the LICENSE file in the project root for more information. │
└──────────────────────────────────────────────────────────────────┘
*/

#nullable enable
using System.ComponentModel;
using com.IvanMurzak.McpPlugin;
using com.IvanMurzak.ReflectorNet.Model;
using com.IvanMurzak.ReflectorNet.Utils;
using com.IvanMurzak.Unity.MCP.Runtime.Data;
using UnityEditor;

namespace com.IvanMurzak.Unity.MCP.Editor.API
{
public partial class Tool_Assets
{
[McpPluginTool
(
"assets-getdata",
Title = "Assets / Get Data"
)]
[Description(@"Get asset data from the asset file in the Unity project. It includes all serializable fields and properties of the asset.")]
public SerializedMember GetData(AssetObjectRef assetRef)
{
return MainThread.Instance.Run(() =>
{
if (string.IsNullOrEmpty(assetRef.AssetPath) && string.IsNullOrEmpty(assetRef.AssetGuid))
throw new System.Exception(Error.NeitherProvided_AssetPath_AssetGuid());

if (string.IsNullOrEmpty(assetRef.AssetPath))
assetRef.AssetPath = AssetDatabase.GUIDToAssetPath(assetRef.AssetGuid);
if (string.IsNullOrEmpty(assetRef.AssetGuid))
assetRef.AssetGuid = AssetDatabase.AssetPathToGUID(assetRef.AssetPath);

var asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetRef.AssetPath);
if (asset == null)
throw new System.Exception(Error.NotFoundAsset(assetRef.AssetPath!, assetRef.AssetGuid!));

var reflector = McpPlugin.McpPlugin.Instance!.McpManager.Reflector;

return reflector.Serialize(
asset,
name: asset.name,
recursive: true,
logger: McpPlugin.McpPlugin.Instance.Logger
);
});
}
}
}
Loading
Loading