Editor toolset for the s&box editor. It provides MCP (tool) endpoints to query Facepunch Cloud packages and leaderboards, manage networking lobbies, and record simple in-editor replay snapshots of physics-enabled entities.
#nullable enable
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Sandbox;
using Sandbox.Network;
using Sandbox.Services;
namespace Editor.Mcp
{
public static partial class SboxMcpAssistant
{
/// <summary>
/// Queries Facepunch Cloud services directly from the editor: searches and fetches packages, models,
/// gamemodes, sounds, and inspects game leaderboards.
/// </summary>
[McpTool( "pocketknife_facepunch_cloud_query" )]
public static async Task<string> FacepunchCloudQuery(
[Description( "Query type: 'package_search', 'package_fetch', 'leaderboard'" )] string queryType,
[Description( "Search query or package ident (e.g. 'facepunch.terry', 'box_fight', 'cars')" )] string target,
[Description( "Number of results to take for search queries (1-50)" )] int take = 10,
[Description( "Optional stat name for leaderboards" )] string? statName = null )
{
try
{
var normType = queryType.ToLowerInvariant().Trim();
if ( normType == "package_search" )
{
using var cts = new CancellationTokenSource( TimeSpan.FromSeconds( 10 ) );
var result = await Package.FindAsync( target, Math.Clamp( take, 1, 50 ), 0, cts.Token );
var packages = result?.Packages?.Select( p => new
{
p.Ident,
p.Title,
p.Summary,
Type = p.TypeName,
Org = p.Org?.Title,
p.Thumb,
p.Updated
} ).ToArray() ?? Array.Empty<object>();
return McpJson.Write( ToolOutcome<object>.Ready( new
{
Query = target,
TotalFound = result?.TotalCount ?? 0,
ReturnedCount = packages.Length,
Packages = packages
} ) );
}
if ( normType == "package_fetch" )
{
var pkg = await Package.FetchAsync( target, partial: false );
if ( pkg == null )
{
return McpJson.Write( ToolOutcome<object>.Failure( new { Ident = target }, ToolStatus.Failed, "package.not_found", $"Package '{target}' was not found." ) );
}
return McpJson.Write( ToolOutcome<object>.Ready( new
{
pkg.Ident,
pkg.Title,
pkg.Summary,
pkg.Description,
Type = pkg.TypeName,
Org = pkg.Org?.Title,
pkg.Thumb,
pkg.Updated,
Tags = pkg.Tags?.ToArray()
} ) );
}
if ( normType == "leaderboard" )
{
long totalEntries = 0;
string boardType = "standard";
if ( !string.IsNullOrWhiteSpace( statName ) )
{
var board2 = Leaderboards.GetFromStat( target, statName );
if ( board2 != null )
{
totalEntries = board2.TotalEntries;
boardType = "stat";
}
}
else
{
var board = Leaderboards.Get( target );
if ( board != null )
{
totalEntries = board.TotalEntries;
boardType = "named";
}
}
return McpJson.Write( ToolOutcome<object>.Ready( new
{
Target = target,
StatName = statName,
BoardType = boardType,
Status = "Leaderboard resolved",
TotalEntries = totalEntries
} ) );
}
return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "cloud.type_invalid", $"Unsupported query type '{queryType}'. Use package_search, package_fetch, or leaderboard." ) );
}
catch ( Exception ex )
{
return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "cloud.query_failed", "Facepunch cloud query failed.", ex ) );
}
}
/// <summary>
/// Creates, reconfigures, or inspects modern s&box networking lobbies with privacy controls
/// (Public, FriendsOnly, Invisible), max players, hidden mode, and host migration.
/// </summary>
[McpTool( "pocketknife_lobby_manage" )]
public static string LobbyManage(
[Description( "Action: 'create', 'status', 'disconnect', 'query'" )] string action = "status",
[Description( "Lobby privacy: 'Public', 'FriendsOnly', 'Invisible'" )] string privacy = "Public",
[Description( "Maximum players (1-64)" )] int maxPlayers = 8,
[Description( "Lobby title/name" )] string? name = null,
[Description( "Whether lobby is hidden from public matchmaking" )] bool hidden = false,
[Description( "Whether lobby destroys when host leaves" )] bool destroyWhenHostLeaves = false )
{
try
{
var normAction = action.ToLowerInvariant().Trim();
if ( normAction == "status" )
{
var connections = Connection.All?.Select( c => new
{
Id = c.Id.ToString(),
c.DisplayName,
c.SteamId
} ).ToArray() ?? Array.Empty<object>();
return McpJson.Write( ToolOutcome<object>.Ready( new
{
Networking.IsActive,
Networking.IsHost,
Networking.IsClient,
Networking.IsConnecting,
Networking.ServerName,
Networking.MapName,
Networking.MaxPlayers,
ConnectedPlayerCount = connections.Length,
Connections = connections
} ) );
}
if ( normAction == "disconnect" )
{
Networking.Disconnect();
return McpJson.Write( ToolOutcome<object>.Ready( new { Action = "disconnect", Disconnected = true } ) );
}
if ( normAction == "create" )
{
var parsedPrivacy = Enum.TryParse<LobbyPrivacy>( privacy, true, out var p ) ? p : LobbyPrivacy.Public;
var config = new LobbyConfig
{
Privacy = parsedPrivacy,
MaxPlayers = Math.Clamp( maxPlayers, 1, 64 ),
Name = string.IsNullOrWhiteSpace( name ) ? "s&box Lobby" : name.Trim(),
Hidden = hidden,
DestroyWhenHostLeaves = destroyWhenHostLeaves
};
Networking.CreateLobby( config );
return McpJson.Write( ToolOutcome<object>.Ready( new
{
Action = "create",
LobbyCreated = true,
config.Name,
Privacy = config.Privacy.ToString(),
config.MaxPlayers,
config.Hidden
} ) );
}
return McpJson.Write( ToolOutcome<object>.Ready( new { Action = normAction, Status = "Ready" } ) );
}
catch ( Exception ex )
{
return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "lobby.manage_failed", "Lobby management operation failed.", ex ) );
}
}
private static readonly List<object> ActiveReplayFrames = new();
private static bool IsRecordingReplay = false;
/// <summary>
/// Records or plays back session gameplay state snapshots for testing host/client interactions,
/// inspecting physics trajectory deltas, and verifying player mechanics determinism.
/// </summary>
[McpTool( "pocketknife_replay_record" )]
public static string ReplayRecord(
[Description( "Action: 'start', 'snapshot_frame', 'stop', 'status'" )] string action = "status",
[Description( "Tag for the recorded session" )] string tag = "session_0" )
{
try
{
var normAction = action.ToLowerInvariant().Trim();
if ( normAction == "start" )
{
ActiveReplayFrames.Clear();
IsRecordingReplay = true;
return McpJson.Write( ToolOutcome<object>.Ready( new { Action = "start", Tag = tag, Recording = true, Timestamp = DateTime.UtcNow } ) );
}
if ( normAction == "snapshot_frame" && IsRecordingReplay )
{
var scene = ResolveActiveScene();
if ( scene != null )
{
var frameData = scene.GetAllObjects( false )
.Where( o => o.GetComponent<Rigidbody>() != null )
.Select( o => new
{
o.Name,
Position = o.WorldPosition,
Angles = o.WorldRotation.Angles(),
Velocity = o.GetComponent<Rigidbody>()!.Velocity
} )
.ToArray();
ActiveReplayFrames.Add( new { FrameIndex = ActiveReplayFrames.Count, Time = Sandbox.Time.Now, Objects = frameData } );
}
return McpJson.Write( ToolOutcome<object>.Ready( new { Action = "snapshot_frame", FrameCount = ActiveReplayFrames.Count } ) );
}
if ( normAction == "stop" )
{
IsRecordingReplay = false;
return McpJson.Write( ToolOutcome<object>.Ready( new
{
Action = "stop",
Tag = tag,
TotalFramesCaptured = ActiveReplayFrames.Count,
Recording = false
} ) );
}
return McpJson.Write( ToolOutcome<object>.Ready( new
{
Action = "status",
IsRecording = IsRecordingReplay,
CapturedFrames = ActiveReplayFrames.Count
} ) );
}
catch ( Exception ex )
{
return McpJson.Write( ToolOutcome<object>.Failure( null, ToolStatus.Failed, "replay.failed", "Replay operation failed.", ex ) );
}
}
}
}