9.2 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What This Project Is
MCP for Unity is a bridge that lets AI assistants (Claude, Cursor, Windsurf, etc.) control the Unity Editor through the Model Context Protocol (MCP). It enables AI-driven game development workflows - creating GameObjects, editing scripts, managing assets, running tests, and more.
Architecture
AI Assistant (Claude/Cursor)
↓ MCP Protocol (stdio/HTTP)
Python Server (Server/src/)
↓ WebSocket + HTTP
Unity Editor Plugin (MCPForUnity/)
↓ Unity Editor API
Scene, Assets, Scripts
Two codebases, one system:
Server/- Python MCP server using FastMCPMCPForUnity/- Unity C# Editor package
Three Layers on the Python Side
The Python server has three distinct layers. These are not auto-generated from each other:
| Layer | Location | Framework | Purpose |
|---|---|---|---|
| MCP Tools | Server/src/services/tools/ |
FastMCP (@mcp_for_unity_tool) |
Exposed to AI assistants via MCP protocol |
| CLI Commands | Server/src/cli/commands/ |
Click (@click.command) |
Terminal interface for developers |
| Resources | Server/src/services/resources/ |
FastMCP (@mcp_for_unity_resource) |
Read-only state exposed to AI assistants |
MCP tools call Unity via WebSocket (send_with_unity_instance). CLI commands call Unity via HTTP (run_command). Both route to the same C# HandleCommand methods.
Transport Modes
- Stdio: Single-agent only. Separate Python process per client. Legacy TCP bridge to Unity. New connections stomp old ones.
- HTTP: Multi-agent ready. Single shared Python server. WebSocket hub at
/hub/plugin. Session isolation viaclient_id.
Code Philosophy
1. Domain Symmetry
Python MCP tools mirror C# Editor tools. Each domain exists in both:
Server/src/services/tools/manage_material.py↔MCPForUnity/Editor/Tools/ManageMaterial.cs- CLI commands (
Server/src/cli/commands/) also mirror these but are a separate implementation.
2. Minimal Abstraction
Avoid premature abstraction. Three similar lines of code is better than a helper that's used once. Only abstract when you have 3+ genuine use cases.
3. Delete Rather Than Deprecate
When removing functionality, delete it completely. No _unused renames, no // removed comments, no backwards-compatibility shims for internal code.
4. Test Coverage Required
Every new feature needs tests. Run them before PRs.
5. Keep Tools Focused
Each MCP tool does one thing well. Resist the urge to add "convenient" parameters that bloat the API surface.
6. Use Resources for Reading
Keep them smart and focused rather than "read everything" type resources. Resources should be quick and LLM-friendly.
Key Patterns
Python MCP Tool Registration
Tools in Server/src/services/tools/ are auto-discovered. Use the @mcp_for_unity_tool decorator:
from services.registry import mcp_for_unity_tool
@mcp_for_unity_tool(
description="Does something in Unity.",
group="core", # core (default), vfx, animation, ui, scripting_ext, testing, probuilder, profiling, docs
)
async def manage_something(
ctx: Context,
action: Annotated[Literal["create", "delete"], "Action to perform"],
) -> dict[str, Any]:
unity_instance = await get_unity_instance_from_context(ctx)
params = {"action": action}
response = await send_with_unity_instance(async_send_command_with_retry, unity_instance, "manage_something", params)
return response
The group parameter controls tool visibility. Only "core" is enabled by default. Non-core groups (vfx, animation, etc.) start disabled and are toggled via manage_tools.
Python CLI Error Handling
CLI commands (not MCP tools) use the @handle_unity_errors decorator:
@handle_unity_errors
async def my_command(ctx, ...):
result = await call_unity_tool(...)
C# Tool Registration
Tools are auto-discovered by CommandRegistry via reflection. Use the [McpForUnityTool] attribute:
[McpForUnityTool("manage_something", AutoRegister = false, Group = "core")]
public static class ManageSomething
{
// Sync handler (most tools):
public static object HandleCommand(JObject @params)
{
var p = new ToolParams(@params);
// ...
return new SuccessResponse("Done.", new { data = result });
}
// OR async handler (for long-running operations like play-test, refresh, batch):
public static async Task<object> HandleCommand(JObject @params)
{
// CommandRegistry detects Task return type automatically
await SomeAsyncOperation();
return new SuccessResponse("Done.");
}
}
Async handlers use EditorApplication.update polling with TaskCompletionSource — see RefreshUnity.cs for the canonical pattern.
C# Parameter Handling
Use ToolParams for consistent parameter validation:
var p = new ToolParams(parameters);
var pageSize = p.GetInt("page_size", "pageSize") ?? 50;
var name = p.RequireString("name");
C# Resources
Resources use [McpForUnityResource] and follow the same HandleCommand pattern as tools. They provide read-only state to AI assistants.
Paging Large Results
Always page results that could be large (hierarchies, components, search results):
- Use
page_sizeandcursorparameters - Return
next_cursorwhen more results exist
Composing Tools Internally (C#)
Use CommandRegistry.InvokeCommandAsync to call other tools from within a handler:
var result = await CommandRegistry.InvokeCommandAsync("read_console", consoleParams);
Unity API Compatibility Shims
We support a wide Unity version range (2021+ → 6.x → CoreCLR 6.8). When an API is renamed, deprecated, or removed across versions, don't sprinkle #if UNITY_x_y_OR_NEWER at every call site — add a shim in MCPForUnity/Runtime/Helpers/Unity*Compat.cs and route every caller through it.
The catalog of active shims, the policy for when to add one, what does NOT belong in a shim, and the reflection-cache pattern all live in MCPForUnity/Runtime/Helpers/UnityCompatShims.cs — the XML doc on that empty marker class is the source of truth and ships inside the UPM package, so end-users can F12/Go-to-definition into it. Sources for current deprecations: Unity 6.x upgrade guides and the CoreCLR 2026 thread.
When you touch a shim or anything else gated by #if UNITY_*_OR_NEWER, run tools/check-unity-versions.sh to compile-check across the CI matrix locally before committing — the matrix lives in tools/unity-versions.json.
Commands
Running Tests
# Python (all tests)
cd Server && uv run pytest tests/ -v
# Python (single test file)
cd Server && uv run pytest tests/test_manage_material.py -v
# Python (single test by name)
cd Server && uv run pytest tests/ -k "test_create_material" -v
# Unity - open TestProjects/UnityMCPTests in Unity, use Test Runner window
# Local multi-version compile check (parity with CI matrix, see tools/unity-versions.json)
tools/check-unity-versions.sh # compile-only across installed Unity Hub editors
tools/check-unity-versions.sh --full # full EditMode test run
Local headless test harness
One command boots a headless Hub-licensed Editor against TestProjects/UnityMCPTests and runs the smoke + EditMode + PlayMode legs over the bridge — the same entrypoint CI uses (.github/workflows/e2e-bridge.yml):
python tools/local_harness.py
Key flags: --legs smoke,editmode,playmode (subset to run), --project-path (target project, default TestProjects/UnityMCPTests), --reuse (attach to an already-resident bridge instead of booting one), --keep-alive (leave the Editor running after the legs), --no-warmup (skip the warm-up import phase).
Exit codes: 0 pass, 1 blocking-leg regression, 2 bridge unreachable / setup failure, 3 project does not compile, 4 no Unity license / Hub seat, 5 Editor binary/version not found. Requires a Hub-activated Editor locally (no ULF/serial).
Local Development
- Set Server Source Override in MCP for Unity Advanced Settings to your local
Server/path - Enable Dev Mode checkbox to force fresh installs
- Use
mcp_source.pyto switch Unity package sources - Test on Windows and Mac if possible, and multiple clients (Claude Desktop and Claude Code are tricky for configuration as of this writing)
Adding a New Tool
- Add Python MCP tool in
Server/src/services/tools/manage_<domain>.pyusing@mcp_for_unity_tool - Add Python CLI commands in
Server/src/cli/commands/<domain>.pyusing Click - Add C# implementation in
MCPForUnity/Editor/Tools/Manage<Domain>.cswith[McpForUnityTool] - Add Python tests in
Server/tests/test_manage_<domain>.py - Add Unity tests in
TestProjects/UnityMCPTests/Assets/Tests/
What Not To Do
- Don't add features without tests
- Don't create helper functions for one-time operations
- Don't add error handling for scenarios that can't happen
- Don't commit to
maindirectly - branch offbetafor PRs - Don't add docstrings/comments to code you didn't change