Skip to main content
Glama
README.md
# bunny — always-on MCP proxy for Unreal Engine 5.8

A small proxy that sits between your MCP harness (Junie, Cursor, Claude Code,
VS Code, MCP Inspector, …) and the **ModelContextProtocol** plugin embedded in
Unreal Engine 5.8. Two jobs:

1. **Fix the protocol-version deadlock.** UE 5.8 whitelists
   `2025-11-25 / 2025-06-18 / 2024-11-05` and replies with `2025-11-25` to
   anything else. Kotlin-based SDKs (e.g. Junie's) offer `2025-03-26`, see
   `2025-11-25` back, throw `Server's protocol version is not supported:
   2025-11-25` and mark the server **Failed**. The proxy negotiates *with each
   side in its own language*: it echoes back whatever protocol version the
   client asked for (including `2025-03-26`) and talks `2025-06-18` upstream
   to UE.

2. **Never be offline.** The proxy is its own MCP server. UE's tool catalog is
   cached to `data/tools_cache.json` on first connect and served even when the
   editor is closed, so harnesses keep the same stable MCP surface through
   code → compile → restart-engine loops. Calls to `ue_*` tools route to UE
   when it is running and return an **actionable** error when it is not
   (never a crash/timeout), and local `proxy__*` tools let the agent run the
   engine, build the project, and refresh the catalog itself.

```
harness ──http://127.0.0.1:8765/mcp──► bunny ──http://127.0.0.1:8000/mcp──► UE 5.8
   (any protocol version)             (shim + cache + launcher)          (2025-06-18)
```

## Quick start

```bat
:: 1. install (once)
uv venv --python 3.14 .venv
uv pip install --python .venv\Scripts\python.exe -r requirements.txt

:: 2. point it at your project + engine
copy config.example.json config.json
notepad config.json

:: 3. run the proxy
bunny.bat
::   -> http://127.0.0.1:8765/health
::   -> MCP endpoint: http://127.0.0.1:8765/mcp
```

Initial tool cache: start UE once, run `ModelContextProtocol.StartServer` in
its console (or set the plugin's **Auto Start Server**), then:

```bat
.venv\Scripts\python.exe -m bunny.server --refresh
```

After that the tools stay cached. UE can come and go freely.

### Point your harness at it

CLI-only (there is deliberately no MCP tool for this — it's setup, not a
runtime capability):

```bat
:: print the entry for a harness to stdout (default --agent claude),
:: plus its default global config location on stderr
.venv\Scripts\python.exe -m bunny.server --emit-client-config

:: write/merge the entry into a file (existing servers in it are preserved)
.venv\Scripts\python.exe -m bunny.server --emit-client-config "C:\path\mcp.json" --agent junie
```

`--agent` picks the harness syntax and default location:

| `--agent` | Syntax emitted | Default global location (no `--path`) |
| --- | --- | --- |
| `claude` (default) | Claude Desktop `mcpServers` stdio entry bridging via `npx -y mcp-remote <url>` (Desktop has no native url transport; needs Node) | `%APPDATA%\Claude\claude_desktop_config.json` |
| `codex` | TOML `[mcp_servers.unreal-via-bunny]` with `url = …` (Streamable HTTP native) | `%USERPROFILE%\.codex\config.toml` |
| `opencode` | `opencode.json` `"mcp": {"unreal-via-bunny": {"type": "remote", "url": …}}` | `%USERPROFILE%\.config\opencode\opencode.json` |
| `hermes` | YAML `mcp_servers.unreal-via-bunny.url` | `%HERMES_HOME%\config.yaml` (`~/.hermes/config.yaml`) |
| `copilot` | VS Code user `mcp.json` `"servers": {"unreal-via-bunny": {"type": "http", "url": …}}` | `%APPDATA%\Code\User\mcp.json` |
| `junie` | `mcpServers` with `"type": "http", "url": …` | `%USERPROFILE%\.junie\mcp\mcp.json` |

Writes merge: JSON files keep their other servers, TOML/YAML replace only the
bunny section. Restart the harness afterwards. Scope notes: Junie project scope
`.junie/mcp/mcp.json`; Copilot CLI is `~/.copilot/mcp-config.json`
(`mcpServers` + `type`, write via explicit path); opencode also reads a
project-root `opencode.json`. Junie example of the emitted entry:

```json
{
  "mcpServers": {
    "unreal-via-bunny": { "type": "http", "url": "http://127.0.0.1:8765/mcp" }
  }
}
```

No special client settings needed: the proxy accepts 2024-11-05, 2025-03-26,
2025-06-18, 2025-11-25 and 2026-07-28 clients.

## The tool surface

Always available (proxy-local, prefixed `proxy__`):

| Tool | Purpose |
| --- | --- |
| `proxy__ue_status` | Is UE's MCP reachable? Editor process running? Launch/build possible? |
| `proxy__ue_launch` | Start the editor (`-ExecCmds=ModelContextProtocol.StartServer`) and block until MCP is reachable; re-caches tools. Always the GUI `UnrealEditor.exe` (see launch rules). |
| `proxy__ue_wait_ready` | Block until MCP reachable (use after you start UE manually / after live-coding). |
| `proxy__ue_build` | `Build.bat <Target> Win64 Development -project=…` — compiles with UE closed; auto-relaunches the editor on success (`auto_launch_after_build`). |
| `proxy__ue_kill` | Terminate the editor (`confirmed=true` required; no autosave). |
| `proxy__ue_refresh_cache` | Re-cache UE's tools/resources/prompts (do this after enabling more toolset plugins). |
| `proxy__cache_stats` | Catalog counts, capture time, effective config. |
| `proxy__ue_configure` | `scan` engine installs / write project+engine paths from inside the harness. |
| `proxy__ue_adopt_project` | Re-detect the running editor's project+engine and adopt as launch/build defaults. |
| `proxy__ue_enable_toolset` | Enable one toolset plugin (e.g. `LiveCodingToolset`) in the active project's `.uproject` (backs it up first). |
| `proxy__ue_sync_toolsets` | `{dry_run?}` — reconcile the project's toolset list with the engine: add gap plugins AllToolsets doesn't cover, prune redundant/vanished ones, respect `Enabled:false` opt-outs, idempotent. |
| `proxy__ue_engines` | List every installed engine (version, full build, path, discovery source, which is selected). |

Cached UE tools are listed as `ue_<toolname>` (e.g. `ue_list_toolsets`,
`ue_call_tool`), with their real UE schemas, at all times — UE online or not.
When UE is live they forward verbatim (name demangled) and responses pass
through untouched.

The intended agent loop:

```
(proxy__ue_status) -> ue offline
(proxy__ue_build)  -> compile the C++ you just edited
(proxy__ue_launch) -> editor up, MCP reachable, catalog refreshed
(ue_call_tool)     -> routed to the live engine
…close editor…     -> proxy stays up, tools stay listed, calls stay actionable
```

## Configuration

`config.json` (copy from `config.example.json`), `data/`, and `logs/` live in
the **root dir**: the folder containing the `bunny` package, overridable with
the `BUNNY_ROOT_DIR` env var — point it at a deployment folder to keep your
config/state separate from the source tree and to run several independent
instances. `config.json` is gitignored; `config.example.json` is the committed
reference.

| Key | Meaning |
| --- | --- |
| `host` / `port` | Proxy bind address (default `127.0.0.1:8765`). |
| `upstream_url` | UE MCP endpoint (default `http://127.0.0.1:8000/mcp`). |
| `project_path` | Absolute path to your `.uproject`. Enables launch/build. |
| `editor_binary` | **`<engine>\Engine\Binaries\Win64\UnrealEditor.exe`** (GUI editor). Never `UnrealEditor-Cmd.exe` — see launch rules. |
| `engine_root` | Derived from `editor_binary` if omitted; used by `proxy__ue_build`. |
| `build_target` | Defaults to the Editor target discovered from `Source/*.Target.cs` (falls back to `<ProjectName>Editor`). |
| `platform` / `configuration` | Build parameters (default `Win64` / `Development`). |
| `launch_args` | Appended on launch; keep `-ExecCmds=ModelContextProtocol.StartServer`. |
| `launch_template` / `build_template` | Full command templates (placeholders `{editor} {project} {target} {platform} {configuration} {engine_root} {extra_args}`). |
| `poll_interval_s` | UE liveness poll (default 12s; cache auto-refresh when UE comes up). |
| `forward_timeout_s`, `build_timeout_s`, `launch_ready_timeout_s` | Long-call budgets. |
| `auto_adopt_project`, `auto_launch_after_build` | Adoption / relaunch behaviors (see below). |
| `scan_extra_roots` | Extra folders to scan for engine installs (engines kept outside the standard roots). |

Any key can be overridden per-run with env vars: `BUNNY_PORT=8766`,
`BUNNY_UPSTREAM_URL=…`, `BUNNY_PROJECT_PATH=…` (handy for second instances).
`--print-config` shows the effective config.

## Follows any project (project adoption)

MCP tool routing is project-agnostic: UE owns `127.0.0.1:8000`, so whichever
project the editor has open is what the `ue_*` tools act on. Launch/build
defaults follow it automatically: when UE comes up, the proxy detects the
running editor's `.uproject` from its command line, resolves its engine,
discovers its build targets from `Source/*.Target.cs`, and stores the result
in `data/adopted.json` (survives proxy restarts; `config.json` stays the
static default). `proxy__ue_status` shows `active_project / active_engine /
adopted_from`; `proxy__ue_adopt_project` forces a re-scan after you open a
different project.

Engine discovery is **layered, never guessed**: running editor's own
executable → Epic Launcher manifests → Windows registry → standard install
roots → `scan_extra_roots`. When every layer misses, the proxy reports and
asks instead of guessing. Details:
[docs/implementation_details.md](docs/implementation_details.md).

`proxy__ue_sync_toolsets {"dry_run": true?}` keeps the project's toolset list
lean and engine-proof: `AllToolsets` is the enable-all switch (its manifest is
engine-maintained), the project file stores only the gap plugins it doesn't
cover. Sync adds missing gap plugins, prunes redundant/vanished ones, respects
explicit `Enabled:false` opt-outs, never touches marketplace plugins, backs up
the `.uproject`, and is idempotent. After a real sync run `proxy__ue_build`
(UE closed) to compile new modules — it auto-relaunches the editor.

`proxy__ue_enable_toolset {"plugin": "LiveCodingToolset"}` enables any engine
toolset plugin in the active project's `.uproject`; afterwards run
`proxy__ue_build` and `ue_list_toolsets` shows the new toolset.
LiveCodingToolset ships `EnabledByDefault: false`, so a harness that wants
in-editor compiles calls this once per project.

## Notes & limits

- The proxy binds to 127.0.0.1 and does not add auth; treat it like UE's own
  MCP server (local only). UE's optional MCP auth token, if you enable it, is
  not forwarded — leave UE's server unauthenticated on loopback.
- Server→client SSE streams (`GET /mcp`) answer 405: allowed by the spec;
  clients fall back to polling. Catalog freshness comes from refresh-on-
  launch / refresh-on-UE-boot / 60s-refresh-on-call.
- Tool-name collisions with harnesses are avoided by the `ue_` prefix.
  Unprefixed cached names are also accepted, in case a harness strips prefixes.
- Resources/prompts are served from cache when offline; `resources/read` /
  `prompts/get` need UE live (forwarded, actionable error otherwise).
- `proxy__ue_launch` detects "editor running but MCP server off" and tells the
  agent to run `ModelContextProtocol.StartServer` inside it.
- Launch args must **not** include `-unattended`: UE then runs the ExecCmds and
  exits seconds later. `-ExecCmds=ModelContextProtocol.StartServer -nosplash`
  keeps the editor alive.
- **The proxy only starts a persistent editor with the GUI
  `UnrealEditor.exe`.** `UnrealEditor-Cmd.exe` is for command/test runs that
  are supposed to close the engine afterwards. If `editor_binary` points at
  `-Cmd`, the proxy silently switches to the sibling `UnrealEditor.exe`; if
  that sibling doesn't exist it refuses with `cmd_editor_not_allowed` and
  tells you to fix `editor_binary`. Editor detection (`running_project`,
  `proxy__ue_status`) still *sees* `-Cmd` processes — reading is fine,
  launching is not. Why these rules exist:
  [docs/implementation_details.md](docs/implementation_details.md).

## Running as a Windows service (nssm)

`install_service.bat` (run **elevated** — `elevate_install.bat` elevates it
with one UAC click; set `NSSM=` at the top of install_service.bat to your
nssm.exe) (re)installs `ue-mcp-bunny`: app = `.venv\Scripts\python.exe -m
bunny.server`, working dir = the folder containing the script, autostart,
account **LocalSystem**. Run it from the folder you want the proxy to live in
(that folder holds its `config.json` / `data/` / `logs/`).

- **LocalSystem is mandatory** — an editor launched from a service must land
  on your desktop, and only LocalSystem has the privileges to reach the
  interactive session. Quick check: `sc qc ue-mcp-bunny` →
  `SERVICE_START_NAME : LocalSystem`, and `proxy__ue_status` →
  `window.cross_session_route: "WTSQueryUserToken"`.
- `restart` to load new code: `nssm restart ue-mcp-bunny` (elevated).
- The full story (why running the service as your user fails, session-0
  spawn routes, environment blocks):
  [docs/implementation_details.md](docs/implementation_details.md).

## Tests

```bat
:: self-contained (mocked, no real UE needed)
.venv\Scripts\python.exe tests\test_proxy.py         :: proxy vs a mock UE (incl. the strict-SDK version deadlock)
.venv\Scripts\python.exe tests\test_sdk_client.py    :: via the official MCP Python SDK client
.venv\Scripts\python.exe tests\test_targets.py       :: build-target discovery
.venv\Scripts\python.exe tests\test_workspace.py     :: harness workspace detection
.venv\Scripts\python.exe tests\test_cmd_guard.py     :: proxy never launches UnrealEditor-Cmd.exe
.venv\Scripts\python.exe tests\test_crosssession.py  :: session-0 -> interactive-session spawn logic
.venv\Scripts\python.exe tests\test_session_policy.py:: client session/policy bookkeeping
.venv\Scripts\python.exe tests\test_window.py        :: child window policies (spawns short consoles)
.venv\Scripts\python.exe tests\test_guardian.py      :: keeps build consoles minimized

:: real end-to-end (needs a running proxy + configured project/engine)
.venv\Scripts\python.exe tests\e2e_real.py           :: launch -> cache -> real tool calls
.venv\Scripts\python.exe tests\e2e_lifecycle.py --keep-ue  :: kill UE -> offline cached tools -> proxy relaunch -> live routing
.venv\Scripts\python.exe tests\e2e_soak.py --minutes 3     :: editor survives 3 min after proxy launch
.venv\Scripts\python.exe tests\e2e_adoption.py       :: running project auto-adoption recorded cleanly in data/adopted.json
.venv\Scripts\python.exe tests\e2e_build.py          :: build with UE closed -> auto-relaunch
```

Logs: `logs/bunny.log` (proxy), `logs/editor_launch.log` (UE), `logs/build.log`.

## Docs

- [docs/implementation_details.md](docs/implementation_details.md) — design
  notes: protocol shim internals, engine resolution layers, launch rules and
  the `-Cmd` trap, the service/session-0 story, toolset sync semantics.

## Credits

Vibecode by [Qwen Flash Next GSQ RCO IQ3_XXS](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF) on [Strata engine](https://github.com/Niko1221/Strata), Human QA by Charnet3D

Maintenance

ActivityMaintained
ResponsivenessNo issues