sbox-mcp-server 1.20.0 → 2.1.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 (59) hide show
  1. package/dist/index.js +125 -82
  2. package/dist/tools/aisystems.d.ts +4 -0
  3. package/dist/tools/aisystems.js +236 -0
  4. package/dist/tools/assets.js +5 -5
  5. package/dist/tools/audio.js +12 -19
  6. package/dist/tools/audit.d.ts +8 -0
  7. package/dist/tools/audit.js +65 -0
  8. package/dist/tools/batch.d.ts +8 -0
  9. package/dist/tools/batch.js +77 -0
  10. package/dist/tools/characters.js +6 -6
  11. package/dist/tools/cinematicrecording.d.ts +31 -0
  12. package/dist/tools/cinematicrecording.js +107 -0
  13. package/dist/tools/cinematics.js +2 -2
  14. package/dist/tools/components.js +2 -2
  15. package/dist/tools/debugdraw.js +4 -4
  16. package/dist/tools/diagnostics.js +7 -5
  17. package/dist/tools/dialoguefx.d.ts +4 -0
  18. package/dist/tools/dialoguefx.js +144 -0
  19. package/dist/tools/discovery.js +6 -6
  20. package/dist/tools/economysave.d.ts +27 -0
  21. package/dist/tools/economysave.js +231 -0
  22. package/dist/tools/gameobjects.js +7 -7
  23. package/dist/tools/gameplay.js +4 -4
  24. package/dist/tools/gameplayrecorder.d.ts +18 -0
  25. package/dist/tools/gameplayrecorder.js +73 -0
  26. package/dist/tools/inspection.js +6 -6
  27. package/dist/tools/leveltools.js +5 -5
  28. package/dist/tools/materials.js +4 -4
  29. package/dist/tools/movieauthoring.d.ts +31 -0
  30. package/dist/tools/movieauthoring.js +84 -0
  31. package/dist/tools/moviemaker.js +1 -1
  32. package/dist/tools/multiplayertest.d.ts +23 -0
  33. package/dist/tools/multiplayertest.js +62 -0
  34. package/dist/tools/networking.js +22 -22
  35. package/dist/tools/physics.js +4 -4
  36. package/dist/tools/playmode.js +5 -5
  37. package/dist/tools/playtest.js +2 -1
  38. package/dist/tools/prefabs.js +7 -3
  39. package/dist/tools/project.js +2 -2
  40. package/dist/tools/publishing.js +3 -3
  41. package/dist/tools/roundui.d.ts +22 -0
  42. package/dist/tools/roundui.js +88 -0
  43. package/dist/tools/scenes.js +2 -2
  44. package/dist/tools/scripts.js +3 -3
  45. package/dist/tools/statsachievements.d.ts +4 -0
  46. package/dist/tools/statsachievements.js +243 -0
  47. package/dist/tools/status.js +1 -1
  48. package/dist/tools/templates.js +7 -7
  49. package/dist/tools/ui.js +5 -5
  50. package/dist/tools/uifeedback.js +1 -1
  51. package/dist/tools/vehicles.d.ts +8 -0
  52. package/dist/tools/vehicles.js +98 -0
  53. package/dist/tools/visuals.js +8 -8
  54. package/dist/tools/workflow.d.ts +8 -0
  55. package/dist/tools/workflow.js +107 -0
  56. package/dist/tools/world.js +61 -58
  57. package/dist/tools/worldrender.d.ts +4 -0
  58. package/dist/tools/worldrender.js +183 -0
  59. package/package.json +1 -1
@@ -64,13 +64,13 @@ export function registerWorldTools(server, bridge) {
64
64
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
65
65
  });
66
66
  // ── add_terrain_hill ─────────────────────────────────────────────
67
- server.tool("add_terrain_hill", "Add a hill (cosine-falloff bump) to MapBuilder. Negative height creates a depression.", {
67
+ server.tool("add_terrain_hill", "Add a hill (cosine-falloff bump) to MapBuilder's Hills list. Negative height creates a depression. Returns `added`, `total` (hills now in the list), and `rebuilt`; verify the surface with raycast_terrain at the hill center.", {
68
68
  x: z.number().describe("World X of hill center"),
69
69
  y: z.number().describe("World Y of hill center"),
70
70
  radius: z.number().default(500).describe("Hill radius in world units"),
71
71
  height: z.number().default(100).describe("Peak height (negative for depression)"),
72
72
  rebuild: z.boolean().default(true).describe("Rebuild terrain after adding (set false to batch)"),
73
- id: z.string().optional(),
73
+ id: z.string().optional().describe("GUID of the GameObject holding the MapBuilder; omit to auto-find the first MapBuilder in the scene"),
74
74
  component: z
75
75
  .string()
76
76
  .optional()
@@ -82,12 +82,12 @@ export function registerWorldTools(server, bridge) {
82
82
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
83
83
  });
84
84
  // ── add_terrain_clearing ─────────────────────────────────────────
85
- server.tool("add_terrain_clearing", "Add a flat clearing zone to MapBuilder (lerps height toward base inside radius).", {
86
- x: z.number(),
87
- y: z.number(),
88
- radius: z.number().default(300),
89
- rebuild: z.boolean().default(true),
90
- id: z.string().optional(),
85
+ server.tool("add_terrain_clearing", "Add a flat clearing zone to MapBuilder's Clearings list (lerps height toward base inside radius). Returns `added`, `total` (clearing count), and `rebuilt`. Rebuilds terrain immediately by default; set rebuild=false to batch edits, then rebuild once (rebuild=true on the last call, or invoke_button 'Build Terrain').", {
86
+ x: z.number().describe("World X of clearing center"),
87
+ y: z.number().describe("World Y of clearing center"),
88
+ radius: z.number().default(300).describe("Clearing radius in world units. Default 300."),
89
+ rebuild: z.boolean().default(true).describe("Rebuild terrain after adding (default true; set false to batch)"),
90
+ id: z.string().optional().describe("GUID of the GameObject holding the MapBuilder; omit to auto-find the first MapBuilder in the scene"),
91
91
  component: z
92
92
  .string()
93
93
  .optional()
@@ -99,11 +99,11 @@ export function registerWorldTools(server, bridge) {
99
99
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
100
100
  });
101
101
  // ── add_terrain_trail ────────────────────────────────────────────
102
- server.tool("add_terrain_trail", "Carve a trail depression between two points on MapBuilder.", {
103
- from: z.object({ x: z.number(), y: z.number() }),
104
- to: z.object({ x: z.number(), y: z.number() }),
105
- rebuild: z.boolean().default(true),
106
- id: z.string().optional(),
102
+ server.tool("add_terrain_trail", "Carve a trail depression between two points on MapBuilder (appends to its Trails list). Returns `added`, `total` (trail count), and `rebuilt`. Rebuilds terrain immediately by default; set rebuild=false to batch, then invoke_button 'Build Terrain' once.", {
103
+ from: z.object({ x: z.number(), y: z.number() }).describe("Trail start point (world x/y)"),
104
+ to: z.object({ x: z.number(), y: z.number() }).describe("Trail end point (world x/y)"),
105
+ rebuild: z.boolean().default(true).describe("Rebuild terrain after adding (default true; set false to batch)"),
106
+ id: z.string().optional().describe("GUID of the GameObject holding the MapBuilder; omit to auto-find the first MapBuilder in the scene"),
107
107
  component: z
108
108
  .string()
109
109
  .optional()
@@ -115,12 +115,13 @@ export function registerWorldTools(server, bridge) {
115
115
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
116
116
  });
117
117
  // ── clear_terrain_features ───────────────────────────────────────
118
- server.tool("clear_terrain_features", "Wipe Hills, Clearings, Trails, or all features from MapBuilder. 'what' is one of: Hills, Clearings, Trails, CavePath, all (default).", {
118
+ server.tool("clear_terrain_features", "Wipe MapBuilder feature lists — Hills, Clearings, Trails, CavePath, or all of them (default). Destructive: the feature definitions are removed. Returns `cleared` (map of list name entries removed) and `rebuilt`; re-add features with add_terrain_hill / add_terrain_clearing / add_terrain_trail.", {
119
119
  what: z
120
120
  .enum(["Hills", "Clearings", "Trails", "CavePath", "all"])
121
- .default("all"),
122
- rebuild: z.boolean().default(true),
123
- id: z.string().optional(),
121
+ .default("all")
122
+ .describe("Which feature list to clear. Default 'all' (clears all four)."),
123
+ rebuild: z.boolean().default(true).describe("Rebuild terrain after clearing (default true)"),
124
+ id: z.string().optional().describe("GUID of the GameObject holding the MapBuilder; omit to auto-find the first MapBuilder in the scene"),
124
125
  component: z
125
126
  .string()
126
127
  .optional()
@@ -132,17 +133,17 @@ export function registerWorldTools(server, bridge) {
132
133
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
133
134
  });
134
135
  // ── add_cave_waypoint ────────────────────────────────────────────
135
- server.tool("add_cave_waypoint", "Append (or insert) a waypoint to CaveBuilder.Path. Z is depth (negative = underground).", {
136
- x: z.number(),
137
- y: z.number(),
136
+ server.tool("add_cave_waypoint", "Append (or insert at `index`) a waypoint to CaveBuilder.Path. Z is depth (negative = underground). Returns `added`, `total` (waypoint count), and `rebuilt` — rebuilds the cave ('Build Cave') immediately unless rebuild=false.", {
137
+ x: z.number().describe("World X of the waypoint"),
138
+ y: z.number().describe("World Y of the waypoint"),
138
139
  z: z.number().default(0).describe("Z depth — negative = underground"),
139
140
  index: z
140
141
  .number()
141
142
  .int()
142
143
  .optional()
143
144
  .describe("Optional insert position (default: append to end)"),
144
- rebuild: z.boolean().default(true),
145
- id: z.string().optional(),
145
+ rebuild: z.boolean().default(true).describe("Rebuild the cave after adding (default true; set false to batch)"),
146
+ id: z.string().optional().describe("GUID of the GameObject holding the CaveBuilder; omit to auto-find the first CaveBuilder in the scene"),
146
147
  component: z
147
148
  .string()
148
149
  .optional()
@@ -154,8 +155,8 @@ export function registerWorldTools(server, bridge) {
154
155
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
155
156
  });
156
157
  // ── clear_cave_path ──────────────────────────────────────────────
157
- server.tool("clear_cave_path", "Clear all waypoints in CaveBuilder and remove the cave from the scene.", {
158
- id: z.string().optional(),
158
+ server.tool("clear_cave_path", "Clear all waypoints in CaveBuilder.Path and remove the built cave from the scene (invokes 'Clear Cave'). Destructive. Returns `cleared` — the number of waypoints removed; start a new path with add_cave_waypoint.", {
159
+ id: z.string().optional().describe("GUID of the GameObject holding the CaveBuilder; omit to auto-find the first CaveBuilder in the scene"),
159
160
  component: z
160
161
  .string()
161
162
  .optional()
@@ -167,11 +168,11 @@ export function registerWorldTools(server, bridge) {
167
168
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
168
169
  });
169
170
  // ── add_forest_poi ───────────────────────────────────────────────
170
- server.tool("add_forest_poi", "Add a point of interest (clearing) to ForestGenerator.POIs. Returns the index of the new POI for use with add_forest_trail.", {
171
- name: z.string().default("POI"),
172
- x: z.number(),
173
- y: z.number(),
174
- radius: z.number().default(300),
171
+ server.tool("add_forest_poi", "Add a point of interest (clearing) to ForestGenerator.POIs. Returns `added`, `index` (the new POI's index pass it to add_forest_trail as from_index/to_index), `total`, and `rebuilt`. Forest gen is slow (~1s), so rebuild defaults to false — batch your POIs/trails, then regenerate once (rebuild=true on the last call, or invoke_button 'Generate Forest').", {
172
+ name: z.string().default("POI").describe("Display name for the POI. Default 'POI'."),
173
+ x: z.number().describe("World X of POI center"),
174
+ y: z.number().describe("World Y of POI center"),
175
+ radius: z.number().default(300).describe("POI radius in world units. Default 300."),
175
176
  density_multiplier: z
176
177
  .number()
177
178
  .default(1)
@@ -180,7 +181,7 @@ export function registerWorldTools(server, bridge) {
180
181
  .boolean()
181
182
  .default(false)
182
183
  .describe("Forest gen is slow (~1s); default false to batch"),
183
- id: z.string().optional(),
184
+ id: z.string().optional().describe("GUID of the GameObject holding the ForestGenerator; omit to auto-find the first ForestGenerator in the scene"),
184
185
  component: z
185
186
  .string()
186
187
  .optional()
@@ -192,11 +193,11 @@ export function registerWorldTools(server, bridge) {
192
193
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
193
194
  });
194
195
  // ── add_forest_trail ─────────────────────────────────────────────
195
- server.tool("add_forest_trail", "Add a trail gap between two POIs (by index) to ForestGenerator.Trails.", {
196
- from_index: z.number().int(),
197
- to_index: z.number().int(),
198
- rebuild: z.boolean().default(false),
199
- id: z.string().optional(),
196
+ server.tool("add_forest_trail", "Add a trail gap between two POIs to ForestGenerator.Trails. Returns `added`, `total` (trail count), and `rebuilt`. rebuild defaults to false — nothing changes visually until you regenerate (rebuild=true, or invoke_button 'Generate Forest').", {
197
+ from_index: z.number().int().describe("Index of the start POI (the `index` returned by add_forest_poi)"),
198
+ to_index: z.number().int().describe("Index of the end POI (the `index` returned by add_forest_poi)"),
199
+ rebuild: z.boolean().default(false).describe("Regenerate the forest after adding (default false — batch, then rebuild once)"),
200
+ id: z.string().optional().describe("GUID of the GameObject holding the ForestGenerator; omit to auto-find the first ForestGenerator in the scene"),
200
201
  component: z
201
202
  .string()
202
203
  .optional()
@@ -208,10 +209,10 @@ export function registerWorldTools(server, bridge) {
208
209
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
209
210
  });
210
211
  // ── set_forest_seed ──────────────────────────────────────────────
211
- server.tool("set_forest_seed", "Set ForestGenerator.Seed and regenerate. Useful for re-rolling layouts.", {
212
- seed: z.number().int().default(77),
213
- rebuild: z.boolean().default(true),
214
- id: z.string().optional(),
212
+ server.tool("set_forest_seed", "Set ForestGenerator.Seed and (by default) regenerate re-rolls the forest layout while keeping POIs, trails, and density regions. Returns `set`, `seed`, and `rebuilt`; take a screenshot afterwards (screenshot_from) to judge the new layout.", {
213
+ seed: z.number().int().default(77).describe("New random seed (integer). Default 77."),
214
+ rebuild: z.boolean().default(true).describe("Regenerate the forest after setting (default true)"),
215
+ id: z.string().optional().describe("GUID of the GameObject holding the ForestGenerator; omit to auto-find the first ForestGenerator in the scene"),
215
216
  component: z
216
217
  .string()
217
218
  .optional()
@@ -223,8 +224,8 @@ export function registerWorldTools(server, bridge) {
223
224
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
224
225
  });
225
226
  // ── clear_forest_pois ────────────────────────────────────────────
226
- server.tool("clear_forest_pois", "Wipe all POIs and trails in ForestGenerator and clear placed forest objects from the scene.", {
227
- id: z.string().optional(),
227
+ server.tool("clear_forest_pois", "Wipe all POIs and trails in ForestGenerator and clear placed forest objects from the scene (invokes 'Clear Forest'). Destructive. Returns `cleared` — the number of POIs removed; rebuild a layout with add_forest_poi + add_forest_trail, then invoke_button 'Generate Forest'.", {
228
+ id: z.string().optional().describe("GUID of the GameObject holding the ForestGenerator; omit to auto-find the first ForestGenerator in the scene"),
228
229
  component: z
229
230
  .string()
230
231
  .optional()
@@ -236,13 +237,13 @@ export function registerWorldTools(server, bridge) {
236
237
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
237
238
  });
238
239
  // ── sculpt_terrain ───────────────────────────────────────────────
239
- server.tool("sculpt_terrain", "Apply a heightmap brush at (x, y) to MapBuilder. Modes: raise, lower, flatten, smooth. Modifies the current heightmap directly and rebuilds the mesh; survives between calls until you press Build Terrain again.", {
240
+ server.tool("sculpt_terrain", "Apply a heightmap brush at (x, y) to MapBuilder. Modes: raise, lower, flatten, smooth. Modifies the current heightmap directly and rebuilds the mesh; edits survive between calls but are lost when 'Build Terrain' regenerates from the feature lists. Returns `sculpted`, `mode`, and `affected_vertices`; verify with raycast_terrain at the brush center.", {
240
241
  x: z.number().describe("World X of brush center"),
241
242
  y: z.number().describe("World Y of brush center"),
242
243
  radius: z.number().default(400).describe("Brush radius in world units"),
243
244
  strength: z.number().default(50).describe("Height delta (units) for raise/lower; ignored for flatten/smooth"),
244
- mode: z.enum(["raise", "lower", "flatten", "smooth"]).default("raise"),
245
- id: z.string().optional(),
245
+ mode: z.enum(["raise", "lower", "flatten", "smooth"]).default("raise").describe("Brush mode. Default 'raise'. `strength` applies to raise/lower only."),
246
+ id: z.string().optional().describe("GUID of the GameObject holding the MapBuilder; omit to auto-find the first MapBuilder in the scene"),
246
247
  component: z
247
248
  .string()
248
249
  .optional()
@@ -254,13 +255,13 @@ export function registerWorldTools(server, bridge) {
254
255
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
255
256
  });
256
257
  // ── paint_forest_density ─────────────────────────────────────────
257
- server.tool("paint_forest_density", "Add a circular biome region with overridden forest density. Multiple regions stack via cosine falloff. density: 0=no trees, 1=normal, 2=double.", {
258
- x: z.number(),
259
- y: z.number(),
260
- radius: z.number().default(800),
258
+ server.tool("paint_forest_density", "Add a circular biome region with overridden forest density to ForestGenerator.DensityRegions. Multiple regions stack via cosine falloff. density: 0=no trees, 1=normal, 2=double. Returns `painted`, `total` (region count), and `rebuilt` — rebuild defaults to false, so regenerate (rebuild=true, or invoke_button 'Generate Forest') to see the change.", {
259
+ x: z.number().describe("World X of region center"),
260
+ y: z.number().describe("World Y of region center"),
261
+ radius: z.number().default(800).describe("Region radius in world units. Default 800."),
261
262
  density: z.number().default(1).describe("Density multiplier (0=clear, 1=normal, 2=dense)"),
262
- rebuild: z.boolean().default(false),
263
- id: z.string().optional(),
263
+ rebuild: z.boolean().default(false).describe("Regenerate the forest after adding (default false; batch, then rebuild once)"),
264
+ id: z.string().optional().describe("GUID of the GameObject holding the ForestGenerator; omit to auto-find the first ForestGenerator in the scene"),
264
265
  component: z
265
266
  .string()
266
267
  .optional()
@@ -272,7 +273,7 @@ export function registerWorldTools(server, bridge) {
272
273
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
273
274
  });
274
275
  // ── place_along_path ─────────────────────────────────────────────
275
- server.tool("place_along_path", "Drop instances of a model along a path (list of points). Useful for fences, lampposts, road markers, lined-up rocks.", {
276
+ server.tool("place_along_path", "Drop instances of a model along a path (list of points). Useful for fences, lampposts, road markers, lined-up rocks. Returns `placed` (instance count) and `folder` — the GUID of the new parent GameObject grouping the instances; pass `folder` to delete_gameobject to remove the whole run, or to set_parent/set_transform to move it.", {
276
277
  model: z.string().describe("Model path (e.g. 'models/dev/box.vmdl' or installed-asset path)"),
277
278
  points: z
278
279
  .array(z.object({ x: z.number(), y: z.number(), z: z.number().default(0) }))
@@ -280,9 +281,9 @@ export function registerWorldTools(server, bridge) {
280
281
  .describe("Path waypoints (at least 2)"),
281
282
  spacing: z.number().default(200).describe("Distance between placements (world units)"),
282
283
  jitter: z.number().default(0).describe("Max random offset perpendicular to path"),
283
- min_scale: z.number().default(1),
284
- max_scale: z.number().default(1),
285
- seed: z.number().int().default(42),
284
+ min_scale: z.number().default(1).describe("Minimum uniform scale per instance (random between min and max). Default 1."),
285
+ max_scale: z.number().default(1).describe("Maximum uniform scale per instance. Default 1."),
286
+ seed: z.number().int().default(42).describe("Random seed for jitter and scale — same seed reproduces the same placement. Default 42."),
286
287
  name: z.string().default("PathItem").describe("Base name for placed objects"),
287
288
  }, async (params) => {
288
289
  const res = await bridge.send("place_along_path", params);
@@ -291,10 +292,10 @@ export function registerWorldTools(server, bridge) {
291
292
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
292
293
  });
293
294
  // ── build_terrain_mesh ───────────────────────────────────────────
294
- server.tool("build_terrain_mesh", "Build a standalone heightmap terrain mesh from a hills/clearings JSON spec — independent of MapBuilder. Use when you don't have a MapBuilder component in the scene and want one-shot terrain.", {
295
+ server.tool("build_terrain_mesh", "Build a standalone heightmap terrain mesh (a MeshComponent) from a hills/clearings JSON spec — independent of MapBuilder. Use when you don't have a MapBuilder component in the scene and want one-shot terrain. Returns `built`, `id` (the new GameObject's GUID), `name`, `vertices`, and `faces`; pass `id` to assign_material, set_transform, or delete_gameobject. Note: raycast_terrain cannot sample this mesh (it requires MapBuilder).", {
295
296
  size: z.number().default(9600).describe("Total terrain size (world units, square)"),
296
297
  resolution: z.number().int().default(64).describe("Grid resolution per side"),
297
- name: z.string().default("Generated Terrain"),
298
+ name: z.string().default("Generated Terrain").describe("Name for the created terrain GameObject. Default 'Generated Terrain'."),
298
299
  hills: z
299
300
  .array(z.object({
300
301
  x: z.number(),
@@ -302,10 +303,12 @@ export function registerWorldTools(server, bridge) {
302
303
  radius: z.number().default(500),
303
304
  height: z.number().default(100),
304
305
  }))
305
- .default([]),
306
+ .default([])
307
+ .describe("Hill bumps: array of {x, y, radius (default 500), height (default 100; negative = depression)}. Default: none."),
306
308
  clearings: z
307
309
  .array(z.object({ x: z.number(), y: z.number(), radius: z.number().default(300) }))
308
- .default([]),
310
+ .default([])
311
+ .describe("Zones flattened back toward height 0: array of {x, y, radius (default 300)}. Default: none."),
309
312
  }, async (params) => {
310
313
  const res = await bridge.send("build_terrain_mesh", params);
311
314
  if (!res.success)
@@ -0,0 +1,4 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { BridgeClient } from "../transport/bridge-client.js";
3
+ export declare function registerWorldRenderTools(server: McpServer, bridge: BridgeClient): void;
4
+ //# sourceMappingURL=worldrender.d.ts.map
@@ -0,0 +1,183 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * World & Render family — three tools:
4
+ *
5
+ * - add_water_body WaterVolume physics volume + trigger BoxCollider
6
+ * footprint + optional tinted visual surface
7
+ * - add_render_target_camera secondary camera → render-target texture →
8
+ * display surface (CCTV / mirror / portal)
9
+ * - add_daynight_sun codegen sun/sky driver bound to the component
10
+ * generated by create_day_night_clock
11
+ *
12
+ * add_water_body is scene-direct; the other two generate a sealed game-code
13
+ * component (hotload to compile). All three are scene/file-mutating and are
14
+ * refused during play mode by the bridge dispatch.
15
+ */
16
+ // Colour / vector accepted as EITHER an object OR a comma string, passed
17
+ // through unchanged. The C# handler parses both forms (source of truth). See
18
+ // the cross-language vector/color contract.
19
+ const ColorObject = z.object({
20
+ r: z.number().min(0).describe("Red, 0-1"),
21
+ g: z.number().min(0).describe("Green, 0-1"),
22
+ b: z.number().min(0).describe("Blue, 0-1"),
23
+ a: z.number().min(0).max(1).optional().describe("Alpha, 0-1 (default 1)"),
24
+ });
25
+ const ColorSchema = z
26
+ .union([
27
+ ColorObject,
28
+ z.string().describe('Comma string "r,g,b,a", e.g. "1,0,0,1"'),
29
+ ])
30
+ .describe('RGBA colour — object {r,g,b,a} (0-1) OR comma string "r,g,b,a"');
31
+ const Vector3Object = z.object({ x: z.number(), y: z.number(), z: z.number() });
32
+ const Vector3Schema = z
33
+ .union([
34
+ Vector3Object,
35
+ z.string().describe('Comma string "x,y,z", e.g. "0,0,200"'),
36
+ ])
37
+ .describe('World position — object {x,y,z} OR comma string "x,y,z"');
38
+ const RotationSchema = z
39
+ .object({ pitch: z.number(), yaw: z.number(), roll: z.number() })
40
+ .describe("Rotation {pitch,yaw,roll} in degrees");
41
+ export function registerWorldRenderTools(server, bridge) {
42
+ // ── add_water_body ───────────────────────────────────────────────
43
+ server.tool("add_water_body", "Create a swimmable/buoyant water region: a GameObject with a WaterVolume (s&box's buoyancy + drag physics volume, region defined by a trigger BoxCollider sized to `size`) plus, by default, a simple flat translucent-blue surface box at the water line so it's visible. HONEST LIMIT: the visual is a tinted dev-box stand-in, NOT a Gerstner/water shader — the dev material may render the tint opaque; swap in a real water material later. Only the WaterVolume properties you pass are set (engine defaults otherwise). Returns {created, gameObject{id,...}, size, appliedProperties[], surface{id,...}|null, note, nextSteps[]}. Next: start_play and drop a physics prop in to verify buoyancy; tune FluidDensity/LinearDrag/WaveAmplitude etc. with set_property on the WaterVolume. Scene-mutating — refused during play mode.", {
44
+ name: z
45
+ .string()
46
+ .optional()
47
+ .describe("GameObject name. Defaults to 'Water'"),
48
+ position: Vector3Schema.optional().describe("World position of the volume's CENTRE (the surface sits at z + size.z/2 + surfaceOffset)"),
49
+ size: z
50
+ .string()
51
+ .optional()
52
+ .describe("Full extents of the volume as 'x,y,z' world units. Defaults to '512,512,128'"),
53
+ density: z
54
+ .number()
55
+ .optional()
56
+ .describe("WaterVolume.FluidDensity — higher = more buoyant. Engine default when omitted"),
57
+ linearDrag: z
58
+ .number()
59
+ .optional()
60
+ .describe("WaterVolume.LinearDrag on submerged bodies. Engine default when omitted"),
61
+ angularDrag: z
62
+ .number()
63
+ .optional()
64
+ .describe("WaterVolume.AngularDrag on submerged bodies. Engine default when omitted"),
65
+ waveAmplitude: z
66
+ .number()
67
+ .optional()
68
+ .describe("WaterVolume.WaveAmplitude — physics wave height (does NOT animate the visual surface)"),
69
+ waveFrequency: z
70
+ .number()
71
+ .optional()
72
+ .describe("WaterVolume.WaveFrequency — physics wave frequency"),
73
+ fluidVelocity: Vector3Schema.optional().describe("WaterVolume.FluidVelocity — current/flow direction and speed, world units/s"),
74
+ surfaceOffset: z
75
+ .number()
76
+ .optional()
77
+ .describe("WaterVolume.SurfaceOffset — shifts the water line relative to the top of the box"),
78
+ visualSurface: z
79
+ .boolean()
80
+ .optional()
81
+ .describe("Create the tinted surface box child. Defaults to true"),
82
+ surfaceColor: ColorSchema.optional().describe("Tint of the visual surface. Defaults to translucent blue (0.15,0.35,0.55,0.6)"),
83
+ parentId: z.string().optional().describe("GUID of a parent GameObject"),
84
+ }, async (params) => {
85
+ const res = await bridge.send("add_water_body", params);
86
+ if (!res.success) {
87
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
88
+ }
89
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
90
+ });
91
+ // ── add_render_target_camera ─────────────────────────────────────
92
+ server.tool("add_render_target_camera", "CCTV / security monitor / mirror / simple portal: creates a secondary camera (IsMainCamera=false, Priority param) positioned/rotated or aimed at a target object, a display surface (scaled box with a ModelRenderer), and GENERATES a sealed wiring component that at runtime creates the render-target Texture (Texture.CreateRenderTarget builder), assigns camera.RenderTarget and sets the texture onto an anonymous complex-shader material on the display (attribute 'Color' = albedo; the screen is scene-lit, not emissive/unlit). Returns {created, camera{id,...}, display{id,...}|null, className, path, generatedCode, attachedAndWired, resolution, nextSteps[]}. FIRST call generates the .cs → follow nextSteps: trigger_hotload, then add_component_with_properties on the display with properties {SourceCamera: <camera GUID>}; a REPEAT call reusing the same `name` after the hotload attaches AND wires everything automatically (attachedAndWired=true). The feed only renders in play mode — verify with start_play + capture_view. If `name`'s .cs file exists but isn't compiled yet, the call errors — hotload first. Scene/file-mutating — refused during play mode.", {
93
+ name: z
94
+ .string()
95
+ .optional()
96
+ .describe("Class name for the generated wiring component. Defaults to 'RenderTargetDisplay'. Reuse the same name for extra cameras — after a hotload the class is reused and wired in one shot"),
97
+ directory: z
98
+ .string()
99
+ .optional()
100
+ .describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
101
+ cameraName: z
102
+ .string()
103
+ .optional()
104
+ .describe("GameObject name for the camera. Defaults to 'RT Camera'"),
105
+ position: Vector3Schema.optional().describe("World position of the camera"),
106
+ rotation: RotationSchema.optional().describe("World rotation of the camera"),
107
+ lookAtId: z
108
+ .string()
109
+ .optional()
110
+ .describe("GUID of a GameObject to aim the camera at (overrides rotation; from get_scene_hierarchy / find_objects)"),
111
+ priority: z
112
+ .number()
113
+ .optional()
114
+ .describe("CameraComponent.Priority (integer). Defaults to 1"),
115
+ fieldOfView: z
116
+ .number()
117
+ .optional()
118
+ .describe("Camera field of view in degrees (clamped 5-170). Engine default when omitted"),
119
+ resolution: z
120
+ .string()
121
+ .optional()
122
+ .describe("Render-target size as 'width,height' pixels (min 16). Defaults to '512,512'"),
123
+ backgroundColor: ColorSchema.optional().describe("Camera background colour. Engine default when omitted"),
124
+ createDisplay: z
125
+ .boolean()
126
+ .optional()
127
+ .describe("Create the display surface GameObject. Defaults to true. Pass false to wire your own surface later"),
128
+ displayPosition: Vector3Schema.optional().describe("World position of the display surface"),
129
+ displayRotation: RotationSchema.optional().describe("World rotation of the display surface"),
130
+ displaySize: z
131
+ .string()
132
+ .optional()
133
+ .describe("Display box extents as 'x,y,z' world units (thin x = a wall screen facing ±x). Defaults to '4,128,72'"),
134
+ attribute: z
135
+ .string()
136
+ .optional()
137
+ .describe("Material attribute that receives the texture. Defaults to 'Color' (albedo on the complex shader). Sanitized to [A-Za-z0-9_]"),
138
+ }, async (params) => {
139
+ const res = await bridge.send("add_render_target_camera", params);
140
+ if (!res.success) {
141
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
142
+ }
143
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
144
+ });
145
+ // ── add_daynight_sun ─────────────────────────────────────────────
146
+ server.tool("add_daynight_sun", "Generate a sun/sky driver component for the clock made by create_day_night_clock — a COMPANION, not a replacement: the clock owns TimeOfDay, this renders it. Every frame it reads TimeOfDay/SunriseHour/SunsetHour off the clock (sibling first, else first in scene, retrying 1/s) and drives a DirectionalLight: sunrise→sunset maps to a 0-180° pitch arc (continuing below the horizon at night), LightColor runs a hardcoded gradient (warm dawn → white noon → orange dusk → dim blue night, HDR-scaled by Intensity since s&box lights have no Brightness field), and optionally lerps the SkyBox2D Tint between night blue and day white. The clock CLASS NAME is baked in as a typed reference — the call ERRORS if `clockClass` isn't found in the TypeLibrary or any project .cs (run create_day_night_clock first; a bad token would break the whole game-assembly compile). Returns {created, path, className, clockClass, sunYaw, intensity, driveSkybox, placedOn, note, nextSteps[]}. Next: trigger_hotload → attach to the DirectionalLight's GameObject → start_play, set_runtime_property TimeOfDay on the clock (6/12/20/0) + capture_view to verify the looks. File/scene-mutating — refused during play mode.", {
147
+ name: z
148
+ .string()
149
+ .optional()
150
+ .describe("Class name for the generated component. Defaults to 'DayNightSun'"),
151
+ directory: z
152
+ .string()
153
+ .optional()
154
+ .describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
155
+ clockClass: z
156
+ .string()
157
+ .optional()
158
+ .describe("Class name of the day-night clock to bind to (the `name` you gave create_day_night_clock). Defaults to 'DayNightClock'. Must already exist as a compiled type or a project .cs file"),
159
+ sunYaw: z
160
+ .number()
161
+ .optional()
162
+ .describe("Compass heading of the sun's arc in degrees. Defaults to 30"),
163
+ intensity: z
164
+ .number()
165
+ .optional()
166
+ .describe("Brightness multiplier on the colour gradient (HDR, >1 valid; intensity = colour magnitude). Defaults to 1"),
167
+ driveSkybox: z
168
+ .boolean()
169
+ .optional()
170
+ .describe("Also tint the scene's SkyBox2D between night blue and day white. Defaults to true"),
171
+ targetId: z
172
+ .string()
173
+ .optional()
174
+ .describe("GUID of a GameObject to attach to (only attaches if the type is already loaded — hotload first)"),
175
+ }, async (params) => {
176
+ const res = await bridge.send("add_daynight_sun", params);
177
+ if (!res.success) {
178
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
179
+ }
180
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
181
+ });
182
+ }
183
+ //# sourceMappingURL=worldrender.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbox-mcp-server",
3
- "version": "1.20.0",
3
+ "version": "2.1.0",
4
4
  "description": "MCP Server for s&box game engine — enables Claude to build games through conversation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",