// writeup

CUE4Parse-MCP: giving an AI assistant access to Unreal assets

9 min read project: CUE4Parse-MCP
csmcp

I built CUE4Parse-MCP so an AI assistant can search a game's cooked assets, inspect what it finds, and export meshes, textures or animations to disk. The idea is to ask for something like "find Brimstone's third-person mesh and export it as glTF", then let the assistant work through the actual asset paths instead of guessing them.

The implementation is C# on .NET 10, using CUE4Parse, the parsing library behind FModel. It reads the game archives directly. FModel doesn't need to be installed or running, and there's no attempt to drive its UI with screenshots and clicks.

It also doesn't need the game running. This is an on-disk asset tool, with no process attachment, memory writes or game hooks. It writes exports and cache files, and can download configured AES keys, mappings and decompression libraries. That distinction matters, but it doesn't give anyone permission to redistribute a game's assets or make modified files safe to use in an online game.

The assistant needs tools, not a directory dump

MCP gives a client a way to discover tools and call them with structured arguments. Here the client starts a local executable and talks JSON-RPC over stdin and stdout. Logs go to stderr, because one stray log line on stdout can corrupt the protocol.

The useful operations are fairly small. list_games tells the assistant what's configured, and load_game mounts a game's archives. search_assets finds package paths, list_folder lets it browse, and inspect_asset exposes export names and classes. The remaining tools are export_mesh, export_textures and export_anim.

Those tools let the assistant investigate before exporting. A search hit might be a blueprint that references the mesh rather than the mesh itself. Asking inspect_asset for properties gives it the serialized references to follow. A texture export can start from a texture, a material, or a mesh's material slots, so the client doesn't have to manually resolve every texture first.

The model isn't doing the parsing or inventing a converter. CUE4Parse supplies that. The server gives it named operations, bounded results, and errors it can act on.

There's a CLI over the same core operations as well. That's useful when a game patch breaks something: I can reproduce the load or export without involving an AI client.

Finding one mesh in a large cooked build

One Valorant load during release validation mounted 32 containers and indexed 289,307 packages. Those are counts from the tested build, not fixed numbers for the game or a performance guarantee.

Handing all of those paths to a model would be useless. The server builds an index of package paths without extensions, excluding payload sidecars such as .uexp and .ubulk. An untyped search can then operate on that index without deserializing every asset.

Search is deliberately ordinary: case-insensitive path matching, with every space-separated term required and * available as a wildcard. It isn't an embedding search, and it doesn't know that a character's public name corresponds to a particular internal asset name. The assistant still has to discover that association. Matches in the asset filename rank ahead of matches that only occur in its folders.

The type filter is more interesting. A path containing "mesh" isn't necessarily a mesh, so typed searches read package headers and inspect export class hierarchies. For the tested character mesh, the hierarchy was:

SkeletalMesh : SkinnedAsset : StreamableRenderAsset : Object

That also allows broader filters. Mesh matches skinned assets and static meshes; Animation matches the animation hierarchy rather than only files whose names contain "anim".

Header reads still cost something. The server scans ranked candidates in chunks of 512, stopping once it has enough results, with a hard cap of 20,000 candidates for a typed search. If the scan is incomplete, the response says so. An unreadable header is counted and skipped during a broad search; inspecting that package directly reports the error.

Here's the CLI version of the search and export used in the validation. The query was sarge, matching the internal name in the tested asset paths:

.\Cue4ParseMcp.exe search valorant sarge --type SkeletalMesh --limit 2

$mesh = 'ShooterGame/Content/Characters/Sarge/S0/3P/Models/TP_Sarge_S0_Skelmesh'
.\Cue4ParseMcp.exe export-mesh valorant $mesh --format gltf --no-materials --out D:/Exports

The search returned both the first-person and third-person skeletal meshes. The glTF export produced a binary .glb; validation checked its header, version and declared file length. That proves the file was written in the expected container format. It isn't a claim that every exported material or animation has been checked in a DCC application.

Asset paths can also use Unreal's /Game/... form, Package.Object, or a class-qualified reference such as SkeletalMesh'/Game/...'. Search returns canonical provider paths so the assistant has an exact path for the next call.

Mounting isn't the same as parsing

Configuration lives in games.json. Each game has an archive directory and a CUE4Parse EGame value, with optional AES keys and .usmap mappings. Local paths in that file resolve against the config's directory, so starting the server from an MCP client's working directory doesn't change where it looks for files.

AES keys and mappings solve different problems. A key is needed to read an encrypted container. Mappings describe the properties of unversioned cooked assets, where the serialized data doesn't carry enough information to reconstruct those properties on its own.

That creates a slightly misleading failure mode: the archives mount, searches find plausible paths, and the export fails when an object is deserialized. A successful mount doesn't prove the .usmap matches the installed game build.

The server can try configured endpoints first and fall back to local keys or mappings. Downloaded mappings are cached by game and filename; if an endpoint fails and there's no usable local file, it can use a cached copy. The fallback is reported in the load warnings. A cached file being available is still no guarantee that it's current.

The timeout handling needed care. HttpClient can report its own timeout as TaskCanceledException, which is also an OperationCanceledException. Treating every cancellation exception as a request to stop prevented the fallback from running when an endpoint was merely slow. The distinction is the caller's token: if that token was cancelled, stop; otherwise an HTTP timeout is an endpoint failure and the configured fallback can run.

That was exercised against a local delayed endpoint, not just a mocked exception. Both key and mappings requests timed out, the configured fallbacks still mounted the real archives, and the warnings named the failures. A separate check confirmed that cancelling the caller interrupted the request rather than silently falling back.

An export includes more than the mesh

Meshes can reference materials, and materials can reference several textures. export_mesh can export material parameter JSON and texture dependencies alongside the mesh. export_textures can start from the mesh and export its materials' textures without exporting the geometry.

Those files keep the in-game folder structure beneath the selected output directory. Flattening everything into one folder would throw away useful path information and invite collisions between assets with the same filename.

The report separates rootFiles, dependencyFiles and failures. That last field is important. A dependency can fail even when the root mesh was written, so a caller shouldn't equate "the request returned" with "the whole export succeeded". If no root produces a file, the operation returns an error rather than an empty success report. When exporting textures from a mesh, material slots that fail to resolve are reported instead of disappearing from the result.

Exports sharing an output directory are serialized. Two requests for different meshes can still reference the same texture, and shouldn't race while writing it.

For this release, the real-asset validation covered glTF mesh export, eight PNG textures from the mesh's materials, and a PSA animation. The converter also exposes ActorX, UEFormat and USD options, plus other texture formats, but those aren't all verified paths for v1.0.0. Other games still need their own matching keys, engine settings and mappings.

Reloading without leaking archive handles

A mounted game stays in memory so every tool call doesn't have to mount it again. There's also a reload operation for changes such as a game patch.

Replacing the provider brings a lifetime problem. Keeping the old one forever leaks archive resources. Disposing it immediately can invalidate an export that's still reading from it.

The server uses a lease for the entire operation:

using var lease = await games.AcquireAsync(game, ct);
var session = lease.Session;

The manager owns a reference to each current session, and an in-flight call owns another through its lease. Reload installs the new session and drops the manager's reference to the old one. The old provider is disposed when its last lease ends. Shutdown follows the same rule.

This was checked directly: hold a lease, reload, read through the old session, then release it and verify that the provider's file collection has been cleared. The shutdown check held an active lease across manager disposal and verified that the final release cleaned up the provider. Concurrent MCP search and reload were exercised too.

None of that changes the public tool arguments, but it makes repeated use of the server much less fragile.

Shipping an executable people can actually run

The v1.0.0 release is a self-contained Windows x64 ZIP. It includes the .NET runtime, the native animation DLL, a safe config template, installation instructions and dependency/license notices. Users don't need the SDK, CMake or Visual Studio to run it. Configured decompression libraries and mappings may still download on first game load.

The release workflow runs on stable version tags such as v1.0.0. It publishes without trimming or single-file bundling, packages the allowlisted files, extracts the ZIP, and starts the extracted CLI and MCP server before creating the GitHub Release. The tag determines the server version. A SHA256 checksum is attached alongside the ZIP.

Ordinary pushes to master only produce build artifacts. They don't turn every commit into a public release.

I also downloaded the ZIP from the published release, checked its checksum, and started the bundled server with the included example configuration. The relocated local bundle was tested against real Valorant archives as well. That catches a different set of mistakes from running an executable inside the source checkout, where installed runtimes and parent-directory configuration can hide packaging problems.

There is a known dependency caveat. CUE4Parse's ImageSharp 3.1.12 has published security advisories; the patched 4.1.2 package requires a valid Six Labors license during Release builds. This release retains 3.1.12 and leaves the audit warnings enabled. The affected TIFF, ICC and histogram operations weren't found in the exposed tool paths, but that doesn't establish that the dependency is safe. The README's security disclosure records the advisories and the limitation. Use trusted game archives, and don't treat this as a clean dependency audit.

Trying it

Download and fully extract the latest Windows ZIP. Copy games.example.json to games.json, then edit the game directory, keys and mappings for your installed build. The executable, template and configured file can live together in a permanent folder.

For Claude Code, registration from that folder is:

claude mcp add cue4parse --scope user --transport stdio -- "$PWD/Cue4ParseMcp.exe" --config "$PWD/games.json"

After restarting the client, start with list_games, then mount a game and search for a known asset. Exports are written on the machine running the server; the tool returns local paths, not uploaded attachments. With several games configured, pass the game id explicitly to avoid relying on whichever one was loaded last.

The installation guide covers other stdio clients, source builds and offline configuration. The source is available with CUE4Parse pinned as a submodule.