Unity MCPカスタムツールの作り方:McpTool Deep Diveと4つの登録方式

Unity AI完全マスターシリーズ
第1回 — Unity AI Assistant入門:Ask・Plan・Agentモード完全解説
第2回 — Unity MCP完全ガイド:Claude Code・Codex・Gemini・CursorをUnity Editorに接続する
第3回 — Unity MCPカスタムツールの作り方:McpTool Deep Diveと4つの登録方式 (この記事)
第4回 — Unity SkillsシステムでAIワークフローを自動化する:SKILL.md完全ガイド
Appendix — Unity MCPビルトインツール完全リファレンス:51個のツールパラメータ・有効化ガイド
第5回 — Unity AI AssistantのProject Overview自動生成:AIがプロジェクトを理解するようにする方法
第6回 — Unity AI Assistant × Profiler:性能ボトルネックを自然言語で診断する実践ガイド

この記事はUnity 6を基準に執筆しています。 Unity MCP Serverが提供するビルトインツールだけでは、プロジェクトごとに異なる要件を満たすことはできません。その差を埋めるのが[McpTool]属性です。Unity MCP Server・AI Gatewayのセットアップは第2回の記事を先に完了してからこの記事をお読みください。この記事ではセットアップ手順は扱わず、カスタムツールの作成に集中します。

TL;DR
[McpTool]属性は、Claude Code・Cursorといった外部AIクライアントがUnity Editorを操作するためのカスタムツール登録手段です。
– Unity起動時にMcpToolRegistryがTypeCacheでアセンブリをスキャンし、ツールを自動発見します。
– 登録方式は4種類:Static typed(自動スキーマ)/ JObject(カスタムスキーマ)/ Class-based(状態保持)/ Runtime API(動的登録)
– 外部AIクライアント → relayバイナリ → Unity Editor MCP Bridge → McpToolRegistry → ユーザー定義ツールの順で呼び出されます。
– この記事では、Missing Script検出・Addressablesバンドルレポート・NavMesh Agent点検の実践例3つをすべて実装します。

目次


McpToolとは?

MCP (stdio) IPC (pipe / socket) External AI Client Claude Code / Cursor Codex / Gemini Relay Binary ~/.unity/relay/ --mcp flag Unity Editor MCP Bridge McpToolRegistry Registered Tools Built-in tools [McpTool] custom tools TypeCache auto-discovery

[McpTool]Unity.AI.MCP.Editor.ToolRegistry名前空間に属する属性です。この属性が付いたメソッド(またはクラス)は、Unity Editorの起動時にMcpToolRegistryへ自動的に登録されます。登録はTypeCacheを使ったアセンブリスキャンによって行われます。つまり、手動で登録コードを呼び出さなくても、[McpTool]を付けるだけでツールが自動登録され、MCPを通じて利用できるようになります。

外部AIクライアント(Claude Code、Cursor、Codex、Gemini)はMCPプロトコル(stdio)でrelayバイナリ(~/.unity/relay/)にコマンドを送信します。relayはIPC(Windows: named pipe、macOS/Linux: Unix socket)でUnity Editor内部のMCP Bridgeに接続し、MCP BridgeはMcpToolRegistryからツールを検索して実行します。ツールの戻り値は同じ経路を逆順に辿ってAIクライアントに届きます。

この仕組みのおかげで、カスタムツールを作成すれば、Claude Codeに「シーンにmissing scriptがないか確認して」と自然言語で入力するだけで、Unity Editorが直接シーンを解析して結果を返してくれます。

McpToolを作るタイミング

ビルトインツール(Unity_ReadConsoleget_componentsなど)だけでは対応できない場面が必ず出てきます。私の経験上、カスタムMcpToolが特に威力を発揮するのは次の3つのケースです。

  • プロジェクト固有の検査の自動化:Missing Script検出やアセット命名規則の違反チェックのように、プロジェクトごとに基準が異なる品質検査をツールとしてカプセル化すると、「検査を実行して」の一言で完結します。
  • ビルド・アセットパイプラインのレポート:Addressablesバンドルサイズ分析のように複数のAPIを組み合わせるレポートをツール1つにまとめておくと、AIエージェントがビルド前の自動チェックルーティンに組み込めます。
  • シーン・ゲームプレイ設定の一括点検:NavMesh Agentのspeed・radiusが上限を超えているオブジェクトの探索のように、繰り返し点検が必要な作業をツール化すると、AIがリファクタリング中にも自動で呼び出してリグレッションを防いでくれます。

4つの登録方式

McpTool登録方式の選択 状態/キャッシュの 保持が必要か? Yes Class-based IUnityMcpTool<T> No 動的登録/解除が 必要か? Yes Runtime API RegisterTool / Unregister No ネスト・動的スキーマが 必要か? Yes JObject + [McpSchema] No Static typed ★ 自動スキーマ(推奨)
方式 パラメータ型 スキーマ生成 適した状況
Static typed カスタムパラメータクラス 自動 ほとんどの場合(推奨)
JObject JObject 手動([McpSchema] ネスト・動的構造が必要なとき
Class-based カスタムパラメータクラス 自動 内部キャッシュ・状態保持が必要なとき
Runtime API N/A 手動 プラグイン・エディター拡張での動的登録・解除

Static typedがデフォルトの選択肢です。パラメータクラスのプロパティからJSONスキーマが自動生成されるため、[McpSchema]メソッドを別途記述する必要がありません。ネスト構造や条件付きフィールドが必要な場合はJObjectに切り替え、ツールが以前の呼び出し結果を保持する必要があればClass-basedを使用します。実際の現場ではStatic typedが80%以上を占め、JObjectはフィルターパラメータが2段階以上ネストする場合、Class-basedはAIが「前回の結果と比べて」という文脈を覚えておく必要があるときに選択します。

方式1:Static typed

パラメータ型からJSONスキーマが自動生成されます。最もシンプルで推奨される方式です。

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 }

方式2:JObject

ネスト構造や条件付きプロパティのように、シンプルなクラスでは表現しにくいスキーマが必要なときに使用します。[McpSchema]属性でスキーマ提供メソッドを紐付けます。

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" }
    };
}

方式3:Class-based

IUnityMcpTool<T>を実装するクラスに[McpTool]属性を付けます。インスタンスフィールドを使用できるため、呼び出し間で状態を保持できます。パラメータなしのpublicコンストラクタが必須です。

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; }
}

方式4:Runtime API

McpToolRegistry.RegisterTool<T>()McpToolRegistry.UnregisterTool()を使って、実行時にツールを動的に登録・解除します。パッケージやエディター拡張が有効化・無効化されるタイミングで特に便利です。

using Unity.AI.MCP.Editor.ToolRegistry;

// ツールを登録
var tool = new MyTool();
McpToolRegistry.RegisterTool<MyParams>("dynamic_tool", tool, "A dynamically registered tool");

// 不要になったら解除
McpToolRegistry.UnregisterTool("dynamic_tool");

属性リファレンス

[McpTool(name, description)]

プロパティ 必須 説明
Name string MCPクライアントに公開される一意のツール識別子
Description string 人間が読むツールの説明
Title string 表示タイトル(デフォルト:description)
Groups string[] ツールを分類するカテゴリタグ

ツール名(Name)はMCPプロトコルを通じてAIクライアントにそのまま公開されます。プロジェクト内で一意である必要があり、スネークケース(snake_case)が慣例です。

[McpDescription(description)]

プロパティ 必須 説明
Description string パラメータの人間が読む説明
Required bool 必須パラメータかどうか(デフォルト:false)
EnumType Type 許容値を制限するenum型
Default object スキーマに明示するデフォルト値

Required = trueが指定されたパラメータは、JSONスキーマのrequired配列に自動的に含まれます。AIクライアントが該当パラメータを省略すると、スキーマレベルでエラーが発生します。

[McpSchema(toolName)] / [McpOutputSchema(toolName)]

JObjectパラメータのツールに対して、カスタムの入出力スキーマを提供するメソッドを紐付けます。どちらの属性もtoolNameで対象ツールを指定します。


実践例1 — Missing Script検出(Static typed)

シーン内でmissing MonoBehaviourが付いたGameObjectのパスをすべて検出して返すツールです。ビルド前のルーティンにAIエージェントが自動で呼び出したり、「シーンを整理して」という自然言語リクエストに対してエージェントが最初のステップとして実行するのに適しています。

using Unity.AI.MCP.Editor.ToolRegistry;
using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections.Generic;

public static class SceneDiagnosticTools
{
    [McpTool("find_missing_scripts", "現在のシーンでmissing MonoBehaviourがあるGameObjectのパスをすべて返します。")]
    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はMissing ScriptをnullのComponentとして扱う
        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("非アクティブなGameObjectも含めるかどうか")]
    public bool IncludeInactive { get; set; } = true;
}

SceneManager.GetActiveScene().GetRootGameObjects()でシーンのルートオブジェクトを取得し、再帰的にスキャンします。Unityはmissing MonoBehaviourをnullのComponentとして表現するため、component == nullという条件で検出できます。戻り値はJSONシリアライズ可能な匿名オブジェクトで、ツール名find_missing_scriptsでClaude Codeが直接呼び出します。

実践例2 — Addressablesバンドルサイズレポート(JObject)

Addressablesのグループ別アセットサイズを分析し、上位N件を返すツールです。グループ名とラベルでフィルタリングする複合パラメータが必要なため、JObject方式を選択しています。com.unity.addressablesパッケージがプロジェクトにインストールされている必要があります。

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", "Addressablesのグループ別アセットサイズを分析して上位N件を返します。")]
    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の設定が見つかりません。" };

        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 = "特定のグループ名でフィルタ(省略時は全件)" },
                label = new { type = "string", description = "特定のラベルでフィルタ(省略時は全件)" },
                top_n = new { type = "integer", description = "サイズ上位N件を返す", @default = 10 }
            }
        };
    }
}

[McpSchema]属性でツールとスキーマメソッドを紐付けます。grouplabeltop_nの3パラメータはネストなしでも十分表現できましたが、今後フィルター条件が複雑になることを見越してJObject方式を選択しています。アセットサイズはAssetDatabaseでパスを取得した後、FileInfoで元ファイルのサイズを読み取る方式のため、実際のバンドルビルド後のサイズとは異なる場合があります。

実践例3 — NavMesh Agent一括点検(Class-based)

シーンのすべてのNavMeshAgentを巡回し、speed・radiusが設定上限を超えているオブジェクトを検出します。直前の点検結果をフィールドにキャッシュして変更を追跡できるため、Class-based方式を選択しています。クラスベースのツールには、パラメータなしのpublicコンストラクタが必須です。

using Unity.AI.MCP.Editor.ToolRegistry;
using UnityEngine;
using UnityEngine.AI;
using System.Collections.Generic;
using System.Threading.Tasks;

[McpTool("inspect_navmesh_agents", "シーンのNavMeshAgentを一括点検し、設定違反のオブジェクトを返します。")]
public class NavMeshInspector : IUnityMcpTool<NavMeshInspectorParams>
{
    int _lastViolationCount = -1;

    public Task<object> ExecuteAsync(NavMeshInspectorParams parameters)
    {
        // true: 非アクティブなオブジェクトも含める
        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("許容する最大speed値", Default = 6.0)]
    public float MaxSpeed { get; set; } = 6.0f;

    [McpDescription("許容する最大radius値", Default = 0.5)]
    public float MaxRadius { get; set; } = 0.5f;
}

changedSinceLastCallフィールドがClass-based方式の核心です。同じツールインスタンスがセッション中に保持されるため、AIエージェントが同じツールを繰り返し呼び出しても、前回の結果との変化を伝えることができます。AIがリファクタリングの途中で「前回より違反オブジェクトが増えている」と自ら検知できるようになります。


ボーナス:Runtime API動的登録

属性方式はUnity起動時に固定登録されますが、プラグインやエディター拡張のように実行時に条件付きでツールを追加・削除する必要があるときはRuntime APIを使用します。[InitializeOnLoad]パターンと組み合わせると、エディター拡張のロードタイミングで自動登録が可能になります。

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,
            "プラグインが有効な間だけ使用できるツール");
    }
}

// プラグイン無効化のタイミングで呼び出す
public static void OnPluginDisabled()
{
    McpToolRegistry.UnregisterTool("my_plugin_tool");
}

RegisterTool<T>()のジェネリック型パラメータがパラメータクラスです。UnregisterTool()はツール名の文字列だけを受け取ります。どちらのメソッドも即座にToolsChangedイベントを発生させ、接続中のクライアントに変更を通知します。

レジストリイベント(ToolsChanged)

ツールが登録・解除・更新されるとMcpToolRegistry.ToolsChangedイベントが発生します。ログ出力やUI更新といったサイドエフェクトを処理する際に活用します。

McpToolRegistry.ToolsChanged += (args) =>
{
    switch (args.ChangeType)
    {
        case McpToolRegistry.ToolChangeType.Added:
            Debug.Log($"ツール登録: {args.ToolName}");
            break;
        case McpToolRegistry.ToolChangeType.Removed:
            Debug.Log($"ツール解除: {args.ToolName}");
            break;
        case McpToolRegistry.ToolChangeType.Refreshed:
            Debug.Log("全ツールリストを更新");
            break;
    }
};

ChangeTypeAddedRemovedRefreshedの3種類です。Refreshedはツールリスト全体が再スキャンされるときに発生します。


まとめ

[McpTool]属性1つで、Claude CodeやCursorがプロジェクト専用のUnity Editorの操作を直接実行できるようになります。どのツールを作るかを決めることが、コードを書くこと以上に重要です。シーンの品質検査、アセットパイプラインのレポート、ゲームプレイ設定の点検のように、繰り返しの基準が明確な作業が最初の候補です。

登録方式はStatic typedから始め、必要に応じてJObject → Class-basedの順に複雑さを上げていくのがよいでしょう。


関連記事


Unity AI完全マスターシリーズ
第1回 — Unity AI Assistant入門:Ask・Plan・Agentモード完全解説
第2回 — Unity MCP完全ガイド:Claude Code・Codex・Gemini・CursorをUnity Editorに接続する
第3回 — Unity MCPカスタムツールの作り方:McpTool Deep Diveと4つの登録方式 (この記事)
第4回 — Unity SkillsシステムでAIワークフローを自動化する:SKILL.md完全ガイド
Appendix — Unity MCPビルトインツール完全リファレンス:51個のツールパラメータ・有効化ガイド

コメントする