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.
Files changed (269) hide show
  1. package/README.md +252 -4
  2. package/dist/ambientcg/client.d.ts +7 -4
  3. package/dist/ambientcg/client.d.ts.map +1 -1
  4. package/dist/ambientcg/client.js +71 -62
  5. package/dist/ambientcg/client.js.map +1 -1
  6. package/dist/audio/generate.d.ts +3 -0
  7. package/dist/audio/generate.d.ts.map +1 -1
  8. package/dist/audio/generate.js +20 -2
  9. package/dist/audio/generate.js.map +1 -1
  10. package/dist/browser-temp.d.ts +13 -0
  11. package/dist/browser-temp.d.ts.map +1 -0
  12. package/dist/browser-temp.js +17 -0
  13. package/dist/browser-temp.js.map +1 -0
  14. package/dist/cli.js +1 -1
  15. package/dist/cli.js.map +1 -1
  16. package/dist/creature/preview.d.ts.map +1 -1
  17. package/dist/creature/preview.js +41 -20
  18. package/dist/creature/preview.js.map +1 -1
  19. package/dist/creature/runner.d.ts +12 -1
  20. package/dist/creature/runner.d.ts.map +1 -1
  21. package/dist/creature/runner.js +30 -16
  22. package/dist/creature/runner.js.map +1 -1
  23. package/dist/data-texture-loader.d.ts +43 -0
  24. package/dist/data-texture-loader.d.ts.map +1 -0
  25. package/dist/data-texture-loader.js +67 -0
  26. package/dist/data-texture-loader.js.map +1 -0
  27. package/dist/discovery/acquisition.d.ts +45 -0
  28. package/dist/discovery/acquisition.d.ts.map +1 -0
  29. package/dist/discovery/acquisition.js +372 -0
  30. package/dist/discovery/acquisition.js.map +1 -0
  31. package/dist/discovery/adapters.d.ts +16 -0
  32. package/dist/discovery/adapters.d.ts.map +1 -0
  33. package/dist/discovery/adapters.js +557 -0
  34. package/dist/discovery/adapters.js.map +1 -0
  35. package/dist/discovery/federation.d.ts +187 -0
  36. package/dist/discovery/federation.d.ts.map +1 -0
  37. package/dist/discovery/federation.js +373 -0
  38. package/dist/discovery/federation.js.map +1 -0
  39. package/dist/discovery/metadata.d.ts +12 -0
  40. package/dist/discovery/metadata.d.ts.map +1 -0
  41. package/dist/discovery/metadata.js +88 -0
  42. package/dist/discovery/metadata.js.map +1 -0
  43. package/dist/discovery/types.d.ts +97 -0
  44. package/dist/discovery/types.d.ts.map +1 -0
  45. package/dist/discovery/types.js +31 -0
  46. package/dist/discovery/types.js.map +1 -0
  47. package/dist/download/direct-asset-downloader.d.ts +8 -0
  48. package/dist/download/direct-asset-downloader.d.ts.map +1 -1
  49. package/dist/download/direct-asset-downloader.js +92 -30
  50. package/dist/download/direct-asset-downloader.js.map +1 -1
  51. package/dist/fab/browser-transport.d.ts.map +1 -1
  52. package/dist/fab/browser-transport.js +33 -9
  53. package/dist/fab/browser-transport.js.map +1 -1
  54. package/dist/fab/fabcli.d.ts +30 -3
  55. package/dist/fab/fabcli.d.ts.map +1 -1
  56. package/dist/fab/fabcli.js +116 -13
  57. package/dist/fab/fabcli.js.map +1 -1
  58. package/dist/fab/routes.d.ts +23 -0
  59. package/dist/fab/routes.d.ts.map +1 -0
  60. package/dist/fab/routes.js +63 -0
  61. package/dist/fab/routes.js.map +1 -0
  62. package/dist/kenney/client.d.ts +41 -0
  63. package/dist/kenney/client.d.ts.map +1 -0
  64. package/dist/kenney/client.js +200 -0
  65. package/dist/kenney/client.js.map +1 -0
  66. package/dist/polyhaven/client.d.ts +9 -5
  67. package/dist/polyhaven/client.d.ts.map +1 -1
  68. package/dist/polyhaven/client.js +99 -55
  69. package/dist/polyhaven/client.js.map +1 -1
  70. package/dist/rig/animation-catalog.json +1 -1
  71. package/dist/rig/preview.d.ts +12 -1
  72. package/dist/rig/preview.d.ts.map +1 -1
  73. package/dist/rig/preview.js +130 -65
  74. package/dist/rig/preview.js.map +1 -1
  75. package/dist/rig/retarget.d.ts +14 -0
  76. package/dist/rig/retarget.d.ts.map +1 -1
  77. package/dist/rig/retarget.js +224 -36
  78. package/dist/rig/retarget.js.map +1 -1
  79. package/dist/server.d.ts +4 -0
  80. package/dist/server.d.ts.map +1 -1
  81. package/dist/server.js +52 -1
  82. package/dist/server.js.map +1 -1
  83. package/dist/sketchfab/client.d.ts +8 -5
  84. package/dist/sketchfab/client.d.ts.map +1 -1
  85. package/dist/sketchfab/client.js +71 -66
  86. package/dist/sketchfab/client.js.map +1 -1
  87. package/dist/smithsonian/client.d.ts +6 -3
  88. package/dist/smithsonian/client.d.ts.map +1 -1
  89. package/dist/smithsonian/client.js +65 -48
  90. package/dist/smithsonian/client.js.map +1 -1
  91. package/dist/tools/download-free-asset.d.ts +1 -1
  92. package/dist/tools/download-free-asset.d.ts.map +1 -1
  93. package/dist/tools/federated-assets.d.ts +381 -0
  94. package/dist/tools/federated-assets.d.ts.map +1 -0
  95. package/dist/tools/federated-assets.js +143 -0
  96. package/dist/tools/federated-assets.js.map +1 -0
  97. package/dist/tools/import-unreal.d.ts +82 -0
  98. package/dist/tools/import-unreal.d.ts.map +1 -1
  99. package/dist/tools/import-unreal.js +106 -6
  100. package/dist/tools/import-unreal.js.map +1 -1
  101. package/dist/tools/rig.d.ts +4 -0
  102. package/dist/tools/rig.d.ts.map +1 -1
  103. package/dist/tools/rig.js +10 -2
  104. package/dist/tools/rig.js.map +1 -1
  105. package/dist/tools/source-directory.js +1 -1
  106. package/dist/tools/source-directory.js.map +1 -1
  107. package/dist/unreal/albedo-fidelity.d.ts +86 -0
  108. package/dist/unreal/albedo-fidelity.d.ts.map +1 -0
  109. package/dist/unreal/albedo-fidelity.js +207 -0
  110. package/dist/unreal/albedo-fidelity.js.map +1 -0
  111. package/dist/unreal/contact-sheet.d.ts +214 -0
  112. package/dist/unreal/contact-sheet.d.ts.map +1 -0
  113. package/dist/unreal/contact-sheet.js +782 -0
  114. package/dist/unreal/contact-sheet.js.map +1 -0
  115. package/dist/unreal/cooking.d.ts +15 -0
  116. package/dist/unreal/cooking.d.ts.map +1 -1
  117. package/dist/unreal/cooking.js +23 -2
  118. package/dist/unreal/cooking.js.map +1 -1
  119. package/dist/unreal/cue4parse-adapter.d.ts +7 -1
  120. package/dist/unreal/cue4parse-adapter.d.ts.map +1 -1
  121. package/dist/unreal/cue4parse-adapter.js +3286 -243
  122. package/dist/unreal/cue4parse-adapter.js.map +1 -1
  123. package/dist/unreal/data-texture-promotion.d.ts +49 -0
  124. package/dist/unreal/data-texture-promotion.d.ts.map +1 -0
  125. package/dist/unreal/data-texture-promotion.js +104 -0
  126. package/dist/unreal/data-texture-promotion.js.map +1 -0
  127. package/dist/unreal/data-texture.d.ts +79 -0
  128. package/dist/unreal/data-texture.d.ts.map +1 -0
  129. package/dist/unreal/data-texture.js +186 -0
  130. package/dist/unreal/data-texture.js.map +1 -0
  131. package/dist/unreal/engine-content.d.ts +40 -0
  132. package/dist/unreal/engine-content.d.ts.map +1 -0
  133. package/dist/unreal/engine-content.js +96 -0
  134. package/dist/unreal/engine-content.js.map +1 -0
  135. package/dist/unreal/errors.d.ts +13 -0
  136. package/dist/unreal/errors.d.ts.map +1 -0
  137. package/dist/unreal/errors.js +11 -0
  138. package/dist/unreal/errors.js.map +1 -0
  139. package/dist/unreal/executable-publish.d.ts +15 -0
  140. package/dist/unreal/executable-publish.d.ts.map +1 -0
  141. package/dist/unreal/executable-publish.js +46 -0
  142. package/dist/unreal/executable-publish.js.map +1 -0
  143. package/dist/unreal/fidelity-baseline.d.ts +30 -0
  144. package/dist/unreal/fidelity-baseline.d.ts.map +1 -0
  145. package/dist/unreal/fidelity-baseline.js +52 -0
  146. package/dist/unreal/fidelity-baseline.js.map +1 -0
  147. package/dist/unreal/graph-baker.d.ts +222 -0
  148. package/dist/unreal/graph-baker.d.ts.map +1 -0
  149. package/dist/unreal/graph-baker.js +628 -0
  150. package/dist/unreal/graph-baker.js.map +1 -0
  151. package/dist/unreal/graph-dump.d.ts +213 -0
  152. package/dist/unreal/graph-dump.d.ts.map +1 -0
  153. package/dist/unreal/graph-dump.js +345 -0
  154. package/dist/unreal/graph-dump.js.map +1 -0
  155. package/dist/unreal/grooms.d.ts +35 -0
  156. package/dist/unreal/grooms.d.ts.map +1 -0
  157. package/dist/unreal/grooms.js +297 -0
  158. package/dist/unreal/grooms.js.map +1 -0
  159. package/dist/unreal/image-diff.d.ts +129 -0
  160. package/dist/unreal/image-diff.d.ts.map +1 -0
  161. package/dist/unreal/image-diff.js +489 -0
  162. package/dist/unreal/image-diff.js.map +1 -0
  163. package/dist/unreal/importer.d.ts +241 -10
  164. package/dist/unreal/importer.d.ts.map +1 -1
  165. package/dist/unreal/importer.js +2365 -186
  166. package/dist/unreal/importer.js.map +1 -1
  167. package/dist/unreal/material-graph.d.ts +260 -0
  168. package/dist/unreal/material-graph.d.ts.map +1 -0
  169. package/dist/unreal/material-graph.js +3479 -0
  170. package/dist/unreal/material-graph.js.map +1 -0
  171. package/dist/unreal/material-metadata.d.ts +39 -0
  172. package/dist/unreal/material-metadata.d.ts.map +1 -0
  173. package/dist/unreal/material-metadata.js +120 -0
  174. package/dist/unreal/material-metadata.js.map +1 -0
  175. package/dist/unreal/materials.d.ts +55 -5
  176. package/dist/unreal/materials.d.ts.map +1 -1
  177. package/dist/unreal/materials.js +360 -61
  178. package/dist/unreal/materials.js.map +1 -1
  179. package/dist/unreal/mesh-build-scale.d.ts +9 -0
  180. package/dist/unreal/mesh-build-scale.d.ts.map +1 -0
  181. package/dist/unreal/mesh-build-scale.js +123 -0
  182. package/dist/unreal/mesh-build-scale.js.map +1 -0
  183. package/dist/unreal/package-thumbnail.d.ts +42 -0
  184. package/dist/unreal/package-thumbnail.d.ts.map +1 -0
  185. package/dist/unreal/package-thumbnail.js +219 -0
  186. package/dist/unreal/package-thumbnail.js.map +1 -0
  187. package/dist/unreal/parity-run.d.ts +438 -0
  188. package/dist/unreal/parity-run.d.ts.map +1 -0
  189. package/dist/unreal/parity-run.js +1001 -0
  190. package/dist/unreal/parity-run.js.map +1 -0
  191. package/dist/unreal/parity.d.ts +138 -0
  192. package/dist/unreal/parity.d.ts.map +1 -0
  193. package/dist/unreal/parity.js +538 -0
  194. package/dist/unreal/parity.js.map +1 -0
  195. package/dist/unreal/preview-reference-workflow.d.ts +77 -0
  196. package/dist/unreal/preview-reference-workflow.d.ts.map +1 -0
  197. package/dist/unreal/preview-reference-workflow.js +106 -0
  198. package/dist/unreal/preview-reference-workflow.js.map +1 -0
  199. package/dist/unreal/property-dump.d.ts +128 -0
  200. package/dist/unreal/property-dump.d.ts.map +1 -0
  201. package/dist/unreal/property-dump.js +83 -0
  202. package/dist/unreal/property-dump.js.map +1 -0
  203. package/dist/unreal/provision.d.ts +145 -13
  204. package/dist/unreal/provision.d.ts.map +1 -1
  205. package/dist/unreal/provision.js +711 -144
  206. package/dist/unreal/provision.js.map +1 -1
  207. package/dist/unreal/render-fidelity.d.ts +142 -0
  208. package/dist/unreal/render-fidelity.d.ts.map +1 -0
  209. package/dist/unreal/render-fidelity.js +361 -0
  210. package/dist/unreal/render-fidelity.js.map +1 -0
  211. package/dist/unreal/scenes.d.ts.map +1 -1
  212. package/dist/unreal/scenes.js +24 -0
  213. package/dist/unreal/scenes.js.map +1 -1
  214. package/dist/unreal/source-material-package.d.ts +36 -0
  215. package/dist/unreal/source-material-package.d.ts.map +1 -0
  216. package/dist/unreal/source-material-package.js +320 -0
  217. package/dist/unreal/source-material-package.js.map +1 -0
  218. package/dist/unreal/source-material.d.ts +42 -0
  219. package/dist/unreal/source-material.d.ts.map +1 -0
  220. package/dist/unreal/source-material.js +400 -0
  221. package/dist/unreal/source-material.js.map +1 -0
  222. package/dist/unreal/spatial-atlas.d.ts +39 -0
  223. package/dist/unreal/spatial-atlas.d.ts.map +1 -0
  224. package/dist/unreal/spatial-atlas.js +302 -0
  225. package/dist/unreal/spatial-atlas.js.map +1 -0
  226. package/dist/unreal/spatial-remap.d.ts +54 -0
  227. package/dist/unreal/spatial-remap.d.ts.map +1 -0
  228. package/dist/unreal/spatial-remap.js +273 -0
  229. package/dist/unreal/spatial-remap.js.map +1 -0
  230. package/dist/unreal/spatial-surface.d.ts +88 -0
  231. package/dist/unreal/spatial-surface.d.ts.map +1 -0
  232. package/dist/unreal/spatial-surface.js +362 -0
  233. package/dist/unreal/spatial-surface.js.map +1 -0
  234. package/dist/unreal/static-mesh-sections.d.ts +42 -0
  235. package/dist/unreal/static-mesh-sections.d.ts.map +1 -0
  236. package/dist/unreal/static-mesh-sections.js +381 -0
  237. package/dist/unreal/static-mesh-sections.js.map +1 -0
  238. package/dist/unreal/surface-normals.d.ts +21 -0
  239. package/dist/unreal/surface-normals.d.ts.map +1 -0
  240. package/dist/unreal/surface-normals.js +106 -0
  241. package/dist/unreal/surface-normals.js.map +1 -0
  242. package/dist/unreal/texture-proof.d.ts +135 -0
  243. package/dist/unreal/texture-proof.d.ts.map +1 -0
  244. package/dist/unreal/texture-proof.js +439 -0
  245. package/dist/unreal/texture-proof.js.map +1 -0
  246. package/dist/unreal/thumbnail-floor.d.ts +45 -0
  247. package/dist/unreal/thumbnail-floor.d.ts.map +1 -0
  248. package/dist/unreal/thumbnail-floor.js +83 -0
  249. package/dist/unreal/thumbnail-floor.js.map +1 -0
  250. package/dist/unreal/thumbnail-lighting.d.ts +62 -0
  251. package/dist/unreal/thumbnail-lighting.d.ts.map +1 -0
  252. package/dist/unreal/thumbnail-lighting.js +125 -0
  253. package/dist/unreal/thumbnail-lighting.js.map +1 -0
  254. package/dist/unreal/thumbnail-reference-profile.d.ts +39 -0
  255. package/dist/unreal/thumbnail-reference-profile.d.ts.map +1 -0
  256. package/dist/unreal/thumbnail-reference-profile.js +218 -0
  257. package/dist/unreal/thumbnail-reference-profile.js.map +1 -0
  258. package/dist/unreal/toolchain.d.ts.map +1 -1
  259. package/dist/unreal/toolchain.js +4 -0
  260. package/dist/unreal/toolchain.js.map +1 -1
  261. package/dist/unreal/umodel-export.d.ts +15 -0
  262. package/dist/unreal/umodel-export.d.ts.map +1 -0
  263. package/dist/unreal/umodel-export.js +90 -0
  264. package/dist/unreal/umodel-export.js.map +1 -0
  265. package/dist/unreal/visual-judge.d.ts +100 -0
  266. package/dist/unreal/visual-judge.d.ts.map +1 -0
  267. package/dist/unreal/visual-judge.js +200 -0
  268. package/dist/unreal/visual-judge.js.map +1 -0
  269. 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 | Common PBR inputs become standard glTF materials; graph inputs with no glTF counterpart remain named in the report instead of being silently discarded |
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 accepted bytes per provider file (10 GiB). |
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
- constructor(code: "AMBIENTCG_INVALID_INPUT" | "AMBIENTCG_NOT_FOUND" | "AMBIENTCG_RATE_LIMITED" | "AMBIENTCG_UPSTREAM_UNAVAILABLE" | "AMBIENTCG_UPSTREAM_CHANGED", message: string, retryable?: boolean);
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":"AAGA,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;IARpB,YACW,IAAI,EACT,yBAAyB,GACzB,qBAAqB,GACrB,wBAAwB,GACxB,gCAAgC,GAChC,4BAA4B,EAChC,OAAO,EAAE,MAAM,EACN,SAAS,UAAQ,EAI3B;CACF;AAED,KAAK,SAAS,GAAG,OAAO,KAAK,CAAC;AAuH9B,qBAAa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,SAAS;IAAtC,YAA6B,SAAS,GAAE,SAAiB,EAAI;YAE/C,OAAO;IAyCf,MAAM,CAAC,KAAK,EAAE;QAClB,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,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAwCjC;IAEK,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAgBlD;IAEK,cAAc,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAkBnD;CACF"}
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"}
@@ -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
- constructor(code, message, retryable = false) {
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 (typeof value === "string" &&
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 || !extension || !url || sizeBytes === undefined)
51
- return;
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:" || parsed.hostname !== "ambientcg.com") {
55
- return;
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
- return;
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
- ? { releaseDate: text(item?.releaseDate, 20) }
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
- let response;
139
- try {
140
- response = await this.fetchImpl(`${API_BASE}${path}`, {
141
- headers: {
142
- accept: "application/json",
143
- "user-agent": "threenative-asset-mcp/0.4.0",
144
- },
145
- signal: AbortSignal.timeout(TIMEOUT_MS),
146
- });
147
- }
148
- catch {
149
- throw new AmbientCgClientError("AMBIENTCG_UPSTREAM_UNAVAILABLE", "ambientCG could not be reached.", true);
150
- }
151
- if (response.status === 429) {
152
- throw new AmbientCgClientError("AMBIENTCG_RATE_LIMITED", "ambientCG rate-limited the request.", true);
153
- }
154
- if (!response.ok) {
155
- throw new AmbientCgClientError("AMBIENTCG_UPSTREAM_UNAVAILABLE", `ambientCG returned HTTP ${response.status}.`, response.status >= 500);
156
- }
157
- try {
158
- return (await response.json());
159
- }
160
- catch {
161
- throw new AmbientCgClientError("AMBIENTCG_UPSTREAM_CHANGED", "ambientCG returned invalid JSON.");
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
- const assets = Array.isArray(payload?.assets)
190
- ? payload.assets.map(normalizeAsset)
191
- : undefined;
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
  }