Editor/Contracts/SboxCapabilityCatalog.cs
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
namespace Editor.Mcp
{
internal sealed record SboxCapabilityMetadata(
string Tool,
string Category,
string Summary,
string AccessMode,
bool RequiresApproval,
string[] RequiredEditorStates,
string Verification,
string[] Keywords)
{
public object ToPayload() => new
{
Tool,
Category,
Summary,
AccessMode,
RequiresApproval,
RequiredEditorStates,
Verification,
Keywords
};
}
internal static class SboxCapabilityCatalog
{
private static readonly IReadOnlyList<SboxCapabilityMetadata> Items = new[]
{
Read("pocketknife_find_capability", "discovery", "Find a small ranked set of editor tools by task terms before loading a tool schema.", Array.Empty<string>(), "Results are ranked from explicit metadata and do not execute a tool.", "capability", "search", "routing", "tool"),
Read("pocketknife_query_schema", "api", "Search the official s&box API schema for types and members.", new[] { "editor-process" }, "The response identifies the official schema source and cache timestamp.", "sbox", "api", "schema", "type"),
Read("pocketknife_search_local_references", "reference", "Search the approved local reference index for code, systems, assets, and documentation.", new[] { "editor-process" }, "Results include bounded snippets and indexed source metadata.", "reference", "vault", "code", "asset", "system"),
Write("pocketknife_rebuild_local_reference_index", "reference", "Rebuild the local reference index from its configured catalog.", new[] { "editor-process" }, "The index reports the scanned source set and resulting entry count.", "reference", "index", "rebuild"),
Read("pocketknife_get_local_reference", "reference", "Read one bounded local reference result by stable ID.", new[] { "editor-process" }, "The returned item is resolved from the local reference index.", "reference", "read", "source"),
Read("pocketknife_search_live_codesearch", "api", "Search public s&box Code Search results through the official page or local worker.", new[] { "editor-process" }, "The response retains official package/file URLs and transport status.", "sbox", "code", "search", "public"),
Analyze("pocketknife_test_clothing", "asset", "Load a clothing asset into the configured test harness for inspection.", new[] { "editor-process", "active-project" }, "The result reports the selected asset and test-harness outcome.", "clothing", "avatar", "asset"),
External("pocketknife_open_publish_wizard", "publishing", "Open the s&box publishing workflow for the active project.", new[] { "editor-process", "active-project" }, "The editor acceptance is reported; publication itself requires a separate explicit workflow.", "publish", "external", "package"),
Write("pocketknife_spawn_procedural_road", "procedural", "Create a project road component using the loaded s&box runtime types.", new[] { "editor-process", "active-scene" }, "The created object and component type are returned.", "road", "procedural", "scene"),
Write("pocketknife_spawn_water_surface", "procedural", "Create a project water surface component using the loaded runtime types.", new[] { "editor-process", "active-scene" }, "The created object and component type are returned.", "water", "procedural", "scene"),
Write("pocketknife_spawn_architecture_plan", "procedural", "Create a bounded architecture plan in the active scene.", new[] { "editor-process", "active-scene" }, "The plan object and requested dimensions are returned.", "architecture", "building", "procedural"),
Write("pocketknife_add_architecture_room", "procedural", "Add a room to an existing architecture plan.", new[] { "editor-process", "active-scene" }, "The parent, room, position, and dimensions are returned.", "architecture", "room", "procedural"),
Write("pocketknife_navmesh_bake", "navigation", "Bake the active scene navigation mesh when its dependency is available.", new[] { "editor-process", "active-scene" }, "The navigation mesh leaves its generating/dirty state before success is reported.", "navmesh", "navigation", "bake"),
Write("pocketknife_switch_editor_tab", "editor", "Open or activate a named editor surface when its state can be verified.", new[] { "editor-process" }, "The matching surface or dock activation is checked after the request.", "editor", "tab", "surface"),
Read("pocketknife_dock_state", "editor", "List bounded editor dock state including open, active, and raised readback.", new[] { "editor-process" }, "Dock identity and state are returned from Editor.DockManager.", "editor", "dock", "state"),
Write("pocketknife_configure_navmesh", "navigation", "Enable or disable the active scene navigation mesh and optionally save it.", new[] { "editor-process", "active-scene" }, "The requested enabled state and save result are read back.", "navmesh", "navigation", "configure"),
Analyze("pocketknife_find_type", "code", "Find loaded s&box types by full name, short name, or fragment.", new[] { "editor-process" }, "The result lists matching loaded types and a bounded count.", "code", "type", "reflection"),
Analyze("pocketknife_type_members", "code", "Inspect properties and methods for one loaded s&box type.", new[] { "editor-process" }, "The returned member list is derived from loaded runtime assemblies.", "code", "type", "members"),
Write("pocketknife_compile_await", "build", "Await project compiler freshness after a compile dispatch.", new[] { "editor-process", "active-project" }, "Baseline and observed compiler revisions plus settled diagnostics are returned.", "compile", "build", "freshness"),
Read("pocketknife_package_references", "project", "Read package references from the active project's .sbproj.", new[] { "editor-process", "active-project" }, "The selected project file and parsed package references are returned.", "project", "packages", "dependencies"),
Write("pocketknife_open_in_hammer", "asset", "Open a .vmap asset in the Hammer editor when surface state is verifiable.", new[] { "editor-process", "active-project" }, "The selected asset path and active-surface verification state are returned.", "hammer", "vmap", "editor"),
Write("pocketknife_edit_material", "asset", "Open a .vmat asset in the material editor when surface state is verifiable.", new[] { "editor-process", "active-project" }, "The selected material path and active-surface verification state are returned.", "material", "vmat", "editor"),
Write("pocketknife_create_sound_event", "asset", "Create a bounded .sound event asset from an approved audio path.", new[] { "editor-process", "active-project" }, "The asset path and editor refresh result are verified.", "sound", "audio", "asset"),
Read("pocketknife_asset_search", "asset", "Search the editor asset index with bounded kind and path filters.", new[] { "editor-process", "active-project" }, "Results include count, pagination, source, and provenance.", "asset", "search", "project"),
Write("pocketknife_paint_terrain", "scene", "Create the supported terrain component for a bounded terrain request.", new[] { "editor-process", "active-scene" }, "The created component is returned; unsupported paint APIs remain unavailable.", "terrain", "scene", "component"),
Write("pocketknife_spawn_sandbox_entity", "scene", "Create a supported sandbox prop entity from a project model asset.", new[] { "editor-process", "active-scene" }, "The object, renderer, collider, and physics state are returned.", "entity", "prop", "physics"),
Read("pocketknife_scene_sessions", "editor", "List open scene sessions with stable IDs and lifecycle state.", new[] { "editor-process" }, "Session ID, path, active, dirty, mounted, and playing state are returned.", "scene", "session", "tabs"),
Write("pocketknife_scene_activate", "editor", "Activate an existing scene session by stable ID or path.", new[] { "editor-process" }, "SceneEditorSession.Active is read back before success.", "scene", "session", "activate"),
Analyze("pocketknife_viewport_capture", "observability", "Capture an allowlisted editor or scene camera mode with verified PNG output.", new[] { "editor-process" }, "Mode, dimensions, bytes, file, and image block are verified.", "screenshot", "viewport", "camera"),
Read("pocketknife_package_search", "package", "Search published packages with bounded provenance-aware results.", new[] { "editor-process" }, "Package ident, title, author, type, source, and provenance are returned.", "package", "cloud", "search"),
Write("pocketknife_package_install", "package", "Install one published package through the typed editor asset system.", new[] { "editor-process", "active-project" }, "Installed revision and dependency state are read back.", "package", "cloud", "install"),
Read("pocketknife_package_status", "package", "Read cloud package metadata and local installed revision state.", new[] { "editor-process" }, "Installed state and revision are returned without exposing credentials.", "package", "cloud", "status"),
Read("pocketknife_asset_info", "asset", "Read indexed asset or package provenance and compile state.", new[] { "editor-process" }, "Resource path, source, package, revision, compile state, and surface are returned.", "asset", "info", "provenance"),
Read("pocketknife_editor_surface_status", "editor", "Report runtime availability and dependencies for an allowlisted editor surface.", new[] { "editor-process" }, "Runtime type, official matches, and missing dependency code are returned.", "surface", "editor", "dependency"),
Write("pocketknife_editor_document_open", "editor", "Open an allowlisted editor document only when active-surface state can be checked.", new[] { "editor-process", "active-project" }, "OpenFile dispatch is not Ready without surface readback.", "document", "surface", "open"),
Read("pocketknife_navmesh_status", "navigation", "Read typed NavMesh state for an open scene session.", new[] { "editor-process", "active-scene" }, "Enabled, generating, dirty, scene, and dependency state are returned.", "navmesh", "navigation", "status"),
Read("pocketknife_navmesh_path", "navigation", "Calculate a bounded path through the active typed NavMesh.", new[] { "editor-process", "active-scene" }, "Returned points are capped and tied to the active scene.", "navmesh", "navigation", "path"),
Read("pocketknife_hotload_await", "build", "Await a typed editor hotload manager state transition.", new[] { "editor-process" }, "Baseline and observed hotload revisions plus pending state are returned.", "hotload", "build", "freshness"),
Read("pocketknife_network_status", "network", "Read redacted typed editor and provider availability state.", new[] { "editor-process" }, "No credentials or unrestricted endpoints are returned.", "network", "status", "redacted"),
Write("pocketknife_lobby_start", "network", "Start a typed runtime lobby when the current runtime exposes one.", new[] { "editor-process" }, "Lobby identity and state are read back before success.", "lobby", "network", "start"),
Write("pocketknife_lobby_stop", "network", "Stop a typed runtime lobby when the current runtime exposes one.", new[] { "editor-process" }, "Lobby stopped state is read back before success.", "lobby", "network", "stop"),
Write("pocketknife_server_start", "provider", "Start one typed Voyageur server operation.", new[] { "editor-process" }, "Provider operation ID and redacted state are verified.", "server", "voyageur", "start"),
Read("pocketknife_server_status", "provider", "Read one typed Voyageur server operation state.", new[] { "editor-process" }, "Provider operation ID and redacted state are returned.", "server", "voyageur", "status"),
Write("pocketknife_server_stop", "provider", "Stop one typed Voyageur server operation.", new[] { "editor-process" }, "Stopped provider state is verified.", "server", "voyageur", "stop")
};
public static IReadOnlyList<SboxCapabilityMetadata> All => Items;
public static SboxCapabilityMetadata? Find(string tool)
=> Items.FirstOrDefault(item => item.Tool.Equals(tool?.Trim(), StringComparison.OrdinalIgnoreCase));
public static IReadOnlyList<SboxCapabilityMetadata> Search(string query, int limit, int offset, out int total)
{
var normalized = (query ?? "").Trim().ToLowerInvariant();
var terms = normalized.Split(new[] { ' ', '\t', '\r', '\n', '/', '\\', '.', ',', ':', ';', '-', '_' }, StringSplitOptions.RemoveEmptyEntries).Where(term => term.Length >= 2).ToArray();
var ranked = Items
.Select(item => new { Item = item, Score = Score(item, normalized, terms) })
.Where(result => terms.Length == 0 || result.Score > 0)
.OrderByDescending(result => result.Score)
.ThenBy(result => result.Item.Tool, StringComparer.OrdinalIgnoreCase)
.ToArray();
total = ranked.Length;
return ranked.Skip(Math.Max(offset, 0)).Take(Math.Clamp(limit, 1, 20)).Select(result => result.Item).ToArray();
}
private static int Score(SboxCapabilityMetadata item, string query, string[] terms)
{
if ( terms.Length == 0 ) return 1;
var tool = item.Tool.ToLowerInvariant();
var category = item.Category.ToLowerInvariant();
var summary = item.Summary.ToLowerInvariant();
var keywords = item.Keywords.Select(keyword => keyword.ToLowerInvariant()).ToArray();
var score = 0;
foreach ( var term in terms )
{
if ( tool.Contains(term, StringComparison.Ordinal) ) score += 8;
if ( category.Contains(term, StringComparison.Ordinal) ) score += 5;
if ( summary.Contains(term, StringComparison.Ordinal) ) score += 2;
if ( keywords.Any(keyword => keyword.Contains(term, StringComparison.Ordinal)) ) score += 4;
}
if ( !string.IsNullOrWhiteSpace(query) && summary.Contains(query, StringComparison.Ordinal) ) score += 3;
return score;
}
private static SboxCapabilityMetadata Read(string tool, string category, string summary, string[] states, string verification, params string[] keywords)
=> new(tool, category, summary, "read", false, states, verification, keywords);
private static SboxCapabilityMetadata Analyze(string tool, string category, string summary, string[] states, string verification, params string[] keywords)
=> new(tool, category, summary, "analyze", false, states, verification, keywords);
private static SboxCapabilityMetadata Write(string tool, string category, string summary, string[] states, string verification, params string[] keywords)
=> new(tool, category, summary, "project-write", true, states, verification, keywords);
private static SboxCapabilityMetadata External(string tool, string category, string summary, string[] states, string verification, params string[] keywords)
=> new(tool, category, summary, "external-write", true, states, verification, keywords);
}
}