Command Palette

Search for a command to run...

UnylyUnyly
Browse all

PythonMCP UEFN

FreeNot checked

Exposes the Unreal Editor for Fortnite to AI assistants via MCP, enabling actor manipulation, device property editing, Verse building, and more through a Python

GitHubEmbed

About

Exposes the Unreal Editor for Fortnite to AI assistants via MCP, enabling actor manipulation, device property editing, Verse building, and more through a Python bridge.

README

Drive Unreal Editor for Fortnite from any MCP client over a local Python bridge.

tests release License Python Status

This is a Model Context Protocol server that exposes the UEFN editor to an AI assistant. With it, an assistant can spawn and move actors, browse and validate assets, read and write device options, run arbitrary editor Python, trigger a Verse build and read the compile result, all without anyone touching the mouse.

It ships 42 tools. The interesting part is the device bridge: it reads and writes the @editable Verse properties that the standard unreal Python API refuses to surface by name. More on that below.

How it works

There are two halves. A listener that runs inside UEFN, and an MCP server that runs on your machine and talks to it.

  MCP client  (Claude Code, or any MCP host)
       |
       |  stdio
       v
  mcp_server.py        host process, this repo
       |
       |  HTTP POST  ->  127.0.0.1:8765-8770  (discovered, not assumed)
       v
  uefn_listener.py     runs inside the UEFN editor
       |
       |  game thread  (slate post-tick callback)
       v
  unreal.*             editor scripting API

The listener is a ThreadingHTTPServer bound to the first free port in 127.0.0.1:8765-8770 (see Ports). The HTTP thread only enqueues work. A slate post tick callback drains that queue on the game thread, because the unreal API is not thread safe and will crash if you call it from anywhere else. A watchdog re-arms the tick and the accept loop after a world or map change, and on a full project reopen the init_unreal hook re-runs the module. The whole listener is one self-contained file so you can paste it straight into Execute Python Script.

On the host side, transport.py adds retry with backoff, per-tool timeouts, and error classification. When something goes wrong it tells you whether the listener is down (socket refused) or wedged (reachable but the game thread stopped draining), so the failure message points at the actual fix.

Ports

The port is discovered, not assumed. 8765 is a popular default and other local bridges bind it too, so the listener takes the first port in 8765-8770 that is not already held, and the host finds it by asking each port's /health to identify itself:

{"ok": true, "service": "uefn-mcp-bridge", "port": 8766, "count": 42}

Only a /health reporting service: "uefn-mcp-bridge" is treated as ours. A plain "something answered on 8765" probe would happily drive a different bridge, which is the failure this avoids. The listener logs which case it hit:

port 8765 held by a foreign service (unknown); trying next
port 8765 holds a previous instance of this bridge; reclaiming
HTTP accept loop started on 127.0.0.1:8766

Nothing needs configuring for this: the MCP tools resolve the port on their own, and diagnostics reports the one in use. It matters only if you POST to the listener by hand, in which case read the port from /health instead of hardcoding it. If a port moves while a host process is already running, the host re-identifies and rescans on the next call rather than talking to whatever took the old port.

Set UEFN_MCP_PORT on both sides to pin a single port and skip discovery entirely. A value that is not a valid port is ignored with a warning rather than being fatal.

Security

The POST surface runs arbitrary editor code, including execute_python. A bind to 127.0.0.1 alone does not make that safe: any other process on the machine can reach it, and a web page you have open can try to POST to it through DNS rebinding. So every request is authenticated.

  • Token. Both halves require a shared secret in the X-MCP-Token header. A request without it, or with the wrong value, gets 401. The check is constant time.
  • Where the token lives. A 64 character hex secret at ~/.uefn_mcp_token. The listener creates it on first start if it is missing; the host reads the same file. The path is fixed and per user rather than next to the script, because the listener may be imported from a copy inside your project while the host runs from this repo, and both must resolve the same file.
  • Override. Set the UEFN_MCP_TOKEN environment variable on both sides to supply your own value. It takes precedence over the file.
  • Host header. The listener also rejects any request whose Host is not a loopback name (127.0.0.1 or localhost) with 403, which blocks DNS rebinding from a browser.

Nothing extra to configure for the default local setup: start the listener, it writes the token, the host reads it. If you run the two on different user accounts or machines, set UEFN_MCP_TOKEN on both.

Requirements

Host:

  • Python 3.11
  • The mcp package (pip install -r requirements.txt)

Editor:

  • UEFN with Python editor scripting enabled (experimental, UEFN 40.40 or newer)

The listener itself has no third party dependencies. It uses only the standard library plus the unreal module that UEFN provides.

Setup

1. Install the host dependencies

pip install -r requirements.txt

2. Register the server with your MCP client

For Claude Code:

claude mcp add uefn --scope user -- python /absolute/path/to/PythonMCP-UEFN/mcp_server.py

For any other MCP host, point it at the same command. The server speaks stdio.

{
  "mcpServers": {
    "uefn": {
      "command": "python",
      "args": ["/absolute/path/to/PythonMCP-UEFN/mcp_server.py"]
    }
  }
}

3. Start the listener inside UEFN

Open your project, then Tools > Execute Python Script and pick uefn_listener.py. It binds a port and starts serving. The log line HTTP accept loop started on 127.0.0.1:<port> tells you which one it took, and it is not always the base of the range (see Ports).

To confirm it is up, call the ping tool from your assistant (it returns {pong, uefn, tools}). The /health endpoint also needs the token now, so a plain browser hit returns 401; check it with the header instead:

TOKEN=$(cat ~/.uefn_mcp_token)
for p in 8765 8766 8767 8768 8769 8770; do
  curl -s -H "X-MCP-Token: $TOKEN" "http://127.0.0.1:$p/health" | grep -q uefn-mcp-bridge \
    && echo "bridge on $p" && break
done

4. Optional: start the listener automatically

Running the script by hand every session gets old. Install an autostart hook into your project:

python install_autostart.py "C:\path\to\YourProject"

That writes Content/Python/init_unreal.py into the project, so the listener comes up on its own whenever you open it.

Usage

Once the server is registered and the listener is running, ask your assistant to work in the level. A request like "spawn a cube at the origin and frame the camera on it" turns into:

spawn_static_mesh   mesh_path=/Engine/BasicShapes/Cube   location={x:0,y:0,z:0}
focus_on_actor      actor=<the new actor>

Reading and writing a device option works the same way:

device_list_options   label="My Trigger"
device_set_option     label="My Trigger"  option="EnabledAtGameStart"  value=false

When you need something no tool covers, execute_python runs arbitrary code in the editor with the unreal module already in scope. Assign your return value to _result.

Tools

Session and level

Tool Purpose
ping Health check. Returns pong, uefn flag, tool count.
get_current_level Current level name and asset path.
save_level Save the current level.
save_all Save every dirty level and asset under /Game.
load_level Load a level by path.
diagnostics Listener health snapshot: uptime, last tick age, accept loop, port, world, autostart.

Actors

Tool Purpose
spawn_actor Spawn an actor from a class path.
spawn_static_mesh Spawn a StaticMeshActor from a mesh asset.
list_actors List actors, optionally filtered by class or label.
delete_actors Delete actors by label or path.
set_actor_transform Set location, rotation, scale.
get_actor_transform Read a transform.
set_actor_label Rename an actor in the Outliner.
duplicate_actor Duplicate, with an optional offset and new label.
select_actors Replace the editor selection.
get_selected_actors Read the current selection.
focus_on_actor Frame the viewport on an actor.

Viewport

Tool Purpose
get_viewport_camera Read the viewport camera transform.
set_viewport_camera Set the viewport camera transform.

Assets

Tool Purpose
list_assets List asset paths under a folder.
find_asset Substring search for assets.
load_asset Load an asset and return its class and name.
asset_exists Check whether an asset exists.
create_folder Create a content folder.
validate_assets Run the editor validator. Catches publish time strips before you push.
describe_class Resolve a class by path and return its name and super.

Devices

Tool Purpose
list_editor_properties List the editor properties exposed on an actor.
set_device_property Set one editor property on a device.
set_device_properties_bulk Apply many updates in one undo group.
device_scan Scan a custom Verse device once to cache its mangled @editable names.
device_list_options List readable option names and values. Auto detects native versus Verse.
device_get_option Read one option by its author facing name.
device_set_option Write a scalar option. Returns the value read back.
device_wire Wire a Verse reference field to another actor.
device_set_prop_array Set a Verse array of reference fields to a set of actors.
device_deep_options Deep-walk the live UClass and list every @editable, including ones left at default that device_list_options misses. No scan needed.
device_deep_set Write a scalar @editable by name, coerced to the field's real UE type (fixes the stringify that breaks device_set_option on bool/number).
device_deep_self_test Validate the deep-reader's memory offsets against unreal.Vector before any walk.

Verse build

Tool Purpose
verse_build_trigger Trigger a Verse build without a manual key press.
verse_compile_status Read the latest compile verdict and errors from the editor log.

Python and autostart

Tool Purpose
execute_python Run arbitrary Python in the editor. Assign to _result to return a value.
install_autostart Write the autostart hook into a project.

The device bridge

A Fortnite device places its author written @editable Verse fields on a BlueprintGeneratedClass. The friendly name you wrote in Verse does not exist as a settable property. The fields live under mangled internal names of the form __verse_0x<hash>_<name>, and that hash changes on every recompile, so you cannot hardcode it.

The bridge recovers the mapping by dumping the placed device to T3D and matching the mangled names with a regex, then caches the result in _device_options_cache.json next to the listener. After that, the device_* tools let you read and write those fields by their real names. Native Epic devices expose clean properties and need no scan. Custom Verse devices need device_scan run once with the device configured, so the dump has values to recover.

The T3D scrape has one blind spot: it only sees fields whose value differs from the default, so a freshly placed device hides most of its knobs. device_deep_options covers that. It walks the device's live compiled UClass field chain directly and lists every @editable, defaults included, with each field's UE type, and needs no scan. The walk reads raw memory, so it runs device_deep_self_test first to confirm its offsets against unreal.Vector on the current build and refuses to walk if they do not validate. device_deep_set is the matching writer: it resolves the field, coerces the value to its real UE type through the plain engine API, and reads it back, which fixes the case where device_set_option stringifies a bool or number and the write fails.

One field type is out of reach: a Verse @editable : string reflects as VerseStringProperty, which the editor Python marshaller has no conversion case for, so it can be neither read nor written by name (a raw memory write also fails to persist). This is an editor limitation, not a bridge one. Expose an @editable Index : int over a const []string in Verse and index into it instead. Details in docs/UEFN_Python_API_gaps.md §1b.

The full background, including the editor Python limitations that made this necessary, is written up in docs/UEFN_Python_API_gaps.md.

Limitations

  • Editor only. Python here cannot run gameplay. Runtime logic stays in Verse.
  • One editor instance. A second listener takes its own port from the range, but the host resolves to whichever answers first, so it is not a supported way to drive two editors.
  • The token gates access, it does not encrypt the channel. Traffic is plain HTTP over loopback. This is a local bridge, not something to expose off the machine.
  • Do not issue tool calls while a Launch Session is starting. A racing call can abort the socket and trip asset validation with a false failure.
  • The device introspection surface in the editor Python API is thin. The bridge works around it rather than relying on official support, so expect rough edges as UEFN changes.

Project layout

mcp_server.py             host MCP server, tool schemas, routing
transport.py              host HTTP client, retry, timeouts, error classification
uefn_listener.py          listener that runs inside UEFN, self-contained
device_deepread.py        deep @editable reader/typed-writer, loaded by the listener
install_autostart.py      writes the project autostart hook
init_unreal_template.py   template for Content/Python/init_unreal.py
tests/                    transport and port-selection tests, no UEFN needed
docs/                     UEFN Python API gaps report

Development

The transport layer runs without UEFN, so its tests run anywhere:

python -m unittest discover tests

The listener imports unreal, so tests/test_listener_port.py imports it against a stub. That covers the pure-Python port selection and nothing else: anything touching the editor still has to be exercised in UEFN.

The listener and the server import unreal and mcp, so a full end to end run needs UEFN open with the listener started.

License

Copyright (C) 2026 yAstrosss

GPLv3. See LICENSE.

from github.com/yAstrosss/PythonMCP-UEFN

Installing PythonMCP UEFN

This server has no published package — it is built from source. Open the repository and follow its README.

▸ github.com/yAstrosss/PythonMCP-UEFN

FAQ

Is PythonMCP UEFN MCP free?

Yes, PythonMCP UEFN MCP is free — one-click install via Unyly at no cost.

Does PythonMCP UEFN need an API key?

No, PythonMCP UEFN runs without API keys or environment variables.

Is PythonMCP UEFN hosted or self-hosted?

Self-hosted: the server runs locally on your machine via the install command above.

How do I install PythonMCP UEFN in Claude Desktop, Claude Code or Cursor?

Open PythonMCP UEFN on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.

Related MCPs

Compare PythonMCP UEFN with

Not sure what to pick?

Find your stack in 60 seconds

Author?

Embed badge for your README

Browse similar

All ai MCPs