How the Unity MCP Editor Script Works: Architecture and Command Flow
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, 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. 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:
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:
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:
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:
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:
[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:
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 |
Main container; handles UXML loading, tab switching, and debounced updates |
| CommandRegistry | MCPForUnity/Editor/Tools/CommandRegistry.cs |
Global command dispatcher; handles reflection-based discovery and async execution |
| McpLog | 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 |
Adds the "MCP for Unity" menu entry that triggers ShowWindow() |
Summary
- The Unity MCP editor script uses
MCPForUnityEditorWindowas a modular container that loads UI Toolkit (UXML/USS) assets and manages tab state viaEditorPrefs. - 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 asynchronousHandleCommandmethods. - 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, 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →