MCP toolset for the s&box editor that exposes library management and publish operations to the MCP server. It lists published versions, installs/uninstalls libraries in Libraries/, and runs the Publish wizard steps (compile, gather files, upload, publish).
using Editor;
using Editor.Mcp;
using Sandbox;
using System.Linq;
using System.Threading.Tasks;
namespace LibraryTools;
/// <summary>
/// The Library Manager and the Publish wizard, exposed to the s&box editor's MCP server, so scripts and AI agents
/// can list, install, remove and publish sbox.game libraries the same way the editor's own buttons do.
/// </summary>
[McpToolset( "libraries", "Library Manager from MCP: list versions, install, uninstall and publish sbox.game libraries" )]
public static class LibraryTools
{
// Installed libraries report idents like "rwtrcsc.lobbysystem#local"; compare the part before the #.
static bool Matches( LibraryProject lib, string ident ) => (lib.Project?.Package?.FullIdent ?? "").Split( '#' )[0] == ident;
/// <summary>Published versions of a library on sbox.game (newest first) and the version installed here, if any.</summary>
/// <param name="ident">Full package ident, e.g. 'rwtrcsc.lobbysystem'.</param>
[McpTool.ReadOnly( "library_versions" )]
public static async Task<string> LibraryVersions( string ident )
{
var versions = await Package.FetchVersions( ident, default );
var installed = LibrarySystem.All.FirstOrDefault( x => Matches( x, ident ) );
var lines = (versions ?? Enumerable.Empty<Package.IRevision>())
.Select( v => $"{v.VersionId} {v.Created:yyyy-MM-dd HH:mm} files={v.FileCount} {v.Summary}" );
return $"installed: {(installed is null ? "no" : installed.Version?.ToString() ?? "local")}\n" + string.Join( "\n", lines );
}
/// <summary>Install a published library version into Libraries/ (newest when versionId is 0).</summary>
/// <param name="ident">Full package ident, e.g. 'rwtrcsc.lobbysystem'.</param>
/// <param name="versionId">A version id from library_versions, or 0 for the newest.</param>
[McpTool( "library_install" )]
public static async Task<string> LibraryInstall( string ident, long versionId = 0 )
{
var versions = (await Package.FetchVersions( ident, default ))?.ToList();
var v = versionId == 0 ? versions?.FirstOrDefault() : versions?.FirstOrDefault( x => x.VersionId == versionId );
if ( v is null ) return $"No published version found for {ident}.";
await LibrarySystem.Install( ident, v.VersionId, default );
return $"Installed {ident} version {v.VersionId} (published {v.Created:yyyy-MM-dd}).";
}
/// <summary>Remove an installed library and delete its folder, as the Library Manager's uninstall button does.</summary>
/// <param name="ident">Full package ident, e.g. 'rwtrcsc.lobbysystem'.</param>
[McpTool( "library_uninstall" )]
public static string LibraryUninstall( string ident )
{
var lib = LibrarySystem.All.FirstOrDefault( x => Matches( x, ident ) );
if ( lib is null )
return $"{ident} is not installed. Installed: " + string.Join( ", ", LibrarySystem.All.Select( x => x.Project?.Package?.FullIdent ) );
lib.RemoveAndDelete();
return $"Removed {ident}.";
}
/// <summary>
/// Publish a library from Libraries/ as a new revision on sbox.game, running the Publish wizard's own steps:
/// compile, gather files, upload, publish. Visibility is not set here: a new package starts Hidden (org members only)
/// and you switch it to Released on its sbox.game Edit page.
/// </summary>
/// <param name="ident">Full package ident, e.g. 'rwtrcsc.combatsystem'.</param>
/// <param name="changeTitle">Revision title shown on the package page.</param>
[McpTool( "library_publish" )]
public static async Task<string> LibraryPublish( string ident, string changeTitle = "" )
{
var project = LibrarySystem.All.FirstOrDefault( x => Matches( x, ident ) )?.Project;
if ( project is null ) return $"{ident} is not in Libraries/.";
if ( project.Config.Org == "local" ) return "Set an organisation in the .sbproj first.";
var publisher = await ProjectPublisher.FromProject( project );
// Same as the Publish wizard: compile only when there is game code (editor-only libraries ship as source).
if ( project.HasCodePath() )
{
var output = await EditorUtility.Projects.Compile( project, _ => { } );
if ( output is null || output.Length == 0 ) return "Compile produced no code.";
await publisher.AddFile( System.Text.Json.JsonSerializer.Serialize( output.Select( x => x.Compiler.AssemblyName ).ToList() ), ".bin/manifest.json" );
foreach ( var asm in output )
{
await publisher.AddFile( asm.XmlDocumentation, $".bin/{asm.Compiler.AssemblyName}.xml" );
await publisher.AddFile( asm.Archive.Serialize(), $".bin/{asm.Compiler.AssemblyName}.cll" );
}
}
await publisher.PrePublish();
foreach ( var file in publisher.Files.Where( x => !x.Skip ).ToList() )
{
bool ok = file.Contents is not null
? await project.Package.UploadFile( file.Contents, file.Name, _ => { }, default )
: file.AbsolutePath is not null && await project.Package.UploadFile( file.AbsolutePath, file.Name, _ => { }, default );
if ( ok ) file.Skip = true;
}
var failed = publisher.Files.Where( x => !x.Skip ).Select( x => x.Name ).ToList();
if ( failed.Count > 0 ) return "Upload failed for: " + string.Join( ", ", failed );
publisher.SetChangeDetails( string.IsNullOrWhiteSpace( changeTitle ) ? $"Changes on {System.DateTime.UtcNow:yyyy-MM-dd}" : changeTitle, "" );
await publisher.Publish();
return $"Published {ident} ({publisher.Files.Count()} files). Visibility is unchanged: new packages start Hidden; release them on sbox.game.";
}
}