# Unreal MCP — Tool Surface Reference How Epic's editor-embedded MCP server organizes, advertises, and executes tools, and how to extend the surface when the shipped tools run out. Everything here is against UE 5.8's experimental plugin (id `ModelContextProtocol`); expect drift between engine versions — the live `describe_toolset` schema always outranks this file. ## Architecture in one paragraph The **Unreal MCP** plugin hosts the HTTP server inside the editor process (default `http://127.0.0.1:8000/mcp`, loopback-only, no auth, HTTP + SSE only — no stdio/WebSocket). It implements the protocol but ships no tools of its own. Tools come from **Toolsets** — classes deriving from `UToolsetDefinition` (C++) or `unreal.ToolsetDefinition` (Python) — collected at startup by the **Toolset Registry** subsystem (sibling plugin, auto-enabled; the registry itself ships no toolsets either). The shipped tools live in per-domain plugins under `Engine/Plugins/Experimental/ Toolsets/` — the workhorse is **EditorToolset** (core editor toolsets, Python + C++) — and **AllToolsets** is a one-checkbox aggregator plugin that depends on ~21 of them (verified from `AllToolsets.uplugin`, 5.8): AIModule, AnimationAssistant, AutomationTest, ConfigSettings, Conversation, DataRegistry, DataflowAgent, Editor, GameFeatures, GameplayTags, GAS, MCPClient, Niagara, PCG, Physics, Plugin, SemanticSearch, SlateInspector, StateTree, UMG, WorldConditions. Project plugins and Game Feature Plugins can contribute more. Unreal MCP wraps every registered tool call as an MCP Tool. Execution is **serialized onto the game thread** — one tool call at a time, editor UI blocked while each runs. ## Tool-search mode (the default contract) With `Enable Tool Search` on (default), `tools/list` advertises exactly three meta-tools: | Meta-tool | Args | Returns | |---|---|---| | `list_toolsets` | — | Registered toolset names + descriptions | | `describe_toolset` | toolset name | JSON Schemas for every tool in that toolset | | `call_tool` | toolset/tool name + arguments object | The tool's result, same turn | Discipline: - `list_toolsets` once per session; re-run only after `RefreshTools`, plugin changes, or reconnect. - `describe_toolset` before first use of any toolset. Parameter names, types, and required fields come from the schema — never from memory or this file. - Results: primitive results arrive wrapped as `{"result": ...}` (CVar `ModelContextProtocol.WrapPODToolResultsInObject`, default true). Structured results serialize with field-level schema. - Errors come back as tool-call errors with the engine-side message — read them; they usually name the offending parameter or missing asset. Eager mode (`Enable Tool Search` off) advertises every tool individually. Under Hermes that means each tool becomes `mcp_unreal_engine_` at session start, and `hermes mcp configure unreal-engine` can prune the list. Schema payload grows with every registered toolset, and tool authors are told NOT to rely on eager advertising — stay in tool-search mode unless a very small fixed surface is wanted. ## call_tool dispatch semantics (live-verified, 5.8) Verified against a running 5.8 server; these details are where naive clients die: - `list_toolsets` returns **fully-qualified** toolset names — Python: `editor_toolset.toolsets.scene.SceneTools`; C++: `EditorToolset.EditorAppToolset`. Epic's prose says "SceneTools"; the registry speaks qualified names. Use them verbatim in `describe_toolset` and `call_tool`'s `toolset_name`. - `tool_name` must be the **short** name (`get_current_level`, `CaptureViewport`). Passing the fully-qualified tool name fails with "Unknown tool" even though `describe_toolset` displays qualified names. - `call_tool` args: `{"toolset_name": ..., "tool_name": ..., "arguments": {...}}`; result returns on the same turn (the HTTP response blocks until the game thread finishes the call). - **`TOptional` parameters must be passed explicitly as `null`** — omitting them errors with `input param "X" needs a default value`. E.g. `CaptureViewport` minimal call is `{"captureTransform": null, "annotations": null, "bShowUI": false}`. - **Schema `required` is literal.** `find_actors` marks `name`, `tag`, `collision_channels` required even though they're semantically optional — pass `""` / `[]` to mean "any". - **Property names are camelCase with UE's `b` prefix intact** at this reflection layer: `bUseTemperature`, `bAtmosphereSunLight`, `fogDensity`, `bRealTimeCapture`, `mobility`. Writing `useTemperature` does NOT error the whole call — the response names each property that could not be set (schema-in-error style; READ error text, it lists the exact failures and often the full input schema). - **Object references travel as `{"refPath": ""}`** everywhere (actors, classes, components). Class refs use `/Script/Module.Class` (e.g. `/Script/Engine.PointLight`); actor refs are the full path (`/Temp/Untitled_1.Untitled_1:PersistentLevel.DirectionalLight_UAID_...`). Spawn/find tools RETURN refPaths — capture and reuse them. - **`ObjectTools.set_properties` takes `values` as a JSON *string***, not an object: `{"instance": {"refPath": ...}, "values": "{\"intensity\": 10.0}"}`. `get_properties` likewise returns a JSON string inside `returnValue`. Double-encode/decode accordingly. - Primitive results arrive wrapped as `{"returnValue": ...}` inside the text content block. ### HTTP wire behavior (for raw clients / debugging) - `initialize` → plain JSON response + `Mcp-Session-Id` header you must echo on every subsequent request; `notifications/initialized` → 202 empty; `tools/call` → **`text/event-stream`**: the result arrives as an `event: message` + `data: ` frame only when the game thread finishes. A client that treats the response as plain JSON reads an empty body. Send `Accept: application/json, text/event-stream` always. ## Shipped toolsets The registry is project-dependent; `describe_toolset` on the live server is the only source of truth for schemas. The core surface below is verified against EditorToolset's source in the 5.8 install (Python: `.../EditorToolset/Content/Python/editor_toolset/toolsets/`; C++: `EditorAppToolset.h`). **EditorToolset plugin (the core), Python toolsets** (live-verified on 5.8; qualified prefix `editor_toolset.toolsets..`): | Toolset | Verified tools (subset) | |---|---| | `scene.SceneTools` | `load_level`, `get_current_level`, `find_actors` (by name/type/tag/bounds), `add_to_scene_from_class`, `add_to_scene_from_asset`, `remove_from_scene`, `save_actor`, `create_level_instance`, folders | | `actor.ActorTools` | `get_label`/`set_label`, tags, `get_actor_transform`/`set_actor_transform` (`xform` fields optional = "don't change"), parenting, components | | `primitive.PrimitiveTools` | `add_cube` (dimensions), `add_sphere` (radius), `add_cylinder`/`add_cone` (radius+height) — adds StaticMeshComponents with `local_transform` to a host actor: spawn `/Script/Engine.Actor`, then compose. The fastest blocking path, zero asset dependencies | | `object.ObjectTools` | `list_properties` (returns full JSON schema of every property), `get_properties`/`set_properties` (JSON-string `values`), `reset_properties` (restore defaults — also your rollback), `get_class`, `search_subclasses` | | `material_instance.MaterialInstanceTools` | `create`, `list_parameters`, `get/set_scalar_parameter`, `get/set_vector_parameter` | | `asset.AssetTools` | `find_assets`, `load_asset`, `exists`, `save_assets`, `is_dirty`, `get_dependencies`/`get_referencers` (check before delete!), `delete`, `move`, `duplicate`, folders, `read_file`/`write_file` (project-scoped) | | `blueprint.BlueprintTools` (+ dsl/layout/node) | Blueprint authoring | | `material.MaterialTools`, `static_mesh.StaticMeshTools`, `texture.TextureTools`, `data_table.DataTableTools`, … | per-asset-type operations | | `programmatic.ProgrammaticToolset` | **the batching escape hatch** — see below | **`EditorToolset.EditorAppToolset` (C++, same plugin) — the agent's eyes (full live list):** `CaptureViewport`, `CaptureEditorImage`, `CaptureAssetImage`, `GetCameraTransform`/`SetCameraTransform`, `GetSelectedActors`/`SelectActors`/`FocusOnActors`/`GetVisibleActors`, `WorldPosToScreenCoords`/`ScreenCoordsToWorld`, `GetSelectedAssets`/`SelectAssets`, `GetContentBrowserPath`/`SetContentBrowserPath`, `OpenEditorForAsset`, `GetOpenAssets`, `SearchCVars`, `StartPIE`/`StopPIE`/`IsPIERunning`. `CaptureViewport` specifics (live-verified): args `{"captureTransform": , "annotations": , "bShowUI": false}`. Returns base64 PNG (decode + save it yourself) plus camera location/rotation/FOV. `captureTransform` captures from any pose WITHOUT moving the user's viewport — use it as a virtual camera. Annotation config `{"gridSpacingCm": 500, "gridExtentCm": 3000, "gridHeight": , "labelActors": true}` overlays a projected ground grid and actor callouts; **grid coordinate labels are in METERS** (world cm ÷ 100). Use annotated captures for placement work, clean ones for beauty checks. Also confirmed live: `ToolsetRegistry.AgentSkillToolset`, `EditorToolset.LogsToolset` (read Output Log + set verbosity — useful for self-debugging), `SemanticSearchToolset` (hybrid vector+BM25 asset search), five `NiagaraToolsets.NiagaraToolset_*` groups, `PCGToolset` (+Spatial), `UMGToolSet`, three `GASToolsets.*`, `AutomationTestToolset`, `ConfigSettingsToolset` (read/write Project Settings & Editor Preferences sections by schema — the remote path to exposure defaults, rendering settings, etc.), `SlateInspectorToolset`, `PluginToolset`, `animation_toolset.toolsets.sequencer.SequencerTools` + keyframing/ controlrig/outliner siblings, `aimodule_toolset` BehaviorTreeTools, `state_tree_toolset` StateTreeTools, and more — 67 toolsets on a blank project with AllToolsets enabled. Known gap: no mesh-modelling tools — spawn/place/instance existing meshes, yes; author new geometry, no. The supported route to parametric geometry is a custom Python toolset wrapping **Geometry Script** (`UDynamicMesh`: append box/cylinder/sphere, booleans, then `Create New Static Mesh Asset from Mesh` to bake an `SM_` asset). For organic/sculpted meshes, model in Blender and import. First-session move: `list_toolsets`, then `describe_toolset` each group you plan to use, and keep those schemas in working memory for the session. ## ProgrammaticToolset — sanctioned batching The serial-call rule makes N-step edits slow over the wire. The shipped answer is `ProgrammaticToolset` (verified in `programmatic.py`): 1. `get_execution_environment` — **mandatory first call** (the tool's own docstring requires it); returns the allowed modules, script constraints, and usage instructions. 2. `execute_tool_script(script)` — runs a **sandboxed** Python script that defines `run() -> dict`. Inside, you call other registered tools programmatically and glue them with logic — one MCP round-trip for a whole loop (e.g. spawn 20 actors with computed transforms). Sandbox facts (from source): allowed imports are `json`, `math`, `datetime`, `copy`, `re`, `time` only; `open()` is restricted to project-contained paths; scripts run inside an editor **transaction scope** (undo-friendly); it is tool orchestration, NOT general Python — arbitrary `unreal.*` calls are not the contract. Data returns via `run()`'s dict. Use it whenever a recipe loop exceeds ~5 homogeneous calls; keep one-off edits as plain `call_tool`. ## Project Agent Skills (AgentSkillToolset) Projects and plugins can register **Agent Skills** — named instruction bundles for project-specific conventions and workflows (naming schemes, folder layout, canonical multi-step sequences). They are NOT listed by `list_toolsets`; reach them through `call_tool`: 1. `AgentSkillToolset.ListSkills` → names + descriptions of registered skills. 2. If one matches the task, `AgentSkillToolset.GetSkills` on it → full instructions, then FOLLOW THEM — a project skill exists precisely because the project's way differs from the obvious way, and it takes precedence over this skill's generic defaults. Check at the start of unfamiliar work in any project, not just once ever. ## Seeing your work: screenshots and captures An agent that can't see the viewport is flying blind. In order of preference: 1. **`EditorAppToolset.CaptureViewport`** (confirmed shipped) — returns the image through MCP as base64 PNG with camera metadata; supports capturing from an arbitrary transform without disturbing the user's viewport, and an optional annotation overlay (world-space meter grid + actor callouts) for spatial-placement work. This is the default verification tool. 2. **Console `HighResShot` via any console/exec tool** when you need resolutions beyond the viewport: `HighResShot 3840x2160` writes to `/Saved/Screenshots//` on the EDITOR host's filesystem; read the file back (same machine) with `vision_analyze`. 3. **Custom toolset escape hatch** for anything else (e.g. camera-actor framed captures with MRQ-quality settings). Always `vision_analyze` the capture and art-direct against the brief before declaring a milestone done. ## Plugin configuration reference Editor Preferences > General > Model Context Protocol: | Property | Default | Notes | |---|---|---| | Auto Start Server | `false` | Turn on for frictionless sessions | | Server Port Number | `8000` | Change on conflict; mirror in Hermes config url | | Server URL Path | `/mcp` | Same | | Enable Tool Search | `true` | Keep on (see above) | Console commands (editor console, backtick): | Command | Effect | |---|---| | `ModelContextProtocol.StartServer [port]` | Start server (optional port override) | | `ModelContextProtocol.StopServer` | Stop server, close all sessions | | `ModelContextProtocol.RefreshTools` | Re-poll toolset providers — run after authoring/hot-reload/Game-Feature activation | | `ModelContextProtocol.GenerateClientConfig ` | Write client config files (ClaudeCode/Cursor/VSCode/Gemini/Codex) — NOT used for Hermes | Command-line flags for launching the editor pre-configured: `-ModelContextProtocolStartServer` (force start regardless of preference), `-ModelContextProtocolPort=N`. Console variables: | CVar | Default | Notes | |---|---|---| | `ModelContextProtocol.WrapPODToolResultsInObject` | `true` | Primitive results wrapped as `{"result": ...}` | | `ModelContextProtocol.AudioResultOggFormat` | `false` | OGG instead of WAV for audio results | | `ModelContextProtocol.ProgressIntervalSeconds` | `1.0` | Min interval between progress notifications | | `ModelContextProtocol.PaginationPageSize` | `0` | 0 = no pagination of list results | | `ModelContextProtocol.EnableAnalytics` | `true` | Epic telemetry gate | ## Debugging the connection - **Output Log** at editor startup logs bind address/port/path — first stop when the server seems absent. Port-in-use and missing-dependency failures surface here. - **Log verbosity:** `Log LogModelContextProtocol Verbose` in the editor console. - **MCP Inspector** (`npx @modelcontextprotocol/inspector`, point at `http://127.0.0.1:8000/mcp`, transport "Streamable HTTP") lists every advertised tool with schemas and offers form-based invocation — isolates "server broken" from "agent calling it wrong". - **After Live Coding / authoring:** connected clients can hold stale schemas. `ModelContextProtocol.RefreshTools`, then reconnect (new Hermes session) if schemas still look stale. ## Extending the surface: custom toolsets When shipped tools don't cover an operation, the supported path is authoring a project toolset — NOT trying to smuggle arbitrary code through unrelated tools. Python toolsets are first-class and hot-loadable, so prefer them. ### Python toolset (recommended) Any enabled plugin's `Content/Python/` directory (or the project's) can hold toolset modules; the registry discovers them at startup. Shape (mirrors Epic's shipped `ActorTools`): ```python import unreal import toolset_registry @unreal.uclass() class MySceneTools(unreal.ToolsetDefinition): """One-line toolset description — surfaces to the agent in list_toolsets.""" @toolset_registry.tool_call @staticmethod def take_viewport_screenshot(filename: str, width: int, height: int) -> str: """Capture the active viewport to Saved/Screenshots. Args: filename: Base filename without extension. width: Output width in pixels. height: Output height in pixels. Returns: Absolute path the screenshot will be written to. """ ... ``` Conventions that matter (they generate the schema the agent sees): - `@unreal.uclass()` on the class; inherit `unreal.ToolsetDefinition`. - Class docstring = toolset description; write it for an agent audience. - Each advertised function: `@toolset_registry.tool_call` + `@staticmethod`. Functions without the decorator stay private. - Type hints (`str`, `bool`, `list[str]`, `unreal.Actor`, dataclasses) drive the JSON Schema; Google-style docstrings (`Args:`/`Returns:`) become the parameter descriptions. Write them with API-surface care. - Small, single-responsibility tools with structured return types beat mega-tools returning prose. Data leaves the tool via its RETURN VALUE — `print()`/stdout go to the UE log, not back over MCP. After authoring: `ModelContextProtocol.RefreshTools` in the editor console, then re-`list_toolsets` from Hermes. Users on Claude Code can scaffold with the `create-toolset` skill from Epic's `unreal-mcp` plugin pack; the conventions above still apply. ### C++ toolset Derive from `UToolsetDefinition`, mark the class `UCLASS(BlueprintType, Hidden)`, expose static `UFUNCTION(meta = (AICallable))` methods; doc comments reflect into schemas. Use only when Python can't reach the API, when reflected `USTRUCT` signatures are needed, or when the Python boundary cost matters. Exclude a function with `meta = (AIIgnore)`. Live Coding propagates edited function bodies, but NEW `UFUNCTION`s require a full editor restart. There is also a direct-registration path (`IModelContextProtocolTool` + `IModelContextProtocolModule::AddTool()`) for runtime-shaped tools; caller owns deregistration. ## Runtime and cooked builds The server is editor-hosted by default but not editor-only: runtime modules can host it in cooked builds via `IModelContextProtocolModule::StartServer()`. The Toolset Registry adapter (and the three tool-search meta-tools) are editor-only, though — cooked-build tools must be registered explicitly through `AddTool()` and are advertised eagerly. MCP Resources and Prompts are not advertised by any shipping toolset. ## Known limitations (5.8, experimental) - HTTP + SSE transports only; loopback-only listener; non-loopback `Origin` headers rejected; no auth layer. Not safe beyond the local machine. - Serial game-thread execution: overlapping calls unsupported; editor UI blocks during each call. - Feature-incomplete by Epic's own labeling; APIs and data formats subject to change without notice. - Live Coding does not propagate new `UFUNCTION` declarations.