Configuration
Configuration documentation.
MCP Client Configuration
The easiest way to configure UE-MCP is to run npx ue-mcp init - it detects your MCP clients and writes the config automatically.
Manual Configuration
{
"mcpServers": {
"ue-mcp": {
"command": "npx",
"args": ["ue-mcp", "C:/path/to/MyGame.uproject"]
}
}
}Codex uses TOML instead:
[mcp_servers.ue-mcp]
command = "npx"
args = ["ue-mcp", "C:/path/to/MyGame.uproject"]
cwd = "C:/path/to"
enabled = trueWhere to Put This
| Client | Config File |
|---|---|
| Claude Code | .mcp.json in project root, or ~/.claude/ global config |
| Claude Desktop | claude_desktop_config.json |
| Cursor | mcp.json in .cursor/ or project root |
| Codex | ~/.codex/config.toml |
Without a Project Path
You can start the server without a .uproject argument. It will run in a limited mode - you can then use project(action="set_project", projectPath="...") at runtime to attach to a project.
Several Editors From One Server
Pass more than one .uproject and each becomes an addressable editor session with its own bridge connection, its own port and its own port lockfile:
{
"mcpServers": {
"ue-mcp": {
"command": "npx",
"args": ["ue-mcp", "C:/games/Alpha/Alpha.uproject", "C:/games/Beta/Beta.uproject"]
}
}
}Sessions are keyed by resolved project root, so one project is one session however its path is spelled, and two editors of the same project are never addressed as two sessions.
| Action | What it does |
|---|---|
project(action="list_editors") | Every session: name, project, bridge port, socket state, liveness, and which one untargeted calls use |
project(action="add_editor", projectPath="...") | Register another project at runtime. start: true also launches its editor and waits until it is ready |
project(action="use_editor", editorTarget="Beta") | Move the default target |
project(action="drop_editor", editorTarget="Beta") | Forget a session and close its socket. The editor keeps running |
While more than one session is registered, every category tool, the flow tool and the HTTP run route accept an editor parameter naming the session, the project, or a .uproject path:
level(action="place_actor", editor="Beta", assetPath="/Game/Rock", location=[0,0,0])
editor(action="start_editor", editor="Beta")
flow(action="run", flowName="build_and_check", editor="Beta")Calls with no editor run in the active session, which is the first project on the command line until use_editor moves it.
One editor is unchanged. The editor parameter is advertised only while a second session exists, so a single-editor client sees exactly the schema, the status response and the events it always has. Registering or dropping a session re-advertises the tool list, so a client that honours tools/list_changed can target an editor it registered at runtime.
Each project needs its own port. Ports are derived from the project root, so this works out of the box. Pinning every project to the same port (the same bridge.port in two ue-mcp.yml files, or a global UE_MCP_PORT) collapses the sessions onto one address, and a call targeted at one of them can then be served by the other. list_editors reports the clash under portSharedWith. Give each project its own port, or leave them derived.
Lifecycle actions act only on the editor you addressed. start_editor, stop_editor and restart_editor resolve their target from the addressed session's own project: its port lockfile, and the PID that lockfile names, checked against the .uproject that process has open. See Which editor lifecycle actions act on. drop_editor detaches only: stopping an editor is always an explicit editor(action="stop_editor") against that session.
Multi-editor needs a bridge built from the current plugin source in each project involved. A project whose bridge is missing or stale is still registered and still startable; project(action="list_editors") reports it.
The CLI takes a target too. Every one-shot subcommand accepts --editor <name-or-path>, resolved as a session name first and a project path second:
npx ue-mcp deploy --editor Beta
npx ue-mcp build --editor Beta
npx ue-mcp doctor --editor Beta
npx ue-mcp context lean --editor Beta
npx ue-mcp feedback list --editor BetaThe name is the one project(action="list_editors") reports, read back from the argv in your MCP client config, so a name means the same editor on the command line that it means in a tool call. Without the flag every subcommand behaves exactly as it did: positional path first, then the project in the current directory.
Project Configuration (ue-mcp.yml)
Project config lives in ue-mcp.yml next to your .uproject, tracked in git so every collaborator shares the same surface. npx ue-mcp init scaffolds and maintains it.
ue-mcp:
version: 1
contentRoots: [/Game/, /MyPlugin/]
disable: [gas]
nativeTools: { enabled: true }
http: { enabled: false }
context: { strategy: full }
tasks: {}
flows: {}
plugins: []The file is one layer in a deep-merged stack (user-global → project → env → local → env vars). For the full anatomy, the layer cascade, where each setting belongs, and every ue-mcp: key, see the dedicated ue-mcp.yml Reference. The behavioral deep-dives for two of its keys live below: Native Epic tools (nativeTools) and Context strategy (context).
Native Epic 5.8 tools
Unreal Engine 5.8 ships an experimental AI Toolset Registry (the plugin behind Unreal's own MCP server). ue-mcp reaches that registry in-process and surfaces every official toolset as first-class actions inside the matching ue-mcp category - Epic's GAS tools appear in gas, Niagara in niagara, and so on - so an agent discovers them in context. Toolsets with no natural home are reachable through the epic gateway (status / list_toolsets / describe_toolset / call_tool).
- On by default.
npx ue-mcp initincludes a "Native Unreal tools (Epic 5.8)" page where you enable the feature and optionally exclude specific categories. The choice is written tonativeToolsinue-mcp.yml. - Requires UE 5.8+ with the
ToolsetRegistryplugin (and the toolset plugins you want) enabled in your project. On older engines or when the plugin is absent, enrichment is skipped andepic(status)reportsavailable: false. - Deterministic surface. The catalog is sourced from the live editor when connected, falling back to a per-project cache and then a snapshot baked into the ue-mcp package, so the wrapped tools appear even on a cold first start and match the generated tool reference (both are built from the same snapshot). The 🧩 badge in the tool reference marks every wrapped official tool.
To turn it off entirely, set nativeTools.enabled: false (the epic gateway stays available). To keep it on but drop a noisy domain, add that category to nativeTools.exclude.
The feedback approval mode (interactive / auto-approve / defer) is intentionally not in ue-mcp.yml - it varies per developer and per machine, so it lives in ~/.ue-mcp/state.json and is managed with npx ue-mcp feedback mode ... or the UE_MCP_FEEDBACK_MODE env var. See Feedback → modes.
Dialog handling mode
A modal dialog blocks Unreal's game thread, so every action is refused until it is answered. One guard per editor enforces that, and every route to the editor passes it, including each step of a running flow and the first call the server makes. This mode decides what happens next.
It lives with the feedback mode and for the same reason: whether somebody is at the keyboard to answer a modal is a property of your machine, not project policy a collaborator should inherit.
Who may press is enforced, not just described. editor(action='respond_to_dialog') is accepted only in auto. Under interactive and defer it comes back refused like any other action, because the answer belongs to the person: interactive asks them in a form, defer waits for them at the editor's own window. editor(action='list_dialogs') stays available in every mode, so the dialog can always be READ; only the press is withheld.
editor(set_dialog_policy) is the exception. A dialog matching an armed pattern is answered immediately under every mode. Nothing else presses a button on its own, and there are no built-in policies.
| Mode | What happens to a blocking dialog |
|---|---|
interactive | You get an elicitation form with the dialog's buttons as the choices, plus "leave it open", under the title "Submission". The button you pick is pressed and the blocked call then runs. A dialog that asks a question per row (the "Save Content" prompt is a checkbox per unsaved package) carries those rows in the same form as one checkbox group titled "Assets", ticked as the dialog has them, and what you tick travels with the button in one press. The agent cannot press it for you: editor(respond_to_dialog) is refused in this mode. |
auto | The refusal includes the editor(action='respond_to_dialog') call for each button. The agent picks one and makes that call. Nothing is pressed until it does. |
defer | The refusal names the dialog and its buttons but not the calls that press them, and editor(respond_to_dialog) is refused. Answer it in the Unreal Editor window. |
Read in this order (highest wins): the UE_MCP_DIALOG_MODE env var, dialog.mode for this project in ~/.ue-mcp/state.json, dialog.mode for this user in the same file, then the default. The default is interactive when your MCP client advertises the elicitation capability and defer when it does not. It never resolves to auto: with no channel to a person, the fallback is the mode that suspends, not the one that lets the agent decide. auto applies only when you name it.
What counts as blocking: the modal stack first, then every top-level window and its children. A window blocks when it is a regular window that is either modal or parented to another one. Menus, tooltips and notifications are not regular windows, so a hover is never mistaken for a question. Visibility is not consulted, because a window that is up but undrawn, or drawn behind the editor, still holds whatever raised it. The detection lives in the C++ bridge, so a project running an older compiled plugin keeps the detection it was built with until the plugin is redeployed and rebuilt.
Set it with npx ue-mcp dialog mode <interactive|auto|defer> (add --editor [name] to scope it to one project, default to clear it). An env value that names no mode is ignored, and the result says so.
Under interactive the dialog is handed back whole on one call and the form goes up on the next, so its text lands somewhere nothing truncates it. An elicitation form is a few lines tall and the client decides how many of them it draws; one that keeps the opening line or two and collapses the rest would otherwise collapse the question itself. dialogPhase on the refusal says which call you are on: relay or asking.
A client known to render the whole message skips that round trip. Every other client, including one nobody has checked, gets the handover: relaying needlessly costs one call, and not relaying against a client that collapses the message asks someone to choose a button for a question they cannot see. UE_MCP_DIALOG_RELAY=off turns the handover off whatever the client is, and =on forces it back on.
The form outlives the call that raised it. A blocked call waits a minute for your answer and then refuses, so one open form does not hold every other call behind it, but the form stays up and the button you pick is still pressed whenever you get to it. The screen is re-read first, so an answer that arrives after the dialog was closed at the editor's own window, or after a different prompt replaced it, presses nothing.
User-machine state (~/.ue-mcp/)
Machine-specific state that ue-mcp commands write but you wouldn't hand-edit lives under ~/.ue-mcp/:
| Path | What |
|---|---|
~/.ue-mcp/state.json | Three things: (a) per-project installedHooks - absolute paths of every Claude Code settings.json where ue-mcp installed the feedback PostToolUse hook, keyed by absolute project root; (b) preferences.feedback.mode - your personal default for the feedback approval mode (interactive / auto-approve / defer). (c) preferences.dialog.mode and per-project dialog.mode - the dialog handling mode (interactive / auto / defer). Maintained by npx ue-mcp init, npx ue-mcp uninstall-hooks, npx ue-mcp feedback mode and npx ue-mcp dialog mode. Written by those commands, not by hand. |
~/.ue-mcp/auth.json | Cached GitHub OAuth token for feedback(submit) author=user mode. Mode 600. Written by npx ue-mcp auth. |
~/.ue-mcp/pending-feedback/[id].json | Submissions captured while feedback mode is defer. Acted on with npx ue-mcp feedback list/approve/discard. |
These files never need to be in your project tree or in version control.
Plugins
The plugins: array in ue-mcp.yml declares npm packages that inject new actions into existing built-in categories. The full author contract lives in Plugins; this is the consumer view.
plugins:
- name: pie-studio
- name: some-other-plugin
version: "0.2.x" # optional - npm semver rangeAt server start, ue-mcp resolves each entry against the project's node_modules/, validates the plugin manifest, and merges its injected actions into the host category tools. Stay-on-disk facts:
- The package must already be installed under
[project]/node_modules/. Useue-mcp plugin install [name]to add an entry and runnpm install --savein one step. - Plugins are loaded only when the server boots - edit the array and restart your MCP client (
/mcpin Claude Code). - A plugin that fails validation is skipped with a loud warning. Other plugins keep loading; the host tools are never partially mutated.
- Use the
pluginstool to introspect the loaded set:plugins(action="list")- name, version, prefix, status, injected count, host UE plugin presence.plugins(action="describe", name="[package]")- full detail including injected actions, knowledge files, and flows.
Order matters: earlier entries win on inter-plugin action-name collisions. A plugin action can never overwrite a built-in.
Host UE plugin dependencies
A plugin can declare uePluginDependency: [PluginName] in its ue-mcp.plugin.yml. The MCP server checks the project's .uproject for Plugins[].Name == "[PluginName]" and exposes the result as uePluginPresent in plugins(action="list"). The npm side loads regardless - the flag is a signal that the host UE plugin needs to be enabled before the injected actions will actually run.
For example, a plugin that declares uePluginDependency: SomePlugin will report uePluginPresent: false until SomePlugin is added to [Project].uproject's Plugins array and the C++ modules are built.
Context strategy (full, lean, micro)
Everything the server injects at session start - the initialize instructions plus the whole tools/list payload (names, descriptions, and parameter schemas) - is the "context tax". Three strategies trade that seed cost against how many discovery round-trips an agent makes. Measure the tax on your own project with npm run context-tax (set ANTHROPIC_API_KEY for exact token counts).
| Strategy | Seed (test project) | What's advertised | Cost to use |
|---|---|---|---|
full (default) | ~45k tokens | all 26 category tools, every action + parameter inline | zero discovery calls |
lean | ~23k tokens | the same 26 tools with their validated action enums, but descriptions collapsed to a summary; a catalog tool (search / describe / list_categories) and a per-category describe action serve the details on demand | ~1 round-trip to learn a category |
micro | ~1k tokens | a single tools gateway - search, list_categories, describe, and call - fronting every category | discovery for everything |
- full is best when the agent should see the entire surface up front and you are not token-constrained.
- lean keeps action names visible (so the model can often call directly, and unknown actions are still rejected up front) while dropping the prose. A solid middle ground.
- micro mirrors the native MCP toolset gateway (
list_toolsets/describe_toolset/call_tool): the agent callstools(action="search", query="rotate clockwise"), thentools(action="describe", category="level", method="nudge_component"), thentools(action="call", category="level", method="nudge_component", args=( ... )). Omitmethodfrom describe to list a whole category. Smallest possible seed, most discovery traffic. - Lean reaches the same search and single-action describe through
catalog. Both compact modes rank with the full-mode intent search and read nested argument fields off the declared schemas, so a narrow lookup never costs a whole category dump.
The seed figures are measured on this repo's test project and move with the
plugins and Epic toolsets you have enabled - npm run context-tax reports
yours. A smaller seed is not automatically a cheaper session: count the
discovery round-trips as well. All three modes carry the same short spatial
interpretation and verification guidance.
Set the strategy with the standalone command (writes ue-mcp.yml for you):
npx ue-mcp context full # every action inline (default)
npx ue-mcp context lean # names visible, descriptions on demand
npx ue-mcp context micro # one gateway tool fronts everything
npx ue-mcp context # show the current strategynpx ue-mcp init also has a Context strategy page. Or edit ue-mcp.yml directly:
ue-mcp:
context:
strategy: microOr per session, without editing the file: UE_MCP_CONTEXT_STRATEGY=micro (the env var wins over the config value). Anything other than lean or micro resolves to full. Restart your MCP client (/mcp in Claude Code) after changing the strategy.
Bridge Connection
The C++ plugin listens on a per-project WebSocket port derived from a hash of the project root path (in the IANA ephemeral range 49152-65535). Deriving the port from the path means two checkouts of the same project - or several unrelated projects - on one machine each get a stable, launch-order-independent port, so their MCP clients never collide on a single fixed number. The Node client and the C++ bridge compute the identical value independently, and the bridge also publishes the actual bound port to [project]/Saved/UE_MCP_Bridge/port.json as the authoritative source (if the port is already taken, the bridge probes upward and the lockfile records where it really landed). The legacy fixed port 9877 remains the fallback when no project root is known. The MCP server auto-connects on startup and reconnects every 15 seconds if the connection drops.
On Windows the listening socket is claimed with SO_EXCLUSIVEADDRUSE, so a second editor of the same project cannot bind the same port. It walks upward instead and publishes where it landed, which is what lets two editors of one project coexist without their clients reaching the wrong one.
Pinning the port
Pin an explicit port with bridge.port in ue-mcp.yml:
ue-mcp:
bridge:
port: 50123Both halves honour it. The client reads the key when it chooses where to connect, and the editor-side plugin reads it when it chooses where to listen, so a pinned project has one number on both ends (#819). Precedence is identical on both sides, highest first:
| Rank | Source | Notes |
|---|---|---|
| 1 | -MCPPort=NNNN on the editor command line, or an explicit port argument to the client | Per launch |
| 2 | UE_MCP_PORT | Per environment. Applies to every project the shell starts, which is why it is the wrong tool for pinning one project |
| 3 | bridge.port in the config layers | Per project, and the one to reach for |
| 4 | The port derived from the project root path | The default when nothing is pinned |
The plugin reads bridge.port from the same layered files as the client, and honours the same winner: ~/.ue-mcp/config.yml, then [project]/ue-mcp.yml, then [project]/ue-mcp.(env).yml when UE_MCP_ENV is set, then [project]/ue-mcp.local.yml. See Config layering.
Two things to know about a pinned port:
- The lockfile still wins at connect time. A pin is a request, not a guarantee. If something else already holds the port, the bridge walks upward, binds what it can, logs a warning naming the port you asked for and the one it took, and publishes the port it actually bound. The client reads
port.json, so it follows. - A value the plugin cannot use is announced, not applied. A
bridge.portthat is not a whole number in1-65535is logged as a warning and the derived port is used instead. The plugin reads this one key with a small purpose-built reader rather than a full YAML parser (Unreal ships none, and one integer is not worth a dependency), so it handles plain nested keys and refuses anything more exotic. Written the ordinary way, as in the example above, it is read. Written as a flow mapping (bridge: ( port: 50123 )) or with anchors, it is not, and the editor log says so.
Which editor lifecycle actions act on
editor(start_editor), editor(stop_editor) and editor(restart_editor) act on a process, so they resolve their target more strictly than ordinary tool calls (#819):
- The port comes from the lockfile and nowhere else. No environment variable, no derived value, no
9877. The bridge writes[project]/Saved/UE_MCP_Bridge/port.jsonwhatever port it binds and removes it on exit, so it is the only file that says where this project's editor is listening. When it is absent, the action fails and names the path it checked instead of probing a port some other project's editor could answer on. - The process check is scoped by
.uproject. "Is an editor running" is never the question; "is the editor holding this project open running" is. A second editor, a headless shard, or somebody else's project no longer blocks a launch. - Stopping verifies before it acts. The lockfile records the editor's PID, and a lockfile outlives a crash, so
stop_editorconfirms that PID is still an editor for this project before it sends the quit request. - No project loaded means no lifecycle action. With nothing loaded there is no editor these calls could be about, so they say so rather than picking one.
Consequence worth knowing: if an editor is killed rather than closed, its lockfile stays behind and stop_editor reports the stale PID it names. Delete [project]/Saved/UE_MCP_Bridge/port.json to clear it.
Bridge state files
The bridge keeps two records under [project]/Saved/UE_MCP_Bridge/. Both are published by rename, so a client polling them never reads a half-written file.
| File | Written when | Contents |
|---|---|---|
port.json | The bridge is listening | port, pid, instanceId, startedAt, status, protocolVersion, handlerApiVersion |
bridge-error.json | The editor came up but the bridge could not bind | status: "bind-failed", the port range tried, the socket error, pid, instanceId |
instanceId identifies the server object that wrote the record, and only that instance removes it. A pid alone is not enough, since pids are recycled: an editor that failed to start can no longer delete the record of one that is running, and a second instance of a project cannot overwrite the first's. The pid is what lifecycle actions check before they act on a record, as described above.
bridge-error.json is what distinguishes "no editor" from "editor running, bridge dead". When a connection fails and this record names a live process, the client quotes its detail in the error.
Connection States
| State | Meaning |
|---|---|
| Connected | Bridge is active, all tools available |
| Disconnected | Editor not running or plugin not loaded. Filesystem tools still work (INI parsing, C++ headers, asset listing) |
| Reconnecting | Connection lost, auto-retry in progress |
Who may connect
The bridge binds loopback only and refuses every upgrade that carries an Origin header. Browsers always send one on a WebSocket upgrade and cannot suppress or forge it, so this keeps out any page served by a local dev server, which would otherwise be able to scan the port range and call execute_python. Native clients (the npm client, curl, editor tooling) omit the header and are unaffected.
Check the current state with project(action="get_status"). Alongside editorConnected, it reports editorTarget: the .uproject the connection belongs to, the port, and how that port was chosen (lockfile, config, derived, env, explicit). The connection is always bound to one project, so editorTarget.projectPath matches the loaded project.
Switching Projects
project(action="set_project", projectPath="...") moves path resolution and the editor connection together. The socket to the previous project's editor is dropped before the new project is loaded, so no action can execute in the project you just left.
The port for the new project is chosen from that project alone: its Saved/UE_MCP_Bridge/port.json lockfile first, then its own bridge.port, then the port derived from its root path.
A port pinned with UE_MCP_PORT (or an explicit port argument) is the exception. It was chosen for whichever project the server started on and says nothing about the one you switched to, so if the new project has published no lockfile the switch still completes and the connection is refused with an explanation. Start the new project's editor, which publishes the lockfile, or clear the pin so each project gets its derived port.
Plugin Deployment
On first run with a project path, the server automatically:
- Copies
plugin/ue_mcp_bridge/→[Project]/Plugins/UE_MCP_Bridge/ - Adds
UE_MCP_Bridgeto the.uprojectplugins list - Enables
PythonScriptPluginif not already enabled (needed forexecute_pythonescape hatch)
The plugin is editor-only and has no runtime footprint.
Plugin Dependencies
The C++ bridge plugin enables these UE plugins (adding them to .uproject if missing):
PythonScriptPlugin- foreditor(action="execute_python")EnhancedInput- for input action/mapping creationGameplayAbilities- for GAS toolsNiagara- for VFX toolsPCG- for procedural generation tools
CLI Subcommands
npx ue-mcp exposes a few utility subcommands beyond the default MCP server entry:
| Command | Description |
|---|---|
npx ue-mcp init | Interactive setup wizard. Deploys the C++ bridge plugin, writes MCP client configs, scaffolds ue-mcp.yml, optionally installs Claude Code skills + feedback prompt hook, optionally runs the GitHub OAuth device flow. Migrates any legacy .ue-mcp.json / ue-mcp.local.yml it finds. |
npx ue-mcp update | Check npm for the latest version and install it. Pass --deploy to also redeploy the plugin sources. |
npx ue-mcp deploy | Copy the C++ bridge plugin sources into the project. Use after ue-mcp update or to force a redeploy. |
npx ue-mcp build | Build the project C++ code using Unreal Build Tool. Stop the editor first. |
npx ue-mcp auth | Run the GitHub device flow standalone so feedback(submit) can author issues as your real GitHub user. Same step that lives inside init; use this if you skipped it at init time. |
npx ue-mcp uninstall-hooks | Remove the feedback PostToolUse hook from every Claude Code settings file recorded for this project in ~/.ue-mcp/state.json. |
npx ue-mcp dialog mode [[mode]] | Read or set how a modal dialog blocking the editor is handled (interactive, auto, or defer). --editor [name] scopes it to one project; default clears it. Stored in ~/.ue-mcp/state.json. See Dialog handling mode. |
npx ue-mcp feedback mode [[mode]] | Read or set your personal feedback approval mode (interactive, auto-approve, or defer). Stored in ~/.ue-mcp/state.json. See Feedback → modes. |
npx ue-mcp feedback list | show | approve | discard | review | Manage submissions queued while feedback mode is defer. review (experimental) walks the queue interactively (approve/discard/skip per item). See Feedback → Reviewing deferred submissions. |
npx ue-mcp resolve [issue] | Fetch a feedback issue, branch, hand it to Claude Code to implement, open a PR. See Feedback. |
npx ue-mcp plugin install [name] | Install a ue-mcp plugin from npm and register it in ue-mcp.yml. See Configuration → Plugins. |
npx ue-mcp plugin uninstall [name] | Inverse of install. |
npx ue-mcp plugin create [name] | Scaffold a new plugin package. See Plugins. |
npx ue-mcp context [full|lean|micro] | Read or set the context strategy in ue-mcp.yml. No argument prints the current strategy. |
Every subcommand above also accepts --editor <name-or-path> to pick which of several editors it acts on. See Several Editors From One Server.
Editor Lifecycle
The server can manage the editor process:
| Command | Description |
|---|---|
editor(action="start_editor") | Launch UE with the current project |
editor(action="stop_editor") | Gracefully stop the editor |
editor(action="restart_editor") | Stop and relaunch |
editor(action="build_project") | Build the project C++ code via UBT |
project(action="build") | The same build, with configuration, platform and clean parameters |
Both builds run UnrealBuildTool as a separate process, so they work with the editor stopped. That is the case that matters: UnrealBuildTool cannot link while an editor holds the module DLLs, so a full rebuild has to happen with the editor down.
Build parallelism
UnrealBuildTool compiles one file per physical core by default, and each of those processes maps Unreal's precompiled header, which costs several GB. On a machine with fewer GB than cores, the compiler does not wait for room: it fails with C3859 or C1076 several minutes in, and the message names a paging file, so it reads as a broken machine rather than a default that does not fit it.
ue-mcp computes a safe number from the machine's memory and core count and passes it to every build, including ue-mcp build, ue-mcp update --build and editor(build_project). The cap is applied only when it is below what UnrealBuildTool would have chosen, so a machine with headroom builds exactly as before. There is nothing to configure.
Set UE_MCP_MAX_PARALLEL_ACTIONS to a positive integer to override it, for a machine where the estimate is wrong in either direction. A build that runs out of room anyway reports what happened and what to change, rather than leaving the compiler's wording as the only explanation.
Which Engine A Project Uses
The engine is resolved once, and the same answer drives the build tool, the
editor launch, engine plugin discovery and the engine-source readers
(project(find_engine_symbol), read_engine_header, search_engine_cpp,
list_engine_modules). Order, most specific first:
UE_MCP_TEST_ENGINE_ROOT- an engine root pinned for the whole process.UE_BUILD_TOOL_PATH- aBuild.bat/Build.shpinned for the process.editor.buildToolPathin the project'sue-mcp.yml- the per-project form.UE_EDITOR_PATH, theneditor.path- an editor binary names its own tree.EngineAssociationread as a path, absolute or relative to the project.- An engine tree beside or above the project: the first directory from the
project upward that holds
Engine/Build/BatchFiles/. This is the layout a Perforce stream produces, with[stream]/Enginenext to[stream]/MyProject/MyProject.uproject. EngineAssociationas a registered build (a GUID, looked up inHKCU\Software\Epic Games\Unreal Engine\Builds) or a version string such as5.8, looked up as a launcher install.- The engine the project's own log says it was last opened with.
- The default launcher install locations.
UE_MCP_PROTECTED_ENGINE_ROOTS outranks all of it. An engine equal to or nested
under a protected root is refused, including one that arrives through an
explicit pin. Entries are separated by the platform path-list delimiter (; on
Windows, : on macOS and Linux) and must be absolute.
When nothing resolves, the error lists every path that was probed and where each came from, so a missing engine, a wrong root and an unsupported layout do not all read the same.