Skip to content

Conversation

dsarno
Copy link
Owner

@dsarno dsarno commented Aug 16, 2025

Just want code rabbit review not actually ready

Summary by CodeRabbit

  • New Features

    • Automatic first-run setup and embedded Python server installation.
    • Expanded client support (Cursor, Claude Code, Windsurf, Claude Desktop, VSCode Copilot) with quick registration.
    • Advanced script tools: apply targeted edits, validate syntax, safer create/delete.
    • Robust, framed Unity connection with per-project port persistence and status heartbeat.
  • Improvements

    • Faster, more reliable console log classification.
    • Wider Python compatibility (3.10+).
  • Documentation

    • Rebranded to CoplayDev; updated install/config guides.
    • New Windows UV path and macOS Claude CLI troubleshooting guides.
  • Chores

    • Package renamed to com.coplaydev.unity-mcp and version bumped.

msanatan and others added 30 commits August 6, 2025 20:48
…stop on domain reload; start/stop locking; per-project sticky ports + brief release wait; Python discovery scans hashed+legacy files and probes; editor window live status refresh.
…ename auto-run toggle to client section ("Auto-connect to MCP Clients"); rename button to "Run Client Setup"; fix dev-mode status by using FindPackagePythonDirectory() for Claude/Desktop path checks
…nstaller

- Switch ServerInstaller to embedded copy-only (no network)
- Simplify Editor UI server status to 'Installed (Embedded)'
- Vendor UnityMcpServer/src into UnityMcpBridge/UnityMcpServer/src for UPM distribution
- Keep bridge recompile robustness (heartbeat + sticky port)
…paths case-insensitively to prevent sticky-port drift across reloads
… port and let bind micro-retry handle release to avoid port swapping on recompiles
…etup with Connected ✓ state; add Debug Logs toggle and gate verbose logs

fix(bridge): reuse stored port in StartAutoConnect; guard listener stop to avoid ObjectDisposedException

chore(clients): reorder dropdown to Cursor, Claude Code, Windsurf, Claude Desktop, VSCode
…v.unity-mcp to comply with Unity package naming standards
…opies embedded server, adds RepairPythonEnvironment() (deletes .venv, runs 'uv sync'); robust uv path discovery; macOS install path -> Application Support\n- UI: Server Status shows Installed(Embedded); Python missing warning with install link; Repair button tooltip; header Show Debug Logs; cleaned layout\n- Python: unpin .python-version; set requires-python >=3.10 in both pyprojects\n- Dev: improved package/dev path resolution
…hind Debug Logs toggle; improve Python and UV detection on Windows (flex versions, where.exe/Path scan); tidy installer messages
dsarno added 27 commits August 12, 2025 21:09
… NVM version parse robustness; help labels [HELP]
…tion-ui

UX and Fix: Claude Code detection + UV gating (Cursor/Windsurf) + VSCode mcp.json schema
…ere.exe); Unregister UX: use 'claude mcp get' exit codes; stop PATH prepend on Windows; safer detection when unregistered
…argparse version (auto-detect manifest/repo; supports interactive/non-interactive)
…rve existing uv command; Claude unregister UI fix; .ps1 handling; add generic mcp_source.py
Windows UV stability: prefer WinGet Links, preserve pinned uv path; Claude Code unregister UI; add generic mcp_source.py
…add ResolveServerSrc; block PackageCache unless dev override; canonical uv args
…etection; atomic writes; uv validation; WinGet Links ordering
…de) after LOCALAPPDATA; drop ProgramData; update comment
Config stability: preserve user configs, prefer installed server, safer uv resolution, XDG paths (Linux)
slight cleanup
…ove blob stream tools; simplify tool registration

- Python server: always consume handshake and negotiate framing on reconnects (prevents invalid framed length).\n- C#: strict FRAMING=1 handshake and NoDelay; debounce AssetDatabase/compilation.\n- Tools: keep manage_script + script edits; remove manage_script_stream and test tools from default registration.\n- Editor window: guard against auto retargeting IDE config.
…Exact, safe write framing; remove unused vars
… MCP edit ops; mitigate false 'no opening brace' errors and allow relaxed validation for text edits
…ority and server apply_text_edits/validate; add resources list/read; deprecate manage_script read/update/edit; remove stdout prints; tweak connection handshake logging
Copy link

coderabbitai bot commented Aug 16, 2025

Walkthrough

Rebrands to Coplay, embeds the Python server within the Unity package, adds robust auto-setup and tooling, introduces framed TCP I/O, per-project port/state files, client config remapping, and extensive Windows/macOS path resolution. Updates docs, scripts, and tests. Replaces legacy Git-based install with local copy workflow and retry-capable command routing.

Changes

Cohort / File(s) Summary
Docs & Branding
README.md, README-DEV.md, LICENSE, CursorHelp.md, claude-chunk.md
Rebrand to Coplay; expanded setup/troubleshooting (UV path, client-specific guidance); added Windows/macOS notes; updated license holder.
Packaging & Utilities
UnityMcpBridge/package.json, package.json, deploy-dev.bat, restore-dev.bat, mcp_source.py
Package renamed to com.coplaydev.unity-mcp with metadata; dev scripts point to embedded server path; new CLI to switch package source.
MCP Clients Mapping
UnityMcpBridge/Editor/Data/McpClients.cs
Redefined clients (Cursor, Claude Code, Windsurf, Claude Desktop, VSCode) with new config paths/types; added constructor to initialize statuses.
Editor Helpers (Paths/Install/Ports)
UnityMcpBridge/Editor/Helpers/ExecPath.cs, .../PackageInstaller.cs, .../PortManager.cs, .../ServerInstaller.cs, .../ServerPathResolver.cs
New path resolution for Claude/UV; first-load server installer from embedded sources; per-project port persistence and status files; repair Python env; embedded source discovery.
Unity Bridge & Tools
UnityMcpBridge/Editor/UnityMcpBridge.cs, .../Tools/ReadConsole.cs, .../Tools/ManageScript.cs, .../Windows/UnityMcpEditorWindow.cs
Deferred initialization with retries; enforced framed I/O; heartbeat/status files; enhanced console parsing; expanded script management (edits/validation); editor UI auto-setup and Claude integration.
Models
UnityMcpBridge/Editor/Models/MCPConfigServer.cs
Adds optional “type” JSON field for transport/type hints.
Embedded Python Server (new/updated)
UnityMcpBridge/UnityMcpServer~/src/server.py, unity_connection.py, port_discovery.py, tools/*, config.py, pyproject.toml, pyrightconfig.json
New MCP server with lifecycle, resources, and tool registration; framed/legacy protocol handling; retryable Unity command helpers; new/manage_script tools (including surgical edits); faster retry/backoff; Python 3.10+.
Legacy Python Removal
UnityMcpServer/src/server.py, unity_connection.py, tools/manage_script.py, port_discovery.py, UnityMcpServer/src/.python-version
Removes prior standalone server/connection and script tool; drops old port discovery and Python version pin.
Tests
test_unity_socket_framing.py, tests/test_script_tools.py
Adds framing test harness; unit tests for script tools and asset management with mocked retries.
Unity Meta Files
UnityMcpBridge/**/**/*.meta, UnityMcpBridge/Runtime/Serialization/UnityTypeConverters.cs.meta
Adds/expands MonoImporter metadata blocks; no runtime logic changes.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant UnityEditor
  participant PackageInstaller
  participant ServerInstaller
  participant PythonServer
  participant MCPClient

  User->>UnityEditor: Open project / Enable package
  UnityEditor->>PackageInstaller: InitializeOnLoad
  PackageInstaller->>ServerInstaller: EnsureServerInstalled()
  ServerInstaller-->>PackageInstaller: Embedded copy ready
  UnityEditor->>UnityEditor: Start UnityMcpBridge (framed TCP)
  MCPClient->>PythonServer: Launch (stdio MCP)
  PythonServer->>UnityEditor: Discover port (status/registry) + connect
  PythonServer-->>MCPClient: MCP ready
Loading
sequenceDiagram
  participant MCPClient
  participant PythonServer
  participant UnityBridge

  MCPClient->>PythonServer: Tool request (e.g., manage_script)
  PythonServer->>UnityBridge: send_command_with_retry(...)
  UnityBridge-->>PythonServer: Framed response JSON
  PythonServer-->>MCPClient: Normalized dict result
Loading
sequenceDiagram
  participant UnityBridge
  participant FS
  participant Status

  UnityBridge->>FS: Read/write per-project port file
  UnityBridge->>Status: Write heartbeat unity-mcp-status-<hash>.json
  Note right of Status: Includes port, state, timestamps
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • feat: add initial script edit tests #3 — Also modifies ManageScript tooling (apply edits, SHA256 preconditions, validation), overlapping with this PR’s ManageScript and surgical edits changes.

Suggested labels

codex

Poem

A hop, a skip, I wire the bridge just right—
Frames click-clack through Unity’s night.
Ports remember, scripts refactor neat,
UV found, Claude taps its feet.
Bundled server snug and warm,
I thump approval—onward, transform! 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch protocol-framing

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@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: 12

🔭 Outside diff range comments (2)
UnityMcpBridge/Editor/Tools/ReadConsole.cs (1)

62-69: Add fallback for older Unity versions: also reflect StopGettingEntries

Some Unity versions expose StopGettingEntries instead of EndGettingEntries. Currently, reflection throws if EndGettingEntries is missing, breaking console reads. Try both and only fail if neither exists.

-                // Try reflecting EndGettingEntries based on warning message
-                _endGettingEntriesMethod = logEntriesType.GetMethod(
-                    "EndGettingEntries",
-                    staticFlags
-                );
-                if (_endGettingEntriesMethod == null)
-                    throw new Exception("Failed to reflect LogEntries.EndGettingEntries");
+                // Try reflecting EndGettingEntries; fall back to StopGettingEntries for older Unity
+                _endGettingEntriesMethod = logEntriesType.GetMethod("EndGettingEntries", staticFlags);
+                if (_endGettingEntriesMethod == null)
+                {
+                    _endGettingEntriesMethod = logEntriesType.GetMethod("StopGettingEntries", staticFlags);
+                }
+                if (_endGettingEntriesMethod == null)
+                    throw new Exception("Failed to reflect LogEntries.EndGettingEntries/StopGettingEntries");
UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs (1)

1839-1873: Extract duplicate UV validation logic

The IsValidUvInstallation method duplicates UV validation logic that appears in multiple places. This should use a shared implementation, potentially from ServerInstaller.ValidateUvBinary.

 private bool IsValidUvInstallation(string uvPath)
 {
-    try
-    {
-        var psi = new ProcessStartInfo
-        {
-            FileName = uvPath,
-            Arguments = "--version",
-            UseShellExecute = false,
-            RedirectStandardOutput = true,
-            RedirectStandardError = true,
-            CreateNoWindow = true
-        };
-        
-        using var process = Process.Start(psi);
-        process.WaitForExit(5000); // 5 second timeout
-        
-        if (process.ExitCode == 0)
-        {
-            string output = process.StandardOutput.ReadToEnd().Trim();
-            // Basic validation - just check if it responds with version info
-            // UV typically outputs "uv 0.x.x" format
-            if (output.StartsWith("uv ") && output.Contains("."))
-            {
-                return true;
-            }
-        }
-        
-        return false;
-    }
-    catch
-    {
-        return false;
-    }
+    // Delegate to the shared validation method in ServerInstaller
+    return ServerInstaller.ValidateUvBinary(uvPath);
 }

Also applies to: 1875-2037

♻️ Duplicate comments (1)
UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs (1)

539-557: Extract SHA1 computation to shared utility

This ComputeSha1 method duplicates the hash computation logic found in UnityMcpBridge.cs and PortManager.cs. It should use the shared utility suggested earlier.

-private static string ComputeSha1(string input)
-{
-    try
-    {
-        using SHA1 sha1 = SHA1.Create();
-        byte[] bytes = Encoding.UTF8.GetBytes(input ?? string.Empty);
-        byte[] hash = sha1.ComputeHash(bytes);
-        StringBuilder sb = new StringBuilder(hash.Length * 2);
-        foreach (byte b in hash)
-        {
-            sb.Append(b.ToString("x2"));
-        }
-        return sb.ToString();
-    }
-    catch
-    {
-        return "";
-    }
-}

// Update the call site:
-string key = $"UnityMCP.AutoRegistered.{ComputeSha1(projectPath)}";
+string key = $"UnityMCP.AutoRegistered.{HashUtility.ComputeProjectHash(projectPath)}";
🧹 Nitpick comments (85)
package.json (1)

1-1: Avoid accidental publish/tooling confusion by adding minimal metadata (or remove the file if unnecessary).

An empty root package.json can:

  • Cause npm/yarn/pnpm to treat the repo root as a Node package unintentionally.
  • Risk accidental npm publish.
  • Interfere with tooling that auto-detects workspaces or expects fields like “name”/“license”.

If you need this manifest, set it to a safe minimal config. Otherwise, consider removing it.

Apply this diff for a safe minimal manifest:

-{}
+{
+  "name": "unity-mcp",
+  "private": true,
+  "license": "MIT"
+}
UnityMcpBridge/UnityMcpServer~/src/pyrightconfig.json (1)

1-4: reportMissingImports: "none" hides real problems; point Pyright at your venv instead and surface warnings.

Given this repo now embeds the Python server and has auto-setup, it’s better to let Pyright locate your venv and warn on missing imports rather than suppressing them entirely.

Consider this tweak (assuming a venv at UnityMcpBridge/UnityMcpServer~/.venv):

 {
   "typeCheckingMode": "basic",
-  "reportMissingImports": "none"
+  "reportMissingImports": "warning",
+  "venvPath": "..",
+  "venv": ".venv"
 }

If the venv path differs, adjust venvPath/venv accordingly. You can keep “basic” mode for now and tighten later.

restore-dev.bat (3)

22-22: Avoid hardcoding a specific version in the example path

Using a placeholder reduces churn and confusion when versions update.

Apply this diff:

-echo Example: X:\UnityProject\Library\PackageCache\com.coplaydev.unity-mcp@1.0.0
+echo Example: X:\UnityProject\Library\PackageCache\com.coplaydev.unity-mcp@<version>

66-70: Guard against non-numeric input for backup selection

If a user types non-numeric input, the numeric comparisons can error out. Add a quick numeric regex check before the lss/gtr comparisons.

Apply this diff:

 :: Validate choice
-if "%choice%"=="" goto :invalid_choice
-if %choice% lss 1 goto :invalid_choice
-if %choice% gtr %counter% goto :invalid_choice
+if "%choice%"=="" goto :invalid_choice
+rem Ensure choice is numeric
+echo %choice% | findstr /r "^[0-9][0-9]*$" >nul || goto :invalid_choice
+if %choice% lss 1 goto :invalid_choice
+if %choice% gtr %counter% goto :invalid_choice

120-147: Consider using robocopy for more robust copies and clearer exit codes

xcopy exit codes can be ambiguous. robocopy is more resilient (preserves directory trees, better error handling). Not required, but improves reliability.

Example refactor for both copy sections (success is ERRORLEVEL < 8):

-    rd /s /q "%PACKAGE_CACHE_PATH%\Editor" 2>nul
-    xcopy "%SELECTED_BACKUP%\UnityBridge\Editor\*" "%PACKAGE_CACHE_PATH%\Editor\" /E /I /Y > nul
-    if !errorlevel! neq 0 (
-        echo Error: Failed to restore Unity Bridge files
-        pause
-        exit /b 1
-    )
+    rd /s /q "%PACKAGE_CACHE_PATH%\Editor" 2>nul
+    robocopy "%SELECTED_BACKUP%\UnityBridge\Editor" "%PACKAGE_CACHE_PATH%\Editor" /MIR /COPY:DAT /R:1 /W:1 /NFL /NDL /NP >nul
+    if !errorlevel! geq 8 (
+        echo Error: Failed to restore Unity Bridge files
+        pause
+        exit /b 1
+    )
@@
-    rd /s /q "%SERVER_PATH%" 2>nul
-    mkdir "%SERVER_PATH%"
-    xcopy "%SELECTED_BACKUP%\PythonServer\*" "%SERVER_PATH%\" /E /I /Y > nul
-    if !errorlevel! neq 0 (
-        echo Error: Failed to restore Python Server files
-        pause
-        exit /b 1
-    )
+    rd /s /q "%SERVER_PATH%" 2>nul
+    robocopy "%SELECTED_BACKUP%\PythonServer" "%SERVER_PATH%" /MIR /COPY:DAT /R:1 /W:1 /NFL /NDL /NP >nul
+    if !errorlevel! geq 8 (
+        echo Error: Failed to restore Python Server files
+        pause
+        exit /b 1
+    )

Note: robocopy is available on modern Windows by default; adjust if you need compatibility with very old environments.

UnityMcpBridge/Editor/Models/MCPConfigServer.cs (1)

15-17: Sensible addition; consider documenting allowed values or constraining via enum

Adding an optional transport “type” is fine and backwards-compatible with NullValueHandling.Ignore. For resilience, consider documenting allowed values (e.g., stdio/framed/etc.) and/or introducing an enum + converter to prevent typos from propagating into configs. Also double-check downstream readers that may switch on this field.

Would you like me to sketch an enum-based approach using StringEnumConverter while keeping the serialized name as "type"?

claude-chunk.md (1)

35-36: Add an actual link target for “More details”

“See: Troubleshooting Unity MCP and Claude Code” lacks a hyperlink. Link to the canonical doc (README section or a dedicated troubleshooting page) so users can follow through.

Point me to the intended doc and I’ll wire up the exact link.

deploy-dev.bat (2)

12-12: DRY: Build SERVER_SOURCE from BRIDGE_SOURCE

This avoids repeating the package root and stays correct if BRIDGE_SOURCE ever changes.

Apply this diff:

-set "SERVER_SOURCE=%SCRIPT_DIR%UnityMcpBridge\UnityMcpServer~\src"
+set "SERVER_SOURCE=%BRIDGE_SOURCE%\UnityMcpServer~\src"

21-23: Update example to current package version or generalize

The example shows @1.0.0 but package.json declares 2.1.1. Update to reduce confusion (or use a placeholder).

Apply this diff:

-echo Example: X:\UnityProject\Library\PackageCache\com.coplaydev.unity-mcp@1.0.0
+echo Example: X:\UnityProject\Library\PackageCache\com.coplaydev.unity-mcp@2.1.1

Optionally: print “com.coplaydev.unity-mcp@”.

UnityMcpBridge/package.json (1)

2-3: Package rename requires consumer migration plan

Renaming to com.coplaydev.unity-mcp is fine but breaks projects pinned to the old name. Ensure README/upgrade notes cover how to switch package names in existing projects (and consider leaving a deprecated stub package if feasible).

I can draft a short “Upgrading from com.justinpbarnett.unity-mcp” section with steps and gotchas.

UnityMcpBridge/UnityMcpServer~/src/config.py (1)

26-32: Mixed time units can be confusing (seconds vs milliseconds)

retry_delay is in seconds while reload_retry_ms is in milliseconds. Consider standardizing units (prefer seconds) or suffixing all fields explicitly with their units to avoid misuse.

Apply one of:

  • Rename reload_retry_ms to reload_retry_s (float) and convert callers.
  • Or add docstrings for each field explicitly stating units and intended usage in client/server handshake responses.
UnityMcpBridge/Editor/Tools/ReadConsole.cs (3)

175-179: “all” should explicitly include assert/exception types

You later special-case Exception/Assert during filtering, but expanding "all" to include them makes behavior clearer and avoids relying on side-effects of “error” coverage.

-                    if (types.Contains("all"))
-                    {
-                        types = new List<string> { "error", "warning", "log" }; // Expand 'all'
-                    }
+                    if (types.Contains("all"))
+                    {
+                        types = new List<string> { "error", "warning", "log", "exception", "assert" };
+                    }

268-275: Severity inference from message is heuristic; prefer mode bits when ambiguous

Relying on substrings like "Exception" can misclassify logs that merely mention exceptions. Recommend flipping the precedence: start with GetLogTypeFromMode(mode), then escalate based on explicit Debug:LogError/LogWarning markers or clear stack-trace signals.

Consider:

  • First compute type = GetLogTypeFromMode(mode).
  • If IsExplicitDebugLog(message) or strong heuristics indicate higher severity, override.
  • Keep current behavior behind a feature flag if cross-version stability is a concern.

451-468: Remove unused remapping helper

GetRemappedTypeForFiltering is not used anymore and can be removed to reduce confusion and maintenance.

-        /// <summary>
-        /// Applies the "one level lower" remapping for filtering, like the old version.
-        /// This ensures compatibility with the filtering logic that expects remapped types.
-        /// </summary>
-        private static LogType GetRemappedTypeForFiltering(LogType unityType)
-        {
-            switch (unityType)
-            {
-                case LogType.Error:
-                    return LogType.Warning; // Error becomes Warning
-                case LogType.Warning:
-                    return LogType.Log; // Warning becomes Log
-                case LogType.Assert:
-                    return LogType.Assert; // Assert remains Assert
-                case LogType.Log:
-                    return LogType.Log; // Log remains Log
-                case LogType.Exception:
-                    return LogType.Warning; // Exception becomes Warning
-                default:
-                    return LogType.Log; // Default fallback
-            }
-        }
test_unity_socket_framing.py (3)

30-46: Legacy JSON reader: consider boundary detection to avoid O(N^2) concatenations

Joining bytes on each recv scales poorly for large payloads. Use a BytesIO buffer or extend chunks only and validate the last few bytes for JSON termination when possible. Not critical for a harness but helps with very large filterText.


47-58: Large filter payload: guard against excessive memory use

FILL * (SIZE_MB * 1024 * 1024) in Python builds a multi-MB string, then JSON-encodes it. For stress tests this is fine; consider a lower default or an environment/arg guard to prevent accidental 100s of MB allocations.


61-73: Handshake robustness

Good handling of optional greeting and framing detection. As a minor enhancement, also tolerate lowercased tokens and trailing metadata (e.g., "FRAMING=1;...").

UnityMcpBridge/UnityMcpServer~/src/port_discovery.py (3)

15-21: Remove unused import 'os'

os is imported but not used. Dropping it keeps the module clean and resolves Ruff F401.

-import os
 import logging

40-55: Be resilient to filesystem races when sorting candidates

p.stat().st_mtime can raise if a file disappears between glob and stat, causing the whole sort to fail. Consider wrapping the stat in a try/except and skipping problematic entries.

I can send a small patch to collect (mtime, path) with guarded stat then sort, if you want.


56-75: Probe heuristic: validate beyond 'pong' if feasible

The ping→"pong" check is lightweight and OK. If you want higher confidence it’s our MCP listener (and not an unrelated local service), consider a stricter marker (e.g., presence of a known key like protocol or product in the JSON) while keeping the short timeout.

UnityMcpBridge/Editor/Helpers/PortManager.cs (3)

44-65: Stored-port reuse logic: refine fallback when busy by non-MCP process

If the stored port is busy and not used by Unity MCP, you currently return the same port, pushing the bind failure to the caller. Consider falling back to FindAvailablePort in that case to reduce churn on the caller side.

-                // Prefer sticking to the same port; let the caller handle bind retries/fallbacks
-                return storedConfig.unity_port;
+                // If not used by our MCP, prefer finding a new free port to avoid immediate bind failure
+                if (!IsPortUsedByUnityMcp(storedConfig.unity_port))
+                    return FindAvailablePort();
+                return storedConfig.unity_port;

139-157: Heuristic 'used by Unity MCP' check: optionally probe a signature

ConnectAsync().Wait(100) only confirms something is listening. If misidentification is an issue, consider a quick signature probe (send b"ping", expect known greeting/pong) behind a short timeout, similar to the Python PortDiscovery._try_probe_unity_mcp.


291-317: Duplicate hashing logic exists elsewhere; consider centralizing

ComputeProjectHash duplicates UnityMcpBridge.Editor.UnityMcpBridge.ComputeProjectHash. Extracting a shared helper (e.g., in a small internal utility class) would avoid drift if the algorithm changes.

mcp_source.py (4)

19-21: Remove unused imports to satisfy Ruff F401 and keep the module lean

os and re are not used.

-import os
+import os  # unused
 import pathlib
-import re
+import re  # unused

If truly unused, please delete:

- import os
- import re

94-109: Ensure consistent Unity Git URL scheme and correct file: URI form

  • Option [1] uses git+https://... but option [2] may be plain https://.... Unity typically accepts https://..., but mixing schemes can be brittle. Prefer consistent git+https:// for both.
  • Local file: URI should use file:///absolute/path (triple slash) for correctness across platforms.
 def build_options(repo_root: pathlib.Path, branch: str, origin_https: str):
-    upstream = "git+https://github.com/CoplayDev/unity-mcp.git?path=/UnityMcpBridge"
+    upstream = "git+https://github.com/CoplayDev/unity-mcp.git?path=/UnityMcpBridge"
     # Ensure origin is https
     origin = origin_https
     # If origin is a local file path or non-https, try to coerce to https github if possible
     if origin.startswith("file:"):
         # Not meaningful for remote option; keep upstream
         origin_remote = upstream
     else:
-        origin_remote = origin
+        # Prefer git+https for consistency with Unity Git resolver
+        origin_remote = origin.replace("https://", "git+https://", 1) if origin.startswith("https://") else origin
-    return [
+    # file: URI should be file:///absolute/path
+    local_abs = (repo_root / BRIDGE_SUBPATH).resolve()
+    local_uri = f"file:///{local_abs.as_posix().lstrip('/')}"
+    return [
         ("[1] Upstream main", upstream),
         ("[2] Remote current branch", f"{origin_remote}?path=/{BRIDGE_SUBPATH}#{branch}"),
-        ("[3] Local workspace", f"file:{(repo_root / BRIDGE_SUBPATH).as_posix()}"),
+        ("[3] Local workspace", local_uri),
     ]

Verification suggestions:

  • Confirm Unity accepts git+https://... for both upstream and origin forms in your environment.
  • If Unity rejects git+https:// for origin remotes, keep https:// but make schemes consistent (i.e., remove git+ from upstream).

141-144: Inline the choice selection with an or-expression (Ruff SIM108)

Minor simplification.

-    if args.choice:
-        choice = args.choice
-    else:
-        choice = input("Enter 1-3: ").strip()
+    choice = args.choice or input("Enter 1-3: ").strip()

153-165: Add a recovery path for missing dependency and improve UX

Currently we error if com.coplaydev.unity-mcp isn’t present. Many users will have the legacy package id. Consider:

  • If legacy id is present, migrate it in-place.
  • Otherwise, offer --force-add to add the dependency.

I can draft a small migration block that:

  • Detects com.justinbarnett.unity-mcp and renames the key.
  • Or adds a new entry when --force-add is set.
    Would you like me to add that?
README-DEV.md (2)

51-68: Clarify Windows path examples and code-fence languages for readability

  • Consider tagging the fenced blocks with a language for syntax highlighting (e.g., text for raw paths).
  • For the “Example (hash)” block, keep the closing backticks aligned with the content to avoid breaking paragraph flow.
-```
+```text
 X:\UnityProject\Library\PackageCache\com.coplaydev.unity-mcp@<version-or-hash>

-Example (hash):
- +Example (hash): +text
X:\UnityProject\Library\PackageCache\com.coplaydev.unity-mcp@272123cfd97e


112-113: Tighten the uv guidance wording

Small tone and clarity tweak to make it directive and consistent with other bullets.

-### Windows uv path issues
-- On Windows, when testing GUI clients, prefer the WinGet Links `uv.exe`; if multiple `uv.exe` exist, use "Choose UV Install Location" to pin the Links shim.
+### Windows uv path issues
+- On Windows, when testing GUI clients, prefer the WinGet Links `uv.exe`. If multiple `uv.exe` are present, use “Choose UV Install Location” to pin the WinGet Links shim.
README.md (2)

108-111: Specify a fenced code language for the Git URL block (MD040)

Use text for non-code URLs to quiet markdownlint and improve consistency.

-    ```
-    https://github.com/CoplayDev/unity-mcp.git?path=/UnityMcpBridge
-    ```
+    ```text
+    https://github.com/CoplayDev/unity-mcp.git?path=/UnityMcpBridge
+    ```

320-323: Minor: standardize PR guidance wording

Small consistency tweak: “to the main branch” reads more naturally.

-6. **Open a Pull Request** against the main branch.
+6. **Open a Pull Request** to the main branch.
UnityMcpBridge/Editor/Tools/ManageScript.cs (3)

230-234: Update the “Unknown action” message to include new actions

List currently valid actions to reduce confusion now that read/update/edit are deprecated in favor of apply_text_edits and validate.

-                    return Response.Error(
-                        $"Unknown action: '{action}'. Valid actions are: create, read, update, delete."
-                    );
+                    return Response.Error(
+                        $"Unknown action: '{action}'. Valid actions: create, delete, apply_text_edits, validate."
+                    );

267-271: Align error guidance with current API (update is deprecated)

The message suggests using update, but update now returns a deprecation error. Point users to apply_text_edits.

-                return Response.Error(
-                    $"Script already exists at '{relativePath}'. Use 'update' action to modify."
-                );
+                return Response.Error(
+                    $"Script already exists at '{relativePath}'. Use 'apply_text_edits' to modify the file."
+                );

571-589: O(n*m) line/col indexing is fine for small edits; consider precomputing line starts for larger files

For many edits on large files, repeatedly scanning the whole string per edit is costly. Precompute line start offsets once and translate line/col to index in O(1).

I can provide a drop-in helper that builds an int[] of line starts and a fast translator that respects CRLF and LF.

UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs.meta (1)

1-11: Meta file LGTM; consider adding a trailing newline

Unity doesn’t require it, but some linters prefer a newline at EOF.

UnityMcpBridge/UnityMcpServer~/src/tools/read_console.py (3)

5-5: Remove unused imports and redundant connection retrieval.

  • time and config are unused (Ruff F401).
  • get_unity_connection() is called to set bridge but bridge is never used. send_command_with_retry internally obtains the connection, so this is redundant.

Apply this diff:

-from typing import List, Dict, Any
-import time
+from typing import List, Dict, Any
 from mcp.server.fastmcp import FastMCP, Context
-from unity_connection import get_unity_connection, send_command_with_retry
-from config import config
+from unity_connection import send_command_with_retry
@@
-        # Get the connection instance
-        bridge = get_unity_connection()
+        # Connection is resolved internally by send_command_with_retry

Also applies to: 7-8, 40-41


67-69: Remove dead branch re-adding 'count'.

count is always present in params_dict (kept by the comprehension allowing k == 'count'), so this check never triggers.

-        # Add count back if it was None, explicitly sending null might be important for C# logic
-        if 'count' not in params_dict:
-             params_dict['count'] = None 

21-22: Parameter name 'format' shadows Python’s built-in.

Not harmful, but consider output_format in future revisions to avoid shadowing if you plan further processing. Double-check external API compatibility before renaming.

UnityMcpBridge/UnityMcpServer~/src/tools/manage_scene.py (3)

3-5: Prune unused imports.

  • get_unity_connection, config, and time are not used (Ruff F401). Keep only send_command_with_retry.
-from unity_connection import get_unity_connection, send_command_with_retry
-from config import config
-import time
+from unity_connection import send_command_with_retry

31-37: Normalize action for consistency.

Other tools (e.g., manage_asset) lowercase action. Do the same here to avoid case-sensitivity bugs.

-        try:
-            params = {
+        try:
+            action = action.lower()
+            params = {
                 "action": action,
                 "name": name,
                 "path": path,
                 "buildIndex": build_index
             }

11-17: Function signature doesn’t match the docstring; make non-essential args optional.

Doc says path defaults to "Assets/" and build_index is action-specific. Consider optional defaults to align behavior and avoid forced arguments for unrelated actions.

-    def manage_scene(
+    def manage_scene(
         ctx: Context,
         action: str,
         name: str,
-        path: str,
-        build_index: int,
+        path: str = "Assets/",
+        build_index: int | None = None,
     ) -> Dict[str, Any]:

Would changing the signature affect any existing clients invoking this tool positionally? If so, we can instead default inside the function and keep the signature unchanged.

UnityMcpBridge/UnityMcpServer~/src/tools/manage_asset.py (4)

9-10: Remove unused imports (config, time).

Both are unused (Ruff F401).

-from config import config
-import time

4-4: Drop unnecessary loop/connection handling; let the helper manage it.

  • async_send_command_with_retry gets the running loop internally when loop=None.
  • Pre-fetching a connection is redundant; the retry helper resolves it.

This simplifies code and reduces noise.

-import asyncio  # Added: Import asyncio for running sync code in async
@@
-from unity_connection import get_unity_connection, async_send_command_with_retry  # Use centralized retry helper
+from unity_connection import async_send_command_with_retry  # Use centralized retry helper
@@
-        # Get the current asyncio event loop
-        loop = asyncio.get_running_loop()
-        # Get the Unity connection instance
-        connection = get_unity_connection()
-        
-        # Use centralized async retry helper to avoid blocking the event loop
-        result = await async_send_command_with_retry("manage_asset", params_dict, loop=loop)
+        # Use centralized async retry helper; it obtains the loop and connection internally
+        result = await async_send_command_with_retry("manage_asset", params_dict)
         # Return the result obtained from Unity
         return result if isinstance(result, dict) else {"success": False, "message": str(result)}

Also applies to: 8-8, 71-79


46-48: Docstring return fields don’t match actual response shape.

You return {"success": False, "message": ...} on failure, not error. Align the docstring for clarity.

-        Returns:
-            A dictionary with operation results ('success', 'data', 'error').
+        Returns:
+            A dictionary with operation results ('success', 'data', 'message').

7-7: Clean up commented-out import.

The commented import is legacy noise. Consider removing it to keep the file tidy.

UnityMcpBridge/UnityMcpServer~/src/tools/manage_editor.py (1)

2-5: Remove unused imports flagged by Ruff

time, get_unity_connection, and config are unused here.

Apply:

-import time
-from unity_connection import get_unity_connection, send_command_with_retry
-from config import config
+from unity_connection import send_command_with_retry
UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py (2)

123-125: Key mismatch: prefab_folder vs prefabFolder

You build params with "prefabFolder" (camelCase) but pop "prefab_folder" (snake_case), so the field is never removed. If the intent is to avoid sending prefabFolder when prefabPath is present, pop the correct key.

-            params.pop("prefab_folder", None) 
+            params.pop("prefabFolder", None)

3-5: Remove unused imports flagged by Ruff

get_unity_connection, config, and time are not used in this module.

-from unity_connection import get_unity_connection, send_command_with_retry
-from config import config
-import time
+from unity_connection import send_command_with_retry
UnityMcpBridge/UnityMcpServer~/src/tools/execute_menu_item.py (2)

6-9: Remove unused imports flagged by Ruff

get_unity_connection, config, and time are unused here.

-from unity_connection import get_unity_connection, send_command_with_retry  # Import retry helper
-from config import config
-import time
+from unity_connection import send_command_with_retry  # Import retry helper

47-49: Optional: Normalize success like other tools for consistency

Other tools unwrap successful dicts into {"success": True, "message", "data"}. Consider aligning for uniformity.

-        resp = send_command_with_retry("execute_menu_item", params_dict)
-        return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
+        resp = send_command_with_retry("execute_menu_item", params_dict)
+        if isinstance(resp, dict) and resp.get("success"):
+            return {"success": True, "message": resp.get("message", "Menu item executed."), "data": resp.get("data")}
+        return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
UnityMcpBridge/UnityMcpServer~/src/tools/manage_shader.py (3)

3-5: Remove unused imports flagged by Ruff

get_unity_connection, config, and time are unused in this file.

-from unity_connection import get_unity_connection, send_command_with_retry
-from config import config
-import time
+from unity_connection import send_command_with_retry

14-19: Make contents optional for non-write actions

contents is required in the signature, but read/delete don’t need it. Making it optional simplifies callers.

-        contents: str,
+        contents: str | None = None,

55-64: Guard base64 decode to avoid KeyError when fields are absent

If contentsEncoded is True but encodedContents is missing, current code will KeyError. Guard and use pop to clean up.

-            if isinstance(response, dict) and response.get("success"):
-                # If the response contains base64 encoded content, decode it
-                if response.get("data", {}).get("contentsEncoded"):
-                    decoded_contents = base64.b64decode(response["data"]["encodedContents"]).decode('utf-8')
-                    response["data"]["contents"] = decoded_contents
-                    del response["data"]["encodedContents"]
-                    del response["data"]["contentsEncoded"]
+            if isinstance(response, dict) and response.get("success"):
+                data = response.get("data") or {}
+                if data.get("contentsEncoded"):
+                    encoded = data.get("encodedContents")
+                    if encoded is not None:
+                        decoded_contents = base64.b64decode(encoded).decode('utf-8')
+                        data["contents"] = decoded_contents
+                    data.pop("encodedContents", None)
+                    data.pop("contentsEncoded", None)
                 
-                return {"success": True, "message": response.get("message", "Operation successful."), "data": response.get("data")}
+                return {"success": True, "message": response.get("message", "Operation successful."), "data": response.get("data")}
UnityMcpBridge/Editor/Helpers/PackageInstaller.cs (4)

14-22: Gate auto-install in batch mode and confirm EditorPrefs scope is appropriate

  • Consider skipping the delayed install when running in batch mode (CI/builds) to avoid side effects during non-interactive sessions.
  • Verify that using EditorPrefs (user-wide) is intended vs. a project-scoped sentinel. If install state should be per project/version, EditorPrefs may be too coarse.

Apply this minimal guard to avoid running in batch mode:

 static PackageInstaller()
 {
     // Check if this is the first time the package is loaded
-    if (!EditorPrefs.GetBool(InstallationFlagKey, false))
+    if (Application.isBatchMode)
+    {
+        // Skip auto-install on build/CI
+        return;
+    }
+    if (!EditorPrefs.GetBool(InstallationFlagKey, false))
     {
         // Schedule the installation for after Unity is fully loaded
         EditorApplication.delayCall += InstallServerOnFirstLoad;
     }
 }

12-12: Make the install sentinel version-aware to support future upgrades

If the embedded server changes materially, a versioned sentinel prevents false positives from older installs.

Apply this diff to version the EditorPrefs key:

-        private const string InstallationFlagKey = "UnityMCP.ServerInstalled";
+        // Bump VERSION when the embedded server layout/requirements change
+        private const string InstallationFlagKey = "UnityMCP.ServerInstalled.v1";

No other code changes needed; existing GetBool/SetBool calls continue to work with the new key.

Also applies to: 16-22, 31-33


28-39: Check branding consistency

Log messages use the "UNITY-MCP" tag. If the rebrand to "Coplay" is intended repo-wide, align these strings for consistency.


24-41: Optional UX: show a progress bar during installation

Long installs can feel unresponsive. Consider a non-blocking EditorUtility.DisplayProgressBar with a finally block to clear it.

Happy to provide a small helper that wraps EnsureServerInstalled with a transient progress UI.

CursorHelp.md (1)

7-7: Minor editorial fixes for clarity and consistency

  • Use straight quotes for readability in Markdown.
  • Capitalize “Unity” consistently.

Apply this diff:

-- Cursor shows the UnityMCP server but never connects or reports it “can’t start.”
+- Cursor shows the UnityMCP server but never connects or reports it "can't start."
-- If the unity window keeps rewriting to a different `uv.exe`, pick the Links shim again; Unity MCP saves a pinned override and will stop auto-rewrites.
+- If the Unity window keeps rewriting to a different `uv.exe`, pick the Links shim again; Unity MCP saves a pinned override and will stop auto-rewrites.

Also applies to: 75-75

UnityMcpBridge/Editor/Data/McpClients.cs (3)

72-77: Honor XDG config on Linux: prefer ApplicationData over hardcoded ~/.config

On Linux, Environment.SpecialFolder.ApplicationData resolves to $XDG_CONFIG_HOME (falling back to ~/.config). This is more robust than concatenating ".config" to $HOME.

Apply this diff:

-                linuxConfigPath = Path.Combine(
-                    Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
-                    ".config",
-                    "Claude",
-                    "claude_desktop_config.json"
-                ),
+                linuxConfigPath = Path.Combine(
+                    Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+                    "Claude",
+                    "claude_desktop_config.json"
+                ),
-                linuxConfigPath = Path.Combine(
-                    Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
-                    ".config",
-                    "Code",
-                    "User",
-                    "mcp.json"
-                ),
+                linuxConfigPath = Path.Combine(
+                    Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+                    "Code",
+                    "User",
+                    "mcp.json"
+                ),

Also applies to: 92-97


103-113: Map more statuses if available

Only "Not Configured" is mapped to McpStatus.NotConfigured. If other string states exist (e.g., "Configured", "Error", "Partial"), consider mapping them here to keep enum in sync.

I can add a small mapping helper and update this loop if you share the expected status strings.


10-101: Consider centralizing client path resolution

Several paths embed OS-specific conventions. A central helper (e.g., ExecPath.ResolveClientConfig(McpType)) would improve maintainability and testability.

tests/test_script_tools.py (1)

5-5: Remove unused import

pytest is not used directly in this test module.

Apply this diff:

-import pytest
UnityMcpBridge/UnityMcpServer~/src/tools/manage_script_edits.py (2)

8-73: Preserve original newline style and add case-insensitive anchor option

  • Keep CRLF vs. LF to minimize noisy diffs on Windows.
  • Allow ignore_case on anchor_insert, similar to regex_replace.

Apply this diff:

 def _apply_edits_locally(original_text: str, edits: List[Dict[str, Any]]) -> str:
     text = original_text
+    # Preserve predominant newline style (default to "\n")
+    eol = "\r\n" if "\r\n" in text else ("\n" if "\n" in text else "\n")
     for edit in edits or []:
         op = (
             (edit.get("op")
              or edit.get("operation")
              or edit.get("type")
              or edit.get("mode")
              or "")
             .strip()
             .lower()
         )
 
         if not op:
             allowed = "anchor_insert, prepend, append, replace_range, regex_replace"
             raise RuntimeError(
                 f"op is required; allowed: {allowed}. Use 'op' (aliases accepted: type/mode/operation)."
             )
 
         if op == "prepend":
             prepend_text = edit.get("text", "")
-            text = (prepend_text if prepend_text.endswith("\n") else prepend_text + "\n") + text
+            if prepend_text and not prepend_text.endswith(eol):
+                prepend_text += eol
+            text = prepend_text + text
         elif op == "append":
             append_text = edit.get("text", "")
-            if not text.endswith("\n"):
-                text += "\n"
+            if not text.endswith(eol):
+                text += eol
             text += append_text
-            if not text.endswith("\n"):
-                text += "\n"
+            if append_text and not text.endswith(eol):
+                text += eol
         elif op == "anchor_insert":
             anchor = edit.get("anchor", "")
             position = (edit.get("position") or "before").lower()
             insert_text = edit.get("text", "")
-            flags = re.MULTILINE
+            flags = re.MULTILINE
+            if edit.get("ignore_case"):
+                flags |= re.IGNORECASE
             m = re.search(anchor, text, flags)
             if not m:
                 if edit.get("allow_noop", True):
                     continue
                 raise RuntimeError(f"anchor not found: {anchor}")
             idx = m.start() if position == "before" else m.end()
             text = text[:idx] + insert_text + text[idx:]
         elif op == "replace_range":
             start_line = int(edit.get("startLine", 1))
             end_line = int(edit.get("endLine", start_line))
             replacement = edit.get("text", "")
             lines = text.splitlines(keepends=True)
             if start_line < 1 or end_line < start_line or end_line > len(lines):
                 raise RuntimeError("replace_range out of bounds")
             a = start_line - 1
             b = end_line
             rep = replacement
-            if rep and not rep.endswith("\n"):
-                rep += "\n"
+            if rep and not rep.endswith(eol):
+                rep += eol
             text = "".join(lines[:a]) + rep + "".join(lines[b:])
         elif op == "regex_replace":
             pattern = edit.get("pattern", "")
             repl = edit.get("replacement", "")
             count = int(edit.get("count", 0))  # 0 = replace all
             flags = re.MULTILINE
             if edit.get("ignore_case"):
                 flags |= re.IGNORECASE
             text = re.sub(pattern, repl, text, count=count, flags=flags)
         else:
             allowed = "anchor_insert, prepend, append, replace_range, regex_replace"
             raise RuntimeError(f"unknown edit op: {op}; allowed: {allowed}. Use 'op' (aliases accepted: type/mode/operation).")
     return text

121-124: Guard against invalid base64 in read responses

A malformed or empty encodedContents should return a structured failure rather than raising.

Apply this diff:

-        if contents is None and data.get("contentsEncoded") and data.get("encodedContents"):
-            contents = base64.b64decode(data["encodedContents"]).decode("utf-8")
+        if contents is None and data.get("contentsEncoded") and data.get("encodedContents"):
+            try:
+                decoded = base64.b64decode(data["encodedContents"])
+                contents = decoded.decode("utf-8")
+            except Exception as e:
+                return {"success": False, "message": f"Failed to decode base64-encoded contents: {e}"}
UnityMcpBridge/Editor/Helpers/ServerPathResolver.cs (1)

62-74: Avoid CPU spin while waiting for PackageManager Client.List()

The busy-wait can peg the editor thread. Sleep briefly while waiting.

Apply this diff:

-                while (!list.IsCompleted) { }
+                while (!list.IsCompleted) { System.Threading.Thread.Sleep(50); }
UnityMcpBridge/UnityMcpServer~/src/tools/manage_script.py (3)

1-8: Remove unused imports (ruff F401)

get_unity_connection, config, and time are unused here.

Apply this diff:

-from mcp.server.fastmcp import FastMCP, Context
-from typing import Dict, Any, List
-from unity_connection import get_unity_connection, send_command_with_retry
-from config import config
-import time
+from mcp.server.fastmcp import FastMCP, Context
+from typing import Dict, Any, List
+from unity_connection import send_command_with_retry
 import os
 import base64

13-23: More robust URI parsing for file:// on Windows

Plain slicing can yield paths like “/C:/…” for file URIs. Use urllib.parse and unquote to normalize cross-platform.

Apply this diff:

+        from urllib.parse import urlparse, unquote
         def _split_uri(uri: str) -> tuple[str, str]:
-        if uri.startswith("unity://path/"):
-            path = uri[len("unity://path/") :]
-        elif uri.startswith("file://"):
-            path = uri[len("file://") :]
-        else:
-            path = uri
-        path = path.replace("\\", "/")
-        name = os.path.splitext(os.path.basename(path))[0]
-        directory = os.path.dirname(path)
-        return name, directory
+            p = urlparse(uri)
+            if p.scheme == "unity" and p.netloc == "path":
+                path = unquote(p.path)
+            elif p.scheme == "file":
+                path = unquote(p.path)
+                # Windows: strip leading slash for drive letters (/C:/...)
+                if os.name == "nt" and len(path) > 3 and path[0] == "/" and path[2] == ":":
+                    path = path[1:]
+            else:
+                path = uri
+            path = path.replace("\\", "/")
+            name = os.path.splitext(os.path.basename(path))[0]
+            directory = os.path.dirname(path)
+            return name, directory

134-142: Guard against missing encoded fields when decoding

If contentsEncoded is true but encodedContents is absent, the dict indexing will KeyError. Use .get() to be safe.

Apply this diff:

-                if response.get("data", {}).get("contentsEncoded"):
-                    decoded_contents = base64.b64decode(response["data"]["encodedContents"]).decode('utf-8')
-                    response["data"]["contents"] = decoded_contents
-                    del response["data"]["encodedContents"]
-                    del response["data"]["contentsEncoded"]
+                data = response.get("data", {})
+                if data.get("contentsEncoded") and "encodedContents" in data:
+                    decoded_contents = base64.b64decode(data["encodedContents"]).decode('utf-8')
+                    data["contents"] = decoded_contents
+                    data.pop("encodedContents", None)
+                    data.pop("contentsEncoded", None)
UnityMcpBridge/UnityMcpServer~/src/unity_connection.py (5)

91-96: Preserve exception context when re-raising (ruff B904)

Chain exceptions with from e to keep the root cause.

Apply this diff:

-            except socket.timeout:
+            except socket.timeout as e:
                 logger.warning("Socket timeout during framed receive")
-                raise Exception("Timeout receiving Unity response")
+                raise Exception("Timeout receiving Unity response") from e
             except Exception as e:
                 logger.error(f"Error during framed receive: {str(e)}")
-                raise
+                raise

144-149: Preserve exception context when re-raising (ruff B904)

Same issue in the unframed receive path.

Apply this diff:

-        except socket.timeout:
+        except socket.timeout as e:
             logger.warning("Socket timeout during receive")
-            raise Exception("Timeout receiving Unity response")
-        except Exception as e:
+            raise Exception("Timeout receiving Unity response") from e
+        except Exception as e:
             logger.error(f"Error during receive: {str(e)}")
-            raise
+            raise

192-195: Simplify nested condition (ruff SIM102)

Combine the nested if checks.

Apply this diff:

-                if not self.sock:
-                    if not self.connect():
-                        raise Exception("Could not connect to Unity")
+                if (self.sock is None) and (not self.connect()):
+                    raise Exception("Could not connect to Unity")

204-207: Replace try/except/pass with contextlib.suppress (ruff SIM105)

Cleaner and intent-revealing around best-effort debug logging.

Apply this diff:

+                import contextlib
                 # Send
-                try:
-                    logger.debug(f"send {len(payload)} bytes; mode={'framed' if self.use_framing else 'legacy'}; head={(payload[:32]).decode('utf-8','ignore')}")
-                except Exception:
-                    pass
+                with contextlib.suppress(Exception):
+                    logger.debug(f"send {len(payload)} bytes; mode={'framed' if self.use_framing else 'legacy'}; head={(payload[:32]).decode('utf-8','ignore')}")
@@
-                try:
-                    logger.debug(f"recv {len(response_data)} bytes; mode={'framed' if self.use_framing else 'legacy'}; head={(response_data[:32]).decode('utf-8','ignore')}")
-                except Exception:
-                    pass
+                with contextlib.suppress(Exception):
+                    logger.debug(f"recv {len(response_data)} bytes; mode={'framed' if self.use_framing else 'legacy'}; head={(response_data[:32]).decode('utf-8','ignore')}")

Also applies to: 220-223


262-283: Remove unused variable backoff (ruff F841) and simplify backoff logic

backoff is computed but never used. Drop it.

Apply this diff:

-                    # Base exponential backoff
-                    backoff = base_backoff * (2 ** attempt)
                     # Decorrelated jitter multiplier
                     jitter = random.uniform(0.1, 0.3)
@@
-                    sleep_s = min(cap, jitter * (2 ** attempt))
+                    sleep_s = min(cap, jitter * (2 ** attempt))

(Note: Only the unused assignment is removed.)

UnityMcpBridge/Editor/Helpers/ServerInstaller.cs (2)

97-118: Remove unused helpers to reduce surface area

IsDirectoryWritable and IsServerInstalled are currently unused.

Apply this diff:

-        private static bool IsDirectoryWritable(string path)
-        {
-            try
-            {
-                File.Create(Path.Combine(path, "test.txt")).Dispose();
-                File.Delete(Path.Combine(path, "test.txt"));
-                return true;
-            }
-            catch
-            {
-                return false;
-            }
-        }
-
-        /// <summary>
-        /// Checks if the server is installed at the specified location.
-        /// </summary>
-        private static bool IsServerInstalled(string location)
-        {
-            return Directory.Exists(location)
-                && File.Exists(Path.Combine(location, ServerFolder, "src", "server.py"));
-        }
+        // (Removed unused helpers IsDirectoryWritable and IsServerInstalled)

134-153: Use streaming enumeration when copying directories

Enumerating rather than materializing arrays reduces memory footprint on large trees.

Apply this diff:

-            foreach (string filePath in Directory.GetFiles(sourceDir))
+            foreach (string filePath in Directory.EnumerateFiles(sourceDir))
             {
                 string fileName = Path.GetFileName(filePath);
                 string destFile = Path.Combine(destinationDir, fileName);
                 File.Copy(filePath, destFile, overwrite: true);
             }
 
-            foreach (string dirPath in Directory.GetDirectories(sourceDir))
+            foreach (string dirPath in Directory.EnumerateDirectories(sourceDir))
             {
                 string dirName = Path.GetFileName(dirPath);
                 foreach (var skip in _skipDirs)
                 {
                     if (dirName.Equals(skip, StringComparison.OrdinalIgnoreCase))
                         goto NextDir;
                 }
                 try { if ((File.GetAttributes(dirPath) & FileAttributes.ReparsePoint) != 0) continue; } catch { }
                 string destSubDir = Path.Combine(destinationDir, dirName);
                 CopyDirectoryRecursive(dirPath, destSubDir);
             NextDir: ;
             }
UnityMcpBridge/UnityMcpServer~/src/server.py (2)

1-12: Remove unused imports (ruff F401)

Image, dataclass, and List are unused.

Apply this diff:

-from mcp.server.fastmcp import FastMCP, Context, Image
+from mcp.server.fastmcp import FastMCP, Context
@@
-from dataclasses import dataclass
@@
-from typing import AsyncIterator, Dict, Any, List
+from typing import AsyncIterator, Dict, Any

121-137: Guard Assets traversal when cwd is not a Unity project

When run outside a Unity project, Assets may not exist. Early-return avoids unnecessary os.walk.

Apply this diff:

     assets = []
     try:
-        root = os.getcwd()
-        for base, _, files in os.walk(os.path.join(root, "Assets")):
+        root = os.getcwd()
+        assets_dir = os.path.join(root, "Assets")
+        if not os.path.isdir(assets_dir):
+            return assets
+        for base, _, files in os.walk(assets_dir):
             for f in files:
                 if f.endswith(".cs"):
                     rel = os.path.relpath(os.path.join(base, f), root).replace("\\", "/")
                     assets.append({
                         "uri": f"unity://path/{rel}",
                         "name": os.path.basename(rel)
                     })
UnityMcpBridge/Editor/UnityMcpBridge.cs (3)

7-7: Remove unused using directive

The System.Threading namespace is imported but not used in this file. Consider removing it to keep imports clean.

-using System.Threading;

25-31: Consider extracting heartbeat configuration to constants

The heartbeat interval and timing values are hardcoded inline. Consider extracting them to named constants for better maintainability.

+private const double InitRetryDelaySeconds = 0.2;
+private const double HeartbeatIntervalSeconds = 0.5;
 private static readonly object startStopLock = new();
 private static bool initScheduled = false;
 private static bool ensureUpdateHooked = false;
 private static bool isStarting = false;
 private static double nextStartAt = 0.0f;
 private static double nextHeartbeatAt = 0.0f;
 private static int heartbeatSeq = 0;

Then update the usage:

-nextStartAt = EditorApplication.timeSinceStartup + 0.20f;
+nextStartAt = EditorApplication.timeSinceStartup + InitRetryDelaySeconds;
-nextHeartbeatAt = now + 0.5f;
+nextHeartbeatAt = now + HeartbeatIntervalSeconds;

502-517: Remove redundant overload and consolidate timeout handling

You have two ReadExactAsync methods - one without timeout and one with timeout. The non-timeout version is only used once (line 434) and could be replaced with the timeout version using the constant FrameIOTimeoutMs.

-private static async System.Threading.Tasks.Task<byte[]> ReadExactAsync(NetworkStream stream, int count)
-{
-    byte[] data = new byte[count];
-    int offset = 0;
-    while (offset < count)
-    {
-        int r = await stream.ReadAsync(data, offset, count - offset);
-        if (r == 0)
-        {
-            throw new System.IO.IOException("Connection closed before reading expected bytes");
-        }
-        offset += r;
-    }
-    return data;
-}
-
 // Timeout-aware exact read helper; avoids indefinite stalls
 private static async System.Threading.Tasks.Task<byte[]> ReadExactAsync(NetworkStream stream, int count, int timeoutMs)

Then update the call site at line 434:

-byte[] header = await ReadExactAsync(stream, 8, FrameIOTimeoutMs);
+byte[] header = await ReadExactAsync(stream, 8, FrameIOTimeoutMs);
UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs (2)

4-8: Remove unused using directives

Several using directives appear to be unused based on the code. Consider removing them to keep imports clean.

-using System.Security.Cryptography;
-using System.Text;
-using System.Net.Sockets;
-using System.Net;
+using System.Text;
+using System.Net.Sockets;
+using System.Net;

2203-2295: Consider extracting Python detection to a shared utility

The IsPythonDetected method is quite long and handles platform-specific Python detection. This functionality might be useful elsewhere and could be extracted to a shared utility class similar to ExecPath or ServerInstaller.

Consider creating a PythonResolver utility class in the Helpers namespace:

// In UnityMcpBridge/Editor/Helpers/PythonResolver.cs
namespace UnityMcpBridge.Editor.Helpers
{
    internal static class PythonResolver
    {
        internal static bool IsPythonInstalled() { /* implementation */ }
        internal static string FindPython() { /* implementation */ }
        // Other Python-related utilities
    }
}

This would improve code organization and reusability across the codebase.

UnityMcpBridge/Editor/Helpers/ExecPath.cs (3)

92-132: Improve NVM version parsing robustness

The NVM version parsing logic could be more robust. The current implementation might fail on version strings with multiple dashes or unusual formats.

 // Extract numeric portion: e.g., v18.19.0-nightly -> 18.19.0
 string versionStr = name.Substring(1);
-int dashIndex = versionStr.IndexOf('-');
-if (dashIndex > 0)
-{
-    versionStr = versionStr.Substring(0, dashIndex);
-}
+// Use regex to extract the version number more reliably
+var match = System.Text.RegularExpressions.Regex.Match(versionStr, @"^(\d+\.\d+\.\d+)");
+if (match.Success)
+{
+    versionStr = match.Groups[1].Value;
+}
 if (Version.TryParse(versionStr, out Version parsed))

166-232: Add XML documentation for the public TryRun method

The TryRun method is a key utility that would benefit from XML documentation explaining its parameters, return value, and behavior - especially the PowerShell script handling on Windows.

+/// <summary>
+/// Executes an external process with the specified arguments and captures output.
+/// </summary>
+/// <param name="file">The executable file path or PowerShell script (.ps1) to run</param>
+/// <param name="args">Command line arguments</param>
+/// <param name="workingDir">Working directory for the process (null for current directory)</param>
+/// <param name="stdout">Captured standard output</param>
+/// <param name="stderr">Captured standard error</param>
+/// <param name="timeoutMs">Maximum time to wait for process completion in milliseconds</param>
+/// <param name="extraPathPrepend">Additional paths to prepend to the PATH environment variable</param>
+/// <returns>True if the process exited with code 0, false otherwise</returns>
 internal static bool TryRun(
     string file,
     string args,
     string workingDir,
     out string stdout,
     out string stderr,
     int timeoutMs = 15000,
     string extraPathPrepend = null)

200-200: Use Path.PathSeparator consistently

Line 200 uses System.IO.Path.PathSeparator while line 246 uses Path.PathSeparator. Consider using the shorter form consistently since you have using System.IO; at the top.

-: (extraPathPrepend + System.IO.Path.PathSeparator + currentPath);
+: (extraPathPrepend + Path.PathSeparator + currentPath);

Comment on lines +39 to +47
def normalize_origin_to_https(url: str) -> str:
"""Map common SSH origin forms to https for Unity's git URL scheme."""
if url.startswith("git@github.com:"):
owner_repo = url.split(":", 1)[1]
if owner_repo.endswith(".git"):
owner_repo = owner_repo[:-4]
return f"https://github.com/{owner_repo}.git"
# already https or file: etc.
return url
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Normalize additional Git origin formats (ssh:// and https without .git) to robustly support option [2]

Currently only git@github.com:... is normalized. If origin is ssh://git@github.com/owner/repo.git or https://github.com/owner/repo (no .git), option [2] may produce a URL Unity can’t resolve.

Proposed normalization:

 def normalize_origin_to_https(url: str) -> str:
     """Map common SSH origin forms to https for Unity's git URL scheme."""
-    if url.startswith("git@github.com:"):
+    if url.startswith("git@github.com:"):
         owner_repo = url.split(":", 1)[1]
         if owner_repo.endswith(".git"):
             owner_repo = owner_repo[:-4]
         return f"https://github.com/{owner_repo}.git"
-    # already https or file: etc.
+    # ssh://git@github.com/owner/repo(.git)
+    if url.startswith("ssh://git@github.com/"):
+        owner_repo = url.rsplit("/", 1)[-2:]  # keep trailing path
+        tail = url.split("github.com/", 1)[1]
+        if tail.endswith(".git"):
+            return "https://github.com/" + tail
+        return "https://github.com/" + tail + ".git"
+    # https://github.com/owner/repo (ensure .git)
+    if url.startswith("https://github.com/") and not url.endswith(".git"):
+        return url + ".git"
+    # already https with .git, or file:, etc.
     return url

To verify in this repo after applying, run:


🏁 Script executed:

#!/usr/bin/env bash
# Show normalized outputs for common remote forms
python - <<'PY'
from mcp_source import normalize_origin_to_https
tests = [
  "git@github.com:CoplayDev/unity-mcp.git",
  "ssh://git@github.com/CoplayDev/unity-mcp.git",
  "https://github.com/CoplayDev/unity-mcp",
  "https://github.com/CoplayDev/unity-mcp.git",
  "file:/tmp/unity-mcp"
]
for t in tests:
    print(t, "=>", normalize_origin_to_https(t))
PY

Length of output: 401


Normalize additional Git origin formats (ssh:// and https:// without .git)

The tests confirm that normalize_origin_to_https currently only handles the git@github.com: form. To support:

  • ssh://git@github.com/owner/repo.git
  • ssh://git@github.com/owner/repo
  • https://github.com/owner/repo (no .git)

we should extend the function to normalize these to https://github.com/owner/repo.git.

mcp_source.py (lines 39–47):

 def normalize_origin_to_https(url: str) -> str:
     """Map common SSH origin forms to https for Unity's git URL scheme."""
-    if url.startswith("git@github.com:"):
-        owner_repo = url.split(":", 1)[1]
-        if owner_repo.endswith(".git"):
-            owner_repo = owner_repo[:-4]
-        return f"https://github.com/{owner_repo}.git"
+    # git@github.com:owner/repo(.git)
+    if url.startswith("git@github.com:"):
+        owner_repo = url.split(":", 1)[1]
+        if owner_repo.endswith(".git"):
+            owner_repo = owner_repo[:-4]
+        return f"https://github.com/{owner_repo}.git"
+
+    # ssh://git@github.com/owner/repo(.git)
+    if url.startswith("ssh://git@github.com/"):
+        owner_repo = url.split("github.com/", 1)[1]
+        if owner_repo.endswith(".git"):
+            owner_repo = owner_repo[:-4]
+        return f"https://github.com/{owner_repo}.git"
+
+    # https://github.com/owner/repo (ensure .git)
+    if url.startswith("https://github.com/"):
+        owner_repo = url.split("github.com/", 1)[1]
+        if owner_repo.endswith(".git"):
+            owner_repo = owner_repo[:-4]
+        return f"https://github.com/{owner_repo}.git"
 
     # already https with .git, file:, etc.
     return url

After applying, rerun your verification script:

python - <<'PY'
from mcp_source import normalize_origin_to_https
tests = [
  "git@github.com:CoplayDev/unity-mcp.git",
  "ssh://git@github.com/CoplayDev/unity-mcp.git",
  "ssh://git@github.com/CoplayDev/unity-mcp",
  "https://github.com/CoplayDev/unity-mcp",
  "https://github.com/CoplayDev/unity-mcp.git",
  "file:/tmp/unity-mcp"
]
for t in tests:
    print(t, "=>", normalize_origin_to_https(t))
PY
🤖 Prompt for AI Agents
mcp_source.py around lines 39-47: normalize_origin_to_https currently only
handles "git@github.com:" SSH shorthand; extend it to also detect and normalize
"ssh://git@github.com/owner/repo(.git)?" and "https://github.com/owner/repo" (no
.git) forms into "https://github.com/owner/repo.git". Implement logic to: 1)
parse and accept URLs that start with "ssh://git@github.com/" by extracting the
owner/repo path, strip a trailing ".git" if present, and return the https form
with ".git" appended; 2) if the URL starts with "https://github.com/" but does
not end with ".git", append ".git"; 3) leave other schemes (file:, other hosts,
or already-correct https with .git) unchanged. Ensure trimming and joining avoid
duplicate slashes and only add ".git" once.

Comment on lines +3 to +4
#### Proudly sponsored and maintained by [Coplay](https://www.coplay.dev/?ref=unity-mcp), the AI assistant for Unity. [Read the backstory here.](https://www.coplay.dev/blog/coplay-and-open-source-unity-mcp-join-forces)

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix heading level increment (MD001) for proper outline structure

Use h2 instead of h4 for this subheading.

-#### Proudly sponsored and maintained by [Coplay](https://www.coplay.dev/?ref=unity-mcp), the AI assistant for Unity. [Read the backstory here.](https://www.coplay.dev/blog/coplay-and-open-source-unity-mcp-join-forces)
+## Proudly sponsored and maintained by [Coplay](https://www.coplay.dev/?ref=unity-mcp), the AI assistant for Unity. [Read the backstory here.](https://www.coplay.dev/blog/coplay-and-open-source-unity-mcp-join-forces)
📝 Committable suggestion

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

Suggested change
#### Proudly sponsored and maintained by [Coplay](https://www.coplay.dev/?ref=unity-mcp), the AI assistant for Unity. [Read the backstory here.](https://www.coplay.dev/blog/coplay-and-open-source-unity-mcp-join-forces)
## Proudly sponsored and maintained by [Coplay](https://www.coplay.dev/?ref=unity-mcp), the AI assistant for Unity. [Read the backstory here.](https://www.coplay.dev/blog/coplay-and-open-source-unity-mcp-join-forces)
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

3-3: Heading levels should only increment by one level at a time
Expected: h2; Actual: h4

(MD001, heading-increment)

🤖 Prompt for AI Agents
In README.md around lines 3 to 4, the heading uses an h4 ("####") which violates
MD001 outline structure; change the subheading to h2 by replacing the leading
four hashes with two ("##") so the document uses the correct heading level and
preserves content and links.

Comment on lines +9 to +12
![GitHub commit activity](https://img.shields.io/github/commit-activity/w/CoplayDev/unity-mcp)
![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/CoplayDev/unity-mcp)
[![](https://img.shields.io/badge/License-MIT-red.svg 'MIT License')](https://opensource.org/licenses/MIT)
[![](https://img.shields.io/badge/Sponsor-Coplay-red.svg 'Coplay')](https://www.coplay.dev/?ref=unity-mcp)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add alt text to badges and images (MD045) for accessibility

Badges and images should include meaningful alt text.

-![GitHub commit activity](https://img.shields.io/github/commit-activity/w/CoplayDev/unity-mcp)
-![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/CoplayDev/unity-mcp)
-[![](https://img.shields.io/badge/License-MIT-red.svg 'MIT License')](https://opensource.org/licenses/MIT)
-[![](https://img.shields.io/badge/Sponsor-Coplay-red.svg 'Coplay')](https://www.coplay.dev/?ref=unity-mcp)
+![GitHub commit activity badge](https://img.shields.io/github/commit-activity/w/CoplayDev/unity-mcp)
+![Open issues badge](https://img.shields.io/github/issues/CoplayDev/unity-mcp)
+[![MIT License badge](https://img.shields.io/badge/License-MIT-red.svg 'MIT License')](https://opensource.org/licenses/MIT)
+[![Sponsor Coplay badge](https://img.shields.io/badge/Sponsor-Coplay-red.svg 'Coplay')](https://www.coplay.dev/?ref=unity-mcp)
📝 Committable suggestion

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

Suggested change
![GitHub commit activity](https://img.shields.io/github/commit-activity/w/CoplayDev/unity-mcp)
![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/CoplayDev/unity-mcp)
[![](https://img.shields.io/badge/License-MIT-red.svg 'MIT License')](https://opensource.org/licenses/MIT)
[![](https://img.shields.io/badge/Sponsor-Coplay-red.svg 'Coplay')](https://www.coplay.dev/?ref=unity-mcp)
![GitHub commit activity badge](https://img.shields.io/github/commit-activity/w/CoplayDev/unity-mcp)
![Open issues badge](https://img.shields.io/github/issues/CoplayDev/unity-mcp)
[![MIT License badge](https://img.shields.io/badge/License-MIT-red.svg 'MIT License')](https://opensource.org/licenses/MIT)
[![Sponsor Coplay badge](https://img.shields.io/badge/Sponsor-Coplay-red.svg 'Coplay')](https://www.coplay.dev/?ref=unity-mcp)
🧰 Tools
🪛 LanguageTool

[grammar] ~9-~9: There might be a mistake here.
Context: ...b/commit-activity/w/CoplayDev/unity-mcp) ![GitHub Issues or Pull Requests](https:...

(QB_NEW_EN)


[grammar] ~10-~10: There might be a mistake here.
Context: ...ds.io/github/issues/CoplayDev/unity-mcp) [![](https://img.shields.io/badge/Licens...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)

11-11: Images should have alternate text (alt text)

(MD045, no-alt-text)


12-12: Images should have alternate text (alt text)

(MD045, no-alt-text)

🤖 Prompt for AI Agents
In README.md around lines 9 to 12, the badge image tags lack meaningful alt text
(MD045); update each Markdown image/link to include descriptive alt text inside
the square brackets (e.g., "GitHub commit activity", "Open issues", "MIT
License", "Sponsor — Coplay") so the first [] contains that text for
accessibility while keeping the existing badge URLs and links intact.

Comment on lines +2 to +11
guid: de8f5721c34f7194392e9d8c7d0226c0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

LGTM on standardized MonoImporter block; verify repo-wide GUID and meta consistency

The metadata looks standard for a C# script (.cs.meta): GUID preserved, MonoImporter present, serializedVersion/executionOrder/defaults all conventional.

Run this script to sanity-check meta integrity across the repo (duplicate GUIDs, missing .meta for .cs, MonoImporter presence, and fileFormatVersion correctness):


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Checking for duplicate GUIDs across all *.meta files..."
dups=$(fd -a -t f -e meta -0 | xargs -0 gawk '/^guid:/ {print FILENAME, $2}' | \
  gawk '{guid=$2; file=$1; c[guid]++; files[guid]=files[guid]" "file} END{ for (g in c) if (c[g]>1) {print "DUPLICATE GUID: " g " ->" files[g]}}')
if [[ -n "${dups}" ]]; then
  echo "${dups}"
  echo "FAIL: Found duplicate GUIDs."
  exit 1
else
  echo "OK: No duplicate GUIDs."
fi

echo
echo "2) Checking that every .cs file has a companion .meta..."
missing_meta=0
while IFS= read -r cs; do
  if [[ ! -f "${cs}.meta" ]]; then
    echo "Missing .meta for: ${cs}"
    missing_meta=1
  fi
done < <(fd -a -t f -e cs)
if [[ $missing_meta -eq 1 ]]; then
  echo "FAIL: Some .cs files are missing .meta files."
  exit 1
else
  echo "OK: All .cs files have .meta files."
fi

echo
echo "3) Ensuring all .cs.meta files contain a MonoImporter block..."
no_importer=0
while IFS= read -r meta; do
  if ! rg -q '^\s*MonoImporter:\s*$' "${meta}"; then
    echo "No MonoImporter in: ${meta}"
    no_importer=1
  fi
done < <(fd -a -t f -g "*.cs.meta")
if [[ $no_importer -eq 1 ]]; then
  echo "FAIL: Some .cs.meta files are missing MonoImporter."
  exit 1
else
  echo "OK: MonoImporter present in all .cs.meta files."
fi

echo
echo "4) Verifying fileFormatVersion: 2 for all .cs.meta files..."
bad_ffv=0
while IFS= read -r meta; do
  if ! rg -q '^fileFormatVersion:\s*2\s*$' "${meta}"; then
    echo "Unexpected fileFormatVersion in: ${meta}"
    bad_ffv=1
  fi
done < <(fd -a -t f -g "*.cs.meta")
if [[ $bad_ffv -eq 1 ]]; then
  echo "FAIL: Some .cs.meta files have unexpected fileFormatVersion."
  exit 1
else
  echo "OK: fileFormatVersion is 2 for all .cs.meta files."
fi

echo
echo "All meta checks passed."

Length of output: 724


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "1) Checking for duplicate GUIDs across all *.meta files..."
dups=$(fd -a -t f -e meta -0 | xargs -0 gawk '/^guid:/ {print FILENAME, $2}' | \
  gawk '{guid=$2; file=$1; c[guid]++; files[guid]=files[guid]" "file} END{ for (g in c) if (c[g]>1) {print "DUPLICATE GUID: " g " ->" files[g]}}')
if [[ -n "$dups" ]]; then
  echo "$dups"
  echo "FAIL: Found duplicate GUIDs."
  exit 1
else
  echo "OK: No duplicate GUIDs."
fi

echo; echo "2) Checking that every .cs file has a companion .meta..."
missing=$(fd -a -t f -e cs -0 | xargs -0 -I{} bash -c '[ ! -f "{}.meta" ] && printf "%s\n" "{}"')
if [[ -n "$missing" ]]; then
  echo "Missing .meta for:"
  printf "%s\n" "$missing"
  echo "FAIL: Some .cs files are missing .meta files."
  exit 1
else
  echo "OK: All .cs files have .meta files."
fi

echo; echo "3) Ensuring all .cs.meta files contain a MonoImporter block..."
no_importer=$(fd -a -t f -g "*.cs.meta" -0 | xargs -0 grep -L '^[[:space:]]*MonoImporter:' || true)
if [[ -n "$no_importer" ]]; then
  echo "No MonoImporter in:"
  printf "%s\n" "$no_importer"
  echo "FAIL: Some .cs.meta files are missing MonoImporter."
  exit 1
else
  echo "OK: MonoImporter present in all .cs.meta files."
fi

echo; echo "4) Verifying fileFormatVersion: 2 for all .cs.meta files..."
bad_ffv=$(fd -a -t f -g "*.cs.meta" -0 | xargs -0 grep -L '^[[:space:]]*fileFormatVersion:[[:space:]]*2[[:space:]]*$' || true)
if [[ -n "$bad_ffv" ]]; then
  echo "Unexpected fileFormatVersion in:"
  printf "%s\n" "$bad_ffv"
  echo "FAIL: Some .cs.meta files have unexpected fileFormatVersion."
  exit 1
else
  echo "OK: fileFormatVersion is 2 for all .cs.meta files."
fi

echo; echo "All meta checks passed."

Length of output: 753


Add missing .meta for UnityMcpEditorWindow.cs

The script check revealed that UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs does not have a corresponding .meta file. Unity requires a .meta alongside every asset to preserve GUIDs and import settings.

• Missing file:
– UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs.meta

Please add and commit the .meta for this script to avoid import/link errors in the Unity Editor.

🤖 Prompt for AI Agents
UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs.meta (around lines 2-11):
the UnityMcpEditorWindow.cs asset is missing its .meta file; create and commit
UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs.meta so Unity can preserve
the asset GUID and import settings. Add a .meta containing a valid guid (use the
existing asset GUID if you have it, otherwise generate a new one via Unity by
reimporting the script), and include standard MonoImporter keys
(externalObjects, serializedVersion, defaultReferences, executionOrder, icon,
userData, assetBundleName, assetBundleVariant) so the editor will import the
script consistently; save and commit the new .meta next to the .cs file.

Comment on lines +2 to +11
guid: 5862c6a6d0a914f4d83224f8d039cf7b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Meta block is standard and sane; keep GUID stable across refactors

Structure matches Unity’s expected MonoImporter format for C# scripts. No functional impact; OK to merge.

If helpful, run this repo-wide check to ensure meta integrity (duplicate GUIDs, missing metas, and MonoImporter presence):


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Scan for duplicate GUIDs across all .meta files..."
fd -a -g '*.meta' UnityMcpBridge | while read -r f; do
  g=$(awk '/^guid:/{print $2}' "$f")
  [ -n "$g" ] && echo "$g $f"
done | tee /tmp/meta_guids.txt >/dev/null

dups=$(cut -d' ' -f1 /tmp/meta_guids.txt | sort | uniq -d)
if [ -n "$dups" ]; then
  echo "Duplicate GUIDs detected:"
  while read -r d; do
    grep "^$d " /tmp/meta_guids.txt
  done <<< "$dups"
  echo
else
  echo "No duplicate GUIDs found."
fi

echo
echo "2) Ensure every .cs has a .cs.meta and vice versa..."
missing=0
while read -r cs; do
  if [ ! -f "${cs}.meta" ]; then
    echo "Missing meta for script: ${cs}.meta"
    missing=1
  fi
done < <(fd -a -g '*.cs' UnityMcpBridge)

while read -r meta; do
  cs="${meta%.meta}"
  if [ ! -f "$cs" ]; then
    echo "Stray .cs.meta without corresponding script: $meta"
    missing=1
  fi
done < <(fd -a -g '*.cs.meta' UnityMcpBridge)
[ $missing -eq 0 ] && echo "All .cs/.cs.meta pairs look consistent."

echo
echo "3) Validate MonoImporter blocks (serializedVersion: 2) in all .cs.meta files..."
issues=0
while read -r meta; do
  rg -q '^MonoImporter:' "$meta" || { echo "Missing MonoImporter in: $meta"; issues=1; }
  rg -q '^  serializedVersion:\s*2\b' "$meta" || { echo "Unexpected or missing serializedVersion in: $meta"; issues=1; }
done < <(fd -a -g '*.cs.meta' UnityMcpBridge)
[ $issues -eq 0 ] && echo "MonoImporter blocks look consistent."

Length of output: 604


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Scan for duplicate GUIDs across all .meta files..."
fd -a -g '*.meta' UnityMcpBridge | while read -r f; do
  guid=$(awk '/^guid:/{print $2}' "$f")
  echo "$guid $f"
done > /tmp/meta_guids_all.txt

dups=$(cut -d' ' -f1 /tmp/meta_guids_all.txt | sort | uniq -d)
if [ -n "$dups" ]; then
  echo "Duplicate GUIDs detected:"
  for d in $dups; do
    grep "^$d " /tmp/meta_guids_all.txt
  done
else
  echo "No duplicate GUIDs found."
fi

echo
echo "2) Ensure every .cs has a .cs.meta and vice versa..."
missing=0
while read -r cs; do
  if [ ! -f "${cs}.meta" ]; then
    echo "Missing meta for script: ${cs}.meta"
    missing=1
  fi
done < <(fd -a -g '*.cs' UnityMcpBridge)

while read -r meta; do
  cs="${meta%.meta}"
  if [ ! -f "$cs" ]; then
    echo "Stray .cs.meta without corresponding script: $meta"
    missing=1
  fi
done < <(fd -a -g '*.cs.meta' UnityMcpBridge)

[ $missing -eq 0 ] && echo "All .cs/.cs.meta pairs look consistent."

echo
echo "3) Validate MonoImporter blocks (serializedVersion: 2) in all .cs.meta files..."
issues=0
for meta in $(fd -a -g '*.cs.meta' UnityMcpBridge); do
  if ! grep -q '^MonoImporter:' "$meta"; then
    echo "Missing MonoImporter in: $meta"
    issues=1
  fi
  if ! grep -q '^  serializedVersion:[[:space:]]*2\b' "$meta"; then
    echo "Unexpected or missing serializedVersion in: $meta"
    issues=1
  fi
done

[ $issues -eq 0 ] && echo "MonoImporter blocks look consistent."

Length of output: 11619


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "2) Checking .cs/.cs.meta consistency..."
missing=0
for cs in $(fd -g '*.cs' UnityMcpBridge); do
  if [[ ! -f "${cs}.meta" ]]; then
    echo "Missing meta for script: ${cs}.meta"
    missing=1
  fi
done

for meta in $(fd -g '*.cs.meta' UnityMcpBridge); do
  cs="${meta%.meta}"
  if [[ ! -f "$cs" ]]; then
    echo "Stray .cs.meta without corresponding script: $meta"
    missing=1
  fi
done

if [[ $missing -eq 0 ]]; then
  echo "All .cs/.cs.meta pairs look consistent."
fi

echo
echo "3) Validating MonoImporter blocks..."
issues=0
for meta in $(fd -g '*.cs.meta' UnityMcpBridge); do
  if ! grep -q '^MonoImporter:' "$meta"; then
    echo "Missing MonoImporter in: $meta"
    issues=1
  fi
  if ! grep -q '^  serializedVersion:[[:space:]]*2\b' "$meta"; then
    echo "Unexpected or missing serializedVersion in: $meta"
    issues=1
  fi
done

if [[ $issues -eq 0 ]]; then
  echo "MonoImporter blocks look consistent."
fi

Length of output: 16803


Fix mismatched .cs/.cs.meta in Editor/Windows before merging

No duplicate GUIDs found and all MonoImporter blocks are correct, but there’s a name mismatch in Editor/Windows:

  • Missing metadata for script:
    UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs → no corresponding .cs.meta
  • Stray metadata without a script:
    UnityMcpBridge/Editor/Windows/UnityMCPEditorWindow.cs.meta

Please rename or add the .meta file so its filename exactly matches the .cs script.

🤖 Prompt for AI Agents
In UnityMcpBridge/Editor/Helpers/ServerInstaller.cs.meta lines 2-11 and the
Editor/Windows folder, there is a name mismatch: the script
UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs lacks a .meta and there is
a stray UnityMCPEditorWindow.cs.meta; rename the stray file to exactly match the
script filename (UnityMcpEditorWindow.cs.meta — preserving case) or create a new
.meta with the same GUID/MonoImporter block if needed, and verify the
MonoImporter block and GUID remain correct and unique after renaming.

Comment on lines +853 to +871
private static string ComputeProjectHash(string input)
{
try
{
using var sha1 = System.Security.Cryptography.SHA1.Create();
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(input ?? string.Empty);
byte[] hashBytes = sha1.ComputeHash(bytes);
var sb = new System.Text.StringBuilder();
foreach (byte b in hashBytes)
{
sb.Append(b.ToString("x2"));
}
return sb.ToString()[..8];
}
catch
{
return "default";
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Extract hash computation to a shared utility

The ComputeProjectHash method is duplicated here and in PortManager.cs (lines 298-316 from the relevant code snippets). Consider extracting this to a shared utility class to avoid duplication.

Create a new utility class:

// In UnityMcpBridge/Editor/Helpers/HashUtility.cs
namespace UnityMcpBridge.Editor.Helpers
{
    internal static class HashUtility
    {
        internal static string ComputeProjectHash(string input)
        {
            try
            {
                using var sha1 = System.Security.Cryptography.SHA1.Create();
                byte[] bytes = System.Text.Encoding.UTF8.GetBytes(input ?? string.Empty);
                byte[] hashBytes = sha1.ComputeHash(bytes);
                var sb = new System.Text.StringBuilder();
                foreach (byte b in hashBytes)
                {
                    sb.Append(b.ToString("x2"));
                }
                return sb.ToString()[..8];
            }
            catch
            {
                return "default";
            }
        }
    }
}

Then update both files to use the shared utility:

-private static string ComputeProjectHash(string input)
-{
-    try
-    {
-        using var sha1 = System.Security.Cryptography.SHA1.Create();
-        byte[] bytes = System.Text.Encoding.UTF8.GetBytes(input ?? string.Empty);
-        byte[] hashBytes = sha1.ComputeHash(bytes);
-        var sb = new System.Text.StringBuilder();
-        foreach (byte b in hashBytes)
-        {
-            sb.Append(b.ToString("x2"));
-        }
-        return sb.ToString()[..8];
-    }
-    catch
-    {
-        return "default";
-    }
-}

// Update the call site:
-string filePath = Path.Combine(dir, $"unity-mcp-status-{ComputeProjectHash(Application.dataPath)}.json");
+string filePath = Path.Combine(dir, $"unity-mcp-status-{HashUtility.ComputeProjectHash(Application.dataPath)}.json");
🤖 Prompt for AI Agents
In UnityMcpBridge/Editor/UnityMcpBridge.cs around lines 853-871, the
ComputeProjectHash method is duplicated elsewhere (PortManager.cs lines
~298-316); extract this logic into a single internal static utility class
UnityMcpBridge.Editor.Helpers.HashUtility with a ComputeProjectHash(string)
method containing the existing try/catch SHA1 logic, then remove the duplicate
method from UnityMcpBridge.cs and PortManager.cs and replace calls to
ComputeProjectHash with HashUtility.ComputeProjectHash; ensure the new file is
added to the project namespace UnityMcpBridge.Editor.Helpers and that necessary
using statements are added where the utility is used.

Comment on lines +634 to +642
string[] strArgs = ((System.Collections.Generic.IEnumerable<object>)args)
.Select(x => x?.ToString() ?? string.Empty)
.ToArray();
string dir = ExtractDirectoryArg(strArgs);
if (string.IsNullOrEmpty(dir)) return false;
return PathsEqual(dir, pythonDir);
}
catch { return false; }
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential null reference when casting args

The cast (System.Collections.Generic.IEnumerable<object>)args could fail if args is not of the expected type, leading to an InvalidCastException. Consider using a safer approach.

-string[] strArgs = ((System.Collections.Generic.IEnumerable<object>)args)
-    .Select(x => x?.ToString() ?? string.Empty)
-    .ToArray();
+string[] strArgs = null;
+if (args != null)
+{
+    if (args is Newtonsoft.Json.Linq.JArray jArray)
+    {
+        strArgs = jArray.Select(x => x?.ToString() ?? string.Empty).ToArray();
+    }
+    else if (args is System.Collections.Generic.IEnumerable<object> enumerable)
+    {
+        strArgs = enumerable.Select(x => x?.ToString() ?? string.Empty).ToArray();
+    }
+}
+if (strArgs == null) return false;
📝 Committable suggestion

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

Suggested change
string[] strArgs = ((System.Collections.Generic.IEnumerable<object>)args)
.Select(x => x?.ToString() ?? string.Empty)
.ToArray();
string dir = ExtractDirectoryArg(strArgs);
if (string.IsNullOrEmpty(dir)) return false;
return PathsEqual(dir, pythonDir);
}
catch { return false; }
}
string[] strArgs = null;
if (args != null)
{
if (args is Newtonsoft.Json.Linq.JArray jArray)
{
strArgs = jArray.Select(x => x?.ToString() ?? string.Empty).ToArray();
}
else if (args is System.Collections.Generic.IEnumerable<object> enumerable)
{
strArgs = enumerable.Select(x => x?.ToString() ?? string.Empty).ToArray();
}
}
if (strArgs == null) return false;
string dir = ExtractDirectoryArg(strArgs);
if (string.IsNullOrEmpty(dir)) return false;
return PathsEqual(dir, pythonDir);
🤖 Prompt for AI Agents
In UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs around lines 634-642,
the direct cast (System.Collections.Generic.IEnumerable<object>)args can throw
if args isn't that exact type; change it to a safe check: first test if args is
IEnumerable (via "as IEnumerable" or "is IEnumerable") and if not return false,
then enumerate its elements and call ToString() on each element (handling nulls)
to build strArgs; avoid the explicit generic cast and the broad catch-all so
only expected failures return false.

Comment on lines +948 to +977
private static bool IsValidUv(string path)
{
return !string.IsNullOrEmpty(path)
&& System.IO.Path.IsPathRooted(path)
&& System.IO.File.Exists(path);
}

private static bool ValidateUvBinarySafe(string path)
{
try
{
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return false;
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = path,
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var p = System.Diagnostics.Process.Start(psi);
if (p == null) return false;
if (!p.WaitForExit(3000)) { try { p.Kill(); } catch { } return false; }
if (p.ExitCode != 0) return false;
string output = p.StandardOutput.ReadToEnd().Trim();
return output.StartsWith("uv ");
}
catch { return false; }
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consolidate duplicate UV validation methods

The methods IsValidUv and ValidateUvBinarySafe appear to perform similar UV binary validation. Consider consolidating them into a single method to avoid duplication.

-private static bool IsValidUv(string path)
-{
-    return !string.IsNullOrEmpty(path)
-        && System.IO.Path.IsPathRooted(path)
-        && System.IO.File.Exists(path);
-}
-
-private static bool ValidateUvBinarySafe(string path)
+private static bool ValidateUvBinary(string path, bool checkVersion = false)
 {
     try
     {
         if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return false;
+        if (!checkVersion) return true;
+        
         var psi = new System.Diagnostics.ProcessStartInfo
         {
             FileName = path,
             Arguments = "--version",
             UseShellExecute = false,
             RedirectStandardOutput = true,
             RedirectStandardError = true,
             CreateNoWindow = true
         };
         using var p = System.Diagnostics.Process.Start(psi);
         if (p == null) return false;
         if (!p.WaitForExit(3000)) { try { p.Kill(); } catch { } return false; }
         if (p.ExitCode != 0) return false;
         string output = p.StandardOutput.ReadToEnd().Trim();
         return output.StartsWith("uv ");
     }
     catch { return false; }
 }

Then update usage:

-string uvPath = (ValidateUvBinarySafe(existingCommand) ? existingCommand : FindUvPath());
+string uvPath = (ValidateUvBinary(existingCommand, true) ? existingCommand : FindUvPath());
-if (IsValidUv(uvPath)) UnityEditor.EditorPrefs.SetString("UnityMCP.UvPath", uvPath);
+if (ValidateUvBinary(uvPath)) UnityEditor.EditorPrefs.SetString("UnityMCP.UvPath", uvPath);
📝 Committable suggestion

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

Suggested change
private static bool IsValidUv(string path)
{
return !string.IsNullOrEmpty(path)
&& System.IO.Path.IsPathRooted(path)
&& System.IO.File.Exists(path);
}
private static bool ValidateUvBinarySafe(string path)
{
try
{
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return false;
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = path,
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var p = System.Diagnostics.Process.Start(psi);
if (p == null) return false;
if (!p.WaitForExit(3000)) { try { p.Kill(); } catch { } return false; }
if (p.ExitCode != 0) return false;
string output = p.StandardOutput.ReadToEnd().Trim();
return output.StartsWith("uv ");
}
catch { return false; }
}
private static bool ValidateUvBinary(string path, bool checkVersion = false)
{
try
{
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return false;
if (!checkVersion) return true;
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = path,
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var p = System.Diagnostics.Process.Start(psi);
if (p == null) return false;
if (!p.WaitForExit(3000))
{
try { p.Kill(); } catch { }
return false;
}
if (p.ExitCode != 0) return false;
string output = p.StandardOutput.ReadToEnd().Trim();
return output.StartsWith("uv ");
}
catch
{
return false;
}
}
🤖 Prompt for AI Agents
In UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs around lines 948 to
977, there are two overlapping UV validation methods (IsValidUv and
ValidateUvBinarySafe); consolidate them into a single robust method (e.g.,
ValidateUv) that checks non-empty path, rooted path, file existence, and safely
executes the binary with --version (use ProcessStartInfo with
RedirectStandardOutput/Error, timeout and safe kill) returning true only if the
output matches the expected prefix; then replace all references to IsValidUv and
ValidateUvBinarySafe with the new method and remove the duplicate method to
avoid redundancy.

Comment on lines +1686 to 1704
string[] possibleNames = { "UnityMCP", "unityMCP", "unity-mcp", "UnityMcpServer" };
bool success = false;

foreach (string serverName in possibleNames)
{
if (ExecPath.TryRun(claudePath, $"mcp remove {serverName}", projectDir, out var stdout, out var stderr, 10000, pathPrepend))
{
// Force refresh the configuration status
var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
if (claudeClient != null)
{
CheckClaudeCodeConfiguration(claudeClient);
}
Repaint();
UnityEngine.Debug.Log("UnityMCP server successfully registered from Claude Code.");


success = true;
UnityEngine.Debug.Log($"Successfully removed MCP server: {serverName}");
break;
}
else if (!string.IsNullOrEmpty(errors))
else if (!string.IsNullOrEmpty(stderr) &&
!stderr.Contains("No MCP server found", StringComparison.OrdinalIgnoreCase))
{
UnityEngine.Debug.LogWarning($"Claude MCP errors: {errors}");
// If it's not a "not found" error, log it and stop trying
UnityEngine.Debug.LogWarning($"Error removing {serverName}: {stderr}");
break;
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Redundant iteration over server names

The code first checks for existing servers using claude mcp get, then attempts to remove them. The second loop (lines 1689-1704) iterates through the same names unnecessarily. Consider using the existingNames list directly.

-// Try different possible server names
-string[] possibleNames = { "UnityMCP", "unityMCP", "unity-mcp", "UnityMcpServer" };
 bool success = false;
 
-foreach (string serverName in possibleNames)
+foreach (string serverName in existingNames)
 {
     if (ExecPath.TryRun(claudePath, $"mcp remove {serverName}", projectDir, out var stdout, out var stderr, 10000, pathPrepend))
     {
         success = true;
         UnityEngine.Debug.Log($"Successfully removed MCP server: {serverName}");
         break;
     }
     else if (!string.IsNullOrEmpty(stderr) &&
              !stderr.Contains("No MCP server found", StringComparison.OrdinalIgnoreCase))
     {
         // If it's not a "not found" error, log it and stop trying
         UnityEngine.Debug.LogWarning($"Error removing {serverName}: {stderr}");
         break;
     }
 }
📝 Committable suggestion

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

Suggested change
string[] possibleNames = { "UnityMCP", "unityMCP", "unity-mcp", "UnityMcpServer" };
bool success = false;
foreach (string serverName in possibleNames)
{
if (ExecPath.TryRun(claudePath, $"mcp remove {serverName}", projectDir, out var stdout, out var stderr, 10000, pathPrepend))
{
// Force refresh the configuration status
var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
if (claudeClient != null)
{
CheckClaudeCodeConfiguration(claudeClient);
}
Repaint();
UnityEngine.Debug.Log("UnityMCP server successfully registered from Claude Code.");
success = true;
UnityEngine.Debug.Log($"Successfully removed MCP server: {serverName}");
break;
}
else if (!string.IsNullOrEmpty(errors))
else if (!string.IsNullOrEmpty(stderr) &&
!stderr.Contains("No MCP server found", StringComparison.OrdinalIgnoreCase))
{
UnityEngine.Debug.LogWarning($"Claude MCP errors: {errors}");
// If it's not a "not found" error, log it and stop trying
UnityEngine.Debug.LogWarning($"Error removing {serverName}: {stderr}");
break;
}
}
bool success = false;
foreach (string serverName in existingNames)
{
if (ExecPath.TryRun(claudePath, $"mcp remove {serverName}", projectDir, out var stdout, out var stderr, 10000, pathPrepend))
{
success = true;
UnityEngine.Debug.Log($"Successfully removed MCP server: {serverName}");
break;
}
else if (!string.IsNullOrEmpty(stderr) &&
!stderr.Contains("No MCP server found", StringComparison.OrdinalIgnoreCase))
{
// If it's not a "not found" error, log it and stop trying
UnityEngine.Debug.LogWarning($"Error removing {serverName}: {stderr}");
break;
}
}
🤖 Prompt for AI Agents
In UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs around lines 1686 to
1704, the removal loop currently iterates over a hard-coded possibleNames array
even though you previously collected existingNames; replace possibleNames with
the existingNames collection and iterate that directly, preserving the same
ExecPath.TryRun call, success flag setting, logging on success, and the stderr
handling that logs non-"No MCP server found" errors and breaks; ensure you keep
the break on success and on unexpected stderr and remove the redundant
possibleNames variable.

Comment on lines +47 to +49
# Use centralized retry helper
resp = send_command_with_retry("execute_menu_item", params_dict)
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Avoid blocking in an async tool: use async_send_command_with_retry or make the tool sync

execute_menu_item is async but invokes a blocking helper, which can block the event loop. Prefer the async helper, or convert the tool to sync for consistency with other tools.

Option A (preferred if available):

-from unity_connection import get_unity_connection, send_command_with_retry  # Import retry helper
+from unity_connection import async_send_command_with_retry  # Import async retry helper

-        resp = send_command_with_retry("execute_menu_item", params_dict)
+        resp = await async_send_command_with_retry("execute_menu_item", params_dict)
         return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}

Verify the async helper exists:

If not present, fallback:

# inside the function
import asyncio
resp = await asyncio.to_thread(send_command_with_retry, "execute_menu_item", params_dict)

🏁 Script executed:

#!/bin/bash
rg -nP -C2 'def\s+async_send_command_with_retry\s*\(' UnityMcpBridge/UnityMcpServer~/

Length of output: 648


Switch to the async retry helper to prevent blocking the event loop

The async_send_command_with_retry helper is already defined in unity_connection.py, so we should use it instead of the blocking send_command_with_retry.

• Replace the import and call in src/tools/execute_menu_item.py:

-from unity_connection import get_unity_connection, send_command_with_retry
+from unity_connection import async_send_command_with_retry

@@
-        resp = send_command_with_retry("execute_menu_item", params_dict)
+        resp = await async_send_command_with_retry("execute_menu_item", params_dict)
         return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
📝 Committable suggestion

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

Suggested change
# Use centralized retry helper
resp = send_command_with_retry("execute_menu_item", params_dict)
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
from unity_connection import async_send_command_with_retry
# Use centralized retry helper
resp = await async_send_command_with_retry("execute_menu_item", params_dict)
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
🤖 Prompt for AI Agents
In UnityMcpBridge/UnityMcpServer~/src/tools/execute_menu_item.py around lines 47
to 49, replace the blocking send_command_with_retry usage with the async helper:
import and call async_send_command_with_retry from unity_connection.py, make the
surrounding function async (add async def) and await
async_send_command_with_retry("execute_menu_item", params_dict), and preserve
the return value shape by returning the awaited result if it's a dict or
{"success": False, "message": str(result)}; also update any callers to await
this function or propagate async changes.

@dsarno dsarno closed this Aug 16, 2025
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.

3 participants