Editor/Tools/CodeAwarenessTools.cs
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using Sandbox;
namespace Editor.Mcp
{
public static partial class SboxMcpAssistant
{
[McpTool("pocketknife_find_type")]
public static string FindType([Description("Type name or fragment")] string name, [Description("Maximum results")] int limit = 20)
{
try
{
var items = AppDomain.CurrentDomain.GetAssemblies().SelectMany(SafeTypes).Where(t => t.Name.Contains(name ?? "", StringComparison.OrdinalIgnoreCase) || (t.FullName?.Contains(name ?? "", StringComparison.OrdinalIgnoreCase) ?? false)).Take(Math.Clamp(limit, 1, 100)).Select(t => new { FullName = t.FullName, Name = t.Name, Kind = t.IsEnum ? "Enum" : t.IsClass ? "Class" : t.IsValueType ? "Struct" : "Interface", Base = t.BaseType?.FullName }).ToArray();
return McpJson.Write(ToolOutcome<object>.Ready(new { Query = name, Count = items.Length, Items = items }));
}
catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "type.search_failed", "Type search failed.", ex)); }
}
[McpTool("pocketknife_type_members")]
public static string TypeMembers([Description("Exact or short type name")] string typeName)
{
try
{
var type = AppDomain.CurrentDomain.GetAssemblies().SelectMany(SafeTypes).FirstOrDefault(t => t.FullName == typeName || t.Name == typeName); if (type is null) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Degraded, "type.not_found", $"Type '{typeName}' was not found."));
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static).Take(200).Select(p => new { p.Name, Type = p.PropertyType.FullName, CanRead = p.CanRead, CanWrite = p.CanWrite }); var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static).Where(m => !m.IsSpecialName).Take(300).Select(m => new { m.Name, ReturnType = m.ReturnType.FullName, Parameters = m.GetParameters().Select(p => new { p.Name, Type = p.ParameterType.FullName }).ToArray() });
return McpJson.Write(ToolOutcome<object>.Ready(new { Type = type.FullName, Properties = properties, Methods = methods }));
}
catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "type.members_failed", "Type member inspection failed.", ex)); }
}
[McpTool("pocketknife_compile_await")]
public static string CompileAwait(string scope = "project", int timeoutMs = 120000)
{
var normalizedScope = (scope ?? "").Trim().ToLowerInvariant();
if (normalizedScope != "project")
return McpJson.Write(ToolOutcome<object>.Failure(new { Scope = scope }, ToolStatus.Failed, "compile.invalid_scope", "Only project compile scope is supported."));
var boundedTimeout = Math.Clamp(timeoutMs, 1000, 120000);
var started = DateTime.UtcNow;
var baselineState = EditorRuntime.InvokeStatic("Editor.Mcp.EditorTools", "CompileStatus");
var baselineRevision = CompilerRevision(baselineState);
CompilerIsSettled(baselineState, out var baselineDiagnostics);
// Official MCP owns compile dispatch; this method only waits for compiler readback.
object? observedState = null;
List<string> observedDiagnostics = new();
var deadline = started.AddMilliseconds(boundedTimeout);
var settled = false;
while (DateTime.UtcNow < deadline)
{
observedState = EditorRuntime.InvokeStatic("Editor.Mcp.EditorTools", "CompileStatus");
settled = CompilerIsSettled(observedState, out observedDiagnostics);
if (settled) break;
System.Threading.Thread.Sleep(100);
}
var observedRevision = CompilerRevision(observedState);
var revisionChanged = !string.Equals(baselineRevision, observedRevision, StringComparison.Ordinal);
var postconditionVerified = settled;
var value = new
{
Scope = normalizedScope,
TimeoutMs = boundedTimeout,
BaselineRevision = baselineRevision,
ObservedRevision = observedRevision,
RevisionChanged = revisionChanged,
ElapsedMs = (long)(DateTime.UtcNow - started).TotalMilliseconds,
Diagnostics = observedDiagnostics.Count > 0 ? observedDiagnostics : baselineDiagnostics,
PostconditionVerified = postconditionVerified
};
return postconditionVerified
? McpJson.Write(ToolOutcome<object>.Ready(value))
: McpJson.Write(ToolOutcome<object>.Degraded(value, "compile.await_timeout", "Compiler freshness did not reach a verified settled state before timeout."));
}
private static bool CompilerIsSettled(object? state, out List<string> diagnostics)
{
diagnostics = new List<string>();
if (state is null || (EditorRuntime.GetMember(state, "IsBuilding") is bool building && building)) return false;
var compilers = EditorRuntime.GetMember(state, "Compilers") as System.Collections.IEnumerable;
if (compilers is null) return false;
var sawCompiler = false;
foreach (var compiler in compilers)
{
sawCompiler = true;
if (EditorRuntime.GetMember(compiler, "IsBuilding") is bool compilerBuilding && compilerBuilding) return false;
if (EditorRuntime.GetMember(compiler, "NeedsBuild") is bool needsBuild && needsBuild) return false;
if (EditorRuntime.GetMember(compiler, "Success") is bool success && !success) return false;
if (EditorRuntime.GetMember(compiler, "Errors") is int errors && errors > 0) return false;
var name = EditorRuntime.GetMember(compiler, "Name")?.ToString() ?? "compiler";
var warningCount = EditorRuntime.GetMember(compiler, "Warnings")?.ToString() ?? "0";
diagnostics.Add($"{name}: warnings={warningCount}");
}
return sawCompiler;
}
private static string CompilerRevision(object? state)
{
var assembly = typeof(SboxMcpAssistant).Assembly;
var location = assembly.Location ?? "";
var fileStamp = File.Exists(location) ? $"{new FileInfo(location).Length}:{File.GetLastWriteTimeUtc(location).Ticks}" : "memory";
var parts = new List<string> { location, fileStamp, EditorRuntime.GetMember(state, "IsBuilding")?.ToString() ?? "unknown" };
if (EditorRuntime.GetMember(state, "Compilers") is System.Collections.IEnumerable compilers)
foreach (var compiler in compilers) parts.Add(string.Join(":", EditorRuntime.GetMember(compiler, "Name"), EditorRuntime.GetMember(compiler, "NeedsBuild"), EditorRuntime.GetMember(compiler, "Success"), EditorRuntime.GetMember(compiler, "Errors")));
return string.Join("|", parts);
}
[McpTool("pocketknife_package_references")]
public static string PackageReferences()
{
try
{
var root = Project.Current?.GetRootPath(); if (string.IsNullOrWhiteSpace(root)) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Unavailable, "project.not_active", "No active project is available.")); var sbproj = Directory.EnumerateFiles(root, "*.sbproj", SearchOption.TopDirectoryOnly).FirstOrDefault(); if (sbproj is null) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Unavailable, "project.sbproj_missing", "No .sbproj was found in the active project root."));
using var doc = JsonDocument.Parse(File.ReadAllText(sbproj)); var refs = doc.RootElement.TryGetProperty("PackageReferences", out var p) && p.ValueKind == JsonValueKind.Array ? p.EnumerateArray().Select(x => x.GetString() ?? x.ToString()).ToArray() : Array.Empty<string>(); return McpJson.Write(ToolOutcome<object>.Ready(new { ProjectFile = sbproj, PackageReferences = refs, Count = refs.Length }));
}
catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "project.references_failed", "Package reference parsing failed.", ex)); }
}
private static IEnumerable<Type> SafeTypes(Assembly assembly) { try { return assembly.GetTypes(); } catch { return Array.Empty<Type>(); } }
}
}