diff --git a/data/tool-guides/export-guide.md b/data/tool-guides/export-guide.md index dc4b28c..4dfb49d 100644 --- a/data/tool-guides/export-guide.md +++ b/data/tool-guides/export-guide.md @@ -1,8 +1,8 @@ --- title: "Export & File Format Guide" category: "export" -tags: ["export", "GLB", "GLTF", "FBX", "OBJ", "STL", "file format", "3D printing", "game engine", "web", "UV", "prepare_uv_layout", "validate_export_readiness", "export_object"] -triggered_by: ["prepare_uv_layout", "validate_export_readiness", "export_object"] +tags: ["export", "GLB", "GLTF", "FBX", "OBJ", "STL", "file format", "3D printing", "game engine", "web", "UV", "prepare_uv_layout", "validate_export_readiness", "export_asset_package", "export_object"] +triggered_by: ["prepare_uv_layout", "validate_export_readiness", "export_asset_package", "export_object"] description: "Domain knowledge for 3D model export, file format selection, pre-export preparation, and format-specific considerations." blender_version: "4.0+" --- @@ -48,6 +48,20 @@ blender_version: "4.0+" - Packed images are acceptable for Blender-internal work, but exported interchange assets should still be validated in the target viewer. ### Recommended Tool Sequence +Use `export_asset_package` for normal user-requested exports. It runs UV prep, optional rotation/scale application, readiness validation, export, and expected-file reporting in one deterministic flow: + +```text +export_asset_package( + names=["HeroMesh"], + filepath="C:/exports/hero.glb", + file_format="GLB", + uv_mode="preserve_original", + apply_transforms=true +) +``` + +Use the lower-level sequence only when a validation report needs manual repair: + 1. `prepare_uv_layout(names, mode: "preserve_original")` 2. If missing UVs on a generated mesh, `prepare_uv_layout(names, mode: "smart_project")` 3. `validate_export_readiness(names, filepath, file_format)` diff --git a/desktop/assets/vipermesh-addon.py b/desktop/assets/vipermesh-addon.py index f8fa280..0991cbc 100644 --- a/desktop/assets/vipermesh-addon.py +++ b/desktop/assets/vipermesh-addon.py @@ -268,6 +268,7 @@ def _execute_command_internal(self, command): "set_visibility": self.set_visibility, "prepare_uv_layout": self.prepare_uv_layout, "validate_export_readiness": self.validate_export_readiness, + "export_asset_package": self.export_asset_package, "export_object": self.export_object, "list_installed_addons": self.list_installed_addons, "create_material": self.create_material, @@ -5626,6 +5627,151 @@ def validate_export_readiness( except Exception as e: return {"error": f"Failed to validate export readiness: {str(e)}"} + def export_asset_package( + self, + names, + filepath, + file_format="GLB", + uv_mode="preserve_original", + apply_transforms=False, + required_uvs=True, + require_materials=False, + check_textures=True, + check_texture_formats=True, + make_dirs=True, + force_export=False, + ): + """Run UV prep, export readiness validation, optional transform apply, and export in one safe step.""" + previous_active = bpy.context.view_layer.objects.active + previous_selection = [obj for obj in bpy.context.scene.objects if obj.select_get()] + previous_mode = previous_active.mode if previous_active else "OBJECT" + try: + if not filepath: + return {"error": "filepath is required"} + + def coerce_bool(value, default=False): + if value is None: + return default + if isinstance(value, bool): + return value + if isinstance(value, str): + return value.strip().lower() in {"1", "true", "yes", "on"} + return bool(value) + + objects, missing, non_mesh = self._resolve_mesh_objects(names) + if missing: + return {"error": f"Object not found: {', '.join(missing)}"} + if not objects: + return {"error": "No mesh objects provided"} + + absolute_filepath = bpy.path.abspath(str(filepath)) + output_dir = os.path.dirname(absolute_filepath) + if output_dir and not os.path.exists(output_dir): + if coerce_bool(make_dirs, True): + os.makedirs(output_dir, exist_ok=True) + else: + return {"error": f"Output directory does not exist: {output_dir}"} + + uv_report = None + mode_key = str(uv_mode or "none").lower().replace("-", "_") + if mode_key not in {"none", "skip", "preserve_original", "smart_project", "lightmap_pack", "pack_existing"}: + return {"error": "uv_mode must be none, preserve_original, smart_project, lightmap_pack, or pack_existing"} + if mode_key not in {"none", "skip"}: + uv_report = self.prepare_uv_layout( + [obj.name for obj in objects], + mode=mode_key, + ) + if uv_report.get("error"): + return {"error": uv_report["error"], "uv_report": uv_report} + + transform_report = {"applied": False, "objects": []} + if coerce_bool(apply_transforms, False): + if bpy.context.object and bpy.context.object.mode != "OBJECT": + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.select_all(action="DESELECT") + for obj in objects: + obj.select_set(True) + transform_report["objects"].append(obj.name) + bpy.context.view_layer.objects.active = objects[0] + bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) + transform_report["applied"] = True + + validation = self.validate_export_readiness( + [obj.name for obj in objects], + filepath=absolute_filepath, + file_format=file_format, + required_uvs=coerce_bool(required_uvs, True), + require_materials=coerce_bool(require_materials, False), + require_applied_transforms=not transform_report["applied"], + check_textures=coerce_bool(check_textures, True), + check_texture_formats=coerce_bool(check_texture_formats, True), + require_absolute_path=True, + ) + if validation.get("error"): + return {"error": validation["error"], "validation": validation, "uv_report": uv_report} + + should_export = validation.get("ready", False) or coerce_bool(force_export, False) + if not should_export: + return { + "success": True, + "exported": False, + "filepath": absolute_filepath, + "format": validation.get("format"), + "uv_report": uv_report, + "transform_report": transform_report, + "validation": validation, + "errors": validation.get("errors", []), + "warnings": validation.get("warnings", []), + "next_safe_action": "fix validation errors or call export_asset_package with force_export=true", + } + + export_result = self.export_object( + [obj.name for obj in objects], + absolute_filepath, + validation.get("format") or file_format, + ) + if export_result.get("error"): + return { + "error": export_result["error"], + "uv_report": uv_report, + "transform_report": transform_report, + "validation": validation, + } + + expected_files = validation.get("expected_files") or [absolute_filepath] + existing_files = [path for path in expected_files if os.path.exists(path)] + missing_expected_files = [path for path in expected_files if not os.path.exists(path)] + + return { + "success": True, + "exported": True, + "filepath": absolute_filepath, + "format": validation.get("format"), + "exported_objects": export_result.get("exported_objects", []), + "file_size_bytes": export_result.get("file_size_bytes", 0), + "expected_files": expected_files, + "existing_files": existing_files, + "missing_expected_files": missing_expected_files, + "uv_report": uv_report, + "transform_report": transform_report, + "validation": validation, + "warnings": validation.get("warnings", []), + } + except Exception as e: + return {"error": f"Failed to export asset package: {str(e)}"} + finally: + with suppress(Exception): + if bpy.context.object and bpy.context.object.mode != "OBJECT": + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.select_all(action="DESELECT") + for obj in previous_selection: + if obj.name in bpy.data.objects: + obj.select_set(True) + if previous_active and previous_active.name in bpy.data.objects: + bpy.context.view_layer.objects.active = previous_active + if previous_mode != "OBJECT": + bpy.ops.object.mode_set(mode=previous_mode) + def export_object(self, names, filepath, file_format='GLB'): """Export selected objects to a file. Supports GLB, GLTF, FBX, OBJ, STL.""" try: diff --git a/docs/blender-mcp-capability-inventory.md b/docs/blender-mcp-capability-inventory.md index 7e09510..a0b7ad3 100644 --- a/docs/blender-mcp-capability-inventory.md +++ b/docs/blender-mcp-capability-inventory.md @@ -6,7 +6,7 @@ ## Summary - Official Blender MCP tools found: 26 -- ViperMesh addon commands found: 88 +- ViperMesh addon commands found: 89 - Decision: keep ViperMesh addon as the product connector, use official MCP as a benchmark/reference, and build deterministic ViperMesh tools where both sides currently rely on Python. ## Capability Matrix @@ -84,6 +84,7 @@ - `duplicate_object` - `duplicate_object_pattern` - `execute_code` +- `export_asset_package` - `export_object` - `focus_viewport_on_objects` - `get_all_object_info` diff --git a/lib/ai/agents.ts b/lib/ai/agents.ts index 6035c66..9c90f37 100644 --- a/lib/ai/agents.ts +++ b/lib/ai/agents.ts @@ -2165,6 +2165,69 @@ const validateExportReadiness = tool( } ) +const exportAssetPackage = tool( + async ({ + names, + filepath, + file_format, + uv_mode, + apply_transforms, + required_uvs, + require_materials, + check_textures, + check_texture_formats, + make_dirs, + force_export, + }: { + names: string[] + filepath: string + file_format?: string + uv_mode?: "none" | "preserve_original" | "smart_project" | "lightmap_pack" | "pack_existing" + apply_transforms?: boolean + required_uvs?: boolean + require_materials?: boolean + check_textures?: boolean + check_texture_formats?: boolean + make_dirs?: boolean + force_export?: boolean + }) => + executeMcpCommand("export_asset_package", { + names, + filepath, + file_format, + uv_mode, + apply_transforms, + required_uvs, + require_materials, + check_textures, + check_texture_formats, + make_dirs, + force_export, + }), + { + name: "export_asset_package", + description: + "Run a safe export package flow: optional UV preparation, optional rotation/scale transform apply, export readiness validation, export_object, and expected-file reporting. " + + "Prefer this for user-requested GLB/GLTF/FBX/OBJ/STL exports unless the workflow needs manual step-by-step repair.", + schema: z.object({ + names: z.array(z.string()).min(1).describe("Mesh object names to export"), + filepath: z.string().describe("Absolute output filepath including extension"), + file_format: z.string().optional().describe("Target format: GLB, GLTF, FBX, OBJ, or STL. Defaults to GLB."), + uv_mode: z + .enum(["none", "preserve_original", "smart_project", "lightmap_pack", "pack_existing"]) + .optional() + .describe("Optional UV preparation mode before validation/export. Defaults to preserve_original."), + apply_transforms: z.boolean().optional().describe("Apply rotation and scale before validation/export. Location is preserved."), + required_uvs: z.boolean().optional().describe("Require active UVs for material-capable formats. Defaults true."), + require_materials: z.boolean().optional().describe("Require at least one material on each mesh. Defaults false."), + check_textures: z.boolean().optional().describe("Check image texture file paths and packed-image status. Defaults true."), + check_texture_formats: z.boolean().optional().describe("Warn about risky portable-export image formats. Defaults true."), + make_dirs: z.boolean().optional().describe("Create the output directory if needed. Defaults true."), + force_export: z.boolean().optional().describe("Export even when validation reports errors. Use only when the user explicitly wants a best-effort export."), + }), + } +) + const listInstalledAddons = tool( async () => executeMcpCommand("list_installed_addons"), { @@ -2961,6 +3024,7 @@ const ALL_TOOLS = [ setVisibility, prepareUvLayout, validateExportReadiness, + exportAssetPackage, exportObject, listInstalledAddons, createMaterial, diff --git a/lib/orchestration/prompts/blender-agent-system.md b/lib/orchestration/prompts/blender-agent-system.md index 37be00c..ad88645 100644 --- a/lib/orchestration/prompts/blender-agent-system.md +++ b/lib/orchestration/prompts/blender-agent-system.md @@ -109,6 +109,7 @@ You have access to the following MCP tools. **Use direct tools whenever one matc - `shade_smooth(name, smooth?, angle?)`: Set smooth/flat shading. - `prepare_uv_layout(names, mode?, uv_map_name?, angle_limit?, island_margin?)`: Preserve original UVs or run Smart Project/lightmap/pack modes before texturing/export. - `validate_export_readiness(names, filepath?, file_format?, required_uvs?, require_materials?, require_applied_transforms?, check_textures?)`: Check UVs, textures, transforms, mesh validity, expected output files, and format risks before export. +- `export_asset_package(names, filepath, file_format?, uv_mode?, apply_transforms?, required_uvs?, require_materials?, check_textures?, check_texture_formats?, make_dirs?, force_export?)`: Run UV prep, optional rotation/scale apply, validation, export, and expected-file reporting in one safe package flow. ### 🎨 Material Tools - `create_material_preset(name, preset?, object_name?, base_color?, metallic?, roughness?, texture_maps?)`: Create/update a deterministic Blender 5.x Principled BSDF material preset and optionally assign it. Prefer this for glass, emissive, metal, fabric, ceramic, rubber, plastic, and PBR map wiring. @@ -171,7 +172,7 @@ You have access to the following MCP tools. **Use direct tools whenever one matc | Simple materials (color, metallic, roughness) | `create_material_preset` or `create_material` + `assign_material` | ✓ | | PBR maps, glass, emissive, fabric, rubber, ceramic, metal presets | `create_material_preset` + `inspect_material_node_graph` | ✓ | | Preserve imported UVs or unwrap generated meshes | `prepare_uv_layout` | ✓ | -| Validate before GLB/FBX/OBJ/STL export | `validate_export_readiness` then `export_object` | ✓ | +| Validate and package GLB/FBX/OBJ/STL exports | `export_asset_package`; use `validate_export_readiness` then `export_object` only for manual repair flows | ✓ | | Studio/product lighting and camera framing | `setup_studio_scene` + `validate_studio_scene` | ✓ | | World background/HDRI/procedural sky | `set_world_environment` | ✓ | | Lightweight thumbnail or preview artifact | `render_thumbnail_to_path` | ✓ | diff --git a/lib/orchestration/tool-filter.ts b/lib/orchestration/tool-filter.ts index cfe2f53..84105f2 100644 --- a/lib/orchestration/tool-filter.ts +++ b/lib/orchestration/tool-filter.ts @@ -6,7 +6,7 @@ const CATEGORY_GROUPS: Record = { animation: ["inspect_animation_data", "set_timeline_settings", "set_keyframe_animation"], organization: ["inspect_collection_hierarchy", "organize_collection_hierarchy", "select_scene_objects", "set_active_collection", "parent_set", "parent_clear", "move_to_collection", "set_visibility"], materials: ["list_materials", "create_material", "assign_material", "create_material_preset", "inspect_material_node_graph", "set_texture"], - pipeline: ["prepare_uv_layout", "validate_export_readiness", "export_object"], + pipeline: ["prepare_uv_layout", "validate_export_readiness", "export_asset_package", "export_object"], lighting: ["set_world_environment", "setup_studio_scene", "validate_studio_scene", "render_thumbnail_to_path", "add_light", "set_light_properties", "set_render_settings", "render_image"], camera: ["inspect_viewport_areas", "focus_viewport_on_objects", "get_viewport_screenshot", "render_viewport_to_path", "setup_studio_scene", "validate_studio_scene", "render_thumbnail_to_path", "add_camera", "set_camera_properties", "aim_camera_at", "set_render_settings", "render_image"], viewport: ["inspect_viewport_areas", "set_viewport_shading", "focus_viewport_on_objects", "select_scene_objects", "set_active_collection", "get_viewport_screenshot", "render_viewport_to_path"], diff --git a/lib/orchestration/tool-registry.ts b/lib/orchestration/tool-registry.ts index ea4a829..12581c7 100644 --- a/lib/orchestration/tool-registry.ts +++ b/lib/orchestration/tool-registry.ts @@ -253,6 +253,14 @@ export const TOOL_REGISTRY: ToolMetadata[] = [ category: "advanced", parameters: "names: string[], filepath: string, file_format?: GLB|GLTF|FBX|OBJ|STL", }, + { + name: "export_asset_package", + description: + "Run UV preparation, optional transform apply, export readiness validation, export, and expected-file reporting in one safe package flow.", + category: "advanced", + parameters: + "names: string[], filepath: string, file_format?: GLB|GLTF|FBX|OBJ|STL, uv_mode?: none|preserve_original|smart_project|lightmap_pack|pack_existing, apply_transforms?: boolean, required_uvs?: boolean, require_materials?: boolean, check_textures?: boolean, check_texture_formats?: boolean, make_dirs?: boolean, force_export?: boolean", + }, { name: "list_installed_addons", description: diff --git a/public/downloads/vipermesh-addon.py b/public/downloads/vipermesh-addon.py index f8fa280..0991cbc 100644 --- a/public/downloads/vipermesh-addon.py +++ b/public/downloads/vipermesh-addon.py @@ -268,6 +268,7 @@ def _execute_command_internal(self, command): "set_visibility": self.set_visibility, "prepare_uv_layout": self.prepare_uv_layout, "validate_export_readiness": self.validate_export_readiness, + "export_asset_package": self.export_asset_package, "export_object": self.export_object, "list_installed_addons": self.list_installed_addons, "create_material": self.create_material, @@ -5626,6 +5627,151 @@ def validate_export_readiness( except Exception as e: return {"error": f"Failed to validate export readiness: {str(e)}"} + def export_asset_package( + self, + names, + filepath, + file_format="GLB", + uv_mode="preserve_original", + apply_transforms=False, + required_uvs=True, + require_materials=False, + check_textures=True, + check_texture_formats=True, + make_dirs=True, + force_export=False, + ): + """Run UV prep, export readiness validation, optional transform apply, and export in one safe step.""" + previous_active = bpy.context.view_layer.objects.active + previous_selection = [obj for obj in bpy.context.scene.objects if obj.select_get()] + previous_mode = previous_active.mode if previous_active else "OBJECT" + try: + if not filepath: + return {"error": "filepath is required"} + + def coerce_bool(value, default=False): + if value is None: + return default + if isinstance(value, bool): + return value + if isinstance(value, str): + return value.strip().lower() in {"1", "true", "yes", "on"} + return bool(value) + + objects, missing, non_mesh = self._resolve_mesh_objects(names) + if missing: + return {"error": f"Object not found: {', '.join(missing)}"} + if not objects: + return {"error": "No mesh objects provided"} + + absolute_filepath = bpy.path.abspath(str(filepath)) + output_dir = os.path.dirname(absolute_filepath) + if output_dir and not os.path.exists(output_dir): + if coerce_bool(make_dirs, True): + os.makedirs(output_dir, exist_ok=True) + else: + return {"error": f"Output directory does not exist: {output_dir}"} + + uv_report = None + mode_key = str(uv_mode or "none").lower().replace("-", "_") + if mode_key not in {"none", "skip", "preserve_original", "smart_project", "lightmap_pack", "pack_existing"}: + return {"error": "uv_mode must be none, preserve_original, smart_project, lightmap_pack, or pack_existing"} + if mode_key not in {"none", "skip"}: + uv_report = self.prepare_uv_layout( + [obj.name for obj in objects], + mode=mode_key, + ) + if uv_report.get("error"): + return {"error": uv_report["error"], "uv_report": uv_report} + + transform_report = {"applied": False, "objects": []} + if coerce_bool(apply_transforms, False): + if bpy.context.object and bpy.context.object.mode != "OBJECT": + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.select_all(action="DESELECT") + for obj in objects: + obj.select_set(True) + transform_report["objects"].append(obj.name) + bpy.context.view_layer.objects.active = objects[0] + bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) + transform_report["applied"] = True + + validation = self.validate_export_readiness( + [obj.name for obj in objects], + filepath=absolute_filepath, + file_format=file_format, + required_uvs=coerce_bool(required_uvs, True), + require_materials=coerce_bool(require_materials, False), + require_applied_transforms=not transform_report["applied"], + check_textures=coerce_bool(check_textures, True), + check_texture_formats=coerce_bool(check_texture_formats, True), + require_absolute_path=True, + ) + if validation.get("error"): + return {"error": validation["error"], "validation": validation, "uv_report": uv_report} + + should_export = validation.get("ready", False) or coerce_bool(force_export, False) + if not should_export: + return { + "success": True, + "exported": False, + "filepath": absolute_filepath, + "format": validation.get("format"), + "uv_report": uv_report, + "transform_report": transform_report, + "validation": validation, + "errors": validation.get("errors", []), + "warnings": validation.get("warnings", []), + "next_safe_action": "fix validation errors or call export_asset_package with force_export=true", + } + + export_result = self.export_object( + [obj.name for obj in objects], + absolute_filepath, + validation.get("format") or file_format, + ) + if export_result.get("error"): + return { + "error": export_result["error"], + "uv_report": uv_report, + "transform_report": transform_report, + "validation": validation, + } + + expected_files = validation.get("expected_files") or [absolute_filepath] + existing_files = [path for path in expected_files if os.path.exists(path)] + missing_expected_files = [path for path in expected_files if not os.path.exists(path)] + + return { + "success": True, + "exported": True, + "filepath": absolute_filepath, + "format": validation.get("format"), + "exported_objects": export_result.get("exported_objects", []), + "file_size_bytes": export_result.get("file_size_bytes", 0), + "expected_files": expected_files, + "existing_files": existing_files, + "missing_expected_files": missing_expected_files, + "uv_report": uv_report, + "transform_report": transform_report, + "validation": validation, + "warnings": validation.get("warnings", []), + } + except Exception as e: + return {"error": f"Failed to export asset package: {str(e)}"} + finally: + with suppress(Exception): + if bpy.context.object and bpy.context.object.mode != "OBJECT": + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.select_all(action="DESELECT") + for obj in previous_selection: + if obj.name in bpy.data.objects: + obj.select_set(True) + if previous_active and previous_active.name in bpy.data.objects: + bpy.context.view_layer.objects.active = previous_active + if previous_mode != "OBJECT": + bpy.ops.object.mode_set(mode=previous_mode) + def export_object(self, names, filepath, file_format='GLB'): """Export selected objects to a file. Supports GLB, GLTF, FBX, OBJ, STL.""" try: diff --git a/scripts/test/test-blender-export-package-tool.ts b/scripts/test/test-blender-export-package-tool.ts new file mode 100644 index 0000000..abb9c69 --- /dev/null +++ b/scripts/test/test-blender-export-package-tool.ts @@ -0,0 +1,49 @@ +import assert from "node:assert/strict" +import { readFileSync } from "node:fs" + +import { extractViperMeshCommandNames } from "../../lib/blender/capability-inventory" +import { filterRelevantTools, formatToolListForPrompt } from "../../lib/orchestration/tool-filter" + +const addonSource = readFileSync("desktop/assets/vipermesh-addon.py", "utf8") +const publicAddonSource = readFileSync("public/downloads/vipermesh-addon.py", "utf8") + +function extractPythonMethod(source: string, methodName: string): string { + const startToken = ` def ${methodName}(` + const start = source.indexOf(startToken) + assert.notEqual(start, -1, `Expected ${methodName} definition`) + const next = source.indexOf("\n def ", start + startToken.length) + return source.slice(start, next === -1 ? source.length : next) +} + +for (const [label, source] of [ + ["desktop addon", addonSource], + ["public addon", publicAddonSource], +] as const) { + const commands = extractViperMeshCommandNames(source) + assert.ok(commands.includes("export_asset_package"), `${label} exposes export_asset_package`) + const body = extractPythonMethod(source, "export_asset_package") + assert.match(body, /prepare_uv_layout/, `${label} can run UV preparation`) + assert.match(body, /validate_export_readiness/, `${label} validates before export`) + assert.match(body, /export_object/, `${label} delegates to exporter`) + assert.match(body, /transform_apply/, `${label} can apply rotation and scale before export`) + assert.match(body, /expected_files/, `${label} reports expected files`) +} + +const exportTools = filterRelevantTools("package this model as a validated GLB export with UV checks") +assert.ok(exportTools.includes("export_asset_package")) +assert.ok(exportTools.includes("validate_export_readiness")) + +const promptToolList = formatToolListForPrompt(["export_asset_package"]) +assert.match(promptToolList, /package/i) +assert.match(promptToolList, /export/i) + +const agentSource = readFileSync("lib/ai/agents.ts", "utf8") +assert.match(agentSource, /executeMcpCommand\("export_asset_package"/) + +const systemPrompt = readFileSync("lib/orchestration/prompts/blender-agent-system.md", "utf8") +assert.match(systemPrompt, /export_asset_package/) + +const exportGuide = readFileSync("data/tool-guides/export-guide.md", "utf8") +assert.match(exportGuide, /export_asset_package/) + +console.log("Blender export package tool tests passed") diff --git a/scripts/test/test-tool-guide-trigger-coverage.ts b/scripts/test/test-tool-guide-trigger-coverage.ts index d53cfa4..68ea260 100644 --- a/scripts/test/test-tool-guide-trigger-coverage.ts +++ b/scripts/test/test-tool-guide-trigger-coverage.ts @@ -60,6 +60,7 @@ const requiredGuidedTools = [ "organize_collection_hierarchy", "set_world_environment", "render_thumbnail_to_path", + "export_asset_package", "get_polyhaven_status", "get_polyhaven_categories", "search_polyhaven_assets",