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이란?
- 언제 McpTool을 만들까
- 4가지 등록 방식
- 어트리뷰트 레퍼런스
- 실무 예제 1 — Missing Script 검출 (Static typed)
- 실무 예제 2 — Addressables 번들 크기 리포트 (JObject)
- 실무 예제 3 — NavMesh Agent 일괄 점검 (Class-based)
- 보너스: Runtime API 동적 등록
- 레지스트리 이벤트 (ToolsChanged)
- 마무리
McpTool이란?
[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_ReadConsole, get_components 등)만으로 부족한 상황이 반드시 찾아옵니다. 제 경험상 커스텀 McpTool이 빛나는 시점은 아래 세 가지입니다.
- 프로젝트 고유 검사 자동화: Missing Script 검출, 에셋 네이밍 규칙 위반 탐지처럼 프로젝트마다 기준이 다른 품질 검사를 도구로 캡슐화하면 “검사 실행해줘”라는 한 마디로 해결됩니다.
- 빌드·에셋 파이프라인 리포팅: Addressables 번들 크기 분석처럼 여러 API를 조합해야 하는 리포트를 도구 하나로 묶어두면, AI 에이전트가 빌드 전 자동 점검 루틴에 끼워넣을 수 있습니다.
- 씬·게임플레이 설정 일괄 점검: NavMesh Agent speed·radius 초과 오브젝트 탐색처럼 반복 점검이 필요한 작업을 도구로 만들면 AI가 리팩토링 중에도 자동으로 호출해 회귀를 막아줍니다.
4가지 등록 방식
| 방식 | 파라미터 타입 | 스키마 생성 | 적합한 상황 |
|---|---|---|---|
| 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] 어트리뷰트로 도구와 스키마 메서드를 연결합니다. group, label, top_n 세 파라미터를 중첩 없이도 충분히 표현할 수 있었지만, 향후 필터 조건이 복잡해질 것을 고려해 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;
}
};
ChangeType은 Added, Removed, Refreshed 세 가지입니다. Refreshed는 도구 목록 전체가 다시 스캔될 때 발생합니다.
마무리
[McpTool] 어트리뷰트 하나로 Claude Code나 Cursor가 프로젝트 맞춤형 Unity 에디터 작업을 직접 실행할 수 있게 됩니다. 어떤 도구를 만들지 결정하는 것이 코드 작성보다 중요합니다. 씬 품질 검사, 에셋 파이프라인 리포트, 게임플레이 설정 점검처럼 반복 기준이 명확한 작업이 첫 번째 후보입니다.
등록 방식은 Static typed부터 시작하고, 필요가 생길 때 JObject→Class-based 순서로 복잡도를 높이면 됩니다.
관련 포스팅
- Unity AI Assistant 완전 입문: Ask·Plan·Agent 모드 — 이 글의 1편. Unity 내장 AI Agent의 세 가지 모드 비교
- Unity MCP 완전 가이드: Claude Code·Codex·Gemini·Cursor를 Unity Editor와 연결하기 — 이 글의 2편. Unity MCP·AI Gateway 셋업 전 과정
- Unity Skills 시스템으로 AI 워크플로우 자동화하기 — 이 글의 4편. SKILL.md 기반 반복 작업 자동화
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개 도구 파라미터·활성화 가이드
4편 — Unity Skills 시스템으로 AI 워크플로우 자동화하기