Editor tool methods for creating, querying, modifying and deleting GameObject instances in the active scene. Implements MCP tool endpoints to spawn models, lights, cameras, set transforms/parents/enabled state, duplicate, find, select and return component details.
using System;
using System.Linq;
using Editor;
using Sandbox;
using SboxMcp.Registry;
using static SboxMcp.Tools.ToolHelpers;
namespace SboxMcp.Tools;
public static class GameObjectTools
{
[McpTool( "gameobject_create", "Creates a new GameObject in the active scene.", ToolCategory.GameObject, Writes = true )]
public static object Create(
string name,
[Desc( "Id of the parent GameObject; omit for scene root" )] string parentId = null,
[Desc( "World position [x, y, z]" )] float[] position = null,
[Desc( "Rotation [pitch, yaw, roll] in degrees" )] float[] rotation = null,
[Desc( "Scale [x, y, z]" )] float[] scale = null )
{
var session = RequireSession();
var parent = parentId is null ? null : FindGameObject( parentId );
using var undo = session.UndoScope( $"MCP: create {name}" ).WithGameObjectCreations().Push();
var go = session.Scene.CreateObject();
go.Name = string.IsNullOrWhiteSpace( name ) ? "GameObject" : name;
if ( parent is not null )
go.Parent = parent;
if ( position is not null )
go.WorldPosition = ToVector3( position, "position" );
if ( rotation is not null )
{
if ( rotation.Length != 3 )
throw new ArgumentException( "'rotation' must be [pitch, yaw, roll]" );
go.WorldRotation = Rotation.From( rotation[0], rotation[1], rotation[2] );
}
if ( scale is not null )
go.LocalScale = ToVector3( scale, "scale" );
return Describe( go );
}
[McpTool( "gameobject_spawn_model", "Spawns a prop in one step: creates a GameObject, adds a ModelRenderer with the given model, and optionally a matching ModelCollider so physics/traces hit it. The common 'place a model' operation (vs gameobject_create + component_add + component_set_property). Note: withCollider uses the model's own collision mesh - dev primitives like box.vmdl have none, so add a BoxCollider yourself for those.", ToolCategory.GameObject, Writes = true )]
public static object SpawnModel(
[Desc( "Model asset path, e.g. 'models/dev/box.vmdl'" )] string model,
[Desc( "Object name; defaults to the model's file name" )] string name = null,
[Desc( "World position [x, y, z]" )] float[] position = null,
[Desc( "Also add a ModelCollider (only solid if the model has a collision mesh)" )] bool withCollider = false )
{
if ( AssetSystem.FindByPath( model ) is null )
throw new InvalidOperationException( $"No model at '{model}' - use asset_search with assetType 'model'" );
var session = RequireSession();
using var undo = session.UndoScope( "MCP: spawn model" ).WithGameObjectCreations().Push();
var go = session.Scene.CreateObject();
go.Name = string.IsNullOrWhiteSpace( name ) ? System.IO.Path.GetFileNameWithoutExtension( model ) : name;
if ( position is not null )
go.WorldPosition = ToVector3( position, "position" );
var loaded = Model.Load( model );
go.Components.Create<ModelRenderer>().Model = loaded;
if ( withCollider )
go.Components.Create<ModelCollider>().Model = loaded;
return Describe( go );
}
[McpTool( "gameobject_spawn_light", "Spawns a light in one step: creates a GameObject with a PointLight, SpotLight, or DirectionalLight (optionally colored/aimed). Scenes need lighting - this is the one-call version.", ToolCategory.GameObject, Writes = true )]
public static object SpawnLight(
[Desc( "Light type: 'point', 'spot', or 'directional'" )] string lightType = "point",
[Desc( "Object name; defaults to the light type" )] string name = null,
[Desc( "World position [x, y, z]" )] float[] position = null,
[Desc( "Rotation [pitch, yaw, roll] - aims spot/directional lights" )] float[] rotation = null,
[Desc( "Light color [r, g, b] (0-1); omit for white" )] float[] color = null )
{
var session = RequireSession();
using var undo = session.UndoScope( "MCP: spawn light" ).WithGameObjectCreations().Push();
var go = session.Scene.CreateObject();
if ( position is not null )
go.WorldPosition = ToVector3( position, "position" );
if ( rotation is not null )
{
if ( rotation.Length != 3 )
throw new ArgumentException( "'rotation' must be [pitch, yaw, roll]" );
go.WorldRotation = Rotation.From( rotation[0], rotation[1], rotation[2] );
}
Light light = (lightType ?? "point").ToLowerInvariant() switch
{
"point" or "" => go.Components.Create<PointLight>(),
"spot" => go.Components.Create<SpotLight>(),
"directional" or "sun" or "dir" => go.Components.Create<DirectionalLight>(),
_ => throw new ArgumentException( "lightType must be 'point', 'spot' or 'directional'" )
};
go.Name = string.IsNullOrWhiteSpace( name ) ? light.GetType().Name : name;
if ( color is not null )
{
if ( color.Length is not (3 or 4) )
throw new ArgumentException( "'color' must be [r, g, b] or [r, g, b, a]" );
light.LightColor = new Color( color[0], color[1], color[2], color.Length > 3 ? color[3] : 1f );
}
return Describe( go );
}
[McpTool( "gameobject_spawn_camera", "Spawns a camera in one step: creates a GameObject with a CameraComponent, optionally positioned/aimed with a field of view. Every scene needs a camera to render in play mode.", ToolCategory.GameObject, Writes = true )]
public static object SpawnCamera(
[Desc( "Object name" )] string name = "Camera",
[Desc( "World position [x, y, z]" )] float[] position = null,
[Desc( "Rotation [pitch, yaw, roll] - where the camera looks" )] float[] rotation = null,
[Desc( "Field of view in degrees (default 60)" )] float fieldOfView = 60f )
{
var session = RequireSession();
using var undo = session.UndoScope( "MCP: spawn camera" ).WithGameObjectCreations().Push();
var go = session.Scene.CreateObject();
go.Name = string.IsNullOrWhiteSpace( name ) ? "Camera" : name;
if ( position is not null )
go.WorldPosition = ToVector3( position, "position" );
if ( rotation is not null )
{
if ( rotation.Length != 3 )
throw new ArgumentException( "'rotation' must be [pitch, yaw, roll]" );
go.WorldRotation = Rotation.From( rotation[0], rotation[1], rotation[2] );
}
go.Components.Create<CameraComponent>().FieldOfView = fieldOfView;
return Describe( go );
}
[McpTool( "gameobject_delete", "Deletes a GameObject (and its children).", ToolCategory.GameObject, Writes = true )]
public static object Delete( [Desc( "GameObject id or unique name" )] string gameObject )
{
var session = RequireSession();
var go = FindGameObject( gameObject );
var name = go.Name;
using var undo = session.UndoScope( $"MCP: delete {name}" )
.WithGameObjectDestructions( new[] { go } ).Push();
go.Destroy();
return new { deleted = name };
}
[McpTool( "gameobject_rename", "Renames a GameObject.", ToolCategory.GameObject, Writes = true )]
public static object Rename( [Desc( "GameObject id or unique name" )] string gameObject, string newName )
{
var session = RequireSession();
var go = FindGameObject( gameObject );
using var undo = session.UndoScope( $"MCP: rename to {newName}" )
.WithGameObjectChanges( go, GameObjectUndoFlags.Properties ).Push();
go.Name = newName;
return Describe( go );
}
[McpTool( "gameobject_set_enabled", "Enables or disables a GameObject.", ToolCategory.GameObject, Writes = true )]
public static object SetEnabled( [Desc( "GameObject id or unique name" )] string gameObject, bool enabled )
{
var session = RequireSession();
var go = FindGameObject( gameObject );
using var undo = session.UndoScope( $"MCP: set enabled {enabled}" )
.WithGameObjectChanges( go, GameObjectUndoFlags.Properties ).Push();
go.Enabled = enabled;
return Describe( go );
}
[McpTool( "gameobject_set_parent", "Reparents a GameObject (keeps world position).", ToolCategory.GameObject, Writes = true )]
public static object SetParent(
[Desc( "GameObject id or unique name" )] string gameObject,
[Desc( "New parent id; omit to move to scene root" )] string parentId = null )
{
var session = RequireSession();
var go = FindGameObject( gameObject );
var parent = parentId is null ? (GameObject)session.Scene : FindGameObject( parentId );
using var undo = session.UndoScope( "MCP: reparent" )
.WithGameObjectChanges( go, GameObjectUndoFlags.All ).Push();
go.SetParent( parent, keepWorldPosition: true );
return Describe( go );
}
[McpTool( "gameobject_get_transform", "Gets a GameObject's world and local transform.", ToolCategory.GameObject )]
public static object GetTransform( [Desc( "GameObject id or unique name" )] string gameObject )
{
var go = FindGameObject( gameObject );
return new
{
id = go.Id,
name = go.Name,
world = new { position = V( go.WorldPosition ), rotation = A( go.WorldRotation ), scale = V( go.WorldScale ) },
local = new { position = V( go.LocalPosition ), rotation = A( go.LocalRotation ), scale = V( go.LocalScale ) }
};
}
[McpTool( "gameobject_set_transform", "Sets position/rotation/scale on a GameObject. Omitted parts stay unchanged.", ToolCategory.GameObject, Writes = true )]
public static object SetTransform(
[Desc( "GameObject id or unique name" )] string gameObject,
[Desc( "Position [x, y, z]" )] float[] position = null,
[Desc( "Rotation [pitch, yaw, roll] in degrees" )] float[] rotation = null,
[Desc( "Scale [x, y, z]" )] float[] scale = null,
[Desc( "Apply in world space instead of local space" )] bool world = false )
{
var session = RequireSession();
var go = FindGameObject( gameObject );
using var undo = session.UndoScope( "MCP: set transform" )
.WithGameObjectChanges( go, GameObjectUndoFlags.Properties ).Push();
if ( position is not null )
{
var v = ToVector3( position, "position" );
if ( world ) go.WorldPosition = v; else go.LocalPosition = v;
}
if ( rotation is not null )
{
if ( rotation.Length != 3 )
throw new ArgumentException( "'rotation' must be [pitch, yaw, roll]" );
var r = Rotation.From( rotation[0], rotation[1], rotation[2] );
if ( world ) go.WorldRotation = r; else go.LocalRotation = r;
}
if ( scale is not null )
go.LocalScale = ToVector3( scale, "scale" );
return GetTransform( go.Id.ToString() );
}
[McpTool( "gameobject_duplicate", "Duplicates a GameObject next to the original.", ToolCategory.GameObject, Writes = true )]
public static object Duplicate( [Desc( "GameObject id or unique name" )] string gameObject )
{
var session = RequireSession();
var go = FindGameObject( gameObject );
using var undo = session.UndoScope( $"MCP: duplicate {go.Name}" ).WithGameObjectCreations().Push();
var clone = go.Clone( go.WorldTransform, go.Parent, go.Enabled, $"{go.Name} (copy)" );
return Describe( clone );
}
[McpTool( "gameobject_find", "Searches GameObjects by name substring, component type, and/or tag.", ToolCategory.GameObject )]
public static object Find(
[Desc( "Name substring (case-insensitive); omit to match all" )] string query = null,
[Desc( "Only objects having this component type" )] string componentType = null,
[Desc( "Only objects carrying this tag" )] string tag = null,
int max = 50 )
{
var scene = RequireScene();
var results = scene.GetAllObjects( false )
.Where( o => o is not Scene )
.Where( o => query is null || o.Name.Contains( query, StringComparison.OrdinalIgnoreCase ) )
.Where( o => tag is null || o.Tags.Has( tag ) )
.Where( o => componentType is null || o.Components.GetAll<Component>( FindMode.EverythingInSelf )
.Any( c => string.Equals( c.GetType().Name, componentType, StringComparison.OrdinalIgnoreCase )
|| string.Equals( c.GetType().FullName, componentType, StringComparison.OrdinalIgnoreCase ) ) )
.Take( max )
.Select( Describe )
.ToArray();
return new { count = results.Length, results };
}
[McpTool( "gameobject_get_details", "Gets a GameObject with all component properties as JSON.", ToolCategory.GameObject )]
public static object GetDetails( [Desc( "GameObject id or unique name" )] string gameObject )
{
var go = FindGameObject( gameObject );
return new
{
id = go.Id,
name = go.Name,
enabled = go.Enabled,
tags = go.Tags.TryGetAll().ToArray(),
world = new { position = V( go.WorldPosition ), rotation = A( go.WorldRotation ), scale = V( go.WorldScale ) },
parent = go.Parent is Scene ? null : (object)new { id = go.Parent?.Id, name = go.Parent?.Name },
isPrefabInstance = go.IsPrefabInstance,
prefabSource = go.PrefabInstanceSource,
components = go.Components.GetAll<Component>( FindMode.EverythingInSelf )
.Select( c => new
{
type = c.GetType().Name,
enabled = c.Enabled,
properties = c.Serialize()
} ).ToArray()
};
}
[McpTool( "gameobject_select", "Selects GameObjects in the editor (replaces current selection).", ToolCategory.GameObject )]
public static object Select( [Desc( "GameObject ids or unique names" )] string[] gameObjects )
{
var session = RequireSession();
var found = gameObjects.Select( FindGameObject ).ToList();
session.Selection.Clear();
foreach ( var go in found )
session.Selection.Add( go );
return new { selected = found.Select( g => g.Name ).ToArray() };
}
}