Editor MCP assistant diagnostics tools. Provides editor-only commands for video recording control, simulated network conditions, voice recording, chat dispatch, runtime play-mode inspection, and running test scenarios, returning JSON tool outcomes.
#nullable enable
using System;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Sandbox;
namespace Editor.Mcp
{
public static partial class SboxMcpAssistant
{
[McpTool( "pocketknife_video_record" )]
public static string VideoRecord(
[Description( "Action: start, stop, status" )] string action,
[Description( "Destination file name (e.g. 'run.mp4')" )] string fileName = "test_run.mp4" )
{
try
{
return McpJson.Write( VideoRecordingDriver.Control( action, fileName ) );
}
catch ( Exception ex )
{
return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "video.control_failed", "Video control failed.", ex ) );
}
}
[McpTool( "pocketknife_network_condition" )]
public static string NetworkCondition(
[Description( "Simulated latency in ms (0-500, -1 to leave unchanged)" )] int fakeLagMs = -1,
[Description( "Simulated packet loss percentage (0-100, -1 to leave unchanged)" )] float packetLossPercent = -1f,
[Description( "Network action: none, start_host, disconnect" )] string action = "none" )
{
try
{
if ( fakeLagMs >= 0 ) ConsoleSystem.SetValue( "net_fakelag", Math.Clamp( fakeLagMs, 0, 500 ).ToString() );
if ( packetLossPercent >= 0 ) ConsoleSystem.SetValue( "net_fakepacketloss", Math.Clamp( packetLossPercent, 0f, 100f ).ToString() );
if ( action.Equals( "start_host", StringComparison.OrdinalIgnoreCase ) ) EditorUtility.Network.StartHosting();
else if ( action.Equals( "disconnect", StringComparison.OrdinalIgnoreCase ) ) EditorUtility.Network.Disconnect();
return McpJson.Write( ToolOutcome<object>.Ready( new
{
FakeLagMs = fakeLagMs,
PacketLoss = packetLossPercent,
IsHosting = EditorUtility.Network.Hosting,
NetworkActive = EditorUtility.Network.Active
} ) );
}
catch ( Exception ex )
{
return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "network.condition_failed", "Network condition adjustment failed.", ex ) );
}
}
[McpTool( "pocketknife_voice_diagnostic" )]
public static string VoiceDiagnostic(
[Description( "Action: status, start_recording, stop_and_save" )] string action,
[Description( "Destination file path when stopping recording" )] string? wavPath = null )
{
try
{
var normAction = action.ToLowerInvariant();
if ( normAction == "start_recording" )
{
EditorUtility.VoiceRecording.Start( 44100, 1 );
return McpJson.Write( ToolOutcome<object>.Ready( new { Action = "start_recording", Status = "Recording started" } ) );
}
if ( normAction == "stop_and_save" )
{
EditorUtility.VoiceRecording.Stop();
var path = string.IsNullOrWhiteSpace( wavPath ) ? "recorded_voice_test.wav" : wavPath;
EditorUtility.VoiceRecording.Save( path );
return McpJson.Write( ToolOutcome<object>.Ready( new { Action = "stop_and_save", SavedPath = path } ) );
}
return McpJson.Write( ToolOutcome<object>.Ready( new { Action = "status", RecordingSupported = true } ) );
}
catch ( Exception ex )
{
return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "voice.diagnostic_failed", "Voice diagnostic failed.", ex ) );
}
}
[McpTool( "pocketknife_chat_dispatch" )]
public static string ChatDispatch(
[Description( "Chat message text" )] string message,
[Description( "If true, dispatches as system event; if false, says as local player" )] bool asSystem = true )
{
try
{
if ( string.IsNullOrWhiteSpace( message ) )
{
return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "chat.message_required", "Chat message cannot be empty." ) );
}
if ( asSystem )
{
var evt = new ChatMessageEvent { Message = message, Sender = null };
Game.ActiveScene?.RunEvent<IChatEvent>( h => h.OnChatMessage( evt ) );
}
else
{
Sandbox.Platform.Chat.Say( message );
}
return McpJson.Write( ToolOutcome<object>.Ready( new { Message = message, AsSystem = asSystem, Delivered = true } ) );
}
catch ( Exception ex )
{
return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "chat.dispatch_failed", "Chat dispatch failed.", ex ) );
}
}
[McpTool( "pocketknife_play_inspect" )]
public static string PlayInspect(
[Description( "Query string to find GameObject by name or tag" )] string query,
[Description( "Specific component name to inspect" )] string? component = null )
{
try
{
if ( !Game.IsPlaying )
{
return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Unavailable, "inspect.playmode_inactive", "Play mode must be active to inspect runtime state." ) );
}
var go = FindGameObjectInScene( Game.ActiveScene?.Children, query );
if ( go == null )
{
return McpJson.Write( ToolOutcome<object>.Failure( new { Query = query }, ToolStatus.Failed, "inspect.not_found", $"GameObject '{query}' not found." ) );
}
var components = go.Components.GetAll();
if ( !string.IsNullOrWhiteSpace( component ) )
{
components = components.Where( c => c.GetType().Name.Equals( component, StringComparison.OrdinalIgnoreCase ) );
}
var componentList = components.Select( c => new
{
Type = c.GetType().Name,
Enabled = c.Enabled
} ).ToArray();
return McpJson.Write( ToolOutcome<object>.Ready( new
{
Name = go.Name,
Id = go.Id.ToString(),
Position = go.WorldPosition,
Components = componentList
} ) );
}
catch ( Exception ex )
{
return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "inspect.failed", "Play inspection failed.", ex ) );
}
}
private static GameObject? FindGameObjectInScene( System.Collections.Generic.IEnumerable<GameObject>? roots, string query )
{
if ( roots is null ) return null;
foreach ( var item in roots )
{
if ( item.Id.ToString().Equals( query, StringComparison.OrdinalIgnoreCase ) ||
item.Name.Equals( query, StringComparison.OrdinalIgnoreCase ) ||
item.Tags.Has( query ) )
{
return item;
}
var nested = FindGameObjectInScene( item.Children, query );
if ( nested != null ) return nested;
}
return null;
}
[McpTool( "pocketknife_run_test_scenario" )]
public static async Task<string> RunTestScenario(
[Description( "Human-readable test scenario name" )] string scenarioName,
[Description( "JSON string containing array of step objects" )] string stepsJson,
[Description( "Record MP4 video of the scenario run" )] bool recordVideo = false )
{
try
{
var result = await ScenarioRunner.ExecuteScenarioAsync( scenarioName, stepsJson, recordVideo );
return McpJson.Write( result );
}
catch ( Exception ex )
{
return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "scenario.execute_failed", "Scenario execution failed.", ex ) );
}
}
}
}