🔍 s&box Package Code Search

Search C# source code, UI razor templates, shaders, and configs across s&box packages.

Showing code results for query: * (2 total matches found)
fobiat.sbox_mcp_server / Editor/SboxMcpServer.Editor.cs
Editor library
//  s&box MCP Server toolset, part two : what the editor believes, and making it notice
//  a change on disk. See SboxMcpServer.cs for the header, licence and the engine-truth
//  half of this same partial class.

#nullable enable

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Sandbox;

namespace Editor.Mcp;

public static partial class SboxMcpServer
{
	public enum CompilerSlot
	{
		Both,
		Game,
		Editor,
	}


	/// <summary>Report which project the editor has open, where it sits on disk, and the compiler settings currently live in memory. Start here when an on-disk change is not taking effect: the settings reported are what Roslyn is actually using, which is not necessarily what the .sbproj on disk now says.</summary>
	[McpTool.ReadOnly( "project_info" )]
	public static ProjectInfo Info()
	{
		var project = Open();

		return new ProjectInfo
		{
			Ident = project.Config?.FullIdent,
			Title = project.Config?.Title,
			Type = project.Config?.Type,
			RootDirectory = project.RootDirectory?.FullName,
			ConfigFilePath = project.ConfigFilePath,
			Active = project.Active,
			Broken = project.Broken,
			IsPublished = project.IsPublished,
			HasCompiler = project.HasCompiler,
			CompileSettings = LiveCompileSettings( project ),
		};
	}

	/// <summary>List the project's compilers with their build state. A compiler sitting at NeedsBuild true while IsBuilding is false has work queued that nothing has started, which is what a stalled build looks like from the outside.</summary>
	[McpTool.ReadOnly( "project_compilers" )]
	public static CompilerList Compilers(
		[Description( "Which compiler to report on." )] CompilerSlot slot = CompilerSlot.Both )
	{
		var project = Open();

		return new CompilerList
		{
			Compilers = Slots( project, slot ).Select( Describe ).ToArray(),
		};
	}

	/// <summary>Ask each compiler what source changes it has actually noticed since its last build. This is the direct answer to "did my edit register", and it separates a file the compiler never saw from a file it saw and rejected.</summary>
	[McpTool.ReadOnly( "project_source_changes" )]
	public static SourceChangeList SourceChanges(
		[Description( "Which compiler to report on." )] CompilerSlot slot = CompilerSlot.Both )
	{
		var project = Open();

		var compilers = Slots( project, slot ).Select( Noticed ).ToArray();

		return new SourceChangeList
		{
			Compilers = compilers,

			// An empty summary is genuinely ambiguous: the engine also returns {} when there is no
			// previous syntax tree to diff against, which is every compiler on a cold editor.
			Hint = compilers.Length > 0 && compilers.All( compiler => compiler.ChangeCount == 0 )
				? "Every change set is empty. That means either nothing changed, or the compilers have no baseline to diff against yet because they have not built since the editor opened. Run project_build once, then ask again - a second empty answer is a real one."
				: null,
		};
	}

	/// <summary>Return current compile diagnostics as structured rows with file and line, so errors can be read without scraping read_console. Errors sort first.</summary>
	[McpTool.ReadOnly( "project_compile_errors" )]
	public static DiagnosticList CompileErrors(
		[Description( "Include warnings alongside errors." )] bool includeWarnings = false,
		[Description( "Maximum rows to return." )] [Sandbox.Range( 1, 500 )] int limit = 50 )
	{
		var raw = Project.CompileGroup?.BuildResult.Diagnostics;

		var floor = includeWarnings ? DiagnosticSeverity.Warning : DiagnosticSeverity.Error;

		var rows = raw is null
			? Array.Empty<DiagnosticRow>()
			: raw.Where( diagnostic => diagnostic.Severity >= floor )
				.OrderByDescending( diagnostic => diagnostic.Severity )
				.Take( limit )
				.Select( Flatten )
				.ToArray();

		return new DiagnosticList
		{
			Count = rows.Length,
			Diagnostics = rows,
			Hint = rows.Length == 0 ? "No diagnostics. If a source edit still is not live, run project_source_changes, then project_assembly_freshness." : null,
		};
	}

	/// <summary>Compare what each compiler last built against what the process has actually loaded. Recompiling does not always cure a stale assembly: the editor goes on serving the version it loaded, and compile_status reads green the whole time.</summary>
	[McpTool.ReadOnly( "project_assembly_freshness" )]
	public static AssemblyFreshness AssemblyFreshnessOf()
	{
		var project = Open();
		var loaded = AppDomain.CurrentDomain.GetAssemblies();

		var rows = Slots( project, CompilerSlot.Both )
			.Select( slot =>
			{
				var built = slot.Compiler.Output?.Version;

				// Hotloading leaves older copies behind under the same simple name, so the newest
				// one loaded is the one the process is serving
				var copies = loaded
					.Select( assembly => assembly.GetName() )
					.Where( name => Same( name.Name, slot.Compiler.AssemblyName ) )
					.Select( name => name.Version )
					.Where( version => version is not null )
					.ToArray();

				var newest = copies.Length == 0 ? null : copies.Max();
				var stale = built is not null && newest is not null && built > newest;

				return new AssemblyFreshnessRow
				{
					Slot = slot.Label,
					Name = slot.Compiler.Name,
					AssemblyName = slot.Compiler.AssemblyName,
					BuiltVersion = built?.ToString(),
					LoadedVersion = newest?.ToString(),
					LoadedCopies = copies.Length,
					Stale = stale,
					Hint = built is null ? "This compiler has never produced a build, so there is nothing to compare."
						: newest is null ? "Nothing by that assembly name is loaded. The build has not been hotloaded into the process at all."
						: stale ? "The process is running an older build than the compiler produced. Rebuilding will not fix this - close and reopen the editor."
						: null,
				};
			} )
			.ToArray();

		return new AssemblyFreshness
		{
			Assemblies = rows,
			AnyStale = rows.Any( row => row.Stale ),
		};
	}

	/// <summary>Resolve one or more content paths against everything currently mounted, applying the same _c suffix rule the engine applies when it loads a resource. A path that resolves to nothing does not throw at runtime: Model.Load hands back the engine's error model, so a typo in a .item or a prefab compiles clean, passes every headless test and ships an orange world.</summary>
	[McpTool.ReadOnly( "project_content_path" )]
	public static ContentPathResult ContentPath(
		[Description( "One content path, or several separated by commas. For example \"models/citizen/citizen.vmdl\"." )] string paths )
	{
		var wanted = (paths ?? "")
			.Split( ',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries )
			.ToArray();

		if ( wanted.Length == 0 )
			throw new Exception( "Pass at least one content path. Separate several with commas." );

		var rows = wanted.Select( path =>
		{
			// ResourceLibrary.LoadGameResource does exactly this before it touches the filesystem
			var compiled = path.EndsWith( "_c", StringComparison.Ordinal );
			var resolved = compiled ? path : path + "_c";
			var exists = FileSystem.Mounted?.FileExists( resolved ) ?? false;

			return new ContentPathRow
			{
				Input = path,
				Resolved = resolved,
				Exists = exists,
				// The asset system indexes source paths, so it wants the name without the suffix
				Package = exists ? AssetSystem.FindByPath( compiled ? path[..^2] : path )?.Package?.FullIdent : null,
				Hint = exists ? null : "Nothing mounted provides this. Run project_content_search on its parent directory to find the real spelling - mounted packages often disagree with their own CDN manifests about the path prefix.",
			};
		} ).ToArray();

		return new ContentPathResult
		{
			Count = rows.Length,
			Found = rows.Count( row => row.Exists ),
			Missing = rows.Count( row => !row.Exists ),
			Paths = rows,
		};
	}

	/// <summary>List files under a directory in the mounted content filesystem. This is the "then what IS the right path" companion to project_content_path: mounted packages routinely disagree with their own CDN manifests about the path prefix, and the manifest spelling fails at runtime with no symptom at all.</summary>
	[McpTool.ReadOnly( "project_content_search" )]
	public static ContentSearchResult ContentSearch(
		[Description( "Directory to search, for example \"models/citizen\". Use \"/\" for everything." )] string directory = "/",
		[Description( "Filename pattern, for example \"*.vmdl_c\". Compiled content carries the _c suffix." )] string pattern = "*",
		[Description( "Recurse into subdirectories. A recursive search from \"/\" walks every mounted package and is slow." )] bool recursive = true,
		[Description( "Maximum results." )] [Sandbox.Range( 1, 500 )] int limit = 50 )
	{
		var mounted = FileSystem.Mounted
			?? throw new Exception( "Nothing is mounted yet. Wait for the editor to finish loading the project." );

		var found = mounted.FindFile( string.IsNullOrWhiteSpace( directory ) ? "/" : directory, pattern, recursive ).ToArray();
		var page = found.Take( limit ).ToArray();

		return new ContentSearchResult
		{
			Directory = directory,
			Pattern = pattern,
			Count = page.Length,
			Total = found.Length,
			Files = page,
			Hint = found.Length > 0
				? "These are relative to the directory searched. Pass a full path to project_content_path to confirm it resolves."
				: "Nothing matched. Widen the pattern, or search the parent directory - the prefix is what usually differs from what a manifest says.",
		};
	}

	/// <summary>Reconcile the package references written in the .sbproj against what is actually installed in the cloud cache. install_package mounts a package for this session and writes nothing to the project, so a package installed over MCP works perfectly until the editor restarts and then is simply gone.</summary>
	[McpTool.ReadOnly( "project_package_references" )]
	public static PackageReferenceReport PackageReferences()
	{
		var project = Open();
		var referenced = project.Config?.PackageReferences ?? new List<string>();
		var installed = AssetSystem.GetInstalledPackages();

		var rows = referenced.Select( ident => new PackageReferenceRow
		{
			Ident = ident,
			Installed = AssetSystem.IsCloudInstalled( ident ),
			Title = installed.FirstOrDefault( package => Same( package.FullIdent, ident ) )?.Title,
		} ).ToArray();

		var extra = installed
			.Select( package => package.FullIdent )
			.Where( ident => ident is not null && !referenced.Any( reference => Same( reference, ident ) ) )
			.OrderBy( ident => ident, StringComparer.OrdinalIgnoreCase )
			.ToArray();

		var missing = rows.Where( row => !row.Installed ).Select( row => row.Ident ).ToArray();

		return new PackageReferenceReport
		{
			References = rows,
			InstalledNotReferenced = extra,
			Hint = missing.Length > 0
				? $"Referenced but not installed: {string.Join( ", ", missing )}. Anything they provide will resolve to nothing at runtime."
				: extra.Length > 0
					? "Installed but not referenced. These are mounted for this session only - install_package writes nothing to the .sbproj, so they vanish at the next editor start. Add them to PackageReferences and run project_reload_config."
					: null,
		};
	}


	/// <summary>Re-read the project's .sbproj from disk into the live config and recreate its compilers, so an externally edited Metadata.Compiler block actually reaches Roslyn. Nothing watches that file, so without this an on-disk config change silently never takes effect.</summary>
	[McpTool( "project_reload_config" )]
	public static ReloadConfigResult ReloadConfig()
	{
		var project = Open();

		var wasGame = project.Compiler;
		var wasEditor = project.EditorCompiler;

		if ( Engine.CallOwned( project, "LoadMinimal" ) is not true )
			throw new Exception( "Reloading the .sbproj failed, most likely a syntax error in it. Check read_console." );

		// UpdateCompiler early-returns when CompilerHash still matches lastCompilerHash, and that
		// hash covers only compile settings, org, ident, type, the standalone flag and package
		// references. Zeroing it is what stops an edit to anything else being a silent no-op.
		Engine.SetOwned( project, "lastCompilerHash", 0 );
		Engine.CallOwned( project, "UpdateCompiler" );

		var recreated = (project.Compiler is { } game && !ReferenceEquals( wasGame, game ))
			|| (project.EditorCompiler is { } editor && !ReferenceEquals( wasEditor, editor ));

		return new ReloadConfigResult
		{
			Reloaded = true,
			CompilersRecreated = recreated,
			CompileSettings = LiveCompileSettings( project ),
			Hint = recreated
				? "Compilers were recreated, so their file watchers are fresh too. Run project_build to compile against the settings above."
				: project.HasCompiler
					? "The config reloaded but no compiler was recreated. That happens when the project loaded a precompiled assembly instead of building one."
					: "The config reloaded and the project now has no compiler at all. Check Active and the code path in project_info - a config edit can deactivate a project.",
		};
	}

	/// <summary>Drop the cached ProjectSettings and push the reloaded input config back into the engine, so an externally edited Input.config, Platform.config or Collision.config actually takes effect. These are cached on first read and never invalidated, which is a separate trap from the .sbproj one.</summary>
	[McpTool( "project_reload_settings" )]
	public static ReloadSettingsResult ReloadSettings()
	{
		var before = Sandbox.Input.GetActions()?.Count() ?? 0;

		Engine.CallShared( typeof( ProjectSettings ), "ClearCache" );

		// Clearing the cache alone changes nothing here: Input.GetActions reads a static field that
		// only Input.ReadConfig ever assigns, and nothing calls it on a settings reload.
		Engine.CallShared( typeof( Sandbox.Input ), "ReadConfig", new object?[] { ProjectSettings.Input } );

		var after = Sandbox.Input.GetActions()?.Count() ?? 0;

		return new ReloadSettingsResult
		{
			Reloaded = true,
			ActionsBefore = before,
			ActionsAfter = after,
			Hint = "Every other config re-reads lazily on next access. Call project_input_actions to confirm Input.config parsed the way you meant it.",
		};
	}

	/// <summary>Dispose and recreate every compiler, then start a build from the source on disk. Returns immediately.</summary>
	[McpTool( "project_rebuild" )]
	public static RebuildResult Rebuild()
	{
		RecreateCompilers();

		return new RebuildResult
		{
			Rebuilding = true,
			Hint = "Poll compile_status until IsBuilding is false, then call project_compile_errors.",
		};
	}

	/// <summary>Build the project and wait for it to finish, then report success along with any errors. This is the one-shot version of project_rebuild followed by polling: reach for it when the question is simply whether the code compiles.</summary>
	[McpTool( "project_build" )]
	public static async Task<BuildResult> Build(
		[Description( "Recreate compilers first, which also resets stale file watchers." )] bool rebuild = true )
	{
		if ( rebuild )
			RecreateCompilers();

		var building = Engine.CallShared( typeof( Project ), "CompileAsync" ) as Task
			?? throw new Exception( "Project.CompileAsync did not return a Task, engine API changed." );

		await building;

		var succeeded = Engine.Peek( building, "Result" ) is true;

		return new BuildResult
		{
			Success = succeeded,
			Errors = succeeded ? null : CompileErrors(),
			Hint = succeeded ? "Built. If the change still is not live, run project_assembly_freshness." : null,
		};
	}


	static Project Open()
	{
		return Project.Current ?? throw new Exception( "No project is open in the editor." );
	}

	static void RecreateCompilers()
	{
		Engine.CallShared( typeof( Project ), "RebuildCompilers" );
	}

	static (string Label, Compiler Compiler)[] Slots( Project project, CompilerSlot slot )
	{
		var found = new List<(string, Compiler)>();

		if ( slot is CompilerSlot.Both or CompilerSlot.Game && project.Compiler is { } game )
			found.Add( ("Game", game) );

		if ( slot is CompilerSlot.Both or CompilerSlot.Editor && project.EditorCompiler is { } editor )
			found.Add( ("Editor", editor) );

		return found.ToArray();
	}

	static Assembly[] ProjectAssemblies()
	{
		var project = Project.Current;
		if ( project is null ) return Array.Empty<Assembly>();

		var names = Slots( project, CompilerSlot.Both ).Select( slot => slot.Compiler.AssemblyName ).ToArray();

		return AppDomain.CurrentDomain.GetAssemblies()
			.Where( assembly => names.Any( name => Same( assembly.GetName().Name, name ) ) )
			.ToArray();
	}

	static CompileSettingsInfo? LiveCompileSettings( Project project )
	{
		if ( project.Config is null ) return null;

		var settings = Engine.CallOwned( project.Config, "GetCompileSettings" );
		if ( settings is null ) return null;

		return new CompileSettingsInfo
		{
			TreatWarningsAsErrors = Engine.Peek( settings, "TreatWarningsAsErrors" ) as bool?,
			Nullables = Engine.Peek( settings, "Nullables" ) as bool?,
			RootNamespace = Engine.Peek( settings, "RootNamespace" ) as string,
			DefineConstants = Engine.Peek( settings, "DefineConstants" ) as string,
			NoWarn = Engine.Peek( settings, "NoWarn" ) as string,
			WarningsAsErrors = Engine.Peek( settings, "WarningsAsErrors" ) as string,
		};
	}

	static CompilerInfo Describe( (string Label, Compiler Compiler) slot )
	{
		// Compiler.BuildSuccess is Output?.Successful ?? false, which cannot tell a failed build
		// from one that has never run. Output itself can.
		var success = slot.Compiler.Output?.Successful;

		return new CompilerInfo
		{
			Slot = slot.Label,
			Name = slot.Compiler.Name,
			AssemblyName = slot.Compiler.AssemblyName,
			IsBuilding = slot.Compiler.IsBuilding,
			NeedsBuild = slot.Compiler.NeedsBuild,
			Success = success,
			Hint = success is null ? "Never built. This is not a failure, it is an absence - run project_build." : null,
		};
	}

	static SourceChangeInfo Noticed( (string Label, Compiler Compiler) slot )
	{
		var changes = Engine.Hidden( slot.Compiler, "ChangeSummary" ) as Dictionary<string, object>;

		return new SourceChangeInfo
		{
			Slot = slot.Label,
			Name = slot.Compiler.Name,
			ChangeCount = changes?.Count ?? 0,
			Changes = changes,
		};
	}


	static DiagnosticRow Flatten( Diagnostic diagnostic )
	{
		var span = diagnostic.Location.GetLineSpan();
		var located = !string.IsNullOrEmpty( span.Path );

		return new DiagnosticRow
		{
			Id = diagnostic.Id,
			Severity = diagnostic.Severity.ToString(),
			Message = diagnostic.GetMessage(),
			File = located ? span.Path : null,
			Line = located ? span.StartLinePosition.Line + 1 : null,
		};
	}


	public class ProjectInfo
	{
		public string? Ident { get; set; }
		public string? Title { get; set; }
		public string? Type { get; set; }
		public string? RootDirectory { get; set; }
		public string? ConfigFilePath { get; set; }
		public bool Active { get; set; }
		public bool Broken { get; set; }
		public bool IsPublished { get; set; }
		public bool HasCompiler { get; set; }
		public CompileSettingsInfo? CompileSettings { get; set; }
	}

	public class CompileSettingsInfo
	{
		public bool? TreatWarningsAsErrors { get; set; }
		public bool? Nullables { get; set; }
		public string? RootNamespace { get; set; }
		public string? DefineConstants { get; set; }
		public string? NoWarn { get; set; }
		public string? WarningsAsErrors { get; set; }
	}

	public class CompilerList
	{
		public CompilerInfo[] Compilers { get; set; } = Array.Empty<CompilerInfo>();
	}

	public class CompilerInfo
	{
		[Description( "Game or Editor." )]
		public string? Slot { get; set; }
		public string? Name { get; set; }
		public string? AssemblyName { get; set; }
		public bool IsBuilding { get; set; }
		public bool NeedsBuild { get; set; }
		[Description( "Whether the last build succeeded. Null when it has never built." )]
		public bool? Success { get; set; }
		public string? Hint { get; set; }
	}

	public class SourceChangeList
	{
		public SourceChangeInfo[] Compilers { get; set; } = Array.Empty<SourceChangeInfo>();
		public string? Hint { get; set; }
	}

	public class SourceChangeInfo
	{
		public string? Slot { get; set; }
		public string? Name { get; set; }
		public int ChangeCount { get; set; }
		[Description( "The compiler's own change summary. Empty also means no baseline to diff against." )]
		public Dictionary<string, object>? Changes { get; set; }
	}

	public class DiagnosticList
	{
		public int Count { get; set; }
		public DiagnosticRow[] Diagnostics { get; set; } = Array.Empty<DiagnosticRow>();
		public string? Hint { get; set; }
	}

	public class DiagnosticRow
	{
		[Description( "The diagnostic id, like \"CS0246\"." )]
		public string? Id { get; set; }
		public string? Severity { get; set; }
		public string? Message { get; set; }
		public string? File { get; set; }
		public int? Line { get; set; }
	}

	public class AssemblyFreshness
	{
		public AssemblyFreshnessRow[] Assemblies { get; set; } = Array.Empty<AssemblyFreshnessRow>();
		[Description( "True when any assembly the process is serving is older than what was built." )]
		public bool AnyStale { get; set; }
	}

	public class AssemblyFreshnessRow
	{
		public string? Slot { get; set; }
		public string? Name { get; set; }
		public string? AssemblyName { get; set; }
		[Description( "The version the compiler last produced. Null when it has never built." )]
		public string? BuiltVersion { get; set; }
		[Description( "The newest version of that assembly loaded in the process." )]
		public string? LoadedVersion { get; set; }
		[Description( "How many copies are loaded. More than one is normal after hotloads." )]
		public int LoadedCopies { get; set; }
		public bool Stale { get; set; }
		public string? Hint { get; set; }
	}

	public class ContentPathResult
	{
		public int Count { get; set; }
		public int Found { get; set; }
		public int Missing { get; set; }
		public ContentPathRow[] Paths { get; set; } = Array.Empty<ContentPathRow>();
	}

	public class ContentPathRow
	{
		public required string Input { get; set; }
		[Description( "The path with the engine's _c suffix applied, which is what it actually opens." )]
		public string? Resolved { get; set; }
		[Description( "False means this loads the error model at runtime without throwing." )]
		public bool Exists { get; set; }
		[Description( "Which mounted package provides it." )]
		public string? Package { get; set; }
		public string? Hint { get; set; }
	}

	public class ContentSearchResult
	{
		public string? Directory { get; set; }
		public string? Pattern { get; set; }
		public int Count { get; set; }
		[Description( "How many matched before the limit was applied." )]
		public int Total { get; set; }
		[Description( "Paths relative to the directory searched." )]
		public string[] Files { get; set; } = Array.Empty<string>();
		public string? Hint { get; set; }
	}

	public class PackageReferenceReport
	{
		public PackageReferenceRow[] References { get; set; } = Array.Empty<PackageReferenceRow>();
		[Description( "Installed in the cloud cache but absent from the .sbproj, so gone at next editor start." )]
		public string[] InstalledNotReferenced { get; set; } = Array.Empty<string>();
		public string? Hint { get; set; }
	}

	public class PackageReferenceRow
	{
		public required string Ident { get; set; }
		public bool Installed { get; set; }
		public string? Title { get; set; }
	}

	public class ReloadConfigResult
	{
		public bool Reloaded { get; set; }
		[Description( "False means the reload changed nothing Roslyn cares about." )]
		public bool CompilersRecreated { get; set; }
		public CompileSettingsInfo? CompileSettings { get; set; }
		public string? Hint { get; set; }
	}

	public class ReloadSettingsResult
	{
		public bool Reloaded { get; set; }
		public int ActionsBefore { get; set; }
		[Description( "Unchanged after editing Input.config means the file did not parse. Check read_console." )]
		public int ActionsAfter { get; set; }
		public string? Hint { get; set; }
	}

	public class RebuildResult
	{
		public bool Rebuilding { get; set; }
		public string? Hint { get; set; }
	}

	public class BuildResult
	{
		public bool Success { get; set; }
		[Description( "The diagnostics that failed it. Null on success." )]
		public DiagnosticList? Errors { get; set; }
		public string? Hint { get; set; }
	}


	static class Engine
	{
		const BindingFlags Shared = BindingFlags.Static | BindingFlags.NonPublic;
		const BindingFlags Owned = BindingFlags.Instance | BindingFlags.NonPublic;

		public static Type Named( string fullName )
		{
			return typeof( Project ).Assembly.GetType( fullName )
				?? throw new Exception( $"{fullName} not found in Sandbox.Engine, engine API changed." );
		}

		public static object? CallShared( Type owner, string name, object?[]? args = null )
		{
			return Required( owner.GetMethod( name, Shared ), owner, name ).Invoke( null, args );
		}

		public static object? CallOwned( object target, string name, object?[]? args = null )
		{
			var owner = target.GetType();
			return Required( owner.GetMethod( name, Owned ), owner, name ).Invoke( target, args );
		}

		public static object? SharedField( Type owner, string name )
		{
			return Required( owner.GetField( name, Shared ), owner, name ).GetValue( null );
		}

		public static void SetOwned( object target, string name, object? value )
		{
			var owner = target.GetType();
			Required( owner.GetField( name, Owned ), owner, name ).SetValue( target, value );
		}

		public static object? Hidden( object target, string name )
		{
			var owner = target.GetType();
			return Required( owner.GetProperty( name, Owned ), owner, name ).GetValue( target );
		}

		public static object? Peek( object? target, string name )
		{
			return target?.GetType().GetProperty( name )?.GetValue( target );
		}

		public static object? Invoke( object? target, string name, object?[]? args = null )
		{
			return target?.GetType().GetMethod( name )?.Invoke( target, args );
		}

		static T Required<T>( T? member, Type owner, string name ) where T : MemberInfo
		{
			return member ?? throw new Exception( $"{owner.Name}.{name} not found, engine API changed." );
		}
	}
}
fobiat.sbox_mcp_server / Editor/SboxMcpServer.cs
Editor library
//  s&box MCP Server toolset : eighteen MCP tools for the s&box editor.
//
//  Ask the running engine what an API really is, ask the editor what it currently
//  believes, and make it notice a change on disk. Drop this file (and its
//  SboxMcpServer.Editor.cs partner) into a project's Editor/ folder; it registers
//  as "sbox_mcp_server".
//
//  Why each tool exists, which engine behaviours swallow an edit silently, and every
//  internal member reached by reflection: editor-mcp/README.md.
//
//  fobiat (Kyle Tarff) <kyle@fobiat.dev>  https://github.com/fobiat/sbox-skill
//  MIT, see LICENSE. Engine 26.08.05, compile-checked by editor-mcp/compilecheck.

#nullable enable

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Sandbox;

namespace Editor.Mcp;

/// <summary>Project, config, content and compiler tools for driving an s&amp;box project from outside the editor.</summary>
[McpToolset( "sbox_mcp_server", "Query the running engine for real type signatures, members, enum values, input actions and console commands, resolve content paths before they silently load the error model, read project, package and compiler state including assembly staleness, list compile errors, reload an externally edited .sbproj or ProjectSettings config, and rebuild from source on disk." )]
public static partial class SboxMcpServer
{
	public enum MemberKind
	{
		All,
		Methods,
		Properties,
		Fields,
	}


	/// <summary>Search the running engine for a type by name and report what it is. Ask this before writing an API you are not certain about.</summary>
	[McpTool.ReadOnly( "project_find_type" )]
	public static TypeSearch FindType(
		[Description( "Type name or fragment, case insensitive. For example \"SceneTrace\"." )] string name,
		[Description( "Maximum results." )] [Sandbox.Range( 1, 500 )] int limit = 20 )
	{
		if ( string.IsNullOrWhiteSpace( name ) )
			throw new Exception( "Pass a type name or fragment to search for." );

		var matches = KnownTypes()
			.Where( type => Matches( type.Name, name ) || Matches( type.FullName, name ) )
			.OrderBy( type => type.Name?.Length ?? int.MaxValue )
			.Take( limit )
			.Select( type => new FoundType
			{
				Name = type.Name,
				Namespace = type.Namespace,
				Kind = Shape( type ),
				BaseType = type.BaseType?.Name,
				Methods = type.Methods?.Length ?? 0,
				Properties = type.Properties?.Length ?? 0,
				Description = type.Description,
			} )
			.ToArray();

		return new TypeSearch
		{
			Count = matches.Length,
			Types = matches,
			Hint = matches.Length > 0
				? "Call project_type_members for the full signature list of one of these, or project_enum_values for an enum."
				: $"Nothing matches \"{name}\" in the loaded engine. Treat that as proof it does not exist rather than as a search that needs rewording.",
		};
	}

	/// <summary>List a type's methods and properties with real signatures, read from the running engine, and mark anything carrying [Obsolete]. This is the ground truth an API reference is only an approximation of, so prefer it whenever the two might disagree, and always for a type you are about to call something unfamiliar on.</summary>
	[McpTool.ReadOnly( "project_type_members" )]
	public static TypeMembers TypeMembersOf(
		[Description( "Exact type name, for example \"SceneTrace\"." )] string type,
		[Description( "Only members whose name contains this. Optional." )] string? filter = null,
		[Description( "Restrict the listing to one kind of member." )] MemberKind kind = MemberKind.All,
		[Description( "Maximum members of each kind." )] [Sandbox.Range( 1, 500 )] int limit = 60 )
	{
		var found = Resolve( type );

		bool Wanted( string memberName ) => string.IsNullOrWhiteSpace( filter ) || Matches( memberName, filter );

		var wantMethods = kind is MemberKind.All or MemberKind.Methods;
		var wantProperties = kind is MemberKind.All or MemberKind.Properties;

		return new TypeMembers
		{
			Type = found.FullName,
			Kind = Shape( found ),
			BaseType = found.BaseType?.FullName,

			Methods = !wantMethods ? Array.Empty<MethodRow>() : (found.Methods ?? Array.Empty<MethodDescription>())
				.Where( method => !method.IsSpecialName && Wanted( method.Name ) )
				.Take( limit )
				.Select( method => new MethodRow
				{
					Name = method.Name,
					Signature = Signature( method ),
					Static = method.IsStatic,
					Obsolete = Deprecation( method ),
					Description = method.Description,
				} )
				.ToArray(),

			Properties = !wantProperties ? Array.Empty<PropertyRow>() : (found.Properties ?? Array.Empty<PropertyDescription>())
				.Where( property => Wanted( property.Name ) )
				.Take( limit )
				.Select( property => new PropertyRow
				{
					Name = property.Name,
					Type = Readable( property.PropertyType ),
					Access = property.CanRead && property.CanWrite ? "get set" : property.CanRead ? "get" : "set",
					Static = property.IsStatic,
					Obsolete = Deprecation( property ),
					Description = property.Description,
				} )
				.ToArray(),

			Hint = found.IsEnum
				? "This is an enum, so it has no methods or properties worth listing. Call project_enum_values for its named values."
				: null,
		};
	}

	/// <summary>Search every loaded type for members whose name contains a fragment, and report which type each one is declared on. Reach for this when you know roughly what a method is called but not what it hangs off, which is the case project_type_members cannot answer because it needs the exact type name up front.</summary>
	[McpTool.ReadOnly( "project_find_member" )]
	public static MemberSearch FindMember(
		[Description( "Member name or fragment, case insensitive. For example \"RunTrace\"." )] string name,
		[Description( "Restrict the search to one kind of member." )] MemberKind kind = MemberKind.All,
		[Description( "Only members declared on types whose name contains this. Optional." )] string? type = null,
		[Description( "Maximum results." )] [Sandbox.Range( 1, 500 )] int limit = 30 )
	{
		if ( string.IsNullOrWhiteSpace( name ) )
			throw new Exception( "Pass a member name or fragment to search for." );

		bool WantedKind( MemberDescription member ) => kind switch
		{
			MemberKind.Methods => member.IsMethod,
			MemberKind.Properties => member.IsProperty,
			MemberKind.Fields => member.IsField,
			_ => member.IsMethod || member.IsProperty || member.IsField,
		};

		var matches = KnownTypes()
			.Where( owner => string.IsNullOrWhiteSpace( type ) || Matches( owner.Name, type ) )
			// DeclaredMembers, not Members: an inherited member would otherwise repeat once per subclass
			.SelectMany( owner => (owner.DeclaredMembers ?? Array.Empty<MemberDescription>())
				.Where( member => WantedKind( member ) && Matches( member.Name, name ) )
				.Select( member => new FoundMember
				{
					Type = owner.FullName,
					Name = member.Name,
					Kind = member.IsMethod ? "method" : member.IsProperty ? "property" : "field",
					Signature = member is MethodDescription method ? Signature( method ) : null,
					Static = member.IsStatic,
					Obsolete = Deprecation( member ),
					Description = member.Description,
				} ) )
			.OrderBy( member => member.Name.Length )
			.Take( limit )
			.ToArray();

		return new MemberSearch
		{
			Count = matches.Length,
			Members = matches,
			Hint = matches.Length > 0
				? "Call project_type_members on one of these types for its full signature list."
				: $"No member anywhere in the loaded engine contains \"{name}\". Treat that as proof it does not exist.",
		};
	}

	/// <summary>List an enum's named values with their numeric values. Enums have no methods or properties, so project_type_members reports one as empty, which reads as "the type does not exist" when it means "wrong tool".</summary>
	[McpTool.ReadOnly( "project_enum_values" )]
	public static EnumValues EnumValuesOf(
		[Description( "Exact enum name, for example \"HitboxTags\"." )] string type )
	{
		var found = Resolve( type );

		if ( !found.IsEnum )
			throw new Exception( $"\"{found.FullName}\" is a {Shape( found )}, not an enum. Call project_type_members for it instead." );

		var description = EditorTypeLibrary.GetEnumDescription( found.TargetType )
			?? throw new Exception( $"The engine has no enum description for \"{found.FullName}\", which should not happen. Check read_console." );

		var entries = description.Select( entry => new EnumEntry
		{
			Name = entry.Name,
			Value = entry.IntegerValue,
			Title = entry.Title,
			Group = entry.Group,
			Description = entry.Description,
		} ).ToArray();

		return new EnumValues
		{
			Type = found.FullName,
			Count = entries.Length,
			Values = entries,
		};
	}

	/// <summary>List the input actions this project defines, with their keyboard and gamepad bindings. Input actions are strings resolved at runtime, so Input.Down( "jump" ) on an action that does not exist compiles cleanly and silently never fires.</summary>
	[McpTool.ReadOnly( "project_input_actions" )]
	public static InputActionList InputActions()
	{
		var actions = Sandbox.Input.GetActions()?.ToArray() ?? Array.Empty<Sandbox.InputAction>();

		return new InputActionList
		{
			Count = actions.Length,
			Actions = actions.Select( action => new InputActionRow
			{
				Name = action.Name,
				Title = action.Title,
				Group = action.GroupName,
				Keyboard = action.KeyboardCode,
				Gamepad = action.GamepadCode.ToString(),
			} ).ToArray(),
		};
	}

	/// <summary>List the console commands and convars the engine currently knows, optionally only the ones this project's own assemblies registered. A console command given the wrong argument form prints its usage and changes nothing, which from the outside is indistinguishable from having run, so check the real name and shape here before driving anything through console_command.</summary>
	[McpTool.ReadOnly( "project_console_commands" )]
	public static ConsoleCommandList ConsoleCommands(
		[Description( "Only commands whose name contains this, case insensitive. Optional." )] string? filter = null,
		[Description( "Only commands registered by this project's own assemblies." )] bool projectOnly = false,
		[Description( "Maximum results." )] [Sandbox.Range( 1, 500 )] int limit = 50 )
	{
		var members = Engine.SharedField( Engine.Named( "Sandbox.ConVarSystem" ), "Members" ) as IDictionary
			?? throw new Exception( "ConVarSystem.Members is not a dictionary any more, engine API changed." );

		var owned = ProjectAssemblies();

		bool FromProject( object command ) =>
			owned.Any( assembly => Engine.Invoke( command, "IsFromAssembly", new object?[] { assembly } ) is true );

		var rows = new List<ConsoleCommandRow>();

		foreach ( var command in members.Values )
		{
			if ( command is null ) continue;

			var name = Engine.Peek( command, "Name" ) as string;
			if ( name is null || !(string.IsNullOrWhiteSpace( filter ) || Matches( name, filter )) ) continue;

			var mine = FromProject( command );
			if ( projectOnly && !mine ) continue;

			rows.Add( new ConsoleCommandRow
			{
				Name = name,
				Kind = Engine.Peek( command, "IsConCommand" ) is true ? "command" : "convar",
				Help = Engine.Peek( command, "Help" ) as string,
				Usage = Engine.Invoke( command, "BuildDescription" ) as string,
				IsAdmin = Engine.Peek( command, "IsAdmin" ) is true,
				IsServer = Engine.Peek( command, "IsServer" ) is true,
				IsCheat = Engine.Peek( command, "IsCheat" ) is true,
				FromProject = mine,
			} );
		}

		var page = rows.OrderBy( row => row.Name, StringComparer.OrdinalIgnoreCase ).Take( limit ).ToArray();

		return new ConsoleCommandList
		{
			Count = page.Length,
			Total = rows.Count,
			Commands = page,
			Hint = page.Length == 0 && projectOnly
				? "Nothing here came from this project. Either its assemblies have not loaded yet or nothing carries [ConVar]."
				: null,
		};
	}


	static IEnumerable<TypeDescription> KnownTypes()
	{
		var library = Sandbox.Internal.GlobalToolsNamespace.EditorTypeLibrary
			?? throw new Exception( "EditorTypeLibrary is not available yet. Wait for the editor to finish loading." );

		return library.GetTypes<object>() ?? Enumerable.Empty<TypeDescription>();
	}

	/// <summary>Resolve a type by simple or full name, preferring a top-level match and naming the alternatives rather than silently answering about the wrong type.</summary>
	static TypeDescription Resolve( string type )
	{
		var matches = KnownTypes()
			.Where( candidate => Same( candidate.Name, type ) || Same( candidate.FullName, type ) )
			.ToArray();

		if ( matches.Length == 0 )
			throw new Exception( $"No type named \"{type}\" in the loaded engine. Run project_find_type first." );

		if ( matches.Length == 1 )
			return matches[0];

		var exact = matches.Where( candidate => Same( candidate.FullName, type ) ).ToArray();
		if ( exact.Length == 1 ) return exact[0];

		// A nested type's FullName carries a '+'. Asking for "SyncFlags" means Sandbox.SyncFlags,
		// not Terrain's nested one, and picking the first match answered about the wrong type.
		var topLevel = matches.Where( candidate => candidate.FullName?.Contains( '+' ) != true ).ToArray();
		if ( topLevel.Length == 1 ) return topLevel[0];

		var names = string.Join( ", ", matches.Select( candidate => candidate.FullName ).Take( 6 ) );
		throw new Exception( $"\"{type}\" is ambiguous across {matches.Length} types: {names}. Pass a full name." );
	}

	static string Shape( TypeDescription type )
	{
		if ( type.IsEnum ) return "enum";
		if ( type.IsInterface ) return "interface";
		if ( type.IsStatic ) return "static class";
		if ( type.IsValueType ) return "struct";
		if ( type.IsAbstract ) return "abstract class";
		return "class";
	}

	static string Signature( MethodDescription method )
	{
		var arguments = method.Parameters.Select( p => $"{Readable( p.ParameterType )} {p.Name}" );
		return $"{Readable( method.ReturnType )} {method.Name}( {string.Join( ", ", arguments )} )";
	}

	static string Readable( Type? type )
	{
		if ( type is null ) return "void";
		if ( !type.IsGenericType ) return type.Name;

		var arguments = type.GetGenericArguments().Select( Readable );
		return $"{type.Name.Split( '`' )[0]}<{string.Join( ", ", arguments )}>";
	}

	static string? Deprecation( MemberDescription member )
	{
		var obsolete = member.GetCustomAttribute<ObsoleteAttribute>();
		if ( obsolete is null ) return null;

		return string.IsNullOrWhiteSpace( obsolete.Message ) ? "obsolete" : obsolete.Message;
	}


	static bool Matches( string? haystack, string? needle ) =>
		needle is not null && haystack?.Contains( needle, StringComparison.OrdinalIgnoreCase ) == true;

	static bool Same( string? a, string? b ) => string.Equals( a, b, StringComparison.OrdinalIgnoreCase );


	public class TypeSearch
	{
		public int Count { get; set; }
		public FoundType[] Types { get; set; } = Array.Empty<FoundType>();
		public string? Hint { get; set; }
	}

	public class FoundType
	{
		public string? Name { get; set; }
		public string? Namespace { get; set; }
		[Description( "class, struct, interface, enum, abstract class or static class." )]
		public string? Kind { get; set; }
		public string? BaseType { get; set; }
		public int Methods { get; set; }
		public int Properties { get; set; }
		public string? Description { get; set; }
	}

	public class TypeMembers
	{
		public string? Type { get; set; }
		public string? Kind { get; set; }
		public string? BaseType { get; set; }
		public MethodRow[] Methods { get; set; } = Array.Empty<MethodRow>();
		public PropertyRow[] Properties { get; set; } = Array.Empty<PropertyRow>();
		public string? Hint { get; set; }
	}

	public class MethodRow
	{
		public required string Name { get; set; }
		[Description( "The signature as C# would write it, return type first." )]
		public string? Signature { get; set; }
		public bool Static { get; set; }
		[Description( "The [Obsolete] message when the member is deprecated, otherwise null. Do not write new code against it." )]
		public string? Obsolete { get; set; }
		public string? Description { get; set; }
	}

	public class PropertyRow
	{
		public required string Name { get; set; }
		public string? Type { get; set; }
		[Description( "\"get\", \"set\" or \"get set\"." )]
		public string? Access { get; set; }
		public bool Static { get; set; }
		[Description( "The [Obsolete] message when the member is deprecated, otherwise null. Do not write new code against it." )]
		public string? Obsolete { get; set; }
		public string? Description { get; set; }
	}

	public class MemberSearch
	{
		public int Count { get; set; }
		public FoundMember[] Members { get; set; } = Array.Empty<FoundMember>();
		public string? Hint { get; set; }
	}

	public class FoundMember
	{
		[Description( "The type declaring this member. Pass it to project_type_members." )]
		public string? Type { get; set; }
		public required string Name { get; set; }
		[Description( "method, property or field." )]
		public string? Kind { get; set; }
		public string? Signature { get; set; }
		public bool Static { get; set; }
		public string? Obsolete { get; set; }
		public string? Description { get; set; }
	}

	public class EnumValues
	{
		public string? Type { get; set; }
		public int Count { get; set; }
		public EnumEntry[] Values { get; set; } = Array.Empty<EnumEntry>();
		public string? Hint { get; set; }
	}

	public class EnumEntry
	{
		[Description( "The name to write in code." )]
		public required string Name { get; set; }
		public long Value { get; set; }
		public string? Title { get; set; }
		public string? Group { get; set; }
		public string? Description { get; set; }
	}

	public class InputActionList
	{
		public int Count { get; set; }
		public InputActionRow[] Actions { get; set; } = Array.Empty<InputActionRow>();
	}

	public class InputActionRow
	{
		[Description( "The exact string Input.Down and friends expect." )]
		public required string Name { get; set; }
		public string? Title { get; set; }
		public string? Group { get; set; }
		public string? Keyboard { get; set; }
		public string? Gamepad { get; set; }
	}

	public class ConsoleCommandList
	{
		public int Count { get; set; }
		[Description( "How many matched before the limit was applied." )]
		public int Total { get; set; }
		public ConsoleCommandRow[] Commands { get; set; } = Array.Empty<ConsoleCommandRow>();
		public string? Hint { get; set; }
	}

	public class ConsoleCommandRow
	{
		public required string Name { get; set; }
		[Description( "command or convar. A convar takes a value, a command takes arguments." )]
		public string? Kind { get; set; }
		public string? Help { get; set; }
		[Description( "For a convar, its current and default value alongside the help text." )]
		public string? Usage { get; set; }
		public bool IsAdmin { get; set; }
		public bool IsServer { get; set; }
		public bool IsCheat { get; set; }
		[Description( "Registered by this project's own code rather than by the engine." )]
		public bool FromProject { get; set; }
	}
}
Debug: View Raw JSON Response
{
    "TotalCount": 2,
    "Files": [
        {
            "Ident": "fobiat.sbox_mcp_server",
            "Path": "Editor/SboxMcpServer.Editor.cs",
            "FileName": "SboxMcpServer.Editor.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 342130,
            "Code": "//  s&box MCP Server toolset, part two : what the editor believes, and making it notice\n//  a change on disk. See SboxMcpServer.cs for the header, licence and the engine-truth\n//  half of this same partial class.\n\n#nullable enable\n\nusing System;\nusing System.Collections;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Reflection;\nusing System.Threading.Tasks;\nusing Microsoft.CodeAnalysis;\nusing Sandbox;\n\nnamespace Editor.Mcp;\n\npublic static partial class SboxMcpServer\n{\n\tpublic enum CompilerSlot\n\t{\n\t\tBoth,\n\t\tGame,\n\t\tEditor,\n\t}\n\n\n\t/// <summary>Report which project the editor has open, where it sits on disk, and the compiler settings currently live in memory. Start here when an on-disk change is not taking effect: the settings reported are what Roslyn is actually using, which is not necessarily what the .sbproj on disk now says.</summary>\n\t[McpTool.ReadOnly( \"project_info\" )]\n\tpublic static ProjectInfo Info()\n\t{\n\t\tvar project = Open();\n\n\t\treturn new ProjectInfo\n\t\t{\n\t\t\tIdent = project.Config?.FullIdent,\n\t\t\tTitle = project.Config?.Title,\n\t\t\tType = project.Config?.Type,\n\t\t\tRootDirectory = project.RootDirectory?.FullName,\n\t\t\tConfigFilePath = project.ConfigFilePath,\n\t\t\tActive = project.Active,\n\t\t\tBroken = project.Broken,\n\t\t\tIsPublished = project.IsPublished,\n\t\t\tHasCompiler = project.HasCompiler,\n\t\t\tCompileSettings = LiveCompileSettings( project ),\n\t\t};\n\t}\n\n\t/// <summary>List the project's compilers with their build state. A compiler sitting at NeedsBuild true while IsBuilding is false has work queued that nothing has started, which is what a stalled build looks like from the outside.</summary>\n\t[McpTool.ReadOnly( \"project_compilers\" )]\n\tpublic static CompilerList Compilers(\n\t\t[Description( \"Which compiler to report on.\" )] CompilerSlot slot = CompilerSlot.Both )\n\t{\n\t\tvar project = Open();\n\n\t\treturn new CompilerList\n\t\t{\n\t\t\tCompilers = Slots( project, slot ).Select( Describe ).ToArray(),\n\t\t};\n\t}\n\n\t/// <summary>Ask each compiler what source changes it has actually noticed since its last build. This is the direct answer to \"did my edit register\", and it separates a file the compiler never saw from a file it saw and rejected.</summary>\n\t[McpTool.ReadOnly( \"project_source_changes\" )]\n\tpublic static SourceChangeList SourceChanges(\n\t\t[Description( \"Which compiler to report on.\" )] CompilerSlot slot = CompilerSlot.Both )\n\t{\n\t\tvar project = Open();\n\n\t\tvar compilers = Slots( project, slot ).Select( Noticed ).ToArray();\n\n\t\treturn new SourceChangeList\n\t\t{\n\t\t\tCompilers = compilers,\n\n\t\t\t// An empty summary is genuinely ambiguous: the engine also returns {} when there is no\n\t\t\t// previous syntax tree to diff against, which is every compiler on a cold editor.\n\t\t\tHint = compilers.Length > 0 && compilers.All( compiler => compiler.ChangeCount == 0 )\n\t\t\t\t? \"Every change set is empty. That means either nothing changed, or the compilers have no baseline to diff against yet because they have not built since the editor opened. Run project_build once, then ask again - a second empty answer is a real one.\"\n\t\t\t\t: null,\n\t\t};\n\t}\n\n\t/// <summary>Return current compile diagnostics as structured rows with file and line, so errors can be read without scraping read_console. Errors sort first.</summary>\n\t[McpTool.ReadOnly( \"project_compile_errors\" )]\n\tpublic static DiagnosticList CompileErrors(\n\t\t[Description( \"Include warnings alongside errors.\" )] bool includeWarnings = false,\n\t\t[Description( \"Maximum rows to return.\" )] [Sandbox.Range( 1, 500 )] int limit = 50 )\n\t{\n\t\tvar raw = Project.CompileGroup?.BuildResult.Diagnostics;\n\n\t\tvar floor = includeWarnings ? DiagnosticSeverity.Warning : DiagnosticSeverity.Error;\n\n\t\tvar rows = raw is null\n\t\t\t? Array.Empty<DiagnosticRow>()\n\t\t\t: raw.Where( diagnostic => diagnostic.Severity >= floor )\n\t\t\t\t.OrderByDescending( diagnostic => diagnostic.Severity )\n\t\t\t\t.Take( limit )\n\t\t\t\t.Select( Flatten )\n\t\t\t\t.ToArray();\n\n\t\treturn new DiagnosticList\n\t\t{\n\t\t\tCount = rows.Length,\n\t\t\tDiagnostics = rows,\n\t\t\tHint = rows.Length == 0 ? \"No diagnostics. If a source edit still is not live, run project_source_changes, then project_assembly_freshness.\" : null,\n\t\t};\n\t}\n\n\t/// <summary>Compare what each compiler last built against what the process has actually loaded. Recompiling does not always cure a stale assembly: the editor goes on serving the version it loaded, and compile_status reads green the whole time.</summary>\n\t[McpTool.ReadOnly( \"project_assembly_freshness\" )]\n\tpublic static AssemblyFreshness AssemblyFreshnessOf()\n\t{\n\t\tvar project = Open();\n\t\tvar loaded = AppDomain.CurrentDomain.GetAssemblies();\n\n\t\tvar rows = Slots( project, CompilerSlot.Both )\n\t\t\t.Select( slot =>\n\t\t\t{\n\t\t\t\tvar built = slot.Compiler.Output?.Version;\n\n\t\t\t\t// Hotloading leaves older copies behind under the same simple name, so the newest\n\t\t\t\t// one loaded is the one the process is serving\n\t\t\t\tvar copies = loaded\n\t\t\t\t\t.Select( assembly => assembly.GetName() )\n\t\t\t\t\t.Where( name => Same( name.Name, slot.Compiler.AssemblyName ) )\n\t\t\t\t\t.Select( name => name.Version )\n\t\t\t\t\t.Where( version => version is not null )\n\t\t\t\t\t.ToArray();\n\n\t\t\t\tvar newest = copies.Length == 0 ? null : copies.Max();\n\t\t\t\tvar stale = built is not null && newest is not null && built > newest;\n\n\t\t\t\treturn new AssemblyFreshnessRow\n\t\t\t\t{\n\t\t\t\t\tSlot = slot.Label,\n\t\t\t\t\tName = slot.Compiler.Name,\n\t\t\t\t\tAssemblyName = slot.Compiler.AssemblyName,\n\t\t\t\t\tBuiltVersion = built?.ToString(),\n\t\t\t\t\tLoadedVersion = newest?.ToString(),\n\t\t\t\t\tLoadedCopies = copies.Length,\n\t\t\t\t\tStale = stale,\n\t\t\t\t\tHint = built is null ? \"This compiler has never produced a build, so there is nothing to compare.\"\n\t\t\t\t\t\t: newest is null ? \"Nothing by that assembly name is loaded. The build has not been hotloaded into the process at all.\"\n\t\t\t\t\t\t: stale ? \"The process is running an older build than the compiler produced. Rebuilding will not fix this - close and reopen the editor.\"\n\t\t\t\t\t\t: null,\n\t\t\t\t};\n\t\t\t} )\n\t\t\t.ToArray();\n\n\t\treturn new AssemblyFreshness\n\t\t{\n\t\t\tAssemblies = rows,\n\t\t\tAnyStale = rows.Any( row => row.Stale ),\n\t\t};\n\t}\n\n\t/// <summary>Resolve one or more content paths against everything currently mounted, applying the same _c suffix rule the engine applies when it loads a resource. A path that resolves to nothing does not throw at runtime: Model.Load hands back the engine's error model, so a typo in a .item or a prefab compiles clean, passes every headless test and ships an orange world.</summary>\n\t[McpTool.ReadOnly( \"project_content_path\" )]\n\tpublic static ContentPathResult ContentPath(\n\t\t[Description( \"One content path, or several separated by commas. For example \\\"models/citizen/citizen.vmdl\\\".\" )] string paths )\n\t{\n\t\tvar wanted = (paths ?? \"\")\n\t\t\t.Split( ',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries )\n\t\t\t.ToArray();\n\n\t\tif ( wanted.Length == 0 )\n\t\t\tthrow new Exception( \"Pass at least one content path. Separate several with commas.\" );\n\n\t\tvar rows = wanted.Select( path =>\n\t\t{\n\t\t\t// ResourceLibrary.LoadGameResource does exactly this before it touches the filesystem\n\t\t\tvar compiled = path.EndsWith( \"_c\", StringComparison.Ordinal );\n\t\t\tvar resolved = compiled ? path : path + \"_c\";\n\t\t\tvar exists = FileSystem.Mounted?.FileExists( resolved ) ?? false;\n\n\t\t\treturn new ContentPathRow\n\t\t\t{\n\t\t\t\tInput = path,\n\t\t\t\tResolved = resolved,\n\t\t\t\tExists = exists,\n\t\t\t\t// The asset system indexes source paths, so it wants the name without the suffix\n\t\t\t\tPackage = exists ? AssetSystem.FindByPath( compiled ? path[..^2] : path )?.Package?.FullIdent : null,\n\t\t\t\tHint = exists ? null : \"Nothing mounted provides this. Run project_content_search on its parent directory to find the real spelling - mounted packages often disagree with their own CDN manifests about the path prefix.\",\n\t\t\t};\n\t\t} ).ToArray();\n\n\t\treturn new ContentPathResult\n\t\t{\n\t\t\tCount = rows.Length,\n\t\t\tFound = rows.Count( row => row.Exists ),\n\t\t\tMissing = rows.Count( row => !row.Exists ),\n\t\t\tPaths = rows,\n\t\t};\n\t}\n\n\t/// <summary>List files under a directory in the mounted content filesystem. This is the \"then what IS the right path\" companion to project_content_path: mounted packages routinely disagree with their own CDN manifests about the path prefix, and the manifest spelling fails at runtime with no symptom at all.</summary>\n\t[McpTool.ReadOnly( \"project_content_search\" )]\n\tpublic static ContentSearchResult ContentSearch(\n\t\t[Description( \"Directory to search, for example \\\"models/citizen\\\". Use \\\"/\\\" for everything.\" )] string directory = \"/\",\n\t\t[Description( \"Filename pattern, for example \\\"*.vmdl_c\\\". Compiled content carries the _c suffix.\" )] string pattern = \"*\",\n\t\t[Description( \"Recurse into subdirectories. A recursive search from \\\"/\\\" walks every mounted package and is slow.\" )] bool recursive = true,\n\t\t[Description( \"Maximum results.\" )] [Sandbox.Range( 1, 500 )] int limit = 50 )\n\t{\n\t\tvar mounted = FileSystem.Mounted\n\t\t\t?? throw new Exception( \"Nothing is mounted yet. Wait for the editor to finish loading the project.\" );\n\n\t\tvar found = mounted.FindFile( string.IsNullOrWhiteSpace( directory ) ? \"/\" : directory, pattern, recursive ).ToArray();\n\t\tvar page = found.Take( limit ).ToArray();\n\n\t\treturn new ContentSearchResult\n\t\t{\n\t\t\tDirectory = directory,\n\t\t\tPattern = pattern,\n\t\t\tCount = page.Length,\n\t\t\tTotal = found.Length,\n\t\t\tFiles = page,\n\t\t\tHint = found.Length > 0\n\t\t\t\t? \"These are relative to the directory searched. Pass a full path to project_content_path to confirm it resolves.\"\n\t\t\t\t: \"Nothing matched. Widen the pattern, or search the parent directory - the prefix is what usually differs from what a manifest says.\",\n\t\t};\n\t}\n\n\t/// <summary>Reconcile the package references written in the .sbproj against what is actually installed in the cloud cache. install_package mounts a package for this session and writes nothing to the project, so a package installed over MCP works perfectly until the editor restarts and then is simply gone.</summary>\n\t[McpTool.ReadOnly( \"project_package_references\" )]\n\tpublic static PackageReferenceReport PackageReferences()\n\t{\n\t\tvar project = Open();\n\t\tvar referenced = project.Config?.PackageReferences ?? new List<string>();\n\t\tvar installed = AssetSystem.GetInstalledPackages();\n\n\t\tvar rows = referenced.Select( ident => new PackageReferenceRow\n\t\t{\n\t\t\tIdent = ident,\n\t\t\tInstalled = AssetSystem.IsCloudInstalled( ident ),\n\t\t\tTitle = installed.FirstOrDefault( package => Same( package.FullIdent, ident ) )?.Title,\n\t\t} ).ToArray();\n\n\t\tvar extra = installed\n\t\t\t.Select( package => package.FullIdent )\n\t\t\t.Where( ident => ident is not null && !referenced.Any( reference => Same( reference, ident ) ) )\n\t\t\t.OrderBy( ident => ident, StringComparer.OrdinalIgnoreCase )\n\t\t\t.ToArray();\n\n\t\tvar missing = rows.Where( row => !row.Installed ).Select( row => row.Ident ).ToArray();\n\n\t\treturn new PackageReferenceReport\n\t\t{\n\t\t\tReferences = rows,\n\t\t\tInstalledNotReferenced = extra,\n\t\t\tHint = missing.Length > 0\n\t\t\t\t? $\"Referenced but not installed: {string.Join( \", \", missing )}. Anything they provide will resolve to nothing at runtime.\"\n\t\t\t\t: extra.Length > 0\n\t\t\t\t\t? \"Installed but not referenced. These are mounted for this session only - install_package writes nothing to the .sbproj, so they vanish at the next editor start. Add them to PackageReferences and run project_reload_config.\"\n\t\t\t\t\t: null,\n\t\t};\n\t}\n\n\n\t/// <summary>Re-read the project's .sbproj from disk into the live config and recreate its compilers, so an externally edited Metadata.Compiler block actually reaches Roslyn. Nothing watches that file, so without this an on-disk config change silently never takes effect.</summary>\n\t[McpTool( \"project_reload_config\" )]\n\tpublic static ReloadConfigResult ReloadConfig()\n\t{\n\t\tvar project = Open();\n\n\t\tvar wasGame = project.Compiler;\n\t\tvar wasEditor = project.EditorCompiler;\n\n\t\tif ( Engine.CallOwned( project, \"LoadMinimal\" ) is not true )\n\t\t\tthrow new Exception( \"Reloading the .sbproj failed, most likely a syntax error in it. Check read_console.\" );\n\n\t\t// UpdateCompiler early-returns when CompilerHash still matches lastCompilerHash, and that\n\t\t// hash covers only compile settings, org, ident, type, the standalone flag and package\n\t\t// references. Zeroing it is what stops an edit to anything else being a silent no-op.\n\t\tEngine.SetOwned( project, \"lastCompilerHash\", 0 );\n\t\tEngine.CallOwned( project, \"UpdateCompiler\" );\n\n\t\tvar recreated = (project.Compiler is { } game && !ReferenceEquals( wasGame, game ))\n\t\t\t|| (project.EditorCompiler is { } editor && !ReferenceEquals( wasEditor, editor ));\n\n\t\treturn new ReloadConfigResult\n\t\t{\n\t\t\tReloaded = true,\n\t\t\tCompilersRecreated = recreated,\n\t\t\tCompileSettings = LiveCompileSettings( project ),\n\t\t\tHint = recreated\n\t\t\t\t? \"Compilers were recreated, so their file watchers are fresh too. Run project_build to compile against the settings above.\"\n\t\t\t\t: project.HasCompiler\n\t\t\t\t\t? \"The config reloaded but no compiler was recreated. That happens when the project loaded a precompiled assembly instead of building one.\"\n\t\t\t\t\t: \"The config reloaded and the project now has no compiler at all. Check Active and the code path in project_info - a config edit can deactivate a project.\",\n\t\t};\n\t}\n\n\t/// <summary>Drop the cached ProjectSettings and push the reloaded input config back into the engine, so an externally edited Input.config, Platform.config or Collision.config actually takes effect. These are cached on first read and never invalidated, which is a separate trap from the .sbproj one.</summary>\n\t[McpTool( \"project_reload_settings\" )]\n\tpublic static ReloadSettingsResult ReloadSettings()\n\t{\n\t\tvar before = Sandbox.Input.GetActions()?.Count() ?? 0;\n\n\t\tEngine.CallShared( typeof( ProjectSettings ), \"ClearCache\" );\n\n\t\t// Clearing the cache alone changes nothing here: Input.GetActions reads a static field that\n\t\t// only Input.ReadConfig ever assigns, and nothing calls it on a settings reload.\n\t\tEngine.CallShared( typeof( Sandbox.Input ), \"ReadConfig\", new object?[] { ProjectSettings.Input } );\n\n\t\tvar after = Sandbox.Input.GetActions()?.Count() ?? 0;\n\n\t\treturn new ReloadSettingsResult\n\t\t{\n\t\t\tReloaded = true,\n\t\t\tActionsBefore = before,\n\t\t\tActionsAfter = after,\n\t\t\tHint = \"Every other config re-reads lazily on next access. Call project_input_actions to confirm Input.config parsed the way you meant it.\",\n\t\t};\n\t}\n\n\t/// <summary>Dispose and recreate every compiler, then start a build from the source on disk. Returns immediately.</summary>\n\t[McpTool( \"project_rebuild\" )]\n\tpublic static RebuildResult Rebuild()\n\t{\n\t\tRecreateCompilers();\n\n\t\treturn new RebuildResult\n\t\t{\n\t\t\tRebuilding = true,\n\t\t\tHint = \"Poll compile_status until IsBuilding is false, then call project_compile_errors.\",\n\t\t};\n\t}\n\n\t/// <summary>Build the project and wait for it to finish, then report success along with any errors. This is the one-shot version of project_rebuild followed by polling: reach for it when the question is simply whether the code compiles.</summary>\n\t[McpTool( \"project_build\" )]\n\tpublic static async Task<BuildResult> Build(\n\t\t[Description( \"Recreate compilers first, which also resets stale file watchers.\" )] bool rebuild = true )\n\t{\n\t\tif ( rebuild )\n\t\t\tRecreateCompilers();\n\n\t\tvar building = Engine.CallShared( typeof( Project ), \"CompileAsync\" ) as Task\n\t\t\t?? throw new Exception( \"Project.CompileAsync did not return a Task, engine API changed.\" );\n\n\t\tawait building;\n\n\t\tvar succeeded = Engine.Peek( building, \"Result\" ) is true;\n\n\t\treturn new BuildResult\n\t\t{\n\t\t\tSuccess = succeeded,\n\t\t\tErrors = succeeded ? null : CompileErrors(),\n\t\t\tHint = succeeded ? \"Built. If the change still is not live, run project_assembly_freshness.\" : null,\n\t\t};\n\t}\n\n\n\tstatic Project Open()\n\t{\n\t\treturn Project.Current ?? throw new Exception( \"No project is open in the editor.\" );\n\t}\n\n\tstatic void RecreateCompilers()\n\t{\n\t\tEngine.CallShared( typeof( Project ), \"RebuildCompilers\" );\n\t}\n\n\tstatic (string Label, Compiler Compiler)[] Slots( Project project, CompilerSlot slot )\n\t{\n\t\tvar found = new List<(string, Compiler)>();\n\n\t\tif ( slot is CompilerSlot.Both or CompilerSlot.Game && project.Compiler is { } game )\n\t\t\tfound.Add( (\"Game\", game) );\n\n\t\tif ( slot is CompilerSlot.Both or CompilerSlot.Editor && project.EditorCompiler is { } editor )\n\t\t\tfound.Add( (\"Editor\", editor) );\n\n\t\treturn found.ToArray();\n\t}\n\n\tstatic Assembly[] ProjectAssemblies()\n\t{\n\t\tvar project = Project.Current;\n\t\tif ( project is null ) return Array.Empty<Assembly>();\n\n\t\tvar names = Slots( project, CompilerSlot.Both ).Select( slot => slot.Compiler.AssemblyName ).ToArray();\n\n\t\treturn AppDomain.CurrentDomain.GetAssemblies()\n\t\t\t.Where( assembly => names.Any( name => Same( assembly.GetName().Name, name ) ) )\n\t\t\t.ToArray();\n\t}\n\n\tstatic CompileSettingsInfo? LiveCompileSettings( Project project )\n\t{\n\t\tif ( project.Config is null ) return null;\n\n\t\tvar settings = Engine.CallOwned( project.Config, \"GetCompileSettings\" );\n\t\tif ( settings is null ) return null;\n\n\t\treturn new CompileSettingsInfo\n\t\t{\n\t\t\tTreatWarningsAsErrors = Engine.Peek( settings, \"TreatWarningsAsErrors\" ) as bool?,\n\t\t\tNullables = Engine.Peek( settings, \"Nullables\" ) as bool?,\n\t\t\tRootNamespace = Engine.Peek( settings, \"RootNamespace\" ) as string,\n\t\t\tDefineConstants = Engine.Peek( settings, \"DefineConstants\" ) as string,\n\t\t\tNoWarn = Engine.Peek( settings, \"NoWarn\" ) as string,\n\t\t\tWarningsAsErrors = Engine.Peek( settings, \"WarningsAsErrors\" ) as string,\n\t\t};\n\t}\n\n\tstatic CompilerInfo Describe( (string Label, Compiler Compiler) slot )\n\t{\n\t\t// Compiler.BuildSuccess is Output?.Successful ?? false, which cannot tell a failed build\n\t\t// from one that has never run. Output itself can.\n\t\tvar success = slot.Compiler.Output?.Successful;\n\n\t\treturn new CompilerInfo\n\t\t{\n\t\t\tSlot = slot.Label,\n\t\t\tName = slot.Compiler.Name,\n\t\t\tAssemblyName = slot.Compiler.AssemblyName,\n\t\t\tIsBuilding = slot.Compiler.IsBuilding,\n\t\t\tNeedsBuild = slot.Compiler.NeedsBuild,\n\t\t\tSuccess = success,\n\t\t\tHint = success is null ? \"Never built. This is not a failure, it is an absence - run project_build.\" : null,\n\t\t};\n\t}\n\n\tstatic SourceChangeInfo Noticed( (string Label, Compiler Compiler) slot )\n\t{\n\t\tvar changes = Engine.Hidden( slot.Compiler, \"ChangeSummary\" ) as Dictionary<string, object>;\n\n\t\treturn new SourceChangeInfo\n\t\t{\n\t\t\tSlot = slot.Label,\n\t\t\tName = slot.Compiler.Name,\n\t\t\tChangeCount = changes?.Count ?? 0,\n\t\t\tChanges = changes,\n\t\t};\n\t}\n\n\n\tstatic DiagnosticRow Flatten( Diagnostic diagnostic )\n\t{\n\t\tvar span = diagnostic.Location.GetLineSpan();\n\t\tvar located = !string.IsNullOrEmpty( span.Path );\n\n\t\treturn new DiagnosticRow\n\t\t{\n\t\t\tId = diagnostic.Id,\n\t\t\tSeverity = diagnostic.Severity.ToString(),\n\t\t\tMessage = diagnostic.GetMessage(),\n\t\t\tFile = located ? span.Path : null,\n\t\t\tLine = located ? span.StartLinePosition.Line + 1 : null,\n\t\t};\n\t}\n\n\n\tpublic class ProjectInfo\n\t{\n\t\tpublic string? Ident { get; set; }\n\t\tpublic string? Title { get; set; }\n\t\tpublic string? Type { get; set; }\n\t\tpublic string? RootDirectory { get; set; }\n\t\tpublic string? ConfigFilePath { get; set; }\n\t\tpublic bool Active { get; set; }\n\t\tpublic bool Broken { get; set; }\n\t\tpublic bool IsPublished { get; set; }\n\t\tpublic bool HasCompiler { get; set; }\n\t\tpublic CompileSettingsInfo? CompileSettings { get; set; }\n\t}\n\n\tpublic class CompileSettingsInfo\n\t{\n\t\tpublic bool? TreatWarningsAsErrors { get; set; }\n\t\tpublic bool? Nullables { get; set; }\n\t\tpublic string? RootNamespace { get; set; }\n\t\tpublic string? DefineConstants { get; set; }\n\t\tpublic string? NoWarn { get; set; }\n\t\tpublic string? WarningsAsErrors { get; set; }\n\t}\n\n\tpublic class CompilerList\n\t{\n\t\tpublic CompilerInfo[] Compilers { get; set; } = Array.Empty<CompilerInfo>();\n\t}\n\n\tpublic class CompilerInfo\n\t{\n\t\t[Description( \"Game or Editor.\" )]\n\t\tpublic string? Slot { get; set; }\n\t\tpublic string? Name { get; set; }\n\t\tpublic string? AssemblyName { get; set; }\n\t\tpublic bool IsBuilding { get; set; }\n\t\tpublic bool NeedsBuild { get; set; }\n\t\t[Description( \"Whether the last build succeeded. Null when it has never built.\" )]\n\t\tpublic bool? Success { get; set; }\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class SourceChangeList\n\t{\n\t\tpublic SourceChangeInfo[] Compilers { get; set; } = Array.Empty<SourceChangeInfo>();\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class SourceChangeInfo\n\t{\n\t\tpublic string? Slot { get; set; }\n\t\tpublic string? Name { get; set; }\n\t\tpublic int ChangeCount { get; set; }\n\t\t[Description( \"The compiler's own change summary. Empty also means no baseline to diff against.\" )]\n\t\tpublic Dictionary<string, object>? Changes { get; set; }\n\t}\n\n\tpublic class DiagnosticList\n\t{\n\t\tpublic int Count { get; set; }\n\t\tpublic DiagnosticRow[] Diagnostics { get; set; } = Array.Empty<DiagnosticRow>();\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class DiagnosticRow\n\t{\n\t\t[Description( \"The diagnostic id, like \\\"CS0246\\\".\" )]\n\t\tpublic string? Id { get; set; }\n\t\tpublic string? Severity { get; set; }\n\t\tpublic string? Message { get; set; }\n\t\tpublic string? File { get; set; }\n\t\tpublic int? Line { get; set; }\n\t}\n\n\tpublic class AssemblyFreshness\n\t{\n\t\tpublic AssemblyFreshnessRow[] Assemblies { get; set; } = Array.Empty<AssemblyFreshnessRow>();\n\t\t[Description( \"True when any assembly the process is serving is older than what was built.\" )]\n\t\tpublic bool AnyStale { get; set; }\n\t}\n\n\tpublic class AssemblyFreshnessRow\n\t{\n\t\tpublic string? Slot { get; set; }\n\t\tpublic string? Name { get; set; }\n\t\tpublic string? AssemblyName { get; set; }\n\t\t[Description( \"The version the compiler last produced. Null when it has never built.\" )]\n\t\tpublic string? BuiltVersion { get; set; }\n\t\t[Description( \"The newest version of that assembly loaded in the process.\" )]\n\t\tpublic string? LoadedVersion { get; set; }\n\t\t[Description( \"How many copies are loaded. More than one is normal after hotloads.\" )]\n\t\tpublic int LoadedCopies { get; set; }\n\t\tpublic bool Stale { get; set; }\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class ContentPathResult\n\t{\n\t\tpublic int Count { get; set; }\n\t\tpublic int Found { get; set; }\n\t\tpublic int Missing { get; set; }\n\t\tpublic ContentPathRow[] Paths { get; set; } = Array.Empty<ContentPathRow>();\n\t}\n\n\tpublic class ContentPathRow\n\t{\n\t\tpublic required string Input { get; set; }\n\t\t[Description( \"The path with the engine's _c suffix applied, which is what it actually opens.\" )]\n\t\tpublic string? Resolved { get; set; }\n\t\t[Description( \"False means this loads the error model at runtime without throwing.\" )]\n\t\tpublic bool Exists { get; set; }\n\t\t[Description( \"Which mounted package provides it.\" )]\n\t\tpublic string? Package { get; set; }\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class ContentSearchResult\n\t{\n\t\tpublic string? Directory { get; set; }\n\t\tpublic string? Pattern { get; set; }\n\t\tpublic int Count { get; set; }\n\t\t[Description( \"How many matched before the limit was applied.\" )]\n\t\tpublic int Total { get; set; }\n\t\t[Description( \"Paths relative to the directory searched.\" )]\n\t\tpublic string[] Files { get; set; } = Array.Empty<string>();\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class PackageReferenceReport\n\t{\n\t\tpublic PackageReferenceRow[] References { get; set; } = Array.Empty<PackageReferenceRow>();\n\t\t[Description( \"Installed in the cloud cache but absent from the .sbproj, so gone at next editor start.\" )]\n\t\tpublic string[] InstalledNotReferenced { get; set; } = Array.Empty<string>();\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class PackageReferenceRow\n\t{\n\t\tpublic required string Ident { get; set; }\n\t\tpublic bool Installed { get; set; }\n\t\tpublic string? Title { get; set; }\n\t}\n\n\tpublic class ReloadConfigResult\n\t{\n\t\tpublic bool Reloaded { get; set; }\n\t\t[Description( \"False means the reload changed nothing Roslyn cares about.\" )]\n\t\tpublic bool CompilersRecreated { get; set; }\n\t\tpublic CompileSettingsInfo? CompileSettings { get; set; }\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class ReloadSettingsResult\n\t{\n\t\tpublic bool Reloaded { get; set; }\n\t\tpublic int ActionsBefore { get; set; }\n\t\t[Description( \"Unchanged after editing Input.config means the file did not parse. Check read_console.\" )]\n\t\tpublic int ActionsAfter { get; set; }\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class RebuildResult\n\t{\n\t\tpublic bool Rebuilding { get; set; }\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class BuildResult\n\t{\n\t\tpublic bool Success { get; set; }\n\t\t[Description( \"The diagnostics that failed it. Null on success.\" )]\n\t\tpublic DiagnosticList? Errors { get; set; }\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\n\tstatic class Engine\n\t{\n\t\tconst BindingFlags Shared = BindingFlags.Static | BindingFlags.NonPublic;\n\t\tconst BindingFlags Owned = BindingFlags.Instance | BindingFlags.NonPublic;\n\n\t\tpublic static Type Named( string fullName )\n\t\t{\n\t\t\treturn typeof( Project ).Assembly.GetType( fullName )\n\t\t\t\t?? throw new Exception( $\"{fullName} not found in Sandbox.Engine, engine API changed.\" );\n\t\t}\n\n\t\tpublic static object? CallShared( Type owner, string name, object?[]? args = null )\n\t\t{\n\t\t\treturn Required( owner.GetMethod( name, Shared ), owner, name ).Invoke( null, args );\n\t\t}\n\n\t\tpublic static object? CallOwned( object target, string name, object?[]? args = null )\n\t\t{\n\t\t\tvar owner = target.GetType();\n\t\t\treturn Required( owner.GetMethod( name, Owned ), owner, name ).Invoke( target, args );\n\t\t}\n\n\t\tpublic static object? SharedField( Type owner, string name )\n\t\t{\n\t\t\treturn Required( owner.GetField( name, Shared ), owner, name ).GetValue( null );\n\t\t}\n\n\t\tpublic static void SetOwned( object target, string name, object? value )\n\t\t{\n\t\t\tvar owner = target.GetType();\n\t\t\tRequired( owner.GetField( name, Owned ), owner, name ).SetValue( target, value );\n\t\t}\n\n\t\tpublic static object? Hidden( object target, string name )\n\t\t{\n\t\t\tvar owner = target.GetType();\n\t\t\treturn Required( owner.GetProperty( name, Owned ), owner, name ).GetValue( target );\n\t\t}\n\n\t\tpublic static object? Peek( object? target, string name )\n\t\t{\n\t\t\treturn target?.GetType().GetProperty( name )?.GetValue( target );\n\t\t}\n\n\t\tpublic static object? Invoke( object? target, string name, object?[]? args = null )\n\t\t{\n\t\t\treturn target?.GetType().GetMethod( name )?.Invoke( target, args );\n\t\t}\n\n\t\tstatic T Required<T>( T? member, Type owner, string name ) where T : MemberInfo\n\t\t{\n\t\t\treturn member ?? throw new Exception( $\"{owner.Name}.{name} not found, engine API changed.\" );\n\t\t}\n\t}\n}\n"
        },
        {
            "Ident": "fobiat.sbox_mcp_server",
            "Path": "Editor/SboxMcpServer.cs",
            "FileName": "SboxMcpServer.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 342130,
            "Code": "//  s&box MCP Server toolset : eighteen MCP tools for the s&box editor.\n//\n//  Ask the running engine what an API really is, ask the editor what it currently\n//  believes, and make it notice a change on disk. Drop this file (and its\n//  SboxMcpServer.Editor.cs partner) into a project's Editor/ folder; it registers\n//  as \"sbox_mcp_server\".\n//\n//  Why each tool exists, which engine behaviours swallow an edit silently, and every\n//  internal member reached by reflection: editor-mcp/README.md.\n//\n//  fobiat (Kyle Tarff) <kyle@fobiat.dev>  https://github.com/fobiat/sbox-skill\n//  MIT, see LICENSE. Engine 26.08.05, compile-checked by editor-mcp/compilecheck.\n\n#nullable enable\n\nusing System;\nusing System.Collections;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Reflection;\nusing System.Threading.Tasks;\nusing Microsoft.CodeAnalysis;\nusing Sandbox;\n\nnamespace Editor.Mcp;\n\n/// <summary>Project, config, content and compiler tools for driving an s&amp;box project from outside the editor.</summary>\n[McpToolset( \"sbox_mcp_server\", \"Query the running engine for real type signatures, members, enum values, input actions and console commands, resolve content paths before they silently load the error model, read project, package and compiler state including assembly staleness, list compile errors, reload an externally edited .sbproj or ProjectSettings config, and rebuild from source on disk.\" )]\npublic static partial class SboxMcpServer\n{\n\tpublic enum MemberKind\n\t{\n\t\tAll,\n\t\tMethods,\n\t\tProperties,\n\t\tFields,\n\t}\n\n\n\t/// <summary>Search the running engine for a type by name and report what it is. Ask this before writing an API you are not certain about.</summary>\n\t[McpTool.ReadOnly( \"project_find_type\" )]\n\tpublic static TypeSearch FindType(\n\t\t[Description( \"Type name or fragment, case insensitive. For example \\\"SceneTrace\\\".\" )] string name,\n\t\t[Description( \"Maximum results.\" )] [Sandbox.Range( 1, 500 )] int limit = 20 )\n\t{\n\t\tif ( string.IsNullOrWhiteSpace( name ) )\n\t\t\tthrow new Exception( \"Pass a type name or fragment to search for.\" );\n\n\t\tvar matches = KnownTypes()\n\t\t\t.Where( type => Matches( type.Name, name ) || Matches( type.FullName, name ) )\n\t\t\t.OrderBy( type => type.Name?.Length ?? int.MaxValue )\n\t\t\t.Take( limit )\n\t\t\t.Select( type => new FoundType\n\t\t\t{\n\t\t\t\tName = type.Name,\n\t\t\t\tNamespace = type.Namespace,\n\t\t\t\tKind = Shape( type ),\n\t\t\t\tBaseType = type.BaseType?.Name,\n\t\t\t\tMethods = type.Methods?.Length ?? 0,\n\t\t\t\tProperties = type.Properties?.Length ?? 0,\n\t\t\t\tDescription = type.Description,\n\t\t\t} )\n\t\t\t.ToArray();\n\n\t\treturn new TypeSearch\n\t\t{\n\t\t\tCount = matches.Length,\n\t\t\tTypes = matches,\n\t\t\tHint = matches.Length > 0\n\t\t\t\t? \"Call project_type_members for the full signature list of one of these, or project_enum_values for an enum.\"\n\t\t\t\t: $\"Nothing matches \\\"{name}\\\" in the loaded engine. Treat that as proof it does not exist rather than as a search that needs rewording.\",\n\t\t};\n\t}\n\n\t/// <summary>List a type's methods and properties with real signatures, read from the running engine, and mark anything carrying [Obsolete]. This is the ground truth an API reference is only an approximation of, so prefer it whenever the two might disagree, and always for a type you are about to call something unfamiliar on.</summary>\n\t[McpTool.ReadOnly( \"project_type_members\" )]\n\tpublic static TypeMembers TypeMembersOf(\n\t\t[Description( \"Exact type name, for example \\\"SceneTrace\\\".\" )] string type,\n\t\t[Description( \"Only members whose name contains this. Optional.\" )] string? filter = null,\n\t\t[Description( \"Restrict the listing to one kind of member.\" )] MemberKind kind = MemberKind.All,\n\t\t[Description( \"Maximum members of each kind.\" )] [Sandbox.Range( 1, 500 )] int limit = 60 )\n\t{\n\t\tvar found = Resolve( type );\n\n\t\tbool Wanted( string memberName ) => string.IsNullOrWhiteSpace( filter ) || Matches( memberName, filter );\n\n\t\tvar wantMethods = kind is MemberKind.All or MemberKind.Methods;\n\t\tvar wantProperties = kind is MemberKind.All or MemberKind.Properties;\n\n\t\treturn new TypeMembers\n\t\t{\n\t\t\tType = found.FullName,\n\t\t\tKind = Shape( found ),\n\t\t\tBaseType = found.BaseType?.FullName,\n\n\t\t\tMethods = !wantMethods ? Array.Empty<MethodRow>() : (found.Methods ?? Array.Empty<MethodDescription>())\n\t\t\t\t.Where( method => !method.IsSpecialName && Wanted( method.Name ) )\n\t\t\t\t.Take( limit )\n\t\t\t\t.Select( method => new MethodRow\n\t\t\t\t{\n\t\t\t\t\tName = method.Name,\n\t\t\t\t\tSignature = Signature( method ),\n\t\t\t\t\tStatic = method.IsStatic,\n\t\t\t\t\tObsolete = Deprecation( method ),\n\t\t\t\t\tDescription = method.Description,\n\t\t\t\t} )\n\t\t\t\t.ToArray(),\n\n\t\t\tProperties = !wantProperties ? Array.Empty<PropertyRow>() : (found.Properties ?? Array.Empty<PropertyDescription>())\n\t\t\t\t.Where( property => Wanted( property.Name ) )\n\t\t\t\t.Take( limit )\n\t\t\t\t.Select( property => new PropertyRow\n\t\t\t\t{\n\t\t\t\t\tName = property.Name,\n\t\t\t\t\tType = Readable( property.PropertyType ),\n\t\t\t\t\tAccess = property.CanRead && property.CanWrite ? \"get set\" : property.CanRead ? \"get\" : \"set\",\n\t\t\t\t\tStatic = property.IsStatic,\n\t\t\t\t\tObsolete = Deprecation( property ),\n\t\t\t\t\tDescription = property.Description,\n\t\t\t\t} )\n\t\t\t\t.ToArray(),\n\n\t\t\tHint = found.IsEnum\n\t\t\t\t? \"This is an enum, so it has no methods or properties worth listing. Call project_enum_values for its named values.\"\n\t\t\t\t: null,\n\t\t};\n\t}\n\n\t/// <summary>Search every loaded type for members whose name contains a fragment, and report which type each one is declared on. Reach for this when you know roughly what a method is called but not what it hangs off, which is the case project_type_members cannot answer because it needs the exact type name up front.</summary>\n\t[McpTool.ReadOnly( \"project_find_member\" )]\n\tpublic static MemberSearch FindMember(\n\t\t[Description( \"Member name or fragment, case insensitive. For example \\\"RunTrace\\\".\" )] string name,\n\t\t[Description( \"Restrict the search to one kind of member.\" )] MemberKind kind = MemberKind.All,\n\t\t[Description( \"Only members declared on types whose name contains this. Optional.\" )] string? type = null,\n\t\t[Description( \"Maximum results.\" )] [Sandbox.Range( 1, 500 )] int limit = 30 )\n\t{\n\t\tif ( string.IsNullOrWhiteSpace( name ) )\n\t\t\tthrow new Exception( \"Pass a member name or fragment to search for.\" );\n\n\t\tbool WantedKind( MemberDescription member ) => kind switch\n\t\t{\n\t\t\tMemberKind.Methods => member.IsMethod,\n\t\t\tMemberKind.Properties => member.IsProperty,\n\t\t\tMemberKind.Fields => member.IsField,\n\t\t\t_ => member.IsMethod || member.IsProperty || member.IsField,\n\t\t};\n\n\t\tvar matches = KnownTypes()\n\t\t\t.Where( owner => string.IsNullOrWhiteSpace( type ) || Matches( owner.Name, type ) )\n\t\t\t// DeclaredMembers, not Members: an inherited member would otherwise repeat once per subclass\n\t\t\t.SelectMany( owner => (owner.DeclaredMembers ?? Array.Empty<MemberDescription>())\n\t\t\t\t.Where( member => WantedKind( member ) && Matches( member.Name, name ) )\n\t\t\t\t.Select( member => new FoundMember\n\t\t\t\t{\n\t\t\t\t\tType = owner.FullName,\n\t\t\t\t\tName = member.Name,\n\t\t\t\t\tKind = member.IsMethod ? \"method\" : member.IsProperty ? \"property\" : \"field\",\n\t\t\t\t\tSignature = member is MethodDescription method ? Signature( method ) : null,\n\t\t\t\t\tStatic = member.IsStatic,\n\t\t\t\t\tObsolete = Deprecation( member ),\n\t\t\t\t\tDescription = member.Description,\n\t\t\t\t} ) )\n\t\t\t.OrderBy( member => member.Name.Length )\n\t\t\t.Take( limit )\n\t\t\t.ToArray();\n\n\t\treturn new MemberSearch\n\t\t{\n\t\t\tCount = matches.Length,\n\t\t\tMembers = matches,\n\t\t\tHint = matches.Length > 0\n\t\t\t\t? \"Call project_type_members on one of these types for its full signature list.\"\n\t\t\t\t: $\"No member anywhere in the loaded engine contains \\\"{name}\\\". Treat that as proof it does not exist.\",\n\t\t};\n\t}\n\n\t/// <summary>List an enum's named values with their numeric values. Enums have no methods or properties, so project_type_members reports one as empty, which reads as \"the type does not exist\" when it means \"wrong tool\".</summary>\n\t[McpTool.ReadOnly( \"project_enum_values\" )]\n\tpublic static EnumValues EnumValuesOf(\n\t\t[Description( \"Exact enum name, for example \\\"HitboxTags\\\".\" )] string type )\n\t{\n\t\tvar found = Resolve( type );\n\n\t\tif ( !found.IsEnum )\n\t\t\tthrow new Exception( $\"\\\"{found.FullName}\\\" is a {Shape( found )}, not an enum. Call project_type_members for it instead.\" );\n\n\t\tvar description = EditorTypeLibrary.GetEnumDescription( found.TargetType )\n\t\t\t?? throw new Exception( $\"The engine has no enum description for \\\"{found.FullName}\\\", which should not happen. Check read_console.\" );\n\n\t\tvar entries = description.Select( entry => new EnumEntry\n\t\t{\n\t\t\tName = entry.Name,\n\t\t\tValue = entry.IntegerValue,\n\t\t\tTitle = entry.Title,\n\t\t\tGroup = entry.Group,\n\t\t\tDescription = entry.Description,\n\t\t} ).ToArray();\n\n\t\treturn new EnumValues\n\t\t{\n\t\t\tType = found.FullName,\n\t\t\tCount = entries.Length,\n\t\t\tValues = entries,\n\t\t};\n\t}\n\n\t/// <summary>List the input actions this project defines, with their keyboard and gamepad bindings. Input actions are strings resolved at runtime, so Input.Down( \"jump\" ) on an action that does not exist compiles cleanly and silently never fires.</summary>\n\t[McpTool.ReadOnly( \"project_input_actions\" )]\n\tpublic static InputActionList InputActions()\n\t{\n\t\tvar actions = Sandbox.Input.GetActions()?.ToArray() ?? Array.Empty<Sandbox.InputAction>();\n\n\t\treturn new InputActionList\n\t\t{\n\t\t\tCount = actions.Length,\n\t\t\tActions = actions.Select( action => new InputActionRow\n\t\t\t{\n\t\t\t\tName = action.Name,\n\t\t\t\tTitle = action.Title,\n\t\t\t\tGroup = action.GroupName,\n\t\t\t\tKeyboard = action.KeyboardCode,\n\t\t\t\tGamepad = action.GamepadCode.ToString(),\n\t\t\t} ).ToArray(),\n\t\t};\n\t}\n\n\t/// <summary>List the console commands and convars the engine currently knows, optionally only the ones this project's own assemblies registered. A console command given the wrong argument form prints its usage and changes nothing, which from the outside is indistinguishable from having run, so check the real name and shape here before driving anything through console_command.</summary>\n\t[McpTool.ReadOnly( \"project_console_commands\" )]\n\tpublic static ConsoleCommandList ConsoleCommands(\n\t\t[Description( \"Only commands whose name contains this, case insensitive. Optional.\" )] string? filter = null,\n\t\t[Description( \"Only commands registered by this project's own assemblies.\" )] bool projectOnly = false,\n\t\t[Description( \"Maximum results.\" )] [Sandbox.Range( 1, 500 )] int limit = 50 )\n\t{\n\t\tvar members = Engine.SharedField( Engine.Named( \"Sandbox.ConVarSystem\" ), \"Members\" ) as IDictionary\n\t\t\t?? throw new Exception( \"ConVarSystem.Members is not a dictionary any more, engine API changed.\" );\n\n\t\tvar owned = ProjectAssemblies();\n\n\t\tbool FromProject( object command ) =>\n\t\t\towned.Any( assembly => Engine.Invoke( command, \"IsFromAssembly\", new object?[] { assembly } ) is true );\n\n\t\tvar rows = new List<ConsoleCommandRow>();\n\n\t\tforeach ( var command in members.Values )\n\t\t{\n\t\t\tif ( command is null ) continue;\n\n\t\t\tvar name = Engine.Peek( command, \"Name\" ) as string;\n\t\t\tif ( name is null || !(string.IsNullOrWhiteSpace( filter ) || Matches( name, filter )) ) continue;\n\n\t\t\tvar mine = FromProject( command );\n\t\t\tif ( projectOnly && !mine ) continue;\n\n\t\t\trows.Add( new ConsoleCommandRow\n\t\t\t{\n\t\t\t\tName = name,\n\t\t\t\tKind = Engine.Peek( command, \"IsConCommand\" ) is true ? \"command\" : \"convar\",\n\t\t\t\tHelp = Engine.Peek( command, \"Help\" ) as string,\n\t\t\t\tUsage = Engine.Invoke( command, \"BuildDescription\" ) as string,\n\t\t\t\tIsAdmin = Engine.Peek( command, \"IsAdmin\" ) is true,\n\t\t\t\tIsServer = Engine.Peek( command, \"IsServer\" ) is true,\n\t\t\t\tIsCheat = Engine.Peek( command, \"IsCheat\" ) is true,\n\t\t\t\tFromProject = mine,\n\t\t\t} );\n\t\t}\n\n\t\tvar page = rows.OrderBy( row => row.Name, StringComparer.OrdinalIgnoreCase ).Take( limit ).ToArray();\n\n\t\treturn new ConsoleCommandList\n\t\t{\n\t\t\tCount = page.Length,\n\t\t\tTotal = rows.Count,\n\t\t\tCommands = page,\n\t\t\tHint = page.Length == 0 && projectOnly\n\t\t\t\t? \"Nothing here came from this project. Either its assemblies have not loaded yet or nothing carries [ConVar].\"\n\t\t\t\t: null,\n\t\t};\n\t}\n\n\n\tstatic IEnumerable<TypeDescription> KnownTypes()\n\t{\n\t\tvar library = Sandbox.Internal.GlobalToolsNamespace.EditorTypeLibrary\n\t\t\t?? throw new Exception( \"EditorTypeLibrary is not available yet. Wait for the editor to finish loading.\" );\n\n\t\treturn library.GetTypes<object>() ?? Enumerable.Empty<TypeDescription>();\n\t}\n\n\t/// <summary>Resolve a type by simple or full name, preferring a top-level match and naming the alternatives rather than silently answering about the wrong type.</summary>\n\tstatic TypeDescription Resolve( string type )\n\t{\n\t\tvar matches = KnownTypes()\n\t\t\t.Where( candidate => Same( candidate.Name, type ) || Same( candidate.FullName, type ) )\n\t\t\t.ToArray();\n\n\t\tif ( matches.Length == 0 )\n\t\t\tthrow new Exception( $\"No type named \\\"{type}\\\" in the loaded engine. Run project_find_type first.\" );\n\n\t\tif ( matches.Length == 1 )\n\t\t\treturn matches[0];\n\n\t\tvar exact = matches.Where( candidate => Same( candidate.FullName, type ) ).ToArray();\n\t\tif ( exact.Length == 1 ) return exact[0];\n\n\t\t// A nested type's FullName carries a '+'. Asking for \"SyncFlags\" means Sandbox.SyncFlags,\n\t\t// not Terrain's nested one, and picking the first match answered about the wrong type.\n\t\tvar topLevel = matches.Where( candidate => candidate.FullName?.Contains( '+' ) != true ).ToArray();\n\t\tif ( topLevel.Length == 1 ) return topLevel[0];\n\n\t\tvar names = string.Join( \", \", matches.Select( candidate => candidate.FullName ).Take( 6 ) );\n\t\tthrow new Exception( $\"\\\"{type}\\\" is ambiguous across {matches.Length} types: {names}. Pass a full name.\" );\n\t}\n\n\tstatic string Shape( TypeDescription type )\n\t{\n\t\tif ( type.IsEnum ) return \"enum\";\n\t\tif ( type.IsInterface ) return \"interface\";\n\t\tif ( type.IsStatic ) return \"static class\";\n\t\tif ( type.IsValueType ) return \"struct\";\n\t\tif ( type.IsAbstract ) return \"abstract class\";\n\t\treturn \"class\";\n\t}\n\n\tstatic string Signature( MethodDescription method )\n\t{\n\t\tvar arguments = method.Parameters.Select( p => $\"{Readable( p.ParameterType )} {p.Name}\" );\n\t\treturn $\"{Readable( method.ReturnType )} {method.Name}( {string.Join( \", \", arguments )} )\";\n\t}\n\n\tstatic string Readable( Type? type )\n\t{\n\t\tif ( type is null ) return \"void\";\n\t\tif ( !type.IsGenericType ) return type.Name;\n\n\t\tvar arguments = type.GetGenericArguments().Select( Readable );\n\t\treturn $\"{type.Name.Split( '`' )[0]}<{string.Join( \", \", arguments )}>\";\n\t}\n\n\tstatic string? Deprecation( MemberDescription member )\n\t{\n\t\tvar obsolete = member.GetCustomAttribute<ObsoleteAttribute>();\n\t\tif ( obsolete is null ) return null;\n\n\t\treturn string.IsNullOrWhiteSpace( obsolete.Message ) ? \"obsolete\" : obsolete.Message;\n\t}\n\n\n\tstatic bool Matches( string? haystack, string? needle ) =>\n\t\tneedle is not null && haystack?.Contains( needle, StringComparison.OrdinalIgnoreCase ) == true;\n\n\tstatic bool Same( string? a, string? b ) => string.Equals( a, b, StringComparison.OrdinalIgnoreCase );\n\n\n\tpublic class TypeSearch\n\t{\n\t\tpublic int Count { get; set; }\n\t\tpublic FoundType[] Types { get; set; } = Array.Empty<FoundType>();\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class FoundType\n\t{\n\t\tpublic string? Name { get; set; }\n\t\tpublic string? Namespace { get; set; }\n\t\t[Description( \"class, struct, interface, enum, abstract class or static class.\" )]\n\t\tpublic string? Kind { get; set; }\n\t\tpublic string? BaseType { get; set; }\n\t\tpublic int Methods { get; set; }\n\t\tpublic int Properties { get; set; }\n\t\tpublic string? Description { get; set; }\n\t}\n\n\tpublic class TypeMembers\n\t{\n\t\tpublic string? Type { get; set; }\n\t\tpublic string? Kind { get; set; }\n\t\tpublic string? BaseType { get; set; }\n\t\tpublic MethodRow[] Methods { get; set; } = Array.Empty<MethodRow>();\n\t\tpublic PropertyRow[] Properties { get; set; } = Array.Empty<PropertyRow>();\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class MethodRow\n\t{\n\t\tpublic required string Name { get; set; }\n\t\t[Description( \"The signature as C# would write it, return type first.\" )]\n\t\tpublic string? Signature { get; set; }\n\t\tpublic bool Static { get; set; }\n\t\t[Description( \"The [Obsolete] message when the member is deprecated, otherwise null. Do not write new code against it.\" )]\n\t\tpublic string? Obsolete { get; set; }\n\t\tpublic string? Description { get; set; }\n\t}\n\n\tpublic class PropertyRow\n\t{\n\t\tpublic required string Name { get; set; }\n\t\tpublic string? Type { get; set; }\n\t\t[Description( \"\\\"get\\\", \\\"set\\\" or \\\"get set\\\".\" )]\n\t\tpublic string? Access { get; set; }\n\t\tpublic bool Static { get; set; }\n\t\t[Description( \"The [Obsolete] message when the member is deprecated, otherwise null. Do not write new code against it.\" )]\n\t\tpublic string? Obsolete { get; set; }\n\t\tpublic string? Description { get; set; }\n\t}\n\n\tpublic class MemberSearch\n\t{\n\t\tpublic int Count { get; set; }\n\t\tpublic FoundMember[] Members { get; set; } = Array.Empty<FoundMember>();\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class FoundMember\n\t{\n\t\t[Description( \"The type declaring this member. Pass it to project_type_members.\" )]\n\t\tpublic string? Type { get; set; }\n\t\tpublic required string Name { get; set; }\n\t\t[Description( \"method, property or field.\" )]\n\t\tpublic string? Kind { get; set; }\n\t\tpublic string? Signature { get; set; }\n\t\tpublic bool Static { get; set; }\n\t\tpublic string? Obsolete { get; set; }\n\t\tpublic string? Description { get; set; }\n\t}\n\n\tpublic class EnumValues\n\t{\n\t\tpublic string? Type { get; set; }\n\t\tpublic int Count { get; set; }\n\t\tpublic EnumEntry[] Values { get; set; } = Array.Empty<EnumEntry>();\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class EnumEntry\n\t{\n\t\t[Description( \"The name to write in code.\" )]\n\t\tpublic required string Name { get; set; }\n\t\tpublic long Value { get; set; }\n\t\tpublic string? Title { get; set; }\n\t\tpublic string? Group { get; set; }\n\t\tpublic string? Description { get; set; }\n\t}\n\n\tpublic class InputActionList\n\t{\n\t\tpublic int Count { get; set; }\n\t\tpublic InputActionRow[] Actions { get; set; } = Array.Empty<InputActionRow>();\n\t}\n\n\tpublic class InputActionRow\n\t{\n\t\t[Description( \"The exact string Input.Down and friends expect.\" )]\n\t\tpublic required string Name { get; set; }\n\t\tpublic string? Title { get; set; }\n\t\tpublic string? Group { get; set; }\n\t\tpublic string? Keyboard { get; set; }\n\t\tpublic string? Gamepad { get; set; }\n\t}\n\n\tpublic class ConsoleCommandList\n\t{\n\t\tpublic int Count { get; set; }\n\t\t[Description( \"How many matched before the limit was applied.\" )]\n\t\tpublic int Total { get; set; }\n\t\tpublic ConsoleCommandRow[] Commands { get; set; } = Array.Empty<ConsoleCommandRow>();\n\t\tpublic string? Hint { get; set; }\n\t}\n\n\tpublic class ConsoleCommandRow\n\t{\n\t\tpublic required string Name { get; set; }\n\t\t[Description( \"command or convar. A convar takes a value, a command takes arguments.\" )]\n\t\tpublic string? Kind { get; set; }\n\t\tpublic string? Help { get; set; }\n\t\t[Description( \"For a convar, its current and default value alongside the help text.\" )]\n\t\tpublic string? Usage { get; set; }\n\t\tpublic bool IsAdmin { get; set; }\n\t\tpublic bool IsServer { get; set; }\n\t\tpublic bool IsCheat { get; set; }\n\t\t[Description( \"Registered by this project's own code rather than by the engine.\" )]\n\t\tpublic bool FromProject { get; set; }\n\t}\n}\n"
        }
    ]
}