Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,75 @@ All notable changes to this project are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0] - 2026-07-02 — the Epic MCP era

> **Version renumbering:** the public line continues from v0.1.1. An internal
> 2.x numbering existed in-repo but was never released; it was retired in this
> release. `pyproject` now reads the single version source
> (`ue_mcp/__version__.py`) via hatchling, and tags must match it.

UE 5.8 ships Epic's official **Unreal MCP** (830 tools across 52 toolsets with
`AllToolsets`). This release repositions the bridge around what Epic does NOT
ship, with every decision grounded in a live probe of the real surface.

### Changed — the retirement flip
- **Tiered tool registry** at the `register_all_tools()` seam. Default profile
is `core`: **20 tools mounted** (the differentiated layer + `ue_status`/
`ue_health_check`). `UE_MCP_PROFILE=full` remounts the 36 Epic-covered
commodity tools; `all` adds the two honest not-implemented slots.
- `ue_health_check` now reports the active profile, mount counts, and Epic
MCP (`:8000`) reachability.
- `ue_undo`/`ue_redo` return an explicit not-implemented error (no editor
round-trip): no scriptable editor-transaction route exists in the UE Python
API, and they previously probed nonexistent APIs on every call.

### Added
- **`docs/EPIC_MCP_MATRIX.md`** — the retirement contract-of-record: verdicts
for all 58 tools against the probed Epic surface, plus raw captures
(`docs/epic_mcp/`) and a repeatable prober (`scripts/probe_epic_mcp.py`).
- **Exec-simulating test harness** (`tests/exec_sim/`): a strict fake `unreal`
module, per-tool sentinel registry, compile/exec/sentinel gates, and
scripted-failure honesty contracts. Proven red on the pre-fix tree — every
failure mapped 1:1 to a known bug (see `tests/exec_sim/README.md`).
**580 tests total** (was 415).
- `tests/test_registry_tiers.py` pins the matrix arithmetic and profile
semantics; tier/tool drift fails CI in both directions.
- Two-server `.mcp.json` (this bridge over stdio + Epic's server on `:8000`);
`ModelContextProtocol` + `AllToolsets` staged `Optional: true` in the
`.uproject` (no-ops on 5.7, self-enables on 5.8).

### Fixed
- **All 11 confirmed bugs** from the 2026-06-11 hand-verified review: the
level-actor resolver (asset-API misuse in delete/transform), `true`→`True`
NameError, spawn-blueprint label indent, `load_level` false success,
`focus_actor` unconditional success, phantom `is_hidden()`, unescaped
`find_assets` patterns, the cloner arg-discard, the viewport-fallback race,
material-parameter API-family mixing, and the wheel-killing shim import.
- Adversarial-verification findings fixed pre-merge: a stale-frame regression
in the perception fallback, wrong ClonerEffector class names for 5.7 (all
cloner writes are now read-back-verified), a working `CAMERA ALIGN` focus
route, and `find_assets` escaping moved into the codegen chokepoint.
- `metrics` uptime rounding (coarse-clock test flake).

### Removed
- **The legacy Translators questionnaire runtime**: the game-flow state
machine entry points, trivia UI, Blueprint relay component, and the
repo-root runner. The `usd_bridge/` package is parked in-repo, out of the
ship path.

### Engine & packaging (UE 5.8)
- **Retargeted to UE 5.8** (`EngineAssociation` 5.8; both plugins
`EngineVersion` 5.8.0). The `ViewportPerception` `FrameProducer` was ported
for 5.8's changed `OnBackBufferReadyToPresent` signature (`FTextureRHIRef`
→ `ISlateViewportProvider&`) behind a dual-version `#if`, so it still
compiles against 5.7. Verified: the editor target builds clean on 5.8.
- **Marketplace-ready plugins**: branded 128×128 icons, storefront-grade
`.uplugin` metadata, explicit `Category` specifiers on every
Blueprint-exposed property (required for engine-module distribution), and a
clean `RunUAT BuildPlugin -installed` package. Draft storefront copy in
`docs/FAB_LISTING.md`. The plugins can now be installed engine-wide and
picked from Edit → Plugins in any 5.8 project.

## [0.1.0] - 2026-05-30

First tagged release. Demo-ready on a maintainer's machine; see **Status** below for what stands between this and a distributable plugin.
Expand Down
11 changes: 5 additions & 6 deletions INVENTORY.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
# UnrealEngine_Bridge v2.1.0 — Inventory
# UnrealEngine_Bridge v0.2.0 — Inventory

MCP server bridging Claude Code to Unreal Engine 5 via Remote Control API.

## Summary

| Metric | Count |
|--------|-------|
| MCP Tools | 56 |
| Tool Modules | 11 |
| Tests | 222 |
| Python source files | 27 |
| Total lines (Python) | ~8,000 |
| MCP Tools | 58 (56 registry + 2 server) |
| Mounted by default | 20 (`core` profile; `UE_MCP_PROFILE=full` remounts the commodity tier) |
| Tool Modules | 14 |
| Tests | 580 |

## Dependencies

Expand Down
8 changes: 8 additions & 0 deletions Plugins/UEBridge/Config/FilterPlugin.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[FilterPlugin]
; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and
; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively.
;
; Examples:
; /README.txt
; /Extras/...
; /Binaries/ThirdParty/*.dll
Binary file modified Plugins/UEBridge/Resources/Icon128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "Serialization/JsonSerializer.h"
#include "Internationalization/Regex.h"
#include "Engine/Engine.h"
#include "Misc/EngineVersion.h"


// === LIFECYCLE ===
Expand Down Expand Up @@ -147,7 +148,7 @@ void UUEBridgeSubsystem::SendAcknowledge()

Content = UpdateUsdaVariant(Content, TEXT("message_type"), TEXT("ack"));
Content = UpdateUsdaAttribute(Content, TEXT("Ack"), TEXT("ready"), TEXT("true"), false);
Content = UpdateUsdaAttribute(Content, TEXT("Ack"), TEXT("ue_version"), FString(ENGINE_VERSION_STRING), true);
Content = UpdateUsdaAttribute(Content, TEXT("Ack"), TEXT("ue_version"), FEngineVersion::Current().ToString(), true);
Content = UpdateUsdaAttribute(Content, TEXT("Ack"), TEXT("project"), TEXT("UnrealEngineBridge"), true);
Content = UpdateUsdaAttribute(Content, TEXT("Ack"), TEXT("timestamp"), Timestamp, true);

Expand All @@ -167,7 +168,7 @@ void UUEBridgeSubsystem::SendAcknowledge()

TSharedPtr<FJsonObject> AckObj = MakeShared<FJsonObject>();
AckObj->SetBoolField(TEXT("ready"), true);
AckObj->SetStringField(TEXT("ue_version"), FString(ENGINE_VERSION_STRING));
AckObj->SetStringField(TEXT("ue_version"), FEngineVersion::Current().ToString());
AckObj->SetStringField(TEXT("project"), TEXT("UnrealEngineBridge"));
JsonObj->SetObjectField(TEXT("ack"), AckObj);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
#include "BridgeTypes.h"
#include "UEBridgeSubsystem.generated.h"

// Used by reference only in method signatures below; forward-declared so the
// header is self-contained under non-unity / installed-plugin builds (a unity
// build would otherwise pull FJsonObject in transitively).
class FJsonObject;

UCLASS()
class UEBRIDGERUNTIME_API UUEBridgeSubsystem
: public UGameInstanceSubsystem
Expand Down
15 changes: 3 additions & 12 deletions Plugins/UEBridge/Source/UEBridgeRuntime/UEBridgeRuntime.Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,8 @@ public UEBridgeRuntime(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

PublicIncludePaths.AddRange(new string[]
{
Path.Combine(ModuleDirectory, "Public"),
Path.Combine(ModuleDirectory, "Public", "UI")
});

PrivateIncludePaths.AddRange(new string[]
{
Path.Combine(ModuleDirectory, "Private"),
Path.Combine(ModuleDirectory, "Private", "UI")
});
PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "Public"));
PrivateIncludePaths.Add(Path.Combine(ModuleDirectory, "Private"));

// Core runtime dependencies — ship in packaged builds
PublicDependencyModuleNames.AddRange(new string[]
Expand Down Expand Up @@ -50,7 +41,7 @@ public UEBridgeRuntime(ReadOnlyTargetRules Target) : base(Target)
PrivateDependencyModuleNames.Add("Projects");

// Version definition — single source of truth
PublicDefinitions.Add("BRIDGE_VERSION=TEXT(\"2.1.0\")");
PublicDefinitions.Add("BRIDGE_VERSION=TEXT(\"0.2.0\")");

// USD support flag: editor-only via pxr, runtime uses text-based USDA parser
if (Target.bBuildEditor)
Expand Down
17 changes: 10 additions & 7 deletions Plugins/UEBridge/UEBridge.uplugin
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"FileVersion": 3,
"Version": 1,
"VersionName": "2.1.0",
"EngineVersion": "5.7.0",
"FriendlyName": "UE Bridge",
"Description": "Agentic AI bridge for Unreal Engine. Provides MCP tool integration, file-based bridge protocol with USD transport, behavioral signal collection, and Remote Control API access for Claude Code.",
"Category": "Gameplay",
"VersionName": "0.2.0",
"EngineVersion": "5.8.0",
"FriendlyName": "UE Bridge — AI Editor Control",
"Description": "Give Claude Code and other MCP clients editor-side control of Unreal Engine: an in-editor status panel, a directory-watch bridge, and Remote Control API access. Pairs with the UnrealEngine Bridge MCP server for AI-driven scene building, lighting, spatial reasoning, and viewport perception.",
"Category": "Code Plugins",
"CreatedBy": "Joseph Ibrahim",
"CreatedByURL": "https://github.com/JosephOIbrahim",
"DocsURL": "https://github.com/JosephOIbrahim/UnrealEngine_Bridge#readme",
Expand All @@ -14,18 +14,21 @@
"CanContainContent": true,
"IsBetaVersion": false,
"IsExperimentalVersion": false,
"Installed": false,
"EnabledByDefault": false,
"SupportedTargetPlatforms": ["Win64"],
"Modules": [
{
"Name": "UEBridgeRuntime",
"Type": "Runtime",
"LoadingPhase": "Default"
"LoadingPhase": "Default",
"WhitelistPlatforms": ["Win64"]
},
{
"Name": "UEBridgeEditor",
"Type": "Editor",
"LoadingPhase": "Default"
"LoadingPhase": "Default",
"WhitelistPlatforms": ["Win64"]
}
],
"Plugins": [
Expand Down
8 changes: 8 additions & 0 deletions Plugins/ViewportPerception/Config/FilterPlugin.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[FilterPlugin]
; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and
; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively.
;
; Examples:
; /README.txt
; /Extras/...
; /Binaries/ThirdParty/*.dll
Binary file added Plugins/ViewportPerception/Resources/Icon128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
#include "RenderingThread.h"
#include "Framework/Application/SlateApplication.h"
#include "RHISurfaceDataConversion.h"
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 8
#include "Slate/SlateViewportProvider.h"
#endif

FFrameProducer::FFrameProducer()
: FrameCounter(0)
Expand Down Expand Up @@ -72,13 +75,20 @@ void FFrameProducer::SetThrottleInterval(double Seconds)
MinCaptureInterval = FMath::Max(Seconds, 0.01); // Cap at 100fps
}

void FFrameProducer::OnFrameBufferReady(SWindow& SlateWindow, const FTextureRHIRef& FrameBuffer)
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 8
void FFrameProducer::OnFrameBufferReady(SWindow& SlateWindow, ISlateViewportProvider& ViewportProvider)
{
FRHITexture* FrameBuffer = ViewportProvider.GetBackBufferResource();
#else
void FFrameProducer::OnFrameBufferReady(SWindow& SlateWindow, const FTextureRHIRef& FrameBufferRef)
{
FRHITexture* FrameBuffer = FrameBufferRef.GetReference();
#endif
// Runs on the render thread. All readback state below is render-thread-only,
// so no synchronization is needed for it. We NEVER block the render thread:
// a copy is enqueued on one present and drained on a later one once the GPU
// has finished, instead of a synchronous ReadSurfaceData stall.
if (!PixelBus || !FrameBuffer.IsValid())
if (!PixelBus || !FrameBuffer)
{
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
#include "CoreMinimal.h"
#include "RHI.h"
#include "RHIGPUReadback.h"
#include "Runtime/Launch/Resources/Version.h"

class FPixelBus;
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 8
class ISlateViewportProvider;
#endif

class FFrameProducer
{
Expand All @@ -29,8 +33,15 @@ class FFrameProducer
bool IsActive() const { return bActive; }

private:
/** Called on the render thread when the backbuffer is ready. */
void OnFrameBufferReady(SWindow& SlateWindow, const FTextureRHIRef& FrameBuffer);
/** Called on the render thread when the backbuffer is ready.
* 5.8 changed OnBackBufferReadyToPresent's second param from the raw
* texture to an ISlateViewportProvider — both signatures are kept so the
* plugin compiles against 5.7 and 5.8 from the same source. */
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 8
void OnFrameBufferReady(SWindow& SlateWindow, ISlateViewportProvider& ViewportProvider);
#else
void OnFrameBufferReady(SWindow& SlateWindow, const FTextureRHIRef& FrameBufferRef);
#endif

FDelegateHandle DelegateHandle;
FPixelBus* PixelBus = nullptr;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ struct FPerceptionCamera
{
GENERATED_BODY()

UPROPERTY(BlueprintReadOnly)
UPROPERTY(BlueprintReadOnly, Category = "Perception")
FVector Location = FVector::ZeroVector;

UPROPERTY(BlueprintReadOnly)
UPROPERTY(BlueprintReadOnly, Category = "Perception")
FRotator Rotation = FRotator::ZeroRotator;

UPROPERTY(BlueprintReadOnly)
UPROPERTY(BlueprintReadOnly, Category = "Perception")
float FOV = 90.0f;
};

Expand All @@ -37,31 +37,31 @@ struct FPerceptionMetadata
GENERATED_BODY()

// Camera
UPROPERTY(BlueprintReadOnly)
UPROPERTY(BlueprintReadOnly, Category = "Perception")
FPerceptionCamera Camera;

// Viewport
UPROPERTY(BlueprintReadOnly)
UPROPERTY(BlueprintReadOnly, Category = "Perception")
FIntPoint ViewportSize = FIntPoint::ZeroValue;

UPROPERTY(BlueprintReadOnly)
UPROPERTY(BlueprintReadOnly, Category = "Perception")
FString ViewportType;

// Scene context
UPROPERTY(BlueprintReadOnly)
UPROPERTY(BlueprintReadOnly, Category = "Perception")
TArray<FString> SelectedActors;

UPROPERTY(BlueprintReadOnly)
UPROPERTY(BlueprintReadOnly, Category = "Perception")
FString MapName;

UPROPERTY(BlueprintReadOnly)
UPROPERTY(BlueprintReadOnly, Category = "Perception")
int32 ActorCount = 0;

// Timing
UPROPERTY(BlueprintReadOnly)
UPROPERTY(BlueprintReadOnly, Category = "Perception")
float DeltaTime = 0.0f;

UPROPERTY(BlueprintReadOnly)
UPROPERTY(BlueprintReadOnly, Category = "Perception")
float FPS = 0.0f;
};

Expand All @@ -76,29 +76,29 @@ struct FPerceptionPacket
TArray<uint8> ImageData;

/** Image dimensions after resize/encode. */
UPROPERTY(BlueprintReadOnly)
UPROPERTY(BlueprintReadOnly, Category = "Perception")
int32 Width = 0;

UPROPERTY(BlueprintReadOnly)
UPROPERTY(BlueprintReadOnly, Category = "Perception")
int32 Height = 0;

/** Format used for encoding. */
UPROPERTY(BlueprintReadOnly)
UPROPERTY(BlueprintReadOnly, Category = "Perception")
EPerceptionImageFormat Format = EPerceptionImageFormat::JPEG;

/** Monotonically increasing frame counter. */
UPROPERTY(BlueprintReadOnly)
UPROPERTY(BlueprintReadOnly, Category = "Perception")
int64 FrameNumber = 0;

/** Platform time at capture. */
UPROPERTY(BlueprintReadOnly)
UPROPERTY(BlueprintReadOnly, Category = "Perception")
double Timestamp = 0.0;

/** Scene metadata collected at capture time. */
UPROPERTY(BlueprintReadOnly)
UPROPERTY(BlueprintReadOnly, Category = "Perception")
FPerceptionMetadata Metadata;

/** True if this packet contains valid data. */
UPROPERTY(BlueprintReadOnly)
UPROPERTY(BlueprintReadOnly, Category = "Perception")
bool bValid = false;
};
Loading
Loading