Performance Optimization Tips for Unity MCP Handlers: 7 Proven Techniques

Cache reflection metadata, paginate large datasets, and skip system assemblies to keep Unity MCP handlers responsive during high-frequency editor operations.

Unity MCP (Message-Command Protocol) handlers execute inside the Unity Editor and are invoked on-demand from external tools. Because these handlers frequently rely on reflection, assembly scanning, and blocking Editor APIs, small inefficiencies can compound into noticeable lag. The isuzu-shiranui/unitymcp repository demonstrates several battle-tested patterns for performance optimization in Unity MCP handlers; applying these patterns ensures your custom handlers remain lightweight and responsive.

1. Limit Assembly Scanning to Reduce Startup Overhead

The handler discovery mechanism in McpHandlerDiscovery.cs intentionally skips large system and Unity assemblies to prevent expensive type inspection during startup.

// McpHandlerDiscovery.cs – skips system & Unity assemblies
if (assembly.FullName.StartsWith("System.") ||
    assembly.FullName.StartsWith("Unity.") ||
    assembly.FullName.StartsWith("UnityEngine.") ||
    assembly.FullName.StartsWith("UnityEditor."))
{
    continue;   // ← avoids scanning huge built‑in assemblies
}

Source: McpHandlerDiscovery.cs lines 39‑44

Tip: Place custom handlers in a dedicated assembly (e.g., MyCompany.MCP.Handlers) so the discovery loop exits early after scanning only your specific types.

2. Cache Reflection Metadata in Static Constructors

ConsoleCommandHandler eliminates per-call reflection overhead by caching MethodInfo and Type objects in a static constructor. This ensures costly GetMethod and GetType lookups execute only once per Editor session.

static ConsoleCommandHandler()
{
    // Cache reflection data for LogEntries only once
    LogEntriesType = typeof(UnityEditor.EditorWindow).Assembly.GetType("UnityEditor.LogEntries");
    StartGettingEntriesMethod = LogEntriesType.GetMethod("StartGettingEntries", BindingFlags.Public | BindingFlags.Static);
    // … other methods cached here …
}

Source: ConsoleCommandHandler.cs lines 26‑46

Tip: Any handler relying on reflection should adopt this static-caching pattern to avoid repeated metadata lookups during high-frequency command execution.

3. Implement Pagination for Large Dataset Retrieval

Fetching the entire Unity Console log can return massive datasets. ConsoleCommandHandler.GetLogs accepts startRow and count parameters, clamps the request to the actual log count, and returns only the requested slice.

var startRow = parameters["startRow"]?.Value<int>() ?? 0;
var count     = parameters["count"]?.Value<int>() ?? 100;

// Clamp to available logs
var totalCount = (int)GetCountMethod.Invoke(null, null);
count = Math.Min(count, totalCount - startRow);

Source: ConsoleCommandHandler.cs lines 10‑16

Tip: Design new handlers to expose pagination parameters (offset/limit) and enforce a maximum batch size (e.g., 200 items) to keep memory usage predictable.

4. Replace Blocking Waits with Asynchronous Callbacks

PackagesResourceHandler currently polls Client.List and Client.SearchAll with a Thread.Sleep(100) loop. While functional, this blocks the calling thread and can stall the Editor when many requests queue up.

while (!listRequest.IsCompleted)
{
    Thread.Sleep(100);
}

Source: PackagesResourceHandler.cs lines 75‑78

Tip: Switch to Unity’s asynchronous callbacks (listRequest.Completed += ...) or use EditorApplication.update to poll without sleeping. This keeps the main thread responsive and reduces UI stutter.

5. Add Early-Exit Guard Clauses

Both console and package handlers check for empty results before entering costly loops, returning a minimal JSON payload immediately.

if (count <= 0 || startRow >= totalCount)
{
    return new JObject { ["success"] = true, ["logs"] = new JArray(), ["totalCount"] = totalCount };
}

Source: ConsoleCommandHandler.cs lines 17‑24

Tip: Insert guard clauses at the top of any heavy operation to avoid unnecessary processing when data is missing or parameters are out of bounds.

6. Minimize Allocations in Processing Loops

When iterating over log entries or packages, the code re-uses a single JObject per entry and adds it to a JArray. Avoiding temporary collections inside loops reduces GC pressure.

var logs = new JArray();
for (var i = 0; i < count; i++)
{
    // … create logEntry, read fields, then add a new JObject once
    logs.Add(new JObject { ... });
}

Source: ConsoleCommandHandler.cs lines 32‑64

Tip: Pool reusable objects or use struct-based containers for high-frequency loops to minimize garbage collection spikes in the Editor.

7. Use Strongly-Typed Parameters Instead of Re-Parsing JSON Repeatedly

Handlers currently read parameters from a JObject each call (parameters["count"]?.Value<int>()). If a handler is invoked many times with the same shape, parse the JSON once into a C# DTO and reuse it.

Tip: Add a small helper method like private static T GetValue<T>(JObject obj, string key, T fallback) to reduce repetitive ?.Value<T>() calls, or define a strongly-typed options class and deserialize once.

Key Implementation Files

File Role Performance Relevance
Editor/Core/McpHandlerDiscovery.cs Scans assemblies and registers handlers Demonstrates the skip-system-assembly optimization to reduce startup time
Editor/Handlers/ConsoleCommandHandler.cs Provides console log access Shows static reflection caching, pagination, and early-exit patterns
Editor/Handlers/PackagesResourceHandler.cs Retrieves Unity package data Contains the blocking wait anti-pattern and serves as the target for async refactoring
Editor/Handlers/AssembliesResourceHandler.cs Lists available assemblies Uses the same discovery flow; extend the same optimizations here
Editor/Handlers/MenuItemCommandHandler.cs Executes menu-item commands Handler invocation site; apply max-batch limits here if processing multiple items

Summary

  • Skip large assemblies during discovery by filtering out System.*, Unity.*, and UnityEngine.* namespaces in McpHandlerDiscovery.cs.
  • Cache reflection metadata in static constructors to eliminate per-call GetMethod/GetType overhead, following the ConsoleCommandHandler pattern.
  • Paginate all data retrieval by accepting startRow/count parameters and clamping to actual dataset size to prevent memory spikes.
  • Replace blocking sleeps with asynchronous callbacks or EditorApplication.update polling to keep the Unity Editor UI responsive.
  • Add early-exit guard clauses to return minimal JSON payloads immediately when parameters are invalid or datasets are empty.
  • Minimize allocations by reusing JObject instances and avoiding temporary collections inside tight loops.

Frequently Asked Questions

How does assembly scanning impact Unity MCP handler performance?

Assembly scanning occurs during handler discovery and can add significant startup latency if the framework inspects every loaded assembly. The isuzu-shiranui/unitymcp repository mitigates this by explicitly skipping System.*, Unity.*, UnityEngine.*, and UnityEditor.* assemblies in McpHandlerDiscovery.cs, reducing the reflection overhead to only user-defined handler assemblies.

What is the best way to cache reflection data in MCP handlers?

The optimal approach is to use a static constructor to populate Type and MethodInfo fields once per editor session. The ConsoleCommandHandler in Editor/Handlers/ConsoleCommandHandler.cs demonstrates this by caching LogEntriesType and related methods at lines 26-46, ensuring that subsequent command executions invoke the cached delegates without repeated GetMethod lookups.

Why should I avoid Thread.Sleep in Unity MCP package handlers?

Thread.Sleep blocks the calling thread, which in the Unity Editor context can stall the main thread and cause UI stutter when multiple requests queue up. The PackagesResourceHandler.cs currently uses while (!listRequest.IsCompleted) { Thread.Sleep(100); } at lines 75-78. Replacing this with the Completed event callback or EditorApplication.update polling keeps the editor responsive during package operations.

How can I implement pagination in custom Unity MCP handlers?

Expose offset (or startRow) and limit (or count) parameters in your handler's JSON input, then clamp the requested range to the actual dataset size before processing. As shown in ConsoleCommandHandler.cs lines 10-16, calculate Math.Min(count, totalCount - startRow) and return only the sliced subset. Always enforce a hard maximum batch size (e.g., 200 items) to prevent memory spikes.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →