Editor/Mcp/BridgeNetworkingTools.cs

Editor toolset wrapper for networking-related MCP commands. It declares static methods that call McpGate.Run to emit or configure networking helper scripts, RPCs, sync properties, lobby managers, spawn/ownership actions and status queries.

File AccessNetworking
// AUTO-GENERATED by scripts/emit-mcp-wrappers.mjs — DO NOT EDIT.
// Regenerate: node scripts/extract-manifest.mjs && node scripts/emit-mcp-wrappers.mjs
// Source of truth: sbox-mcp-server/src/tools/ (zod schemas) → scripts/tools-manifest.json

using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Editor.Mcp;

/// <summary>
/// Multiplayer setup and codegen: network helpers, lobby managers, networked players, [Sync]
/// properties, RPCs (broadcast/host/targeted), ownership, spawning, and host-migration recovery.
/// </summary>
[McpToolset( "bridge_networking", "Multiplayer setup and codegen: network helpers, lobby managers, networked players, [Sync] properties, RPCs (broadcast/host/targeted), ownership, spawning, and host-migration recovery." )]
public static class BridgeNetworkingTools
{
	/// <summary>
	/// Generate a host-migration recovery component — it detects when THIS machine takes authority over
	/// its GameObject (the proxy→authority transition that promotes a client to host when the old host
	/// leaves) and gives you a clean hook to rebuild host-only state. It tracks IsProxy each frame;
	/// when it flips from true (someone else was the authority) to false (now it's us), it fires the
	/// static OnBecameHost(GameObject) event and runs a virtual-style TODO rebuild region, then — after
	/// a tunable SettleSeconds delay so in-flight packets can land — runs a deferred validation region.
	/// s&amp;box has a known bug where networked objects are often destroyed during host migration and
	/// the new host inherits stale transient state, so the recommended shape is: detect becoming host,
	/// aggressively rebuild (re-arm host-only loops/TimeUntil timers against your clock, TakeOwnership
	/// of orphans, rebuild handle maps by world position, reconcile the [Sync] registry against the
	/// real scene), and defer the sanity check ~1s. Inert offline (IsProxy is always false with no
	/// session). Attach to your host-authoritative manager object (NetworkSpawn'd). Next steps:
	/// hotload, attach, subscribe to OnBecameHost, and fill the two TODO regions. Requires a real host
	/// migration (a second client that becomes host) to fire — it can't be exercised in a solo
	/// playtest. Optionally attached to an existing GameObject by GUID (after a hotload).
	/// </summary>
	/// <param name="name">Class name for the generated component. Defaults to 'HostMigrationRecovery'.</param>
	/// <param name="directory">Subdirectory for the generated .cs file. Defaults to 'Code'.</param>
	/// <param name="targetId">GUID of a GameObject to attach to (only attaches if the type is already loaded — hotload first).</param>
	[McpTool( "add_host_migration_recovery" )]
	public static Task<object> AddHostMigrationRecovery( string name = null, string directory = null, string targetId = null )
		=> McpGate.Run( "add_host_migration_recovery", McpGate.Args( ( "name", name ), ( "directory", directory ), ( "targetId", targetId ) ) );

	/// <summary>
	/// Add a NetworkHelper component (with StartServer=true) to an existing GameObject for quick
	/// multiplayer setup — at runtime it creates the lobby and spawns the player prefab per connection.
	/// Returns { added, id, component:'NetworkHelper' }. NOTE: the current handler requires id (create
	/// a holder with create_gameobject first) and does not apply maxPlayers/playerPrefab — wire
	/// PlayerPrefab afterward with set_prefab_ref/set_property.
	/// </summary>
	/// <param name="id">GUID of the GameObject to attach to. Required in practice — the current handler errors when omitted (create a holder with create_gameobject first).</param>
	/// <param name="name">Rename the target GameObject to this. Omit to keep its current name.</param>
	/// <param name="maxPlayers">Maximum number of players in the lobby (currently not applied by the handler).</param>
	/// <param name="playerPrefab">Path to the player prefab to spawn for each connection (currently not applied by the handler — set the NetworkHelper's PlayerPrefab afterward with set_prefab_ref).</param>
	[McpTool( "add_network_helper" )]
	public static Task<object> AddNetworkHelper( string id = null, string name = null, double? maxPlayers = null, string playerPrefab = null )
		=> McpGate.Run( "add_network_helper", McpGate.Args( ( "id", id ), ( "name", name ), ( "maxPlayers", maxPlayers ), ( "playerPrefab", playerPrefab ) ) );

	/// <summary>
	/// Generate an RPC method stub in a C# script. Inserts the chosen RPC attribute ([Rpc.Broadcast]
	/// all clients, [Rpc.Host] host only, [Rpc.Owner] owner only) above a method with an empty body.
	/// Pass methodParams to give it a parameter list (e.g. 'Vector3 pos, int damage'); the body is left
	/// as a TODO for you to fill in.
	/// </summary>
	/// <param name="path">Relative path to the script file.</param>
	/// <param name="methodName">Name for the RPC method.</param>
	/// <param name="rpcType">RPC type: Broadcast (all), Host (server), Owner (owning client). Defaults to Broadcast. One of: Broadcast | Host | Owner.</param>
	/// <param name="methodParams">Optional parameter list for the RPC method signature, e.g. 'Vector3 pos, int damage'. Omit for a parameterless method.</param>
	/// <param name="body">Currently ignored — not yet implemented. The addon emits an empty method body; fill it in yourself afterward.</param>
	[McpTool( "add_rpc_method" )]
	public static Task<object> AddRpcMethod( string path, string methodName, string rpcType = null, string methodParams = null, string body = null )
		=> McpGate.Run( "add_rpc_method", McpGate.Args( ( "path", path ), ( "methodName", methodName ), ( "rpcType", rpcType ), ( "methodParams", methodParams ), ( "body", body ) ) );

	/// <summary>
	/// Annotate an EXISTING public property in a C# script with the [Sync] attribute so s&amp;box
	/// replicates it across the network. This does NOT create a new property — the property named by
	/// `propertyName` must already be declared in the file; the tool only inserts the [Sync] attribute
	/// above it. Returns { added, path, property, attribute } — attribute echoes the exact [Sync...]
	/// emitted; errors if the property already has [Sync] or isn't found. Follow with trigger_hotload,
	/// then get_compile_errors.
	/// </summary>
	/// <param name="path">Relative path to the script file (e.g. 'code/Player.cs').</param>
	/// <param name="propertyName">Name of the existing public property to annotate with [Sync].</param>
	/// <param name="propertyType">Currently ignored — not yet implemented. The addon only annotates an existing property; it does not declare a new one, so the type comes from the existing declaration.</param>
	/// <param name="syncFlags">Optional SyncFlags to emit as [Sync( SyncFlags.X )] — e.g. 'Interpolate' (smooth interpolation), 'Query', 'FromHost'. Omit for a plain [Sync].</param>
	/// <param name="defaultValue">Currently ignored — not yet implemented. The addon does not create or initialize a property, so no default is written.</param>
	[McpTool( "add_sync_property" )]
	public static Task<object> AddSyncProperty( string path, string propertyName, string propertyType = null, string syncFlags = null, string defaultValue = null )
		=> McpGate.Run( "add_sync_property", McpGate.Args( ( "path", path ), ( "propertyName", propertyName ), ( "propertyType", propertyType ), ( "syncFlags", syncFlags ), ( "defaultValue", defaultValue ) ) );

	/// <summary>
	/// Generate a component demonstrating the unicast (single-client) RPC pattern via
	/// Rpc.FilterInclude. A normal [Rpc.Broadcast] runs on EVERY machine; the generated component's
	/// host-side SendTo(Connection target, string message) wraps its [Rpc.Broadcast] call in `using (
	/// Rpc.FilterInclude( target ) )` so ONLY the target connection executes the body, which raises the
	/// static OnReceived(string) event. This is the RIGHT way to send something to one player — a
	/// private prompt, a personal reward toast, a per-player cutscene cue — instead of broadcasting to
	/// everyone and filtering on the client (which leaks data and wastes bandwidth). Call SendTo on the
	/// host (guarded behind Networking.IsActive so Networking.IsHost can't throw with no session; runs
	/// locally in solo). Attach to a networked manager object that is NetworkSpawn'd. Next steps:
	/// hotload, attach, call SendTo(player.Network.Owner, "...") from the host, and subscribe to
	/// OnReceived on the target client. Optionally attached to an existing GameObject by GUID (after a
	/// hotload).
	/// </summary>
	/// <param name="name">Class name for the generated component. Defaults to 'TargetedRpc'.</param>
	/// <param name="directory">Subdirectory for the generated .cs file. Defaults to 'Code'.</param>
	/// <param name="targetId">GUID of a GameObject to attach to (only attaches if the type is already loaded — hotload first).</param>
	[McpTool( "add_targeted_rpc" )]
	public static Task<object> AddTargetedRpc( string name = null, string directory = null, string targetId = null )
		=> McpGate.Run( "add_targeted_rpc", McpGate.Args( ( "name", name ), ( "directory", directory ), ( "targetId", targetId ) ) );

	/// <summary>
	/// Configure lobby settings on Sandbox.Networking. Currently only lobbyName is applied (sets
	/// Networking.ServerName) — Networking.MaxPlayers is read-only on this SDK, and
	/// playerPrefab/startServer are not applied by the handler (use add_network_helper + set_prefab_ref
	/// for those). Returns { configured, maxPlayers, serverName } reflecting the live Networking
	/// values.
	/// </summary>
	/// <param name="maxPlayers">Maximum number of players (currently not applied — Networking.MaxPlayers is read-only on this SDK; the live value is echoed in the response).</param>
	/// <param name="lobbyName">Display name for the lobby (sets Networking.ServerName — the only setting this handler applies).</param>
	/// <param name="playerPrefab">Path to the player prefab (currently not applied by the handler — set the NetworkHelper's PlayerPrefab via set_prefab_ref instead).</param>
	/// <param name="startServer">Start the server/lobby immediately (currently not applied by the handler — add_network_helper sets StartServer=true on the NetworkHelper).</param>
	[McpTool( "configure_network" )]
	public static Task<object> ConfigureNetwork( double? maxPlayers = null, string lobbyName = null, string playerPrefab = null, bool? startServer = null )
		=> McpGate.Run( "configure_network", McpGate.Args( ( "maxPlayers", maxPlayers ), ( "lobbyName", lobbyName ), ( "playerPrefab", playerPrefab ), ( "startServer", startServer ) ) );

	/// <summary>
	/// Generate a validated, rate-limited host-action component — the SAFE skeleton for 'a client asks
	/// the host to DO something' (buy, use, vote, interact, spend). The generated sealed Component
	/// exposes a client-callable Request() that forwards to an [Rpc.Host] SubmitRequest() which runs ON
	/// THE HOST: it re-resolves WHO called it via Rpc.Caller (never trusting client args for identity),
	/// enforces a per-SteamId cooldown backed by a Dictionary&lt;ulong, TimeSince&gt;, runs a
	/// clearly-marked TODO block for your authoritative action, and fires the static
	/// OnActionExecuted(Connection) event. This is the correct answer to the #1 multiplayer exploit
	/// class: [Rpc.Host] is callable by ANY client with forged args — NetFlags restrict who may INVOKE,
	/// which is not security — so identity + rate-limit + validation all live inside the host body.
	/// Single-player safe (no session → the RPC runs locally, caller falls back to Connection.Local).
	/// Attach it to the object that owns the action (a player, a station, or your game manager). Next
	/// steps: hotload, tag/attach, call Request() from input or a UI button, fill the TODO host block,
	/// and subscribe to OnActionExecuted. Covers the backlog's add_rate_limited_rpc. Optionally
	/// attached to an existing GameObject by GUID (after a hotload).
	/// </summary>
	/// <param name="name">Class name for the generated component. Defaults to 'HostRpcAction'.</param>
	/// <param name="directory">Subdirectory for the generated .cs file. Defaults to 'Code'.</param>
	/// <param name="cooldownSeconds">Minimum seconds between accepted requests, per calling player (the per-SteamId rate limit). Defaults to 1.</param>
	/// <param name="targetId">GUID of a GameObject to attach to (only attaches if the type is already loaded — hotload first).</param>
	[McpTool( "create_host_rpc_action" )]
	public static Task<object> CreateHostRpcAction( string name = null, string directory = null, double? cooldownSeconds = null, string targetId = null )
		=> McpGate.Run( "create_host_rpc_action", McpGate.Args( ( "name", name ), ( "directory", directory ), ( "cooldownSeconds", cooldownSeconds ), ( "targetId", targetId ) ) );

	/// <summary>
	/// Generate a lobby manager Component implementing Component.INetworkListener: a static Instance
	/// singleton, a [Sync] PlayerCount maintained in OnActive/OnDisconnected, and a LobbyState
	/// [Property] that flips to 'playing' when the lobby fills. Writes &lt;name&gt;.cs and returns {
	/// created, path, className }. Follow with trigger_hotload, then get_compile_errors, then place via
	/// add_component_to_new_object.
	/// </summary>
	/// <param name="name">Class name. Defaults to 'LobbyManager'.</param>
	/// <param name="directory">Subdirectory under code/.</param>
	/// <param name="maxPlayers">Currently not applied by the handler — the generated MaxPlayers [Property] defaults to 16; tune it per-instance with set_property.</param>
	[McpTool( "create_lobby_manager" )]
	public static Task<object> CreateLobbyManager( string name = null, string directory = null, double? maxPlayers = null )
		=> McpGate.Run( "create_lobby_manager", McpGate.Args( ( "name", name ), ( "directory", directory ), ( "maxPlayers", maxPlayers ) ) );

	/// <summary>
	/// Generate a proxy-safe 'who is MY player?' resolver — the corpus footgun-killer that stops you
	/// running local-player logic against a proxy of someone else's player. The generated sealed
	/// Component exposes a static Local property that lazily finds the player GameObject belonging to
	/// THIS machine: online it's the tagged object whose Network.Owner is the local connection
	/// (Network.Owner == Connection.Local, or Network.IsOwner); offline/solo (no session) it's the
	/// first/only tagged player. The result is cached and revalidated with IsValid() so a destroyed or
	/// respawned player is re-resolved automatically. Also exposes a static IsLocal(GameObject) helper
	/// for filtering events (e.g. only open a station overlay for your own player). Works identically
	/// online and offline — no `if (Network.IsOwner)` guard that silently disables everything in a solo
	/// playtest. Attach ONE to a persistent object (your game manager) so PlayerTag is configurable in
	/// the inspector; the resolver is static and callable from anywhere. Next steps: hotload, attach,
	/// tag each player GameObject with the PlayerTag, then read &lt;Name&gt;.Local / filter with
	/// &lt;Name&gt;.IsLocal(go). Optionally attached to an existing GameObject by GUID (after a
	/// hotload).
	/// </summary>
	/// <param name="name">Class name for the generated component. Defaults to 'LocalPlayerResolver'.</param>
	/// <param name="directory">Subdirectory for the generated .cs file. Defaults to 'Code'.</param>
	/// <param name="playerTag">Tag that marks a player GameObject (players must carry this tag). Baked as the default PlayerTag, editable per-instance in the inspector. Defaults to 'player'.</param>
	/// <param name="targetId">GUID of a GameObject to attach to (only attaches if the type is already loaded — hotload first).</param>
	[McpTool( "create_local_player_resolver" )]
	public static Task<object> CreateLocalPlayerResolver( string name = null, string directory = null, string playerTag = null, string targetId = null )
		=> McpGate.Run( "create_local_player_resolver", McpGate.Args( ( "name", name ), ( "directory", directory ), ( "playerTag", playerTag ), ( "targetId", targetId ) ) );

	/// <summary>
	/// Generate a network event relay Component: [Rpc.Broadcast] SendEvent(eventName, payload) to all
	/// clients and [Rpc.Host] SendEventToHost(...), both dispatching into a local OnNetworkEvent switch
	/// you extend. It does NOT implement INetworkListener — use create_lobby_manager for
	/// connect/disconnect hooks. Writes &lt;name&gt;.cs and returns { created, path, className }.
	/// Follow with trigger_hotload, then get_compile_errors.
	/// </summary>
	/// <param name="name">Class name. Defaults to 'NetworkEvents'.</param>
	/// <param name="directory">Subdirectory under code/.</param>
	/// <param name="includeChat">Currently not applied by the handler — no chat system is generated.</param>
	[McpTool( "create_network_events" )]
	public static Task<object> CreateNetworkEvents( string name = null, string directory = null, bool? includeChat = null )
		=> McpGate.Run( "create_network_events", McpGate.Args( ( "name", name ), ( "directory", directory ), ( "includeChat", includeChat ) ) );

	/// <summary>
	/// Generate a network-aware player controller with [Sync] properties, owner-only input, and
	/// [Rpc.Broadcast] actions. Writes &lt;name&gt;.cs and returns { created, path, className }. The
	/// generated Component syncs PlayerName/Health, moves a CharacterController from Input.AnalogMove
	/// behind an IsProxy guard, and exposes a [Property] MoveSpeed plus an [Rpc.Broadcast]
	/// TakeDamage(int). Follow with trigger_hotload, then get_compile_errors, then attach +
	/// network_spawn.
	/// </summary>
	/// <param name="name">Class name. Defaults to 'NetworkedPlayer'.</param>
	/// <param name="directory">Subdirectory under code/.</param>
	/// <param name="moveSpeed">Movement speed (generated MoveSpeed [Property]). Defaults to 200.</param>
	/// <param name="includeHealth">Currently not applied by the handler — the [Sync] Health and [Rpc.Broadcast] TakeDamage are always generated.</param>
	[McpTool( "create_networked_player" )]
	public static Task<object> CreateNetworkedPlayer( string name = null, string directory = null, double? moveSpeed = null, bool? includeHealth = null )
		=> McpGate.Run( "create_networked_player", McpGate.Args( ( "name", name ), ( "directory", directory ), ( "moveSpeed", moveSpeed ), ( "includeHealth", includeHealth ) ) );

	/// <summary>
	/// Check the current multiplayer status. Returns { isActive, isHost, isClient, isConnecting,
	/// maxPlayers, serverName } read from Sandbox.Networking — meaningful mostly in play mode with
	/// networking active. It does NOT return a player list or networked-object dump; use
	/// inspect_networked_object for a specific object's Network/[Sync] state.
	/// </summary>
	[McpTool.ReadOnly( "get_network_status" )]
	public static Task<object> GetNetworkStatus()
		=> McpGate.Run( "get_network_status", McpGate.Args() );

	/// <summary>
	/// Network-enable a GameObject so it is synchronized across all connected clients. Calls
	/// NetworkSpawn(). Returns { spawned, id } on success — follow with inspect_networked_object to
	/// confirm the Network state, or set_ownership to hand it to a connection.
	/// </summary>
	/// <param name="id">GUID of the GameObject to network.</param>
	[McpTool( "network_spawn" )]
	public static Task<object> NetworkSpawn( string id )
		=> McpGate.Run( "network_spawn", McpGate.Args( ( "id", id ) ) );

	/// <summary>
	/// Assign network ownership of a GameObject to a connection, or drop ownership. Omitting
	/// connectionId (or passing an empty string) calls Network.DropOwnership(); passing a connection Id
	/// or SteamId calls Network.AssignOwnership() on the matching live Connection. Returns {
	/// ownershipAssigned, id, connectionId } or { ownershipDropped, id }; errors if no connection
	/// matches (connections only exist while networking is active).
	/// </summary>
	/// <param name="id">GUID of the networked GameObject.</param>
	/// <param name="connectionId">Connection Id GUID or SteamId of the target connection. Omit OR pass an empty string to DROP ownership (there is no take-ownership mode in the current handler).</param>
	[McpTool( "set_ownership" )]
	public static Task<object> SetOwnership( string id, string connectionId = null )
		=> McpGate.Run( "set_ownership", McpGate.Args( ( "id", id ), ( "connectionId", connectionId ) ) );
}