threenative-asset-mcp 0.9.5 → 0.11.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 (298) hide show
  1. package/README.md +260 -3
  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/creature.d.ts +1 -1
  92. package/dist/tools/creature.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/animation-data.d.ts +342 -0
  112. package/dist/unreal/animation-data.d.ts.map +1 -0
  113. package/dist/unreal/animation-data.js +375 -0
  114. package/dist/unreal/animation-data.js.map +1 -0
  115. package/dist/unreal/contact-sheet.d.ts +231 -0
  116. package/dist/unreal/contact-sheet.d.ts.map +1 -0
  117. package/dist/unreal/contact-sheet.js +809 -0
  118. package/dist/unreal/contact-sheet.js.map +1 -0
  119. package/dist/unreal/cooking.d.ts +82 -0
  120. package/dist/unreal/cooking.d.ts.map +1 -1
  121. package/dist/unreal/cooking.js +287 -6
  122. package/dist/unreal/cooking.js.map +1 -1
  123. package/dist/unreal/cue4parse-adapter.d.ts +7 -1
  124. package/dist/unreal/cue4parse-adapter.d.ts.map +1 -1
  125. package/dist/unreal/cue4parse-adapter.js +4639 -239
  126. package/dist/unreal/cue4parse-adapter.js.map +1 -1
  127. package/dist/unreal/data-texture-promotion.d.ts +49 -0
  128. package/dist/unreal/data-texture-promotion.d.ts.map +1 -0
  129. package/dist/unreal/data-texture-promotion.js +104 -0
  130. package/dist/unreal/data-texture-promotion.js.map +1 -0
  131. package/dist/unreal/data-texture.d.ts +79 -0
  132. package/dist/unreal/data-texture.d.ts.map +1 -0
  133. package/dist/unreal/data-texture.js +186 -0
  134. package/dist/unreal/data-texture.js.map +1 -0
  135. package/dist/unreal/effects-dump.d.ts +81 -0
  136. package/dist/unreal/effects-dump.d.ts.map +1 -0
  137. package/dist/unreal/effects-dump.js +113 -0
  138. package/dist/unreal/effects-dump.js.map +1 -0
  139. package/dist/unreal/effects-preview.d.ts +76 -0
  140. package/dist/unreal/effects-preview.d.ts.map +1 -0
  141. package/dist/unreal/effects-preview.js +386 -0
  142. package/dist/unreal/effects-preview.js.map +1 -0
  143. package/dist/unreal/effects-schema.d.ts +1797 -0
  144. package/dist/unreal/effects-schema.d.ts.map +1 -0
  145. package/dist/unreal/effects-schema.js +1670 -0
  146. package/dist/unreal/effects-schema.js.map +1 -0
  147. package/dist/unreal/engine-content.d.ts +40 -0
  148. package/dist/unreal/engine-content.d.ts.map +1 -0
  149. package/dist/unreal/engine-content.js +96 -0
  150. package/dist/unreal/engine-content.js.map +1 -0
  151. package/dist/unreal/errors.d.ts +13 -0
  152. package/dist/unreal/errors.d.ts.map +1 -0
  153. package/dist/unreal/errors.js +11 -0
  154. package/dist/unreal/errors.js.map +1 -0
  155. package/dist/unreal/executable-publish.d.ts +15 -0
  156. package/dist/unreal/executable-publish.d.ts.map +1 -0
  157. package/dist/unreal/executable-publish.js +46 -0
  158. package/dist/unreal/executable-publish.js.map +1 -0
  159. package/dist/unreal/fidelity-baseline.d.ts +30 -0
  160. package/dist/unreal/fidelity-baseline.d.ts.map +1 -0
  161. package/dist/unreal/fidelity-baseline.js +52 -0
  162. package/dist/unreal/fidelity-baseline.js.map +1 -0
  163. package/dist/unreal/game-path.d.ts +7 -0
  164. package/dist/unreal/game-path.d.ts.map +1 -0
  165. package/dist/unreal/game-path.js +12 -0
  166. package/dist/unreal/game-path.js.map +1 -0
  167. package/dist/unreal/gameplay-data.d.ts +350 -0
  168. package/dist/unreal/gameplay-data.d.ts.map +1 -0
  169. package/dist/unreal/gameplay-data.js +534 -0
  170. package/dist/unreal/gameplay-data.js.map +1 -0
  171. package/dist/unreal/graph-baker.d.ts +222 -0
  172. package/dist/unreal/graph-baker.d.ts.map +1 -0
  173. package/dist/unreal/graph-baker.js +628 -0
  174. package/dist/unreal/graph-baker.js.map +1 -0
  175. package/dist/unreal/graph-dump.d.ts +213 -0
  176. package/dist/unreal/graph-dump.d.ts.map +1 -0
  177. package/dist/unreal/graph-dump.js +345 -0
  178. package/dist/unreal/graph-dump.js.map +1 -0
  179. package/dist/unreal/grooms.d.ts +35 -0
  180. package/dist/unreal/grooms.d.ts.map +1 -0
  181. package/dist/unreal/grooms.js +297 -0
  182. package/dist/unreal/grooms.js.map +1 -0
  183. package/dist/unreal/image-diff.d.ts +129 -0
  184. package/dist/unreal/image-diff.d.ts.map +1 -0
  185. package/dist/unreal/image-diff.js +489 -0
  186. package/dist/unreal/image-diff.js.map +1 -0
  187. package/dist/unreal/importer.d.ts +371 -11
  188. package/dist/unreal/importer.d.ts.map +1 -1
  189. package/dist/unreal/importer.js +3214 -203
  190. package/dist/unreal/importer.js.map +1 -1
  191. package/dist/unreal/material-graph.d.ts +260 -0
  192. package/dist/unreal/material-graph.d.ts.map +1 -0
  193. package/dist/unreal/material-graph.js +3479 -0
  194. package/dist/unreal/material-graph.js.map +1 -0
  195. package/dist/unreal/material-metadata.d.ts +39 -0
  196. package/dist/unreal/material-metadata.d.ts.map +1 -0
  197. package/dist/unreal/material-metadata.js +120 -0
  198. package/dist/unreal/material-metadata.js.map +1 -0
  199. package/dist/unreal/materials.d.ts +55 -5
  200. package/dist/unreal/materials.d.ts.map +1 -1
  201. package/dist/unreal/materials.js +360 -61
  202. package/dist/unreal/materials.js.map +1 -1
  203. package/dist/unreal/mesh-build-scale.d.ts +9 -0
  204. package/dist/unreal/mesh-build-scale.d.ts.map +1 -0
  205. package/dist/unreal/mesh-build-scale.js +123 -0
  206. package/dist/unreal/mesh-build-scale.js.map +1 -0
  207. package/dist/unreal/package-thumbnail.d.ts +42 -0
  208. package/dist/unreal/package-thumbnail.d.ts.map +1 -0
  209. package/dist/unreal/package-thumbnail.js +219 -0
  210. package/dist/unreal/package-thumbnail.js.map +1 -0
  211. package/dist/unreal/parity-run.d.ts +438 -0
  212. package/dist/unreal/parity-run.d.ts.map +1 -0
  213. package/dist/unreal/parity-run.js +1001 -0
  214. package/dist/unreal/parity-run.js.map +1 -0
  215. package/dist/unreal/parity.d.ts +138 -0
  216. package/dist/unreal/parity.d.ts.map +1 -0
  217. package/dist/unreal/parity.js +538 -0
  218. package/dist/unreal/parity.js.map +1 -0
  219. package/dist/unreal/preview-reference-workflow.d.ts +77 -0
  220. package/dist/unreal/preview-reference-workflow.d.ts.map +1 -0
  221. package/dist/unreal/preview-reference-workflow.js +106 -0
  222. package/dist/unreal/preview-reference-workflow.js.map +1 -0
  223. package/dist/unreal/property-dump.d.ts +128 -0
  224. package/dist/unreal/property-dump.d.ts.map +1 -0
  225. package/dist/unreal/property-dump.js +83 -0
  226. package/dist/unreal/property-dump.js.map +1 -0
  227. package/dist/unreal/provision.d.ts +145 -13
  228. package/dist/unreal/provision.d.ts.map +1 -1
  229. package/dist/unreal/provision.js +711 -144
  230. package/dist/unreal/provision.js.map +1 -1
  231. package/dist/unreal/psa.d.ts +45 -1
  232. package/dist/unreal/psa.d.ts.map +1 -1
  233. package/dist/unreal/psa.js +95 -12
  234. package/dist/unreal/psa.js.map +1 -1
  235. package/dist/unreal/render-fidelity.d.ts +142 -0
  236. package/dist/unreal/render-fidelity.d.ts.map +1 -0
  237. package/dist/unreal/render-fidelity.js +361 -0
  238. package/dist/unreal/render-fidelity.js.map +1 -0
  239. package/dist/unreal/scenes.d.ts +12 -0
  240. package/dist/unreal/scenes.d.ts.map +1 -1
  241. package/dist/unreal/scenes.js +64 -2
  242. package/dist/unreal/scenes.js.map +1 -1
  243. package/dist/unreal/source-material-package.d.ts +36 -0
  244. package/dist/unreal/source-material-package.d.ts.map +1 -0
  245. package/dist/unreal/source-material-package.js +320 -0
  246. package/dist/unreal/source-material-package.js.map +1 -0
  247. package/dist/unreal/source-material.d.ts +42 -0
  248. package/dist/unreal/source-material.d.ts.map +1 -0
  249. package/dist/unreal/source-material.js +400 -0
  250. package/dist/unreal/source-material.js.map +1 -0
  251. package/dist/unreal/spatial-atlas.d.ts +39 -0
  252. package/dist/unreal/spatial-atlas.d.ts.map +1 -0
  253. package/dist/unreal/spatial-atlas.js +302 -0
  254. package/dist/unreal/spatial-atlas.js.map +1 -0
  255. package/dist/unreal/spatial-remap.d.ts +54 -0
  256. package/dist/unreal/spatial-remap.d.ts.map +1 -0
  257. package/dist/unreal/spatial-remap.js +273 -0
  258. package/dist/unreal/spatial-remap.js.map +1 -0
  259. package/dist/unreal/spatial-surface.d.ts +88 -0
  260. package/dist/unreal/spatial-surface.d.ts.map +1 -0
  261. package/dist/unreal/spatial-surface.js +362 -0
  262. package/dist/unreal/spatial-surface.js.map +1 -0
  263. package/dist/unreal/static-mesh-sections.d.ts +58 -0
  264. package/dist/unreal/static-mesh-sections.d.ts.map +1 -0
  265. package/dist/unreal/static-mesh-sections.js +475 -0
  266. package/dist/unreal/static-mesh-sections.js.map +1 -0
  267. package/dist/unreal/surface-normals.d.ts +21 -0
  268. package/dist/unreal/surface-normals.d.ts.map +1 -0
  269. package/dist/unreal/surface-normals.js +106 -0
  270. package/dist/unreal/surface-normals.js.map +1 -0
  271. package/dist/unreal/texture-proof.d.ts +135 -0
  272. package/dist/unreal/texture-proof.d.ts.map +1 -0
  273. package/dist/unreal/texture-proof.js +439 -0
  274. package/dist/unreal/texture-proof.js.map +1 -0
  275. package/dist/unreal/thumbnail-floor.d.ts +45 -0
  276. package/dist/unreal/thumbnail-floor.d.ts.map +1 -0
  277. package/dist/unreal/thumbnail-floor.js +83 -0
  278. package/dist/unreal/thumbnail-floor.js.map +1 -0
  279. package/dist/unreal/thumbnail-lighting.d.ts +62 -0
  280. package/dist/unreal/thumbnail-lighting.d.ts.map +1 -0
  281. package/dist/unreal/thumbnail-lighting.js +125 -0
  282. package/dist/unreal/thumbnail-lighting.js.map +1 -0
  283. package/dist/unreal/thumbnail-reference-profile.d.ts +39 -0
  284. package/dist/unreal/thumbnail-reference-profile.d.ts.map +1 -0
  285. package/dist/unreal/thumbnail-reference-profile.js +218 -0
  286. package/dist/unreal/thumbnail-reference-profile.js.map +1 -0
  287. package/dist/unreal/toolchain.d.ts.map +1 -1
  288. package/dist/unreal/toolchain.js +4 -0
  289. package/dist/unreal/toolchain.js.map +1 -1
  290. package/dist/unreal/umodel-export.d.ts +15 -0
  291. package/dist/unreal/umodel-export.d.ts.map +1 -0
  292. package/dist/unreal/umodel-export.js +90 -0
  293. package/dist/unreal/umodel-export.js.map +1 -0
  294. package/dist/unreal/visual-judge.d.ts +100 -0
  295. package/dist/unreal/visual-judge.d.ts.map +1 -0
  296. package/dist/unreal/visual-judge.js +200 -0
  297. package/dist/unreal/visual-judge.js.map +1 -0
  298. 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.
@@ -140,6 +145,14 @@ Fab:
140
145
  flows.
141
146
  - `asset_import_unreal` — converts an already-downloaded Unreal directory to
142
147
  self-contained GLBs plus `import-report.json`; Unreal Engine is not required.
148
+ Cascade `ParticleSystem` and Niagara systems are exported as data-only
149
+ descriptors under `Effects/<system>-<hash>.<cascade|niagara>.json`, one per
150
+ canonical package and object path so two same-basename systems in different
151
+ folders stay distinct. No runtime plays them: each descriptor lists the
152
+ modules and inputs it does not carry, and a placed effect component points at
153
+ its descriptor through `import-report.json`. Effects with no exported
154
+ descriptor stay named in the report; the importer never reconstructs a
155
+ particle simulation.
143
156
  - `fab_import_asset` — downloads an owned listing through the user's existing
144
157
  FabCLI session, then runs the same Unreal importer. It never signs in, claims,
145
158
  or purchases an asset.
@@ -316,6 +329,38 @@ Unified source and download routing:
316
329
  revalidated, existing files are never overwritten, and the result includes
317
330
  SHA-256.
318
331
 
332
+ The additive `asset_search` tool searches individual `3d-model`, `texture`,
333
+ `hdri` and `animation` candidates across supported clients. It reports each
334
+ provider, bounded results and local continuation actions. Unknown prices fail
335
+ `freeOnly`; unknown rights fail explicit commercial-use/attribution filters.
336
+ Fab remains a specialized action and generic Sketchfab uses anonymous metadata,
337
+ without authenticated acquisition. Existing tools retain their public names.
338
+
339
+ Use `asset_get` with the returned stable `assetId` to inspect current terms and
340
+ available variants. It prefers GLB, then glTF with its supplied dependencies;
341
+ explicit unavailable formats return alternatives. The read-only call returns an
342
+ expiring `planToken`, required relative files and acknowledgement information.
343
+ A plan is not consent. Call `asset_download` with that token and explicit
344
+ `acceptLicense: true` only after reviewing the terms. Optional `maxBytes` can
345
+ only tighten the configured cap. Changed terms/files invalidate the plan.
346
+
347
+ Acquisition stages originals under `ASSET_DOWNLOAD_DIR/acquisitions`, verifies
348
+ checksums and all glTF buffer/texture references, then atomically publishes a
349
+ complete directory with `asset.acquisition.json`. Replays verify cached bytes
350
+ and provenance; a corrupt cache fails rather than being silently accepted.
351
+ Receipts preserve asset attribution separately from Poly Haven API credit.
352
+ No transfer/signed URLs or credentials enter the new public plan or receipt.
353
+ Archives stay archives with `requiresExtraction: true`; runtime readiness is
354
+ `unverified`. Acquiring originals does not change or adopt assets into a project.
355
+
356
+ Kenney 3D packs are enabled by default; `ASSET_ENABLE_KENNEY=0` disables the
357
+ generic adapter. Each query uses one official listing page and at most three
358
+ item detail checks,
359
+ paces requests, verifies item CC0/free/official-ZIP evidence and never crawls
360
+ or extracts archives. One official Mini Dungeon archive was qualified through
361
+ the packed public MCP on 2026-10-06. Broader catalog coverage and runtime import
362
+ remain unverified; ordinary tests use hand-authored local fixtures.
363
+
319
364
  Recommended agent flow:
320
365
 
321
366
  1. Call `asset_search_sources` with the requested category/query. Its default
@@ -346,18 +391,64 @@ it out of process. Modern UE5 packages rejected by UE Viewer use a pinned,
346
391
  out-of-process CUE4Parse adapter. Set `THREENATIVE_TOOLCHAIN_AUTOINSTALL=0` to
347
392
  require manually installed tools instead.
348
393
 
394
+ ### Fab login for owned packs
395
+
396
+ `fab_import_asset` and `fab_list_owned` read your Fab library through
397
+ [FabCLI](https://github.com/zirklerite/FabCLI), which the importer installs on first
398
+ use. The MCP never signs in for you. Log in once; the session is kept in the OS
399
+ keyring (Secret Service on Linux) and lasts about 90 days:
400
+
401
+ ```bash
402
+ ~/.cache/threenative-asset-mcp/toolchain/fabcli/fabcli auth login # opens a login window
403
+ ~/.cache/threenative-asset-mcp/toolchain/fabcli/fabcli auth login --manual # no window: paste a code
404
+ ~/.cache/threenative-asset-mcp/toolchain/fabcli/fabcli auth status
405
+ ```
406
+
407
+ `--manual` reuses the browser where you are already signed in to Epic. FabCLI prints a
408
+ `epicgames.com/id/login?redirectUrl=…` link; open it there, copy `authorizationCode` from the
409
+ JSON the page shows, and paste it into the prompt within a few minutes. It needs an
410
+ interactive terminal. In Claude Code, run it with a `!` prefix. Never paste the code into an
411
+ untrusted service: it grants full access to the Epic account.
412
+
413
+ `fab_list_owned` lists each listing's Unreal artifacts separately (`artifacts[]`: `artifactId`,
414
+ `engineVersions`, `oldestEngine`, decoder `route`, `targetPlatforms`). An artifact's format is its
415
+ oldest listed engine, and that picks the decoder. When a listing has several artifacts and
416
+ `fab_import_asset` gets neither `engine` nor `artifactId`, it takes the best route (CUE4Parse, then
417
+ the MeshDescription converter, then UE Viewer; the newest source format breaks a tie) and says so
418
+ in the report `warnings`. Pass `artifactId` or `engine` to override.
419
+
420
+ **Graph-only colour.** Some Unreal materials compute BaseColor in the material graph (layered
421
+ masters that blend colour textures by mask channels and tint them), so no texture binding can
422
+ reproduce them. A section left without a base colour is baked from its graph: the pack's editor
423
+ package is read with CUE4Parse, a closed node set is evaluated per texel, and the result is a
424
+ base-colour PNG with `source: "graph"` in the section's bindings. The bake is `exact` only when
425
+ every node is understood; engine layer functions that are not in the pack, a mesh without vertex
426
+ colours standing in white, and a UV set the importer drops make it `heuristic`, and the section's
427
+ `graph` field names each approximation. View-dependent nodes (`Fresnel`, `DepthFade`, `TwoSidedSign`)
428
+ stand in as a recorded approximation: a bake has no camera, so `Fresnel` is its mean over a sphere's visible
429
+ surface, `DepthFade` is fully faded in and `TwoSidedSign` is the front face. Nodes it cannot evaluate (for
430
+ example `ReflectionVectorWS` feeding a cubemap lookup, or `Time`) leave the neutral fallback and are listed
431
+ under `graph.unsupportedNodes`.
432
+ The first bake installs the CUE4Parse converter and its private .NET SDK (hundreds of MB, once);
433
+ pass `graphBake: false` to the importer, or set `THREENATIVE_TOOLCHAIN_AUTOINSTALL=0`, to skip it.
434
+ `fab_import_asset` also skips it when its environment has `THREENATIVE_GRAPH_BAKE=0` (the parity
435
+ sweep's `--no-graph-bake` uses this to produce a baseline).
436
+
349
437
  | Input | Current result |
350
438
  | --- | --- |
351
439
  | Cooked loose UE4 static meshes and textures | GLB geometry, LOD sections, embedded textures, and common PBR reconstruction |
352
440
  | 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 |
441
+ | UE5 `AnimSequence`, blend spaces, aim offsets, montages and composites | UE5.0–5.5 cooked clips become glTF animations through CUE4Parse and attach by bone name; blend spaces, aim offsets, montages and composites become `Animation/<name>.json` with typed axes, mirror flags and resolved section starts, and each clip gets `<clip>.clip.json` notifies, curves and sync markers. A clip with no raw or compressed bone data is reported from its package's own evidence (a `DataModel` export, a `ControlRig` model, or `NumFrames`) instead of exporting a bind pose |
353
442
  | 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) |
354
443
  | 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 |
355
444
  | 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) |
356
445
  | Standalone `Texture2D` packages | Collision-free source-relative PNGs under `textures/`; duplicate Unreal basenames are exported independently instead of overwriting one another |
446
+ | `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) |
357
447
  | `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 |
358
448
  | `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 |
359
449
  | 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 |
360
450
  | Standalone `SoundWave` packages | Collision-free WAV/Ogg/MP3/FLAC files under `audio/`; modern UE5 editor source WAVs and cooked formats exposed by CUE4Parse feed the same report path, verified through Three.js `AudioLoader` |
451
+ | Skeletal and static mesh sockets, static-mesh simple collision, `PhysicsAsset`, and `SoundCue` graphs | Socket nodes (`socket:<name>`) under the joint their bone names (or the mesh node for a static socket); simple collision on the mesh node's `extras.unrealCollision` and in `Collision/<package>.json`; physics bodies, constraints (limits, motions and drives) and collision-disabled pairs in metre-scale `Physics/<asset>.json`, referenced from the skinned mesh node; and each SoundCue graph in `Audio/<cue>.cue.json` with every `WavePlayer` leaf rewritten to the exported audio file or reported as a missing wave. A UE4 package that holds one of these classes routes to the CUE4Parse adapter, which UE Viewer cannot decode; an ordinary UE4 mesh whose only gameplay signal is a `BodySetup` or a cross-package PhysicsAsset/Skeleton reference joins the adapter run for its metadata while keeping its verified UE Viewer geometry route |
361
452
  | Runtime `Font` and inline `FontFace` packages | Validated TTF/OTF faces under collision-free `fonts/` paths; direct SFNT and legacy multi-block zlib `FontBulkData` are supported without Unreal Engine, with family/style/weight metadata for the browser `FontFace` API |
362
453
  | Offline `Font` packages | Pre-baked glyph pages and serialized `FontCharacter` rectangles become collision-safe atlas PNGs plus BMFont-compatible JSON under `bitmap-fonts/`; Unicode remaps, baseline metrics, kerning, page indices, and distance-field metadata are retained for Three.js bitmap/SDF text |
363
454
  | Paper2D `PaperSprite` and `PaperFlipbook` packages | Each sprite becomes a self-contained, unlit GLB with exact baked triangles and a cropped atlas region; flipbooks become JSON manifests preserving FPS and per-frame run lengths. Verified in Three.js on a real UE5.2 Paper2D project without Unreal Engine |
@@ -371,9 +462,110 @@ require manually installed tools instead.
371
462
  | 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 |
372
463
  | UE4 editor `LandscapeComponent` heightfields | Package-relative compressed BGRA8 heightmaps become indexed terrain meshes with decoded normals, component transforms, and original material names |
373
464
  | 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 |
374
- | 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 |
465
+ | 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 |
375
466
  | Encrypted Pak/IoStore | Unsupported without user-supplied keys and archive extraction; never reported as a complete conversion |
376
467
 
468
+ Importer version 49 also respects material-instance override flags and modern
469
+ parameter names. Contradictory texture samples duplicated across distinct mesh
470
+ section families are repaired only when referenced texture families are
471
+ unambiguous in the same export namespace. These bindings remain heuristic.
472
+ Separate same-resolution opacity maps can supply base-colour alpha without
473
+ altering RGB; different source dimensions are currently unsupported. Existing
474
+ cached conversions are invalidated. A textured result does not certify the
475
+ appearance of the original Unreal shader.
476
+
477
+ Importer version 51 uses adaptive PNG row filtering for composed opacity maps,
478
+ preserving the previous encoder's decoded RGBA and PNG metadata. Untouched textures
479
+ retain their original bytes; their profiles, bit depth, and metadata are not
480
+ re-encoded for storage savings.
481
+
482
+ Importer version 56 stops a colour parameter's zero alpha from becoming the base
483
+ colour factor's opacity (a `Tint` of `A=0` clipped every pixel of a masked grass
484
+ card, so the GLB drew nothing), binds a masked or translucent material's separate
485
+ opacity map (`Opacity=` or an `Other` named `*_Opacity*`) as the base colour's alpha
486
+ heuristically, and composes an opacity map of another resolution when the aspect
487
+ ratio matches. A sweep sheet tile that renders nothing although Unreal has a
488
+ thumbnail for it is recorded under the entry's `sheet.warnings`.
489
+
490
+ Importer version 58 stops a texture-parameter default from inventing a glTF slot from a
491
+ texture UE Viewer already named as another output. A parameter can exist without being
492
+ wired to its own output, so when its default texture is already the resolved `.mat`'s
493
+ Diffuse (or Normal, and so on) it is a shared placeholder, not a second live slot (Old
494
+ West - VOL 5 Town Props declared an unused `Emissive` default equal to the Fill albedo,
495
+ which painted every dark-wood model with a flat grey emissive wash). Real parameter
496
+ overrides and `.mat`-named Emissive slots still bind, a default whose texture the `.mat`
497
+ left in `Other[]` still recovers its slot by parameter name, and a `.mat` that names no
498
+ slot (the modern converter's `Other[n]` references only) still falls back to the
499
+ collected defaults.
500
+
501
+ Importer version 60 recovers the cut-out of a masked foliage card whose UE Viewer `.mat`
502
+ resolves `Opacity=` to the albedo itself, which has no alpha. When the material references a
503
+ packed `<stem>_AORO` map (AO / roughness / opacity in R / G / B) beside that albedo, the map's
504
+ blue channel becomes the base colour's alpha. Temperate Vegetation: conifer Bushes & Saplings
505
+ had every needle card render as a solid rectangle (Pine_GroundTwig_01 as a full quad with the
506
+ needle atlas painted inside it) because the mask was never applied. The binding is heuristic
507
+ and named in the section's limitations; an opaque material, an unrelated texture stem, or a
508
+ map the pack did not export is left alone.
509
+
510
+ Importer version 59 applies a material instance's base-colour tint vector even when its
511
+ parameter name carries extra words. Old West - VOL 5 Town Props overrides both
512
+ `Albedo Color Tint (Base)` (a bright global multiplier) and `Base Color Tint (Mask)` (the
513
+ dark cloth colour) on a master that tints its albedo through a mask; the exact names missed
514
+ both, so every curtain kept `baseColorFactor` [1,1,1] and rendered the untinted light-grey
515
+ albedo. When both a global base multiplier and a mask colour are overridden, the mask tint is
516
+ the surface colour that shows through. Only an instance's own override is matched broadly, so
517
+ a master's placeholder default (the master's red `Base Color Tint (Mask)`) never paints an
518
+ un-tinted instance; a lone mask tint with no global multiplier (the 03a curtain, whose editor
519
+ mesh thumbnail is the untinted albedo) is left alone, and a `RockTint`-style name with no
520
+ colour token beside `Tint` is left to the graph baker.
521
+
522
+ Importer version 57 gives a mesh the `.mat` / `.props.txt` that sit beside it when two
523
+ packages share an object name (a pack with `MI_Rock_Inst` in two folders bound one
524
+ folder's normal map to the other's meshes); with none beside the mesh the pick is named
525
+ in the section's limitations. The graph bake now evaluates `ObjectScale` (1),
526
+ `PerInstanceRandom` (0.5) and `ObjectPositionWS` (origin) as named heuristic
527
+ approximations and `SplitComponents` exactly, which lets layered cliff-rock masters bake
528
+ their colour texture instead of falling back to neutral grey. The parity S4 check no
529
+ longer lets a 90% colour share hide sections whose graph is known to carry colour.
530
+
531
+ Legacy instance sidecars without override flags retain inherited settings for
532
+ ambiguous opaque, false, and zero defaults. Their non-default values retain the
533
+ previous behavior; missing flags are reported as a material limitation.
534
+
535
+ Importer version 50 reads a bounded authored-material subset from tagged UE4
536
+ object-version 516 packages (legacy -7, UE3 864, licensee 0). Exact source paths
537
+ and closest flagged static overrides select the active branches before reduction.
538
+ Mesh-only package selection still loads needed material parents from the source
539
+ file inventory without exporting unrelated packages; lookups stay within the same
540
+ Content namespace. Mesh import-table material references resolve duplicate source
541
+ basenames by exact object path. Multiple matching references refuse source routing;
542
+ a unique basename fallback reports its unresolved mesh routing.
543
+ Reroutes, scalar constants/parameters and representable multiplication recover
544
+ scalar roughness; glTF normalization retains the raw authored value in the report.
545
+ An authored scalar replaces any earlier roughness texture contribution while
546
+ retaining its packed metallic channel. Source functions, Specular conversion,
547
+ foliage shading and unsupported sampling remain explicit limitations.
548
+
549
+ AO.R is attached only with explicit `Texture2D.SRGB=false`, a serialized linear
550
+ sampler mode (`LinearColor`, `LinearGrayscale` or `Masks`), and source coordinates
551
+ matching the sampler of the currently bound albedo. Its existing glTF coordinate
552
+ mapping is reused only after the selected albedo attaches; an incoming unrelated
553
+ base texture cannot establish that reference. Omitted Unreal defaults remain
554
+ unresolved. Unknown color
555
+ interpretation, ambiguous source paths or exported PNG basenames, nonidentity AO
556
+ multipliers and unproved UV transforms withhold AO, including earlier filename
557
+ guesses. Texture metadata is
558
+ read lazily for the selected AO candidate; unrelated large images are untouched
559
+ by the material reader. Other package profiles keep prior reconstruction with a
560
+ named source-reader limitation. Cached conversions are invalidated.
561
+
562
+ The Buffer reader is independently implemented from the tagged package, property
563
+ and native-input schemas at [CUE4Parse commit b4e9544](https://github.com/FabianFG/CUE4Parse/tree/b4e95441bcf0c975eb3adb68c0fb44c740c2cf62/CUE4Parse/UE4).
564
+ Native expression inputs require verified Core=2 and Framework=33 custom versions;
565
+ tagged instances without native inputs do not require these versions. Native
566
+ trailers and omitted class defaults are outside its scope. This subset does not
567
+ certify original Unreal appearance.
568
+
377
569
  Automatic uncooked conversion needs Python 3 with `venv` and `pip`. A Linux
378
570
  source build fallback for UE Viewer additionally needs `git`, `g++`, `perl`,
379
571
  zlib, and SDL2 development headers. Unreal import currently runs on Linux and
@@ -460,11 +652,45 @@ texture.type = THREE.UnsignedByteType;
460
652
  texture.needsUpdate = true;
461
653
  ```
462
654
 
655
+ Raw `RGBA16F` textures load through the package's `./data-texture` subpath. The loader
656
+ checks the manifest against the binary (exact length, SHA-256, finite samples) and refuses the
657
+ load, rather than skipping a check, when one cannot run. The source package cannot be fetched
658
+ or re-hashed in a browser, so pin the value the report gives for the texture's source package:
659
+
660
+ ```js
661
+ import { loadRgba16fDataTexture } from "threenative-asset-mcp/data-texture";
662
+
663
+ const { texture, manifest } = await loadRgba16fDataTexture(
664
+ new URL(dataTexture.manifest, reportBaseUrl),
665
+ { expectedSourceSha256: dataTexture.sourceSha256 },
666
+ );
667
+ // texture: THREE.DataTexture, RGBAFormat, HalfFloatType, NoColorSpace, no mipmaps.
668
+ // The stored values are unchanged: negative, HDR, subnormal and alpha bits reach the shader as stored.
669
+ ```
670
+
671
+ `loadRgba16fDataTexture` needs an absolute URL and `crypto.subtle`, so serve the page over
672
+ https or from localhost. Its limit is the import's: the texture holds linear half-float
673
+ samples with no decoding, so a material must decide what they mean.
674
+
463
675
  Installation-only Unreal `BasicShapes/Plane` references are generated locally
464
676
  when the asset pack does not contain that mesh. Rectangular area lights are
465
677
  preserved in scene metadata and approximated as punctual point lights because
466
678
  glTF's standard punctual-light extension has no area-light type.
467
679
 
680
+ ### Parity sweep and material metadata dumps
681
+
682
+ `npm run parity:fab` imports every owned Fab artifact and scores it against its source package
683
+ (`scorecard.json`). `--export-metadata <dir>` also writes one replayable material dump per pack;
684
+ replay them with `FAB_METADATA_DIR=<dir> npx vitest run tests/fab-metadata.test.ts`. Dumps hold
685
+ licensed pack names and are local-only: never commit them.
686
+
687
+ Set `THREENATIVE_UNREAL_PREVIEW_REFERENCE=/path/to/reference.json` to light the comparison sheets (`parity:fab` and
688
+ `scripts/fidelity-sheet.ts`) with a local version-1 reference profile: radiance and Grid binaries beside the JSON, each
689
+ checked by SHA-256. It is read once per command, and an invalid profile stops the run before any download. Unset, the
690
+ sheets use the approximation and say so. Each picture records what lit it (`identity` in `picture-lighting.json`,
691
+ `sheets/<label>.json` and the scorecard entry). `--resume --sheets` refuses a kept sheet drawn otherwise: resume into a
692
+ new `--out`, or add `--no-sheets`. Profiles and binaries derive from licensed packs: keep them local-only, never commit them.
693
+
468
694
  ## Configuration
469
695
 
470
696
  | Variable | Default | Purpose |
@@ -491,8 +717,13 @@ glTF's standard punctual-light extension has no area-light type.
491
717
  | `AUDIO_CLAP_CHECKPOINT` | unset | Path to the `630k-audioset-best.pt` checkpoint. |
492
718
  | `AUDIO_CLAP_CHECKPOINT_SHA256` | unset | Optional pin; a mismatched checkpoint is refused. |
493
719
  | `ASSET_DOWNLOAD_DIR` | `~/Downloads/threenative-asset-mcp/assets` | Dedicated directory for direct provider downloads. |
494
- | `ASSET_MAX_DOWNLOAD_BYTES` | `10737418240` | Maximum accepted bytes per provider file (10 GiB). |
720
+ | `ASSET_MAX_DOWNLOAD_BYTES` | `10737418240` | Maximum bytes per direct file or complete generic acquisition (10 GiB). |
495
721
  | `ASSET_DOWNLOAD_TIMEOUT_MS` | `1800000` | Total timeout for one provider download (30 minutes). |
722
+ | `ASSET_ENABLE_KENNEY` | `1` | Bounded official Kenney 3D-pack discovery; `0` disables it. Other values fail initialization. |
723
+ | `ASSET_SEARCH_CONCURRENCY` | `4` | Concurrent provider operations per search; range 1–4. |
724
+ | `ASSET_PROVIDER_TIMEOUT_MS` | `8000` | Provider deadline; range 1–30000 milliseconds. |
725
+ | `ASSET_SEARCH_TIMEOUT_MS` | `10000` | Aggregate search/metadata deadline including queue time; range 1–60000. |
726
+ | `ASSET_PLAN_TTL_MS` | `300000` | Acquisition-plan TTL; range 1–900000; at most 100 bounded plans. |
496
727
  | `THREENATIVE_TOOLCHAIN_AUTOINSTALL` | `1` | Set to `0` to disable first-use external-tool installs. |
497
728
  | `THREENATIVE_TOOLCHAIN_DIR` | OS cache under `threenative-asset-mcp/toolchain` | UE Viewer, FabCLI, and uncooked-converter cache. |
498
729
  | `THREENATIVE_UMODEL_PATH` | auto-detected/provisioned | Absolute path to a UE Viewer executable override. |
@@ -639,6 +870,32 @@ The MCP host shows no tools
639
870
 
640
871
  ## Verification
641
872
 
873
+ GitHub-hosted CI checks pull requests, pushes to main and manual runs on the
874
+ minimum supported Node 20.19 and Node 24. It installs the committed npm lockfile,
875
+ runs typecheck/build, deterministic fixtures and actual built/npm-packed stdio
876
+ consumers. Reports distinguish intentionally conditional proprietary Unreal
877
+ fixtures. No provider live probes, credentials, package publishing or account
878
+ changes run in that workflow. Local CI-equivalent commands are:
879
+
880
+ ```bash
881
+ npm ci
882
+ npm run build
883
+ npm run typecheck
884
+ npm run test:ci
885
+ npm run test:smoke
886
+ ```
887
+
888
+ The suite needs Chromium (`npx playwright install --with-deps chromium`),
889
+ ffmpeg and Python with NumPy/Pillow for local render/audio fixtures. Live probes
890
+ remain separate from deterministic acceptance tests.
891
+
892
+ Run `npm run doctor` to see which of these your host has. Each missing tool is
893
+ listed with its install command. Locally, a suite whose tool is missing is
894
+ skipped with that command as the reason. With `CI=true` it fails instead.
895
+ Add `--toolchain` to also require what the Unreal importers need on a fresh
896
+ Linux host, or `--toolchain-only` to check only that group.
897
+
898
+
642
899
  ```bash
643
900
  npm ci
644
901
  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
  }