Skip to main content
Glama
assets.ts8.79 kB
import { UnrealBridge } from '../unreal-bridge.js'; import { coerceBoolean, coerceString, interpretStandardResult } from '../utils/result-helpers.js'; export class AssetResources { constructor(private bridge: UnrealBridge) {} // Simple in-memory cache for asset listing private cache = new Map<string, { timestamp: number; data: any }>(); private get ttlMs(): number { return Number(process.env.ASSET_LIST_TTL_MS || 10000); } private makeKey(dir: string, recursive: boolean, page?: number) { return page !== undefined ? `${dir}::${recursive ? 1 : 0}::${page}` : `${dir}::${recursive ? 1 : 0}`; } // Normalize UE content paths: // - Map '/Content' -> '/Game' // - Ensure forward slashes private normalizeDir(dir: string): string { try { if (!dir || typeof dir !== 'string') return '/Game'; let d = dir.replace(/\\/g, '/'); if (!d.startsWith('/')) d = '/' + d; if (d.toLowerCase().startsWith('/content')) { d = '/Game' + d.substring('/Content'.length); } // Collapse multiple slashes d = d.replace(/\/+/g, '/'); // Remove trailing slash except root if (d.length > 1) d = d.replace(/\/$/, ''); return d; } catch { return '/Game'; } } async list(dir = '/Game', _recursive = false, limit = 50) { // ALWAYS use non-recursive listing to show only immediate children // This prevents timeouts and makes navigation clearer _recursive = false; // Force non-recursive // Normalize directory first dir = this.normalizeDir(dir); // Cache fast-path try { const key = this.makeKey(dir, false); const entry = this.cache.get(key); const now = Date.now(); if (entry && (now - entry.timestamp) < this.ttlMs) { return entry.data; } } catch {} // Check if bridge is connected if (!this.bridge.isConnected) { return { assets: [], warning: 'Unreal Engine is not connected. Please ensure Unreal Engine is running with Remote Control enabled.', connectionStatus: 'disconnected' }; } // Always use directory-only listing (immediate children) return this.listDirectoryOnly(dir, false, limit); // End of list method - all logic is now in listDirectoryOnly } /** * List assets with pagination support * @param dir Directory to list assets from * @param page Page number (0-based) * @param pageSize Number of assets per page (max 50 to avoid socket failures) */ async listPaged(dir = '/Game', page = 0, pageSize = 30, recursive = false) { // Ensure pageSize doesn't exceed safe limit const safePageSize = Math.min(pageSize, 50); const offset = page * safePageSize; // Normalize directory and check cache for this specific page dir = this.normalizeDir(dir); const cacheKey = this.makeKey(dir, recursive, page); const cached = this.cache.get(cacheKey); if (cached && (Date.now() - cached.timestamp) < this.ttlMs) { return cached.data; } if (!this.bridge.isConnected) { return { assets: [], page, pageSize: safePageSize, warning: 'Unreal Engine is not connected.', connectionStatus: 'disconnected' }; } try { // Use search API with pagination // Use the same directory listing approach but with pagination const allAssets = await this.listDirectoryOnly(dir, false, 1000); // Paginate the results const start = offset; const end = offset + safePageSize; const pagedAssets = allAssets.assets ? allAssets.assets.slice(start, end) : []; const result = { assets: pagedAssets, page, pageSize: safePageSize, count: pagedAssets.length, totalCount: allAssets.assets ? allAssets.assets.length : 0, hasMore: end < (allAssets.assets ? allAssets.assets.length : 0), method: 'directory_listing_paged' }; this.cache.set(cacheKey, { timestamp: Date.now(), data: result }); return result; } catch (err: any) { console.warn(`Asset listing page ${page} failed:`, err.message); } return { assets: [], page, pageSize: safePageSize, error: 'Failed to fetch page' }; } /** * Directory-based listing of immediate children using AssetRegistry. * Returns both subfolders and assets at the given path. */ private async listDirectoryOnly(dir: string, _recursive: boolean, limit: number) { // Always return only immediate children to avoid timeout and improve navigation try { const py = ` import unreal import json _dir = r"${this.normalizeDir(dir)}" try: ar = unreal.AssetRegistryHelpers.get_asset_registry() # Immediate subfolders sub_paths = ar.get_sub_paths(_dir, False) folders_list = [] for p in sub_paths: try: name = p.split('/')[-1] folders_list.append({'n': name, 'p': p}) except Exception: pass # Immediate assets at this path assets_data = ar.get_assets_by_path(_dir, False) assets = [] for a in assets_data[:${limit}]: try: assets.append({ 'n': str(a.asset_name), 'p': str(a.object_path), 'c': str(a.asset_class) }) except Exception: pass print("RESULT:" + json.dumps({ 'success': True, 'path': _dir, 'folders': len(folders_list), 'files': len(assets), 'folders_list': folders_list, 'assets': assets })) except Exception as e: print("RESULT:" + json.dumps({'success': False, 'error': str(e), 'path': _dir})) `.trim(); const resp = await this.bridge.executePython(py); const interpreted = interpretStandardResult(resp, { successMessage: 'Directory contents retrieved', failureMessage: 'Failed to list directory contents' }); if (interpreted.success) { const payload = interpreted.payload as Record<string, unknown>; const foldersArr = Array.isArray(payload.folders_list) ? payload.folders_list.map((f: any) => ({ Name: coerceString(f?.n) ?? '', Path: coerceString(f?.p) ?? '', Class: 'Folder', isFolder: true })) : []; const assetsArr = Array.isArray(payload.assets) ? payload.assets.map((a: any) => ({ Name: coerceString(a?.n) ?? '', Path: coerceString(a?.p) ?? '', Class: coerceString(a?.c) ?? 'Asset', isFolder: false })) : []; const total = foldersArr.length + assetsArr.length; const summary = { total, folders: foldersArr.length, assets: assetsArr.length }; const resolvedPath = coerceString(payload.path) ?? this.normalizeDir(dir); return { success: true, path: resolvedPath, summary, foldersList: foldersArr, assets: assetsArr, count: total, note: `Immediate children of ${resolvedPath}: ${foldersArr.length} folder(s), ${assetsArr.length} asset(s)`, method: 'asset_registry_listing' }; } } catch (err: any) { console.warn('Engine asset listing failed:', err.message); } // Fallback: return empty with explanation return { success: true, path: this.normalizeDir(dir), summary: { total: 0, folders: 0, assets: 0 }, foldersList: [], assets: [], warning: 'No items at this path or failed to query AssetRegistry.', method: 'asset_registry_fallback' }; } async find(assetPath: string) { // Guard against invalid paths (trailing slash, empty, whitespace) if (!assetPath || typeof assetPath !== 'string' || assetPath.trim() === '' || assetPath.endsWith('/')) { return false; } // Normalize asset path (support users passing /Content/...) const ap = this.normalizeDir(assetPath); const py = ` import unreal apath = r"${ap}" try: exists = unreal.EditorAssetLibrary.does_asset_exist(apath) print("RESULT:{'success': True, 'exists': %s}" % ('True' if exists else 'False')) except Exception as e: print("RESULT:{'success': False, 'error': '" + str(e) + "'}") `.trim(); const resp = await this.bridge.executePython(py); const interpreted = interpretStandardResult(resp, { successMessage: 'Asset existence verified', failureMessage: 'Failed to verify asset existence' }); if (interpreted.success) { return coerceBoolean(interpreted.payload.exists, false) ?? false; } return false; } }

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