# MCP for Unity - Developer Guide

| English | [简体中文](https://github.com/CoplayDev/unity-mcp/blob/beta/docs/development/README-DEV-zh.md) |
|---------------------------|------------------------------|

## Contributing

**Branch off `beta`** to create PRs. The `main` branch is reserved for stable releases.

Before proposing major new features, please reach out to discuss - someone may already be working on it or it may have been considered previously. Open an issue or discussion to coordinate.

Keep PRs focused. A small fix with a clear repro and the exact Unity/package versions is much easier to review than a broad cleanup bundled with behavior changes.

For bug fixes, include:

- Unity version(s) tested
- package source used (`#beta`, `#main`, tag, fork branch, or `file:`)
- resolved Git commit from `Packages/packages-lock.json` when the package source is a Git URL
- commands/tests run locally

Avoid mixing release/version bumps with feature or bug-fix PRs. The release workflows handle package and server version updates.

## Repository Map

- `MCPForUnity/` - the Unity package: Editor UI, C# tool handlers, resources, compatibility shims, package metadata.
- `Server/` - the Python MCP server, CLI, FastMCP tool/resource registry, transports, and server-side tests.
- `TestProjects/UnityMCPTests/` - Unity project used for EditMode/PlayMode tests. Its manifest points at `file:../../../MCPForUnity`.
- `docs/` - user, contributor, release, migration, and reference documentation.
- `tools/` - release/build helpers, documentation update prompt, stress tools, and publishing scripts.
- `CustomTools/` - examples and support code for project-defined custom tools.

## Local Development Setup

### 0. Prepare the Python Server

For server work, use the same setup path as CI:

```bash
cd Server
uv sync
uv pip install -e ".[dev]"
uv run pytest tests/ -v --tb=short
```

Most Python unit tests do not need Unity running. Integration tests that talk to the Editor require a Unity instance with the bridge connected.

### 1. Point Unity to Your Local Server

For the fastest iteration when working on the Python server:

1. Open Unity and go to **Window > MCP for Unity**
2. Open **Settings > Advanced Settings**
3. Set **Server Source Override** to your local `Server/` directory path
4. Enable **Dev Mode (Force fresh server install)** - this adds `--refresh` to uvx commands so your changes are picked up on every server start

### 2. Switch Package Sources

You may want to use the `mcp_source.py` script to quickly switch your Unity project between different MCP package sources [allows you to quickly point your personal project to your local or remote unity-mcp repo, or the live upstream (Coplay) versions of the unity-mcp package]:

```bash
python mcp_source.py
```

Options:
1. **Upstream main** - stable release (CoplayDev/unity-mcp)
2. **Upstream beta** - development branch (CoplayDev/unity-mcp#beta)
3. **Remote branch** - your fork's current branch
4. **Local workspace** - file: URL to your local MCPForUnity folder

After switching, open Package Manager in Unity and Refresh to re-resolve packages.

When testing Unity package changes, prefer **Local workspace**. Do not patch `Library/PackageCache/` directly; Unity may overwrite it on the next resolve.

`#beta` and `#main` are moving branch names. When debugging a package-source issue, check `Packages/packages-lock.json` as well as `Packages/manifest.json`; the lock file tells you the commit Unity actually resolved.

Example Git package lock entry:

```json
"com.coplaydev.unity-mcp": {
  "version": "https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#beta",
  "source": "git",
  "hash": "<resolved-git-commit>"
}
```

## Adding or Changing Tools and Resources

Built-in Unity tools usually have two sides:

- a C# handler under `MCPForUnity/Editor/Tools/`
- a Python-facing tool under `Server/src/services/tools/`

Resources follow the same split:

- C# resources under `MCPForUnity/Editor/Resources/`
- Python resources under `Server/src/services/resources/`

Use `[McpForUnityTool]` / `[McpForUnityResource]` on the Unity side and `@mcp_for_unity_tool` / `@mcp_for_unity_resource` on the server side. If the tool is long-running, use the existing `PendingResponse` / polling patterns instead of blocking the bridge.

Current server tool groups are:

- `core`
- `docs`
- `vfx`
- `animation`
- `ui`
- `scripting_ext`
- `testing`
- `probuilder`
- `profiling`

Tools with `group=None` are server meta-tools and are always visible. The default enabled group is `core`; other groups can be enabled through the Tools tab or `manage_tools`.

When adding, removing, or renaming tools/resources, update the public docs and client metadata. Start with `tools/UPDATE_DOCS_PROMPT.md`, then review the changes manually.

## Tool Selection & the Meta-Tool

MCP for Unity organizes tools into **groups**. You can selectively enable or disable tools to control which capabilities are exposed to AI clients — reducing context window usage and focusing the AI on relevant tools.

### Using the Tools Tab in the Editor

Open **Window > MCP for Unity** and switch to the **Tools** tab. Each tool group is displayed as a collapsible foldout with:

- **Per-tool toggles** — click individual tool toggles to enable or disable them.
- **Group checkbox** — a checkbox embedded directly in each group's foldout header (next to the group title) enables or disables all tools in that group at once without expanding or collapsing the foldout.
- **Enable All / Disable All** — global buttons to toggle all tools.
- **Rescan** — re-discovers tools from assemblies (useful after adding new `[McpForUnityTool]` classes).
- **Reconfigure Clients** — re-registers tools with the server and reconfigures all detected MCP clients in one click, applying your changes without navigating back to the Clients tab.

### How Changes Propagate

Tool visibility changes work differently depending on the transport mode:

**HTTP mode** (recommended):

1. Toggling a tool calls `ReregisterToolsAsync()`, which sends the updated enabled tool list to the Python server over WebSocket.
2. The server updates its internal tool visibility via `mcp.enable()`/`mcp.disable()` per group.
3. The server sends a `tools/list_changed` MCP notification to all connected client sessions.
4. Already-connected clients (Claude Desktop, VS Code, etc.) automatically receive the updated tool list.

**Stdio mode**:

1. Toggles are persisted locally but cannot be pushed to the server (no WebSocket connection).
2. The server starts with all groups enabled. After changing toggles, ask the AI to run `manage_tools` with `action='sync'` — this pulls the current tool states from Unity and syncs server visibility.
3. Alternatively, restart the server to pick up changes.

### The `manage_tools` Meta-Tool

The server exposes a built-in `manage_tools` tool (always visible, not group-gated) that AIs can call directly:

| Action | Description |
|--------|-------------|
| `list_groups` | Lists all tool groups with their tools and enable/disable status |
| `activate` | Enables a tool group by name (e.g., `group="vfx"`) |
| `deactivate` | Disables a tool group by name |
| `sync` | Pulls current tool states from Unity and syncs server visibility (essential for stdio mode) |
| `reset` | Restores default tool visibility |

### When You Need to Reconfigure

After toggling tools on/off, MCP clients need to learn about the changes:

- **HTTP mode**: Changes propagate automatically via `tools/list_changed`. Most clients pick this up immediately. If a client doesn't, click **Reconfigure Clients** on the Tools tab, or go to Clients tab and click Configure.
- **Stdio mode**: The server process needs to be told about changes. Either ask the AI to call `manage_tools(action='sync')`, or restart the MCP session. Click **Reconfigure Clients** to re-register all clients with updated config.

## Running Tests

All major new features (and some minor ones) must include test coverage. It's so easy to get LLMs to write tests, ya gotta do it!

### Python Tests 

Located in `Server/tests/`:

```bash
cd Server
uv run pytest tests/ -v
```

Useful narrower runs:

```bash
uv run pytest tests/test_manage_camera.py -v
uv run pytest tests/integration/test_run_tests_async.py -v
uv run pytest tests/ -v --tb=short
```

### Unity C# Tests

Located in `TestProjects/UnityMCPTests/Assets/Tests/`.

The test project consumes the local Unity package from this repo. Use it for package import/compile checks before opening a PR that touches `MCPForUnity/Runtime/`, `MCPForUnity/Editor/`, or package metadata.

**Using the CLI** (requires Unity running with MCP bridge connected):

```bash
cd Server

# Run EditMode tests (default)
uv run python -m cli.main editor tests

# Run PlayMode tests
uv run python -m cli.main editor tests --mode PlayMode

# Run async and poll for results (useful for long test runs)
uv run python -m cli.main editor tests --async
uv run python -m cli.main editor poll-test <job_id> --wait 60

# Show only failed tests
uv run python -m cli.main editor tests --failed-only
```

**Using MCP tools directly** (from any MCP client):

```
run_tests(mode="EditMode")
run_tests(mode="PlayMode", init_timeout=120000)  # PlayMode may need longer init due to domain reload
get_test_job(job_id="<id>", wait_timeout=60)
```

### Local headless test harness

`python tools/local_harness.py` boots a headless Editor and runs the smoke + EditMode + PlayMode legs over the bridge — the same entrypoint CI uses. See [Testing → Local headless test harness](./testing.md#local-headless-test-harness) for the flags and exit-code contract.

### Code Coverage

```bash
cd Server
uv run pytest tests/ --cov --cov-report=html
open htmlcov/index.html
```

## Compatibility Notes

The Unity package currently declares Unity `2021.3` as its minimum version in `MCPForUnity/package.json`. Code that uses Unity API conditionals (`UNITY_2022_3_OR_NEWER`, `UNITY_2023_1_OR_NEWER`, `UNITY_6000_*`) should be tested against the affected editor line, not just the oldest supported version.

For compatibility PRs, note the exact editor versions you tested in the PR body. If a Git package URL is involved, include the resolved `packages-lock.json` hash.

## Troubleshooting During Development

- **Unity still loads an old Git package**: close Unity, check `Packages/packages-lock.json`, then refresh Package Manager. If needed, remove only the stale `Library/PackageCache/com.coplaydev.unity-mcp@<hash>` folder while Unity is closed.
- **Unity opens in Safe Mode after changing package source**: the package failed to compile before MCP can start. Fix the compile errors first; the MCP server cannot recover from package compile failures.
- **Server changes are not picked up**: make sure **Server Source Override** points to your local `Server/` directory and **Dev Mode (Force fresh server install)** is enabled.
- **Stdio tool visibility looks stale**: call `manage_tools(action="sync")` or restart the MCP session. HTTP mode can push `tools/list_changed` notifications automatically.
- **Multiple Unity editors are open**: use `mcpforunity://instances` and `set_active_instance` to confirm which project the server is targeting.
## Unity-version CI matrix

CI exercises the package across multiple Unity versions to catch breaks in `#if UNITY_*_OR_NEWER` branches. The matrix is configured in `tools/unity-versions.json` and consumed by `.github/workflows/unity-tests.yml`.

**Every PR gets a unity-tests status check on open** (mirrors `python-tests.yml`). For same-repo PRs the default Unity 6 leg actually runs; for fork PRs the workflow appears but skips with a "missing license secrets" notice until a maintainer applies `safe-to-test` (existing secret-safety gate). The full 4-version matrix is opt-in via the `full-matrix` label.

**When the full matrix runs (all 4 versions in parallel):**

- Push to `beta` (the release gate).
- `workflow_call` from `beta-release.yml` / `release.yml`.
- Manual `workflow_dispatch` from the Actions tab.
- Any PR (in-repo or fork) labeled with **`full-matrix`** — apply when your change touches compat shims, conditional compilation, or anything else version-sensitive. Triggers a full-matrix run on the next `pull_request` or `pull_request_target` event. Cost is ~6-8 min wall clock vs ~3 min for the default leg.

Fork PRs still need `safe-to-test` as the base gate (so secrets are exposed against reviewed-only fork code); `full-matrix` is layered on top for fork-PR full-matrix runs.

**Default (single leg)** — every other path runs only against `defaultVersion` from `tools/unity-versions.json` (currently Unity 6.0 LTS, `6000.0.75f1`). The `floor` role (`2021.3.45f2`) still identifies the package minimum and runs as part of the full matrix; it's no longer the default-leg version.

## Local Unity-version parity check

The same `tools/unity-versions.json` drives a local script so you can reproduce CI behavior before pushing.

The script has two runners. Use whichever fits your setup:

| Runner | When to use | Cost |
|---|---|---|
| **Local Unity Hub** (default) | You already have one or more matrix versions installed via Unity Hub. Fastest. | Disk: each editor is 3-6 GB. |
| **GameCI Docker** (`--docker`) | You don't want to install every editor locally. Same containers CI uses. | One-time pull is 5-15 GB per version. On Apple Silicon, expect ~5-10× slowdown from amd64 emulation. |

### Local Unity Hub mode

```bash
# Compile-only check across every locally-installed Unity in the matrix (~30-60s warm per version).
tools/check-unity-versions.sh

# Full EditMode test run — matches what CI runs end-to-end.
tools/check-unity-versions.sh --full

# Filter to one version family.
tools/check-unity-versions.sh --only 6000.0
```

Windows uses the PowerShell companion:

```powershell
pwsh .\tools\check-unity-versions.ps1
pwsh .\tools\check-unity-versions.ps1 -Full
pwsh .\tools\check-unity-versions.ps1 -Only 6000.0
```

Versions not installed via Unity Hub are skipped — the script never forces you to install every editor in the matrix. Install the versions you most care about (typically the floor `2021.3.45f2` and your daily-driver Unity 6) and CI / Docker mode cover the rest.

### GameCI Docker mode (no Unity Hub install required)

```bash
tools/check-unity-versions.sh --docker                # all matrix versions, compile-only
tools/check-unity-versions.sh --docker --full         # full EditMode run
tools/check-unity-versions.sh --docker --only 2022.3  # one version family
```

```powershell
pwsh .\tools\check-unity-versions.ps1 -Docker
pwsh .\tools\check-unity-versions.ps1 -Docker -Full
```

**One-time setup: get a Unity license**

GameCI containers still need an activated Unity license. Free Personal activations work fine and are tied to the machine, not the editor version — so a single `.ulf` covers every version in the matrix.

```bash
# 1. Generate the request file (.alf) — outputs Unity_v<version>.alf in the current directory.
docker run --rm -v "$PWD":/work unityci/editor:ubuntu-2021.3.45f2-base-3 \
  /opt/unity/Editor/Unity -batchmode -nographics -quit \
  -createManualActivationFile -logFile /dev/stdout

# 2. Upload Unity_v<version>.alf at https://license.unity3d.com/manual
#    Choose Personal license → save the resulting .ulf file.

# 3. Export the .ulf contents (add to ~/.zshrc or ~/.bashrc to persist):
export UNITY_LICENSE="$(cat /path/to/Unity_v<version>.ulf)"

# 4. Run the check.
tools/check-unity-versions.sh --docker
```

PowerShell equivalent for step 3: `$env:UNITY_LICENSE = Get-Content C:\path\to\Unity_v<version>.ulf -Raw`.

The same `UNITY_LICENSE` secret is what `.github/workflows/unity-tests.yml` uses in CI — once you have it, your local Docker runs and CI behave identically.

**Coverage gap on Apple Silicon Macs**: GameCI publishes only `linux/amd64` images. Docker Desktop runs them under Rosetta/QEMU at ~5-10× the native amd64 speed. A compile that takes 30s on Intel takes 3-5 min on M-series. Still faster than installing four Unity editors, but plan for it.

**Opt-in pre-push hook**

```bash
tools/install-hooks.sh             # installs .git/hooks/pre-push (idempotent)
tools/install-hooks.sh --uninstall # removes our hooks
```

Once installed, `git push` runs the compile-only check first when the push touches `MCPForUnity/Editor/**`, `MCPForUnity/Runtime/**`, `TestProjects/UnityMCPTests/**`, `tools/unity-versions.json`, or `.github/workflows/unity-tests.yml`. Pushes that touch only docs, Server/, or unrelated files skip the check.

To bypass for a single push: `git push --no-verify`.

**Bumping the matrix**

Edit `tools/unity-versions.json` and update CI + local scripts both in one commit. The file is the single source of truth.

