Claude Code(MCP)を使ってUnityを操作していると、update_component ツールでは GameObject参照やComponent参照をフィールドにアサインできない という制限に気づきます。
この記事では「なぜできないのか」を内部コードから説明し、その制限を回避する汎用Editorスクリプトを紹介します。
1. なぜMCP経由ではComponent操作ができないのか
環境
- Unity 6 (6000.3.x)
- Universal Render Pipeline (URP) 17.3.0
- mcp-unity 1.2.0
- MCPクライアント: Claude Code (claude-sonnet-4-6)
update_component の内部実装
mcp-unityの update_component ツールは、Unityエディタ側の UpdateComponentTool.cs で処理されます。フィールドに値をセットする際、JSON(JToken)から目的の型に変換するヘルパーメソッド ConvertJTokenToValue が呼ばれます。
// UpdateComponentTool.cs(mcp-unityパッケージ内)の抜粋
private static object ConvertJTokenToValue(JToken token, Type targetType)
{
// ...(プリミティブ型、Vector系、Color系などの変換)...
// UnityEngine.Object のハンドリング
if (targetType == typeof(UnityEngine.Object)) // ← ここが問題
{
return token.ToObject<UnityEngine.Object>();
}
return token.ToObject(targetType);
}
問題は targetType == typeof(UnityEngine.Object) という比較です。
== による型の比較は 完全一致 しか通りません。Camera や Canvas は UnityEngine.Object のサブクラスですが、この条件には一致しません。
結果として処理は token.ToObject(targetType)(最終フォールバック)に流れ、JSON文字列 "Main Camera" を Camera 型に変換しようとして失敗し、silentlyにnullが返ります。
IsAssignableFrom を使えば解決できるが…
本来であれば次のように書けば Camera や Canvas などのサブクラスも捕捉できます。
if (typeof(UnityEngine.Object).IsAssignableFrom(targetType))
{
int instanceId = token.Value<int>();
return EditorUtility.InstanceIDToObject(instanceId);
}
しかしこれはパッケージ内のコードです。直接編集すると将来のアップデートで上書きされますし、そもそも 生成ファイルを編集すべきではありません。
補足:GraphicRaycaster.eventCamera の罠
Canvas.worldCamera が null のとき、GraphicRaycaster.eventCamera は Camera.main をフォールバックとして返します。
そのため get_gameobject で確認すると eventCamera = "Main Camera" と表示され、一見アサインが成功したように見えますが、worldCamera フィールド自体は null のままです。これはデバッグ時に混乱を招くので注意が必要です。
2. MCP経由でComponent操作をできるようにする拡張スクリプト
解決策:JSONパラメータファイル × [MenuItem]
MCPが苦手な操作を Unityエディタ側のEditorスクリプト に委譲する方法を使います。
① MCPがパラメータをJSON形式でファイルに書き込む
② MCPが [MenuItem] を execute_menu_item で呼び出す
③ EditorスクリプトがJSONを読み込み、リフレクションでフィールドをセット
MCPはファイルの読み書きと任意のメニュー項目の実行が得意です。Unityのエディタ処理(オブジェクト検索・参照解決)はC#側に任せることで、両者の得意領域を活かせます。
実装
Assets/Scripts/Editor/McpGenericComponentTool.cs として作成します。
#nullable enable
using System;
using System.IO;
using System.Reflection;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace MyProject.Editor
{
// ============================================================
// パラメータ定義(JsonUtility で読み込む)
// ============================================================
[Serializable]
public class McpComponentRefOperation
{
[Header("--- ターゲット(変更先)---")]
/// <summary>ターゲット GameObject のシーン階層パス(例: "Canvases/Canvas_Main")</summary>
public string targetPath = "";
/// <summary>ターゲット GameObject の instanceId(0 の場合は targetPath を使用)</summary>
public int targetInstanceId = 0;
/// <summary>変更するコンポーネント名(例: "Canvas")</summary>
public string componentName = "";
/// <summary>変更するフィールド/プロパティ名(例: "worldCamera")</summary>
public string fieldName = "";
[Header("--- ソース(設定値)--- いずれかひとつを指定 ---")]
/// <summary>
/// [優先度1] ソース GameObject のシーン階層パス(例: "Main Camera")
/// sourceComponentName と組み合わせて Component を取得する。
/// sourceComponentName が空の場合は GameObject 自体を値として使う。
/// </summary>
public string sourceGameObjectPath = "";
/// <summary>
/// [優先度1-b] sourceGameObjectPath で取得した GameObject 上のコンポーネント名
/// (例: "Camera")。空の場合は GameObject を直接セット。
/// </summary>
public string sourceComponentName = "";
/// <summary>[優先度2] Unity オブジェクトの instanceId を直接指定</summary>
public int sourceInstanceId = 0;
/// <summary>[優先度3] アセットパス(例: "Assets/Materials/MyMat.mat")</summary>
public string sourceAssetPath = "";
}
[Serializable]
public class McpComponentRefParams
{
public McpComponentRefOperation[] operations = Array.Empty<McpComponentRefOperation>();
}
// ============================================================
// メインツール
// ============================================================
public static class McpGenericComponentTool
{
private static string ParamsFilePath =>
Path.GetFullPath(Path.Combine(UnityEngine.Application.dataPath, "..", "Temp", "McpComponentRef.json"));
[MenuItem("Tools/MCP/Apply Component References")]
public static void ApplyComponentReferences()
{
if (!File.Exists(ParamsFilePath))
{
Debug.LogError(
$"[McpGenericComponentTool] パラメータファイルが見つかりません。\n" +
$"次のパスに JSON を配置してから再実行してください:\n{ParamsFilePath}");
return;
}
string json = File.ReadAllText(ParamsFilePath);
McpComponentRefParams? param = JsonUtility.FromJson<McpComponentRefParams>(json);
if (param?.operations == null || param.operations.Length == 0)
{
Debug.LogWarning("[McpGenericComponentTool] 処理する操作が 0 件でした。JSON を確認してください。");
return;
}
int successCount = 0;
int failCount = 0;
foreach (var op in param.operations)
{
if (ApplyOperation(op)) successCount++;
else failCount++;
}
EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());
Debug.Log($"[McpGenericComponentTool] 完了 ── 成功: {successCount}件 / 失敗: {failCount}件");
}
[MenuItem("Tools/MCP/Show Params File Path")]
public static void ShowParamsFilePath()
{
Debug.Log($"[McpGenericComponentTool] パラメータファイルのパス:\n{ParamsFilePath}");
}
// --------------------------------------------------------
private static bool ApplyOperation(McpComponentRefOperation op)
{
GameObject? targetGO = FindGameObject(op.targetPath, op.targetInstanceId);
if (targetGO == null)
{
Debug.LogError(
$"[McpGenericComponentTool] ターゲット GameObject が見つかりません。" +
$" path='{op.targetPath}', instanceId={op.targetInstanceId}");
return false;
}
Component? targetComponent = targetGO.GetComponent(op.componentName);
if (targetComponent == null)
{
Debug.LogError(
$"[McpGenericComponentTool] Component '{op.componentName}' が" +
$" '{targetGO.name}' に存在しません。");
return false;
}
UnityEngine.Object? sourceValue = ResolveSource(op);
return SetFieldOrProperty(targetComponent, op.fieldName, sourceValue);
}
private static GameObject? FindGameObject(string path, int instanceId)
{
if (instanceId != 0)
return EditorUtility.InstanceIDToObject(instanceId) as GameObject;
if (string.IsNullOrEmpty(path)) return null;
string[] parts = path.Split('/');
foreach (GameObject root in SceneManager.GetActiveScene().GetRootGameObjects())
{
if (root.name != parts[0]) continue;
GameObject current = root;
bool found = true;
for (int i = 1; i < parts.Length; i++)
{
Transform? child = current.transform.Find(parts[i]);
if (child == null) { found = false; break; }
current = child.gameObject;
}
if (found) return current;
}
return null;
}
private static UnityEngine.Object? ResolveSource(McpComponentRefOperation op)
{
// 優先度1: GameObjectパス + Component名
if (!string.IsNullOrEmpty(op.sourceGameObjectPath))
{
GameObject? sourceGO = FindGameObject(op.sourceGameObjectPath, 0);
if (sourceGO == null)
{
Debug.LogError($"[McpGenericComponentTool] ソース GameObject が見つかりません: '{op.sourceGameObjectPath}'");
return null;
}
if (string.IsNullOrEmpty(op.sourceComponentName)) return sourceGO;
Component? comp = sourceGO.GetComponent(op.sourceComponentName);
if (comp == null)
Debug.LogError($"[McpGenericComponentTool] ソース Component '{op.sourceComponentName}' が '{sourceGO.name}' に存在しません。");
return comp;
}
// 優先度2: instanceId
if (op.sourceInstanceId != 0)
return EditorUtility.InstanceIDToObject(op.sourceInstanceId);
// 優先度3: アセットパス
if (!string.IsNullOrEmpty(op.sourceAssetPath))
{
var asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(op.sourceAssetPath);
if (asset == null)
Debug.LogError($"[McpGenericComponentTool] アセットが見つかりません: '{op.sourceAssetPath}'");
return asset;
}
return null; // null クリア用
}
private static bool SetFieldOrProperty(Component component, string fieldName, UnityEngine.Object? value)
{
Undo.RecordObject(component, $"MCP Set {component.GetType().Name}.{fieldName}");
Type type = component.GetType();
const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
FieldInfo? field = type.GetField(fieldName, flags);
if (field != null)
{
field.SetValue(component, value);
EditorUtility.SetDirty(component);
Debug.Log($"[McpGenericComponentTool] ✓ {component.gameObject.name}.{type.Name}.{fieldName} = {(value != null ? value.name : "null")}");
return true;
}
PropertyInfo? prop = type.GetProperty(fieldName, flags);
if (prop != null && prop.CanWrite)
{
prop.SetValue(component, value);
EditorUtility.SetDirty(component);
Debug.Log($"[McpGenericComponentTool] ✓ {component.gameObject.name}.{type.Name}.{fieldName} = {(value != null ? value.name : "null")}");
return true;
}
Debug.LogError($"[McpGenericComponentTool] フィールド / プロパティ '{fieldName}' が '{type.Name}' に見つかりません。");
return false;
}
}
}
使い方
パラメータJSONの書式
Temp/McpComponentRef.json(プロジェクトルートの Temp/ フォルダ。Gitignore対象)に以下の形式で記述します。
{
"operations": [
{
"targetPath": "UI/Canvas",
"componentName": "Canvas",
"fieldName": "worldCamera",
"sourceGameObjectPath": "Main Camera",
"sourceComponentName": "Camera"
}
]
}
| フィールド | 説明 |
|---|---|
targetPath |
ターゲットGameObjectのシーン階層パス(/区切り) |
targetInstanceId |
instanceIdで指定する場合(0のときはtargetPathを使用) |
componentName |
変更するComponent名(例: "Canvas") |
fieldName |
変更するフィールド/プロパティ名(例: "worldCamera") |
sourceGameObjectPath |
ソースGameObjectのシーン階層パス |
sourceComponentName |
ソースのComponent名(空の場合はGameObject自体をセット) |
sourceInstanceId |
ソースをinstanceIdで指定する場合 |
sourceAssetPath |
Assetをパスで指定する場合(例: "Assets/Materials/MyMat.mat") |
ソースは sourceGameObjectPath → sourceInstanceId → sourceAssetPath の優先順位で解決されます。すべて未指定の場合は null がセットされます(フィールドのクリアに使用)。
MCPからの呼び出し手順
MCPクライアント(Claude Codeなど)から操作する場合は次の2ステップです。
Step 1: Writeツールで Temp/McpComponentRef.json を書き込む
Step 2: execute_menu_item で Tools/MCP/Apply Component References を呼び出す
execute_menu_item("Tools/MCP/Apply Component References")
実行結果の例
[McpGenericComponentTool] ✓ Canvas.Canvas.worldCamera = Main Camera
[McpGenericComponentTool] 完了 ── 成功: 1件 / 失敗: 0件
ポイントまとめ
-
なぜ
Temp/フォルダか?Temp/はUnityがビルド等に使う作業フォルダでGit管理外。MCPが一時ファイルを置く場所として最適。 -
なぜリフレクションか?
Canvas.worldCameraのような型がUnityEngine.Objectのサブクラスであるフィールドは、updatecomponentが使うJToken.ToObject<T>()で復元できない。リフレクションで実インスタンスを直接セットすることで回避できる。 -
複数操作を一括実行
operations配列に複数の操作を並べれば、1回のメニュー実行で全部まとめて適用できる。 -
Undo対応
Undo.RecordObjectを呼んでいるため、Ctrl+Z で元に戻せる。
まとめ
| 方法 | できること | できないこと |
|---|---|---|
update_component(標準) |
プリミティブ型、Vector、Colorの設定 | GameObject/Component参照のアサイン |
| 本記事の拡張スクリプト | リフレクション経由で任意のフィールド・プロパティをセット | ランタイムコードの実行(Editorのみ) |
MCPはファイルI/OとメニューItemの実行が得意、UnityはオブジェクトグラフのトラバースとAPIアクセスが得意という棲み分けで、お互いの苦手を補い合う構成になっています。
ぜひ活用してみてください。