Editor/McpServerWindow.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using Editor;
using Sandbox;
namespace SboxMcpServer;
public class McpServerWindow : Widget
{
/// <summary>Resolves a path relative to the library root using the source file location at compile time.</summary>
private static string GetAssetPath( string folder, string file, [CallerFilePath] string sourceFile = "" )
{
var libRoot = Path.GetFullPath( Path.Combine( Path.GetDirectoryName( sourceFile ), ".." ) );
return Path.Combine( libRoot, folder, file );
}
private Label _statusLabel;
private Label _portLabel;
private Label _sessionCountLabel;
private Button _toggleButton;
private Widget _logCanvas;
private ToggleSwitch _autoStartToggle;
private static readonly List<string> _logEntries = new();
private const int MaxLogEntries = 200;
[ConVar( "mcp_server_autostart", ConVarFlags.Saved )]
public static bool AutoStart { get; set; } = true;
// Open via the Editor menu
[Menu( "Editor", "MCP/Open MCP Panel" )]
public static void OpenPanel()
{
var win = new McpServerWindow();
win.Show();
}
// Static constructor runs once when the library assembly is loaded by the editor.
static McpServerWindow()
{
if ( AutoStart && !McpServer.IsRunning )
{
McpServer.StartServer();
}
}
public McpServerWindow() : base( null )
{
WindowTitle = "MCP Server";
MinimumSize = new Vector2( 500, 400 );
McpServer.OnServerStateChanged += OnStateChanged;
McpServer.OnLogMessage += OnLogMessage;
BuildUI();
OnStateChanged();
}
public override void OnDestroyed()
{
McpServer.OnServerStateChanged -= OnStateChanged;
McpServer.OnLogMessage -= OnLogMessage;
base.OnDestroyed();
}
private void BuildUI()
{
var root = Layout.Column();
root.Margin = 8;
root.Spacing = 6;
// ── Logo Header ────────────────────────────────────────────────
var logo = new Widget();
try
{
// Resolve the logo path from the source file location (Assembly.Location is null in S&box)
var logoPath = GetAssetPath( "Image", "Logo.jpg" ).Replace( '\\', '/' );
logo.SetStyles( $"background-image: url('{logoPath}'); background-position: center; background-repeat: no-repeat; min-height: 80px; margin-bottom: 8px;" );
}
catch { }
root.Add( logo );
// ── Status Row ─────────────────────────────────────────────────
var statusRow = Layout.Row();
statusRow.Spacing = 16;
var statusCol = Layout.Column();
var statusTitle = new Label( "STATUS" );
statusTitle.SetStyles( "font-size: 10px; color: #888;" );
statusCol.Add( statusTitle );
_statusLabel = new Label( "● Stopped" );
_statusLabel.SetStyles( "font-size: 15px; font-weight: bold; color: #f87171;" );
statusCol.Add( _statusLabel );
statusRow.Add( statusCol );
var portCol = Layout.Column();
var portTitle = new Label( "PORT" );
portTitle.SetStyles( "font-size: 10px; color: #888;" );
portCol.Add( portTitle );
_portLabel = new Label( McpServer.Port.ToString() );
_portLabel.SetStyles( "font-size: 15px; font-weight: bold;" );
portCol.Add( _portLabel );
statusRow.Add( portCol );
var sessionCol = Layout.Column();
var sessionTitle = new Label( "SESSIONS" );
sessionTitle.SetStyles( "font-size: 10px; color: #888;" );
sessionCol.Add( sessionTitle );
_sessionCountLabel = new Label( "0" );
_sessionCountLabel.SetStyles( "font-size: 15px; font-weight: bold;" );
sessionCol.Add( _sessionCountLabel );
statusRow.Add( sessionCol );
statusRow.AddStretchCell();
_toggleButton = new Button( "Start MCP Server", "play_arrow" );
_toggleButton.Clicked += ToggleServer;
statusRow.Add( _toggleButton );
root.Add( statusRow );
// ── Auto-start Row ─────────────────────────────────────────────
var autoRow = Layout.Row();
autoRow.Spacing = 8;
_autoStartToggle = new ToggleSwitch( "Auto-start MCP Server on editor load" );
_autoStartToggle.Value = AutoStart;
_autoStartToggle.ToolTip = $"mcp_server_autostart (default: true). Change takes effect on next editor launch.";
_autoStartToggle.MouseClick += () =>
{
AutoStart = _autoStartToggle.Value;
};
autoRow.Add( _autoStartToggle );
root.Add( autoRow );
root.AddSeparator();
// ── Log Header ─────────────────────────────────────────────────
var logHeader = Layout.Row();
var logTitle = new Label( "Activity Log" );
logTitle.SetStyles( "font-weight: bold; font-size: 13px;" );
logHeader.Add( logTitle );
logHeader.AddStretchCell();
var clearBtn = new Button( "", "delete_sweep" );
clearBtn.ToolTip = "Clear Log";
clearBtn.FixedWidth = 26;
clearBtn.FixedHeight = 26;
clearBtn.Clicked += () => { _logEntries.Clear(); _logCanvas.DestroyChildren(); };
logHeader.Add( clearBtn );
root.Add( logHeader );
// ── Log Area ───────────────────────────────────────────────────
var scroll = new ScrollArea( null );
scroll.MinimumHeight = 250;
_logCanvas = new Widget();
_logCanvas.Layout = Layout.Column();
scroll.Canvas = _logCanvas;
foreach ( var entry in _logEntries )
{
AddLogLabel( entry );
}
root.Add( scroll, 1 );
Layout = root;
}
private void AddLogLabel( string text )
{
var lbl = new Label( text );
lbl.WordWrap = true;
string color = "#e5e7eb";
string weight = "normal";
if ( text.Contains( "[ERROR]" ) )
{
color = "#f87171";
weight = "bold";
}
else if ( text.Contains( "] Tool: " ) )
{
color = "#60a5fa";
weight = "bold";
}
else if ( text.Contains( "Started" ) || text.Contains( "Created new MCP" ) || text.Contains( "initialized" ) )
{
color = "#4ade80";
}
else if ( text.Contains( "] Waiting for" ) || text.Contains( "] Resumed on" ) || text.Contains( "Closed MCP" ) || text.Contains( "Stopped" ) )
{
color = "#9ca3af";
}
lbl.SetStyles( $"font-family: monospace; font-size: 11px; padding: 2px; color: {color}; font-weight: {weight};" );
_logCanvas.Layout.Add( lbl );
}
private void ToggleServer()
{
if ( McpServer.IsRunning )
McpServer.StopServer();
else
McpServer.StartServer();
}
private void OnStateChanged()
{
if ( !IsValid ) return;
_portLabel.Text = McpServer.Port.ToString();
_sessionCountLabel.Text = McpServer.SessionCount.ToString();
if ( McpServer.IsRunning )
{
_statusLabel.Text = "● Running";
_statusLabel.SetStyles( "font-size: 15px; font-weight: bold; color: #4ade80;" );
_toggleButton.Text = "Stop MCP Server";
}
else
{
_statusLabel.Text = "● Stopped";
_statusLabel.SetStyles( "font-size: 15px; font-weight: bold; color: #f87171;" );
_toggleButton.Text = "Start MCP Server";
}
}
private void OnLogMessage( string message )
{
if ( !IsValid ) return;
var text = $"[{DateTime.Now:HH:mm:ss}] {message}";
_logEntries.Add( text );
AddLogLabel( text );
if ( _logEntries.Count > MaxLogEntries )
{
_logEntries.RemoveAt( 0 );
var firstChild = _logCanvas.Children.FirstOrDefault();
firstChild?.Destroy();
}
}
}