Editor/RpcDispatcher.cs
using System;
using System.Text.Json;
using System.Threading.Tasks;
using Sandbox;
namespace SboxMcpServer;
/// <summary>
/// Handles JSON-RPC method dispatch for the MCP server.
/// Translates incoming method names into tool handler calls and sends
/// the result back over the SSE connection via McpServer.SendSseEvent.
/// </summary>
internal static class RpcDispatcher
{
/// <summary>
/// Parses and dispatches a single JSON-RPC request, then sends the response
/// as an SSE event on the given session.
/// </summary>
internal static async Task ProcessRpcRequest(
McpSession session,
object id,
string method,
string rawBody,
JsonSerializerOptions jsonOptions,
Action<string> logInfo,
Action<string> logError )
{
object result = null;
object error = null;
using var doc = JsonDocument.Parse( rawBody );
var root = doc.RootElement;
try
{
if ( method == "initialize" )
{
result = new
{
protocolVersion = "2024-11-05",
capabilities = new { tools = new { listChanged = true } },
serverInfo = new { name = "SboxMcpServer", version = "1.2.0" }
};
}
else if ( method == "tools/list" )
{
result = new { tools = ToolDefinitions.All };
}
else if ( method == "tools/call" )
{
var args = root.TryGetProperty( "params", out var p ) && p.TryGetProperty( "arguments", out var a ) ? a : default;
var toolName = root.GetProperty( "params" ).GetProperty( "name" ).GetString();
// run_console_command is dispatched through its own method so that
// its try/catch can intercept exceptions thrown by ConsoleSystem.Run
// on the main thread (nested catches in async methods don't reliably
// catch these in s&box's sandbox environment).
if ( toolName == "run_console_command" )
{
result = RunConsoleCommandSafe( args );
}
else
{
// Scene API calls must run on the main thread.
logInfo?.Invoke( $"Waiting for GameTask.MainThread() to execute tool {toolName}..." );
await GameTask.MainThread();
logInfo?.Invoke( $"Resumed on MainThread for tool {toolName}." );
result = toolName switch
{
// ── Read tools ───────────────────────────────────────────────────────
"get_scene_summary" => SceneToolHandlers.GetSceneSummary( jsonOptions ),
"get_scene_hierarchy" => SceneToolHandlers.GetSceneHierarchy( args ),
"find_game_objects" => SceneToolHandlers.FindGameObjects( args, jsonOptions ),
"find_game_objects_in_radius" => SceneToolHandlers.FindGameObjectsInRadius( args, jsonOptions ),
"get_game_object_details" => SceneToolHandlers.GetGameObjectDetails( args, jsonOptions ),
"get_component_properties" => SceneToolHandlers.GetComponentProperties( args, jsonOptions ),
"get_prefab_instances" => SceneToolHandlers.GetPrefabInstances( args, jsonOptions ),
// ── Asset + console ──────────────────────────────────────────────────
"browse_assets" => AssetToolHandlers.BrowseAssets( args, jsonOptions ),
"get_editor_context" => AssetToolHandlers.GetEditorContext( jsonOptions ),
"list_console_commands" => ConsoleToolHandlers.ListConsoleCommands( args, jsonOptions ),
// ── Write tools ──────────────────────────────────────────────────────
"create_game_object" => OzmiumWriteHandlers.CreateGameObject( args ),
"add_component" => OzmiumWriteHandlers.AddComponent( args ),
"remove_component" => OzmiumWriteHandlers.RemoveComponent( args ),
"set_component_property" => OzmiumWriteHandlers.SetComponentProperty( args ),
"destroy_game_object" => OzmiumWriteHandlers.DestroyGameObject( args ),
"reparent_game_object" => OzmiumWriteHandlers.ReparentGameObject( args ),
"set_game_object_tags" => OzmiumWriteHandlers.SetGameObjectTags( args ),
"instantiate_prefab" => OzmiumWriteHandlers.InstantiatePrefab( args ),
"save_scene" => OzmiumWriteHandlers.SaveScene(),
"undo" => OzmiumWriteHandlers.Undo(),
"redo" => OzmiumWriteHandlers.Redo(),
// ── Batch transform & object management ────────────────────────────
"set_game_object_transform" => OzmiumWriteHandlers.SetGameObjectTransform( args ),
"duplicate_game_object" => OzmiumWriteHandlers.DuplicateGameObject( args ),
"set_game_object_enabled" => OzmiumWriteHandlers.SetGameObjectEnabled( args ),
"set_game_object_name" => OzmiumWriteHandlers.SetGameObjectName( args ),
"set_component_enabled" => OzmiumWriteHandlers.SetComponentEnabled( args ),
// ── Extended asset tools ─────────────────────────────────────────────
"get_model_info" => OzmiumAssetHandlers.GetModelInfo( args ),
"get_material_properties" => OzmiumAssetHandlers.GetMaterialProperties( args ),
"get_prefab_structure" => OzmiumAssetHandlers.GetPrefabStructure( args ),
"reload_asset" => OzmiumAssetHandlers.ReloadAsset( args ),
"get_component_types" => OzmiumAssetHandlers.GetComponentTypes( args ),
"search_assets" => OzmiumAssetHandlers.SearchAssets( args ),
"get_scene_statistics" => OzmiumAssetHandlers.GetSceneStatistics(),
// ── Editor control ───────────────────────────────────────────────────
"select_game_object" => OzmiumEditorHandlers.SelectGameObject( args ),
"open_asset" => OzmiumEditorHandlers.OpenAsset( args ),
"get_play_state" => OzmiumEditorHandlers.GetPlayState(),
"start_play_mode" => OzmiumEditorHandlers.StartPlayMode(),
"stop_play_mode" => OzmiumEditorHandlers.StopPlayMode(),
"get_editor_log" => OzmiumEditorHandlers.GetEditorLog( args ),
// ── Selection tools ──────────────────────────────────────────────────
"get_selected_objects" => OzmiumEditorHandlers.GetSelectedObjects(),
"set_selected_objects" => OzmiumEditorHandlers.SetSelectedObjects( args ),
"clear_selection" => OzmiumEditorHandlers.ClearSelection(),
// ── Mesh editing tools ───────────────────────────────────────────────
"create_block" => MeshEditHandlers.CreateBlock( args ),
"set_face_material" => MeshEditHandlers.SetFaceMaterial( args ),
"set_texture_parameters" => MeshEditHandlers.SetTextureParameters( args ),
"set_vertex_position" => MeshEditHandlers.SetVertexPosition( args ),
"set_vertex_color" => MeshEditHandlers.SetVertexColor( args ),
"set_vertex_blend" => MeshEditHandlers.SetVertexBlend( args ),
"get_mesh_info" => MeshEditHandlers.GetMeshInfo( args ),
// ── Lighting tools ───────────────────────────────────────────────────
"create_light" => LightingToolHandlers.CreateLight( args ),
"configure_light" => LightingToolHandlers.ConfigureLight( args ),
"create_sky_box" => LightingToolHandlers.CreateSkyBox( args ),
"set_sky_box" => LightingToolHandlers.SetSkyBox( args ),
"create_ambient_light" => LightingToolHandlers.CreateAmbientLight( args ),
"create_indirect_light_volume" => LightingToolHandlers.CreateIndirectLightVolume( args ),
// ── Physics & collider tools ────────────────────────────────────────
"add_collider" => PhysicsToolHandlers.AddCollider( args ),
"configure_collider" => PhysicsToolHandlers.ConfigureCollider( args ),
"add_rigidbody" => PhysicsToolHandlers.AddRigidbody( args ),
"create_character_controller" => PhysicsToolHandlers.CreateCharacterController( args ),
"add_plane_collider" => PhysicsToolHandlers.AddPlaneCollider( args ),
"add_hull_collider" => PhysicsToolHandlers.AddHullCollider( args ),
"create_model_physics" => PhysicsToolHandlers.CreateModelPhysics( args ),
// ── Audio tools ─────────────────────────────────────────────────────
"create_sound_point" => AudioToolHandlers.CreateSoundPoint( args ),
"configure_sound" => AudioToolHandlers.ConfigureSound( args ),
"create_soundscape_trigger" => AudioToolHandlers.CreateSoundscapeTrigger( args ),
"create_sound_box" => AudioToolHandlers.CreateSoundBox( args ),
"create_dsp_volume" => AudioToolHandlers.CreateDspVolume( args ),
"create_audio_listener" => AudioToolHandlers.CreateAudioListener( args ),
// ── Camera tools ────────────────────────────────────────────────────
"create_camera" => CameraToolHandlers.CreateCamera( args ),
"configure_camera" => CameraToolHandlers.ConfigureCamera( args ),
// ── Effect & environment tools ──────────────────────────────────────
"create_particle_effect" => EffectToolHandlers.CreateParticleEffect( args ),
"configure_particle_effect" => EffectToolHandlers.ConfigureParticleEffect( args ),
"create_fog_volume" => EffectToolHandlers.CreateFogVolume( args ),
"configure_post_processing" => EffectToolHandlers.ConfigurePostProcessing( args ),
"create_environment_light" => EffectToolHandlers.CreateEnvironmentLight( args ),
// ── Utility tools ───────────────────────────────────────────────────
"get_asset_dependencies" => UtilityToolHandlers.GetAssetDependencies( args ),
"batch_transform" => UtilityToolHandlers.BatchTransform( args ),
"copy_component" => UtilityToolHandlers.CopyComponent( args ),
"get_object_bounds" => UtilityToolHandlers.GetObjectBounds( args ),
// ── Navigation tools ──────────────────────────────────────────────────
"create_nav_mesh_agent" => NavigationToolHandlers.CreateNavMeshAgent( args ),
"create_nav_mesh_link" => NavigationToolHandlers.CreateNavMeshLink( args ),
"create_nav_mesh_area" => NavigationToolHandlers.CreateNavMeshArea( args ),
// ── Rendering tools ──────────────────────────────────────────────────
"create_render_entity" => RenderingToolHandlers.CreateRenderEntity( args ),
// ── Game tools ──────────────────────────────────────────────────────
"create_game_entity" => GameToolHandlers.CreateGameEntity( args ),
// ── Effect & physics extension tools ────────────────────────────────
"create_beam_effect" => EffectToolHandlers.CreateBeamEffect( args ),
"create_verlet_rope" => EffectToolHandlers.CreateVerletRope( args ),
"create_joint" => EffectToolHandlers.CreateJoint( args ),
"create_clutter" => EffectToolHandlers.CreateClutter( args ),
"create_radius_damage" => EffectToolHandlers.CreateRadiusDamage( args ),
// ── Editor & scene extension tools ──────────────────────────────────
"frame_selection" => OzmiumEditorHandlers.FrameSelection( args ),
"save_scene_as" => OzmiumEditorHandlers.SaveSceneAs( args ),
"get_scene_unsaved" => OzmiumEditorHandlers.GetSceneUnsaved(),
"break_from_prefab" => OzmiumEditorHandlers.BreakFromPrefab( args ),
"update_from_prefab" => OzmiumEditorHandlers.UpdateFromPrefab( args ),
_ => throw new InvalidOperationException( $"Tool '{toolName}' not found" )
};
}
logInfo( $"Tool: {toolName}" );
}
else
{
error = new { code = -32601, message = $"Method '{method}' not found" };
}
}
catch ( ArgumentException ex )
{
// Invalid parameters (e.g. missing required arg)
error = new { code = -32602, message = ex.Message };
}
catch ( Exception ex )
{
logError( $"ProcessRpcRequest catch: method={method} ex={ex.Message}" );
// For run_console_command, convert engine exceptions into a friendly text result.
// Parse rawBody fresh since root/doc may be in an uncertain state after the fault.
if ( method == "tools/call" )
{
string toolNameCatch = null;
string cmdStrCatch = "?";
try
{
var bodyDoc = JsonDocument.Parse( rawBody );
var paramsEl = bodyDoc.RootElement.GetProperty( "params" );
toolNameCatch = paramsEl.GetProperty( "name" ).GetString();
if ( paramsEl.TryGetProperty( "arguments", out var argsEl ) &&
argsEl.TryGetProperty( "command", out var cmdEl ) )
cmdStrCatch = cmdEl.GetString() ?? "?";
}
catch ( Exception parseEx )
{
logError( $"ProcessRpcRequest catch parse error: {parseEx.Message}" );
}
logError( $"ProcessRpcRequest catch: toolName={toolNameCatch}" );
if ( toolNameCatch == "run_console_command" )
{
result = ToolHandlerBase.TextResult( $"Command failed: {cmdStrCatch}\nError: {ex.Message}" );
error = null;
}
else
{
error = new { code = -32603, message = $"Internal error: {ex.Message}" };
}
}
else
{
error = new { code = -32603, message = $"Internal error: {ex.Message}" };
}
}
var response = new { jsonrpc = "2.0", id, result, error };
var json = JsonSerializer.Serialize( response, jsonOptions );
await McpServer.SendSseEvent( session, "message", json );
}
/// <summary>
/// Runs run_console_command in a plain try/catch (no async context) so that
/// exceptions from ConsoleSystem are catchable.
/// </summary>
private static object RunConsoleCommandSafe( JsonElement args )
{
var cmdStr = args.ValueKind != JsonValueKind.Undefined && args.TryGetProperty( "command", out var cp )
? cp.GetString()
: "";
try
{
return ConsoleToolHandlers.RunConsoleCommand( args );
}
catch ( Exception ex )
{
return ToolHandlerBase.TextResult( $"Command failed: {cmdStr}\nError: {ex.Message}" );
}
}
}