# How the Unity MCP Editor Script Works: Architecture and Command Flow

> Explore the Unity MCP editor script architecture. Learn how the CommandRegistry processes tools via reflection and handles sync async command invocation for efficient Unity development.

- Repository: [Coplay/unity-mcp](https://github.com/CoplayDev/unity-mcp)
- Tags: architecture
- Published: 2026-07-07

---

**The Unity MCP editor script centers around the `MCPForUnityEditorWindow` class, which loads a modular UI Toolkit interface and delegates all tool execution to a static `CommandRegistry` that auto-discovers commands via reflection and handles both synchronous and asynchronous invocation.**

The editor script in the CoplayDev/unity-mcp repository provides the Unity-side interface for the Model Context Protocol (MCP) bridge. It transforms the Unity Editor into an MCP host that AI assistants can query, exposing project tools and resources through a unified command surface while maintaining a responsive, decoupled UI.

## The Main Editor Window (MCPForUnityEditorWindow)

At the heart of the Unity MCP editor script is **`MCPForUnityEditorWindow`**, a subclass of Unity's `EditorWindow` that serves as the primary container for all MCP-related functionality.

### Window Lifecycle and Initialization

The window follows Unity's UI Toolkit pipeline for creation and rendering. When a user selects the menu item added by [`MCPForUnityMenu.cs`](https://github.com/CoplayDev/unity-mcp/blob/main/MCPForUnityMenu.cs), the static method **`ShowWindow()`** checks for an existing instance or creates a new one via `GetWindow<MCPForUnityEditorWindow>()`.

Upon creation, Unity calls **`CreateGUI()`**, which loads the main layout from `MCPForUnityEditorWindow.uxml` and applies styling from the associated `.uss` files. The method caches references to critical UI elements—such as the version label, update notification banner, and tab toggles—before instantiating the individual section controllers.

To prevent performance degradation during heavy editor use, the window implements **`OnEditorUpdate`**, a debounced update method that runs at most every 2 seconds. This polls connection status and refreshes UI state without executing expensive per-frame network checks.

### Lazy-Loaded UI Sections

Rather than initializing all panels at startup, the editor employs **lazy loading** for its modular sections. Each tab—Connection, Tools, Resources, Asset Generation—has a dedicated controller class (e.g., `McpConnectionSection`, `McpToolsSection`) located in `MCPForUnity/Editor/Windows/Components/`.

When a user switches tabs via `SwitchPanel()`, the window checks `EditorPrefs` to persist the active selection, then calls **`EnsureToolsLoaded()`** or **`EnsureResourcesLoaded()`**. These methods instantiate the section's UXML subtree and initialize the controller only when the tab becomes visible, keeping the initial window creation lightweight.

## Command Registry and Auto-Discovery

The editor script delegates all business logic to the **`CommandRegistry`** class in [`MCPForUnity/Editor/Tools/CommandRegistry.cs`](https://github.com/CoplayDev/unity-mcp/blob/main/MCPForUnity/Editor/Tools/CommandRegistry.cs). This static registry eliminates tight coupling between the UI and tool implementations, allowing both the C# editor and external Python bridges to invoke the same commands.

### Tool and Resource Discovery via Reflection

On first access, **`CommandRegistry.Initialize()`** scans all assemblies for classes marked with **`[McpForUnityTool]`** or **`[McpForUnityResource]`**. For each discovered type, it extracts the public static **`HandleCommand(JObject)`** method and stores a **`HandlerInfo`** struct containing either a synchronous delegate or an asynchronous `Task`-based delegate.

This reflection-based approach means new tools appear automatically after compilation without requiring manual registration in the window code.

### Async Command Execution

When the UI needs to invoke a tool, it calls:

```csharp
var tcs = new TaskCompletionSource<string>();
CommandRegistry.ExecuteCommand("manage_material", parameters, tcs);

```

If the handler is asynchronous, the registry executes it on a background thread and fulfills the `TaskCompletionSource<string>` with a JSON payload containing success or error status. Synchronous handlers return results directly. This design prevents the Unity Editor UI from freezing during long-running operations like asset generation or play-mode testing.

## Practical Implementation Examples

### Opening the MCP Window Programmatically

You can trigger the editor window from custom editor scripts:

```csharp
using MCPForUnity.Editor.Windows;
using UnityEditor;

public static class MCPLauncher 
{
    [MenuItem("MCP/Show Editor Window")]
    public static void Open() => MCPForUnityEditorWindow.ShowWindow();
}

```

### Creating a Custom Tool

Add new capabilities by creating a static class with the appropriate attribute:

```csharp
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;

[McpForUnityTool("my_custom_tool")]
public static class MyCustomTool 
{
    public static object HandleCommand(JObject @params) 
    {
        var name = @params.RequireString("name");
        // Unity-specific logic here...
        return new { status = "ok", message = $"Hello, {name}!" };
    }
}

```

After compilation, `CommandRegistry` automatically registers `"my_custom_tool"`. Invoke it from the UI or external scripts:

```csharp
var payload = new JObject { ["name"] = "World" };
var result = await CommandRegistry.InvokeCommandAsync("my_custom_tool", payload);

```

### Handling Asynchronous Operations

For long-running tasks, implement an async handler:

```csharp
[McpForUnityTool("play_test")]
public static class PlayTestTool 
{
    public static async Task<object> HandleCommand(JObject @params) 
    {
        await Task.Delay(2000); // Simulate work
        return new { status = "done", result = "Play test finished" };
    }
}

```

The UI calls this using the `TaskCompletionSource` pattern:

```csharp
var tcs = new TaskCompletionSource<string>();
CommandRegistry.ExecuteCommand("play_test", new JObject(), tcs);
string json = await tcs.Task; // Returns {"status":"success","result":...}

```

## Key Architectural Components

| Component | File Path | Purpose |
|-----------|-----------|---------|
| **MCPForUnityEditorWindow** | [`MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs`](https://github.com/CoplayDev/unity-mcp/blob/main/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs) | Main container; handles UXML loading, tab switching, and debounced updates |
| **CommandRegistry** | [`MCPForUnity/Editor/Tools/CommandRegistry.cs`](https://github.com/CoplayDev/unity-mcp/blob/main/MCPForUnity/Editor/Tools/CommandRegistry.cs) | Global command dispatcher; handles reflection-based discovery and async execution |
| **McpLog** | [`MCPForUnity/Editor/Helpers/McpLog.cs`](https://github.com/CoplayDev/unity-mcp/blob/main/MCPForUnity/Editor/Helpers/McpLog.cs) | Centralized logging with consistent prefixing for editor diagnostics |
| **Section Controllers** | `MCPForUnity/Editor/Windows/Components/*` | Individual UI panels (Connection, Tools, Resources) that own specific UXML subtrees |
| **MCPForUnityMenu** | [`MCPForUnity/Editor/MenuItems/MCPForUnityMenu.cs`](https://github.com/CoplayDev/unity-mcp/blob/main/MCPForUnity/Editor/MenuItems/MCPForUnityMenu.cs) | Adds the "MCP for Unity" menu entry that triggers `ShowWindow()` |

## Summary

- The **Unity MCP editor script** uses `MCPForUnityEditorWindow` as a modular container that loads UI Toolkit (UXML/USS) assets and manages tab state via `EditorPrefs`.
- **Lazy initialization** ensures that heavy panels like Tools and Resources only load when accessed, while a 2-second debounced update loop keeps connection status fresh without performance impact.
- **CommandRegistry** auto-discovers capabilities via reflection on `[McpForUnityTool]` and `[McpForUnityResource]` attributes, storing delegates for synchronous or asynchronous `HandleCommand` methods.
- All command execution routes through `CommandRegistry.ExecuteCommand()`, which handles threading and JSON serialization, allowing the UI to remain responsive during long-running MCP operations.

## Frequently Asked Questions

### How does the Unity MCP editor script load its interface?

The editor script loads its interface through Unity's UI Toolkit system. In [`MCPForUnityEditorWindow.cs`](https://github.com/CoplayDev/unity-mcp/blob/main/MCPForUnityEditorWindow.cs), the `CreateGUI()` method instantiates the visual tree from `MCPForUnityEditorWindow.uxml` and applies stylesheets. Individual sections (like the Tools panel) load their own UXML assets only when the user selects the corresponding tab, implementing a lazy-loading pattern that improves startup performance.

### What is the CommandRegistry and why is it important?

`CommandRegistry` is a static class in [`MCPForUnity/Editor/Tools/CommandRegistry.cs`](https://github.com/CoplayDev/unity-mcp/blob/main/MCPForUnity/Editor/Tools/CommandRegistry.cs) that functions as a global command dispatcher. It is crucial because it decouples the UI from tool implementations through reflection-based discovery. It searches for classes marked with `[McpForUnityTool]` or `[McpForUnityResource]`, caches their `HandleCommand` methods, and provides a unified `ExecuteCommand` API that handles both synchronous returns and asynchronous `TaskCompletionSource` patterns for background operations.

### How do I add a custom tool that appears in the Unity MCP editor?

Create a public static class decorated with `[McpForUnityTool("tool_name")]` and implement a public static method named `HandleCommand` that accepts a `JObject` parameter. The method can return an object directly for synchronous operations or `Task<object>` for async work. After compiling, `CommandRegistry.Initialize()` automatically discovers and registers the tool, making it available to both the editor UI and external MCP clients without modifying window code.

### How does the editor handle asynchronous commands without freezing the UI?

When invoking async commands, the UI creates a `TaskCompletionSource<string>` and passes it to `CommandRegistry.ExecuteCommand()`. The registry runs the async handler on a background thread and sets the task result upon completion. The UI awaits this task, allowing Unity's main thread to continue processing while the command executes. This pattern is used throughout the `McpToolsSection` and similar controllers to maintain editor responsiveness during asset generation or play-mode testing.