threenative-asset-mcp 0.9.4 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +252 -4
- package/dist/ambientcg/client.d.ts +7 -4
- package/dist/ambientcg/client.d.ts.map +1 -1
- package/dist/ambientcg/client.js +71 -62
- package/dist/ambientcg/client.js.map +1 -1
- package/dist/audio/generate.d.ts +3 -0
- package/dist/audio/generate.d.ts.map +1 -1
- package/dist/audio/generate.js +20 -2
- package/dist/audio/generate.js.map +1 -1
- package/dist/browser-temp.d.ts +13 -0
- package/dist/browser-temp.d.ts.map +1 -0
- package/dist/browser-temp.js +17 -0
- package/dist/browser-temp.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/creature/preview.d.ts.map +1 -1
- package/dist/creature/preview.js +41 -20
- package/dist/creature/preview.js.map +1 -1
- package/dist/creature/runner.d.ts +12 -1
- package/dist/creature/runner.d.ts.map +1 -1
- package/dist/creature/runner.js +30 -16
- package/dist/creature/runner.js.map +1 -1
- package/dist/data-texture-loader.d.ts +43 -0
- package/dist/data-texture-loader.d.ts.map +1 -0
- package/dist/data-texture-loader.js +67 -0
- package/dist/data-texture-loader.js.map +1 -0
- package/dist/discovery/acquisition.d.ts +45 -0
- package/dist/discovery/acquisition.d.ts.map +1 -0
- package/dist/discovery/acquisition.js +372 -0
- package/dist/discovery/acquisition.js.map +1 -0
- package/dist/discovery/adapters.d.ts +16 -0
- package/dist/discovery/adapters.d.ts.map +1 -0
- package/dist/discovery/adapters.js +557 -0
- package/dist/discovery/adapters.js.map +1 -0
- package/dist/discovery/federation.d.ts +187 -0
- package/dist/discovery/federation.d.ts.map +1 -0
- package/dist/discovery/federation.js +373 -0
- package/dist/discovery/federation.js.map +1 -0
- package/dist/discovery/metadata.d.ts +12 -0
- package/dist/discovery/metadata.d.ts.map +1 -0
- package/dist/discovery/metadata.js +88 -0
- package/dist/discovery/metadata.js.map +1 -0
- package/dist/discovery/types.d.ts +97 -0
- package/dist/discovery/types.d.ts.map +1 -0
- package/dist/discovery/types.js +31 -0
- package/dist/discovery/types.js.map +1 -0
- package/dist/download/direct-asset-downloader.d.ts +8 -0
- package/dist/download/direct-asset-downloader.d.ts.map +1 -1
- package/dist/download/direct-asset-downloader.js +92 -30
- package/dist/download/direct-asset-downloader.js.map +1 -1
- package/dist/fab/browser-transport.d.ts.map +1 -1
- package/dist/fab/browser-transport.js +33 -9
- package/dist/fab/browser-transport.js.map +1 -1
- package/dist/fab/fabcli.d.ts +30 -3
- package/dist/fab/fabcli.d.ts.map +1 -1
- package/dist/fab/fabcli.js +116 -13
- package/dist/fab/fabcli.js.map +1 -1
- package/dist/fab/routes.d.ts +23 -0
- package/dist/fab/routes.d.ts.map +1 -0
- package/dist/fab/routes.js +63 -0
- package/dist/fab/routes.js.map +1 -0
- package/dist/kenney/client.d.ts +41 -0
- package/dist/kenney/client.d.ts.map +1 -0
- package/dist/kenney/client.js +200 -0
- package/dist/kenney/client.js.map +1 -0
- package/dist/polyhaven/client.d.ts +9 -5
- package/dist/polyhaven/client.d.ts.map +1 -1
- package/dist/polyhaven/client.js +99 -55
- package/dist/polyhaven/client.js.map +1 -1
- package/dist/rig/animation-catalog.json +1 -1
- package/dist/rig/preview.d.ts +12 -1
- package/dist/rig/preview.d.ts.map +1 -1
- package/dist/rig/preview.js +130 -65
- package/dist/rig/preview.js.map +1 -1
- package/dist/rig/retarget.d.ts +14 -0
- package/dist/rig/retarget.d.ts.map +1 -1
- package/dist/rig/retarget.js +224 -36
- package/dist/rig/retarget.js.map +1 -1
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +52 -1
- package/dist/server.js.map +1 -1
- package/dist/sketchfab/client.d.ts +8 -5
- package/dist/sketchfab/client.d.ts.map +1 -1
- package/dist/sketchfab/client.js +71 -66
- package/dist/sketchfab/client.js.map +1 -1
- package/dist/smithsonian/client.d.ts +6 -3
- package/dist/smithsonian/client.d.ts.map +1 -1
- package/dist/smithsonian/client.js +65 -48
- package/dist/smithsonian/client.js.map +1 -1
- package/dist/tools/download-free-asset.d.ts +1 -1
- package/dist/tools/download-free-asset.d.ts.map +1 -1
- package/dist/tools/federated-assets.d.ts +381 -0
- package/dist/tools/federated-assets.d.ts.map +1 -0
- package/dist/tools/federated-assets.js +143 -0
- package/dist/tools/federated-assets.js.map +1 -0
- package/dist/tools/import-unreal.d.ts +82 -0
- package/dist/tools/import-unreal.d.ts.map +1 -1
- package/dist/tools/import-unreal.js +106 -6
- package/dist/tools/import-unreal.js.map +1 -1
- package/dist/tools/rig.d.ts +4 -0
- package/dist/tools/rig.d.ts.map +1 -1
- package/dist/tools/rig.js +10 -2
- package/dist/tools/rig.js.map +1 -1
- package/dist/tools/source-directory.js +1 -1
- package/dist/tools/source-directory.js.map +1 -1
- package/dist/unreal/albedo-fidelity.d.ts +86 -0
- package/dist/unreal/albedo-fidelity.d.ts.map +1 -0
- package/dist/unreal/albedo-fidelity.js +207 -0
- package/dist/unreal/albedo-fidelity.js.map +1 -0
- package/dist/unreal/contact-sheet.d.ts +214 -0
- package/dist/unreal/contact-sheet.d.ts.map +1 -0
- package/dist/unreal/contact-sheet.js +782 -0
- package/dist/unreal/contact-sheet.js.map +1 -0
- package/dist/unreal/cooking.d.ts +15 -0
- package/dist/unreal/cooking.d.ts.map +1 -1
- package/dist/unreal/cooking.js +23 -2
- package/dist/unreal/cooking.js.map +1 -1
- package/dist/unreal/cue4parse-adapter.d.ts +7 -1
- package/dist/unreal/cue4parse-adapter.d.ts.map +1 -1
- package/dist/unreal/cue4parse-adapter.js +3286 -243
- package/dist/unreal/cue4parse-adapter.js.map +1 -1
- package/dist/unreal/data-texture-promotion.d.ts +49 -0
- package/dist/unreal/data-texture-promotion.d.ts.map +1 -0
- package/dist/unreal/data-texture-promotion.js +104 -0
- package/dist/unreal/data-texture-promotion.js.map +1 -0
- package/dist/unreal/data-texture.d.ts +79 -0
- package/dist/unreal/data-texture.d.ts.map +1 -0
- package/dist/unreal/data-texture.js +186 -0
- package/dist/unreal/data-texture.js.map +1 -0
- package/dist/unreal/engine-content.d.ts +40 -0
- package/dist/unreal/engine-content.d.ts.map +1 -0
- package/dist/unreal/engine-content.js +96 -0
- package/dist/unreal/engine-content.js.map +1 -0
- package/dist/unreal/errors.d.ts +13 -0
- package/dist/unreal/errors.d.ts.map +1 -0
- package/dist/unreal/errors.js +11 -0
- package/dist/unreal/errors.js.map +1 -0
- package/dist/unreal/executable-publish.d.ts +15 -0
- package/dist/unreal/executable-publish.d.ts.map +1 -0
- package/dist/unreal/executable-publish.js +46 -0
- package/dist/unreal/executable-publish.js.map +1 -0
- package/dist/unreal/fidelity-baseline.d.ts +30 -0
- package/dist/unreal/fidelity-baseline.d.ts.map +1 -0
- package/dist/unreal/fidelity-baseline.js +52 -0
- package/dist/unreal/fidelity-baseline.js.map +1 -0
- package/dist/unreal/graph-baker.d.ts +222 -0
- package/dist/unreal/graph-baker.d.ts.map +1 -0
- package/dist/unreal/graph-baker.js +628 -0
- package/dist/unreal/graph-baker.js.map +1 -0
- package/dist/unreal/graph-dump.d.ts +213 -0
- package/dist/unreal/graph-dump.d.ts.map +1 -0
- package/dist/unreal/graph-dump.js +345 -0
- package/dist/unreal/graph-dump.js.map +1 -0
- package/dist/unreal/grooms.d.ts +35 -0
- package/dist/unreal/grooms.d.ts.map +1 -0
- package/dist/unreal/grooms.js +297 -0
- package/dist/unreal/grooms.js.map +1 -0
- package/dist/unreal/image-diff.d.ts +129 -0
- package/dist/unreal/image-diff.d.ts.map +1 -0
- package/dist/unreal/image-diff.js +489 -0
- package/dist/unreal/image-diff.js.map +1 -0
- package/dist/unreal/importer.d.ts +241 -10
- package/dist/unreal/importer.d.ts.map +1 -1
- package/dist/unreal/importer.js +2365 -186
- package/dist/unreal/importer.js.map +1 -1
- package/dist/unreal/material-graph.d.ts +260 -0
- package/dist/unreal/material-graph.d.ts.map +1 -0
- package/dist/unreal/material-graph.js +3479 -0
- package/dist/unreal/material-graph.js.map +1 -0
- package/dist/unreal/material-metadata.d.ts +39 -0
- package/dist/unreal/material-metadata.d.ts.map +1 -0
- package/dist/unreal/material-metadata.js +120 -0
- package/dist/unreal/material-metadata.js.map +1 -0
- package/dist/unreal/materials.d.ts +55 -5
- package/dist/unreal/materials.d.ts.map +1 -1
- package/dist/unreal/materials.js +360 -61
- package/dist/unreal/materials.js.map +1 -1
- package/dist/unreal/mesh-build-scale.d.ts +9 -0
- package/dist/unreal/mesh-build-scale.d.ts.map +1 -0
- package/dist/unreal/mesh-build-scale.js +123 -0
- package/dist/unreal/mesh-build-scale.js.map +1 -0
- package/dist/unreal/package-thumbnail.d.ts +42 -0
- package/dist/unreal/package-thumbnail.d.ts.map +1 -0
- package/dist/unreal/package-thumbnail.js +219 -0
- package/dist/unreal/package-thumbnail.js.map +1 -0
- package/dist/unreal/parity-run.d.ts +438 -0
- package/dist/unreal/parity-run.d.ts.map +1 -0
- package/dist/unreal/parity-run.js +1001 -0
- package/dist/unreal/parity-run.js.map +1 -0
- package/dist/unreal/parity.d.ts +138 -0
- package/dist/unreal/parity.d.ts.map +1 -0
- package/dist/unreal/parity.js +538 -0
- package/dist/unreal/parity.js.map +1 -0
- package/dist/unreal/preview-reference-workflow.d.ts +77 -0
- package/dist/unreal/preview-reference-workflow.d.ts.map +1 -0
- package/dist/unreal/preview-reference-workflow.js +106 -0
- package/dist/unreal/preview-reference-workflow.js.map +1 -0
- package/dist/unreal/property-dump.d.ts +128 -0
- package/dist/unreal/property-dump.d.ts.map +1 -0
- package/dist/unreal/property-dump.js +83 -0
- package/dist/unreal/property-dump.js.map +1 -0
- package/dist/unreal/provision.d.ts +145 -13
- package/dist/unreal/provision.d.ts.map +1 -1
- package/dist/unreal/provision.js +711 -144
- package/dist/unreal/provision.js.map +1 -1
- package/dist/unreal/render-fidelity.d.ts +142 -0
- package/dist/unreal/render-fidelity.d.ts.map +1 -0
- package/dist/unreal/render-fidelity.js +361 -0
- package/dist/unreal/render-fidelity.js.map +1 -0
- package/dist/unreal/scenes.d.ts.map +1 -1
- package/dist/unreal/scenes.js +24 -0
- package/dist/unreal/scenes.js.map +1 -1
- package/dist/unreal/source-material-package.d.ts +36 -0
- package/dist/unreal/source-material-package.d.ts.map +1 -0
- package/dist/unreal/source-material-package.js +320 -0
- package/dist/unreal/source-material-package.js.map +1 -0
- package/dist/unreal/source-material.d.ts +42 -0
- package/dist/unreal/source-material.d.ts.map +1 -0
- package/dist/unreal/source-material.js +400 -0
- package/dist/unreal/source-material.js.map +1 -0
- package/dist/unreal/spatial-atlas.d.ts +39 -0
- package/dist/unreal/spatial-atlas.d.ts.map +1 -0
- package/dist/unreal/spatial-atlas.js +302 -0
- package/dist/unreal/spatial-atlas.js.map +1 -0
- package/dist/unreal/spatial-remap.d.ts +54 -0
- package/dist/unreal/spatial-remap.d.ts.map +1 -0
- package/dist/unreal/spatial-remap.js +273 -0
- package/dist/unreal/spatial-remap.js.map +1 -0
- package/dist/unreal/spatial-surface.d.ts +88 -0
- package/dist/unreal/spatial-surface.d.ts.map +1 -0
- package/dist/unreal/spatial-surface.js +362 -0
- package/dist/unreal/spatial-surface.js.map +1 -0
- package/dist/unreal/static-mesh-sections.d.ts +42 -0
- package/dist/unreal/static-mesh-sections.d.ts.map +1 -0
- package/dist/unreal/static-mesh-sections.js +381 -0
- package/dist/unreal/static-mesh-sections.js.map +1 -0
- package/dist/unreal/surface-normals.d.ts +21 -0
- package/dist/unreal/surface-normals.d.ts.map +1 -0
- package/dist/unreal/surface-normals.js +106 -0
- package/dist/unreal/surface-normals.js.map +1 -0
- package/dist/unreal/texture-proof.d.ts +135 -0
- package/dist/unreal/texture-proof.d.ts.map +1 -0
- package/dist/unreal/texture-proof.js +439 -0
- package/dist/unreal/texture-proof.js.map +1 -0
- package/dist/unreal/thumbnail-floor.d.ts +45 -0
- package/dist/unreal/thumbnail-floor.d.ts.map +1 -0
- package/dist/unreal/thumbnail-floor.js +83 -0
- package/dist/unreal/thumbnail-floor.js.map +1 -0
- package/dist/unreal/thumbnail-lighting.d.ts +62 -0
- package/dist/unreal/thumbnail-lighting.d.ts.map +1 -0
- package/dist/unreal/thumbnail-lighting.js +125 -0
- package/dist/unreal/thumbnail-lighting.js.map +1 -0
- package/dist/unreal/thumbnail-reference-profile.d.ts +39 -0
- package/dist/unreal/thumbnail-reference-profile.d.ts.map +1 -0
- package/dist/unreal/thumbnail-reference-profile.js +218 -0
- package/dist/unreal/thumbnail-reference-profile.js.map +1 -0
- package/dist/unreal/toolchain.d.ts.map +1 -1
- package/dist/unreal/toolchain.js +4 -0
- package/dist/unreal/toolchain.js.map +1 -1
- package/dist/unreal/umodel-export.d.ts +15 -0
- package/dist/unreal/umodel-export.d.ts.map +1 -0
- package/dist/unreal/umodel-export.js +90 -0
- package/dist/unreal/umodel-export.js.map +1 -0
- package/dist/unreal/visual-judge.d.ts +100 -0
- package/dist/unreal/visual-judge.d.ts.map +1 -0
- package/dist/unreal/visual-judge.js +200 -0
- package/dist/unreal/visual-judge.js.map +1 -0
- package/package.json +18 -5
package/README.md
CHANGED
|
@@ -34,7 +34,12 @@ dependency relationships with pagination and resolution/format filters.
|
|
|
34
34
|
- Node.js 20.19 or newer
|
|
35
35
|
- A local environment capable of running Playwright Chromium when Fab requests
|
|
36
36
|
browser verification
|
|
37
|
-
- No Epic or Fab login is required or automated
|
|
37
|
+
- No Epic or Fab login is required or automated for search and free downloads.
|
|
38
|
+
`fab_import_asset` and `fab_list_owned` use your own FabCLI session; see
|
|
39
|
+
[Fab login for owned packs](#fab-login-for-owned-packs)
|
|
40
|
+
- **On Linux, `g++`, `perl`, `zlib1g-dev`, `libpng-dev` and `libsdl2-dev`** for Unreal imports
|
|
41
|
+
of UE4.0–4.20 packs. UE Viewer's Linux prebuilt is a 32-bit binary that 64-bit-only
|
|
42
|
+
hosts cannot run, so the importer builds it from source once
|
|
38
43
|
- **FFmpeg and FFprobe on `PATH`** for `audio_inspect_asset` and
|
|
39
44
|
`audio_generate_sound`. Without them nothing is decoded, and the tools report
|
|
40
45
|
that they could not check rather than reporting a pass.
|
|
@@ -316,6 +321,38 @@ Unified source and download routing:
|
|
|
316
321
|
revalidated, existing files are never overwritten, and the result includes
|
|
317
322
|
SHA-256.
|
|
318
323
|
|
|
324
|
+
The additive `asset_search` tool searches individual `3d-model`, `texture`,
|
|
325
|
+
`hdri` and `animation` candidates across supported clients. It reports each
|
|
326
|
+
provider, bounded results and local continuation actions. Unknown prices fail
|
|
327
|
+
`freeOnly`; unknown rights fail explicit commercial-use/attribution filters.
|
|
328
|
+
Fab remains a specialized action and generic Sketchfab uses anonymous metadata,
|
|
329
|
+
without authenticated acquisition. Existing tools retain their public names.
|
|
330
|
+
|
|
331
|
+
Use `asset_get` with the returned stable `assetId` to inspect current terms and
|
|
332
|
+
available variants. It prefers GLB, then glTF with its supplied dependencies;
|
|
333
|
+
explicit unavailable formats return alternatives. The read-only call returns an
|
|
334
|
+
expiring `planToken`, required relative files and acknowledgement information.
|
|
335
|
+
A plan is not consent. Call `asset_download` with that token and explicit
|
|
336
|
+
`acceptLicense: true` only after reviewing the terms. Optional `maxBytes` can
|
|
337
|
+
only tighten the configured cap. Changed terms/files invalidate the plan.
|
|
338
|
+
|
|
339
|
+
Acquisition stages originals under `ASSET_DOWNLOAD_DIR/acquisitions`, verifies
|
|
340
|
+
checksums and all glTF buffer/texture references, then atomically publishes a
|
|
341
|
+
complete directory with `asset.acquisition.json`. Replays verify cached bytes
|
|
342
|
+
and provenance; a corrupt cache fails rather than being silently accepted.
|
|
343
|
+
Receipts preserve asset attribution separately from Poly Haven API credit.
|
|
344
|
+
No transfer/signed URLs or credentials enter the new public plan or receipt.
|
|
345
|
+
Archives stay archives with `requiresExtraction: true`; runtime readiness is
|
|
346
|
+
`unverified`. Acquiring originals does not change or adopt assets into a project.
|
|
347
|
+
|
|
348
|
+
Kenney 3D packs are enabled by default; `ASSET_ENABLE_KENNEY=0` disables the
|
|
349
|
+
generic adapter. Each query uses one official listing page and at most three
|
|
350
|
+
item detail checks,
|
|
351
|
+
paces requests, verifies item CC0/free/official-ZIP evidence and never crawls
|
|
352
|
+
or extracts archives. One official Mini Dungeon archive was qualified through
|
|
353
|
+
the packed public MCP on 2026-10-06. Broader catalog coverage and runtime import
|
|
354
|
+
remain unverified; ordinary tests use hand-authored local fixtures.
|
|
355
|
+
|
|
319
356
|
Recommended agent flow:
|
|
320
357
|
|
|
321
358
|
1. Call `asset_search_sources` with the requested category/query. Its default
|
|
@@ -346,13 +383,58 @@ it out of process. Modern UE5 packages rejected by UE Viewer use a pinned,
|
|
|
346
383
|
out-of-process CUE4Parse adapter. Set `THREENATIVE_TOOLCHAIN_AUTOINSTALL=0` to
|
|
347
384
|
require manually installed tools instead.
|
|
348
385
|
|
|
386
|
+
### Fab login for owned packs
|
|
387
|
+
|
|
388
|
+
`fab_import_asset` and `fab_list_owned` read your Fab library through
|
|
389
|
+
[FabCLI](https://github.com/zirklerite/FabCLI), which the importer installs on first
|
|
390
|
+
use. The MCP never signs in for you. Log in once; the session is kept in the OS
|
|
391
|
+
keyring (Secret Service on Linux) and lasts about 90 days:
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
~/.cache/threenative-asset-mcp/toolchain/fabcli/fabcli auth login # opens a login window
|
|
395
|
+
~/.cache/threenative-asset-mcp/toolchain/fabcli/fabcli auth login --manual # no window: paste a code
|
|
396
|
+
~/.cache/threenative-asset-mcp/toolchain/fabcli/fabcli auth status
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
`--manual` reuses the browser where you are already signed in to Epic. FabCLI prints a
|
|
400
|
+
`epicgames.com/id/login?redirectUrl=…` link; open it there, copy `authorizationCode` from the
|
|
401
|
+
JSON the page shows, and paste it into the prompt within a few minutes. It needs an
|
|
402
|
+
interactive terminal. In Claude Code, run it with a `!` prefix. Never paste the code into an
|
|
403
|
+
untrusted service: it grants full access to the Epic account.
|
|
404
|
+
|
|
405
|
+
`fab_list_owned` lists each listing's Unreal artifacts separately (`artifacts[]`: `artifactId`,
|
|
406
|
+
`engineVersions`, `oldestEngine`, decoder `route`, `targetPlatforms`). An artifact's format is its
|
|
407
|
+
oldest listed engine, and that picks the decoder. When a listing has several artifacts and
|
|
408
|
+
`fab_import_asset` gets neither `engine` nor `artifactId`, it takes the best route (CUE4Parse, then
|
|
409
|
+
the MeshDescription converter, then UE Viewer; the newest source format breaks a tie) and says so
|
|
410
|
+
in the report `warnings`. Pass `artifactId` or `engine` to override.
|
|
411
|
+
|
|
412
|
+
**Graph-only colour.** Some Unreal materials compute BaseColor in the material graph (layered
|
|
413
|
+
masters that blend colour textures by mask channels and tint them), so no texture binding can
|
|
414
|
+
reproduce them. A section left without a base colour is baked from its graph: the pack's editor
|
|
415
|
+
package is read with CUE4Parse, a closed node set is evaluated per texel, and the result is a
|
|
416
|
+
base-colour PNG with `source: "graph"` in the section's bindings. The bake is `exact` only when
|
|
417
|
+
every node is understood; engine layer functions that are not in the pack, a mesh without vertex
|
|
418
|
+
colours standing in white, and a UV set the importer drops make it `heuristic`, and the section's
|
|
419
|
+
`graph` field names each approximation. View-dependent nodes (`Fresnel`, `DepthFade`, `TwoSidedSign`)
|
|
420
|
+
stand in as a recorded approximation: a bake has no camera, so `Fresnel` is its mean over a sphere's visible
|
|
421
|
+
surface, `DepthFade` is fully faded in and `TwoSidedSign` is the front face. Nodes it cannot evaluate (for
|
|
422
|
+
example `ReflectionVectorWS` feeding a cubemap lookup, or `Time`) leave the neutral fallback and are listed
|
|
423
|
+
under `graph.unsupportedNodes`.
|
|
424
|
+
The first bake installs the CUE4Parse converter and its private .NET SDK (hundreds of MB, once);
|
|
425
|
+
pass `graphBake: false` to the importer, or set `THREENATIVE_TOOLCHAIN_AUTOINSTALL=0`, to skip it.
|
|
426
|
+
`fab_import_asset` also skips it when its environment has `THREENATIVE_GRAPH_BAKE=0` (the parity
|
|
427
|
+
sweep's `--no-graph-bake` uses this to produce a baseline).
|
|
428
|
+
|
|
349
429
|
| Input | Current result |
|
|
350
430
|
| --- | --- |
|
|
351
431
|
| Cooked loose UE4 static meshes and textures | GLB geometry, LOD sections, embedded textures, and common PBR reconstruction |
|
|
352
432
|
| Cooked loose UE4 skeletal meshes and standalone animation packages | Standard skinned GLB; joints, weights, inverse-bind matrices, and existing clips are preserved; ActorX PSA clips are attached by case-insensitive bone name when at least 80% of tracks match, without duplicating an existing clip name |
|
|
353
433
|
| Uncooked UE4 `FMeshDescription` static meshes (object versions 517–522) | GLB geometry with centimetres converted to metres; verified on Fab Office Pack Vol.1 (47/47 meshes) |
|
|
434
|
+
| Uncooked UE5 editor static meshes (source models only, no render data) | LOD0 of the `FMeshDescription` source model is decoded from the package trailer's Oodle payloads, with the cooked glTF writer's axes, scale, and material naming. Verified on Fab's Megascans Common Hazel and European Hornbeam UE5.1 artifacts: triangle counts match the UE4.27 decoder of the same scans exactly |
|
|
354
435
|
| Uncooked UE4.0–4.20 static and skeletal meshes (object versions below 517) | Exported by UE Viewer from the editor source data, with materials, skins, and ActorX clips like cooked packages. Verified on Fab packs from UE4.10 to UE4.20: Modular Building Set (129 meshes), Animal Variety Pack (6 rigged animals, 146 clips), STF Landscape Pro 2.0 (61), Power Plant (65), and a UE4.19 fern pack (38) |
|
|
355
436
|
| Standalone `Texture2D` packages | Collision-free source-relative PNGs under `textures/`; duplicate Unreal basenames are exported independently instead of overwriting one another |
|
|
437
|
+
| `Texture2D` with an `RGBA16F` source | Raw little-endian half-float RGBA binaries and a versioned JSON manifest under `data-textures/`, promoted together under the package's qualified path. Values are stored, not decoded: no PivotPainter, classifier, source UV, graph or WPO semantics, and no material binding. The PNG exporters cannot encode the format, so the importer asks the converter for this route after a legacy PNG export fails; a UE 4.x pack needs its `engine` (`UE_4.19`), since the default detector reads every UE4 package as the latest UE4. Loaded in the browser with `threenative-asset-mcp/data-texture` (see Three.js usage) |
|
|
356
438
|
| `TextureCube` environment maps | Collision-free 2:1 equirectangular PNG or lossless Radiance HDR files under `cubemaps/`; CUE4Parse decodes cooked faces, while editor `TSF_BGRE8` source art retains its lighting range without Unreal Engine |
|
|
357
439
|
| `Texture2DArray`, `TextureCubeArray`, and `VolumeTexture` | Collision-free RGBA8 slice data plus JSON dimensions under `textures3d/`; use `DataArrayTexture` for arrays and `Data3DTexture` for volumes. Cube-array faces remain ordered in groups of six for custom shaders. Floating-point/HDR stacks are rejected rather than reduced to RGBA8 |
|
|
358
440
|
| Standalone `Material` and `MaterialInstanceConstant` packages | One directly loadable `Materials/UnrealMaterialLibrary.glb`; each source-relative swatch name is collision-free, follows parent instances, embeds resolved texture inputs, and applies common base-color/emissive vectors plus roughness/metallic/opacity scalars |
|
|
@@ -363,16 +445,117 @@ require manually installed tools instead.
|
|
|
363
445
|
| Paper2D `PaperTileSet` and `PaperTileMap` packages | Populated layers become indexed, unlit GLB quads with atlas UVs, empty cells omitted, layer order retained, and packed horizontal/vertical/diagonal tile flips decoded. Placed tile maps and inherited Blueprint flipbook components are reconstructed in modern UE5 levels |
|
|
364
446
|
| Paper2D grouped sprites and `TextRenderActor` | Grouped sprite instances become `EXT_mesh_gpu_instancing` batches over shared sprite GLBs; text becomes unlit glyph geometry from offline `UFont` atlas pages, preserving alignment, colour, world size, and placement |
|
|
365
447
|
| `DataTable`, `StringTable`, `CurveTable`, and float/vector/colour curves | Collision-free, directly fetchable JSON under `data/`; row names, typed fields, localized strings, asset references, and rich-curve keys/tangents are retained |
|
|
366
|
-
| Modern UE5 loose static meshes, skeletal meshes, `Texture2D`, Material, and `.umap` packages rejected by UE Viewer | CUE4Parse converts directly to the same output/report pipeline; verified with UE5.5 cooked static geometry, UE5 editor skeletal geometry, Oodle-compressed UE5.3 editor texture source art, and a UE5.2 Paper2D map. Cooked packages using unversioned properties require one matching `.usmap` file beside the imported tree |
|
|
448
|
+
| Modern UE5 loose static meshes, skeletal meshes, `Texture2D`, Material, and `.umap` packages rejected by UE Viewer | CUE4Parse converts directly to the same output/report pipeline; verified with UE5.5 cooked static geometry, UE5 editor skeletal geometry, Oodle-compressed UE5.3 editor texture source art, and a UE5.2 Paper2D map. Editor source art is restored to display colour: `TSF_BGRA8` sources are stored with red and blue swapped, and sRGB-sampled `TSF_RGBA16` sources are linear, so both are corrected (checked against UE Viewer's export of the same textures from the UE4.27 artifact). Cooked packages using unversioned properties require one matching `.usmap` file beside the imported tree |
|
|
367
449
|
| Separate roughness/metalness, solid palette maps, glass, and mirrors | Packed glTF PBR maps or explicit material fallbacks; extra graph inputs remain named sidecars |
|
|
368
450
|
| UE4 `.umap` placement, direct lights, and serialized Blueprint component templates (object versions 517–522) | Directly loadable scene GLB plus a transform/source manifest; repeated actors instance shared meshes; inherited mesh/light defaults are merged into placed instances; directional, point, and spot lights use `KHR_lights_punctual` |
|
|
369
451
|
| Standalone modern UE5 Blueprint prefabs | Serialized static/skeletal mesh and light component defaults become directly loadable scene GLBs; referenced meshes, transforms, skins, and bone hierarchies are retained, while bytecode is explicitly reported and never executed |
|
|
370
452
|
| UE4 ISM/HISM and painted static-mesh foliage placement | Bulk-serialized instance matrices become `EXT_mesh_gpu_instancing`, which Three.js `GLTFLoader` loads as GPU-instanced meshes |
|
|
371
453
|
| UE4 editor `LandscapeComponent` heightfields | Package-relative compressed BGRA8 heightmaps become indexed terrain meshes with decoded normals, component transforms, and original material names |
|
|
372
454
|
| ActorX per-frame bone scale, dynamic Blueprint bytecode/construction scripts, Paper Terrain/spline deformation, and Nanite-only data without a fallback mesh | Detected or reported honestly; scene-level omissions are listed in `scenes[].omittedActors`, and Blueprint bytecode is never executed by the importer |
|
|
373
|
-
| Arbitrary Unreal shader graphs |
|
|
455
|
+
| Arbitrary Unreal shader graphs | Recoverable PBR inputs become standard glTF materials. Reports mark materials as degraded when shader connections are unavailable, even if every section has a texture; section `limitations` explain omitted layering, graph UV transforms, subsurface lighting, normal strength, and deformation |
|
|
374
456
|
| Encrypted Pak/IoStore | Unsupported without user-supplied keys and archive extraction; never reported as a complete conversion |
|
|
375
457
|
|
|
458
|
+
Importer version 49 also respects material-instance override flags and modern
|
|
459
|
+
parameter names. Contradictory texture samples duplicated across distinct mesh
|
|
460
|
+
section families are repaired only when referenced texture families are
|
|
461
|
+
unambiguous in the same export namespace. These bindings remain heuristic.
|
|
462
|
+
Separate same-resolution opacity maps can supply base-colour alpha without
|
|
463
|
+
altering RGB; different source dimensions are currently unsupported. Existing
|
|
464
|
+
cached conversions are invalidated. A textured result does not certify the
|
|
465
|
+
appearance of the original Unreal shader.
|
|
466
|
+
|
|
467
|
+
Importer version 51 uses adaptive PNG row filtering for composed opacity maps,
|
|
468
|
+
preserving the previous encoder's decoded RGBA and PNG metadata. Untouched textures
|
|
469
|
+
retain their original bytes; their profiles, bit depth, and metadata are not
|
|
470
|
+
re-encoded for storage savings.
|
|
471
|
+
|
|
472
|
+
Importer version 56 stops a colour parameter's zero alpha from becoming the base
|
|
473
|
+
colour factor's opacity (a `Tint` of `A=0` clipped every pixel of a masked grass
|
|
474
|
+
card, so the GLB drew nothing), binds a masked or translucent material's separate
|
|
475
|
+
opacity map (`Opacity=` or an `Other` named `*_Opacity*`) as the base colour's alpha
|
|
476
|
+
heuristically, and composes an opacity map of another resolution when the aspect
|
|
477
|
+
ratio matches. A sweep sheet tile that renders nothing although Unreal has a
|
|
478
|
+
thumbnail for it is recorded under the entry's `sheet.warnings`.
|
|
479
|
+
|
|
480
|
+
Importer version 58 stops a texture-parameter default from inventing a glTF slot from a
|
|
481
|
+
texture UE Viewer already named as another output. A parameter can exist without being
|
|
482
|
+
wired to its own output, so when its default texture is already the resolved `.mat`'s
|
|
483
|
+
Diffuse (or Normal, and so on) it is a shared placeholder, not a second live slot (Old
|
|
484
|
+
West - VOL 5 Town Props declared an unused `Emissive` default equal to the Fill albedo,
|
|
485
|
+
which painted every dark-wood model with a flat grey emissive wash). Real parameter
|
|
486
|
+
overrides and `.mat`-named Emissive slots still bind, a default whose texture the `.mat`
|
|
487
|
+
left in `Other[]` still recovers its slot by parameter name, and a `.mat` that names no
|
|
488
|
+
slot (the modern converter's `Other[n]` references only) still falls back to the
|
|
489
|
+
collected defaults.
|
|
490
|
+
|
|
491
|
+
Importer version 60 recovers the cut-out of a masked foliage card whose UE Viewer `.mat`
|
|
492
|
+
resolves `Opacity=` to the albedo itself, which has no alpha. When the material references a
|
|
493
|
+
packed `<stem>_AORO` map (AO / roughness / opacity in R / G / B) beside that albedo, the map's
|
|
494
|
+
blue channel becomes the base colour's alpha. Temperate Vegetation: conifer Bushes & Saplings
|
|
495
|
+
had every needle card render as a solid rectangle (Pine_GroundTwig_01 as a full quad with the
|
|
496
|
+
needle atlas painted inside it) because the mask was never applied. The binding is heuristic
|
|
497
|
+
and named in the section's limitations; an opaque material, an unrelated texture stem, or a
|
|
498
|
+
map the pack did not export is left alone.
|
|
499
|
+
|
|
500
|
+
Importer version 59 applies a material instance's base-colour tint vector even when its
|
|
501
|
+
parameter name carries extra words. Old West - VOL 5 Town Props overrides both
|
|
502
|
+
`Albedo Color Tint (Base)` (a bright global multiplier) and `Base Color Tint (Mask)` (the
|
|
503
|
+
dark cloth colour) on a master that tints its albedo through a mask; the exact names missed
|
|
504
|
+
both, so every curtain kept `baseColorFactor` [1,1,1] and rendered the untinted light-grey
|
|
505
|
+
albedo. When both a global base multiplier and a mask colour are overridden, the mask tint is
|
|
506
|
+
the surface colour that shows through. Only an instance's own override is matched broadly, so
|
|
507
|
+
a master's placeholder default (the master's red `Base Color Tint (Mask)`) never paints an
|
|
508
|
+
un-tinted instance; a lone mask tint with no global multiplier (the 03a curtain, whose editor
|
|
509
|
+
mesh thumbnail is the untinted albedo) is left alone, and a `RockTint`-style name with no
|
|
510
|
+
colour token beside `Tint` is left to the graph baker.
|
|
511
|
+
|
|
512
|
+
Importer version 57 gives a mesh the `.mat` / `.props.txt` that sit beside it when two
|
|
513
|
+
packages share an object name (a pack with `MI_Rock_Inst` in two folders bound one
|
|
514
|
+
folder's normal map to the other's meshes); with none beside the mesh the pick is named
|
|
515
|
+
in the section's limitations. The graph bake now evaluates `ObjectScale` (1),
|
|
516
|
+
`PerInstanceRandom` (0.5) and `ObjectPositionWS` (origin) as named heuristic
|
|
517
|
+
approximations and `SplitComponents` exactly, which lets layered cliff-rock masters bake
|
|
518
|
+
their colour texture instead of falling back to neutral grey. The parity S4 check no
|
|
519
|
+
longer lets a 90% colour share hide sections whose graph is known to carry colour.
|
|
520
|
+
|
|
521
|
+
Legacy instance sidecars without override flags retain inherited settings for
|
|
522
|
+
ambiguous opaque, false, and zero defaults. Their non-default values retain the
|
|
523
|
+
previous behavior; missing flags are reported as a material limitation.
|
|
524
|
+
|
|
525
|
+
Importer version 50 reads a bounded authored-material subset from tagged UE4
|
|
526
|
+
object-version 516 packages (legacy -7, UE3 864, licensee 0). Exact source paths
|
|
527
|
+
and closest flagged static overrides select the active branches before reduction.
|
|
528
|
+
Mesh-only package selection still loads needed material parents from the source
|
|
529
|
+
file inventory without exporting unrelated packages; lookups stay within the same
|
|
530
|
+
Content namespace. Mesh import-table material references resolve duplicate source
|
|
531
|
+
basenames by exact object path. Multiple matching references refuse source routing;
|
|
532
|
+
a unique basename fallback reports its unresolved mesh routing.
|
|
533
|
+
Reroutes, scalar constants/parameters and representable multiplication recover
|
|
534
|
+
scalar roughness; glTF normalization retains the raw authored value in the report.
|
|
535
|
+
An authored scalar replaces any earlier roughness texture contribution while
|
|
536
|
+
retaining its packed metallic channel. Source functions, Specular conversion,
|
|
537
|
+
foliage shading and unsupported sampling remain explicit limitations.
|
|
538
|
+
|
|
539
|
+
AO.R is attached only with explicit `Texture2D.SRGB=false`, a serialized linear
|
|
540
|
+
sampler mode (`LinearColor`, `LinearGrayscale` or `Masks`), and source coordinates
|
|
541
|
+
matching the sampler of the currently bound albedo. Its existing glTF coordinate
|
|
542
|
+
mapping is reused only after the selected albedo attaches; an incoming unrelated
|
|
543
|
+
base texture cannot establish that reference. Omitted Unreal defaults remain
|
|
544
|
+
unresolved. Unknown color
|
|
545
|
+
interpretation, ambiguous source paths or exported PNG basenames, nonidentity AO
|
|
546
|
+
multipliers and unproved UV transforms withhold AO, including earlier filename
|
|
547
|
+
guesses. Texture metadata is
|
|
548
|
+
read lazily for the selected AO candidate; unrelated large images are untouched
|
|
549
|
+
by the material reader. Other package profiles keep prior reconstruction with a
|
|
550
|
+
named source-reader limitation. Cached conversions are invalidated.
|
|
551
|
+
|
|
552
|
+
The Buffer reader is independently implemented from the tagged package, property
|
|
553
|
+
and native-input schemas at [CUE4Parse commit b4e9544](https://github.com/FabianFG/CUE4Parse/tree/b4e95441bcf0c975eb3adb68c0fb44c740c2cf62/CUE4Parse/UE4).
|
|
554
|
+
Native expression inputs require verified Core=2 and Framework=33 custom versions;
|
|
555
|
+
tagged instances without native inputs do not require these versions. Native
|
|
556
|
+
trailers and omitted class defaults are outside its scope. This subset does not
|
|
557
|
+
certify original Unreal appearance.
|
|
558
|
+
|
|
376
559
|
Automatic uncooked conversion needs Python 3 with `venv` and `pip`. A Linux
|
|
377
560
|
source build fallback for UE Viewer additionally needs `git`, `g++`, `perl`,
|
|
378
561
|
zlib, and SDL2 development headers. Unreal import currently runs on Linux and
|
|
@@ -459,11 +642,45 @@ texture.type = THREE.UnsignedByteType;
|
|
|
459
642
|
texture.needsUpdate = true;
|
|
460
643
|
```
|
|
461
644
|
|
|
645
|
+
Raw `RGBA16F` textures load through the package's `./data-texture` subpath. The loader
|
|
646
|
+
checks the manifest against the binary (exact length, SHA-256, finite samples) and refuses the
|
|
647
|
+
load, rather than skipping a check, when one cannot run. The source package cannot be fetched
|
|
648
|
+
or re-hashed in a browser, so pin the value the report gives for the texture's source package:
|
|
649
|
+
|
|
650
|
+
```js
|
|
651
|
+
import { loadRgba16fDataTexture } from "threenative-asset-mcp/data-texture";
|
|
652
|
+
|
|
653
|
+
const { texture, manifest } = await loadRgba16fDataTexture(
|
|
654
|
+
new URL(dataTexture.manifest, reportBaseUrl),
|
|
655
|
+
{ expectedSourceSha256: dataTexture.sourceSha256 },
|
|
656
|
+
);
|
|
657
|
+
// texture: THREE.DataTexture, RGBAFormat, HalfFloatType, NoColorSpace, no mipmaps.
|
|
658
|
+
// The stored values are unchanged: negative, HDR, subnormal and alpha bits reach the shader as stored.
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
`loadRgba16fDataTexture` needs an absolute URL and `crypto.subtle`, so serve the page over
|
|
662
|
+
https or from localhost. Its limit is the import's: the texture holds linear half-float
|
|
663
|
+
samples with no decoding, so a material must decide what they mean.
|
|
664
|
+
|
|
462
665
|
Installation-only Unreal `BasicShapes/Plane` references are generated locally
|
|
463
666
|
when the asset pack does not contain that mesh. Rectangular area lights are
|
|
464
667
|
preserved in scene metadata and approximated as punctual point lights because
|
|
465
668
|
glTF's standard punctual-light extension has no area-light type.
|
|
466
669
|
|
|
670
|
+
### Parity sweep and material metadata dumps
|
|
671
|
+
|
|
672
|
+
`npm run parity:fab` imports every owned Fab artifact and scores it against its source package
|
|
673
|
+
(`scorecard.json`). `--export-metadata <dir>` also writes one replayable material dump per pack;
|
|
674
|
+
replay them with `FAB_METADATA_DIR=<dir> npx vitest run tests/fab-metadata.test.ts`. Dumps hold
|
|
675
|
+
licensed pack names and are local-only: never commit them.
|
|
676
|
+
|
|
677
|
+
Set `THREENATIVE_UNREAL_PREVIEW_REFERENCE=/path/to/reference.json` to light the comparison sheets (`parity:fab` and
|
|
678
|
+
`scripts/fidelity-sheet.ts`) with a local version-1 reference profile: radiance and Grid binaries beside the JSON, each
|
|
679
|
+
checked by SHA-256. It is read once per command, and an invalid profile stops the run before any download. Unset, the
|
|
680
|
+
sheets use the approximation and say so. Each picture records what lit it (`identity` in `picture-lighting.json`,
|
|
681
|
+
`sheets/<label>.json` and the scorecard entry). `--resume --sheets` refuses a kept sheet drawn otherwise: resume into a
|
|
682
|
+
new `--out`, or add `--no-sheets`. Profiles and binaries derive from licensed packs: keep them local-only, never commit them.
|
|
683
|
+
|
|
467
684
|
## Configuration
|
|
468
685
|
|
|
469
686
|
| Variable | Default | Purpose |
|
|
@@ -490,8 +707,13 @@ glTF's standard punctual-light extension has no area-light type.
|
|
|
490
707
|
| `AUDIO_CLAP_CHECKPOINT` | unset | Path to the `630k-audioset-best.pt` checkpoint. |
|
|
491
708
|
| `AUDIO_CLAP_CHECKPOINT_SHA256` | unset | Optional pin; a mismatched checkpoint is refused. |
|
|
492
709
|
| `ASSET_DOWNLOAD_DIR` | `~/Downloads/threenative-asset-mcp/assets` | Dedicated directory for direct provider downloads. |
|
|
493
|
-
| `ASSET_MAX_DOWNLOAD_BYTES` | `10737418240` | Maximum
|
|
710
|
+
| `ASSET_MAX_DOWNLOAD_BYTES` | `10737418240` | Maximum bytes per direct file or complete generic acquisition (10 GiB). |
|
|
494
711
|
| `ASSET_DOWNLOAD_TIMEOUT_MS` | `1800000` | Total timeout for one provider download (30 minutes). |
|
|
712
|
+
| `ASSET_ENABLE_KENNEY` | `1` | Bounded official Kenney 3D-pack discovery; `0` disables it. Other values fail initialization. |
|
|
713
|
+
| `ASSET_SEARCH_CONCURRENCY` | `4` | Concurrent provider operations per search; range 1–4. |
|
|
714
|
+
| `ASSET_PROVIDER_TIMEOUT_MS` | `8000` | Provider deadline; range 1–30000 milliseconds. |
|
|
715
|
+
| `ASSET_SEARCH_TIMEOUT_MS` | `10000` | Aggregate search/metadata deadline including queue time; range 1–60000. |
|
|
716
|
+
| `ASSET_PLAN_TTL_MS` | `300000` | Acquisition-plan TTL; range 1–900000; at most 100 bounded plans. |
|
|
495
717
|
| `THREENATIVE_TOOLCHAIN_AUTOINSTALL` | `1` | Set to `0` to disable first-use external-tool installs. |
|
|
496
718
|
| `THREENATIVE_TOOLCHAIN_DIR` | OS cache under `threenative-asset-mcp/toolchain` | UE Viewer, FabCLI, and uncooked-converter cache. |
|
|
497
719
|
| `THREENATIVE_UMODEL_PATH` | auto-detected/provisioned | Absolute path to a UE Viewer executable override. |
|
|
@@ -638,6 +860,32 @@ The MCP host shows no tools
|
|
|
638
860
|
|
|
639
861
|
## Verification
|
|
640
862
|
|
|
863
|
+
GitHub-hosted CI checks pull requests, pushes to main and manual runs on the
|
|
864
|
+
minimum supported Node 20.19 and Node 24. It installs the committed npm lockfile,
|
|
865
|
+
runs typecheck/build, deterministic fixtures and actual built/npm-packed stdio
|
|
866
|
+
consumers. Reports distinguish intentionally conditional proprietary Unreal
|
|
867
|
+
fixtures. No provider live probes, credentials, package publishing or account
|
|
868
|
+
changes run in that workflow. Local CI-equivalent commands are:
|
|
869
|
+
|
|
870
|
+
```bash
|
|
871
|
+
npm ci
|
|
872
|
+
npm run build
|
|
873
|
+
npm run typecheck
|
|
874
|
+
npm run test:ci
|
|
875
|
+
npm run test:smoke
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
The suite needs Chromium (`npx playwright install --with-deps chromium`),
|
|
879
|
+
ffmpeg and Python with NumPy/Pillow for local render/audio fixtures. Live probes
|
|
880
|
+
remain separate from deterministic acceptance tests.
|
|
881
|
+
|
|
882
|
+
Run `npm run doctor` to see which of these your host has. Each missing tool is
|
|
883
|
+
listed with its install command. Locally, a suite whose tool is missing is
|
|
884
|
+
skipped with that command as the reason. With `CI=true` it fails instead.
|
|
885
|
+
Add `--toolchain` to also require what the Unreal importers need on a fresh
|
|
886
|
+
Linux host, or `--toolchain-only` to check only that group.
|
|
887
|
+
|
|
888
|
+
|
|
641
889
|
```bash
|
|
642
890
|
npm ci
|
|
643
891
|
npm run browser:install
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type MetadataContext } from "../discovery/metadata.js";
|
|
1
2
|
export declare const AMBIENTCG_TYPES: readonly ["material", "hdri", "substance", "decal", "atlas", "3d-model", "plain-image", "brush", "terrain", "hdri-element"];
|
|
2
3
|
export type AmbientCgAssetType = (typeof AMBIENTCG_TYPES)[number];
|
|
3
4
|
export interface AmbientCgDownload {
|
|
@@ -39,11 +40,13 @@ export interface AmbientCgCategory {
|
|
|
39
40
|
export declare class AmbientCgClientError extends Error {
|
|
40
41
|
readonly code: "AMBIENTCG_INVALID_INPUT" | "AMBIENTCG_NOT_FOUND" | "AMBIENTCG_RATE_LIMITED" | "AMBIENTCG_UPSTREAM_UNAVAILABLE" | "AMBIENTCG_UPSTREAM_CHANGED";
|
|
41
42
|
readonly retryable: boolean;
|
|
42
|
-
|
|
43
|
+
readonly retryAfter?: string | undefined;
|
|
44
|
+
constructor(code: "AMBIENTCG_INVALID_INPUT" | "AMBIENTCG_NOT_FOUND" | "AMBIENTCG_RATE_LIMITED" | "AMBIENTCG_UPSTREAM_UNAVAILABLE" | "AMBIENTCG_UPSTREAM_CHANGED", message: string, retryable?: boolean, retryAfter?: string | undefined);
|
|
43
45
|
}
|
|
44
46
|
type FetchLike = typeof fetch;
|
|
45
47
|
export declare class AmbientCgClient {
|
|
46
48
|
private readonly fetchImpl;
|
|
49
|
+
private readonly requests;
|
|
47
50
|
constructor(fetchImpl?: FetchLike);
|
|
48
51
|
private request;
|
|
49
52
|
search(input: {
|
|
@@ -53,9 +56,9 @@ export declare class AmbientCgClient {
|
|
|
53
56
|
limit: number;
|
|
54
57
|
offset: number;
|
|
55
58
|
includeDownloads?: boolean;
|
|
56
|
-
}): Promise<AmbientCgSearchResult>;
|
|
57
|
-
getAsset(id: string): Promise<AmbientCgAsset>;
|
|
58
|
-
listCategories(): Promise<AmbientCgCategory[]>;
|
|
59
|
+
}, context?: MetadataContext): Promise<AmbientCgSearchResult>;
|
|
60
|
+
getAsset(id: string, context?: MetadataContext): Promise<AmbientCgAsset>;
|
|
61
|
+
listCategories(context?: MetadataContext): Promise<AmbientCgCategory[]>;
|
|
59
62
|
}
|
|
60
63
|
export {};
|
|
61
64
|
//# sourceMappingURL=client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/ambientcg/client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/ambientcg/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAKhG,eAAO,MAAM,eAAe,YAC1B,UAAU,EACV,MAAM,EACN,WAAW,EACX,OAAO,EACP,OAAO,EACP,UAAU,EACV,aAAa,EACb,OAAO,EACP,SAAS,EACT,cAAc,CACN,CAAC;AACX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAElE,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,EAAE,iBAAiB,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,kBAAkB,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,oBAAqB,SAAQ,KAAK;IAE3C,QAAQ,CAAC,IAAI,EACT,yBAAyB,GACzB,qBAAqB,GACrB,wBAAwB,GACxB,gCAAgC,GAChC,4BAA4B;IAEhC,QAAQ,CAAC,SAAS;IAClB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM;IAT9B,YACW,IAAI,EACT,yBAAyB,GACzB,qBAAqB,GACrB,wBAAwB,GACxB,gCAAgC,GAChC,4BAA4B,EAChC,OAAO,EAAE,MAAM,EACN,SAAS,UAAQ,EACjB,UAAU,CAAC,EAAE,MAAM,YAAA,EAI7B;CACF;AAED,KAAK,SAAS,GAAG,OAAO,KAAK,CAAC;AAoI9B,qBAAa,eAAe;IAEd,OAAO,CAAC,QAAQ,CAAC,SAAS;IADtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA0B;IACnD,YAA6B,SAAS,GAAE,SAAiB,EAAI;YAE/C,OAAO;IA+Cf,MAAM,CACV,KAAK,EAAE;QACL,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,kBAAkB,CAAC;QAC1B,IAAI,EAAE,SAAS,GAAG,QAAQ,GAAG,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAC;QAC5E,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,gBAAgB,CAAC,EAAE,OAAO,CAAC;KAC5B,EACD,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,qBAAqB,CAAC,CAyChC;IAEK,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CAajF;IAEK,cAAc,CAAC,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAkBhF;CACF"}
|
package/dist/ambientcg/client.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MetadataRequests, readMetadata } from "../discovery/metadata.js";
|
|
1
2
|
const API_BASE = "https://ambientcg.com/api/v3";
|
|
2
3
|
const TIMEOUT_MS = 20_000;
|
|
3
4
|
export const AMBIENTCG_TYPES = [
|
|
@@ -15,10 +16,12 @@ export const AMBIENTCG_TYPES = [
|
|
|
15
16
|
export class AmbientCgClientError extends Error {
|
|
16
17
|
code;
|
|
17
18
|
retryable;
|
|
18
|
-
|
|
19
|
+
retryAfter;
|
|
20
|
+
constructor(code, message, retryable = false, retryAfter) {
|
|
19
21
|
super(message);
|
|
20
22
|
this.code = code;
|
|
21
23
|
this.retryable = retryable;
|
|
24
|
+
this.retryAfter = retryAfter;
|
|
22
25
|
this.name = "AmbientCgClientError";
|
|
23
26
|
}
|
|
24
27
|
}
|
|
@@ -28,18 +31,13 @@ function record(value) {
|
|
|
28
31
|
: undefined;
|
|
29
32
|
}
|
|
30
33
|
function text(value, max = 12_000) {
|
|
31
|
-
return typeof value === "string" && value.length > 0
|
|
32
|
-
? value.slice(0, max)
|
|
33
|
-
: undefined;
|
|
34
|
+
return typeof value === "string" && value.length > 0 ? value.slice(0, max) : undefined;
|
|
34
35
|
}
|
|
35
36
|
function number(value) {
|
|
36
|
-
return typeof value === "number" && Number.isFinite(value)
|
|
37
|
-
? value
|
|
38
|
-
: undefined;
|
|
37
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
39
38
|
}
|
|
40
39
|
function isType(value) {
|
|
41
|
-
return
|
|
42
|
-
AMBIENTCG_TYPES.includes(value));
|
|
40
|
+
return typeof value === "string" && AMBIENTCG_TYPES.includes(value);
|
|
43
41
|
}
|
|
44
42
|
function normalizeDownload(value) {
|
|
45
43
|
const item = record(value);
|
|
@@ -47,16 +45,27 @@ function normalizeDownload(value) {
|
|
|
47
45
|
const extension = text(item?.extension, 20);
|
|
48
46
|
const url = text(item?.url, 2_048);
|
|
49
47
|
const sizeBytes = number(item?.size);
|
|
50
|
-
if (!attributes ||
|
|
51
|
-
|
|
48
|
+
if (!attributes ||
|
|
49
|
+
!extension ||
|
|
50
|
+
!url ||
|
|
51
|
+
sizeBytes === undefined ||
|
|
52
|
+
!Number.isSafeInteger(sizeBytes) ||
|
|
53
|
+
sizeBytes < 0) {
|
|
54
|
+
throw new AmbientCgClientError("AMBIENTCG_UPSTREAM_CHANGED", "ambientCG returned invalid download metadata.");
|
|
55
|
+
}
|
|
52
56
|
try {
|
|
53
57
|
const parsed = new URL(url);
|
|
54
|
-
if (parsed.protocol !== "https:" ||
|
|
55
|
-
|
|
58
|
+
if (parsed.protocol !== "https:" ||
|
|
59
|
+
parsed.hostname !== "ambientcg.com" ||
|
|
60
|
+
parsed.username ||
|
|
61
|
+
parsed.password ||
|
|
62
|
+
parsed.port ||
|
|
63
|
+
parsed.hash) {
|
|
64
|
+
throw new Error("Unsafe provider file URL.");
|
|
56
65
|
}
|
|
57
66
|
}
|
|
58
67
|
catch {
|
|
59
|
-
|
|
68
|
+
throw new AmbientCgClientError("AMBIENTCG_UPSTREAM_CHANGED", "ambientCG returned an unsafe download URL.");
|
|
60
69
|
}
|
|
61
70
|
return { attributes, extension, url, sizeBytes };
|
|
62
71
|
}
|
|
@@ -69,10 +78,7 @@ function normalizeAsset(value) {
|
|
|
69
78
|
if (!id || !isType(type) || !title || !url) {
|
|
70
79
|
throw new AmbientCgClientError("AMBIENTCG_UPSTREAM_CHANGED", "ambientCG returned invalid asset metadata.");
|
|
71
80
|
}
|
|
72
|
-
const descriptions = [
|
|
73
|
-
text(item?.longDescription),
|
|
74
|
-
text(item?.shortDescription),
|
|
75
|
-
];
|
|
81
|
+
const descriptions = [text(item?.longDescription), text(item?.shortDescription)];
|
|
76
82
|
const description = descriptions.find((entry) => entry !== undefined);
|
|
77
83
|
const statistics = record(item?.downloadStatistics);
|
|
78
84
|
const dimensions = record(item?.dimensions);
|
|
@@ -80,6 +86,8 @@ function normalizeAsset(value) {
|
|
|
80
86
|
const thumbnailUrl = text(thumbnails?.["512-WEBP"], 2_048) ??
|
|
81
87
|
text(thumbnails?.["512-PNG"], 2_048) ??
|
|
82
88
|
text(thumbnails?.["256-WEBP"], 2_048);
|
|
89
|
+
if (Array.isArray(item?.downloads) && item.downloads.length > 500)
|
|
90
|
+
throw new AmbientCgClientError("AMBIENTCG_UPSTREAM_CHANGED", "ambientCG download variants exceed the entry limit.");
|
|
83
91
|
const downloads = Array.isArray(item?.downloads)
|
|
84
92
|
? item.downloads
|
|
85
93
|
.map(normalizeDownload)
|
|
@@ -105,12 +113,8 @@ function normalizeAsset(value) {
|
|
|
105
113
|
: [],
|
|
106
114
|
downloads,
|
|
107
115
|
...(description ? { description } : {}),
|
|
108
|
-
...(text(item?.releaseDate, 20)
|
|
109
|
-
|
|
110
|
-
: {}),
|
|
111
|
-
...(text(item?.technique, 100)
|
|
112
|
-
? { technique: text(item?.technique, 100) }
|
|
113
|
-
: {}),
|
|
116
|
+
...(text(item?.releaseDate, 20) ? { releaseDate: text(item?.releaseDate, 20) } : {}),
|
|
117
|
+
...(text(item?.technique, 100) ? { technique: text(item?.technique, 100) } : {}),
|
|
114
118
|
...(number(statistics?.total) !== undefined
|
|
115
119
|
? { downloadCount: number(statistics?.total) }
|
|
116
120
|
: {}),
|
|
@@ -131,37 +135,44 @@ function normalizeAsset(value) {
|
|
|
131
135
|
}
|
|
132
136
|
export class AmbientCgClient {
|
|
133
137
|
fetchImpl;
|
|
138
|
+
requests = new MetadataRequests();
|
|
134
139
|
constructor(fetchImpl = fetch) {
|
|
135
140
|
this.fetchImpl = fetchImpl;
|
|
136
141
|
}
|
|
137
|
-
async request(path) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
response
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
142
|
+
async request(path, context = {}) {
|
|
143
|
+
return this.requests.run(path, context.signal, async (callerSignal) => {
|
|
144
|
+
const signal = AbortSignal.any([callerSignal, AbortSignal.timeout(TIMEOUT_MS)]);
|
|
145
|
+
let response;
|
|
146
|
+
try {
|
|
147
|
+
response = await this.fetchImpl(`${API_BASE}${path}`, {
|
|
148
|
+
headers: {
|
|
149
|
+
accept: "application/json",
|
|
150
|
+
"user-agent": "threenative-asset-mcp/0.4.0",
|
|
151
|
+
},
|
|
152
|
+
signal,
|
|
153
|
+
redirect: "error",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
throw new AmbientCgClientError("AMBIENTCG_UPSTREAM_UNAVAILABLE", "ambientCG could not be reached.", true);
|
|
158
|
+
}
|
|
159
|
+
if (!response.ok)
|
|
160
|
+
await response.body?.cancel();
|
|
161
|
+
if (response.status === 429) {
|
|
162
|
+
throw new AmbientCgClientError("AMBIENTCG_RATE_LIMITED", "ambientCG rate-limited the request.", true, response.headers.get("retry-after")?.slice(0, 100));
|
|
163
|
+
}
|
|
164
|
+
if (!response.ok) {
|
|
165
|
+
throw new AmbientCgClientError("AMBIENTCG_UPSTREAM_UNAVAILABLE", `ambientCG returned HTTP ${response.status}.`, response.status >= 500);
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
return JSON.parse(await readMetadata(response, 16 * 1024 * 1024, signal));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
throw new AmbientCgClientError("AMBIENTCG_UPSTREAM_CHANGED", "ambientCG returned invalid JSON.");
|
|
172
|
+
}
|
|
173
|
+
});
|
|
163
174
|
}
|
|
164
|
-
async search(input) {
|
|
175
|
+
async search(input, context = {}) {
|
|
165
176
|
const params = new URLSearchParams({
|
|
166
177
|
sort: input.sort,
|
|
167
178
|
limit: String(input.limit),
|
|
@@ -185,10 +196,10 @@ export class AmbientCgClient {
|
|
|
185
196
|
params.set("q", input.query);
|
|
186
197
|
if (input.type)
|
|
187
198
|
params.set("type", input.type);
|
|
188
|
-
const payload = record(await this.request(`/assets?${params}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
199
|
+
const payload = record(await this.request(`/assets?${params}`, context));
|
|
200
|
+
if (Array.isArray(payload?.assets) && payload.assets.length > 1000)
|
|
201
|
+
throw new AmbientCgClientError("AMBIENTCG_UPSTREAM_CHANGED", "ambientCG asset list exceeds its entry limit.");
|
|
202
|
+
const assets = Array.isArray(payload?.assets) ? payload.assets.map(normalizeAsset) : undefined;
|
|
192
203
|
const total = number(payload?.totalResults);
|
|
193
204
|
if (!assets || total === undefined) {
|
|
194
205
|
throw new AmbientCgClientError("AMBIENTCG_UPSTREAM_CHANGED", "ambientCG returned an invalid search result.");
|
|
@@ -196,26 +207,24 @@ export class AmbientCgClient {
|
|
|
196
207
|
return {
|
|
197
208
|
assets,
|
|
198
209
|
total,
|
|
199
|
-
...(input.offset + assets.length < total
|
|
200
|
-
? { nextOffset: input.offset + assets.length }
|
|
201
|
-
: {}),
|
|
210
|
+
...(input.offset + assets.length < total ? { nextOffset: input.offset + assets.length } : {}),
|
|
202
211
|
};
|
|
203
212
|
}
|
|
204
|
-
async getAsset(id) {
|
|
213
|
+
async getAsset(id, context = {}) {
|
|
205
214
|
const params = new URLSearchParams({
|
|
206
215
|
id,
|
|
207
216
|
limit: "1",
|
|
208
217
|
include: "type,releaseDate,shortDescription,longDescription,title,url,tags,dimensions,downloadStatistics,downloads,technique,maps,thumbnails",
|
|
209
218
|
});
|
|
210
|
-
const payload = record(await this.request(`/assets?${params}
|
|
219
|
+
const payload = record(await this.request(`/assets?${params}`, context));
|
|
211
220
|
const first = Array.isArray(payload?.assets) ? payload.assets[0] : undefined;
|
|
212
221
|
if (!first) {
|
|
213
222
|
throw new AmbientCgClientError("AMBIENTCG_NOT_FOUND", "The ambientCG asset was not found.");
|
|
214
223
|
}
|
|
215
224
|
return normalizeAsset(first);
|
|
216
225
|
}
|
|
217
|
-
async listCategories() {
|
|
218
|
-
const payload = await this.request("/categories");
|
|
226
|
+
async listCategories(context = {}) {
|
|
227
|
+
const payload = await this.request("/categories", context);
|
|
219
228
|
if (!Array.isArray(payload)) {
|
|
220
229
|
throw new AmbientCgClientError("AMBIENTCG_UPSTREAM_CHANGED", "ambientCG returned an invalid category list.");
|
|
221
230
|
}
|