Skip to content

Conversation

msanatan
Copy link
Contributor

@msanatan msanatan commented Oct 17, 2025

One of the biggest problems facing users is adding tools. The Unity plugin dynamically gets tools with the McpForUnityTool attribute. However, Python changes require that users add tools in the server's location. This works well if users install the package locally, however, only developers do that.

We need a way for users to install tools, including the necessary Python code, until we can add a tool in pure C# (see #293)

Closes #308 and closes #309

Summary by CodeRabbit

  • New Features

    • Python tool asset management with content-aware syncing to the embedded server.
    • Automatic tool sync on editor startup (default on) with menu toggle, manual sync, and “reimport Python files” actions.
    • Unity import support for .py files so Python code is treated as assets and synchronized.
  • Documentation

    • Completely revised custom tools guide with quick start, workflow, and troubleshooting.
  • Tests

    • Added editor tests covering registry, syncing, and sync-result behaviors.

JohanHoltby and others added 12 commits October 9, 2025 09:36
…n.txt

This allows for auto finding new tools. A good dir on a custom tool would look like this:
CustomTool/
├── CustomTool.MCPEnabler.asmdef
├── CustomTool.MCPEnabler.asmdef.meta
├── ExternalAssetToolFunction.cs
├── ExternalAssetToolFunction.cs.meta
├── external_asset_tool_function.py
├── external_asset_tool_function.py.meta
├── version.txt
└── version.txt.meta

CS files are left in the tools folder. The asmdef is recommended to allow for dependency on MCPForUnity when MCP For Unity is installed:

asmdef example
{
    "name": "CustomTool.MCPEnabler",
    "rootNamespace": "MCPForUnity.Editor.Tools",
    "references": [
        "CustomTool",
        "MCPForUnity.Editor"
    ],
    "includePlatforms": [
        "Editor"
    ],
    "excludePlatforms": [],
    "allowUnsafeCode": false,
    "overrideReferences": false,
    "precompiledReferences": [],
    "autoReferenced": true,
    "defineConstraints": [],
    "versionDefines": [],
    "noEngineReferences": false
}
… GetToolsFolderIdentifier, fix tools copy dir, and limit scan scope
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@msanatan msanatan self-assigned this Oct 17, 2025
Copy link
Contributor

coderabbitai bot commented Oct 17, 2025

Walkthrough

Adds editor support to discover, track, and synchronize Python tool files: new ScriptableObject registry and state, a .py importer, registry and sync services with service-locator wiring, an editor processor for auto/manual sync, Python-side module discovery, tests, and updated docs.

Changes

Cohort / File(s) Summary
Python Tools Asset
MCPForUnity/Editor/Data/PythonToolsAsset.cs, MCPForUnity/Editor/Data/PythonToolsAsset.cs.meta
New PythonToolsAsset ScriptableObject and PythonFileState to list TextAsset Python files, track content hashes and last-sync times, compute sync necessity, record syncs, and cleanup stale states.
Editor Importer
MCPForUnity/Editor/Importers/PythonFileImporter.cs, MCPForUnity/Editor/Importers/PythonFileImporter.cs.meta, MCPForUnity/Editor/Importers.meta
Adds ScriptedImporter for .py files that produces TextAsset assets on import.
Auto-sync Processor & Menu
MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs, MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs.meta
New PythonToolSyncProcessor that auto-syncs on startup and asset changes, exposes manual sync/reimport/toggle commands, persists auto-sync via EditorPrefs, and guards reentrancy.
Tool Sync Contracts
MCPForUnity/Editor/Services/IToolSyncService.cs, MCPForUnity/Editor/Services/IToolSyncService.cs.meta, MCPForUnity/Editor/Services/IPythonToolRegistryService.cs, MCPForUnity/Editor/Services/IPythonToolRegistryService.cs.meta
Adds IToolSyncService, ToolSyncResult DTO, and IPythonToolRegistryService interface to define sync and registry operations.
Service Implementations
MCPForUnity/Editor/Services/PythonToolRegistryService.cs, .../PythonToolRegistryService.cs.meta, MCPForUnity/Editor/Services/ToolSyncService.cs, .../ToolSyncService.cs.meta
Implements PythonToolRegistryService (discover registries, compute hashes, record syncs) and ToolSyncService (orchestrates file writes, stale cleanup, batching, result aggregation).
Service Locator
MCPForUnity/Editor/Services/MCPServiceLocator.cs
Registers and exposes PythonToolRegistry and ToolSync services; extends Register<T>() and Reset() lifecycle to handle new services.
Server Installer & Minor Edits
MCPForUnity/Editor/Helpers/ServerInstaller.cs, MCPForUnity/Editor/Data/McpClients.cs
Minor formatting edits and removal of an unused using in McpClients.cs.
Python-side discovery
MCPForUnity/UnityMcpServer~/src/module_discovery.py
New discover_modules(base_dir, package_name) generator to locate and import top-level modules and one-level subdirectory modules, yielding fully qualified names.
Python resources/tools init
MCPForUnity/UnityMcpServer~/src/resources/__init__.py, MCPForUnity/UnityMcpServer~/src/tools/__init__.py
Replace ad-hoc pkgutil/importlib imports with centralized discover_modules() usage; update docstrings to include subdirectory scanning.
Tests (Editor)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Data/PythonToolsAssetTests.cs, .../Services/PythonToolRegistryServiceTests.cs, .../Services/ToolSyncServiceTests.cs, plus corresponding .meta and folder .meta files
New unit tests covering PythonToolsAsset behaviors, PythonToolRegistryService (hashing, needs/record), and ToolSyncService (sync, cleanup, result reporting).
Docs
docs/CUSTOM_TOOLS.md
Major rewrite: step-by-step quick start and technical details describing PythonToolsAsset, sync flow, example handlers, troubleshooting, and best practices.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Editor as Unity Editor
    participant Proc as PythonToolSyncProcessor
    participant AssetDB as AssetDatabase
    participant RegistrySvc as PythonToolRegistryService
    participant SyncSvc as ToolSyncService
    participant Dest as MCP Server tools dir

    Editor->>Proc: Asset change (.py added/modified/moved)
    Proc->>AssetDB: query/import events
    Proc->>SyncSvc: SyncAllTools()
    SyncSvc->>RegistrySvc: GetAllRegistries()
    RegistrySvc->>AssetDB: Find PythonToolsAsset assets
    AssetDB-->>RegistrySvc: registries list
    RegistrySvc-->>SyncSvc: registries

    loop per registry -> per file
        SyncSvc->>RegistrySvc: NeedsSync(registry, file)?
        alt hashing enabled
            RegistrySvc->>RegistrySvc: ComputeHash(file)
            RegistrySvc-->>SyncSvc: yes/no
        else
            RegistrySvc-->>SyncSvc: yes
        end

        alt needs sync
            SyncSvc->>Dest: Write .py file
            SyncSvc->>RegistrySvc: RecordSync(registry, file)
            RegistrySvc->>AssetDB: SetDirty(registry)
            SyncSvc-->>SyncSvc: increment CopiedCount
        else skipped
            SyncSvc-->>SyncSvc: increment SkippedCount
        end
    end

    SyncSvc->>RegistrySvc: CleanupStaleStates()
    SyncSvc->>Dest: CleanupStaleFiles()
    SyncSvc->>AssetDB: StartAssetEditing / StopAssetEditing
    SyncSvc-->>Proc: ToolSyncResult
    Proc-->>Editor: Log summary
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • justinpbarnett
  • dsarno

Poem

🐇 I hopped through folders, sniffed each .py,

I hashed their lines, then nudged them to fly.
Registries tidy, stale files swept away—
Hop, sync, repeat, for a brighter build day!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.11% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Allow users to easily add tools in the Asset folder" directly captures the core objective of the PR as described. The changeset introduces PythonToolsAsset, PythonToolSyncProcessor, and related services that enable users to manage and sync Python tool files from the Assets folder without requiring package edits. The title is concise, clear, and accurately represents the main improvement being delivered. It avoids vague terms and communicates the primary benefit at a glance.
Linked Issues Check ✅ Passed The PR addresses the core objectives from issues #308 and #309 by enabling Python tool files to be located and synced from the Assets folder [#308, #309]. The implementation includes PythonToolsAsset for managing tool files, PythonToolSyncProcessor for automatic synchronization on startup and asset changes, content hashing for tracking file state, and comprehensive state management via PythonFileState [#308, #309]. The module discovery system has been enhanced to support hierarchical module loading, and tests validate the registry and sync service functionality. However, the implementation uses a manual ScriptableObject configuration approach rather than the automatic "MCPForUnityTools" folder discovery pattern described in issue #309, representing an interim solution that prioritizes flexibility and ease of use while deferring full automatic discovery.
Out of Scope Changes Check ✅ Passed All code changes are directly related to implementing the tool discovery and synchronization feature. The additions include core components (PythonToolsAsset, PythonToolSyncProcessor, registry and sync services), the PythonFileImporter for asset integration, supporting infrastructure updates to MCPServiceLocator, Python-side module discovery utilities, comprehensive test coverage, documentation updates, and minimal formatting adjustments to ServerInstaller.cs. Each change either implements a required feature, supports the feature infrastructure, provides necessary test coverage, or documents the new functionality.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@msanatan msanatan marked this pull request as ready for review October 18, 2025 03:48
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
docs/CUSTOM_TOOLS.md (2)

185-186: Append .png to the saved filename to match PNG encoding.

The sample encodes PNGs but doesn’t add the extension; the earlier parameter hints “without extension”.

-                string absolutePath = Path.Combine(Application.dataPath, "Screenshots", filename);
+                string absolutePath = Path.Combine(Application.dataPath, "Screenshots", filename + ".png");

Also applies to: 219-221


200-217: Ensure RenderTexture/Texture2D are always released (finally).

On exceptions before cleanup, RT/screenshot can leak. Wrap cleanup in finally.

-                // Create a RenderTexture
-                RenderTexture rt = new RenderTexture(Screen.width, Screen.height, 24);
-                camera.targetTexture = rt;
-                // Render...
-                ...
-                Object.DestroyImmediate(rt);
-                // Save to file
-                byte[] bytes = screenshot.EncodeToPNG();
-                File.WriteAllBytes(absolutePath, bytes);
-                Object.DestroyImmediate(screenshot);
+                RenderTexture rt = null;
+                Texture2D screenshot = null;
+                try
+                {
+                    rt = new RenderTexture(Screen.width, Screen.height, 24);
+                    camera.targetTexture = rt;
+                    camera.Render();
+                    RenderTexture.active = rt;
+                    screenshot = new Texture2D(Screen.width, Screen.height, TextureFormat.RGB24, false);
+                    screenshot.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
+                    screenshot.Apply();
+                    byte[] bytes = screenshot.EncodeToPNG();
+                    File.WriteAllBytes(absolutePath, bytes);
+                }
+                finally
+                {
+                    camera.targetTexture = null;
+                    RenderTexture.active = null;
+                    if (rt) Object.DestroyImmediate(rt);
+                    if (screenshot) Object.DestroyImmediate(screenshot);
+                }
🧹 Nitpick comments (24)
MCPForUnity/UnityMcpServer~/src/module_discovery.py (1)

31-35: Consider adding more context to the warning message.

When logging import failures, including the full module path (not just the module name) and potentially the exception type would help with debugging.

         try:
             full_module_name = f'.{module_name}'
             importlib.import_module(full_module_name, package_name)
             yield full_module_name
         except Exception as e:
-            logger.warning(f"Failed to import module {module_name}: {e}")
+            logger.warning(f"Failed to import module {package_name}.{module_name}: {type(e).__name__}: {e}")

Apply a similar change at lines 48-55 for subdirectory imports.

MCPForUnity/Editor/Importers/PythonFileImporter.cs (1)

14-19: Consider specifying encoding explicitly.

File.ReadAllText defaults to UTF-8 on most systems, but explicitly specifying Encoding.UTF8 improves clarity and ensures consistent behavior across platforms.

Apply this diff if you'd like to make the encoding explicit:

+using System.Text;
+
 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));
+            var textAsset = new TextAsset(File.ReadAllText(ctx.assetPath, Encoding.UTF8));
             ctx.AddObjectToAsset("main obj", textAsset);
             ctx.SetMainObject(textAsset);
         }
     }
 }
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolSyncServiceTests.cs (5)

20-25: Harden temp-dir cleanup in SetUp to avoid sporadic IO failures.

Mirror TearDown's try/catch to prevent flaky failures on Windows AV/file locks.

Apply this diff:

-            // Clean up any existing test directory
-            if (Directory.Exists(_testToolsDir))
-            {
-                Directory.Delete(_testToolsDir, true);
-            }
+            // Clean up any existing test directory
+            if (Directory.Exists(_testToolsDir))
+            {
+                try
+                {
+                    Directory.Delete(_testToolsDir, true);
+                }
+                catch
+                {
+                    // Ignore cleanup errors
+                }
+            }

52-60: Assert Success and SkippedCount for the “no assets” path.

This verifies contract more fully.

Apply this diff:

         var result = _service.SyncProjectTools(_testToolsDir);
 
         Assert.IsNotNull(result, "Should return a result");
-        Assert.AreEqual(0, result.CopiedCount, "Should not copy any files");
-        Assert.AreEqual(0, result.ErrorCount, "Should not have errors");
+        Assert.IsTrue(result.Success, "Should succeed when no assets are present");
+        Assert.AreEqual(0, result.CopiedCount, "Should not copy any files");
+        Assert.AreEqual(0, result.SkippedCount, "Should not skip any files");
+        Assert.AreEqual(0, result.ErrorCount, "Should not have errors");

92-100: Rename or ignore this placeholder test; it doesn’t exercise multiple assets.

Either populate multiple PythonToolsAssets and assert counts, or mark ignored until an integration path exists.

Example diff (ignore for now):

-        [Test]
-        public void SyncProjectTools_HandlesMultipleAssets()
+        [Test, Ignore("Populate multiple PythonToolsAssets and verify aggregation; convert to integration test.")]
+        public void SyncProjectTools_HandlesMultipleAssets()
         {
             // This would require creating multiple PythonToolsAssets
             // Better suited for integration testing
             var result = _service.SyncProjectTools(_testToolsDir);
 
             Assert.IsNotNull(result, "Should handle multiple assets gracefully");
         }

102-110: Also assert Success reflects ErrorCount.

Small contract check.

Apply this diff:

         var result = _service.SyncProjectTools(_testToolsDir);
 
         Assert.IsTrue(result.CopiedCount >= 0, "Copied count should be non-negative");
         Assert.IsTrue(result.SkippedCount >= 0, "Skipped count should be non-negative");
         Assert.IsTrue(result.ErrorCount >= 0, "Error count should be non-negative");
+        Assert.AreEqual(result.ErrorCount == 0, result.Success, "Success should be true when ErrorCount is zero.");

1-112: Add tests for single .py per MCPForUnityTools folder and version.txt update semantics.

These are key behaviors from issues #308/#309 and the established design.

  • Verify that a folder with multiple .py files is rejected or only one is used.
  • Verify that changing version.txt triggers an update and copy; same version does not.
    Based on learnings

I can add Editor integration tests that scaffold temp MCPForUnityTools folders, import assets, and assert ToolSyncResult behavior.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Data/PythonToolsAssetTests.cs (2)

125-128: Prefer asserting non-empty identifier, not just non-null.

Guard against empty GUIDs when assets aren’t in AssetDatabase.

Apply this diff:

-            Assert.IsNotNull(_asset.fileStates[0].assetGuid, "Should store the GUID");
+            Assert.IsFalse(string.IsNullOrEmpty(_asset.fileStates[0].assetGuid),
+                "Should store a non-empty GUID or fallback identifier");

139-147: Reduce flakiness: increase sleep or avoid timing dependence.

10ms can be flaky on CI; bump to 50–100ms or assert with retry.

Apply this diff:

-            System.Threading.Thread.Sleep(10);
+            System.Threading.Thread.Sleep(50);

Even better: loop-wait until lastSyncTime changes with a short timeout. I can draft that if preferred.

MCPForUnity/Editor/Services/IToolSyncService.cs (2)

5-12: DTO looks good; consider a small convenience for callers.

Optionally add a HadChanges property (CopiedCount > 0) and a Summary string to simplify UI/logging. Low effort, backward compatible.

 public class ToolSyncResult
 {
     public int CopiedCount { get; set; }
     public int SkippedCount { get; set; }
     public int ErrorCount { get; set; }
     public List<string> Messages { get; set; } = new List<string>();
     public bool Success => ErrorCount == 0;
+    public bool HadChanges => CopiedCount > 0;
+    public override string ToString() =>
+        $"copied={CopiedCount}, skipped={SkippedCount}, errors={ErrorCount}";
 }

16-16: Clarify destToolsDir contract.

Add XML docs to state destToolsDir should point to the server’s custom tools folder (e.g., UnityMcpServer~/src/tools/custom). Prevents misuse.

docs/CUSTOM_TOOLS.md (4)

253-260: “description” is recommended, not strictly required — align wording.

The code comment says “Required” but the note says “not strictly required.” Make both say “Recommended (strongly)” for consistency and accuracy.

-    description="Tool description",  # Required: describe what the tool does
+    description="Tool description",  # Recommended: include for client compatibility
...
-**Note:** All tools should have the `description` field. It's not strictly required, however, that parameter is the best place...
+**Note:** All tools should include the `description` field. While not strictly required, it maximizes client compatibility and parsing quality...

Also applies to: 267-267


163-168: Use consistent Response helper import to avoid fully qualified calls.

Add Helpers using and drop long qualifiers in the example for clarity.

 using System.IO;
 using Newtonsoft.Json.Linq;
 using UnityEngine;
-using MCPForUnity.Editor.Tools;
+using MCPForUnity.Editor.Helpers;
+using MCPForUnity.Editor.Tools; // if needed elsewhere
@@
-                return MCPForUnity.Editor.Helpers.Response.Error("filename is required");
+                return Response.Error("filename is required");
@@
-                return MCPForUnity.Editor.Helpers.Response.Success($"Screenshot saved to {absolutePath}", new
+                return Response.Success($"Screenshot saved to {absolutePath}", new
@@
-                return MCPForUnity.Editor.Helpers.Response.Error($"Failed to capture screenshot: {ex.Message}");
+                return Response.Error($"Failed to capture screenshot: {ex.Message}");

Also applies to: 223-231


291-295: Call out cleanup behavior to prevent surprises.

Note that sync deletes any .py in the destination not managed by PythonToolsAsset. Warn users not to place manual files there.

 - Stale files are automatically cleaned up
+ - Stale files are automatically cleaned up
+   - Warning: any .py in the destination folder that isn’t managed by a PythonToolsAsset will be deleted during sync

356-361: Align troubleshooting log text with actual messages.

ToolSyncService logs “Synced Python tool: .py” per file. Adjust the guidance to look for that line (or add an aggregate summary log if desired).

-  - Check Unity Console for sync messages: "Python tools synced: X copied"
+  - Check Unity Console for: "Synced Python tool: <name>.py"
MCPForUnity/Editor/Services/PythonToolRegistryService.cs (2)

1-9: Minor hygiene: remove unused using.

System.IO isn’t used in this class.

-using System.IO;

42-53: Normalize line endings before hashing to reduce spurious resyncs across OS.

Converts CRLF/LF inconsistencies to a canonical LF prior to hashing.

-            using (var sha256 = SHA256.Create())
+            using (var sha256 = SHA256.Create())
             {
-                byte[] bytes = System.Text.Encoding.UTF8.GetBytes(file.text);
+                // Normalize line endings to LF to avoid OS-specific hash churn
+                string normalized = file.text.Replace("\r\n", "\n");
+                byte[] bytes = System.Text.Encoding.UTF8.GetBytes(normalized);
                 byte[] hash = sha256.ComputeHash(bytes);
                 return BitConverter.ToString(hash).Replace("-", "").ToLower();
             }
MCPForUnity/Editor/Data/PythonToolsAsset.cs (3)

29-33: Filter to real .py assets and de‑dup by GUID

Currently any TextAsset is accepted; duplicates are possible. Filter by path under Assets/ and .py extension, and de‑dup by GUID to avoid redundant work.

Apply:

-        public IEnumerable<TextAsset> GetValidFiles()
-        {
-            return pythonFiles.Where(f => f != null);
-        }
+        public IEnumerable<TextAsset> GetValidFiles()
+        {
+            return pythonFiles
+                .Where(f => f != null)
+                .Where(f =>
+                {
+                    var path = UnityEditor.AssetDatabase.GetAssetPath(f);
+                    return !string.IsNullOrEmpty(path)
+                           && path.StartsWith("Assets/", StringComparison.Ordinal)
+                           && path.EndsWith(".py", StringComparison.OrdinalIgnoreCase);
+                })
+                .GroupBy(GetAssetGuid)
+                .Select(g => g.First());
+        }

82-96: OnValidate may trigger redundant second sync

RecordSync marks the asset dirty; OnValidate then schedules another SyncAllTools after the first completes, producing an unnecessary pass. Throttle scheduling (e.g., coalesce with a static pending flag) or only schedule when the pythonFiles list actually changes.


22-25: Hide internal state in Inspector

fileStates is user-editable despite the “Read-only” hint. Hide to prevent accidental edits while keeping serialization.

-        [Header("Sync State (Read-only)")]
-        [Tooltip("Internal tracking - do not modify")]
-        public List<PythonFileState> fileStates = new List<PythonFileState>();
+        [Header("Sync State (Read-only)")]
+        [Tooltip("Internal tracking - do not modify")]
+        [HideInInspector]
+        public List<PythonFileState> fileStates = new List<PythonFileState>();
MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs (2)

51-59: Trigger only for project .py changes and handle case‑insensitive extensions

Avoid firing on package or external paths and handle “.PY”.

-            foreach (string path in importedAssets.Concat(movedAssets))
+            foreach (string path in importedAssets.Concat(movedAssets))
             {
                 // Check if any .py files were modified
-                if (path.EndsWith(".py"))
+                if (path.StartsWith("Assets/", System.StringComparison.Ordinal)
+                    && path.EndsWith(".py", System.StringComparison.OrdinalIgnoreCase))
                 {
                     needsSync = true;
                     break;
                 }
             }
@@
-            if (!needsSync && deletedAssets.Any(path => path.EndsWith(".py")))
+            if (!needsSync && deletedAssets.Any(path =>
+                    path.StartsWith("Assets/", System.StringComparison.Ordinal) &&
+                    path.EndsWith(".py", System.StringComparison.OrdinalIgnoreCase)))
             {
                 needsSync = true;
             }

Also applies to: 61-71


94-101: Consider isolating sync into a managed subfolder

To prevent accidental deletion of user files, target tools/custom/unity-managed rather than tools/custom.

-                string toolsDir = Path.Combine(srcPath, "tools", "custom");
+                string toolsDir = Path.Combine(srcPath, "tools", "custom", "unity-managed");

Coordinate with ToolSyncService cleanup accordingly. Based on learnings.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PythonToolRegistryServiceTests.cs (2)

79-91: Test masks empty GUID case; strengthen assertion

TextAsset created via new TextAsset(...) isn’t an asset; GUID is empty. The test asserts “not null” which passes on empty strings. Assert non‑empty and create a real asset to exercise GUID logic.

-            Assert.IsNotNull(asset.fileStates[0].assetGuid, "GUID should be stored");
+            Assert.IsFalse(string.IsNullOrEmpty(asset.fileStates[0].assetGuid), "GUID should be stored");

And create a temporary .py file under Assets for the test:

  • Write Assets/TempTests/test_tool.py
  • AssetDatabase.ImportAsset(...)
  • Load TextAsset via AssetDatabase.LoadAssetAtPath<TextAsset>(...)
  • Clean up in TearDown.

20-27: Add a duplicate‑name collision test

Add a test ensuring two registries referencing different .py assets with the same name are detected and surfaced as an error by ToolSyncService. This guards against silent overwrites.

I can sketch the test if you want it added now.

Also applies to: 93-109

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 90911aa and 1d6c55b.

⛔ Files ignored due to path filters (2)
  • docs/screenshots/v6_2_create_python_tools_asset.png is excluded by !**/*.png
  • docs/screenshots/v6_2_python_tools_asset.png is excluded by !**/*.png
📒 Files selected for processing (30)
  • MCPForUnity/Editor/Data/McpClients.cs (0 hunks)
  • MCPForUnity/Editor/Data/PythonToolsAsset.cs (1 hunks)
  • MCPForUnity/Editor/Data/PythonToolsAsset.cs.meta (1 hunks)
  • MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs (1 hunks)
  • MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs.meta (1 hunks)
  • MCPForUnity/Editor/Helpers/ServerInstaller.cs (2 hunks)
  • MCPForUnity/Editor/Importers.meta (1 hunks)
  • MCPForUnity/Editor/Importers/PythonFileImporter.cs (1 hunks)
  • MCPForUnity/Editor/Importers/PythonFileImporter.cs.meta (1 hunks)
  • MCPForUnity/Editor/Services/IPythonToolRegistryService.cs (1 hunks)
  • MCPForUnity/Editor/Services/IPythonToolRegistryService.cs.meta (1 hunks)
  • MCPForUnity/Editor/Services/IToolSyncService.cs (1 hunks)
  • MCPForUnity/Editor/Services/IToolSyncService.cs.meta (1 hunks)
  • MCPForUnity/Editor/Services/MCPServiceLocator.cs (3 hunks)
  • MCPForUnity/Editor/Services/PythonToolRegistryService.cs (1 hunks)
  • MCPForUnity/Editor/Services/PythonToolRegistryService.cs.meta (1 hunks)
  • MCPForUnity/Editor/Services/ToolSyncService.cs (1 hunks)
  • MCPForUnity/Editor/Services/ToolSyncService.cs.meta (1 hunks)
  • MCPForUnity/UnityMcpServer~/src/module_discovery.py (1 hunks)
  • MCPForUnity/UnityMcpServer~/src/resources/__init__.py (2 hunks)
  • MCPForUnity/UnityMcpServer~/src/tools/__init__.py (2 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Data.meta (1 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Data/PythonToolsAssetTests.cs (1 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Data/PythonToolsAssetTests.cs.meta (1 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services.meta (1 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PythonToolRegistryServiceTests.cs (1 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PythonToolRegistryServiceTests.cs.meta (1 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolSyncServiceTests.cs (1 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolSyncServiceTests.cs.meta (1 hunks)
  • docs/CUSTOM_TOOLS.md (6 hunks)
💤 Files with no reviewable changes (1)
  • MCPForUnity/Editor/Data/McpClients.cs
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: JohanHoltby
PR: CoplayDev/unity-mcp#309
File: MCPForUnity/Editor/Helpers/ServerInstaller.cs:478-508
Timestamp: 2025-10-13T13:41:00.086Z
Learning: In the MCPForUnityTools feature (MCPForUnity/Editor/Helpers/ServerInstaller.cs), the design intentionally forces users to have only one .py file per MCPForUnityTools folder to keep file tracking simple. Package-style tools (subdirectories with __init__.py) are not supported.
📚 Learning: 2025-10-13T13:41:00.086Z
Learnt from: JohanHoltby
PR: CoplayDev/unity-mcp#309
File: MCPForUnity/Editor/Helpers/ServerInstaller.cs:478-508
Timestamp: 2025-10-13T13:41:00.086Z
Learning: In the MCPForUnityTools feature (MCPForUnity/Editor/Helpers/ServerInstaller.cs), the design intentionally forces users to have only one .py file per MCPForUnityTools folder to keep file tracking simple. Package-style tools (subdirectories with __init__.py) are not supported.

Applied to files:

  • docs/CUSTOM_TOOLS.md
  • MCPForUnity/Editor/Data/PythonToolsAsset.cs
📚 Learning: 2025-10-03T22:11:46.002Z
Learnt from: msanatan
PR: CoplayDev/unity-mcp#301
File: docs/CUSTOM_TOOLS.md:54-62
Timestamp: 2025-10-03T22:11:46.002Z
Learning: In Unity MCP, the `description` parameter in the `mcp_for_unity_tool` decorator is technically optional but should always be included as a best practice. Without it, there's a higher chance that MCP clients will not parse the tool correctly. All Unity MCP tools should include the description in the decorator for compatibility.

Applied to files:

  • docs/CUSTOM_TOOLS.md
🧬 Code graph analysis (12)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolSyncServiceTests.cs (2)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Data/PythonToolsAssetTests.cs (14)
  • SetUp (13-17)
  • TearDown (19-26)
  • Test (28-34)
  • Test (36-46)
  • Test (48-62)
  • Test (64-73)
  • Test (75-84)
  • Test (86-100)
  • Test (102-115)
  • Test (117-128)
  • Test (130-148)
  • Test (150-173)
  • Test (175-186)
  • Test (188-203)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PythonToolRegistryServiceTests.cs (5)
  • SetUp (14-18)
  • Test (20-27)
  • Test (29-42)
  • Test (44-57)
  • Test (59-76)
MCPForUnity/UnityMcpServer~/src/resources/__init__.py (1)
MCPForUnity/UnityMcpServer~/src/module_discovery.py (1)
  • discover_modules (13-55)
MCPForUnity/Editor/Services/PythonToolRegistryService.cs (2)
MCPForUnity/Editor/Data/PythonToolsAsset.cs (3)
  • IEnumerable (29-32)
  • NeedsSync (37-43)
  • RecordSync (48-62)
MCPForUnity/Editor/Services/IPythonToolRegistryService.cs (4)
  • IEnumerable (9-9)
  • NeedsSync (10-10)
  • ComputeHash (12-12)
  • RecordSync (11-11)
MCPForUnity/UnityMcpServer~/src/tools/__init__.py (1)
MCPForUnity/UnityMcpServer~/src/module_discovery.py (1)
  • discover_modules (13-55)
MCPForUnity/Editor/Services/IPythonToolRegistryService.cs (2)
MCPForUnity/Editor/Data/PythonToolsAsset.cs (2)
  • NeedsSync (37-43)
  • RecordSync (48-62)
MCPForUnity/Editor/Services/PythonToolRegistryService.cs (3)
  • NeedsSync (27-33)
  • RecordSync (35-40)
  • ComputeHash (42-53)
MCPForUnity/Editor/Services/IToolSyncService.cs (1)
MCPForUnity/Editor/Services/ToolSyncService.cs (1)
  • ToolSyncResult (19-101)
MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs (2)
MCPForUnity/Editor/Services/MCPServiceLocator.cs (1)
  • MCPServiceLocator (8-64)
MCPForUnity/Editor/Resources/MenuItems/GetMenuItems.cs (1)
  • Refresh (49-69)
MCPForUnity/Editor/Services/MCPServiceLocator.cs (2)
MCPForUnity/Editor/Services/PythonToolRegistryService.cs (1)
  • PythonToolRegistryService (12-54)
MCPForUnity/Editor/Services/ToolSyncService.cs (2)
  • ToolSyncService (10-133)
  • ToolSyncService (14-17)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Data/PythonToolsAssetTests.cs (1)
MCPForUnity/Editor/Data/PythonToolsAsset.cs (1)
  • CleanupStaleStates (67-71)
MCPForUnity/Editor/Data/PythonToolsAsset.cs (3)
MCPForUnity/Editor/Services/IPythonToolRegistryService.cs (3)
  • IEnumerable (9-9)
  • NeedsSync (10-10)
  • RecordSync (11-11)
MCPForUnity/Editor/Services/PythonToolRegistryService.cs (3)
  • IEnumerable (14-25)
  • NeedsSync (27-33)
  • RecordSync (35-40)
MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs (2)
  • PythonToolSyncProcessor (22-32)
  • SyncAllTools (76-122)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PythonToolRegistryServiceTests.cs (2)
MCPForUnity/Editor/Services/PythonToolRegistryService.cs (1)
  • PythonToolRegistryService (12-54)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Data/PythonToolsAssetTests.cs (12)
  • Test (28-34)
  • Test (36-46)
  • Test (48-62)
  • Test (64-73)
  • Test (75-84)
  • Test (86-100)
  • Test (102-115)
  • Test (117-128)
  • Test (130-148)
  • Test (150-173)
  • Test (175-186)
  • Test (188-203)
MCPForUnity/Editor/Services/ToolSyncService.cs (5)
MCPForUnity/Editor/Services/MCPServiceLocator.cs (1)
  • MCPServiceLocator (8-64)
MCPForUnity/Editor/Services/IToolSyncService.cs (2)
  • ToolSyncResult (5-12)
  • ToolSyncResult (16-16)
MCPForUnity/Editor/Data/PythonToolsAsset.cs (3)
  • NeedsSync (37-43)
  • RecordSync (48-62)
  • CleanupStaleStates (67-71)
MCPForUnity/Editor/Services/IPythonToolRegistryService.cs (2)
  • NeedsSync (10-10)
  • RecordSync (11-11)
MCPForUnity/Editor/Services/PythonToolRegistryService.cs (2)
  • NeedsSync (27-33)
  • RecordSync (35-40)
🪛 Ruff (0.14.0)
MCPForUnity/UnityMcpServer~/src/module_discovery.py

34-34: Do not catch blind exception: Exception

(BLE001)


53-53: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (19)
MCPForUnity/Editor/Importers.meta (1)

1-8: No actionable review for Unity metadata file.

This is an auto-generated Unity metadata file that defines folder-level asset configuration. The structure is correct and follows Unity standards. No functional code or logic to review.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Data.meta (1)

1-8: Standard Unity metadata file—looks good.

This is a standard auto-generated folder metadata file for the test data directory. No concerns.

MCPForUnity/UnityMcpServer~/src/module_discovery.py (1)

13-55: Good refactoring to centralize module discovery.

The implementation correctly discovers modules at both the top level and one level deep in subdirectories, with appropriate filtering of private modules and error handling. The broad Exception catches flagged by Ruff are actually appropriate here—import failures can stem from various causes (syntax errors, missing dependencies, etc.), and the discovery system should continue loading other modules rather than halt entirely.

One minor suggestion: consider validating that base_dir exists and is a directory before attempting discovery, to provide clearer error messages if misconfigured.

MCPForUnity/UnityMcpServer~/src/resources/__init__.py (1)

11-11: Clean refactoring to use centralized module discovery.

The changes correctly replace the previous inline discovery logic with the new discover_modules utility, and the docstring accurately reflects that subdirectories are now included in discovery. Using list() to consume the generator ensures all modules are imported before proceeding with registration.

Also applies to: 23-23, 30-31

MCPForUnity/UnityMcpServer~/src/tools/__init__.py (1)

11-11: Clean refactoring mirroring the resources discovery pattern.

The changes are consistent with the resources module refactoring, replacing inline discovery logic with the centralized discover_modules utility. The docstring update correctly documents the subdirectory discovery capability.

Also applies to: 23-23, 30-31

MCPForUnity/Editor/Helpers/ServerInstaller.cs (1)

68-68: LGTM!

The added blank lines improve code readability without affecting functionality.

Also applies to: 414-414

MCPForUnity/Editor/Services/PythonToolRegistryService.cs.meta (1)

1-11: Meta file looks standard.

GUID committed; no concerns.

MCPForUnity/Editor/Data/PythonToolsAsset.cs.meta (1)

1-11: Meta file OK.

No action needed.

MCPForUnity/Editor/Services/IPythonToolRegistryService.cs.meta (1)

1-11: Interface meta looks good.

Consistent MonoImporter settings.

MCPForUnity/Editor/Services/ToolSyncService.cs.meta (1)

1-11: Service meta verified.

Nothing to change.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PythonToolRegistryServiceTests.cs.meta (1)

1-11: Test meta fine.

Committed GUID; all good.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Data/PythonToolsAssetTests.cs (1)

1-204: Nice coverage of asset behaviors.

Tests exercise filtering, hashing toggles, state add/update, and cleanup paths. Looks solid.

MCPForUnity/Editor/Services/IPythonToolRegistryService.cs (1)

7-13: Interface contract is tight and clear.

Methods map 1:1 to asset operations and hashing. LGTM.

MCPForUnity/Editor/Services/MCPServiceLocator.cs (2)

13-15: Service wiring looks correct.

Lazy init + Register/Reset updates are consistent. No issues.

Also applies to: 20-23, 53-56, 60-63


37-42: Review comment is incorrect; no typo exists in the code.

The verification shows that ToolSyncService.cs line 28 already uses the correct camelCase naming _registryService, not snake_case _registry_service. The field is declared on line 12 as _registryService and used consistently in camelCase throughout the file (lines 16, 28, 49, 57). There is no CS0103 compile error and no fix is needed.

Likely an incorrect or invalid review comment.

MCPForUnity/Editor/Services/PythonToolRegistryService.cs (1)

14-25: Registry discovery and state updates look solid.

AssetDatabase queries and SetDirty usage are appropriate for Editor context. No issues.

Also applies to: 27-40

MCPForUnity/Editor/Services/ToolSyncService.cs (1)

39-41: AssetDatabase batching is fine here

Start/StopAssetEditing bracketing is correct to minimize reimports; SaveAssets afterwards is appropriate for registry changes.

Ensure no code inside the try writes back into Assets/ to avoid long import stalls during large syncs.

Also applies to: 87-93

MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs (1)

144-161: Editor UX helpers look good

ReimportPythonFiles/ManualSync/ToggleAutoSync are straightforward and useful. Nice touch to default auto‑sync to on.

Also applies to: 166-171, 176-181

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PythonToolRegistryServiceTests.cs (1)

59-76: Happy path hash tests are good

Hash compute + NeedsSync/RecordSync flow is covered. Nice.

Comment on lines +37 to +43
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;
}
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 18, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

-        public bool NeedsSync(TextAsset file, string currentHash)
+        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));
+            if (!useContentHashing) return true; // Always sync if hashing disabled
+            if (!TryGetAssetGuid(file, out var guid)) return true; // treat as needing sync but don't record
+            var state = fileStates.FirstOrDefault(s => s.assetGuid == guid);
             return state == null || state.contentHash != currentHash;
         }
@@
-        public void RecordSync(TextAsset file, string hash)
+        public void RecordSync(TextAsset file, string hash)
         {
-            string guid = GetAssetGuid(file);
-            var state = fileStates.FirstOrDefault(s => s.assetGuid == guid);
+            if (!TryGetAssetGuid(file, out var guid))
+            {
+                Debug.LogWarning($"[{nameof(PythonToolsAsset)}] Skipping sync state for non-asset TextAsset '{file?.name}'. Save it under Assets/ to track.");
+                return;
+            }
+            var state = fileStates.FirstOrDefault(s => s.assetGuid == guid);
@@
-        private string GetAssetGuid(TextAsset asset)
-        {
-            return UnityEditor.AssetDatabase.AssetPathToGUID(UnityEditor.AssetDatabase.GetAssetPath(asset));
-        }
+        private string GetAssetGuid(TextAsset asset)
+        {
+            return UnityEditor.AssetDatabase.AssetPathToGUID(UnityEditor.AssetDatabase.GetAssetPath(asset));
+        }
+
+        private bool TryGetAssetGuid(TextAsset asset, out string guid)
+        {
+            guid = UnityEditor.AssetDatabase.AssetPathToGUID(UnityEditor.AssetDatabase.GetAssetPath(asset));
+            return !string.IsNullOrEmpty(guid);
+        }

Also applies to: 48-62, 73-76

🤖 Prompt for AI Agents
In MCPForUnity/Editor/Data/PythonToolsAsset.cs around lines 37-43 (also apply
same check to ranges 48-62 and 73-76): AssetDatabase.GetAssetPath returns an
empty path for non-asset/unsaved TextAsset which makes AssetPathToGUID return an
empty GUID, causing collisions in fileStates; modify methods that call
GetAssetGuid/GetAssetPath to detect empty asset paths (or empty GUIDs) and skip
processing those TextAssets (return false from NeedsSync, or do not add/update
entries in fileStates) so non-asset TextAssets are ignored. Ensure the guard is
applied consistently in all three affected ranges so you never use an empty GUID
as a key.

Copy link
Contributor Author

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.

Copy link
Contributor

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
Learnt from: JohanHoltby
PR: CoplayDev/unity-mcp#309
File: MCPForUnity/Editor/Helpers/ServerInstaller.cs:478-508
Timestamp: 2025-10-13T13:41:00.086Z
Learning: In the MCPForUnityTools feature (MCPForUnity/Editor/Helpers/ServerInstaller.cs), the design intentionally forces users to have only one .py file per MCPForUnityTools folder to keep file tracking simple. Package-style tools (subdirectories with __init__.py) are not supported.

Comment on lines +20 to 23
public static IPythonToolRegistryService PythonToolRegistry => _pythonToolRegistryService ??= new PythonToolRegistryService();
public static ITestRunnerService Tests => _testRunnerService ??= new TestRunnerService();
public static IToolSyncService ToolSync => _toolSyncService ??= new ToolSyncService();

Copy link
Contributor

@coderabbitai coderabbitai bot Oct 18, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Name collision risk in sync output (cross-file).

ToolSyncService writes to dest as file.name + ".py". Two assets with same file name will overwrite each other, and cleanup may delete the other’s file. Consider namespacing by asset or GUID.

- string destPath = Path.Combine(destToolsDir, file.name + ".py");
+ var fileGuid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(file));
+ string safeSuffix = fileGuid.Substring(0, 8);
+ string destPath = Path.Combine(destToolsDir, $"{file.name}.{safeSuffix}.py");

Also update the else-branch and syncedFiles set similarly, and keep cleanup keyed to these paths.


🏁 Script executed:

#!/bin/bash
# Grep current destPath construction for both branches to update consistently
rg -n -C1 $'Path\\.Combine\\(destToolsDir,\\s*file\\.name\\s*\\+\\s*\"\\.py\"\\)' MCPForUnity/Editor/Services/ToolSyncService.cs

Length of output: 354


🏁 Script executed:

#!/bin/bash
# Get broader context around both occurrences to understand function structure
rg -n -B10 -A5 'string destPath = Path\.Combine\(destToolsDir, file\.name \+ "\.py"\)' MCPForUnity/Editor/Services/ToolSyncService.cs

Length of output: 1571


🏁 Script executed:

#!/bin/bash
# Search for cleanup or removal logic that might reference these synced paths
rg -n -A3 -B3 'syncedFiles' MCPForUnity/Editor/Services/ToolSyncService.cs

Length of output: 1226


🏁 Script executed:

#!/bin/bash
# Check for any GUID or asset path references already in use
rg -n 'AssetDatabase\|GetAssetPath\|AssetPathToGUID\|GUID' MCPForUnity/Editor/Services/ToolSyncService.cs

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find CleanupStaleFiles method implementation
rg -n -A20 'void CleanupStaleFiles' MCPForUnity/Editor/Services/ToolSyncService.cs

Length of output: 959


🏁 Script executed:

#!/bin/bash
# Find the file object type - check the registry interface/implementation
rg -n -B5 'foreach.*file.*registry\.GetValidFiles' MCPForUnity/Editor/Services/ToolSyncService.cs | head -20

Length of output: 256


🏁 Script executed:

#!/bin/bash
# Search for IPythonToolRegistry or related interface to understand file object
rg -n 'interface.*Registry\|class.*Registry' MCPForUnity/Editor/Services/ --type cs

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find registry interface and file type - broader search
rg -n 'GetValidFiles\|interface IPython' MCPForUnity/ -A3 --type cs

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check imports in ToolSyncService.cs
head -30 MCPForUnity/Editor/Services/ToolSyncService.cs

Length of output: 899


🏁 Script executed:

#!/bin/bash
# Search for file object definition or property
rg -n 'class.*PythonTool\|struct.*PythonTool' MCPForUnity/ --type cs

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for service implementations and interfaces more broadly
fd -t f -e cs . MCPForUnity/Editor/Services/ | head -20

Length of output: 682


🏁 Script executed:

#!/bin/bash
# Search for GetValidFiles definition
rg -n 'GetValidFiles' MCPForUnity/ --type cs -A5

Length of output: 1576


🏁 Script executed:

#!/bin/bash
# Search for properties like .name and .text on potential file types
rg -n '\.name.*\.text\|file\.name\|file\.text' MCPForUnity/ --type cs -B3 -A1 | head -40

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Get GetAssetGuid implementation and nearby code
rg -n -B3 -A8 'private string GetAssetGuid' MCPForUnity/Editor/Data/PythonToolsAsset.cs

Length of output: 471


🏁 Script executed:

#!/bin/bash
# Check IPythonToolRegistryService interface for available methods
cat MCPForUnity/Editor/Services/IPythonToolRegistryService.cs

Length of output: 423


Fix name collision in ToolSyncService file naming to prevent overwrites and stale cleanup issues.

The collision risk is valid: multiple TextAssets with identical names overwrite each other's destination path (lines 51 and 65 both construct Path.Combine(destToolsDir, file.name + ".py")). Since only the final written file path enters syncedFiles, CleanupStaleFiles will incorrectly delete the overwritten files as "stale."

The suggested GUID-based namespacing is sound and implementable. Note that PythonToolsAsset already has a private GetAssetGuid method using the same pattern, but since it's not exposed through IPythonToolRegistryService, the inline approach in the suggestion is the viable path. Ensure both branches (lines 51 and 65) and the cleanup keying use the namespaced paths consistently.

🤖 Prompt for AI Agents
In MCPForUnity/Editor/Services/MCPServiceLocator.cs around lines 20-23 (and
update usages at lines ~51 and ~65 in the ToolSyncService implementation), there
is a filename collision risk because TextAssets are written to destToolsDir
using only file.name + ".py", which causes overwrites and makes
CleanupStaleFiles delete overwritten assets as stale; fix it by namespacing
generated filenames with a stable GUID per asset (e.g., compute or derive the
asset GUID similarly to PythonToolsAsset.GetAssetGuid and prepend or append it
to file.name), apply the same GUID-based naming on both branches where files are
written, and ensure the same namespaced path/key is what gets stored in
syncedFiles so CleanupStaleFiles uses the correct keys to identify stale files
consistently.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's OK to expect tools to have different names

Copy link
Contributor

Choose a reason for hiding this comment

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

@msanatan, understood! That's a reasonable constraint for the tool system. I'll note that unique tool names are expected.

Comment on lines +42 to +67
foreach (var registry in registries)
{
foreach (var file in registry.GetValidFiles())
{
try
{
// Check if needs syncing (hash-based or always)
if (_registryService.NeedsSync(registry, file))
{
string destPath = Path.Combine(destToolsDir, file.name + ".py");

// Write the Python file content
File.WriteAllText(destPath, file.text);

// Record sync
_registryService.RecordSync(registry, file);

result.CopiedCount++;
syncedFiles.Add(destPath);
McpLog.Info($"Synced Python tool: {file.name}.py");
}
else
{
string destPath = Path.Combine(destToolsDir, file.name + ".py");
syncedFiles.Add(destPath);
result.SkippedCount++;
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 18, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Detect duplicates and sanitize file names before writing

Two tools with the same asset name will overwrite each other silently; invalid chars may also break paths. Sanitize and enforce uniqueness.

-                        foreach (var file in registry.GetValidFiles())
+                        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+                        foreach (var file in registry.GetValidFiles())
                         {
@@
-                                if (_registryService.NeedsSync(registry, file))
+                                var safeName = SanitizeFileName(file.name);
+                                string destPath = Path.Combine(destToolsDir, safeName + ".py");
+                                if (!seen.Add(destPath))
+                                {
+                                    result.ErrorCount++;
+                                    result.Messages.Add($"Duplicate tool name '{safeName}.py' across registries; skipping subsequent copies.");
+                                    continue;
+                                }
+                                if (_registryService.NeedsSync(registry, file))
                                 {
-                                    string destPath = Path.Combine(destToolsDir, file.name + ".py");
-
                                     // Write the Python file content
-                                    File.WriteAllText(destPath, file.text);
+                                    File.WriteAllText(destPath, file.text, new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
@@
-                                    string destPath = Path.Combine(destToolsDir, file.name + ".py");
                                     syncedFiles.Add(destPath);

Add helper:

     }
-}
+    private static string SanitizeFileName(string name)
+    {
+        foreach (var c in System.IO.Path.GetInvalidFileNameChars())
+            name = name.Replace(c, '_');
+        return name.Trim();
+    }
+}
🤖 Prompt for AI Agents
In MCPForUnity/Editor/Services/ToolSyncService.cs around lines 42 to 67, the
current loop writes files using raw asset names which can contain invalid path
chars and can collide when two tools share a name; update the code to sanitize
file names (remove/replace invalid path characters and trim whitespace) and
enforce uniqueness before writing to destToolsDir by generating a safe base name
and, if that name already exists in syncedFiles (or on disk), append a
deterministic disambiguator (incrementing counter or short hash of asset id) to
the file name; implement a small helper method (e.g.,
GetUniqueSanitizedFilename(registry, file, destDir, HashSet<string> existing))
that returns the safe, unique file name and use it for both the copy and the
syncedFiles tracking so duplicates are not overwritten and paths remain valid.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As mentioned before, it's OK to expect tools to have different names

Copy link
Contributor

Choose a reason for hiding this comment

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

@msanatan Understood! If tools are expected to have unique names as part of the usage pattern, then the additional duplicate detection isn't necessary. Thank you for clarifying the design decision.

Comment on lines +103 to +132
private void CleanupStaleFiles(string destToolsDir, HashSet<string> currentFiles)
{
try
{
if (!Directory.Exists(destToolsDir)) return;

// Find all .py files in destination that aren't in our current set
var existingFiles = Directory.GetFiles(destToolsDir, "*.py");

foreach (var file in existingFiles)
{
if (!currentFiles.Contains(file))
{
try
{
File.Delete(file);
McpLog.Info($"Cleaned up stale tool: {Path.GetFileName(file)}");
}
catch (Exception ex)
{
McpLog.Warn($"Failed to cleanup {file}: {ex.Message}");
}
}
}
}
catch (Exception ex)
{
McpLog.Warn($"Failed to cleanup stale files: {ex.Message}");
}
}
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 18, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Cleanup deletes any .py in destToolsDir, including user‑managed files

Removing all non‑current .py files in tools/custom risks deleting hand‑placed tools or files managed outside this sync. Constrain cleanup to a dedicated subfolder (e.g., tools/custom/unity-managed) or manage a manifest of files created by this service and delete only those.

Follow‑up: If you adopt a subfolder, update PythonToolSyncProcessor to write into that subfolder and keep current broad cleanup.

Copy link
Contributor Author

@msanatan msanatan Oct 18, 2025

Choose a reason for hiding this comment

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

Fair enough, but it's very unlikely that the user will put something else there

Copy link
Contributor

Choose a reason for hiding this comment

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

@msanatan, that's a fair point—tools/custom is specifically intended for this sync workflow, and users have the PythonToolsAsset system for adding tools properly. If someone manually places files there, they're working outside the intended pattern.

I'll accept your judgment on this. The risk is low given the specific directory and clear workflow.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs (1)

147-157: Consider using AssetDatabase.FindAssets for better performance.

While the current approach works correctly, iterating all assets can be slow on large projects. Consider using AssetDatabase.FindAssets with a path filter for better performance:

-string[] allAssets = AssetDatabase.GetAllAssetPaths();
-int count = 0;
-
-foreach (string path in allAssets)
-{
-    if (path.EndsWith(".py") && path.StartsWith("Assets/"))
-    {
-        AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
-        count++;
-    }
-}
+var pythonGuids = AssetDatabase.FindAssets("t:DefaultAsset", new[] { "Assets" })
+    .Select(AssetDatabase.GUIDToAssetPath)
+    .Where(path => path.EndsWith(".py"))
+    .ToArray();
+
+foreach (string path in pythonGuids)
+{
+    AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
+}
+
+int count = pythonGuids.Length;

Since this is a manual action, the current implementation is acceptable, but the optimization would improve responsiveness on larger projects.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1d6c55b and 1c676c4.

📒 Files selected for processing (1)
  • MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs (1)
MCPForUnity/Editor/Services/MCPServiceLocator.cs (1)
  • MCPServiceLocator (8-64)
🔇 Additional comments (8)
MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs (8)

1-20: LGTM!

The class structure and attributes are well-designed for Unity's asset pipeline. The InitializeOnLoad and AssetPostprocessor combination appropriately enables both startup synchronization and automatic monitoring of Python file changes.


22-32: LGTM!

Using EditorApplication.delayCall to defer the startup sync is the correct Unity pattern, ensuring the sync runs after Unity finishes initialization.


37-71: LGTM!

The asset monitoring logic correctly guards against re-entrancy and efficiently detects Python file changes across import, move, and delete operations.


76-122: LGTM!

The synchronization logic is well-structured with appropriate re-entrancy protection, error handling, and logging. The try-finally block ensures the _isSyncing flag is always reset, preventing deadlock scenarios.


127-139: LGTM!

The auto-sync enable/disable methods correctly use EditorPrefs for persistence, with an appropriate default of true to match the PR's intent of automatic synchronization by default.


166-171: LGTM!

The manual sync menu item provides a clear way for users to trigger synchronization on demand.


176-180: LGTM!

The toggle implementation correctly inverts the current state.


185-190: Past issue resolved - paths now match correctly!

The menu path in Menu.SetChecked (line 188) now correctly matches the MenuItem attribute path (line 176), so the checkbox will toggle properly. The previous review concern has been addressed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
MCPForUnity/Editor/Services/IPythonToolRegistryService.cs (1)

7-13: Consider adding XML documentation for the public API.

To improve maintainability and developer experience, add XML documentation comments to clarify the contract:

  • GetAllRegistries: What registries are discovered and from where
  • NeedsSync: Return value meaning (true = needs sync, false = up-to-date) and behavior when hashing is disabled
  • RecordSync: Side effects (updates registry state, marks asset dirty)
  • ComputeHash: Return format (SHA-256 as lowercase hex) and null handling (returns empty string)

Example:

+    /// <summary>
+    /// Service for managing Python tool registry assets and synchronization state.
+    /// </summary>
     public interface IPythonToolRegistryService
     {
+        /// <summary>
+        /// Discovers all PythonToolsAsset instances in the project.
+        /// </summary>
+        /// <returns>Enumerable of all registry assets found via AssetDatabase.</returns>
         IEnumerable<PythonToolsAsset> GetAllRegistries();
+        
+        /// <summary>
+        /// Determines whether a Python file needs synchronization to the server.
+        /// </summary>
+        /// <param name="registry">The registry tracking the file state.</param>
+        /// <param name="file">The Python file to check.</param>
+        /// <returns>True if the file needs sync (hash mismatch or hashing disabled); otherwise false.</returns>
         bool NeedsSync(PythonToolsAsset registry, TextAsset file);
+        
+        /// <summary>
+        /// Records that a file has been synchronized, updating its hash and timestamp.
+        /// Marks the registry asset dirty to persist changes.
+        /// </summary>
+        /// <param name="registry">The registry to update.</param>
+        /// <param name="file">The synchronized Python file.</param>
         void RecordSync(PythonToolsAsset registry, TextAsset file);
+        
+        /// <summary>
+        /// Computes a SHA-256 hash of the file content.
+        /// </summary>
+        /// <param name="file">The file to hash.</param>
+        /// <returns>Lowercase hexadecimal SHA-256 hash, or empty string if file is null or empty.</returns>
         string ComputeHash(TextAsset file);
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1c676c4 and b0ec0eb.

📒 Files selected for processing (3)
  • MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs (1 hunks)
  • MCPForUnity/Editor/Services/IPythonToolRegistryService.cs (1 hunks)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolSyncServiceTests.cs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolSyncServiceTests.cs
🧰 Additional context used
🧬 Code graph analysis (1)
MCPForUnity/Editor/Services/IPythonToolRegistryService.cs (2)
MCPForUnity/Editor/Data/PythonToolsAsset.cs (2)
  • NeedsSync (37-43)
  • RecordSync (48-62)
MCPForUnity/Editor/Services/PythonToolRegistryService.cs (3)
  • NeedsSync (27-33)
  • RecordSync (35-40)
  • ComputeHash (42-53)
🔇 Additional comments (1)
MCPForUnity/Editor/Services/IPythonToolRegistryService.cs (1)

7-13: LGTM! Clean interface design.

The interface provides a well-structured contract for managing Python tool registries with clear responsibilities: discovery (GetAllRegistries), synchronization checks (NeedsSync), state tracking (RecordSync), and content hashing (ComputeHash). The design follows SOLID principles and enables dependency injection and testing.

@msanatan msanatan merged commit 85cc93f into CoplayDev:main Oct 18, 2025
1 check passed
@msanatan msanatan deleted the feature/create-tools-in-asset-folder branch October 18, 2025 04:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Py files not found and tracked by external tools added.

2 participants