UE-MCP

Architecture

Architecture documentation.

UE-MCP has two main components: a TypeScript MCP server that handles the AI protocol, and a C++ plugin that runs inside the Unreal Editor and exposes engine APIs over WebSocket.

MCP Server (TypeScript)

Entry point: src/index.ts

The server creates an McpServer instance (from @modelcontextprotocol/sdk), registers 26 category tools plus a flow tool, and communicates with the AI client over stdio.

Key Modules

ModulePurpose
index.tsTool registration, MCP server lifecycle
tools.tsThe ALL_TOOLS registry consumed by index.ts and tests
bridge.tsEditorBridge (implements IBridge) - WebSocket client, JSON-RPC messaging, auto-reconnect
project.tsProjectContext - path resolution, INI parsing, C++ header parsing
types.tsToolDef, ActionSpec, categoryTool() factory
schemas.tsShared Zod schemas - Vec3, Rotator, Color, Quat
errors.tsMcpError class with ErrorCode enum for structured error handling
deployer.tsFirst-run deployment: copy plugin, mutate .uproject
editor-control.tsStart/stop/restart the Unreal Editor process
instructions.tsAI-facing server instructions (embedded documentation)
auth.tsGitHub OAuth device flow + ~/.ue-mcp/auth.json token cache (default authorship path for feedback issues)
github-app.tsGitHub App auth used as the bot fallback when OAuth isn't authorized
flow/Flow engine (registry, loader, task factory, HTTP server) - see Flows
init.ts / update.ts / resolve.ts / hook-handler.tsCLI subcommands (npx ue-mcp init, update, resolve, hook)

Tool Registration Pattern

All tools use the categoryTool() factory:

export const levelTool: ToolDef = categoryTool(
  "level",                              // tool name
  "Actors, selection, components...",    // description
  {
    get_outliner: bp("get_outliner"),           // bridge action
    get_current:  { handler: localHandler },    // local action
  },
  "- get_outliner: List actors...",     // AI-facing docs
);

Two action types:

  • Bridge actions (bp()) - forwarded to the C++ plugin over WebSocket
  • Local actions - handled in Node.js (filesystem operations like INI parsing, C++ header reading)

Bridge Communication

The EditorBridge maintains a WebSocket connection to the bridge's per-project port (derived from the project root path, published to [project]/Saved/UE_MCP_Bridge/port.json; see Configuration). The legacy fixed 9877 is the fallback when no project root is known.

Editor lifecycle actions (start_editor, stop_editor, restart_editor) do not share that fallback. They act on a process rather than on a connection, so they resolve the target editor from the project's lockfile alone and refuse when it is absent, and they scope every process check to the .uproject on the command line. See Which editor lifecycle actions act on.

Protocol: JSON-RPC 2.0

// Request
{
  "jsonrpc": "2.0",
  "id": "req-42",
  "method": "get_outliner",
  "params": { "classFilter": "StaticMeshActor" }
}

// Response
{
  "jsonrpc": "2.0",
  "id": "req-42",
  "result": { "actors": [...] }
}
  • Timeout: 30 seconds per request
  • Reconnect: Automatic every 15 seconds if disconnected
  • Thread safety: All responses are correlated by request ID

Framing

A TCP read is a byte-stream event, not a message event, so the bridge treats it as one. Both ends accumulate bytes, decode as many whole WebSocket frames as have arrived, and join continuation frames into one message. Several pipelined requests in a single segment all arrive; a payload split across segments is reassembled rather than dropped.

A single message is bounded at 64 MiB, as is the unparsed receive buffer, and so is any single frame's declared length. Exceeding any of them closes the connection with WebSocket status 1009 and a reason naming both the size and the limit, which the client repeats verbatim rather than reporting a generic lost connection. A frame stream that stops parsing (reserved bits set, an unknown opcode, a fragmented control frame, or a client frame sent unmasked, which RFC 6455 forbids) closes with 1002.

Control frames are answered as the protocol requires: a close frame gets its status code echoed back, a ping gets a pong carrying the same payload. When the editor shuts down with a client attached, the bridge closes with 1001 going away rather than severing the socket.

The upgrade request is read through to its blank line under one deadline and one size bound, and is validated before a 101 is sent: GET, HTTP/1.1, Upgrade: websocket, Connection: Upgrade, Sec-WebSocket-Version: 13, and a Sec-WebSocket-Key that decodes to 16 bytes. A refusal answers with an HTTP status and a sentence. Anything the client pipelined behind the request is handed straight to the frame reader.

Capability handshake

On connect the client asks get_bridge_capabilities, which the bridge answers on the socket thread without touching the game thread. The reply reports:

FieldMeaning
protocolVersionWire protocol the plugin speaks (UEMCP_BRIDGE_PROTOCOL_VERSION)
handlerApiVersionHandler ABI for native plugins (UEMCP_BRIDGE_API_VERSION)
builtAtCompile timestamp of the loaded binary. The stale-build tell
engineVersion, projectName, pid, port, instanceId, startedAtWhich editor answered
featuresNamed capabilities, for asking about one thing rather than a version floor
actions, actionCountThe method names the running binary actually registered

A plugin built before the handshake existed answers Unknown method, which the client records as protocol version 1. When the plugin and client versions differ, the client says so once at connect, repeats it on any unknown-method answer (naming both versions and the method), and reports it under bridgeProtocol in project(get_status).

bridgeApiVersion in project(get_status) is read from the header on disk and therefore describes the source; bridgeProtocol comes from the running binary. When the two disagree, the deployed plugin has not been rebuilt.

Socket and thread ownership

The accept loop creates a client socket and hands it to one connection thread, which owns it from that moment and closes it exactly once. No other code closes a client socket.

Connections are counted by the accept loop before their thread exists and released by the thread on its way out, so shutdown waits for the count to reach zero before the module frees the server object. Connections notice the stop flag at the end of their current one-second select; only if that grace period lapses does shutdown half-close their sockets, and only after a further wait does it give up and log which connections are stuck. The game-thread executor abandons in-flight waits once shutdown begins, since module teardown runs on the game thread and a queued handler will never execute.

C++ Bridge Plugin

Location: plugin/ue_mcp_bridge/ Module type: Editor-only

The plugin runs a raw WebSocket server on a dedicated thread, dispatches incoming JSON-RPC requests to registered handler functions, and executes them on the game thread.

Core Classes

ClassPurpose
FMCPBridgeServerWebSocket server (raw platform sockets, Windows + Linux/Mac)
FMCPHandlerRegistryMaps method names to C++ handler functions
FMCPGameThreadExecutorQueues tasks to the game thread (required for UE API access)
HandlerUtils.h + HandlerAssetCreate.hShared utilities - MCPError/MCPSuccess/MCPResult, RequireString/OptionalVec3/OptionalRotator/etc., FindActorByLabel/FindActorByLabelOrName, MCPCheckAssetExists/MCPCheckActorLabelExists, LoadAssetByPath[T], LoadBlueprintCDO[T], MCPCreateAssetIdempotent[T], SaveAssetPackage.

Handler Categories

34 C++ handler groups are registered in BridgeServer.cpp. Together they expose 1931+ method names (some of which are aliases mapped onto a smaller number of canonical handlers):

Handler groupCoverage
EditorHandlersConsole, Python, PIE, viewport, build, logs, perf, screenshots, scalability
AssetHandlersCRUD, import, search, datatables, textures, sockets, FTS search
BlueprintHandlersRead/write, graphs, compilation, node types, T3D import/export, reparent, validate
LevelHandlersActors, components, volumes, lights, world settings, splines
ReflectionHandlersClass/struct/enum reflection, gameplay tags
MaterialHandlersMaterials, instances, expression graph authoring, declarative builder, render preview
AnimationHandlersAnim BPs, montages, blendspaces, skeletons, IK Rig, ControlRig, virtual bones, live-actor bone reads + leader-pose rebind + preview-animation toggle
AudioHandlersPlayback, ambient sounds, SoundCues, MetaSounds
WidgetHandlersUMG widget trees, editor utility widgets and blueprints
FoliageHandlersFoliage types, instance queries
LandscapeHandlersLandscape proxies, layer-info assets, materials
NetworkingHandlersReplication, dormancy, relevancy, net priority
NiagaraHandlersVFX systems, emitters, renderers, data interfaces, GPU HLSL inspection
PCGHandlersProcedural generation graphs, mesh spawner authoring
GasHandlersGameplay Ability System (attributes, abilities, effects, cues)
GameplayHandlersPhysics, collision, navigation, AI (BTs, EQS, perception), input, game framework
PhysicsHandlersCollision profiles, simulation toggles, body properties
SequencerHandlersLevel sequences and tracks
SplineHandlersSpline actor authoring
DialogHandlersModal dialog auto-response policies
StateTreeHandlersStateTree asset authoring (states, transitions, tasks, root parameters)
ChooserHandlersChooser table authoring
EpicHandlersEpic 5.8 native toolset surfacing
FabHandlersFab owned-library import
LockHandlersPer-asset exclusive locks for concurrent agents (acquire/release/list, TTL-leased)
DiffHandlersSemantic Blueprint and asset diffing
ProjectHandlersProject info, world subsystem queries
DemoHandlersNeon Shrine demo builder
AssetBulkReadHandlersBatched property reads across many assets in one call
AssetGeometryHandlersGeometry Script mesh authoring and fracture
AssetMeshBooleanHandlersBoolean mesh operations
CollisionQueryHandlersWorld traces, overlaps and sweeps
MassHandlersMass Entity fragments, traits and processors
SkeletalMeshHandlersSkeletal mesh LODs, sections and instancing settings

Plugin Modules

The plugin ships two modules, loading at different phases:

ModuleLoading phaseRole
UE_MCP_BridgeStatusPostConfigInitPublishes what the engine is doing (phase, slow-task name and percent, modal dialog, compile counts, game-thread stall) to Saved/UE_MCP_Bridge/status.json from a writer thread. Core-only dependencies, so it can load this early.
UE_MCP_BridgePostEngineInitThe WebSocket server, the handler registry, and the Slate/Engine-backed sensors it injects into the status snapshot.

The split exists because the interesting failures happen before PostEngineInit: RHI init, plugin module loading, map load and Python startup all run while a single-module plugin would not yet exist. The status module covers that window; the bridge module upgrades the same snapshot once Slate, the shader compiler and the asset compiler are available.

Plugin Dependencies

The C++ plugin links against a wide range of UE modules:

  • Core: Core, CoreUObject, Engine, Json, JsonUtilities, GameplayTags
  • Editor: UnrealEd, AssetRegistry, BlueprintGraph, Kismet, KismetCompiler, PropertyEditor
  • Systems: Landscape, Niagara, PCG, Sequencer, UMG, GameplayAbilities, NavigationSystem, AIModule
  • Tools: LiveCoding (Windows only), MaterialEditor, EditorScriptingUtilities, DataValidation

Hybrid Architecture

A key design principle: read operations work without the editor.

Operation TypeRequires Editor?How
INI config parsingNoDirect filesystem
C++ header reflectionNoRegex-based parsing
Asset directory listingNoFilesystem scan
Blueprint readingYesC++ bridge
Actor placementYesC++ bridge
Material authoringYesC++ bridge
PIE controlYesC++ bridge
Build pipelineYesC++ bridge

This means the AI can explore project structure, read configs, and understand C++ code even when the editor isn't running.

Path Resolution

The ProjectContext handles path formats:

InputResolved To
/Game/MyAsset[ProjectDir]/Content/MyAsset
/MyPlugin/Assets/Foo[ProjectDir]/Plugins/MyPlugin/Content/Assets/Foo
Absolute pathUsed as-is
Relative pathRelative to project root

Data Flow Example

Here's what happens when the AI calls blueprint(action="read", assetPath="/Game/BP_Player"):

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.