UE-MCP

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 = true

Where to Put This

ClientConfig File
Claude Code.mcp.json in project root, or ~/.claude/ global config
Claude Desktopclaude_desktop_config.json
Cursormcp.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.

ActionWhat 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 Beta

The 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 init includes a "Native Unreal tools (Epic 5.8)" page where you enable the feature and optionally exclude specific categories. The choice is written to nativeTools in ue-mcp.yml.
  • Requires UE 5.8+ with the ToolsetRegistry plugin (and the toolset plugins you want) enabled in your project. On older engines or when the plugin is absent, enrichment is skipped and epic(status) reports available: 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.

ModeWhat happens to a blocking dialog
interactiveYou 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.
autoThe 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.
deferThe 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/:

PathWhat
~/.ue-mcp/state.jsonThree 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.jsonCached GitHub OAuth token for feedback(submit) author=user mode. Mode 600. Written by npx ue-mcp auth.
~/.ue-mcp/pending-feedback/[id].jsonSubmissions 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 range

At 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/. Use ue-mcp plugin install [name] to add an entry and run npm install --save in one step.
  • Plugins are loaded only when the server boots - edit the array and restart your MCP client (/mcp in 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 plugins tool 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).

StrategySeed (test project)What's advertisedCost to use
full (default)~45k tokensall 26 category tools, every action + parameter inlinezero discovery calls
lean~23k tokensthe 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 tokensa single tools gateway - search, list_categories, describe, and call - fronting every categorydiscovery 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 calls tools(action="search", query="rotate clockwise"), then tools(action="describe", category="level", method="nudge_component"), then tools(action="call", category="level", method="nudge_component", args=( ... )). Omit method from 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 strategy

npx ue-mcp init also has a Context strategy page. Or edit ue-mcp.yml directly:

ue-mcp:
  context:
    strategy: micro

Or 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: 50123

Both 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:

RankSourceNotes
1-MCPPort=NNNN on the editor command line, or an explicit port argument to the clientPer launch
2UE_MCP_PORTPer environment. Applies to every project the shell starts, which is why it is the wrong tool for pinning one project
3bridge.port in the config layersPer project, and the one to reach for
4The port derived from the project root pathThe 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.port that is not a whole number in 1-65535 is 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.json whatever 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_editor confirms 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.

FileWritten whenContents
port.jsonThe bridge is listeningport, pid, instanceId, startedAt, status, protocolVersion, handlerApiVersion
bridge-error.jsonThe editor came up but the bridge could not bindstatus: "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

StateMeaning
ConnectedBridge is active, all tools available
DisconnectedEditor not running or plugin not loaded. Filesystem tools still work (INI parsing, C++ headers, asset listing)
ReconnectingConnection 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:

  1. Copies plugin/ue_mcp_bridge/[Project]/Plugins/UE_MCP_Bridge/
  2. Adds UE_MCP_Bridge to the .uproject plugins list
  3. Enables PythonScriptPlugin if not already enabled (needed for execute_python escape 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 - for editor(action="execute_python")
  • EnhancedInput - for input action/mapping creation
  • GameplayAbilities - for GAS tools
  • Niagara - for VFX tools
  • PCG - for procedural generation tools

CLI Subcommands

npx ue-mcp exposes a few utility subcommands beyond the default MCP server entry:

CommandDescription
npx ue-mcp initInteractive 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 updateCheck npm for the latest version and install it. Pass --deploy to also redeploy the plugin sources.
npx ue-mcp deployCopy the C++ bridge plugin sources into the project. Use after ue-mcp update or to force a redeploy.
npx ue-mcp buildBuild the project C++ code using Unreal Build Tool. Stop the editor first.
npx ue-mcp authRun 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-hooksRemove 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 | reviewManage 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:

CommandDescription
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:

  1. UE_MCP_TEST_ENGINE_ROOT - an engine root pinned for the whole process.
  2. UE_BUILD_TOOL_PATH - a Build.bat / Build.sh pinned for the process.
  3. editor.buildToolPath in the project's ue-mcp.yml - the per-project form.
  4. UE_EDITOR_PATH, then editor.path - an editor binary names its own tree.
  5. EngineAssociation read as a path, absolute or relative to the project.
  6. 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]/Engine next to [stream]/MyProject/MyProject.uproject.
  7. EngineAssociation as a registered build (a GUID, looked up in HKCU\Software\Epic Games\Unreal Engine\Builds) or a version string such as 5.8, looked up as a launcher install.
  8. The engine the project's own log says it was last opened with.
  9. 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.

UE-MCP is an independent, community-built project. It is not affiliated with, sponsored by, or endorsed by Epic Games, Inc. Unreal® and Unreal Engine® are trademarks or registered trademarks of Epic Games, Inc. in the United States and elsewhere, used here to describe compatibility.