-
Notifications
You must be signed in to change notification settings - Fork 171
Using the Gateway
The server exposes exactly one MCP tool, unreal. Everything the editor can do sits behind it, reached through four operations:
|
🔎 |
📄 |
|
⚙️ |
Your assistant drives all of this by itself; the tool description and the server's instructions tell it the order. This page is for understanding what it does, writing better prompts, debugging a failed call, or building your own client.
Note
The replies on this page are real, trimmed for length, taken from the native HTTP route. The stdio route carries the same information with a few differences in field names and browsing; they are listed in Differences between the routes.
On this page · Workflow · search · describe · execute · configure · Errors · Old direct calls · Route differences · Tips
sequenceDiagram
autonumber
participant M as AI model
participant G as unreal tool
participant E as Unreal Editor
M->>G: search · "spawn actor"
G-->>M: matching rows, each with a nextCall
M->>G: describe · tool + action from the row
G-->>M: the exact contract
M->>G: execute · tool + action + params
G->>E: validated request
E-->>G: result
G-->>M: data + receipt
- search with 2-4 words naming a verb and an object.
-
describe the row that fits, by sending that row's
nextCallunchanged. -
execute with the same
toolandaction, plusparamsthat use only the parameter names describe listed.
Tip
Search rows and errors carry a nextCall, a ready-to-send request for the next step. A model that follows nextCall rarely gets lost. Nothing is built from guessed names.
{ "operation": "search", "query": "spawn actor", "limit": 2 }Reply
{
"success": true,
"operation": "search",
"query": "spawn actor",
"catalogRevision": "fbe48b864b6054b5",
"total": 95,
"hasMore": true,
"nextCursor": "2",
"results": [
{
"capability": "control_actor.spawn",
"parent": "control_actor",
"domain": "actor",
"family": "spawn",
"effect": "write",
"available": true,
"summary": "Spawn an actor from a class or mesh path, or from a Blueprint, or many actors in one batch.",
"score": 452,
"matchReasons": ["id-exact", "id", "family", "domain", "topic", "summary", "parent"],
"nextCall": { "operation": "describe", "tool": "control_actor", "action": "spawn" }
},
{
"capability": "control_actor.delete",
"parent": "control_actor",
"effect": "destructive",
"summary": "Delete actors by name, or every actor carrying a tag.",
"nextCall": { "operation": "describe", "tool": "control_actor", "action": "delete" }
}
]
}Each row names the capability, the internal tool it belongs to (parent), whether it reads, writes or destroys (effect), and a one-line summary.
| Field | Use |
|---|---|
query |
2-4 plain words: spawn actor, add variable blueprint, save level, set light intensity. Full sentences and filler words rank worse. |
domain · family · tool
|
Narrow to one area, for example "domain": "widget" or "tool": "manage_sequence"
|
effect |
read, write or destructive. "effect": "read" is handy for "look, don't touch". |
limit · offset · cursor
|
Paging. limit defaults to 12 and caps at 25; pass nextCursor back as cursor to continue. |
maxBytes |
Byte ceiling for the reply (512 to 262,144). Rows drop from the end until it fits, and truncated says so. |
Send the row's nextCall:
{ "operation": "describe", "tool": "control_actor", "action": "spawn" }Reply (trimmed)
{
"success": true,
"operation": "describe",
"scope": "capability",
"capability": "control_actor.spawn",
"tool": "control_actor",
"action": "spawn",
"summary": "Spawn an actor from a class or mesh path, or from a Blueprint, or many actors in one batch.",
"parameters": [
{ "name": "classPath", "type": "string", "required": false, "description": "Unreal class path (e.g. /Script/Engine.PointLight) for the actor to spawn.", "variants": ["class"] },
{ "name": "blueprintPath", "type": "string", "required": false, "description": "Canonical /Game Blueprint asset path to spawn from.", "variants": ["blueprint"] },
{ "name": "actors", "type": "array", "required": false, "description": "Actors to spawn, 1-500. ...", "variants": ["batch"] }
],
"inputSchema": { "...": "JSON Schema of exactly these parameters" },
"outputSchema": { "...": "what a successful reply contains" },
"examples": [ { "title": "...", "input": { "...": "..." }, "output": { "...": "..." } } ],
"policy": { "requiredScope": "write", "consent": "none", "dataAccess": "project-write" },
"availability": { "requiredPlugins": [], "editorStates": ["edit"] },
"whenToUse": ["..."],
"whenNotToUse": ["..."]
}The reply is the whole contract for that one capability and nothing else: parameters, input and output schemas, a worked example, policy, availability, and when to use it or not.
variants shows up when a capability is a family: one capability whose selector parameter picks the variant. Here spawnKind is class (the default), blueprint or batch, and actors is only read by the batch variant. More in 🧰 Tools Reference.
describe with fewer selectors walks the catalog:
| Request | Returns |
|---|---|
{ "operation": "describe" } |
The 23 internal tools, each with its action count and a nextCall
|
{ "operation": "describe", "tool": "control_actor" } |
That tool's action names, its domains and families, and a drillDown call |
{ "operation": "describe", "tool": "control_actor", "action": "spawn" } |
One exact contract |
{ "operation": "describe", "tool": "control_actor", "action": "spawn", "param": "actors" } |
One parameter's full schema, plus the consent grant when the capability needs one |
{ "operation": "describe", "tool": "manage_asset", "action": "create_noise_texture" } |
The contract an old name resolves to: the texture.create_texture family. Executing the old name fills in kind: "noise" for you. |
The stdio route also browses by domain and family; see Differences between the routes.
{
"operation": "execute",
"tool": "control_actor",
"action": "spawn",
"params": {
"classPath": "/Script/Engine.PointLight",
"actorName": "KeyLight",
"location": [0, 0, 300]
}
}The gateway checks four things before anything reaches the editor:
| Rule | Detail |
|---|---|
| ✅ Only declared parameters | An unknown name is refused with UNDECLARED_PARAMETER and the list of allowed names. Casing matters. |
| ✅ Types as described | Use the shapes describe lists. control_actor.spawn takes location as [x, y, z], for example. |
✅ No control fields in params
|
action, subAction, operation and consent are envelope fields, not parameters. |
✅ Name the target by tool + action
|
Exactly as the search row and describe gave them. The stdio route also accepts the capability id as "capability": "control_actor.spawn"; sending both is fine only when they agree, otherwise the call gets FORM_CONFLICT. |
A successful reply has success: true, the payload in data (checked against the capability's output schema), and a receipt:
| Receipt field | Meaning |
|---|---|
status |
success or error
|
capabilityId |
The capability that actually ran |
changes |
What the call changed: assets, actors, levels |
handles |
Typed references to what it created or touched, for follow-up calls |
warnings |
Things that worked but deserve a look |
timingMs |
How long the editor took |
validation |
Whether the reply passed the capability's output schema |
liveRevisions |
Selection, level, asset-registry and package counters after the call |
catalogRevision · capabilityRevision · schemaRevision
|
Which contract version validated the call |
correlationId · requestId
|
For matching the call to log lines |
error |
On failure: kind, code, message, and pointer for validation errors |
Replies report what actually happened. A partial result is a failure (DELETE_PARTIAL, PARTIAL_FAILURE) that lists what did and didn't happen, rather than a quiet success.
62 capabilities require a consent grant: all 29 destructive ones, plus 33 writes such as duplicating an asset. describe returns the exact grant, here from { "operation": "describe", "tool": "control_actor", "action": "delete", "param": "actorName" }:
{
"consentGrant": {
"capability": "control_actor.delete",
"acknowledge": "explicit",
"nonce": "62A12661-4F6A-1C97-339E-69921EE9E2A7"
}
}Send it back, unchanged, as a top-level consent field next to params:
{
"operation": "execute",
"tool": "control_actor",
"action": "delete",
"params": { "actorName": "OldLight" },
"consent": { "capability": "control_actor.delete", "acknowledge": "explicit", "nonce": "62A12661-4F6A-1C97-339E-69921EE9E2A7" }
}- Without a grant, the call is refused with
CONSENT_REQUIRED. - The
noncemakes a grant single-use: sending it a second time is refused withCONSENT_REUSED, so rundescribeagain for the next call. The stdio route issues grants without a nonce. - A grant is never inferred from earlier calls or from running on localhost, and the plugin checks it itself whatever the client sends. More in 🔐 Security.
Optional controls go in a top-level options object, never inside params:
| Option | Effect |
|---|---|
idempotencyKey |
1-128 characters. Repeating an execute with the same key and parameters returns the first receipt instead of running again, so retries are safe. |
expectedCatalogRevision |
Refuse with STALE_STATE if the catalog changed since you read it |
expectedRevisions |
{ selection, level, assetRegistry, package } counters from a receipt's liveRevisions or the ue://state/revisions resource. Refuse with STALE_STATE if any moved. |
timeoutMs |
Deadline for this call, 1 to 600,000 ms |
Anything else is refused with UNSUPPORTED_OPTION. Without timeoutMs, each capability gets a budget from its declared cost. ⚙️ Configuration › Timeouts has the details, including the 5-minute ceiling per call.
configure runs one manage_tools action. It changes which internal tools execute may use. The visible tool list never changes: it is always just unreal.
{ "operation": "configure", "action": "disable_category", "params": { "category": "gameplay" } }| Action | Params |
|---|---|
get_status · list_tools · list_categories
|
none |
enable_tools · disable_tools
|
tools: array of internal tool names |
enable_category · disable_category
|
category: core, world, gameplay, utility or all
|
reset |
none. Re-enables everything. |
All 23 tools start enabled. inspect and manage_tools can't be disabled, so the core category can't be switched off as a whole. A disabled tool's capabilities still show up in search, marked unavailable, and execute refuses them with TOOL_DISABLED. On the native route, turning off Load All Tools on Start starts with only the core tools enabled.
Every failure carries errorCode, message and an executable nextCall; validation errors add pointer, naming the offending field. A misspelled parameter on a read-only call, for example:
{ "operation": "execute", "tool": "control_actor", "action": "list", "params": { "colour": "red" } }{
"success": false,
"errorCode": "UNDECLARED_PARAMETER",
"message": "Undeclared parameter 'colour' (allowed: className, filter, folder, limit, near, offset, propertyNames, radius, summary, tag)",
"pointer": "/colour",
"nextCall": { "operation": "describe", "tool": "control_actor", "action": "list" }
}| Code | Meaning | What to do | |
|---|---|---|---|
| 🔤 |
UNKNOWN_TOOL · UNKNOWN_ACTION · UNKNOWN_CAPABILITY
|
A name was guessed |
search again |
| 🧾 |
UNDECLARED_PARAMETER · MISSING_REQUIRED_PARAMETER · MISSING_REQUIRED_ONEOF · INVALID_PARAMETER_VALUE · INVALID_PARAMETER_TYPE
|
Parameters don't match the contract | Read pointer and the message, or describe again |
| ✋ |
CONSENT_REQUIRED · CONSENT_REUSED
|
The capability needs a (fresh) consent grant |
describe for a grant, send it once |
| 🔌 | NOT_CONNECTED |
The editor isn't reachable | Start the editor; see 🩺 Troubleshooting |
| 🚫 | TOOL_DISABLED |
Switched off with configure
|
configure › enable_tools or reset
|
| 🔐 |
SCOPE_NOT_GRANTED · PATH_NOT_PERMITTED · PROJECT_NOT_PERMITTED · COMMAND_BLOCKED · QUOTA_EXCEEDED
|
Refused by the security policy | See 🔐 Security |
| ⏳ | STALE_STATE |
A revision pin in options no longer matches |
Re-read, then retry |
| 📏 | RESULT_TOO_LARGE |
The reply would exceed 100,000 characters (6,000,000 for images) | Narrow the request with filters or limits |
| ⚙️ | UNSUPPORTED_OPTION |
Unknown key in options
|
Use only the four options above |
| 🪟 | EDITOR_BLOCKED |
The editor's game thread stalled for over 15 s, usually a modal dialog | Dismiss the dialog in the editor |
| 🎮 | Handler codes: ASSET_NOT_FOUND, MESH_NOT_FOUND, DELETE_PARTIAL, … |
The editor refused, or only partly did, the work | Read message; it names the cause |
Before 0.6, clients called manage_asset, control_actor and the rest directly. Such a call is no longer executed, on either route. It returns a receipt whose nextCall is the same request through the gateway:
{
"success": false,
"errorCode": "DIRECT_TOOL_CALL_REMOVED",
"tool": "manage_asset",
"message": "Direct tool calls are removed. Call the 'unreal' gateway with operation 'execute' to run 'manage_asset.list'.",
"nextCall": { "operation": "execute", "tool": "manage_asset", "action": "list", "params": { "path": "/Game" } }
}More in ⬆️ Upgrading.
Both routes serve the same catalog and enforce the same rules; they differ in a few shapes. Following nextCall works on both.
| 🌐 Native HTTP | 🧩 stdio | |
|---|---|---|
| Name a capability |
tool + action
|
tool + action, or capability (the id, e.g. control_actor.spawn) |
describe with no selector |
Lists the 23 internal tools | Lists the 40 domains; domain and family then browse one level at a time |
describe reply |
The contract | The contract, plus a nextCall for execute |
| Search row fields |
parent, available, score, matchReasons
|
parentTool, availability, policy, runnable, reasons
|
| Consent grant | Includes a single-use nonce
|
capability + acknowledge
|
| Validation errors | pointer |
pointer, plus suggestions and allowedParameters
|
| Tip | |
|---|---|
| 🎯 | Ask for outcomes, not tool calls. "Add a red point light above the door and make it flicker" works better than naming capabilities; the assistant searches for each step itself. |
| 📁 |
Give real paths. Assets live under /Game/... (not /Content/...), and classes look like /Script/Engine.PointLight. |
| 🔍 |
Ask it to check its work. A screenshot (control_editor.screenshot), a property read (inspect), or control_actor.audit_placement for actors sunk into the floor or floating catch mistakes that a successful reply won't. |
| 📦 |
Batch big jobs. Spawning hundreds of actors, or building a whole Blueprint event graph (manage_blueprint.build_graph), is one call rather than hundreds. |
| ↩️ |
Undo exists. control_editor.undo and redo step through the editor's transaction history, like Ctrl+Z. |
| 🧭 | If the model keeps inventing names, tell it once: "search first, then describe, then execute, and copy each nextCall." The server's instructions say the same, but not every client passes them on. |
📖 This wiki covers the 0.6 line (dev branch, npm @beta) · ✏️ Something wrong or missing? Open an issue or start a discussion
🏠 Home
Get started
🚀 Quick Start
📦 Installation
🔌 Connecting Clients
Use it
🧭 Using the Gateway
🧰 Tools Reference
📚 Resources and Prompts
Set it up
⚙️ Configuration
🔐 Security
Help
🩺 Troubleshooting
💬 FAQ
⬆️ Upgrading from 0.5.x
Contribute
🛠️ Development
Covers the 0.6 line · Releases · Discussions