Editor/ConsoleToolHandlers.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Sandbox;
namespace SboxMcpServer;
/// <summary>
/// Handlers for console-related MCP tools: list_console_commands and run_console_command.
/// </summary>
internal static class ConsoleToolHandlers
{
// ── list_console_commands ──────────────────────────────────────────────
internal static object ListConsoleCommands( JsonElement args, JsonSerializerOptions jsonOptions )
{
string filter = null;
if ( args.ValueKind != JsonValueKind.Undefined &&
args.TryGetProperty( "filter", out var fP ) )
filter = fP.GetString();
var entries = new List<Dictionary<string, object>>();
var skippedAssemblies = new List<string>();
foreach ( var assembly in AppDomain.CurrentDomain.GetAssemblies() )
{
try
{
foreach ( var type in assembly.GetTypes() )
{
foreach ( var prop in type.GetProperties(
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Static ) )
{
var attr = prop.GetCustomAttributes( typeof( ConVarAttribute ), false )
.FirstOrDefault() as ConVarAttribute;
if ( attr == null ) continue;
var cvarName = !string.IsNullOrEmpty( attr.Name )
? attr.Name
: prop.Name.ToLowerInvariant();
if ( !string.IsNullOrEmpty( filter ) &&
cvarName.IndexOf( filter, StringComparison.OrdinalIgnoreCase ) < 0 )
continue;
string currentValue = null;
try { currentValue = ConsoleSystem.GetValue( cvarName ); } catch { }
entries.Add( new Dictionary<string, object>
{
["name"] = cvarName,
["help"] = attr.Help ?? "",
["flags"] = attr.Flags.ToString(),
["saved"] = attr.Saved,
["currentValue"] = currentValue,
["declaringType"]= type.Name
} );
}
}
}
catch ( Exception ex )
{
skippedAssemblies.Add( $"{assembly.GetName().Name}: {ex.Message}" );
}
}
entries = entries
.GroupBy( e => e["name"]?.ToString() )
.Select( g => g.First() )
.OrderBy( e => e["name"]?.ToString() )
.ToList();
var summary = $"Found {entries.Count} [ConVar] entries" +
( string.IsNullOrEmpty( filter ) ? "." : $" matching '{filter}'." );
if ( skippedAssemblies.Count > 0 )
summary += $" ({skippedAssemblies.Count} assemblies skipped due to reflection errors.)";
var json = JsonSerializer.Serialize( new { summary, entries, skippedAssemblies }, jsonOptions );
return ToolHandlerBase.TextResult( json );
}
// ── run_console_command ────────────────────────────────────────────────
internal static object RunConsoleCommand( JsonElement args )
{
var cmd = args.GetProperty( "command" ).GetString();
var parts = cmd.Trim().Split( ' ', StringSplitOptions.RemoveEmptyEntries );
var cmdName = parts[0];
// Only support convars (readable via GetValue). ConCmd methods and unknown
// commands are rejected with a friendly message — ConsoleSystem.Run throws
// uncatchable exceptions in s&box's sandbox for both cases.
string currentValue = null;
try { currentValue = ConsoleSystem.GetValue( cmdName ); } catch { }
if ( currentValue == null )
return ToolHandlerBase.TextResult( $"Unknown convar: '{cmdName}'. Only [ConVar] properties are supported. Use list_console_commands to see available names." );
// Read-only query (no value argument) — just return the current value.
if ( parts.Length == 1 )
return ToolHandlerBase.TextResult( $"{cmdName} = {currentValue}" );
// Write: set the convar value using SetValue (no main-thread restriction).
var newValue = string.Join( " ", parts, 1, parts.Length - 1 );
ConsoleSystem.SetValue( cmdName, newValue );
string readback = null;
try { readback = ConsoleSystem.GetValue( cmdName ); } catch { }
return ToolHandlerBase.TextResult( $"Set {cmdName} = {readback ?? newValue}" );
}
}