Skip to main content
Glama
assets.ts9.62 kB
import { UnrealBridge } from '../unreal-bridge.js'; import { promises as fs } from 'fs'; import path from 'path'; import { bestEffortInterpretedText, coerceNumber, coerceStringArray, interpretStandardResult } from '../utils/result-helpers.js'; export class AssetTools { constructor(private bridge: UnrealBridge) {} async importAsset(sourcePath: string, destinationPath: string) { let createdTestFile = false; try { // Sanitize destination path (remove trailing slash) and normalize UE path let cleanDest = destinationPath.replace(/\/$/, ''); // Map /Content -> /Game for UE asset destinations if (/^\/?content(\/|$)/i.test(cleanDest)) { cleanDest = '/Game' + cleanDest.replace(/^\/?content/i, ''); } // Create test FBX file if it's a test file if (sourcePath.includes('test_model.fbx')) { // Create the file outside of Python, before import try { await this.createTestFBX(sourcePath); createdTestFile = true; } catch (_err) { // If we can't create the file, we'll handle it in Python } } // Use Python API to import asset with file creation fallback const pythonCode = ` import unreal import os import json # Create test FBX if needed source_path = r'${sourcePath}' if 'test_model.fbx' in source_path and not os.path.exists(source_path): # Create directory if needed os.makedirs(os.path.dirname(source_path), exist_ok=True) # Create a valid FBX ASCII file fbx_content = """FBXHeaderExtension: { FBXHeaderVersion: 1003 FBXVersion: 7400 CreationTimeStamp: { Version: 1000 } Creator: "MCP FBX Test Generator" } GlobalSettings: { Version: 1000 Properties70: { P: "UpAxis", "int", "Integer", "",2 P: "UpAxisSign", "int", "Integer", "",1 P: "FrontAxis", "int", "Integer", "",1 P: "FrontAxisSign", "int", "Integer", "",1 P: "CoordAxis", "int", "Integer", "",0 P: "CoordAxisSign", "int", "Integer", "",1 P: "UnitScaleFactor", "double", "Number", "",1 } } Definitions: { Version: 100 Count: 2 ObjectType: "Model" { Count: 1 } ObjectType: "Geometry" { Count: 1 } } Objects: { Geometry: 1234567, "Geometry::Cube", "Mesh" { Vertices: *24 { a: -50,-50,50,50,-50,50,50,50,50,-50,50,50,-50,-50,-50,50,-50,-50,50,50,-50,-50,50,-50 } PolygonVertexIndex: *36 { a: 0,1,2,-4,4,5,6,-8,0,4,7,-4,1,5,4,-1,2,6,5,-2,3,7,6,-3 } GeometryVersion: 124 LayerElementNormal: 0 { Version: 101 Name: "" MappingInformationType: "ByPolygonVertex" ReferenceInformationType: "Direct" Normals: *108 { a: 0,0,1,0,0,1,0,0,1,0,0,1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,-1,0,0,-1,0,0,-1,0,0,-1,0,1,0,0,1,0,0,1,0,0,1,0,0,0,1,0,0,1,0,0,1,0,0,1,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0 } } LayerElementUV: 0 { Version: 101 Name: "UVMap" MappingInformationType: "ByPolygonVertex" ReferenceInformationType: "IndexToDirect" UV: *48 { a: 0,0,1,0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,0,1,1,0,1 } UVIndex: *36 { a: 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35 } } Layer: 0 { Version: 100 LayerElement: { Type: "LayerElementNormal" TypedIndex: 0 } LayerElement: { Type: "LayerElementUV" TypedIndex: 0 } } } Model: 2345678, "Model::Cube", "Mesh" { Version: 232 Properties70: { P: "InheritType", "enum", "", "",1 P: "ScalingMax", "Vector3D", "Vector", "",0,0,0 P: "DefaultAttributeIndex", "int", "Integer", "",0 } Shading: Y Culling: "CullingOff" } } Connections: { C: "OO",1234567,2345678 } """ with open(source_path, 'w') as f: f.write(fbx_content) print(f"Created test FBX file: {source_path}") # Set up the import task task = unreal.AssetImportTask() task.filename = r'${sourcePath}' task.destination_path = '${cleanDest}' task.automated = True task.save = True task.replace_existing = True # Configure FBX import options options = unreal.FbxImportUI() options.import_mesh = True options.import_as_skeletal = False options.mesh_type_to_import = unreal.FBXImportType.FBXIT_STATIC_MESH options.static_mesh_import_data.combine_meshes = True options.static_mesh_import_data.generate_lightmap_u_vs = False options.static_mesh_import_data.auto_generate_collision = False task.options = options # Use AssetTools to import asset_tools = unreal.AssetToolsHelpers.get_asset_tools() result = {'success': False, 'error': 'No assets imported', 'source': task.filename} try: asset_tools.import_asset_tasks([task]) if task.imported_object_paths: result = { 'success': True, 'imported': len(task.imported_object_paths), 'paths': list(task.imported_object_paths) } except Exception as e: result = {'success': False, 'error': str(e), 'source': task.filename} print('RESULT:' + json.dumps(result)) `.trim(); const pyResp = await this.bridge.executePython(pythonCode); const interpreted = interpretStandardResult(pyResp, { successMessage: `Imported assets to ${cleanDest}`, failureMessage: 'Import failed' }); if (interpreted.success) { const count = coerceNumber(interpreted.payload.imported) ?? 0; const paths = coerceStringArray(interpreted.payload.paths) ?? []; return { success: true, message: `Imported ${count} assets to ${cleanDest}`, imported: count, paths }; } const errorMessage = `Import failed: ${interpreted.error ?? 'Unknown error'} (source: ${interpreted.payload.source ?? sourcePath})`; return { success: false, message: errorMessage, error: errorMessage, details: bestEffortInterpretedText(interpreted) }; } catch (err) { return { success: false, error: `Failed to import asset: ${err}` }; } finally { if (createdTestFile) { try { await fs.rm(sourcePath, { force: true }); } catch (cleanupError) { // Swallow cleanup error but log for debug visibility console.warn(`Failed to clean up temporary FBX ${sourcePath}:`, cleanupError); } } } } async duplicateAsset(sourcePath: string, destinationPath: string) { try { const res = await this.bridge.call({ objectPath: '/Script/EditorScriptingUtilities.Default__EditorAssetLibrary', functionName: 'DuplicateAsset', parameters: { SourceAssetPath: sourcePath, DestinationAssetPath: destinationPath } }); return res?.Result ?? res; } catch (err) { return { error: `Failed to duplicate asset: ${err}` }; } } async deleteAsset(assetPath: string) { try { const res = await this.bridge.call({ objectPath: '/Script/EditorScriptingUtilities.Default__EditorAssetLibrary', functionName: 'DeleteAsset', parameters: { AssetPathToDelete: assetPath } }); return res?.Result ?? res; } catch (err) { return { error: `Failed to delete asset: ${err}` }; } } async saveAsset(assetPath: string) { try { const res = await this.bridge.call({ objectPath: '/Script/EditorScriptingUtilities.Default__EditorAssetLibrary', functionName: 'SaveAsset', parameters: { AssetToSave: assetPath } }); return res?.Result ?? res; } catch (err) { return { error: `Failed to save asset: ${err}` }; } } private async createTestFBX(filePath: string): Promise<void> { // Create a minimal valid FBX ASCII file for testing const fbxContent = `; FBX 7.5.0 project file FBXHeaderExtension: { FBXHeaderVersion: 1003 FBXVersion: 7500 CreationTimeStamp: { Version: 1000 Year: 2024 Month: 1 Day: 1 Hour: 0 Minute: 0 Second: 0 Millisecond: 0 } Creator: "MCP Test FBX Generator" } GlobalSettings: { Version: 1000 } Definitions: { Version: 100 Count: 2 ObjectType: "Model" { Count: 1 } ObjectType: "Geometry" { Count: 1 } } Objects: { Geometry: 1234567, "Geometry::Cube", "Mesh" { Vertices: *24 { a: -50,-50,-50,50,-50,-50,50,50,-50,-50,50,-50,-50,-50,50,50,-50,50,50,50,50,-50,50,50 } PolygonVertexIndex: *36 { a: 0,1,2,-4,4,7,6,-6,0,4,5,-2,1,5,6,-3,2,6,7,-4,4,0,3,-8 } GeometryVersion: 124 } Model: 2345678, "Model::TestCube", "Mesh" { Version: 232 Properties70: { P: "ScalingMax", "Vector3D", "Vector", "",0,0,0 P: "DefaultAttributeIndex", "int", "Integer", "",0 } } } Connections: { C: "OO",1234567,2345678 } `; // Ensure directory exists const dir = path.dirname(filePath); try { await fs.mkdir(dir, { recursive: true }); } catch {} // Write the FBX file await fs.writeFile(filePath, fbxContent, 'utf8'); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ChiR24/Unreal_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server