-
Notifications
You must be signed in to change notification settings - Fork 171
Security
This server lets a program drive your editor: create, change and delete assets, run console commands, even run Python. The defaults keep that to your own machine and to clients that hold your project's token.
On this page · Defaults · How a request is checked · Capability tokens · LAN access · Scopes · Consent · Other safeguards · Refusal codes · Reporting
| Area | Default | |
|---|---|---|
| 🏠 | Network | Both routes listen on 127.0.0.1 only. The WebSocket listener is on 8090 and 8091; the native HTTP server is off, and uses port 3000 when on. |
| 🔑 | Capability token | Required on both routes |
| ✋ | Consent | Required in every call to 62 capabilities: all deletes, plus some other writes |
| ⌨️ | Console commands | Filtered: no command chaining, no quit or crash commands |
| 📁 | Asset paths |
/Game, /Engine, /Script, /Temp, /Niagara, plus any you add |
| 🛡️ | The plugin's own settings | Out of reach: automation can't read or change them, so an assistant can't lift its own limits |
The plugin is the authority. The Node.js server checks requests early so it can fail fast, but the plugin re-checks every request, whichever route it came from, before it reaches the editor's queue. A refused request does no editor work at all.
flowchart TB
req(["Request from a client"]) --> tok["① Capability token"]
tok -- "ok" --> sc["② Scope"]
sc -- "ok" --> con["③ Consent"]
con -- "ok" --> pol["④ Project · paths · console · quota"]
pol -- "ok" --> q[["Editor queue · game thread"]]
tok -- "missing or wrong" --> r1["401 Invalid capability token"]
sc -- "not held" --> r2["SCOPE_NOT_GRANTED"]
con -- "no grant" --> r3["CONSENT_REQUIRED"]
pol -- "outside the rules" --> r4["PATH_NOT_PERMITTED<br/>COMMAND_BLOCKED · QUOTA_EXCEEDED"]
classDef check fill:#30363d,stroke:#8b949e,color:#f0f6fc
classDef bad fill:#da3633,stroke:#f85149,color:#ffffff
classDef good fill:#238636,stroke:#3fb950,color:#ffffff
class tok,sc,con,pol check
class r1,r2,r3,r4 bad
class q good
The first time the editor starts with the plugin, it generates a random 32-byte token and writes it, as 64 hex characters, to:
<YourProject>/Saved/MCP/capability-token
| Who | How the token is presented |
|---|---|
| 🌐 Native HTTP clients | In the X-MCP-Capability-Token header. Without it they get 401 Invalid capability token. |
| 🧩 The stdio server | Reads the file itself (it finds the project through UE_PROJECT_PATH) and presents the token in its WebSocket handshake. MCP_AUTOMATION_CAPABILITY_TOKEN overrides the file, for when the server can't read the project folder. |
| ✏️ Your own token | A value typed into Capability Token in the plugin settings replaces the generated file |
- Tokens are compared in constant time, and never written to logs, receipts or health output.
- A session can't switch tokens halfway through.
- The file is readable by your OS user, like the rest of the project. If
Saved/sits on a shared drive, tighten its permissions.Saved/is normally not committed; keep it that way.
To rotate the token, delete Saved/MCP/capability-token (or clear Capability Token), restart the editor, and update your HTTP clients. The native server notices a new file immediately; WebSocket clients pick it up after the restart.
Warning
Unticking Require Capability Token lets any program running on your machine drive the editor through either route, with no credentials. That can be acceptable on a single-user machine where nothing else runs. Keep it on in every other case, and always with LAN access (the native server refuses to bind to a LAN address without it).
Even with the token off, the native server refuses requests that come from web pages (requests with an Origin header). Web pages are only let through while token auth is on, so a website open in your browser can't reach the editor.
Both routes listen on loopback only. To reach the editor from another machine:
-
Plugin: turn on Allow Non Loopback and set Listen Host to the editor machine's LAN address (or
0.0.0.0). This one switch opens both the WebSocket listener and the native HTTP server. - Token: keep Require Capability Token on. Consider giving the remote client a scoped token rather than the main one.
- Firewall: restart the editor and allow the ports only from the machines that need them.
-
Client machine:
-
stdio route: set
MCP_AUTOMATION_ALLOW_NON_LOOPBACK=true,MCP_AUTOMATION_HOST=<editor address>andMCP_AUTOMATION_CAPABILITY_TOKEN=<token>. For encryption, turn on Enable TLS in the plugin (PEM certificate and key) and setMCP_AUTOMATION_USE_TLS=true. -
native route: point the client at
http://<editor address>:3000/mcpwith the token header.
-
stdio route: set
Caution
The native server speaks plain HTTP, so the token crosses the network unencrypted. Beyond a network you fully trust, reach it through an SSH tunnel or a TLS reverse proxy rather than exposing the port.
Every capability requires one of four scopes:
| Scope | Covers |
|---|---|
Read |
Capabilities that only look |
Write |
Capabilities that change things |
Destructive |
Capabilities that delete |
Admin |
Everything |
Membership is exact: holding Write does not grant Read; only Admin covers everything. An action the catalog doesn't know demands Admin, so unknown requests are refused by default. The main token (generated or typed in) is Admin.
For anything narrower, add entries under Security › Scoped Tokens › Scoped Capability Tokens:
| Field | Meaning |
|---|---|
| Profile | A label for the token |
| Token | The secret the client presents, exactly like the main token |
| Scopes | Any of Read, Write, Destructive. Admin is not allowed on a scoped token. |
| Allowed Path Prefixes | Confine the token to these content roots, e.g. /Game/Sandbox/
|
| Allowed Projects | Limit the token to these projects |
| Max Requests Per Minute · Max Tool Calls Per Minute | Per-minute quotas for this token, counted across both routes |
Typical uses: a read-only token for an assistant that should only look, a token confined to one folder for an experimental agent, or a rate-limited token on a shared machine. With a path-limited token, write targets must be named explicitly: when the plugin can't prove that a write stays inside the allowed prefixes, it refuses it.
62 capabilities declare a consent mode: explicit, or elevated for the most destructive, such as bulk deletes. A call to one of them must carry a grant that names that capability:
"consent": { "capability": "control_actor.delete", "acknowledge": "explicit", "nonce": "62A12661-4F6A-1C97-339E-69921EE9E2A7" }-
describereturns the exact grant, asconsentGrant. Send it back unchanged. - A grant covers one call to one capability. On the native route its
noncemakes it single-use: a replay is refused withCONSENT_REUSED. The stdio route issues grants without a nonce. - It is never inferred from localhost, from earlier calls, or from an idempotency key.
-
elevatedalso satisfies anexplicitrequirement, not the other way round. - Consent is not permission: the caller still needs the capability's scope.
- The plugin checks consent itself, on both routes.
Tip
Consent makes deleting work a deliberate step the model has to take, rather than a side effect of a loosely worded request. To approve such calls yourself, use your client's tool-approval setting for the unreal tool.
| Safeguard | What it does |
|---|---|
| 📁 Paths | Asset paths must fall under /Game, /Engine, /Script, /Temp or /Niagara, plus the prefixes in MCP_ADDITIONAL_PATH_PREFIXES. /Content/... maps to /Game/.... File-system paths are checked for traversal and symbolic links, once when accepted and again just before use. |
| ⌨️ Console commands | Command chaining (;, &&, ||, |, backticks, newlines) is refused, as are commands that quit or crash the editor (quit, exit, shutdown, crash, gpucrash, …) and attempts to reach Python or a shell through the console. |
| 🐍 Python |
system_control.execute_python exists. It needs Write scope and explicit consent per call, and scripts are limited to 1 MB. Anything that can run Python can do anything the editor can, so treat that consent as a real decision. |
| 🚦 Rate limits | Native HTTP allows 16 sessions and 32 connections, and per session 600 requests and 120 tool calls a minute. The WebSocket listener has optional per-client message limits (off by default). |
| Code | Meaning |
|---|---|
SCOPE_NOT_GRANTED |
The token doesn't hold the scope this capability requires |
CONSENT_REQUIRED |
The capability needs a consent grant, and no matching one was sent |
CONSENT_REUSED |
A single-use grant was sent a second time |
PATH_NOT_PERMITTED |
A path is outside the allowed prefixes, or couldn't be proven to be inside them |
PROJECT_NOT_PERMITTED |
The token isn't allowed on this project |
QUOTA_EXCEEDED |
The token's per-minute quota is used up. The one refusal worth retrying later. |
COMMAND_BLOCKED |
A console command matched the block list |
Important
Please report security problems privately, through GitHub's private vulnerability reporting, not in a public issue. Published advisories are listed under the repository's Security tab.
📖 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