How Commands Are Handled in Unity MCP: A Complete Technical Guide
Unity MCP processes every client operation as a command through a seven-stage pipeline involving dynamic handler discovery, prefix-based routing, and TCP socket communication with the Unity Editor.
Unity MCP (Model Context Protocol) bridges external clients and the Unity Editor via a robust command-handling architecture. Understanding how commands are handled in Unity MCP is essential for developers extending the protocol or debugging integration issues. This guide examines the complete command lifecycle from discovery to execution, referencing the actual TypeScript implementation in the isuzu-shiranui/unitymcp repository.
Command Discovery and Registration
The server initializes its command vocabulary by scanning and registering handler classes dynamically.
Dynamic Handler Discovery
When the server starts, HandlerDiscovery scans the compiled handlers directory (unity-mcp-ts/src/handlers/). It dynamically imports each .js file and instantiates every exported class.
// Conceptual flow from HandlerDiscovery.ts
const handlerFiles = await scanHandlersDirectory();
for (const file of handlerFiles) {
const module = await import(file);
const HandlerClass = module.default;
const instance = new HandlerClass();
// Registration happens next...
}
CommandRegistry Registration
Each discovered instance is examined against the ICommandHandler interface. Valid handlers are added to the central CommandRegistry, which maintains a mapping between command prefixes and handler instances.
// From CommandRegistry.ts
registerHandler(handler: ICommandHandler): void {
const prefix = handler.commandPrefix; // e.g., "menu", "console"
this.handlers.set(prefix, handler);
}
The same instance is also handed to HandlerAdapter so the adapter can later route calls to the correct handler.
Request Reception and Routing
Once registered, handlers process incoming TCP requests through a structured routing mechanism.
TCP Socket Communication via UnityConnection
Clients (such as the Unity Editor plugin) send JSON requests over TCP sockets. UnityConnection.sendRequest tags each request with a unique id, writes it to the active Unity client, and stores a promise in pendingRequests for later resolution.
// Client-side usage pattern
import { UnityConnection } from './unity-mcp-ts/src/core/UnityConnection.js';
async function executeMenuItem(menuPath: string) {
const conn = UnityConnection.getInstance();
await conn.start();
const response = await conn.sendRequest({
command: 'menu.execute',
params: { menuItem: menuPath }
});
return response;
}
Command Prefix Routing
When the server receives a response or request, HandlerAdapter extracts the command string (e.g., menu.execute). The segment before the first dot (menu) serves as the command prefix.
HandlerAdapter looks up the corresponding handler in CommandRegistry and invokes its execute method, passing the action (execute) and parameters.
Command Execution Flow
Handlers implement a consistent execution pattern through base class abstractions.
BaseCommandHandler Abstraction
Most concrete handlers extend BaseCommandHandler. The base class first guarantees a live Unity connection via ensureUnityConnection, then delegates to the handler-specific executeCommand method.
// From BaseCommandHandler.ts
async execute(action: string, params: JObject): Promise<JObject> {
await this.ensureUnityConnection();
return this.executeCommand(action, params);
}
protected abstract executeCommand(
action: string,
params: JObject
): Promise<JObject>;
Handlers typically forward requests to Unity using sendUnityRequest, which serializes the command and manages the socket write operation.
Concrete Handler Implementation
The MenuItemCommandHandler demonstrates a typical implementation. It declares its prefix as menu and implements executeCommand to forward menu execution requests to the Unity Editor.
// Excerpt from MenuItemCommandHandler.ts
class MenuItemCommandHandler extends BaseCommandHandler {
get commandPrefix(): string { return 'menu'; }
protected async executeCommand(
action: string,
params: JObject
): Promise<JObject> {
if (action === 'execute') {
await this.ensureUnityConnection();
const response = await this.sendUnityRequest(
`${this.commandPrefix}.execute`,
{ menuItem: params.menuItem }
);
return response;
}
return { success: false, error: 'Unknown action' };
}
}
Implementing Custom Command Handlers
Developers can extend Unity MCP by creating new handler classes. The system automatically discovers and registers them without modifying core router code.
Here is a complete custom handler that adds a ping health-check command:
// Save as: unity-mcp-ts/src/handlers/PingCommandHandler.ts
import { BaseCommandHandler } from '../core/BaseCommandHandler.js';
import { JObject } from '../types/index.js';
export class PingCommandHandler extends BaseCommandHandler {
public get commandPrefix(): string { return 'ping'; }
public get description(): string { return 'Simple health-check command'; }
public getToolDefinitions() { return null; }
protected async executeCommand(
action: string,
_params: JObject
): Promise<JObject> {
if (action.toLowerCase() !== 'ping') {
return { success: false, error: `Unsupported action ${action}` };
}
// Directly answer without contacting Unity
return { success: true, reply: 'pong' };
}
}
Because HandlerDiscovery loads every compiled handler automatically, the new ping command becomes available as ping.pong immediately upon server restart.
Summary
- Dynamic Discovery:
HandlerDiscoveryscans the handlers directory at startup, instantiating every exported class it finds. - Prefix Registration: Valid handlers implementing
ICommandHandlerregister their command prefix (e.g.,menu,console) withCommandRegistry. - Socket Communication:
UnityConnectionmanages TCP sockets, tagging requests with unique IDs and tracking pending promises. - Prefix Routing:
HandlerAdapterextracts the command prefix from incoming requests (e.g.,menufrommenu.execute) and routes to the appropriate handler. - Base Abstraction:
BaseCommandHandlerensures Unity connectivity before delegating to handler-specificexecuteCommandimplementations. - Extensibility: New commands require only creating a handler class extending
BaseCommandHandler; no core router modifications are necessary.
Frequently Asked Questions
What is the role of the command prefix in Unity MCP?
The command prefix is the segment before the first dot in a command string (e.g., menu in menu.execute). It acts as a namespace that HandlerAdapter uses to look up the correct ICommandHandler instance in CommandRegistry. Each handler declares its prefix via the commandPrefix getter, enabling the router to dispatch requests without hardcoding handler references.
How does Unity MCP handle requests when Unity Editor is not connected?
BaseCommandHandler includes an ensureUnityConnection method that verifies an active TCP connection to the Unity Editor before executing commands. If the connection is lost or unavailable, the method throws an error or returns a failure response, preventing commands from being dropped silently. Handlers that do not require Unity interaction (like the example PingCommandHandler) can bypass this check by implementing custom logic directly in executeCommand.
Can I add new commands without modifying the core server code?
Yes. Unity MCP uses dynamic discovery via HandlerDiscovery to load handlers from the src/handlers/ directory at startup. To add a new command, create a class extending BaseCommandHandler, implement the commandPrefix getter and executeCommand method, and export the class from a new file in the handlers directory. The server automatically registers the new prefix and routes matching commands to your handler without requiring changes to CommandRegistry or HandlerAdapter.
What is the difference between HandlerAdapter and CommandRegistry?
CommandRegistry serves as a static storage mechanism that maps command prefixes to handler instances, providing methods like registerHandler and getHandler. HandlerAdapter acts as the active router that intercepts incoming requests, extracts the command prefix, queries CommandRegistry for the appropriate handler, and invokes the handler's execute method. While CommandRegistry maintains the lookup table, HandlerAdapter performs the runtime dispatch logic.
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 →