Finding assets — the full MCP tool loop
Companion to the short Finding assets section in this project's AGENTS.md. Reach for the asset tools when the asset is conventional; build anything specific to this game yourself in src/render/. A fetched asset has to match what the game needs, not merely exist.
On this page
Installing @threenative/core writes the .mcp.json that launches threenative-asset-mcp, so
your host lists its tools alongside your own. Your host reads that file from the directory it was
launched in: start the session in this project, not in a parent of it. It advertises 44; these 8
are the loop you will use for nearly everything:
For a humanoid that needs a skeleton or more motion, use the complete
rigging-characters.md recipe. Inspect with asset_inspect_rig,
preserve or fit a rig with asset_auto_rig, select and bake donor clips with
asset_retarget_animations, and confirm the result with asset_preview_animation on a nonblank
contact sheet. AETHER / 02 is the pinned sample; its 18-bone rig has no fingers or toes, and the
tools report those absent roles instead of inventing them.
For a bespoke animated creature, use the complete creating-creatures.md
recipe. It keeps the editable spec and claims under .threenative/creatures/, compiles the GLB
into the configured assets/ source, and requires independent preview review before game
delivery. The five creature tools are creature_status, creature_guide, creature_compile,
creature_preview and creature_check; call creature_status first because compile can work
without optional preview dependencies while a complete preview/check loop may not.
asset_search_sources— start here, never at a provider. It returns every catalogued source with its license summary, attribution requirement, browse URL, and whether an agent can complete a download from it. That output is the authority on what is reachable — not your memory of some other project.polyhaven_search_assets(CC0 models, textures, HDRIs),ambientcg_search_assets(CC0 materials and textures), oraudio_search_assets(Kenney, Sonniss).polyhaven_list_files/ambientcg_list_files— the license, official URL, byte size and md5 of every resolution and format. Read this before downloading, not after, and pick a sane one: Poly Haven lists 16k PNGs over 1 GB beside 8k JPEGs at 28 MB. A game does not need the 16k.asset_download_filefor textures and models,audio_download_assetfor audio. Both takeacceptLicense: true— you are asserting you read step 3. They ignore any path you pass and write to the directories.mcp.jsonsets (public/assets/<provider>/<sha>/andpublic/audio/<source>/<pack>/); without that config they would write to~/Downloadsand never reach the game.- Append the file, its source, its license and its URL to
CREDITS.mdbefore the turn ends. Poly Haven requires a visible Poly Haven credit when its API is used, ambientCG is CC0 per asset page, and audio and bundle licenses are per pack.
A Poly Haven model takes one extra step, and no tool does it for you. Poly Haven publishes
models as a .gltf beside a .bin and loose texture files and never as a .glb, so the file
list is the whole model: call polyhaven_list_files with the assetId, a resolution such as
1k, and includeDependencies: true, then asset_download_file once per entry — the .gltf and
every URL the tool reported as a dependency — with provider: "polyhaven", the url and fileName
the listing gave, and acceptLicense: true. Keep the downloaded names exactly as the listing
spelled them: the glTF's relative URIs resolve against them, so a renamed texture loads as a
missing file rather than as an error. Serve that directory from public/ and load it with
ctx.assets.model().
No tool packs those files into one self-contained GLB. If you want one, convert the downloaded
.gltf in Blender or with gltf-transform in a build step, and report the triangle count and
per-texture dimensions yourself — create-threenative inspect does not read a Poly Haven listing,
so those two numbers come from the conversion. Textures and HDRIs stay on asset_download_file.
Never state a license you did not read off a tool result. If polyhaven_list_files or
ambientcg_search_assets did not tell you, you do not know it.
What arrives is usually a ZIP, not a texture
ambientCG and Kenney ship archives: unpack one, keep only the maps you actually use (_Color,
_NormalGL, _Roughness — not the .blend, .usdc or displacement), and put those beside
your code under public/. A 1K JPEG set is right for a game; the 8K set of the same material
is 200 MB.
Two argument shapes that bite
Both learned the hard way: ambientcg_search_assets takes lowercase type values (material,
hdri, 3d-model), and the audio catalog is pack-level — audio_search_assets matches
pack names, so query: "pickup coin" returns nothing while kind: "sfx" returns the packs
that exist. Pick a pack, download it, unzip it, and choose a file yourself.
Fab: searching, and importing an Unreal asset you own
Most Fab listings ship Unreal-only — 272 .uasset files and no .glb anywhere — so Fab is two
steps, not one: find the listing, then convert it.
1. Search free first. fab_search_assets talks to Fab's public API anonymously; no account is
involved. Its default priceMode: "free" is the right default and should stay your first query —
a free asset is one anybody who clones this project can fetch too.
// fab_search_assets
{
"query": "cave rocks",
"formats": ["unreal-engine"], // "gltf", "fbx", "obj", "unity" ... omit for every format
"priceMode": "any", // SEE BELOW — the default is "free"
"sort": "relevance", // or rating, newest, price_asc, recently_updated
"limit": 24
}Then look at what the account already owns. A paid listing sitting in the library costs
nothing further to use, and it will never appear in a free search — priceMode: "free" returns a
different result set than "any" does. fab_list_owned answers that directly, without guessing
at search terms:
// fab_list_owned
{ "unrealOnly": true, "query": "vegetation" } // both optional; unrealOnly drops engine installs
// and plugins the importer cannot take
// -> [{ listingId, title, url, categories, engineVersions, hasUnrealArtifact }]Reach for priceMode: "any" in fab_search_assets only when you mean to look at paid listings
the account does not own yet — buying is not something these tools can do for you.
Each item carries a title and an id — that id is the listing UID the next two tools take.
fab_list_filters returns the usable formats, categories and tags slugs; guessing them
silently narrows the search instead of erroring.
2. Check the licence. Search results carry no licence — Fab's search endpoint omits it, so
every item comes back with an empty licenses array. fab_get_asset is where the slugs live:
{ "listingIdOrUrl": "75f42402-40bb-4a1b-b557-18e2c9604273" }
// -> licenses: [{ slug: "personal" }, { slug: "professional" }], freeLicenseSlugs: [...]3. Import. fab_import_asset verifies the entitlement itself, downloads through the FabCLI
session, converts every static mesh, and writes source GLBs into your assets/:
// fab_import_asset
{
"listingIdOrUrl": "https://www.fab.com/listings/<uid>",
"outputDir": "assets/fab/soul-cave",
"packages": ["SM_S_Soul_Statue"], // omit to convert every static mesh — packs run to gigabytes
"maxTextureSize": 2048, // omit to keep Unreal's own resolution
"acceptFabEula": true
}Then load the returned path the ordinary way:
ctx.assets.model("fab/soul-cave/.../SM_S_Soul_Statue.glb"). The normal asset compiler picks the
GLBs up from assets/ with no extra configuration.
Cook the pack with the default settings. models: "none" and textures: "none" in
threenative.config.ts are not a fix, and a reason to reach for them is a bug report, not a
workaround. A converted Fab pack is ordinary input to that cook, and the cook is where an
expensive import stops being expensive: embedded PNG and WebP maps are transcoded to KTX2 and
capped, mesh attributes are quantized and Meshopt-compressed, and a MASK (alpha-cutout)
primitive is handed a discrete LOD chain, so the LOD0 triangle count in the GLB is not the
triangle count the scene pays for. One measured tree: SM_EuropeanHornbeam_Forest_01.glb cooks
126,934,280 → 26,428,684 B, its six embedded PNGs → KTX2 (45.4 MB → 16.4 MB), and its
755,677-triangle foliage primitive down to a 5.1% far rung. Turning a pass off ships every source
byte verbatim instead, counts all of them against assets.budget.uncooked, and is precisely what
makes threenative build fail with TN_ASSETS_BUDGET_EXCEEDED — read dist.build-report.json
before you conclude a pack is too big. An extension the cook owns no pass for (.dna,
.strands.bin, a vendor data file) is copied through and still resolves through the same manifest
entry, so nothing needs public/ to serve it. Ignore what the cook writes beside the manifest —
public/basis/, public/shared/ and public/bake.receipt.json are outputs too, and a
content-addressed name is not the only output name.
What the import will and will not do:
- A pack of uncooked editor assets cannot be converted at all, and this is the common case for
marketplace packs, which ship as Unreal source projects. Cooking is what writes the renderable
vertex and index buffers into the file; an uncooked package keeps only the editable source mesh
and lets the editor derive the rest, so there is no geometry for UE Viewer to read. The import
detects this and fails
UNREAL_SOURCE_UNCOOKEDnaming the evidence it found —/Script/UnrealEd,AssetImportData,SourceModels. Believe it and stop. No engine version,packagesfilter or flag changes the answer, and the download is gigabytes each time you try. The only route to those meshes is opening the pack in Unreal Editor and exporting FBX or glTF yourself. Prefer a listing that ships a non-Unreal format, and treat one that does not as a likely dead end before you spend the bandwidth. - A free listing still has to be in the library first.
fab_import_assetrefuses withFABCLI_NOT_OWNEDuntil the listing is claimed, and it never claims — by design. Runfabcli claim <uid>, which needs the interactivefabcli auth loginsession rather than the API tokenfabcli auth statusreports as authenticated. Note also thatfab_get_assetreports a free CC-BY offer asisFree: falsewith an emptyfreeLicenseSlugs; the listing appearing infab_search_assetswithpriceMode: "free"atstartingPrice.amount: 0is the reliable signal, so check the search before concluding a listing costs money. - Only assets you already own, and only under Fab Standard (Personal or Professional) or
CC-BY. Unreal-Engine-only and legacy entitlements are refused, and a licence it cannot read is
refused too. The MCP never logs in, claims, or purchases. If FabCLI needs a session, the agent
must proactively run
fabcli auth loginand complete it through Claude browser or Codex'schrome:control-chrome, reusing the user's active signed-in browser session. Never inspect, copy, print, or persist cookies or tokens; interact with the login page instead. Searching andfab_get_assetstay anonymous, so the licence check never depends on the authenticated path it is guarding. - If the WebView login fails, use the manual paste flow. On Wayland the WebView dies with Gdk
"Error 71 (Protocol error)", and
GDK_BACKEND=x11still may not let the user sign in.fabcli auth login --manualrefuses a non-TTY stdin, so run it under a pty fed by a FIFO:mkfifo f; (sleep 1200 > f &); (script -qfec "fabcli auth login --manual" /dev/null < f > log &). The user opens Epic's redirect endpoint (https://www.epicgames.com/id/api/redirect?clientId=<fabcli's client id>&responseType=code;strings "$(command -v fabcli)" | grep epicgames.com/id/loginprints the full login URL) in their own signed-in browser. It returns JSON with anauthorizationCode; the user pastes that value to the agent (or, when asked, the agent reads it from the page through browser control), and the agent writes it withprintf '%s\n' "$CODE" > fwithout echoing it. The pty echoes the code intolog, so keep that file private and delete it oncefabcli auth statusreportsauthenticated: true. The separate Fab web session can still readsession_present: false;fab_import_assetresolves ownership through the library and works regardless. Never runpkill -f <pattern>in the same shell command that names the pattern: it matches its own command line and kills the shell. - Two external tools install themselves on first use (FabCLI and UE Viewer). Set
THREENATIVE_TOOLCHAIN_AUTOINSTALL=0to require you install them instead. Linux and Windows only. packagesis not optional in practice. A marketplace pack converts to many gigabytes of GLBs; name the handful of meshes your scene uses. Run it once withoutpackagesagainst a scratch directory if you need to see what a pack contains, then re-run with the names.- Read
import-report.jsonbefore trusting the result. It reportsmaterials: "complete"or"degraded", counts textured against total sections, and names every mesh it skipped and every texture it could not map. Unreal shader graphs have no glTF equivalent, so some sections arrive with a neutral grey and say so..umaplevels, Blueprints, Niagara and foliage placement are reported as unsupported, never silently dropped. asset_import_unrealtakes a localsourceDirinstead of a listing, for a pack you already downloaded by any means.
fab_download_free_asset is the unrelated older path: it downloads a directly-available free
glb/fbx/obj/unity file with no account at all. Use it when the listing already ships a
format you can load, and the importer only when it does not.
From a downloaded .fbx to a running character
An .fbx, .blend, .obj or .dae is not a file the runtime can load. Put it in your assets
directory anyway and the build converts it:
- Download it —
fab_download_free_asset,asset_import_unreal,asset_download_bundle_entry, or by hand. - Write it into
assets/, beside your.glbfiles. Nothing else to configure. - Build.
compileAssetsclassifies it as a model, converts it to GLB through Blender, then optimizes it exactly like an authored GLB — skeleton, animation clips, materials and embedded textures survive. - Load it the ordinary way:
ctx.assets.model("hero").
Blender must be installed, and the framework never installs it for you. With no Blender the build fails and names the install command for your platform — it does not copy the unusable file through and report success. Ask the user before installing anything; it is roughly 350 MB.
The threenative-blender MCP server answers the rest:
blender_status— can this machine convert at all? Never fails; with no Blender it returnsavailable: falseand the install command. Call it first.blender_inspect— what is in this file? Mesh, triangle, vertex and bone counts, material names, clip names, image names. Use it before committing a download.blender_convert— write a GLB somewhere other than the build's output. Zero meshes out is a failure, never an empty GLB.blender_recipes— the shipped bpy recipes (decimate,unwrap,bake_ao,retarget) and their full source text. Read one and adapt it; do not write bpy cold.blender_run_python— run a bpy script of your own. It is not a sandbox and does not claim to be; it gives you a resolved Blender and the same fail-closed handling, not extra privilege.
The narrower tools
The directory spells out the conditions on each. The Fab tools are covered above; the server
never purchases anything on any path. smithsonian_search_assets returns museum scans at scan resolution, which this project has no
pipeline to decimate — that geometry is the wrong shape for a game. When in doubt check
asset_search_sources first; its caution and license fields are the current truth for the
pinned version, and they change between versions.
Load what you downloaded the ordinary way — ctx.assets.model("crate.glb"),
ctx.assets.texture(...), ctx.assets.audio(...) — and write your own material and lighting
around it in src/render/. The framework ships no asset and picks none for you.