sbox-mcp-server 1.19.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.
Files changed (48) hide show
  1. package/dist/index.js +119 -82
  2. package/dist/tools/assets.js +5 -5
  3. package/dist/tools/audio.js +12 -19
  4. package/dist/tools/audit.d.ts +8 -0
  5. package/dist/tools/audit.js +65 -0
  6. package/dist/tools/batch.d.ts +8 -0
  7. package/dist/tools/batch.js +77 -0
  8. package/dist/tools/characters.js +6 -6
  9. package/dist/tools/cinematics.d.ts +19 -0
  10. package/dist/tools/cinematics.js +73 -0
  11. package/dist/tools/components.js +2 -2
  12. package/dist/tools/debugdraw.js +4 -4
  13. package/dist/tools/diagnostics.js +7 -5
  14. package/dist/tools/discovery.js +6 -6
  15. package/dist/tools/gameobjects.js +7 -7
  16. package/dist/tools/gameplay.js +4 -4
  17. package/dist/tools/inspection.js +6 -6
  18. package/dist/tools/interactionpack.d.ts +16 -0
  19. package/dist/tools/interactionpack.js +107 -0
  20. package/dist/tools/leveltools.js +5 -5
  21. package/dist/tools/looteconomy.d.ts +18 -0
  22. package/dist/tools/looteconomy.js +107 -0
  23. package/dist/tools/materials.js +4 -4
  24. package/dist/tools/moviemaker.d.ts +17 -0
  25. package/dist/tools/moviemaker.js +98 -0
  26. package/dist/tools/netprimitives.d.ts +18 -0
  27. package/dist/tools/netprimitives.js +110 -0
  28. package/dist/tools/networking.js +22 -22
  29. package/dist/tools/physics.js +4 -4
  30. package/dist/tools/playmode.js +5 -5
  31. package/dist/tools/playtest.js +2 -1
  32. package/dist/tools/prefabs.js +7 -3
  33. package/dist/tools/project.js +2 -2
  34. package/dist/tools/publishing.js +3 -3
  35. package/dist/tools/scenes.js +2 -2
  36. package/dist/tools/scripts.js +3 -3
  37. package/dist/tools/status.js +1 -1
  38. package/dist/tools/templates.js +7 -7
  39. package/dist/tools/ui.js +5 -5
  40. package/dist/tools/uifeedback.d.ts +16 -0
  41. package/dist/tools/uifeedback.js +83 -0
  42. package/dist/tools/vehicles.d.ts +8 -0
  43. package/dist/tools/vehicles.js +98 -0
  44. package/dist/tools/visuals.js +8 -8
  45. package/dist/tools/workflow.d.ts +8 -0
  46. package/dist/tools/workflow.js +107 -0
  47. package/dist/tools/world.js +61 -58
  48. package/package.json +55 -55
@@ -0,0 +1,73 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Cinematics & Dialogue pack — two engine-proof, no-asset scaffolds (v1.20.0):
4
+ *
5
+ * - create_cutscene_director hand-authored camera-shot cutscene player
6
+ * - create_dialogue_system typewriter NPC/story dialogue (Component + Razor HUD)
7
+ *
8
+ * These are the HAND-AUTHORED cinematic path (zero assets, full C# control). The
9
+ * keyframed/timeline path is the MovieMaker family (add_movie_player / play_movie
10
+ * / stop_movie / list_movies), which wires a Sandbox.MovieMaker MoviePlayer to a
11
+ * .movie clip authored in the editor's Movie Maker dock.
12
+ *
13
+ * All generate self-contained, sandbox-safe, LOCAL/visual-only code; the Razor
14
+ * output is razor_lint-safe by construction. File/scene-mutating, so refused
15
+ * during play mode by the bridge dispatch.
16
+ */
17
+ export function registerCinematicsTools(server, bridge) {
18
+ // ── create_cutscene_director ──────────────────────────────────────
19
+ server.tool("create_cutscene_director", "Generate a hand-authored cutscene player component that needs NO .movie asset — the zero-asset alternative to the MovieMaker family (add_movie_player/play_movie, which play keyframed .movie clips authored in the editor's Movie Maker dock). You author the shots directly in the inspector as parallel lists: ShotPositions (Vector3), ShotAngles (pitch/yaw/roll — Angles, not raw quaternions), ShotHoldSeconds, ShotBlendSeconds, and an optional per-shot ShotLookAt GameObject (aim at a target instead of using ShotAngles). At runtime it takes over the main camera (Scene.Camera) in OnPreRender ONLY while playing — smoothstep-eased Vector3.Lerp + Rotation.Slerp between shots — captures the camera's prior transform and restores it exactly when finished (same un-apply discipline as create_camera_shake). Play from any game code via the static <Name>.Play() (first director) or <Name>.Play(\"name\") (matches CutsceneName); subscribe the static <Name>.OnCutsceneFinished, or gate game logic on the static <Name>.IsCutscenePlaying. LockInput freezes player input each frame via Input.ClearActions() while still reading the SkipAction press first so the cutscene stays skippable. Optional letterbox generates a razor_lint-safe black-bars overlay panel (host under a ScreenPanel) shown while IsCutscenePlaying. LOCAL-only (each client renders its own view) — trigger inside an [Rpc.Broadcast] for all clients. Attach to ANY GameObject; it drives the camera itself and does not need to sit on the camera. Returns { created, path, className, skipAction, lockInput, letterbox, nextSteps } — path is the generated .cs (letterbox lists the overlay files when enabled). Follow with trigger_hotload, then get_compile_errors, then attach via add_component_with_properties (component=className).", {
20
+ name: z
21
+ .string()
22
+ .optional()
23
+ .describe("Class name for the generated component. Defaults to 'CutsceneDirector'"),
24
+ directory: z
25
+ .string()
26
+ .optional()
27
+ .describe("Subdirectory for the generated files. Defaults to 'Code'"),
28
+ skipAction: z
29
+ .string()
30
+ .optional()
31
+ .describe("Input action that ends the cutscene early (read before input is cleared, so a locked cutscene is still skippable). Sanitized to a safe token. Empty = not skippable. Defaults to 'jump'"),
32
+ lockInput: z
33
+ .boolean()
34
+ .optional()
35
+ .describe("Freeze player input during playback via Input.ClearActions() each frame. Defaults to true"),
36
+ letterbox: z
37
+ .boolean()
38
+ .optional()
39
+ .describe("Also generate a razor_lint-safe letterbox overlay (two black bars, shown while IsCutscenePlaying — host it under a ScreenPanel). Defaults to false"),
40
+ }, async (params) => {
41
+ const res = await bridge.send("create_cutscene_director", params);
42
+ if (!res.success) {
43
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
44
+ }
45
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
46
+ });
47
+ // ── create_dialogue_system ────────────────────────────────────────
48
+ server.tool("create_dialogue_system", "Generate an NPC/story dialogue system — a sealed state+data Component paired with a razor_lint-safe Razor HUD panel. Lines are authored in the inspector as a List<string> using the 'Speaker: text' convention (the part before the first colon is the speaker). The generated HUD panel binds to <Name>.Current automatically (no wiring) and renders the current line with a TimeSince-driven typewriter reveal at CharsPerSecond, folding the visible substring into BuildHash so it re-renders as characters appear. Press the AdvanceAction once to snap the whole line into view instantly, again to move to the next line; dismissing the last line ends the conversation. Start from any game code via the static <Name>.StartDialogue(string[] lines) or set Lines and call the instance Begin(). Static events for hooks: OnLineShown(index, speaker) — pair with add_lipsync to drive facial morphs / audio per line — and OnDialogueFinished when the conversation ends. Pairs with create_interactable to trigger dialogue on use. LOCAL-only (per-client HUD) — call StartDialogue inside an [Rpc.Broadcast] if every client should see it. Attach the panel under a ScreenPanel (add_screen_panel) so the HUD renders. Returns { created, path, className, charsPerSecond, advanceAction, panel, nextSteps } — panel lists the generated '<Name>Panel' HUD files. Follow with trigger_hotload, then get_compile_errors, then attach both components via add_component_with_properties.", {
49
+ name: z
50
+ .string()
51
+ .optional()
52
+ .describe("Class name for the generated dialogue component (the HUD panel is generated as '<Name>Panel'). Defaults to 'DialogueSystem'"),
53
+ directory: z
54
+ .string()
55
+ .optional()
56
+ .describe("Subdirectory for the generated files. Defaults to 'Code'"),
57
+ charsPerSecond: z
58
+ .number()
59
+ .optional()
60
+ .describe("Typewriter reveal speed in characters per second (clamped to >= 1). Defaults to 40"),
61
+ advanceAction: z
62
+ .string()
63
+ .optional()
64
+ .describe("Input action that completes the reveal / advances to the next line. Sanitized to a safe token. Defaults to 'use'"),
65
+ }, async (params) => {
66
+ const res = await bridge.send("create_dialogue_system", params);
67
+ if (!res.success) {
68
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
69
+ }
70
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
71
+ });
72
+ }
73
+ //# sourceMappingURL=cinematics.js.map
@@ -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()
@@ -0,0 +1,16 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { BridgeClient } from "../transport/bridge-client.js";
3
+ /**
4
+ * Interaction pack + Carry (v1.20.0) — three interaction scaffolds:
5
+ *
6
+ * - add_interaction_prompt eye-traced "Press E" HUD bound to IPressable targets
7
+ * (generates a .razor + .razor.scss pair, razor_lint-safe)
8
+ * - create_hold_to_confirm hold-to-fill progress action + static OnConfirmed
9
+ * - create_carry_system pickup / carry / throw with host-routed RPCs + ownership
10
+ *
11
+ * All are file/scene-mutating (refused during play mode by the bridge dispatch).
12
+ * The generated game code was authored against live reflection on this SDK
13
+ * (Scene.Trace, Component.IPressable, Rigidbody, GameObject.Network) — not memory.
14
+ */
15
+ export declare function registerInteractionPackTools(server: McpServer, bridge: BridgeClient): void;
16
+ //# sourceMappingURL=interactionpack.d.ts.map
@@ -0,0 +1,107 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Interaction pack + Carry (v1.20.0) — three interaction scaffolds:
4
+ *
5
+ * - add_interaction_prompt eye-traced "Press E" HUD bound to IPressable targets
6
+ * (generates a .razor + .razor.scss pair, razor_lint-safe)
7
+ * - create_hold_to_confirm hold-to-fill progress action + static OnConfirmed
8
+ * - create_carry_system pickup / carry / throw with host-routed RPCs + ownership
9
+ *
10
+ * All are file/scene-mutating (refused during play mode by the bridge dispatch).
11
+ * The generated game code was authored against live reflection on this SDK
12
+ * (Scene.Trace, Component.IPressable, Rigidbody, GameObject.Network) — not memory.
13
+ */
14
+ export function registerInteractionPackTools(server, bridge) {
15
+ // ── add_interaction_prompt ────────────────────────────────────────
16
+ server.tool("add_interaction_prompt", 'Generate an eye-traced interaction-prompt HUD — a PanelComponent (.razor + .razor.scss pair, like create_leaderboard_panel) that every frame traces a ray from the scene camera (Scene.Trace.Ray, out to [Property] float Range) and, when the crosshair is on a component implementing Component.IPressable, shows a centered "Press E"-style pill. The prompt text comes from the target\'s IPressable.GetTooltip() when it overrides it (most don\'t), else a [Property] DefaultPrompt built from the action. This is the visible half of the interaction loop: it PAIRS with create_interactable / add_interaction_station (which implement IPressable) — this tool tells the player they CAN press, those tools handle the press. Host it under a ScreenPanel (add_screen_panel), then add the component to that panel object. The generated Razor is razor_lint-safe by construction: PanelComponent + BuildHash override folding the visible state, no switch-expressions and no non-ASCII in @code, and a class root selector in the SCSS. LOCAL/visual-only (no [Sync]).', {
17
+ name: z
18
+ .string()
19
+ .optional()
20
+ .describe("Class/file name for the generated .razor. Defaults to 'InteractionPrompt'"),
21
+ directory: z
22
+ .string()
23
+ .optional()
24
+ .describe("Subdirectory for the generated .razor + .razor.scss. Defaults to 'Code/UI'"),
25
+ action: z
26
+ .string()
27
+ .optional()
28
+ .describe("Verb woven into the default prompt text ('Press E to <action>'). Defaults to 'use'"),
29
+ range: z
30
+ .number()
31
+ .optional()
32
+ .describe("Eye-trace reach in world units — how close the crosshair must be to a pressable to show the prompt. Defaults to 120"),
33
+ }, async (params) => {
34
+ const res = await bridge.send("add_interaction_prompt", params);
35
+ if (!res.success) {
36
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
37
+ }
38
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
39
+ });
40
+ // ── create_hold_to_confirm ────────────────────────────────────────
41
+ server.tool("create_hold_to_confirm", "Generate a hold-to-confirm action component (sealed Component). While a named input action is held (Input.Down), a public Progress value fills 0→1 over [Property] float HoldSeconds; releasing early snaps back to 0, or drains down if [Property] bool DecayOnRelease. Reaching 1 fires the static OnConfirmed(GameObject) event, then a short CooldownSeconds blocks re-triggering. The classic 'hold E to disarm / open / revive' interaction. No UI is generated — read the public Progress (0..1) from your own HUD to draw a radial or bar; a #region Feedback hook marks where to tie in a sound/effect. LOCAL/owner-only: input is IsProxy-guarded so it never fires on proxies and is single-player safe. For a host-authoritative outcome, call an [Rpc.Host] from inside the OnConfirmed subscriber. Attach to the player (or any owned object that reads input); optionally attach to an existing GameObject by GUID after a hotload.", {
42
+ name: z
43
+ .string()
44
+ .optional()
45
+ .describe("Class name for the generated component. Defaults to 'HoldToConfirm'"),
46
+ directory: z
47
+ .string()
48
+ .optional()
49
+ .describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
50
+ action: z
51
+ .string()
52
+ .optional()
53
+ .describe("Input action name that must be held (must exist in the project's Input settings — see ensure_input_action). Defaults to 'use'"),
54
+ holdSeconds: z
55
+ .number()
56
+ .optional()
57
+ .describe("Seconds of continuous hold required to confirm. Defaults to 1.5"),
58
+ decayOnRelease: z
59
+ .boolean()
60
+ .optional()
61
+ .describe("Baked default for DecayOnRelease: if true, releasing early drains Progress back down instead of snapping to 0 (editable per-instance). Defaults to false"),
62
+ targetId: z
63
+ .string()
64
+ .optional()
65
+ .describe("GUID of a GameObject to attach the component to (only attaches if the type is already loaded — hotload first)"),
66
+ }, async (params) => {
67
+ const res = await bridge.send("create_hold_to_confirm", params);
68
+ if (!res.success) {
69
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
70
+ }
71
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
72
+ });
73
+ // ── create_carry_system ───────────────────────────────────────────
74
+ server.tool("create_carry_system", "Generate a first-person pickup / carry / throw component (sealed Component) for physics props. Attach it to the PLAYER (the object that owns the camera). It eye-traces from Scene.Camera for a Rigidbody-bearing GameObject tagged [Property] CarryTag (default 'carryable') within [Property] Range; grabbing routes a host-authoritative [Rpc.Host] request that re-validates the target and caller, hands the object's network ownership to the carrier (GameObject.Network.AssignOwnership), and disables the rigidbody's MotionEnabled while held. The held object follows a hold point ([Property] Vector3 HoldOffset in front of the camera) each FixedUpdate; dropping restores physics, throwing applies an impulse ([Property] float ThrowForce). The held-object id is [Sync(SyncFlags.FromHost)] so proxies see the carrying state, and static OnPickedUp / OnDropped events fire uniformly for SFX/VFX. PAIRS with physics props — give each carryable a Rigidbody + Collider and the CarryTag (set_tags); network-spawn them for multiplayer so ownership + transform replicate. Single-player safe (IsProxy is false and RPCs run locally with no session). Inputs: GrabAction (default 'use') grabs/drops, ThrowAction (default 'attack1') throws. Optionally attach to an existing player GameObject by GUID after a hotload.", {
75
+ name: z
76
+ .string()
77
+ .optional()
78
+ .describe("Class name for the generated component. Defaults to 'CarrySystem'"),
79
+ directory: z
80
+ .string()
81
+ .optional()
82
+ .describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
83
+ range: z
84
+ .number()
85
+ .optional()
86
+ .describe("Eye-trace reach for grabbing a carryable, in world units. Defaults to 130"),
87
+ throwForce: z
88
+ .number()
89
+ .optional()
90
+ .describe("Impulse magnitude applied on throw (scales with the prop's mass — tune per game). Defaults to 20000"),
91
+ carryTag: z
92
+ .string()
93
+ .optional()
94
+ .describe("Only objects with this tag (and a Rigidbody) can be picked up; lower-cased/underscored to match s&box tag convention. Defaults to 'carryable'"),
95
+ targetId: z
96
+ .string()
97
+ .optional()
98
+ .describe("GUID of the PLAYER GameObject (the one with the camera) to attach to (only attaches if the type is already loaded — hotload first)"),
99
+ }, async (params) => {
100
+ const res = await bridge.send("create_carry_system", params);
101
+ if (!res.success) {
102
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
103
+ }
104
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
105
+ });
106
+ }
107
+ //# sourceMappingURL=interactionpack.js.map
@@ -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)"),
@@ -0,0 +1,18 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { BridgeClient } from "../transport/bridge-client.js";
3
+ /**
4
+ * Loot / Economy variants pack — three Tier-2 scaffolds (v1.20.0, Track D):
5
+ *
6
+ * - create_gacha_drop_table per-rarity roll + pity counter + duplicate detection,
7
+ * host-authoritative ([Rpc.Host] roll → [Rpc.Broadcast])
8
+ * - create_currency_pickup networked coin: optional magnet + host-validated grant
9
+ * into create_economy_wallet's AddMoney + replicated despawn
10
+ * - create_offline_progress DateTime delta on enable + clamp + deterministic tick replay
11
+ *
12
+ * All generate a clean, self-contained sealed Component .cs; file/scene-mutating, refused
13
+ * during play mode by the bridge dispatch. create_currency_pickup + create_offline_progress
14
+ * emit host/owner-authoritative code ([Sync]-free state stays host-side); create_gacha_drop_table
15
+ * routes the roll through [Rpc.Host] and announces via [Rpc.Broadcast].
16
+ */
17
+ export declare function registerLootEconomyTools(server: McpServer, bridge: BridgeClient): void;
18
+ //# sourceMappingURL=looteconomy.d.ts.map