sbox-mcp-server 1.7.0 → 1.9.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.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # sbox-mcp-server
2
2
 
3
- MCP Server for the s&box game engine. Lets Claude Code build s&box games through conversation — **150+ tools** for scenes, scripts, GameObjects, components, assets, materials, audio, physics, UI, networking, publishing, world-gen, lighting & atmosphere, characters, scene layout, navmesh & spatial queries, particles, self-diagnosis, console/C# execution, live docs search, and type discovery.
3
+ MCP Server for the s&box game engine. Lets Claude Code build s&box games through conversation — **170+ tools** for scenes, scripts, GameObjects, components, assets, materials, audio, physics, UI, networking, publishing, world-gen, lighting & atmosphere, characters, scene layout, navmesh & spatial queries, particles, animation, NPC brains, playable-game scaffolds, networking & scene inspection/lint, save & services queries, self-diagnosis, console/C# execution, live docs search, and type discovery.
4
4
 
5
5
  ## Fastest install — the Claude Code plugin
6
6
 
7
- If you use Claude Code, the easiest install is the companion plugin. It registers this MCP server automatically, ships a workflow skill, and includes the `sbox-game-dev` specialist agent.
7
+ If you use Claude Code, the easiest install is the companion plugin. It registers this MCP server automatically, ships the workflow + recipe skills (`sbox-build-feature`, `sbox-api`, `sbox-cookbook`, `sbox-scaffold-game`, `sbox-setup`), and includes the `sbox-game-dev` specialist agent.
8
8
 
9
9
  ```
10
10
  /plugin marketplace add LouSputthole/Sbox-Claude
@@ -54,7 +54,7 @@ Claude Code → (stdio) → sbox-mcp-server → (file IPC) → bridge addon →
54
54
 
55
55
  Communication uses file-based IPC through `%TEMP%/sbox-bridge-ipc/`. The MCP server writes request JSON files, the bridge addon (running inside s&box) polls and processes on the main editor thread, then writes response files back. WebSocket is not used — s&box's sandboxed C# environment blocks `System.Net`.
56
56
 
57
- ## Tools (v1.5.2)
57
+ ## Tools (v1.9.0)
58
58
 
59
59
  `get_bridge_status` reports the `handlerCount` — that's the C# handlers compiled inside the editor. Six tools run **MCP-server-side** and need no editor handler: `read_log`, `get_compile_errors`, `execute_csharp`, `search_docs`, `get_doc_page`, `list_doc_categories`. They read the log / hotload-eval / fetch docs directly, so they keep working even when the editor has crashed or stalled.
60
60
 
@@ -99,6 +99,7 @@ Communication uses file-based IPC through `%TEMP%/sbox-bridge-ipc/`. The MCP ser
99
99
  | **Console / Exec** *(v1.5.0)* | console_run, execute_csharp *(experimental)* |
100
100
  | **Object utilities** *(v1.5.0)* | remove_component, get_tags |
101
101
  | **Docs search** *(v1.5.0, MCP-server-side)* | search_docs, get_doc_page, list_doc_categories — official `Facepunch/sbox-docs` |
102
+ | **Inspection & validation** *(v1.9.0)* | inspect_networked_object (per-object `Network.*` + every component's `[Sync]` fields/values), networking_lint (static scan for `[Sync]`/RPC footguns), scene_validate (no-camera / stray root Rigidbody / trigger-vs-trace), save_inspect (list/read/diff `FileSystem.Data` saves), services_query (`Sandbox.Services` stats + leaderboards), simulate_input (drive named input actions in play mode) |
102
103
 
103
104
  ## Working with Claude effectively
104
105
 
@@ -126,6 +127,6 @@ The companion plugin's `sbox-build-feature` skill encodes this workflow plus the
126
127
 
127
128
  ## License
128
129
 
129
- **GPL-3.0** — see [LICENSE](../LICENSE) for details.
130
+ **AGPL-3.0-or-later** — see [LICENSE](../LICENSE) and [NOTICE](../NOTICE) for details. The code is open under AGPL; the "s&box Claude Bridge" / "sboxskins.gg" name and branding may not be reused to pass a fork off as the original.
130
131
 
131
132
  Copyright (c) 2026 [sboxskins.gg](https://sboxskins.gg)
package/dist/index.js CHANGED
@@ -43,6 +43,7 @@ import { registerNavigationTools } from "./tools/navigation.js";
43
43
  import { registerSelfTestTools } from "./tools/selftest.js";
44
44
  import { registerGameplayTools } from "./tools/gameplay.js";
45
45
  import { registerNpcTools } from "./tools/npc.js";
46
+ import { registerInspectionTools } from "./tools/inspection.js";
46
47
  // ── CLI flags ──────────────────────────────────────────────────────
47
48
  const args = process.argv.slice(2);
48
49
  /** Read the package version from package.json, or return "unknown" on failure. */
@@ -189,6 +190,7 @@ registerNavigationTools(server, bridge);
189
190
  registerSelfTestTools(server, bridge);
190
191
  registerGameplayTools(server, bridge);
191
192
  registerNpcTools(server, bridge);
193
+ registerInspectionTools(server, bridge);
192
194
  /** Start the MCP server on stdio and attempt initial Bridge connection. */
193
195
  async function main() {
194
196
  const transport = new StdioServerTransport();
@@ -0,0 +1,17 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { BridgeClient } from "../transport/bridge-client.js";
3
+ /**
4
+ * Inspection & validation tools — the high-leverage gaps surfaced by mining 27 shipped
5
+ * s&box games. These let an assistant VERIFY the things that are otherwise invisible from
6
+ * a single editor instance: networking authority, save state, input-driven behavior, and
7
+ * scene/networking correctness.
8
+ *
9
+ * inspect_networked_object — per-object Network.Owner/IsProxy/[Sync] values (vs session-only get_network_status)
10
+ * networking_lint — static scan for unguarded [Sync] mutators, money-as-[Sync], List-as-[Sync], etc.
11
+ * scene_validate — missing camera/controller, multiple root Rigidbodies, IsTrigger-vs-trace footguns
12
+ * save_inspect — list/read/diff FileSystem.Data save files (21 of 27 games persist; assistant was blind)
13
+ * services_query — Sandbox.Services Stats / leaderboard reads
14
+ * simulate_input — press/release a named action + AnalogMove/Look during play (drive movement/IPressable/weapons)
15
+ */
16
+ export declare function registerInspectionTools(server: McpServer, bridge: BridgeClient): void;
17
+ //# sourceMappingURL=inspection.d.ts.map
@@ -0,0 +1,109 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Inspection & validation tools — the high-leverage gaps surfaced by mining 27 shipped
4
+ * s&box games. These let an assistant VERIFY the things that are otherwise invisible from
5
+ * a single editor instance: networking authority, save state, input-driven behavior, and
6
+ * scene/networking correctness.
7
+ *
8
+ * inspect_networked_object — per-object Network.Owner/IsProxy/[Sync] values (vs session-only get_network_status)
9
+ * networking_lint — static scan for unguarded [Sync] mutators, money-as-[Sync], List-as-[Sync], etc.
10
+ * scene_validate — missing camera/controller, multiple root Rigidbodies, IsTrigger-vs-trace footguns
11
+ * save_inspect — list/read/diff FileSystem.Data save files (21 of 27 games persist; assistant was blind)
12
+ * services_query — Sandbox.Services Stats / leaderboard reads
13
+ * simulate_input — press/release a named action + AnalogMove/Look during play (drive movement/IPressable/weapons)
14
+ */
15
+ export function registerInspectionTools(server, bridge) {
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.", {
18
+ id: z.string().describe("GUID of the GameObject to inspect"),
19
+ allProps: z
20
+ .boolean()
21
+ .default(false)
22
+ .describe("Include all component properties, not just [Sync]-marked ones"),
23
+ }, async (params) => {
24
+ const res = await bridge.send("inspect_networked_object", params);
25
+ if (!res.success)
26
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
27
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
28
+ });
29
+ // ── networking_lint ───────────────────────────────────────────────
30
+ server.tool("networking_lint", "Static-scan the project's C# for the highest-frequency networking/authority bugs: a mutator that writes a [Sync] field with no IsProxy/Networking.IsHost guard; money/health/score-shaped fields marked plain [Sync] (should be SyncFlags.FromHost); List<>/Dictionary<> marked [Sync] (should be NetList/NetDictionary); [Sync] fields typed Connection/GameObject (sync a Guid instead); [Rpc.Host] methods that mutate without re-checking Rpc.Caller; and component swaps / reflection writes missing Network.Refresh(). Returns findings with file:line + the suggested fix.", {
31
+ path: z
32
+ .string()
33
+ .optional()
34
+ .describe("Optional sub-path under the project (e.g. 'Code/Player') to scope the scan; omit for the whole project"),
35
+ }, async (params) => {
36
+ const res = await bridge.send("networking_lint", params);
37
+ if (!res.success)
38
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
39
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
40
+ });
41
+ // ── scene_validate ────────────────────────────────────────────────
42
+ server.tool("scene_validate", "Validate the active scene for the silent setup footguns that break controllers/physics/cameras: no CameraComponent, no player controller, multiple root Rigidbodies, a Rigidbody with MotionEnabled=false fighting a kinematic root, IsTrigger colliders that Scene.Trace will ignore, child Rigidbodies breaking collider binding, and missing required child anchors. Returns each issue with the GameObject and the exact fix.", {}, async () => {
43
+ const res = await bridge.send("scene_validate", {});
44
+ if (!res.success)
45
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
46
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
47
+ });
48
+ // ── save_inspect ──────────────────────────────────────────────────
49
+ 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.", {
50
+ action: z.enum(["list", "read", "diff"]).default("list"),
51
+ path: z
52
+ .string()
53
+ .optional()
54
+ .describe("File or folder path under FileSystem.Data (e.g. 'lumber_corp2_progress' or '<folder>/steam_123.json')"),
55
+ pathB: z
56
+ .string()
57
+ .optional()
58
+ .describe("Second file path for action='diff'"),
59
+ }, async (params) => {
60
+ const res = await bridge.send("save_inspect", params);
61
+ if (!res.success)
62
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
63
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
64
+ });
65
+ // ── services_query ────────────────────────────────────────────────
66
+ 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.", {
67
+ action: z.enum(["stats", "leaderboard"]).default("stats"),
68
+ name: z
69
+ .string()
70
+ .optional()
71
+ .describe("Stat name (action='stats') or leaderboard/board name (action='leaderboard')"),
72
+ limit: z.number().int().default(10).describe("Max leaderboard entries to return"),
73
+ }, async (params) => {
74
+ const res = await bridge.send("services_query", params);
75
+ if (!res.success)
76
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
77
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
78
+ });
79
+ // ── simulate_input ────────────────────────────────────────────────
80
+ 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.", {
81
+ action: z
82
+ .string()
83
+ .optional()
84
+ .describe("Named input action to drive (must exist in Input.config), e.g. 'jump', 'attack1', 'use'"),
85
+ state: z
86
+ .enum(["press", "hold", "release"])
87
+ .default("press")
88
+ .describe("press = one tick (Pressed); hold = held for durationMs; release = clear a held action"),
89
+ analogMove: z
90
+ .object({ x: z.number(), y: z.number(), z: z.number().default(0) })
91
+ .optional()
92
+ .describe("AnalogMove vector to apply (forward/left)"),
93
+ analogLook: z
94
+ .object({ pitch: z.number(), yaw: z.number(), roll: z.number().default(0) })
95
+ .optional()
96
+ .describe("AnalogLook angles delta to apply"),
97
+ durationMs: z
98
+ .number()
99
+ .int()
100
+ .default(100)
101
+ .describe("How long to hold the action / apply the analog values"),
102
+ }, async (params) => {
103
+ const res = await bridge.send("simulate_input", params);
104
+ if (!res.success)
105
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
106
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
107
+ });
108
+ }
109
+ //# sourceMappingURL=inspection.js.map
@@ -114,7 +114,7 @@ export function registerNetworkingTools(server, bridge) {
114
114
  syncFlags: z
115
115
  .string()
116
116
  .optional()
117
- .describe("Currently ignorednot yet implemented. The addon emits a plain [Sync] with no SyncFlags"),
117
+ .describe("Optional SyncFlags to emit as [Sync( SyncFlags.X )] e.g. 'Interpolate' (smooth interpolation), 'Query', 'FromHost'. Omit for a plain [Sync]"),
118
118
  defaultValue: z
119
119
  .string()
120
120
  .optional()
@@ -129,7 +129,7 @@ export function registerNetworkingTools(server, bridge) {
129
129
  };
130
130
  });
131
131
  // ── add_rpc_method ────────────────────────────────────────────────
132
- server.tool("add_rpc_method", "Generate an EMPTY, no-argument RPC method stub in a C# script for you to fill in. The addon inserts the chosen RPC attribute ([Rpc.Broadcast] all clients, [Rpc.Host] host only, [Rpc.Owner] owner only) above a parameterless method with an empty body it does NOT add parameters or generate body code", {
132
+ server.tool("add_rpc_method", "Generate an RPC method stub in a C# script. Inserts the chosen RPC attribute ([Rpc.Broadcast] all clients, [Rpc.Host] host only, [Rpc.Owner] owner only) above a method with an empty body. Pass methodParams to give it a parameter list (e.g. 'Vector3 pos, int damage'); the body is left as a TODO for you to fill in", {
133
133
  path: z
134
134
  .string()
135
135
  .describe("Relative path to the script file"),
@@ -141,7 +141,7 @@ export function registerNetworkingTools(server, bridge) {
141
141
  methodParams: z
142
142
  .string()
143
143
  .optional()
144
- .describe("Currently ignored not yet implemented. The addon emits a no-argument method; add parameters yourself afterward"),
144
+ .describe("Optional parameter list for the RPC method signature, e.g. 'Vector3 pos, int damage'. Omit for a parameterless method"),
145
145
  body: z
146
146
  .string()
147
147
  .optional()
@@ -82,7 +82,7 @@ export function registerPlayModeTools(server, bridge) {
82
82
  property: z.string().describe("Property name to set"),
83
83
  value: z
84
84
  .unknown()
85
- .describe("New value as a string. Primitive: '5', 'true'. Color/Vector3: '1,0,0,1' / '0,0,200'. Enum: the member name. Asset ref (Model/Material/...): the asset path e.g. 'models/dev/box.vmdl'. GameObject/Component ref: the target GameObject's GUID. Empty/'null' clears the property"),
85
+ .describe("New value. Primitive: '5', 'true'. Color/Vector3: a comma string ('1,0,0,1' / '0,0,200'), an array ([0,0,200]), or an object ({r,g,b,a} / {x,y,z}). Enum: the member name. Asset ref (Model/Material/...): the asset path e.g. 'models/dev/box.vmdl'. GameObject/Component ref: the target GameObject's GUID. Empty/'null' clears the property"),
86
86
  }, async (params) => {
87
87
  const res = await bridge.send("set_property", params);
88
88
  if (!res.success) {
@@ -61,30 +61,30 @@ export function registerVisualTools(server, bridge) {
61
61
  };
62
62
  });
63
63
  // ── set_fog ────────────────────────────────────────────────────────
64
- server.tool("set_fog", "Add or update distance fog in the active scene. v1 supports gradient fog (atmospheric distance haze — great for mood/horror). Re-running on the same target updates it rather than duplicating.", {
64
+ server.tool("set_fog", "Add or update fog in the active scene. Types: 'gradient' (distance haze — great for mood/horror), 'cubemap' (sky-tinted distance fog), 'volumetric' (a localized fog volume). Re-running on the same target updates it rather than duplicating.", {
65
65
  type: z
66
- .enum(["gradient"])
66
+ .enum(["gradient", "cubemap", "volumetric"])
67
67
  .optional()
68
- .describe("Fog type (default gradient; cubemap/volumetric coming later)"),
68
+ .describe("Fog type (default gradient)"),
69
69
  name: z.string().optional().describe("GameObject name when creating a new fog object"),
70
70
  targetId: z
71
71
  .string()
72
72
  .optional()
73
- .describe("GUID of an existing GameObject to host the fog (else a new 'Gradient Fog' object is created)"),
74
- color: ColorSchema.optional().describe("Fog colour"),
75
- startDistance: z
76
- .number()
77
- .optional()
78
- .describe("Distance (units) where fog begins"),
79
- endDistance: z
80
- .number()
81
- .optional()
82
- .describe("Distance (units) where fog reaches full density"),
83
- height: z.number().optional().describe("World height the fog settles around"),
84
- falloff: z
85
- .number()
73
+ .describe("GUID of an existing GameObject to host the fog (else a new fog object is created)"),
74
+ color: ColorSchema.optional().describe("Fog colour (gradient/volumetric Color, cubemap Tint)"),
75
+ startDistance: z.number().optional().describe("gradient/cubemap: distance (units) where fog begins"),
76
+ endDistance: z.number().optional().describe("gradient/cubemap: distance (units) where fog reaches full density"),
77
+ height: z.number().optional().describe("gradient: world height the fog settles around"),
78
+ falloff: z.number().optional().describe("Distance/density falloff exponent (higher = sharper onset)"),
79
+ blur: z.number().optional().describe("cubemap: sky blur amount"),
80
+ heightStart: z.number().optional().describe("cubemap: world height where height-fog starts"),
81
+ heightWidth: z.number().optional().describe("cubemap: height-fog band width"),
82
+ heightExponent: z.number().optional().describe("cubemap: height-fog falloff exponent"),
83
+ strength: z.number().optional().describe("volumetric: fog density/strength"),
84
+ size: z
85
+ .object({ x: z.number(), y: z.number(), z: z.number() })
86
86
  .optional()
87
- .describe("Distance falloff exponent (higher = sharper onset)"),
87
+ .describe("volumetric: bounds size (units) centred on the object"),
88
88
  }, async (params) => {
89
89
  const res = await bridge.send("set_fog", params);
90
90
  if (!res.success) {
@@ -17,17 +17,21 @@ import { z } from "zod";
17
17
  */
18
18
  export function registerWorldTools(server, bridge) {
19
19
  // ── invoke_button ────────────────────────────────────────────────
20
- server.tool("invoke_button", "Call a no-argument public method on a component. Matching is tried in order: (1) a [Button] attribute label, (2) the exact method NAME, (3) case-insensitive name with spaces stripped. So this calls ANY parameterless public method, not only [Button]-attributed ones (e.g. 'StartGame' works even without a [Button]). This is the general 'call a component method to drive game state' tool. LIMITATION: parameterless methods only methods that take arguments are skipped. (list_component_buttons only lists [Button] methods, so a plain method may be invokable yet not appear there.)", {
20
+ server.tool("invoke_button", "Call a public method on a component. Matching is tried in order: (1) a [Button] attribute label, (2) the exact method NAME, (3) case-insensitive name with spaces stripped. Calls ANY public method, not only [Button]-attributed ones (e.g. 'StartGame'). Pass `args` to call methods that take parameters — the arg count must match and each value is coerced to the parameter type (primitives: string/number/bool work; complex types like Vector3 may not coerce). Omit args (or []) for parameterless methods. (list_component_buttons only lists [Button] methods, so a plain method may be invokable yet not appear there.)", {
21
21
  component: z
22
22
  .string()
23
23
  .describe("Component type name (e.g. 'MapBuilder', 'SasquatchedGame')"),
24
24
  button: z
25
25
  .string()
26
- .describe("A [Button] label OR a public no-arg method name (e.g. 'Build Terrain', 'StartGame'); case- and space-insensitive"),
26
+ .describe("A [Button] label OR a public method name (e.g. 'Build Terrain', 'StartGame'); case- and space-insensitive"),
27
27
  id: z
28
28
  .string()
29
29
  .optional()
30
30
  .describe("Optional GameObject GUID — if omitted, finds first matching component in scene"),
31
+ args: z
32
+ .array(z.unknown())
33
+ .optional()
34
+ .describe("Arguments to pass (must match the method's parameter count); coerced to each parameter type"),
31
35
  }, async (params) => {
32
36
  const res = await bridge.send("invoke_button", params);
33
37
  if (!res.success)
@@ -49,6 +53,10 @@ export function registerWorldTools(server, bridge) {
49
53
  x: z.number().describe("World X coordinate"),
50
54
  y: z.number().describe("World Y coordinate"),
51
55
  id: z.string().optional().describe("Optional GameObject GUID for MapBuilder"),
56
+ component: z
57
+ .string()
58
+ .optional()
59
+ .describe("Override the terrain component type name (default MapBuilder)"),
52
60
  }, async (params) => {
53
61
  const res = await bridge.send("raycast_terrain", params);
54
62
  if (!res.success)
@@ -63,6 +71,10 @@ export function registerWorldTools(server, bridge) {
63
71
  height: z.number().default(100).describe("Peak height (negative for depression)"),
64
72
  rebuild: z.boolean().default(true).describe("Rebuild terrain after adding (set false to batch)"),
65
73
  id: z.string().optional(),
74
+ component: z
75
+ .string()
76
+ .optional()
77
+ .describe("Override the builder component type name — set this if your project's terrain/cave/forest component is named differently than the default (MapBuilder/CaveBuilder/ForestGenerator)"),
66
78
  }, async (params) => {
67
79
  const res = await bridge.send("add_terrain_hill", params);
68
80
  if (!res.success)
@@ -76,6 +88,10 @@ export function registerWorldTools(server, bridge) {
76
88
  radius: z.number().default(300),
77
89
  rebuild: z.boolean().default(true),
78
90
  id: z.string().optional(),
91
+ component: z
92
+ .string()
93
+ .optional()
94
+ .describe("Override the builder component type name — set this if your project's terrain/cave/forest component is named differently than the default (MapBuilder/CaveBuilder/ForestGenerator)"),
79
95
  }, async (params) => {
80
96
  const res = await bridge.send("add_terrain_clearing", params);
81
97
  if (!res.success)
@@ -88,6 +104,10 @@ export function registerWorldTools(server, bridge) {
88
104
  to: z.object({ x: z.number(), y: z.number() }),
89
105
  rebuild: z.boolean().default(true),
90
106
  id: z.string().optional(),
107
+ component: z
108
+ .string()
109
+ .optional()
110
+ .describe("Override the builder component type name — set this if your project's terrain/cave/forest component is named differently than the default (MapBuilder/CaveBuilder/ForestGenerator)"),
91
111
  }, async (params) => {
92
112
  const res = await bridge.send("add_terrain_trail", params);
93
113
  if (!res.success)
@@ -101,6 +121,10 @@ export function registerWorldTools(server, bridge) {
101
121
  .default("all"),
102
122
  rebuild: z.boolean().default(true),
103
123
  id: z.string().optional(),
124
+ component: z
125
+ .string()
126
+ .optional()
127
+ .describe("Override the builder component type name — set this if your project's terrain/cave/forest component is named differently than the default (MapBuilder/CaveBuilder/ForestGenerator)"),
104
128
  }, async (params) => {
105
129
  const res = await bridge.send("clear_terrain_features", params);
106
130
  if (!res.success)
@@ -119,6 +143,10 @@ export function registerWorldTools(server, bridge) {
119
143
  .describe("Optional insert position (default: append to end)"),
120
144
  rebuild: z.boolean().default(true),
121
145
  id: z.string().optional(),
146
+ component: z
147
+ .string()
148
+ .optional()
149
+ .describe("Override the builder component type name — set this if your project's terrain/cave/forest component is named differently than the default (MapBuilder/CaveBuilder/ForestGenerator)"),
122
150
  }, async (params) => {
123
151
  const res = await bridge.send("add_cave_waypoint", params);
124
152
  if (!res.success)
@@ -128,6 +156,10 @@ export function registerWorldTools(server, bridge) {
128
156
  // ── clear_cave_path ──────────────────────────────────────────────
129
157
  server.tool("clear_cave_path", "Clear all waypoints in CaveBuilder and remove the cave from the scene.", {
130
158
  id: z.string().optional(),
159
+ component: z
160
+ .string()
161
+ .optional()
162
+ .describe("Override the builder component type name — set this if your project's terrain/cave/forest component is named differently than the default (MapBuilder/CaveBuilder/ForestGenerator)"),
131
163
  }, async (params) => {
132
164
  const res = await bridge.send("clear_cave_path", params);
133
165
  if (!res.success)
@@ -149,6 +181,10 @@ export function registerWorldTools(server, bridge) {
149
181
  .default(false)
150
182
  .describe("Forest gen is slow (~1s); default false to batch"),
151
183
  id: z.string().optional(),
184
+ component: z
185
+ .string()
186
+ .optional()
187
+ .describe("Override the builder component type name — set this if your project's terrain/cave/forest component is named differently than the default (MapBuilder/CaveBuilder/ForestGenerator)"),
152
188
  }, async (params) => {
153
189
  const res = await bridge.send("add_forest_poi", params);
154
190
  if (!res.success)
@@ -161,6 +197,10 @@ export function registerWorldTools(server, bridge) {
161
197
  to_index: z.number().int(),
162
198
  rebuild: z.boolean().default(false),
163
199
  id: z.string().optional(),
200
+ component: z
201
+ .string()
202
+ .optional()
203
+ .describe("Override the builder component type name — set this if your project's terrain/cave/forest component is named differently than the default (MapBuilder/CaveBuilder/ForestGenerator)"),
164
204
  }, async (params) => {
165
205
  const res = await bridge.send("add_forest_trail", params);
166
206
  if (!res.success)
@@ -172,6 +212,10 @@ export function registerWorldTools(server, bridge) {
172
212
  seed: z.number().int().default(77),
173
213
  rebuild: z.boolean().default(true),
174
214
  id: z.string().optional(),
215
+ component: z
216
+ .string()
217
+ .optional()
218
+ .describe("Override the builder component type name — set this if your project's terrain/cave/forest component is named differently than the default (MapBuilder/CaveBuilder/ForestGenerator)"),
175
219
  }, async (params) => {
176
220
  const res = await bridge.send("set_forest_seed", params);
177
221
  if (!res.success)
@@ -181,6 +225,10 @@ export function registerWorldTools(server, bridge) {
181
225
  // ── clear_forest_pois ────────────────────────────────────────────
182
226
  server.tool("clear_forest_pois", "Wipe all POIs and trails in ForestGenerator and clear placed forest objects from the scene.", {
183
227
  id: z.string().optional(),
228
+ component: z
229
+ .string()
230
+ .optional()
231
+ .describe("Override the builder component type name — set this if your project's terrain/cave/forest component is named differently than the default (MapBuilder/CaveBuilder/ForestGenerator)"),
184
232
  }, async (params) => {
185
233
  const res = await bridge.send("clear_forest_pois", params);
186
234
  if (!res.success)
@@ -195,6 +243,10 @@ export function registerWorldTools(server, bridge) {
195
243
  strength: z.number().default(50).describe("Height delta (units) for raise/lower; ignored for flatten/smooth"),
196
244
  mode: z.enum(["raise", "lower", "flatten", "smooth"]).default("raise"),
197
245
  id: z.string().optional(),
246
+ component: z
247
+ .string()
248
+ .optional()
249
+ .describe("Override the builder component type name — set this if your project's terrain/cave/forest component is named differently than the default (MapBuilder/CaveBuilder/ForestGenerator)"),
198
250
  }, async (params) => {
199
251
  const res = await bridge.send("sculpt_terrain", params);
200
252
  if (!res.success)
@@ -209,6 +261,10 @@ export function registerWorldTools(server, bridge) {
209
261
  density: z.number().default(1).describe("Density multiplier (0=clear, 1=normal, 2=dense)"),
210
262
  rebuild: z.boolean().default(false),
211
263
  id: z.string().optional(),
264
+ component: z
265
+ .string()
266
+ .optional()
267
+ .describe("Override the builder component type name — set this if your project's terrain/cave/forest component is named differently than the default (MapBuilder/CaveBuilder/ForestGenerator)"),
212
268
  }, async (params) => {
213
269
  const res = await bridge.send("paint_forest_density", params);
214
270
  if (!res.success)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbox-mcp-server",
3
- "version": "1.7.0",
3
+ "version": "1.9.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",
@@ -51,5 +51,5 @@
51
51
  "engines": {
52
52
  "node": ">=18.0.0"
53
53
  },
54
- "license": "GPL-3.0-or-later"
54
+ "license": "AGPL-3.0-or-later"
55
55
  }