Editor-side tool that scans a given project asset file (prefab, scene, model, material, sound, etc.) and builds a dependency list by regex-parsing referenced asset and package identifiers, checking whether referenced files exist on disk and categorizing them.
#nullable enable
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Sandbox;
namespace Editor.Mcp
{
public static partial class SboxMcpAssistant
{
private static readonly Regex AssetRefRegex = new(
@"(?:""|')([^""']+\.(?:vmdl|vmat|vtex|sound|prefab|scene|vanim|shader|png|jpg|fbx|glb|mp3|wav|ogg))(?:""|')",
RegexOptions.Compiled | RegexOptions.IgnoreCase );
private static readonly Regex PackageRefRegex = new(
@"(?:package:|ident:)([a-zA-Z0-9_\-\.]+\.[a-zA-Z0-9_\-\.]+)",
RegexOptions.Compiled | RegexOptions.IgnoreCase );
/// <summary>
/// Scans a project asset (.prefab, .scene, .vmdl, .vmat, .sound, .vanim) and builds a full dependency tree
/// of referenced materials, textures, sounds, models, and child prefabs, verifying integrity on disk.
/// </summary>
[McpTool( "pocketknife_asset_dependencies" )]
public static string AssetDependencies(
[Description( "Relative path to asset inside project (e.g. 'scenes/untitled.scene', 'prefabs/player.prefab', 'sounds/Camera.sound')" )] string assetPath )
{
try
{
var root = Project.Current?.GetRootPath();
if ( string.IsNullOrWhiteSpace( root ) )
{
return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Unavailable, "project.not_active", "No active project is loaded." ) );
}
if ( string.IsNullOrWhiteSpace( assetPath ) )
{
return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "asset.path_required", "assetPath is required." ) );
}
var normalized = assetPath.Replace( '\\', '/' ).TrimStart( '/' );
var fullPath = Path.IsPathRooted( normalized ) ? normalized : Path.Combine( root, normalized );
if ( !File.Exists( fullPath ) )
{
return McpJson.Write( ToolOutcome<object>.Failure( new { Path = normalized, FullPath = fullPath }, ToolStatus.Unavailable, "asset.not_found", $"Asset file '{normalized}' not found on disk." ) );
}
var content = File.ReadAllText( fullPath );
var foundRefs = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
foreach ( Match m in AssetRefRegex.Matches( content ) )
{
var refPath = m.Groups[1].Value.Trim().Replace( '\\', '/' );
if ( !refPath.Equals( normalized, StringComparison.OrdinalIgnoreCase ) )
{
foundRefs.Add( refPath );
}
}
var packageRefs = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
foreach ( Match m in PackageRefRegex.Matches( content ) )
{
packageRefs.Add( m.Groups[1].Value.Trim() );
}
var resolvedDeps = new List<object>();
var missingDeps = new List<string>();
foreach ( var r in foundRefs )
{
var ext = Path.GetExtension( r ).ToLowerInvariant();
var category = ext switch
{
".vmdl" or ".fbx" or ".glb" => "Model",
".vmat" => "Material",
".vtex" or ".png" or ".jpg" => "Texture",
".sound" or ".wav" or ".mp3" or ".ogg" => "Sound",
".prefab" => "Prefab",
".scene" => "Scene",
".vanim" => "Animation",
".shader" => "Shader",
_ => "Asset"
};
var localDepPath = Path.Combine( root, r.TrimStart( '/' ) );
var existsLocally = File.Exists( localDepPath );
var status = existsLocally ? "FoundLocally" : (r.StartsWith( "models/" ) || r.StartsWith( "materials/" ) || r.StartsWith( "sounds/" ) ? "EngineOrCloudAsset" : "Missing");
if ( status == "Missing" )
{
missingDeps.Add( r );
}
resolvedDeps.Add( new
{
Reference = r,
Category = category,
Extension = ext,
Status = status,
ExistsOnDisk = existsLocally
} );
}
return McpJson.Write( ToolOutcome<object>.Ready( new
{
TargetAsset = normalized,
FileSize = new FileInfo( fullPath ).Length,
TotalDependencies = resolvedDeps.Count,
PackageDependencies = packageRefs.ToArray(),
MissingCount = missingDeps.Count,
MissingDependencies = missingDeps,
Dependencies = resolvedDeps,
Success = true
} ) );
}
catch ( Exception ex )
{
return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "asset.dependency_scan_failed", "Asset dependency analysis failed.", ex ) );
}
}
}
}