sbox-mcp-server 1.20.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,7 +24,7 @@ export function registerComponentTools(server, bridge) {
24
24
  };
25
25
  });
26
26
  // ── get_all_properties ───────────────────────────────────────────
27
- server.tool("get_all_properties", "Dump all public properties of every component on a GameObject as JSON -- names, types, and current values", {
27
+ server.tool("get_all_properties", "Dump all public properties of every component on a GameObject. Returns { id, components } where each entry is { component, properties: [{ name, type, value }] } — values are stringified (unreadable ones show '<error>'). Use the exact component/property names it reports with set_property or get_property; can be large on component-heavy objects.", {
28
28
  id: z.string().describe("GUID of the GameObject"),
29
29
  }, async (params) => {
30
30
  const res = await bridge.send("get_all_properties", params);
@@ -36,7 +36,7 @@ export function registerComponentTools(server, bridge) {
36
36
  };
37
37
  });
38
38
  // ── list_available_components ────────────────────────────────────
39
- server.tool("list_available_components", "List all component types available in s&box (built-in and custom). Search by name or filter by category", {
39
+ server.tool("list_available_components", "List all instantiable component types in the TypeLibrary — built-in AND your project's custom components (abstract types excluded); filter does a substring match on the type name. Returns { count, components } with { name, title, description, fullName } per type, sorted by name the unfiltered list is LARGE, so pass filter. Use the returned name with add_component_with_properties, and describe_type for a type's full property list.", {
40
40
  filter: z
41
41
  .string()
42
42
  .optional()
@@ -28,25 +28,25 @@ export function registerDebugDrawTools(server, bridge) {
28
28
  color,
29
29
  thickness,
30
30
  }, async (p) => reply(await bridge.send("debug_draw_line", p)));
31
- server.tool("debug_draw_ray", "Draw a debug ray (drawn as an arrow) from an origin along a direction for a given length. Renders in editor and play. Ideal for visualizing a raycast result or a facing/normal direction. Accumulates until debug_clear.", {
31
+ server.tool("debug_draw_ray", "Draw a debug ray (drawn as an arrow) from an origin along a direction for a given length. Renders in editor and play. Ideal for visualizing a raycast result or a facing/normal direction. Accumulates until debug_clear. Returns { drawn: 'ray', count, mode } — count is the total accumulated primitives, mode is 'edit' or 'play' (edit-mode gizmos are NOT in take_screenshot; use capture_view in play mode).", {
32
32
  origin: z.string().describe('Ray origin, world-space "x,y,z"'),
33
33
  direction: z.string().describe('Direction vector "x,y,z" (normalized internally)'),
34
34
  length: z.number().optional().describe("Ray length in units (default 64)"),
35
35
  color,
36
36
  thickness,
37
37
  }, async (p) => reply(await bridge.send("debug_draw_ray", p)));
38
- server.tool("debug_draw_box", "Draw a wireframe debug box centered at a point. Renders in editor and play. Ideal for visualizing a trigger_zone's bounds or a physics_overlap box volume. Accumulates until debug_clear.", {
38
+ server.tool("debug_draw_box", "Draw a wireframe debug box centered at a point. Renders in editor and play. Ideal for visualizing a trigger_zone's bounds or a physics_overlap box volume. Accumulates until debug_clear. Returns { drawn: 'box', count, mode } — count is the total accumulated primitives, mode is 'edit' or 'play' (edit-mode gizmos are NOT in take_screenshot; use capture_view in play mode).", {
39
39
  center: z.string().describe('Box center, world-space "x,y,z"'),
40
40
  size: z.string().optional().describe('Full size "x,y,z" in units (default "32,32,32")'),
41
41
  color,
42
42
  thickness,
43
43
  }, async (p) => reply(await bridge.send("debug_draw_box", p)));
44
- server.tool("debug_draw_sphere", "Draw a wireframe debug sphere at a point. Renders in editor and play. Ideal for visualizing a physics_overlap radius or an NPC's hearing/sight range. Accumulates until debug_clear.", {
44
+ server.tool("debug_draw_sphere", "Draw a wireframe debug sphere at a point. Renders in editor and play. Ideal for visualizing a physics_overlap radius or an NPC's hearing/sight range. Accumulates until debug_clear. Returns { drawn: 'sphere', count, mode } — count is the total accumulated primitives, mode is 'edit' or 'play' (edit-mode gizmos are NOT in take_screenshot; use capture_view in play mode).", {
45
45
  center: z.string().describe('Sphere center, world-space "x,y,z"'),
46
46
  radius: z.number().optional().describe("Radius in units (default 32)"),
47
47
  color,
48
48
  thickness,
49
49
  }, async (p) => reply(await bridge.send("debug_draw_sphere", p)));
50
- server.tool("debug_clear", "Remove all debug-draw primitives by destroying the debug holder for the current scene (edit or play). Call before redrawing a fresh frame of debug shapes.", {}, async (p) => reply(await bridge.send("debug_clear", p)));
50
+ server.tool("debug_clear", "Remove ALL debug-draw primitives at once by destroying the debug holder for the current scene (edit or play) — there is no way to remove a single shape. Call before redrawing a fresh frame of debug shapes. Returns { cleared, removed } where removed is how many primitives were destroyed.", {}, async (p) => reply(await bridge.send("debug_clear", p)));
51
51
  }
52
52
  //# sourceMappingURL=debugdraw.js.map
@@ -281,7 +281,7 @@ export function registerDiagnosticTools(server, bridge) {
281
281
  };
282
282
  });
283
283
  // ── console_run ──────────────────────────────────────────────────── (bridge)
284
- server.tool("console_run", "Run an s&box console command / ConCmd via Sandbox.ConsoleSystem.Run — e.g. a cvar ('sv_cheats 1') or a registered command. Also the invocation primitive behind execute_csharp.", {
284
+ server.tool("console_run", "Run an s&box console command / ConCmd via Sandbox.ConsoleSystem.Run — e.g. a cvar ('sv_cheats 1') or a registered command. Also the invocation primitive behind execute_csharp. Fire-and-forget: returns only { ran, command } and does NOT capture console output — follow with read_log to see what the command printed.", {
285
285
  command: z.string().describe("The console command line to run"),
286
286
  }, async (params) => {
287
287
  const res = await bridge.send("console_run", params);
@@ -310,14 +310,16 @@ export function registerDiagnosticTools(server, bridge) {
310
310
  const cmd = `claude_exec_${id}`;
311
311
  const filePath = `Editor/__Exec_${id}.cs`;
312
312
  const marker = `[EXEC ${id}]`;
313
+ // Bare `Log` (via `using Sandbox;`), NOT `Sandbox.Log` — the latter stopped
314
+ // resolving in project editor assemblies on engine 26.07.08b and broke every call.
313
315
  const inner = params.expression
314
- ? `var __r = (${params.code});\n\t\t\tSandbox.Log.Info( "${marker} RESULT=" + System.Text.Json.JsonSerializer.Serialize( __r ) );`
315
- : `${params.code}\n\t\t\tSandbox.Log.Info( "${marker} DONE" );`;
316
+ ? `var __r = (${params.code});\n\t\t\tLog.Info( "${marker} RESULT=" + System.Text.Json.JsonSerializer.Serialize( __r ) );`
317
+ : `${params.code}\n\t\t\tLog.Info( "${marker} DONE" );`;
316
318
  const cs = `using Editor;\nusing Sandbox;\nusing System;\n\n` +
317
319
  `public static class __Exec_${id}\n{\n` +
318
320
  `\t[ConCmd( "${cmd}" )]\n\tpublic static void Run()\n\t{\n` +
319
321
  `\t\ttry\n\t\t{\n\t\t\t${inner}\n\t\t}\n` +
320
- `\t\tcatch ( System.Exception __e ) { Sandbox.Log.Error( "${marker} ERROR=" + __e.Message ); }\n` +
322
+ `\t\tcatch ( System.Exception __e ) { Log.Error( "${marker} ERROR=" + __e.Message ); }\n` +
321
323
  `\t}\n}\n`;
322
324
  const timeout = params.timeoutMs ?? 20000;
323
325
  const wr = await bridge.send("write_file", { path: filePath, content: cs });
@@ -390,7 +392,7 @@ export function registerDiagnosticTools(server, bridge) {
390
392
  };
391
393
  });
392
394
  // ── get_bounds ─────────────────────────────────────────────────────── (bridge, Batch 33)
393
- server.tool("get_bounds", "Get a GameObject's world-space bounding box center, size, extents, mins/maxs, and a radius. Useful for placing/framing objects and sizing camera moves. Reads GameObject.GetBounds(); objects with no renderer report empty:true with their world position.", {
395
+ server.tool("get_bounds", "Get a GameObject's world-space bounding box. Returns { id, name, center, size, extents, mins, maxs, radius, position, empty } objects with no renderer report empty:true (bounds collapse to the world position) plus an explanatory note. Feed center/radius into screenshot_from or frame_camera to frame the object, or use mins/maxs for placement math (screenshot_orbit calls this internally).", {
394
396
  id: z.string().describe("GUID of the GameObject to measure"),
395
397
  }, async (params) => {
396
398
  const res = await bridge.send("get_bounds", params);
@@ -19,21 +19,21 @@ export function registerDiscoveryTools(server, bridge) {
19
19
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
20
20
  });
21
21
  // ── list_libraries ───────────────────────────────────────────────
22
- server.tool("list_libraries", "List the s&box libraries/addons installed in this project (reads Libraries/ + each .sbproj). Discovers what's available to build ON — e.g. character controllers (fish.scc = Shrimple Character Controller, facepunch.playercontroller), world/spline/road tools — so you can leverage an installed library (add its components via add_component_with_properties, or generate code against its API) instead of writing from scratch. Returns ident/org/title/type/enabled per library. Read-only.", {}, async () => {
22
+ server.tool("list_libraries", "List the s&box libraries/addons installed in this project (reads Libraries/ + each .sbproj). Discovers what's available to build ON — e.g. character controllers (fish.scc = Shrimple Character Controller, facepunch.playercontroller), world/spline/road tools — so you can leverage an installed library (add its components via add_component_with_properties, or generate code against its API) instead of writing from scratch. Returns `count` and `libraries` [{folder, ident, org, title, type, enabled}] ALL libraries, no limit or pagination; `enabled` is false when the library's .sbproj has been disabled (renamed .sbproj.disabled). Read-only.", {}, async () => {
23
23
  const res = await bridge.send("list_libraries", {});
24
24
  if (!res.success)
25
25
  return { content: [{ type: "text", text: `Error: ${res.error}` }] };
26
26
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
27
27
  });
28
28
  // ── search_types ─────────────────────────────────────────────────
29
- server.tool("search_types", "Find types matching a name pattern. Pass components_only=true to filter to Component subclasses only. Useful for discovering 'is there a built-in X for this?'", {
29
+ server.tool("search_types", "Find loaded types matching a name pattern. Useful for discovering 'is there a built-in X for this?'. Returns `count` and `matches` with each type's name, fullName, isComponent, and isAbstract — results are silently truncated at `limit` (default 50), so narrow the pattern if you hit the cap. Pass a match's name to describe_type for its full member surface.", {
30
30
  pattern: z.string().describe("Substring to match against type name (case-insensitive)"),
31
31
  namespace: z
32
32
  .string()
33
33
  .optional()
34
34
  .describe("Optional namespace filter (case-insensitive substring)"),
35
- components_only: z.boolean().default(false),
36
- limit: z.number().int().default(50),
35
+ components_only: z.boolean().default(false).describe("Only return Component subclasses (default false)"),
36
+ limit: z.number().int().default(50).describe("Maximum matches to return (default 50); the search stops silently at this cap"),
37
37
  }, async (params) => {
38
38
  const res = await bridge.send("search_types", params);
39
39
  if (!res.success)
@@ -51,10 +51,10 @@ export function registerDiscoveryTools(server, bridge) {
51
51
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
52
52
  });
53
53
  // ── find_in_project ──────────────────────────────────────────────
54
- server.tool("find_in_project", "Grep the user's s&box project for a symbol. Returns file paths and line numbers. Useful for finding usage examples of an API or seeing how the project already does something.", {
54
+ server.tool("find_in_project", "Grep the user's s&box project for a symbol (case-sensitive substring; skips .git/bin/obj). Useful for finding usage examples of an API or seeing how the project already does something. Returns `symbol`, `count`, and `results` [{file, line, text}], capped at `max_results` (default 25) — raise it if you may be missing hits. Follow up with read_file on a result's file path.", {
55
55
  symbol: z.string().describe("Substring or symbol to search for"),
56
56
  extension: z.string().default(".cs").describe("File extension filter"),
57
- max_results: z.number().int().default(25),
57
+ max_results: z.number().int().default(25).describe("Maximum hits to return (default 25); the search stops once reached"),
58
58
  }, async (params) => {
59
59
  const res = await bridge.send("find_in_project", params);
60
60
  if (!res.success)
@@ -70,7 +70,7 @@ export function registerGameObjectTools(server, bridge) {
70
70
  };
71
71
  });
72
72
  // ── duplicate_gameobject ─────────────────────────────────────────
73
- server.tool("duplicate_gameobject", "Clone a GameObject with all its components. Optionally offset position or rename", {
73
+ server.tool("duplicate_gameobject", "Clone a GameObject with all its components. Returns { duplicated, original, gameObject } — gameObject.id is the clone's new GUID; pass it to set_transform / add_component_with_properties. If offset is omitted the clone lands exactly on top of the original.", {
74
74
  id: z.string().describe("GUID of the GameObject to duplicate"),
75
75
  name: z.string().optional().describe("New name for the clone"),
76
76
  offset: Vector3Schema.optional().describe("Position offset from original so the clone doesn't overlap"),
@@ -84,7 +84,7 @@ export function registerGameObjectTools(server, bridge) {
84
84
  };
85
85
  });
86
86
  // ── rename_gameobject ────────────────────────────────────────────
87
- server.tool("rename_gameobject", "Change the display name of a GameObject", {
87
+ server.tool("rename_gameobject", "Change the display name of a GameObject identified by its GUID (the GUID itself never changes, so existing references stay valid). Returns { renamed, id, oldName, newName }. Name-based lookups (e.g. find_objects) will see the new name immediately.", {
88
88
  id: z.string().describe("GUID of the GameObject"),
89
89
  name: z.string().describe("New display name"),
90
90
  }, async (params) => {
@@ -114,7 +114,7 @@ export function registerGameObjectTools(server, bridge) {
114
114
  };
115
115
  });
116
116
  // ── set_enabled ──────────────────────────────────────────────────
117
- server.tool("set_enabled", "Enable or disable a GameObject (disabled objects are invisible and inactive)", {
117
+ server.tool("set_enabled", "Enable or disable a GameObject (disabled objects are invisible and inactive, including their components and children). Returns { id, enabled } confirming the new state.", {
118
118
  id: z.string().describe("GUID of the GameObject"),
119
119
  enabled: z.boolean().describe("true to enable, false to disable"),
120
120
  }, async (params) => {
@@ -127,7 +127,7 @@ export function registerGameObjectTools(server, bridge) {
127
127
  };
128
128
  });
129
129
  // ── set_transform ────────────────────────────────────────────────
130
- server.tool("set_transform", "Set position, rotation, and/or scale on a GameObject. Only provided values are changed", {
130
+ server.tool("set_transform", "Set position, rotation, and/or scale on a GameObject. Only provided values are changed; values apply in world space unless local=true. Returns { transformed, gameObject } with the full serialized object (id, name, position, rotation, scale, components) so you can verify what actually applied.", {
131
131
  id: z.string().describe("GUID of the GameObject"),
132
132
  position: Vector3Schema.optional().describe("New position"),
133
133
  rotation: RotationSchema.optional().describe("New rotation"),
@@ -170,7 +170,7 @@ export function registerGameObjectTools(server, bridge) {
170
170
  };
171
171
  });
172
172
  // ── get_selected_objects ─────────────────────────────────────────
173
- server.tool("get_selected_objects", "Get the GameObjects currently selected by the user in the s&box editor", {}, async () => {
173
+ server.tool("get_selected_objects", "Get the GameObjects currently selected by the user in the s&box editor. Returns { count, selected } where each entry is a serialized GameObject (id, name, enabled, position, rotation, scale, components, childCount) — use the ids with set_transform, add_component_with_properties, etc. Handy for 'do X to what I have selected' requests.", {}, async () => {
174
174
  const res = await bridge.send("get_selected_objects");
175
175
  if (!res.success) {
176
176
  return { content: [{ type: "text", text: `Error: ${res.error}` }] };
@@ -180,7 +180,7 @@ export function registerGameObjectTools(server, bridge) {
180
180
  };
181
181
  });
182
182
  // ── select_object ────────────────────────────────────────────────
183
- server.tool("select_object", "Select a GameObject in the editor (highlights it in the hierarchy and scene view)", {
183
+ server.tool("select_object", "Select a GameObject in the editor (highlights it in the hierarchy and scene view). Replaces the current selection unless addToSelection=true. Returns { selected, id }; confirm the result with get_selected_objects.", {
184
184
  id: z.string().describe("GUID of the GameObject to select"),
185
185
  addToSelection: z
186
186
  .boolean()
@@ -196,7 +196,7 @@ export function registerGameObjectTools(server, bridge) {
196
196
  };
197
197
  });
198
198
  // ── focus_object ─────────────────────────────────────────────────
199
- server.tool("focus_object", "Move the editor camera to focus on a specific GameObject (like double-clicking in the hierarchy)", {
199
+ server.tool("focus_object", "Highlight a GameObject by selecting it in the editor. NOTE: s&box exposes no dedicated focus API, so this only sets the selection — it does NOT move any camera (returns { focused, id, note } saying so). To actually point the viewport at an object use frame_camera; to aim a screenshot use screenshot_from.", {
200
200
  id: z.string().describe("GUID of the GameObject to focus"),
201
201
  }, async (params) => {
202
202
  const res = await bridge.send("focus_object", params);
@@ -109,7 +109,7 @@ export function registerGameplayTools(server, bridge) {
109
109
  });
110
110
  // ── create_objective_system ───────────────────────────────────────
111
111
  // The win/lose primitive — turns "objects in a scene" into "a game with a goal".
112
- server.tool("create_objective_system", "Generate an ObjectiveManager component — the win/lose brain of a game. Tracks an objective (collect_all / reach_goal / survive_time / eliminate_all), fires a win, and handles a lose condition (fall below kill-Z / timer / out of lives). Self-contained C#; other systems call ObjectiveManager.Instance. Optionally placed as a scene singleton", {
112
+ server.tool("create_objective_system", "Generate an ObjectiveManager component — the win/lose brain of a game. Tracks an objective (collect_all / reach_goal / survive_time / eliminate_all), fires a win, and handles a lose condition (fall below kill-Z / timer / out of lives). Self-contained C#; other systems call ObjectiveManager.Instance. Optionally placed as a scene singleton. Returns { created, path, className, gameObject, note } — gameObject is the placed singleton, or null with a note when the fresh type isn't in the TypeLibrary yet. Follow with trigger_hotload, then get_compile_errors; if placement was skipped, place with add_component_to_new_object after the hotload", {
113
113
  name: z
114
114
  .string()
115
115
  .optional()
@@ -310,7 +310,7 @@ export function registerGameplayTools(server, bridge) {
310
310
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
311
311
  });
312
312
  // -- create_pickup -----------------------------------------------------
313
- server.tool("create_pickup", "Generate a trigger-based collectible component. On enter by a tagged object it raises OnCollected (wire it to your objective/score system) and despawns. Optionally builds a visible pickup GameObject with a trigger SphereCollider (+ a model) in one call", {
313
+ server.tool("create_pickup", "Generate a trigger-based collectible component. On enter by a tagged object it raises OnCollected (wire it to your objective/score system) and despawns. Optionally builds a visible pickup GameObject with a trigger SphereCollider (+ a model) in one call. Returns { created, path, className, gameObject, note } — gameObject is the placed pickup (null unless placeInScene=true); a note flags when the component couldn't attach because the fresh type needs a hotload. Follow with trigger_hotload, then get_compile_errors", {
314
314
  name: z.string().optional().describe("Class name. Defaults to 'Pickup'"),
315
315
  directory: z
316
316
  .string()
@@ -379,7 +379,7 @@ export function registerGameplayTools(server, bridge) {
379
379
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
380
380
  });
381
381
  // -- create_leaderboard_panel ------------------------------------------
382
- server.tool("create_leaderboard_panel", "Generate a Razor PanelComponent that fetches and displays a Sandbox.Services leaderboard derived from a stat name. Produces TWO files: {name}.razor and {name}.razor.scss. The panel auto-refreshes every 30 s, shows rank/displayName/value rows, handles loading state, and includes a BuildHash() override (razor-lint clean). Must be hosted under a ScreenPanel or WorldPanel. Stats must be configured for the project ident on sbox.game. Uses Leaderboards.Get(statName) + board.Refresh() -- the exact API from ServicesQueryHandler.", {
382
+ server.tool("create_leaderboard_panel", "Generate a Razor PanelComponent that fetches and displays a Sandbox.Services leaderboard derived from a stat name. Produces TWO files: {name}.razor and {name}.razor.scss. The panel auto-refreshes every 30 s, shows rank/displayName/value rows, handles loading state, and includes a BuildHash() override (razor-lint clean). Must be hosted under a ScreenPanel or WorldPanel. Stats must be configured for the project ident on sbox.game. Uses Leaderboards.Get(statName) + board.Refresh() -- the exact API from ServicesQueryHandler. Returns { created, razorPath, scssPath, className, note }. Follow with trigger_hotload, then get_compile_errors, then host it via add_screen_panel (panelComponent=className).", {
383
383
  name: z.string().optional().describe("Class name for the panel component. Defaults to 'LeaderboardPanel'"),
384
384
  directory: z.string().optional().describe("Subdirectory for the generated files. Defaults to 'Code/UI'"),
385
385
  statName: z.string().optional().describe("Sandbox.Services stat name the leaderboard is derived from. Defaults to 'score'"),
@@ -407,7 +407,7 @@ export function registerGameplayTools(server, bridge) {
407
407
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
408
408
  });
409
409
  // -- create_stat_modifier_system ---------------------------------------
410
- server.tool("create_stat_modifier_system", "Generate an enum-keyed stat modifier system with three modifier layers: SET (highest-priority-wins hard override), ADD (summed bonuses), MULT (multiplied factors applied last). Modifier storage uses parallel private Lists of primitive types (serialization-safe). RemoveModifiersFrom(source) cleans up all mods from a buff/debuff source by reference. Static OnStatChanged(stat, value) event fires after every add/remove. Mined from RPG/buff/debuff patterns across shipped s&box games.", {
410
+ server.tool("create_stat_modifier_system", "Generate an enum-keyed stat modifier system with three modifier layers: SET (highest-priority-wins hard override), ADD (summed bonuses), MULT (multiplied factors applied last). Modifier storage uses parallel private Lists of primitive types (serialization-safe). RemoveModifiersFrom(source) cleans up all mods from a buff/debuff source by reference. Static OnStatChanged(stat, value) event fires after every add/remove. Mined from RPG/buff/debuff patterns across shipped s&box games. Returns { created, path, className, stats, placedOn, note } — stats echoes the sanitized stat names ({name}Stat enum values); placedOn is the target GameObject when attached (needs the type hotloaded). Follow with trigger_hotload, then get_compile_errors.", {
411
411
  name: z.string().optional().describe("Class name prefix -- generates {name}Stat enum + {name} Component. Defaults to 'StatSystem'"),
412
412
  directory: z.string().optional().describe("Subdirectory for the .cs file. Defaults to 'Code'"),
413
413
  stats: z.union([z.array(z.string()), z.string()]).optional().describe("Stat names as a JSON array or comma-separated string. Defaults to 'Health,Speed,Damage'"),
@@ -14,7 +14,7 @@ import { z } from "zod";
14
14
  */
15
15
  export function registerInspectionTools(server, bridge) {
16
16
  // ── inspect_networked_object ──────────────────────────────────────
17
- server.tool("inspect_networked_object", "Inspect the live networking contract of a GameObject: Network.Owner/IsProxy/IsOwner/Active, OwnerTransfer + OrphanedMode, and every component's [Sync] property names, SyncFlags, and current values (host vs proxy). Unlike get_network_status (session-only), this is per-object — the way to verify a host-authoritative or ownership change actually replicated. Works in edit or play mode.", {
17
+ server.tool("inspect_networked_object", "Inspect the live networking contract of a GameObject. Returns {id, name, network: {active, isProxy, isOwner, isCreator, ownerId, ownerSteamId, ownerTransfer, orphaned, flags}, components: [{component, fields: [{name, type, isSync, syncFlags, value}]}]} by default only [Sync]-marked fields are listed (components with none are omitted). Unlike get_network_status (session-only), this is per-object — the way to verify a host-authoritative or ownership change actually replicated; works in edit or play mode. Follow up with set_ownership to change the owner, or networking_lint to find the code-level cause of a bad [Sync] value.", {
18
18
  id: z.string().describe("GUID of the GameObject to inspect"),
19
19
  allProps: z
20
20
  .boolean()
@@ -70,8 +70,8 @@ export function registerInspectionTools(server, bridge) {
70
70
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
71
71
  });
72
72
  // ── save_inspect ──────────────────────────────────────────────────
73
- server.tool("save_inspect", "Inspect the game's FileSystem.Data save files — the assistant is otherwise blind to persisted state. action='list' enumerates files under a folder (with size/mtime); action='read' dumps a save file's JSON; action='diff' compares two save files key-by-key. Use to verify a save actually wrote, debug a load/migration, or confirm a sanitize/clamp ran.", {
74
- action: z.enum(["list", "read", "diff"]).default("list"),
73
+ server.tool("save_inspect", "Inspect the game's FileSystem.Data save files — the assistant is otherwise blind to persisted state. action='list' (default) returns `directories` and `files` [{name, path, size}] under `path` (omit path for the Data root); action='read' returns {path, length, content}, truncating content at 60,000 chars; action='diff' compares two save files key-by-key, returning `diffCount` and up to 200 `diffs` [{key, change: added|removed|changed}]. Use to verify a save actually wrote, debug a load/migration, or confirm a sanitize/clamp ran.", {
74
+ action: z.enum(["list", "read", "diff"]).default("list").describe("'list' (default) enumerates a folder; 'read' dumps one file's JSON; 'diff' compares `path` vs `pathB`"),
75
75
  path: z
76
76
  .string()
77
77
  .optional()
@@ -87,8 +87,8 @@ export function registerInspectionTools(server, bridge) {
87
87
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
88
88
  });
89
89
  // ── services_query ────────────────────────────────────────────────
90
- server.tool("services_query", "Read from Sandbox.Services — the cloud stats/leaderboard layer many games use as their real DB. action='stats' lists/reads the project's stat definitions and the local player's values; action='leaderboard' fetches a leaderboard board's top entries. Read-only. Use to verify a Stats.Increment/SetValue path or a leaderboard wired correctly.", {
91
- action: z.enum(["stats", "leaderboard"]).default("stats"),
90
+ server.tool("services_query", "Read from Sandbox.Services — the cloud stats/leaderboard layer many games use as their real DB. action='stats' with `name` returns the local player's stat {ident, value, sum, min, max, lastValue, valueString}; without `name` it returns only the package ident plus a usage note (it does NOT list stat definitions). action='leaderboard' (name required) returns {board, displayName, totalEntries, count, entries} with at most `limit` entries (default 10). Read-only; use to verify a Stats.Increment/SetValue path or a leaderboard wired correctly.", {
91
+ action: z.enum(["stats", "leaderboard"]).default("stats").describe("'stats' (default) reads a local-player stat by `name`; 'leaderboard' fetches a board's top entries"),
92
92
  name: z
93
93
  .string()
94
94
  .optional()
@@ -101,7 +101,7 @@ export function registerInspectionTools(server, bridge) {
101
101
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
102
102
  });
103
103
  // ── simulate_input ────────────────────────────────────────────────
104
- server.tool("simulate_input", "Synthesize player input during PLAY mode so behavior can be verified without a human at the keyboard: press/hold/release a named input action (e.g. 'jump','attack1','use','reload') and/or set AnalogMove / AnalogLook for a duration. Drives movement, IPressable interactions, weapon fire/cooldown/reload, vehicle controls, and HUD advance-on-click. Requires play mode.", {
104
+ server.tool("simulate_input", "Press or release a named input action during PLAY mode (via Sandbox.Input.SetAction) so input-driven behavior — jump, attack1, use, reload, IPressable, weapon fire — can be verified without a human at the keyboard. REQUIRES play mode and a named `action`; returns {action, state: 'down'|'up', note}. Caveats: SetAction applies to the current input frame ('press' and 'hold' both set the action down; 'release' clears it), and analogMove/analogLook/durationMs are accepted but IGNORED Sandbox.Input has no analog injection API, so for movement use drive_player or the playtest harness instead.", {
105
105
  action: z
106
106
  .string()
107
107
  .optional()
@@ -28,7 +28,7 @@ const ColorSchema = z
28
28
  .describe('RGBA colour — object {r,g,b,a} (0-1) OR comma string "r,g,b,a"');
29
29
  export function registerLevelTools(server, bridge) {
30
30
  // ── snap_to_ground ─────────────────────────────────────────────────
31
- server.tool("snap_to_ground", "Drop a GameObject straight down onto the surface below it (physics raycast). Works best on collider-less props (an object with its own collider may self-hit). Optional offset lifts it off the surface.", {
31
+ server.tool("snap_to_ground", "Drop a GameObject straight down onto the surface below it (physics raycast). Works best on collider-less props (an object with its own collider may self-hit). Optional offset lifts it off the surface. Returns { snapped, groundZ, gameObject } with the object's updated transform — or { snapped: false, reason } (not an error) when no ground was hit below.", {
32
32
  id: z.string().describe("GUID of the GameObject to snap"),
33
33
  offset: z.number().optional().describe("Height above the surface to place it (default 0)"),
34
34
  startHeight: z.number().optional().describe("How far above the object to start the trace (default 2000)"),
@@ -43,7 +43,7 @@ export function registerLevelTools(server, bridge) {
43
43
  };
44
44
  });
45
45
  // ── align_objects ──────────────────────────────────────────────────
46
- server.tool("align_objects", "Align several GameObjects on one axis so they share a coordinate. mode = first (match the first object), min, max, or average.", {
46
+ server.tool("align_objects", "Align several GameObjects on one axis so they share a coordinate. mode = first (match the first object), min, max, or average; defaults to first. Returns { aligned, axis, mode, target } — aligned is the object count and target the shared coordinate; verify positions with get_scene_hierarchy or a screenshot.", {
47
47
  ids: z.array(z.string()).describe("GUIDs of the GameObjects to align (>= 2)"),
48
48
  axis: z.enum(["x", "y", "z"]).describe("Axis to align on"),
49
49
  mode: z
@@ -60,7 +60,7 @@ export function registerLevelTools(server, bridge) {
60
60
  };
61
61
  });
62
62
  // ── distribute_objects ─────────────────────────────────────────────
63
- server.tool("distribute_objects", "Evenly space GameObjects along an axis between the lowest and highest (keeps the two ends fixed, spreads the rest evenly).", {
63
+ server.tool("distribute_objects", "Evenly space GameObjects along an axis between the lowest and highest (keeps the two ends fixed, spreads the rest evenly). Returns { distributed, axis, from, to } — the object count and the fixed end coordinates the rest were spread between.", {
64
64
  ids: z.array(z.string()).describe("GUIDs of the GameObjects to distribute (>= 3)"),
65
65
  axis: z.enum(["x", "y", "z"]).describe("Axis to distribute along"),
66
66
  }, async (params) => {
@@ -104,7 +104,7 @@ export function registerLevelTools(server, bridge) {
104
104
  };
105
105
  });
106
106
  // ── scatter_props ──────────────────────────────────────────────────
107
- server.tool("scatter_props", "Scatter N copies of a model randomly within a radius around a center point — instant foliage, rocks, debris. Each copy gets a random yaw and (by default) is snapped to the ground. Seeded for reproducibility; copies are grouped under one parent by default. Count capped at 300.", {
107
+ server.tool("scatter_props", "Scatter N copies of a model randomly within a radius around a center point — instant foliage, rocks, debris. Each copy gets a random yaw and (by default) is snapped to the ground. Seeded for reproducibility; copies are grouped under one parent by default. Count capped at 300. Returns { scattered, groupId, seed } — individual prop GUIDs are not returned, so use groupId with get_scene_hierarchy (rootId) to enumerate them, or with set_transform/delete_gameobject to move/remove the whole batch.", {
108
108
  model: z.string().describe("Model path to scatter, e.g. 'models/dev/box.vmdl'"),
109
109
  center: Vector3Schema.optional().describe("Centre of the scatter area (default origin)"),
110
110
  radius: z.number().optional().describe("Scatter radius in units (default 256)"),
@@ -129,7 +129,7 @@ export function registerLevelTools(server, bridge) {
129
129
  };
130
130
  });
131
131
  // ── randomize_transforms ───────────────────────────────────────────
132
- server.tool("randomize_transforms", "Add natural variation to existing objects: random yaw and/or random uniform scale within a range. Great for breaking up repetition in placed foliage/rocks/crates. Seeded.", {
132
+ server.tool("randomize_transforms", "Add natural variation to existing objects: random yaw and/or random uniform scale within a range. Great for breaking up repetition in placed foliage/rocks/crates. Seeded — the same seed reproduces the same layout. Returns { randomized, seed } (the count of objects changed); scale only varies when scaleMax > scaleMin.", {
133
133
  ids: z.array(z.string()).describe("GUIDs of the GameObjects to randomize"),
134
134
  randomYaw: z.boolean().optional().describe("Randomize Z rotation (default true)"),
135
135
  scaleMin: z.number().optional().describe("Min uniform scale (default 1)"),
@@ -5,7 +5,7 @@ import { z } from "zod";
5
5
  */
6
6
  export function registerMaterialTools(server, bridge) {
7
7
  // ── assign_model ─────────────────────────────────────────────────
8
- server.tool("assign_model", "Set a 3D model on a GameObject's ModelRenderer. Creates the renderer component if it doesn't exist", {
8
+ server.tool("assign_model", "Set a 3D model on a GameObject's ModelRenderer. Creates the renderer component if it doesn't exist; errors if the model path can't be loaded. Returns { assigned, id, model } — follow with assign_material / set_material_property to style it, or take a screenshot to verify.", {
9
9
  id: z.string().describe("GUID of the GameObject"),
10
10
  model: z
11
11
  .string()
@@ -20,7 +20,7 @@ export function registerMaterialTools(server, bridge) {
20
20
  };
21
21
  });
22
22
  // ── create_material ──────────────────────────────────────────────
23
- server.tool("create_material", "Create a new material file (.vmat) with a shader and properties like color, roughness, metallic, texture", {
23
+ server.tool("create_material", "Create a new material file (.vmat, KV1 format) with a shader and properties like color, roughness, metallic, texture. Errors if the file already exists; when no properties are given it writes sensible PBR defaults (g_flMetalness 0, g_flRoughness 1). Returns { created, path, shader, propertiesWritten } — pass the returned path to recompile_asset (so the editor compiles it) and then assign_material.", {
24
24
  path: z
25
25
  .string()
26
26
  .describe("Relative path for the material (e.g. 'materials/walls/brick.vmat')"),
@@ -42,7 +42,7 @@ export function registerMaterialTools(server, bridge) {
42
42
  };
43
43
  });
44
44
  // ── assign_material ──────────────────────────────────────────────
45
- server.tool("assign_material", "Apply a material to a ModelRenderer on a GameObject. Optionally target a specific material slot", {
45
+ server.tool("assign_material", "Apply a material to a GameObject by setting its ModelRenderer's MaterialOverride (overrides the whole model's material). Requires an existing ModelRenderer (assign_model first) and errors if the material path can't be loaded. Returns { assigned, id, material } — tweak values afterwards with set_material_property.", {
46
46
  id: z.string().describe("GUID of the GameObject"),
47
47
  material: z
48
48
  .string()
@@ -61,7 +61,7 @@ export function registerMaterialTools(server, bridge) {
61
61
  };
62
62
  });
63
63
  // ── set_material_property ────────────────────────────────────────
64
- server.tool("set_material_property", "Change a property on the material assigned to a GameObject — color, roughness, metallic, texture, etc.", {
64
+ server.tool("set_material_property", "Change a property on the material assigned to a GameObject — color, roughness, metallic, texture, etc. Operates on the ModelRenderer's MaterialOverride; if none is assigned it auto-creates one from the default complex shader (no separate assign_material step needed). Returns { set, id, property, autoCreatedMaterial } — screenshot to verify the visual change.", {
65
65
  id: z.string().describe("GUID of the GameObject"),
66
66
  property: z
67
67
  .string()
@@ -14,7 +14,7 @@ import { z } from "zod";
14
14
  */
15
15
  export function registerMovieMakerTools(server, bridge) {
16
16
  // ── list_movies ───────────────────────────────────────────────────
17
- server.tool("list_movies", "List the project's .movie resources (Sandbox.MovieMaker clips authored in the editor's Movie Maker dock: Window → Movie Maker). Returns each movie's asset-relative path, whether it currently loads via ResourceLibrary, and whether it has a compiled clip. Start here before add_movie_player / play_movie — and if the list is empty, the movie has to be authored in the dock first (the bridge plays movies; it doesn't author keyframes).", {}, async () => {
17
+ server.tool("list_movies", "List the project's .movie resources (Sandbox.MovieMaker clips authored in the editor's Movie Maker dock: Window → Movie Maker). Scans the ENTIRE Assets folder recursively and returns every .movie found — no limit or paging. Returns { count, movies, note } where each movie has { path (asset-relative the form add_movie_player/play_movie expect), name, loadable (resolves via ResourceLibrary), hasCompiledClip }. Start here before add_movie_player / play_movie — if the list is empty, the movie has to be authored in the dock first (the bridge plays movies; it doesn't author keyframes).", {}, async () => {
18
18
  const res = await bridge.send("list_movies", {});
19
19
  if (!res.success) {
20
20
  return { content: [{ type: "text", text: `Error: ${res.error}` }] };
@@ -8,23 +8,23 @@ import { z } from "zod";
8
8
  */
9
9
  export function registerNetworkingTools(server, bridge) {
10
10
  // ── add_network_helper ────────────────────────────────────────────
11
- server.tool("add_network_helper", "Add a NetworkHelper component to the scene for quick multiplayer setup. Handles lobby creation and player prefab spawning", {
11
+ server.tool("add_network_helper", "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", {
12
12
  id: z
13
13
  .string()
14
14
  .optional()
15
- .describe("GUID of existing GameObject. Creates new if omitted"),
15
+ .describe("GUID of the GameObject to attach to. Required in practice — the current handler errors when omitted (create a holder with create_gameobject first)"),
16
16
  name: z
17
17
  .string()
18
18
  .optional()
19
- .describe("Name for the network manager object. Defaults to 'Network Manager'"),
19
+ .describe("Rename the target GameObject to this. Omit to keep its current name"),
20
20
  maxPlayers: z
21
21
  .number()
22
22
  .optional()
23
- .describe("Maximum number of players in the lobby"),
23
+ .describe("Maximum number of players in the lobby (currently not applied by the handler)"),
24
24
  playerPrefab: z
25
25
  .string()
26
26
  .optional()
27
- .describe("Path to the player prefab to spawn for each connection"),
27
+ .describe("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)"),
28
28
  }, async (params) => {
29
29
  const res = await bridge.send("add_network_helper", params);
30
30
  if (!res.success) {
@@ -35,23 +35,23 @@ export function registerNetworkingTools(server, bridge) {
35
35
  };
36
36
  });
37
37
  // ── configure_network ─────────────────────────────────────────────
38
- server.tool("configure_network", "Configure networking settings on the existing NetworkHelper: max players, lobby name, player prefab, start server", {
38
+ server.tool("configure_network", "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", {
39
39
  maxPlayers: z
40
40
  .number()
41
41
  .optional()
42
- .describe("Maximum number of players"),
42
+ .describe("Maximum number of players (currently not applied — Networking.MaxPlayers is read-only on this SDK; the live value is echoed in the response)"),
43
43
  lobbyName: z
44
44
  .string()
45
45
  .optional()
46
- .describe("Display name for the lobby"),
46
+ .describe("Display name for the lobby (sets Networking.ServerName — the only setting this handler applies)"),
47
47
  playerPrefab: z
48
48
  .string()
49
49
  .optional()
50
- .describe("Path to the player prefab"),
50
+ .describe("Path to the player prefab (currently not applied by the handler — set the NetworkHelper's PlayerPrefab via set_prefab_ref instead)"),
51
51
  startServer: z
52
52
  .boolean()
53
53
  .optional()
54
- .describe("Start the server/lobby immediately"),
54
+ .describe("Start the server/lobby immediately (currently not applied by the handler — add_network_helper sets StartServer=true on the NetworkHelper)"),
55
55
  }, async (params) => {
56
56
  const res = await bridge.send("configure_network", params);
57
57
  if (!res.success) {
@@ -62,7 +62,7 @@ export function registerNetworkingTools(server, bridge) {
62
62
  };
63
63
  });
64
64
  // ── get_network_status ────────────────────────────────────────────
65
- server.tool("get_network_status", "Check the current multiplayer status: connection state, player count, lobby info, networked objects", {}, async (params) => {
65
+ server.tool("get_network_status", "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", {}, async (params) => {
66
66
  const res = await bridge.send("get_network_status", params);
67
67
  if (!res.success) {
68
68
  return { content: [{ type: "text", text: `Error: ${res.error}` }] };
@@ -72,7 +72,7 @@ export function registerNetworkingTools(server, bridge) {
72
72
  };
73
73
  });
74
74
  // ── network_spawn ─────────────────────────────────────────────────
75
- server.tool("network_spawn", "Network-enable a GameObject so it is synchronized across all connected clients. Calls NetworkSpawn()", {
75
+ server.tool("network_spawn", "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", {
76
76
  id: z.string().describe("GUID of the GameObject to network"),
77
77
  }, async (params) => {
78
78
  const res = await bridge.send("network_spawn", params);
@@ -84,12 +84,12 @@ export function registerNetworkingTools(server, bridge) {
84
84
  };
85
85
  });
86
86
  // ── set_ownership ─────────────────────────────────────────────────
87
- server.tool("set_ownership", "Transfer network ownership of a GameObject to a different connection, or take/drop ownership", {
87
+ server.tool("set_ownership", "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)", {
88
88
  id: z.string().describe("GUID of the networked GameObject"),
89
89
  connectionId: z
90
90
  .string()
91
91
  .optional()
92
- .describe("GUID of the target connection. Empty string = drop ownership. Omit = take ownership"),
92
+ .describe("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)"),
93
93
  }, async (params) => {
94
94
  const res = await bridge.send("set_ownership", params);
95
95
  if (!res.success) {
@@ -100,7 +100,7 @@ export function registerNetworkingTools(server, bridge) {
100
100
  };
101
101
  });
102
102
  // ── add_sync_property ─────────────────────────────────────────────
103
- server.tool("add_sync_property", "Annotate an EXISTING public property in a C# script with the [Sync] attribute so s&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", {
103
+ server.tool("add_sync_property", "Annotate an EXISTING public property in a C# script with the [Sync] attribute so s&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", {
104
104
  path: z
105
105
  .string()
106
106
  .describe("Relative path to the script file (e.g. 'code/Player.cs')"),
@@ -156,7 +156,7 @@ export function registerNetworkingTools(server, bridge) {
156
156
  };
157
157
  });
158
158
  // ── create_networked_player ───────────────────────────────────────
159
- server.tool("create_networked_player", "Generate a network-aware player controller with [Sync] properties, owner-only input, and [Rpc.Broadcast] actions", {
159
+ server.tool("create_networked_player", "Generate a network-aware player controller with [Sync] properties, owner-only input, and [Rpc.Broadcast] actions. Writes <name>.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", {
160
160
  name: z
161
161
  .string()
162
162
  .optional()
@@ -165,11 +165,11 @@ export function registerNetworkingTools(server, bridge) {
165
165
  .string()
166
166
  .optional()
167
167
  .describe("Subdirectory under code/"),
168
- moveSpeed: z.number().optional().describe("Movement speed. Defaults to 300"),
168
+ moveSpeed: z.number().optional().describe("Movement speed (generated MoveSpeed [Property]). Defaults to 200"),
169
169
  includeHealth: z
170
170
  .boolean()
171
171
  .optional()
172
- .describe("Include health/damage system with host-authoritative TakeDamage. Defaults to true"),
172
+ .describe("Currently not applied by the handler — the [Sync] Health and [Rpc.Broadcast] TakeDamage are always generated"),
173
173
  }, async (params) => {
174
174
  const res = await bridge.send("create_networked_player", params);
175
175
  if (!res.success) {
@@ -180,7 +180,7 @@ export function registerNetworkingTools(server, bridge) {
180
180
  };
181
181
  });
182
182
  // ── create_lobby_manager ──────────────────────────────────────────
183
- server.tool("create_lobby_manager", "Generate a lobby manager script with create/join/leave lobby, player spawning, and connection cleanup", {
183
+ server.tool("create_lobby_manager", "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 <name>.cs and returns { created, path, className }. Follow with trigger_hotload, then get_compile_errors, then place via add_component_to_new_object", {
184
184
  name: z
185
185
  .string()
186
186
  .optional()
@@ -192,7 +192,7 @@ export function registerNetworkingTools(server, bridge) {
192
192
  maxPlayers: z
193
193
  .number()
194
194
  .optional()
195
- .describe("Default max players. Defaults to 8"),
195
+ .describe("Currently not applied by the handler — the generated MaxPlayers [Property] defaults to 16; tune it per-instance with set_property"),
196
196
  }, async (params) => {
197
197
  const res = await bridge.send("create_lobby_manager", params);
198
198
  if (!res.success) {
@@ -203,7 +203,7 @@ export function registerNetworkingTools(server, bridge) {
203
203
  };
204
204
  });
205
205
  // ── create_network_events ─────────────────────────────────────────
206
- server.tool("create_network_events", "Generate a network event handler script implementing INetworkListener for connect/disconnect/chat events", {
206
+ server.tool("create_network_events", "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 <name>.cs and returns { created, path, className }. Follow with trigger_hotload, then get_compile_errors", {
207
207
  name: z
208
208
  .string()
209
209
  .optional()
@@ -215,7 +215,7 @@ export function registerNetworkingTools(server, bridge) {
215
215
  includeChat: z
216
216
  .boolean()
217
217
  .optional()
218
- .describe("Include a chat message broadcast system. Defaults to false"),
218
+ .describe("Currently not applied by the handler — no chat system is generated"),
219
219
  }, async (params) => {
220
220
  const res = await bridge.send("create_network_events", params);
221
221
  if (!res.success) {