-
Notifications
You must be signed in to change notification settings - Fork 471
Allow users to easily add tools in the Asset folder #324
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f713810
48d912e
68f09e1
069f1be
b1f696d
88a6491
6a4d55b
fac3b95
3fef23e
571ca8c
da98ba9
d502dae
81f5006
9a152e3
6e5ccb5
650806f
38fe922
04798f9
4c8c611
8753c30
9627e47
7ba6967
366936a
1d6c55b
1c676c4
141064b
e58285e
b0ec0eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using UnityEngine; | ||
|
||
namespace MCPForUnity.Editor.Data | ||
{ | ||
/// <summary> | ||
/// Registry of Python tool files to sync to the MCP server. | ||
/// Add your Python files here - they can be stored anywhere in your project. | ||
/// </summary> | ||
[CreateAssetMenu(fileName = "PythonTools", menuName = "MCP For Unity/Python Tools")] | ||
public class PythonToolsAsset : ScriptableObject | ||
{ | ||
[Tooltip("Add Python files (.py) to sync to the MCP server. Files can be located anywhere in your project.")] | ||
public List<TextAsset> pythonFiles = new List<TextAsset>(); | ||
|
||
[Header("Sync Options")] | ||
[Tooltip("Use content hashing to detect changes (recommended). If false, always copies on startup.")] | ||
public bool useContentHashing = true; | ||
|
||
[Header("Sync State (Read-only)")] | ||
[Tooltip("Internal tracking - do not modify")] | ||
public List<PythonFileState> fileStates = new List<PythonFileState>(); | ||
|
||
/// <summary> | ||
/// Gets all valid Python files (filters out null/missing references) | ||
/// </summary> | ||
public IEnumerable<TextAsset> GetValidFiles() | ||
{ | ||
return pythonFiles.Where(f => f != null); | ||
} | ||
|
||
/// <summary> | ||
/// Checks if a file needs syncing | ||
/// </summary> | ||
public bool NeedsSync(TextAsset file, string currentHash) | ||
{ | ||
if (!useContentHashing) return true; // Always sync if hashing disabled | ||
|
||
var state = fileStates.FirstOrDefault(s => s.assetGuid == GetAssetGuid(file)); | ||
return state == null || state.contentHash != currentHash; | ||
} | ||
|
||
/// <summary> | ||
/// Records that a file was synced | ||
/// </summary> | ||
public void RecordSync(TextAsset file, string hash) | ||
{ | ||
string guid = GetAssetGuid(file); | ||
var state = fileStates.FirstOrDefault(s => s.assetGuid == guid); | ||
|
||
if (state == null) | ||
{ | ||
state = new PythonFileState { assetGuid = guid }; | ||
fileStates.Add(state); | ||
} | ||
|
||
state.contentHash = hash; | ||
state.lastSyncTime = DateTime.UtcNow; | ||
state.fileName = file.name; | ||
} | ||
|
||
/// <summary> | ||
/// Removes state entries for files no longer in the list | ||
/// </summary> | ||
public void CleanupStaleStates() | ||
{ | ||
var validGuids = new HashSet<string>(GetValidFiles().Select(GetAssetGuid)); | ||
fileStates.RemoveAll(s => !validGuids.Contains(s.assetGuid)); | ||
} | ||
|
||
private string GetAssetGuid(TextAsset asset) | ||
{ | ||
return UnityEditor.AssetDatabase.AssetPathToGUID(UnityEditor.AssetDatabase.GetAssetPath(asset)); | ||
} | ||
|
||
/// <summary> | ||
/// Called when the asset is modified in the Inspector | ||
/// Triggers sync to handle file additions/removals | ||
/// </summary> | ||
private void OnValidate() | ||
{ | ||
// Cleanup stale states immediately | ||
CleanupStaleStates(); | ||
|
||
// Trigger sync after a delay to handle file removals | ||
// Delay ensures the asset is saved before sync runs | ||
UnityEditor.EditorApplication.delayCall += () => | ||
{ | ||
if (this != null) // Check if asset still exists | ||
{ | ||
MCPForUnity.Editor.Helpers.PythonToolSyncProcessor.SyncAllTools(); | ||
} | ||
}; | ||
} | ||
} | ||
|
||
[Serializable] | ||
public class PythonFileState | ||
{ | ||
public string assetGuid; | ||
public string fileName; | ||
public string contentHash; | ||
public DateTime lastSyncTime; | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
using System.IO; | ||
using System.Linq; | ||
using MCPForUnity.Editor.Data; | ||
using MCPForUnity.Editor.Services; | ||
using UnityEditor; | ||
using UnityEngine; | ||
|
||
namespace MCPForUnity.Editor.Helpers | ||
{ | ||
/// <summary> | ||
/// Automatically syncs Python tools to the MCP server when: | ||
/// - PythonToolsAsset is modified | ||
/// - Python files are imported/reimported | ||
/// - Unity starts up | ||
/// </summary> | ||
[InitializeOnLoad] | ||
public class PythonToolSyncProcessor : AssetPostprocessor | ||
{ | ||
private const string SyncEnabledKey = "MCPForUnity.AutoSyncEnabled"; | ||
private static bool _isSyncing = false; | ||
|
||
static PythonToolSyncProcessor() | ||
{ | ||
// Sync on Unity startup | ||
EditorApplication.delayCall += () => | ||
{ | ||
if (IsAutoSyncEnabled()) | ||
{ | ||
SyncAllTools(); | ||
} | ||
}; | ||
} | ||
|
||
/// <summary> | ||
/// Called after any assets are imported, deleted, or moved | ||
/// </summary> | ||
private static void OnPostprocessAllAssets( | ||
string[] importedAssets, | ||
string[] deletedAssets, | ||
string[] movedAssets, | ||
string[] movedFromAssetPaths) | ||
{ | ||
// Prevent infinite loop - don't process if we're currently syncing | ||
if (_isSyncing || !IsAutoSyncEnabled()) | ||
return; | ||
|
||
bool needsSync = false; | ||
|
||
// Only check for .py file changes, not PythonToolsAsset changes | ||
// (PythonToolsAsset changes are internal state updates from syncing) | ||
foreach (string path in importedAssets.Concat(movedAssets)) | ||
{ | ||
// Check if any .py files were modified | ||
if (path.EndsWith(".py")) | ||
{ | ||
needsSync = true; | ||
break; | ||
} | ||
} | ||
|
||
// Check if any .py files were deleted | ||
if (!needsSync && deletedAssets.Any(path => path.EndsWith(".py"))) | ||
{ | ||
needsSync = true; | ||
} | ||
|
||
if (needsSync) | ||
{ | ||
SyncAllTools(); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Syncs all Python tools from all PythonToolsAsset instances to the MCP server | ||
/// </summary> | ||
public static void SyncAllTools() | ||
{ | ||
// Prevent re-entrant calls | ||
if (_isSyncing) | ||
{ | ||
McpLog.Warn("Sync already in progress, skipping..."); | ||
return; | ||
} | ||
|
||
_isSyncing = true; | ||
try | ||
{ | ||
if (!ServerPathResolver.TryFindEmbeddedServerSource(out string srcPath)) | ||
{ | ||
McpLog.Warn("Cannot sync Python tools: MCP server source not found"); | ||
return; | ||
} | ||
|
||
string toolsDir = Path.Combine(srcPath, "tools", "custom"); | ||
|
||
var result = MCPServiceLocator.ToolSync.SyncProjectTools(toolsDir); | ||
|
||
if (result.Success) | ||
{ | ||
if (result.CopiedCount > 0 || result.SkippedCount > 0) | ||
{ | ||
McpLog.Info($"Python tools synced: {result.CopiedCount} copied, {result.SkippedCount} skipped"); | ||
} | ||
} | ||
else | ||
{ | ||
McpLog.Error($"Python tool sync failed with {result.ErrorCount} errors"); | ||
foreach (var msg in result.Messages) | ||
{ | ||
McpLog.Error($" - {msg}"); | ||
} | ||
} | ||
} | ||
catch (System.Exception ex) | ||
{ | ||
McpLog.Error($"Python tool sync exception: {ex.Message}"); | ||
} | ||
finally | ||
{ | ||
_isSyncing = false; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Checks if auto-sync is enabled (default: true) | ||
/// </summary> | ||
public static bool IsAutoSyncEnabled() | ||
{ | ||
return EditorPrefs.GetBool(SyncEnabledKey, true); | ||
} | ||
|
||
/// <summary> | ||
/// Enables or disables auto-sync | ||
/// </summary> | ||
public static void SetAutoSyncEnabled(bool enabled) | ||
{ | ||
EditorPrefs.SetBool(SyncEnabledKey, enabled); | ||
McpLog.Info($"Python tool auto-sync {(enabled ? "enabled" : "disabled")}"); | ||
} | ||
|
||
/// <summary> | ||
/// Menu item to reimport all Python files in the project | ||
/// </summary> | ||
[MenuItem("Window/MCP For Unity/Tool Sync/Reimport Python Files", priority = 99)] | ||
public static void ReimportPythonFiles() | ||
{ | ||
// Find all Python files (imported as TextAssets by PythonFileImporter) | ||
var pythonGuids = AssetDatabase.FindAssets("t:TextAsset", new[] { "Assets" }) | ||
.Select(AssetDatabase.GUIDToAssetPath) | ||
.Where(path => path.EndsWith(".py", System.StringComparison.OrdinalIgnoreCase)) | ||
.ToArray(); | ||
|
||
foreach (string path in pythonGuids) | ||
{ | ||
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate); | ||
} | ||
|
||
int count = pythonGuids.Length; | ||
McpLog.Info($"Reimported {count} Python files"); | ||
AssetDatabase.Refresh(); | ||
} | ||
|
||
/// <summary> | ||
/// Menu item to manually trigger sync | ||
/// </summary> | ||
[MenuItem("Window/MCP For Unity/Tool Sync/Sync Python Tools", priority = 100)] | ||
public static void ManualSync() | ||
{ | ||
McpLog.Info("Manually syncing Python tools..."); | ||
SyncAllTools(); | ||
} | ||
|
||
/// <summary> | ||
/// Menu item to toggle auto-sync | ||
/// </summary> | ||
[MenuItem("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", priority = 101)] | ||
public static void ToggleAutoSync() | ||
{ | ||
SetAutoSyncEnabled(!IsAutoSyncEnabled()); | ||
} | ||
|
||
/// <summary> | ||
/// Validate menu item (shows checkmark when enabled) | ||
/// </summary> | ||
[MenuItem("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", true, priority = 101)] | ||
public static bool ToggleAutoSyncValidate() | ||
{ | ||
Menu.SetChecked("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", IsAutoSyncEnabled()); | ||
return true; | ||
} | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
using UnityEngine; | ||
using UnityEditor.AssetImporters; | ||
using System.IO; | ||
|
||
namespace MCPForUnity.Editor.Importers | ||
{ | ||
/// <summary> | ||
/// Custom importer that allows Unity to recognize .py files as TextAssets. | ||
/// This enables Python files to be selected in the Inspector and used like any other text asset. | ||
/// </summary> | ||
[ScriptedImporter(1, "py")] | ||
public class PythonFileImporter : ScriptedImporter | ||
{ | ||
public override void OnImportAsset(AssetImportContext ctx) | ||
{ | ||
var textAsset = new TextAsset(File.ReadAllText(ctx.assetPath)); | ||
ctx.AddObjectToAsset("main obj", textAsset); | ||
ctx.SetMainObject(textAsset); | ||
} | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Non‑asset TextAssets get empty GUIDs → state collisions
AssetDatabase.GetAssetPath on non‑asset TextAssets returns empty, so AssetPathToGUID is empty. Multiple unsaved TextAssets then share the same key, corrupting fileStates. Guard and skip non‑asset entries.
Also applies to: 48-62, 73-76
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is super unlikely, devs are just going to drag Python files the scriptable object, or add them to the list directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@msanatan, you're right—in the typical workflow where developers drag Python asset files from the Project window into the inspector, those will always have valid GUIDs. I understand the defensive guard isn't needed for the expected use case. Thanks for clarifying! 🐰
🧠 Learnings used