headless-unity-mcp

headless-unity-mcp

Enables headless batchmode control of Unity projects for compile, test, build, and screenshot tasks, avoiding the need for an interactive editor.

Category
访问服务器

README

headless-unity-mcp

drive a unity project's build/test/screenshot loop from an MCP client — in batchmode, with no interactive editor open, ever.

six tools: unity_compile, unity_test, unity_scene, unity_build, unity_shot, unity_targets, plus unity_status. each returns a compact structured verdict, not a wall of unity log:

{"ok": true, "verdict": "OK EditMode total=234 passed=234 failed=0",
 "totals": [{"platform": "EditMode", "total": 234, "passed": 234, "failed": 0,
             "results_xml": "/path/Logs/editmode-results.xml"}]}

macOS only, for now. see limitations.


the problem

if you point an agent at a unity project, three things go wrong.

unity's exit codes lie. the editor exits 0 with error CS0103 sitting in the log. it exits nonzero when everything compiled. it writes no test results XML at all and still exits 0. an agent that trusts $? will tell you the build is green when it isn't, and that's the single worst failure an agent can have — not being wrong, but being confidently wrong.

batchmode output is a context bomb. a single -batchmode compile emits tens of thousands of lines even with -logFile set. a player screenshot is a PNG that costs thousands of tokens to look at. dump either into an agent's context and you've spent the budget you needed for the actual work. the usual workaround — wrap every call in a throwaway subagent that summarizes — is real overhead you shouldn't have to pay.

the existing unity MCP servers need a live editor. near all of them (coplaydev, codergamester, ivanmurzak, anklebreaker, unity's own first-party package) are editor-embedded bridges: a C# package that opens a socket inside a running, interactive unity editor. that's the architecture, not a gap in the docs. it means you cannot run headless, you cannot run in CI, and you inherit the domain-reload problem — recompile your scripts and the bridge's C# state is torn down underneath it, so the agent goes deaf mid-task (coplaydev #814 — "agents sleep after script changes").

what this does instead

spawn-and-exit. every verb launches Unity -batchmode -nographics, waits for it to exit, and parses the evidence it left on disk. there's no long-lived editor to go stale, so the domain-reload bug class doesn't exist here — the process is gone before the next call starts.

the verdict is derived from evidence, never from the exit code:

what the actual verdict
compile a error CS[0-9]+ grep of the unity log. exit code is ignored.
test the NUnit results XML. missing XML is a FAIL, and any result="Failed" is a FAIL.
build the .app exists on disk (and a .app is a directory — tested with -d, not -f).
shot a PNG over 10KB captured from the player's real window, by CGWindowID.
fatal abort Aborting batchmode due to fatal error never reaches -logFile — it goes to the process's stdout. we read that too, and surface it as an abort: line instead of a truncated log and a mystery exit code.

why this over the alternatives

editor-embedded bridges headless-unity-mcp
needs an interactive editor open yes no
survives a script recompile mid-task no (domain reload tears down the bridge) n/a — nothing is long-lived
usable in CI / over ssh / on a headless box no yes
"run tests" means the live test-runner API -runTests + the results XML as evidence
trusts unity's exit code typically yes never
screenshots the editor window (or unsupported on macOS) the built player's window
concurrent calls undefined machine-wide lock, one cycle at a time
project-specific setup a C# package in your project one toml file

the honest counterpoint: an embedded bridge can do things a batchmode process fundamentally can't — inspect a live scene graph, read the editor console in real time, poke a GameObject while the game is running, offer 40+ fine-grained tools. if that's what you need, use one of those. this server does five verbs and does them verifiably.

requirements

  • macOS. the screenshot path is Quartz + screencapture, and the build targets a mac standalone player. the compile/test/build verbs are portable in principle; nothing else has been ported yet.
  • unity — any version, installed via the hub (or point [unity] path anywhere).
  • python 3.11+ (needs tomllib).
  • for unity_shot: pyobjc-framework-Quartz, plus the Screen Recording grant for whichever app hosts your shell. macOS gates all window capture behind it; without the grant every capture fails with could not create image from window. the driver preflights this and refuses with the fix rather than writing you a black PNG.

install

git clone https://github.com/Shonas301/headless-unity-mcp
cd headless-unity-mcp
python3 -m venv .venv
.venv/bin/pip install -e ".[shot]"      # drop [shot] if you don't need screenshots

configure

drop a unity-mcp.toml in the root of your unity project (next to Assets/ and ProjectSettings/). this is the only project-specific file — the server and the driver stay generic.

default_target = "mac"

[unity]
version = "6000.5.0f1"        # validated against ProjectSettings/ProjectVersion.txt
# path = "/Applications/Unity/Hub/Editor/6000.5.0f1/Unity.app/Contents/MacOS/Unity"

[shot]
delay_s = 9                   # wait out the unity splash; the splash window is NOT the game window
width = 800
height = 600

[targets.mac]
build_method = "MyGame.Editor.BuildScript.BuildMacDev"   # a public static method, via -executeMethod
app_path = "Build/Mac/MyGame.app"

# optional — only if your project authors scenes from code
scene_method = "MyGame.Editor.SceneBuilder.Generate"
scene_path = "Assets/Scenes/Main.unity"

build_method is an ordinary editor static method, the same one you'd call from CI:

public static class BuildScript {
    public static void BuildMacDev() { BuildPipeline.BuildPlayer(/* ... */); }
}

that [unity] version check is load-bearing. it's what stops a call aimed at the wrong project from cheerfully reporting all-green — a real failure mode, and one that is very hard to notice, because every individual line of the output looks fine.

see unity-mcp.example.toml for the annotated version.

register with an MCP client

.mcp.json (claude code, and most clients take the same shape):

{
  "mcpServers": {
    "unity": {
      "command": "/abs/path/to/headless-unity-mcp/.venv/bin/python",
      "args": ["-m", "unity_mcp.server"],
      "timeout": 1800000
    }
  }
}

the long timeout matters: a build or a PlayMode run takes minutes, and a queued call blocks until the machine lock frees. progress notifications do not extend an MCP client's tool timeout, so the timeout has to actually cover the wait.

every tool takes an optional workspace (absolute path to the unity project). omit it and the server uses its own cwd, which is what you want when the client launches one server per project.

the tools

tool what it does
unity_compile import + compile all assemblies. verdict is a log grep, never $?.
unity_test platform: edit | play | all. returns parsed per-platform totals. missing XML = FAIL.
unity_scene regenerate a target's scene via its scene_method.
unity_build build a target's standalone player via its build_method.
unity_shot launch the built player, screenshot its window, return the PNG path.
unity_targets list the targets this project defines. never takes the lock.
unity_status who holds the machine lock right now. never takes the lock.

one build at a time, machine-wide

there is one unity binary and one license on your machine, and two editors cannot open the same project at all — the second one dies with Multiple Unity instances cannot open the same project. so the server serializes.

it has to be a real OS lock, not an in-process one: each MCP client spawns its own stdio server process, so an asyncio.Lock would serialize nothing across them. this uses fcntl.flock on a file under ~/.cache/headless-unity-mcp/, machine-wide. a second caller blocks until the first finishes (bounded by queue_wait_timeout_s, default 900s), then runs. if it gives up, the refusal names who held the lock and since when.

unity_status reads the holder without taking the lock, so you can always ask.

about unity_shot

it returns the PNG's path, not the image. that's deliberate, but it means the one context bomb this server can't defuse is still live: deciding whether a screenshot shows a real gameplay frame or the unity splash requires looking at pixels, and that's thousands of tokens whoever does it.

so: read the PNG in a throwaway subagent, not in your main context. ask it for one sentence. the other five verbs return one-line verdicts and need no such thing.

standalone / CI

the driver is a plain shell script and works with no MCP client at all:

UNITY_MCP_PROJECT=/path/to/project ./src/unity_mcp/driver.sh compile
UNITY_MCP_PROJECT=/path/to/project ./src/unity_mcp/driver.sh test edit
UNITY_MCP_PROJECT=/path/to/project ./src/unity_mcp/driver.sh build mac

it prints the same verdict lines the tools parse, and exits nonzero on failure. the MCP server shells out to exactly this file — a verdict you see through a tool is a verdict this script printed. there is no second implementation to drift.

verifying it

the unit suite is hermetic (a stub driver fakes unity's failure modes; no unity needed):

.venv/bin/python -m pytest -q
.venv/bin/python -m ruff check .
.venv/bin/python -m mypy

the gates are the real harness — they assert the properties that actually matter, and they write machine-readable evidence to gates/evidence/:

.venv/bin/python gates/run_gates.py                    # G0–G4, hermetic
UNITY_MCP_GATE_WORKSPACE=/path/to/project \
UNITY_MCP_GATE_EDIT_FLOOR=230 \
  .venv/bin/python gates/run_gates.py                  # + G5/G6 against real unity
gate asserts
G0 ruff + mypy + unit suite clean
G1 two concurrent calls to one server don't overlap
G2 two concurrent calls to separate server processes don't overlap (the test an asyncio.Lock fails)
G3 two workspaces, two processes, neutral cwd — each reports its own numbers (arithmetic tripwire: A must say 7, B must say 13)
G4 guard parity: exit-code-lies, fatal abort, missing XML, bad project and bad target both refused before the lock, and a timeout kills the whole process group (no orphaned 2GB editor)
G5 real unity: compile + EditMode against a real project, with a totals floor
G6 real screenshot: a >10KB PNG off the built player

SKIPPED always carries a reason and is never counted as a pass.

limitations

  • macOS only. the shot path is Quartz-specific and the build targets a mac player.
  • no live-editor introspection. no scene graph, no live console, no poking a running game. that's the trade for not needing an editor.
  • you cannot inject input into the player. a screenshot shows you a frame; it can't press a key. drive gameplay from PlayMode tests instead — give your controller an input seam (a Func<Vector2> the test can set) and assert on real physics.
  • one project at a time per machine, by construction. that's the lock, and it's a feature.

license

MIT.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选