Unity AI Complete Mastery Series
Part 1 — Unity AI Assistant Complete Beginner’s Guide: Ask, Plan & Agent Modes
Part 2 — Unity MCP Complete Guide: Connecting Claude Code, Codex, Gemini & Cursor to Unity Editor
Part 3 — Building Custom Unity MCP Tools: McpTool Deep Dive and 4 Registration Methods (this post)
Part 4 — Automating AI Workflows with the Unity Skills System: SKILL.md Complete Guide
Appendix — Unity MCP Built-in Tools Complete Reference: Parameters & Activation Guide for 51 Tools
Part 5 — Unity AI Assistant Project Overview Auto-Generation: Making AI Understand Your Project
Part 6 — Unity AI Assistant × Profiler: Diagnose Performance Bottlenecks in Plain Language
This post is written for Unity 6. The built-in tools that ship with Unity MCP Server can only take you so far — every project has its own requirements. That gap is exactly what the [McpTool] attribute is designed to fill. Before reading this post, make sure you’ve completed the setup covered in Part 2 (Unity MCP Server + AI Gateway). Setup is not repeated here; this post focuses entirely on writing custom tools.
TL;DR
– The[McpTool]attribute is how you register custom tools for external AI clients like Claude Code and Cursor to control the Unity Editor.
– On startup,McpToolRegistryscans assemblies viaTypeCacheand auto-discovers all tools.
– There are 4 registration methods: Static typed (auto schema) / JObject (custom schema) / Class-based (stateful) / Runtime API (dynamic).
– The call chain runs: External AI client → relay binary → Unity Editor MCP Bridge → McpToolRegistry → your custom tool.
– This post walks through all three real-world examples: Missing Script detection, Addressables bundle report, and NavMesh Agent inspection.
Table of Contents
- What Is McpTool?
- When Should You Build a McpTool?
- 4 Registration Methods
- Attribute Reference
- Example 1 — Missing Script Detection (Static typed)
- Example 2 — Addressables Bundle Size Report (JObject)
- Example 3 — NavMesh Agent Batch Inspection (Class-based)
- Bonus: Runtime API Dynamic Registration
- Registry Events (ToolsChanged)
- Wrapping Up
What Is McpTool?
[McpTool] is an attribute in the Unity.AI.MCP.Editor.ToolRegistry namespace. Any method (or class) decorated with this attribute is automatically registered in McpToolRegistry when the Unity Editor starts, via an assembly scan using TypeCache. In other words, you don’t have to call any registration code manually — attaching [McpTool] is all it takes for your tool to become available over MCP.
External AI clients (Claude Code, Cursor, Codex, Gemini) send commands to the relay binary (~/.unity/relay/) over the MCP protocol (stdio). The relay connects to the MCP Bridge inside the Unity Editor via IPC (named pipe on Windows, Unix socket on macOS/Linux). The MCP Bridge looks up the tool in McpToolRegistry and executes it. The return value travels back through the same path to the AI client.
Thanks to this architecture, you can type something like “check for missing scripts in the scene” directly in Claude Code, and the Unity Editor will analyze the scene and return the results.
When Should You Build a McpTool?
There will always come a point where the built-in tools (Unity_ReadConsole, get_components, etc.) aren’t enough. In my experience, there are three situations where custom McpTools really shine:
- Project-specific quality checks: Things like Missing Script detection or asset naming convention violations vary by project. Encapsulate the logic in a tool and a single “run the check” request handles it.
- Build and asset pipeline reports: A report that combines multiple APIs — like Addressables bundle size analysis — becomes a one-liner for your AI agent, which can drop it into a pre-build routine automatically.
- Scene and gameplay setting audits: Repetitive checks like finding NavMesh Agents with speed or radius over a threshold become AI-callable tools that can run automatically during refactoring to catch regressions.
4 Registration Methods
| Method | Parameter type | Schema generation | Best for |
|---|---|---|---|
| Static typed | Custom parameter class | Automatic | Most cases (recommended) |
| JObject | JObject |
Manual ([McpSchema]) |
Nested or dynamic schemas |
| Class-based | Custom parameter class | Automatic | When you need to preserve state between calls |
| Runtime API | N/A | Manual | Dynamic registration/removal in plugins or editor extensions |
Start with Static typed. The JSON schema is generated automatically from your parameter class properties — no need to write a separate [McpSchema] method. Move to JObject when you need nested or conditional fields, and reach for Class-based when your tool needs to remember results across calls. In practice, Static typed covers 80%+ of use cases. JObject is the right pick when your filter parameters go more than two levels deep; Class-based is for when the AI needs to compare against “what it saw last time.”
Method 1: Static typed
The JSON schema is generated automatically from the parameter type. This is the simplest approach and the one you should default to.
using Unity.AI.MCP.Editor.ToolRegistry;
[McpTool("my_tool", "Description of what this tool does")]
public static object MyTool(MyParameters parameters)
{
return new { success = true, message = $"Processed: {parameters.Name}" };
}
public class MyParameters
{
[McpDescription("Action to perform", Required = true, EnumType = typeof(ActionType))]
public string Action { get; set; }
[McpDescription("Target name")]
public string Name { get; set; } = "default";
}
public enum ActionType { Create, Update, Delete }
Method 2: JObject
Use this when your schema can’t be cleanly expressed as a simple class — nested structures, conditional properties, and the like. Link your schema provider method with the [McpSchema] attribute.
using Newtonsoft.Json.Linq;
using Unity.AI.MCP.Editor.ToolRegistry;
[McpTool("flexible_tool", "Tool with custom schema")]
public static object HandleFlexibleTool(JObject parameters)
{
var action = parameters["action"]?.ToString();
var data = parameters["data"];
return new { processed = action, data };
}
[McpSchema("flexible_tool")]
public static object GetFlexibleToolSchema()
{
return new
{
type = "object",
properties = new
{
action = new { type = "string", @enum = new[] { "process", "validate" } },
data = new { type = "object", description = "Flexible data object" }
},
required = new[] { "action" }
};
}
Method 3: Class-based
Apply [McpTool] to a class that implements IUnityMcpTool<T>. Instance fields let you preserve state across calls. A public parameterless constructor is required.
using Unity.AI.MCP.Editor.ToolRegistry;
using System.Collections.Generic;
using System.Threading.Tasks;
[McpTool("stateful_tool", "Tool with internal state")]
public class StatefulTool : IUnityMcpTool<StatefulParams>
{
readonly Dictionary<string, object> _cache = new();
public Task<object> ExecuteAsync(StatefulParams parameters)
{
var result = new { id = parameters.Id, processed = true };
_cache[parameters.Id] = result;
return Task.FromResult<object>(result);
}
}
public class StatefulParams
{
[McpDescription("Unique identifier", Required = true)]
public string Id { get; set; }
}
Method 4: Runtime API
Use McpToolRegistry.RegisterTool<T>() and McpToolRegistry.UnregisterTool() to register and remove tools dynamically at runtime. Useful when a package or editor extension is enabled or disabled.
using Unity.AI.MCP.Editor.ToolRegistry;
// Register the tool
var tool = new MyTool();
McpToolRegistry.RegisterTool<MyParams>("dynamic_tool", tool, "A dynamically registered tool");
// Remove it when no longer needed
McpToolRegistry.UnregisterTool("dynamic_tool");
Attribute Reference
[McpTool(name, description)]
| Property | Type | Required | Description |
|---|---|---|---|
Name |
string | ✅ | Unique tool identifier exposed to MCP clients |
Description |
string | Human-readable description of the tool | |
Title |
string | Display title (defaults to description) | |
Groups |
string[] | Category tags for grouping tools |
The tool name (Name) is exposed as-is to AI clients over the MCP protocol. It must be unique within your project, and snake_case is the convention.
[McpDescription(description)]
| Property | Type | Required | Description |
|---|---|---|---|
Description |
string | ✅ | Human-readable description of the parameter |
Required |
bool | Whether the parameter is required (default: false) | |
EnumType |
Type | Enum type to restrict allowed values | |
Default |
object | Default value to declare in the schema |
Parameters marked Required = true are automatically added to the required array in the JSON schema. If an AI client omits a required parameter, the schema validation catches it before your code ever runs.
[McpSchema(toolName)] / [McpOutputSchema(toolName)]
Links a method that provides a custom input or output schema to a JObject-based tool. Both attributes take toolName to identify the target tool.
Example 1 — Missing Script Detection (Static typed)
This tool scans the active scene and returns the hierarchy paths of every GameObject with a missing MonoBehaviour. It’s a natural fit for a pre-build routine or as the first step an AI agent takes when you ask it to “clean up the scene.”
using Unity.AI.MCP.Editor.ToolRegistry;
using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections.Generic;
public static class SceneDiagnosticTools
{
[McpTool("find_missing_scripts", "Returns all GameObject paths in the active scene that have a missing MonoBehaviour.")]
public static object FindMissingScripts(FindMissingScriptsParams parameters)
{
var scene = SceneManager.GetActiveScene();
var results = new List<string>();
foreach (var root in scene.GetRootGameObjects())
ScanGameObject(root, "", results, parameters.IncludeInactive);
return new
{
scene = scene.name,
count = results.Count,
gameObjects = results
};
}
static void ScanGameObject(GameObject go, string parentPath, List<string> results, bool includeInactive)
{
if (!includeInactive && !go.activeInHierarchy) return;
var path = string.IsNullOrEmpty(parentPath) ? go.name : $"{parentPath}/{go.name}";
// Unity represents missing scripts as null Components
foreach (var component in go.GetComponents<Component>())
{
if (component == null)
{
results.Add(path);
break;
}
}
foreach (Transform child in go.transform)
ScanGameObject(child.gameObject, path, results, includeInactive);
}
}
public class FindMissingScriptsParams
{
[McpDescription("Whether to include inactive GameObjects")]
public bool IncludeInactive { get; set; } = true;
}
SceneManager.GetActiveScene().GetRootGameObjects() retrieves all root objects in the scene, which are then traversed recursively. Unity represents a missing MonoBehaviour as a null Component, so the component == null check is all you need to detect them. The return value is an anonymous object that serializes to JSON, and Claude Code calls this tool directly using the name find_missing_scripts.
Example 2 — Addressables Bundle Size Report (JObject)
This tool analyzes asset sizes per Addressables group and returns the top N largest assets. The filtering logic — by group name and label — requires a composite parameter structure, which is why JObject was chosen here. The com.unity.addressables package must be installed in your project.
using Newtonsoft.Json.Linq;
using Unity.AI.MCP.Editor.ToolRegistry;
using UnityEditor.AddressableAssets;
using UnityEditor;
using System.Collections.Generic;
using System.IO;
using System.Linq;
public static class AddressablesTools
{
[McpTool("addressables_bundle_report", "Analyzes asset sizes per Addressables group and returns the top N largest assets.")]
public static object AddressablesBundleReport(JObject parameters)
{
var groupFilter = parameters["group"]?.ToString();
var labelFilter = parameters["label"]?.ToString();
int topN = parameters["top_n"]?.ToObject<int>() ?? 10;
var settings = AddressableAssetSettingsDefaultObject.Settings;
if (settings == null)
return new { error = "Addressables settings not found." };
var entries = new List<(string path, string group, long size)>();
foreach (var group in settings.groups)
{
if (group == null) continue;
if (!string.IsNullOrEmpty(groupFilter) && group.Name != groupFilter) continue;
foreach (var entry in group.entries)
{
if (!string.IsNullOrEmpty(labelFilter) && !entry.labels.Contains(labelFilter)) continue;
var assetPath = AssetDatabase.GUIDToAssetPath(entry.guid);
long size = 0;
if (!string.IsNullOrEmpty(assetPath))
{
var fullPath = Path.GetFullPath(assetPath);
if (File.Exists(fullPath))
size = new FileInfo(fullPath).Length;
}
entries.Add((assetPath, group.Name, size));
}
}
var top = entries
.OrderByDescending(e => e.size)
.Take(topN)
.Select(e => new { path = e.path, group = e.group, sizeKb = e.size / 1024 });
return new
{
totalEntries = entries.Count,
topAssets = top
};
}
[McpSchema("addressables_bundle_report")]
public static object GetSchema()
{
return new
{
type = "object",
properties = new
{
group = new { type = "string", description = "Filter by group name (omit for all groups)" },
label = new { type = "string", description = "Filter by label (omit for all labels)" },
top_n = new { type = "integer", description = "Return top N assets by size", @default = 10 }
}
};
}
}
The [McpSchema] attribute links the schema provider method to the tool. The three parameters — group, label, and top_n — could be expressed without nesting, but JObject was chosen here to keep the door open as filter conditions grow more complex over time. Note that asset sizes are read from the source files via FileInfo after resolving paths through AssetDatabase, so figures may differ from actual post-build bundle sizes.
Example 3 — NavMesh Agent Batch Inspection (Class-based)
This tool iterates over all NavMeshAgents in the scene and flags any whose speed or radius exceeds configured limits. It caches the previous violation count as an instance field to track changes between calls — which is exactly why Class-based is the right choice here. Class-based tools require a public parameterless constructor.
using Unity.AI.MCP.Editor.ToolRegistry;
using UnityEngine;
using UnityEngine.AI;
using System.Collections.Generic;
using System.Threading.Tasks;
[McpTool("inspect_navmesh_agents", "Inspects all NavMeshAgents in the scene and returns objects that violate configured limits.")]
public class NavMeshInspector : IUnityMcpTool<NavMeshInspectorParams>
{
int _lastViolationCount = -1;
public Task<object> ExecuteAsync(NavMeshInspectorParams parameters)
{
// true: include inactive objects as well
var agents = GameObject.FindObjectsOfType<NavMeshAgent>(true);
var violations = new List<object>();
foreach (var agent in agents)
{
var issues = new List<string>();
if (agent.speed > parameters.MaxSpeed)
issues.Add($"speed {agent.speed:F1} > max {parameters.MaxSpeed:F1}");
if (agent.radius > parameters.MaxRadius)
issues.Add($"radius {agent.radius:F2} > max {parameters.MaxRadius:F2}");
if (issues.Count > 0)
violations.Add(new
{
gameObject = agent.gameObject.name,
issues
});
}
bool changed = _lastViolationCount >= 0 && violations.Count != _lastViolationCount;
_lastViolationCount = violations.Count;
return Task.FromResult<object>(new
{
totalAgents = agents.Length,
violationCount = violations.Count,
changedSinceLastCall = changed,
violations
});
}
}
public class NavMeshInspectorParams
{
[McpDescription("Maximum allowed speed", Default = 6.0)]
public float MaxSpeed { get; set; } = 6.0f;
[McpDescription("Maximum allowed radius", Default = 0.5)]
public float MaxRadius { get; set; } = 0.5f;
}
changedSinceLastCall is the key advantage of Class-based tools. Because the same tool instance lives for the duration of the session, the AI agent can call this tool repeatedly during a refactor and know whether the number of violations has gone up or down since the last check — without any extra context management on the AI side.
Bonus: Runtime API Dynamic Registration
The attribute-based approach registers tools at Unity startup and keeps them fixed. When you need to add or remove tools conditionally at runtime — say, when a plugin or editor extension is activated or deactivated — use the Runtime API instead. Combined with the [InitializeOnLoad] pattern, you can auto-register tools the moment an editor extension loads.
using Unity.AI.MCP.Editor.ToolRegistry;
using UnityEditor;
[InitializeOnLoad]
public static class MyPluginToolRegistrar
{
static readonly MyPluginTool _tool = new MyPluginTool();
static MyPluginToolRegistrar()
{
McpToolRegistry.RegisterTool<MyPluginParams>(
"my_plugin_tool",
_tool,
"A tool available only while the plugin is active");
}
}
// Call this when the plugin is disabled
public static void OnPluginDisabled()
{
McpToolRegistry.UnregisterTool("my_plugin_tool");
}
The generic type parameter of RegisterTool<T>() is your parameter class. UnregisterTool() takes just the tool name string. Both methods fire the ToolsChanged event immediately, notifying any connected clients of the change.
Registry Events (ToolsChanged)
McpToolRegistry.ToolsChanged fires whenever a tool is registered, removed, or refreshed. Use it to handle side effects like logging or updating UI.
McpToolRegistry.ToolsChanged += (args) =>
{
switch (args.ChangeType)
{
case McpToolRegistry.ToolChangeType.Added:
Debug.Log($"Tool registered: {args.ToolName}");
break;
case McpToolRegistry.ToolChangeType.Removed:
Debug.Log($"Tool removed: {args.ToolName}");
break;
case McpToolRegistry.ToolChangeType.Refreshed:
Debug.Log("Full tool list refreshed");
break;
}
};
ChangeType has three values: Added, Removed, and Refreshed. Refreshed fires when the entire tool list is rescanned.
Wrapping Up
A single [McpTool] attribute is all it takes to let Claude Code or Cursor execute project-specific Unity Editor operations directly. Deciding what to build matters more than the code itself. The best candidates are tasks with clear, repeatable criteria — scene quality checks, asset pipeline reports, and gameplay setting audits.
Start with Static typed. When you need more, step up to JObject first, then Class-based — add complexity only as the need arises.
Related Posts
- Unity AI Assistant: Ask, Plan & Agent Modes Explained — Part 1 of this series. Comparing three modes of Unity’s built-in AI Agent.
- Unity MCP Complete Guide: Connecting Claude Code, Codex, Gemini & Cursor — Part 2. Full setup walkthrough for Unity MCP and AI Gateway.
- Automating Unity AI Workflows with the Skills System — Part 4. Automating repetitive tasks with SKILL.md.
Unity AI Complete Mastery Series
Part 1 — Unity AI Assistant Complete Beginner’s Guide: Ask, Plan & Agent Modes
Part 2 — Unity MCP Complete Guide: Connecting Claude Code, Codex, Gemini & Cursor to Unity Editor
Part 3 — Building Custom Unity MCP Tools: McpTool Deep Dive and 4 Registration Methods (this post)
Part 4 — Automating AI Workflows with the Unity Skills System: SKILL.md Complete Guide
Appendix — Unity MCP Built-in Tools Complete Reference: Parameters & Activation Guide for 51 Tools