sbox-mcp-server 1.3.2 → 1.4.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,113 +1,119 @@
1
- # sbox-mcp-server
2
-
3
- MCP Server for the s&box game engine. Lets Claude Code build s&box games through conversation — 99 working tools for scenes, scripts, GameObjects, components, assets, materials, audio, physics, UI, networking, publishing, world-gen, and type discovery.
4
-
5
- ## Fastest install — the Claude Code plugin
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.
8
-
9
- ```
10
- /plugin marketplace add LouSputthole/Sbox-Claude
11
- /plugin install sbox-claude
12
- ```
13
-
14
- You still need to install the s&box-side **bridge addon** into your project's `Libraries/` folder (see step 1 below). The plugin handles the Claude side; the addon handles the s&box side.
15
-
16
- ## Manual install — three steps
17
-
18
- ### 1. Install the bridge addon in s&box
19
-
20
- The bridge addon runs inside the s&box editor and receives commands from this MCP server. It MUST live inside a project's `Libraries/` folder — putting it in s&box's global `addons/` will silently fail to compile.
21
-
22
- ```powershell
23
- git clone https://github.com/LouSputthole/Sbox-Claude.git
24
- cd Sbox-Claude
25
- .\install.ps1 -RemoveStaleAddons # Windows, auto-detects your s&box project
26
- ./install.sh --remove-stale # Linux/Mac/WSL
27
- ```
28
-
29
- See [INSTALL.md](https://github.com/LouSputthole/Sbox-Claude/blob/main/INSTALL.md) for the full guide and manual fallback.
30
-
31
- ### 2. Register the MCP server with Claude Code
32
-
33
- ```bash
34
- claude mcp add sbox -- npx sbox-mcp-server
35
- ```
36
-
37
- This is the bare command — equivalent to what the plugin's `.mcp.json` does for you.
38
-
39
- ### 3. Open s&box
40
-
41
- Open your project. The bridge starts automatically. Verify with:
42
-
43
- ```
44
- Check the bridge status.
45
- ```
46
-
47
- You should see `connected: true, handlerCount: 99`.
48
-
49
- ## How it works
50
-
51
- ```
52
- Claude Code → (stdio) → sbox-mcp-server → (file IPC) → bridge addon → s&box editor
53
- ```
54
-
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
-
57
- ## Tools (99 working)
58
-
59
- | Category | Tools |
60
- |----------|-------|
61
- | **Project** | get_project_info, list_project_files, read_file, write_file |
62
- | **Scripts** | create_script, edit_script, delete_script, trigger_hotload |
63
- | **Scenes** | list_scenes, load_scene, save_scene, create_scene |
64
- | **GameObjects** | create/delete/duplicate/rename, set_parent/enabled/transform |
65
- | **Components** | get/set_property, get_all_properties, list_available, add_component, set_prefab_ref |
66
- | **Hierarchy** | get_scene_hierarchy (with `maxDepth` + `rootId`), get/select/focus_object |
67
- | **Assets** | search_assets, list_asset_library, install_asset, get_asset_info |
68
- | **Materials** | assign_model, create/assign_material, set_material_property |
69
- | **Audio** | list_sounds, create_sound_event, assign_sound, play_sound_preview |
70
- | **Play Mode** | start/stop_play, is_playing |
71
- | **Runtime** | get/set_runtime_property, take_screenshot |
72
- | **Editor** | undo, redo |
73
- | **Prefabs** | create/instantiate_prefab, list_prefabs, get_prefab_info |
74
- | **Physics** | add_physics, add_collider, add_joint, raycast |
75
- | **UI** | create_razor_ui, add_screen_panel, add_world_panel |
76
- | **Templates** | create_player/npc_controller, create_game_manager, create_trigger_zone |
77
- | **Networking** | network_helper, configure/status, spawn, ownership, sync, RPCs, lobby/event templates |
78
- | **Publishing** | project_config, validate, thumbnail, package_details |
79
- | **World gen** | invoke_button, list_component_buttons, raycast_terrain, build_terrain_mesh |
80
- | **Map edit** | add_terrain_hill/clearing/trail, clear_terrain_features, sculpt_terrain |
81
- | **Caves / Forest** | add_cave_waypoint, clear_cave_path, add_forest_poi/trail, set_forest_seed, clear_forest_pois, paint_forest_density |
82
- | **Placement** | place_along_path |
83
- | **Discovery** | describe_type, search_types, get_method_signature, find_in_project |
84
- | **Status** | get_bridge_status |
85
-
86
- ## Working with Claude effectively
87
-
88
- Two disciplines prevent the iteration-loop trap:
89
-
90
- 1. **After visual changes, call `take_screenshot` and read the PNG.** Claude is a multimodal model it can see the result. Guessing about visual outcomes from code alone produces long iteration loops.
91
- 2. **Before writing code that touches an unfamiliar s&box type, call `describe_type` or `search_types`.** Reflection is the source of truth; training data goes stale across SDK versions.
92
-
93
- The companion plugin's `sbox-build-feature` skill encodes this workflow plus the common gotchas. If you're not using the plugin, the same rules apply manually.
94
-
95
- ## Requirements
96
-
97
- - **Node.js 18+**
98
- - **s&box** with the bridge addon installed in your project's `Libraries/` folder
99
- - **Claude Code**
100
-
101
- ## Documentation
102
-
103
- - [Main README](https://github.com/LouSputthole/Sbox-Claude/blob/main/README.md) — full project overview
104
- - [INSTALL.md](https://github.com/LouSputthole/Sbox-Claude/blob/main/INSTALL.md) install + manual fallback
105
- - [TROUBLESHOOTING.md](https://github.com/LouSputthole/Sbox-Claude/blob/main/TROUBLESHOOTING.md) — 10 most common failures
106
- - [CHANGELOG.md](https://github.com/LouSputthole/Sbox-Claude/blob/main/CHANGELOG.md) — release history
107
- - [Plugin README](https://github.com/LouSputthole/Sbox-Claude/blob/main/plugins/sbox-claude/README.md) — Claude Code plugin docs
108
-
109
- ## License
110
-
111
- **GPL-3.0** — see [LICENSE](../LICENSE) for details.
112
-
113
- Copyright (c) 2026 [sboxskins.gg](https://sboxskins.gg)
1
+ # sbox-mcp-server
2
+
3
+ MCP Server for the s&box game engine. Lets Claude Code build s&box games through conversation — 131 working tools for scenes, scripts, GameObjects, components, assets, materials, audio, physics, UI, networking, publishing, world-gen, lighting & atmosphere, characters, scene layout, and type discovery.
4
+
5
+ ## Fastest install — the Claude Code plugin
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.
8
+
9
+ ```
10
+ /plugin marketplace add LouSputthole/Sbox-Claude
11
+ /plugin install sbox-claude
12
+ ```
13
+
14
+ You still need to install the s&box-side **bridge addon** into your project's `Libraries/` folder (see step 1 below). The plugin handles the Claude side; the addon handles the s&box side.
15
+
16
+ ## Manual install — three steps
17
+
18
+ ### 1. Install the bridge addon in s&box
19
+
20
+ The bridge addon runs inside the s&box editor and receives commands from this MCP server. It MUST live inside a project's `Libraries/` folder — putting it in s&box's global `addons/` will silently fail to compile.
21
+
22
+ ```powershell
23
+ git clone https://github.com/LouSputthole/Sbox-Claude.git
24
+ cd Sbox-Claude
25
+ .\install.ps1 -RemoveStaleAddons # Windows, auto-detects your s&box project
26
+ ./install.sh --remove-stale # Linux/Mac/WSL
27
+ ```
28
+
29
+ See [INSTALL.md](https://github.com/LouSputthole/Sbox-Claude/blob/main/INSTALL.md) for the full guide and manual fallback.
30
+
31
+ ### 2. Register the MCP server with Claude Code
32
+
33
+ ```bash
34
+ claude mcp add sbox -- npx sbox-mcp-server
35
+ ```
36
+
37
+ This is the bare command — equivalent to what the plugin's `.mcp.json` does for you.
38
+
39
+ ### 3. Open s&box
40
+
41
+ Open your project. The bridge starts automatically. Verify with:
42
+
43
+ ```
44
+ Check the bridge status.
45
+ ```
46
+
47
+ You should see `connected: true, handlerCount: 99`.
48
+
49
+ ## How it works
50
+
51
+ ```
52
+ Claude Code → (stdio) → sbox-mcp-server → (file IPC) → bridge addon → s&box editor
53
+ ```
54
+
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
+
57
+ ## Tools (131 — v1.4.0)
58
+
59
+ | Category | Tools |
60
+ |----------|-------|
61
+ | **Project** | get_project_info, list_project_files, read_file, write_file |
62
+ | **Scripts** | create_script, edit_script, delete_script, trigger_hotload |
63
+ | **Scenes** | list_scenes, load_scene, save_scene, create_scene |
64
+ | **GameObjects** | create/delete/duplicate/rename, set_parent/enabled/transform |
65
+ | **Components** | get/set_property, get_all_properties, list_available, add_component, set_prefab_ref |
66
+ | **Hierarchy** | get_scene_hierarchy (with `maxDepth` + `rootId`), get/select/focus_object |
67
+ | **Assets** | search_assets, list_asset_library, install_asset, get_asset_info |
68
+ | **Materials** | assign_model, create/assign_material, set_material_property |
69
+ | **Audio** | list_sounds, create_sound_event, assign_sound, play_sound_preview |
70
+ | **Play Mode** | start/stop_play, is_playing |
71
+ | **Runtime** | get/set_runtime_property, take_screenshot |
72
+ | **Editor** | undo, redo |
73
+ | **Prefabs** | create/instantiate_prefab, list_prefabs, get_prefab_info |
74
+ | **Physics** | add_physics, add_collider, add_joint, raycast |
75
+ | **UI** | create_razor_ui, add_screen_panel, add_world_panel |
76
+ | **Templates** | create_player/npc_controller, create_game_manager, create_trigger_zone |
77
+ | **Networking** | network_helper, configure/status, spawn, ownership, sync, RPCs, lobby/event templates |
78
+ | **Publishing** | project_config, validate, thumbnail, package_details |
79
+ | **World gen** | invoke_button, list_component_buttons, raycast_terrain, build_terrain_mesh |
80
+ | **Map edit** | add_terrain_hill/clearing/trail, clear_terrain_features, sculpt_terrain |
81
+ | **Caves / Forest** | add_cave_waypoint, clear_cave_path, add_forest_poi/trail, set_forest_seed, clear_forest_pois, paint_forest_density |
82
+ | **Placement** | place_along_path |
83
+ | **Discovery** | describe_type, search_types, get_method_signature, find_in_project |
84
+ | **Status** | get_bridge_status |
85
+ | **Visual & atmosphere** *(v1.4.0)* | add_light, set_fog, add_post_process, set_skybox, add_envmap_probe, apply_atmosphere, apply_post_fx_look |
86
+ | **Characters** *(v1.4.0)* | spawn_model, spawn_citizen, dress_citizen, set_bodygroup, pose_citizen, equip_model, set_look_at, add_ragdoll, set_expression |
87
+ | **Scene & level** *(v1.4.0)* | snap_to_ground, align_objects, distribute_objects, grid_duplicate, measure_distance |
88
+ | **Environment** *(v1.4.0)* | scatter_props, randomize_transforms, group_objects |
89
+ | **Object utilities** *(v1.4.0)* | find_objects, set_tint, replace_model, set_tags |
90
+ | **VFX** *(v1.4.0, experimental)* | spawn_particle, create_particle_effect, add_trail, add_beam compile but runtime rendering unverified through the bridge; use a legacy `.vpcf` for visible particles |
91
+
92
+ ## Working with Claude effectively
93
+
94
+ Two disciplines prevent the iteration-loop trap:
95
+
96
+ 1. **After visual changes, call `take_screenshot` and read the PNG.** Claude is a multimodal model — it can see the result. Guessing about visual outcomes from code alone produces long iteration loops.
97
+ 2. **Before writing code that touches an unfamiliar s&box type, call `describe_type` or `search_types`.** Reflection is the source of truth; training data goes stale across SDK versions.
98
+
99
+ The companion plugin's `sbox-build-feature` skill encodes this workflow plus the common gotchas. If you're not using the plugin, the same rules apply manually.
100
+
101
+ ## Requirements
102
+
103
+ - **Node.js 18+**
104
+ - **s&box** with the bridge addon installed in your project's `Libraries/` folder
105
+ - **Claude Code**
106
+
107
+ ## Documentation
108
+
109
+ - [Main README](https://github.com/LouSputthole/Sbox-Claude/blob/main/README.md) — full project overview
110
+ - [INSTALL.md](https://github.com/LouSputthole/Sbox-Claude/blob/main/INSTALL.md) — install + manual fallback
111
+ - [TROUBLESHOOTING.md](https://github.com/LouSputthole/Sbox-Claude/blob/main/TROUBLESHOOTING.md) 10 most common failures
112
+ - [CHANGELOG.md](https://github.com/LouSputthole/Sbox-Claude/blob/main/CHANGELOG.md) — release history
113
+ - [Plugin README](https://github.com/LouSputthole/Sbox-Claude/blob/main/plugins/sbox-claude/README.md) — Claude Code plugin docs
114
+
115
+ ## License
116
+
117
+ **GPL-3.0** — see [LICENSE](../LICENSE) for details.
118
+
119
+ Copyright (c) 2026 [sboxskins.gg](https://sboxskins.gg)
package/dist/index.js CHANGED
@@ -33,6 +33,10 @@ import { registerNetworkingTools } from "./tools/networking.js";
33
33
  import { registerPublishingTools } from "./tools/publishing.js";
34
34
  import { registerWorldTools } from "./tools/world.js";
35
35
  import { registerDiscoveryTools } from "./tools/discovery.js";
36
+ import { registerVisualTools } from "./tools/visuals.js";
37
+ import { registerCharacterTools } from "./tools/characters.js";
38
+ import { registerLevelTools } from "./tools/leveltools.js";
39
+ import { registerObjectTools } from "./tools/objecttools.js";
36
40
  // ── CLI flags ──────────────────────────────────────────────────────
37
41
  const args = process.argv.slice(2);
38
42
  /** Read the package version from package.json, or return "unknown" on failure. */
@@ -68,7 +72,7 @@ ENVIRONMENT VARIABLES
68
72
  CONNECT TO CLAUDE CODE
69
73
  claude mcp add sbox -- node /path/to/sbox-mcp-server/dist/index.js
70
74
 
71
- TOOLS (99 working was 109; 10 unimplementable tools removed in v1.3.0)
75
+ TOOLS (131+32 in v1.4.0: visual, characters, scene, environment, utilities)
72
76
  Project: get_project_info, list_project_files, read_file, write_file
73
77
  Scripts: create_script, edit_script, delete_script, trigger_hotload
74
78
  Scenes: list_scenes, load_scene, save_scene, create_scene
@@ -95,6 +99,14 @@ TOOLS (99 working — was 109; 10 unimplementable tools removed in v1.3.0)
95
99
  Placement: place_along_path
96
100
  Discovery: describe_type, search_types, get_method_signature, find_in_project
97
101
  Status: get_bridge_status
102
+
103
+ ── New in v1.4.0 ───────────────────────────────────
104
+ Visual: add_light, set_fog, add_post_process, set_skybox, add_envmap_probe, apply_atmosphere, apply_post_fx_look
105
+ Characters: spawn_model, spawn_citizen, dress_citizen, set_bodygroup, pose_citizen, equip_model, set_look_at, add_ragdoll, set_expression
106
+ Scene: snap_to_ground, align_objects, distribute_objects, grid_duplicate, measure_distance
107
+ Environment: scatter_props, randomize_transforms, group_objects
108
+ Utilities: find_objects, set_tint, replace_model, set_tags
109
+ VFX (exp): spawn_particle, create_particle_effect, add_trail, add_beam
98
110
  `);
99
111
  process.exit(0);
100
112
  }
@@ -148,6 +160,10 @@ registerNetworkingTools(server, bridge);
148
160
  registerPublishingTools(server, bridge);
149
161
  registerWorldTools(server, bridge);
150
162
  registerDiscoveryTools(server, bridge);
163
+ registerVisualTools(server, bridge);
164
+ registerCharacterTools(server, bridge);
165
+ registerLevelTools(server, bridge);
166
+ registerObjectTools(server, bridge);
151
167
  /** Start the MCP server on stdio and attempt initial Bridge connection. */
152
168
  async function main() {
153
169
  const transport = new StdioServerTransport();
@@ -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 registerCharacterTools(server: McpServer, bridge: BridgeClient): void;
4
+ //# sourceMappingURL=characters.d.ts.map
@@ -0,0 +1,209 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Character & model tools (Batch 19): spawn world models, spawn animated
4
+ * Citizen characters, dress them with clothing, toggle bodygroups, and pose
5
+ * them via CitizenAnimationHelper.
6
+ *
7
+ * Unlike particles (Batch 18), everything here is a STATIC mesh/pose that
8
+ * renders in the editor viewport — so the screenshot loop works: after a
9
+ * change, take_screenshot and read the result. Animation *playback* is
10
+ * runtime, but poses preview in-editor via PlayAnimationsInEditorScene.
11
+ */
12
+ const ColorSchema = z
13
+ .object({
14
+ r: z.number().min(0).describe("Red, 0-1"),
15
+ g: z.number().min(0).describe("Green, 0-1"),
16
+ b: z.number().min(0).describe("Blue, 0-1"),
17
+ a: z.number().min(0).max(1).optional().describe("Alpha, 0-1 (default 1)"),
18
+ })
19
+ .describe("RGBA colour as 0-1 floats (model tint)");
20
+ const Vector3Schema = z
21
+ .object({ x: z.number(), y: z.number(), z: z.number() })
22
+ .describe("World position {x,y,z}");
23
+ const RotationSchema = z
24
+ .object({ pitch: z.number(), yaw: z.number(), roll: z.number() })
25
+ .describe("Rotation {pitch,yaw,roll} in degrees");
26
+ export function registerCharacterTools(server, bridge) {
27
+ // ── spawn_model ────────────────────────────────────────────────────
28
+ server.tool("spawn_model", "Spawn a GameObject with a ModelRenderer showing a model — the quick way to place a prop. Pass an engine/project model path (e.g. 'models/citizen/citizen.vmdl', 'models/dev/box.vmdl'). Cloud (sbox.game) models must be installed first via install_asset, then pass the resulting path. Add physics separately with add_collider/add_physics if needed.", {
29
+ model: z
30
+ .string()
31
+ .describe("Model path, e.g. 'models/dev/box.vmdl' or an installed model path"),
32
+ name: z.string().optional().describe("GameObject name"),
33
+ position: Vector3Schema.optional().describe("World position"),
34
+ rotation: RotationSchema.optional().describe("World rotation"),
35
+ scale: Vector3Schema.optional().describe("World scale (default 1,1,1)"),
36
+ tint: ColorSchema.optional().describe("Model tint colour"),
37
+ parentId: z.string().optional().describe("GUID of a parent GameObject"),
38
+ }, async (params) => {
39
+ const res = await bridge.send("spawn_model", params);
40
+ if (!res.success) {
41
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
42
+ }
43
+ return {
44
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
45
+ };
46
+ });
47
+ // ── spawn_citizen ──────────────────────────────────────────────────
48
+ server.tool("spawn_citizen", "Spawn an animated Citizen character: a SkinnedModelRenderer with the Citizen model, plus (by default) a CitizenAnimationHelper so it idles. PlayAnimationsInEditorScene is enabled so the idle pose shows in the editor view (screenshot-verifiable). Dress it afterward with dress_citizen, pose it with pose_citizen.", {
49
+ name: z.string().optional().describe("GameObject name (default 'Citizen')"),
50
+ model: z
51
+ .string()
52
+ .optional()
53
+ .describe("Override the skinned model (default 'models/citizen/citizen.vmdl')"),
54
+ position: Vector3Schema.optional().describe("World position"),
55
+ rotation: RotationSchema.optional().describe("World rotation"),
56
+ scale: Vector3Schema.optional().describe("World scale (default 1,1,1)"),
57
+ tint: ColorSchema.optional().describe("Body tint colour"),
58
+ animator: z
59
+ .boolean()
60
+ .optional()
61
+ .describe("Add a CitizenAnimationHelper for idle/pose (default true)"),
62
+ holdType: z
63
+ .string()
64
+ .optional()
65
+ .describe("Initial hold pose, e.g. None, Pistol, Rifle, Shotgun, HoldItem, Punch, Swing"),
66
+ moveStyle: z
67
+ .string()
68
+ .optional()
69
+ .describe("Movement style, e.g. Auto, Walk, Run"),
70
+ parentId: z.string().optional().describe("GUID of a parent GameObject"),
71
+ }, async (params) => {
72
+ const res = await bridge.send("spawn_citizen", params);
73
+ if (!res.success) {
74
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
75
+ }
76
+ return {
77
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
78
+ };
79
+ });
80
+ // ── dress_citizen ──────────────────────────────────────────────────
81
+ server.tool("dress_citizen", "Dress a spawned Citizen (or any GameObject with a SkinnedModelRenderer) by applying .clothing resources. Pass an array of clothing resource paths; they're loaded, added to a ClothingContainer, and applied to the body. Returns which paths applied vs were not found.", {
82
+ id: z.string().describe("GUID of the Citizen GameObject"),
83
+ clothing: z
84
+ .array(z.string())
85
+ .describe("Clothing resource paths, e.g. ['models/citizen_clothes/jacket/jacket.clothing']"),
86
+ tint: z
87
+ .number()
88
+ .min(0)
89
+ .max(1)
90
+ .optional()
91
+ .describe("ClothingContainer tint position 0-1 (skin/clothing colour variation)"),
92
+ }, async (params) => {
93
+ const res = await bridge.send("dress_citizen", params);
94
+ if (!res.success) {
95
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
96
+ }
97
+ return {
98
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
99
+ };
100
+ });
101
+ // ── set_bodygroup ──────────────────────────────────────────────────
102
+ server.tool("set_bodygroup", "Show/hide a bodygroup on a SkinnedModelRenderer (e.g. hide hands when holding a tool, swap head variants). Provide value (int index) or choice (string name).", {
103
+ id: z.string().describe("GUID of the GameObject with a SkinnedModelRenderer"),
104
+ name: z.string().describe("Bodygroup name"),
105
+ value: z.number().int().optional().describe("Bodygroup choice index"),
106
+ choice: z.string().optional().describe("Bodygroup choice by name (alternative to value)"),
107
+ }, async (params) => {
108
+ const res = await bridge.send("set_bodygroup", params);
109
+ if (!res.success) {
110
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
111
+ }
112
+ return {
113
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
114
+ };
115
+ });
116
+ // ── pose_citizen ───────────────────────────────────────────────────
117
+ server.tool("pose_citizen", "Pose a Citizen by setting CitizenAnimationHelper params (enables PlayAnimationsInEditorScene so the pose shows in-editor). Set holdType (None/Pistol/Rifle/Shotgun/HoldItem/Punch/Swing), moveStyle (Auto/Walk/Run), specialMove, sitting (bool), and/or duckLevel (0-1).", {
118
+ id: z.string().describe("GUID of the Citizen GameObject (must have a CitizenAnimationHelper)"),
119
+ holdType: z.string().optional().describe("Hold pose, e.g. None, Pistol, Rifle, Shotgun, HoldItem"),
120
+ moveStyle: z.string().optional().describe("Movement style, e.g. Auto, Walk, Run"),
121
+ specialMove: z.string().optional().describe("Special move style, e.g. None, LedgeGrab, Roll"),
122
+ sitting: z.boolean().optional().describe("Sit the character (IsSitting)"),
123
+ duckLevel: z.number().min(0).max(1).optional().describe("Crouch amount, 0-1"),
124
+ }, async (params) => {
125
+ const res = await bridge.send("pose_citizen", params);
126
+ if (!res.success) {
127
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
128
+ }
129
+ return {
130
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
131
+ };
132
+ });
133
+ // ── equip_model ────────────────────────────────────────────────────
134
+ server.tool("equip_model", "Attach a prop model (weapon, hat, tool) to a Citizen's bone or attachment point — the prop is parented so it follows that point. Tries the point as an attachment (hand_R, hand_L, eyes, hat) then as a bone. Great for arming NPCs or adding accessories.", {
135
+ id: z.string().describe("GUID of the GameObject with a SkinnedModelRenderer"),
136
+ model: z.string().describe("Prop model path, e.g. 'models/dev/box.vmdl'"),
137
+ point: z
138
+ .string()
139
+ .optional()
140
+ .describe("Attachment or bone name (default 'hand_R'; try hand_L, eyes, hat, head)"),
141
+ offset: Vector3Schema.optional().describe("Local position offset from the point"),
142
+ rotation: RotationSchema.optional().describe("Local rotation offset"),
143
+ tint: ColorSchema.optional().describe("Prop tint colour"),
144
+ name: z.string().optional().describe("Name for the prop GameObject"),
145
+ }, async (params) => {
146
+ const res = await bridge.send("equip_model", params);
147
+ if (!res.success) {
148
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
149
+ }
150
+ return {
151
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
152
+ };
153
+ });
154
+ // ── set_look_at ────────────────────────────────────────────────────
155
+ server.tool("set_look_at", "Aim a Citizen's gaze. Pass target {x,y,z} (spawns a LookTarget) or targetId (existing GameObject) and the head/eyes track it. Pass enabled:false to turn gaze tracking off. Tune eyesWeight/headWeight/bodyWeight (0-1).", {
156
+ id: z.string().describe("GUID of the Citizen (must have a CitizenAnimationHelper)"),
157
+ target: Vector3Schema.optional().describe("World point to look at"),
158
+ targetId: z
159
+ .string()
160
+ .optional()
161
+ .describe("GUID of a GameObject to look at (overrides target)"),
162
+ enabled: z.boolean().optional().describe("false to disable gaze tracking"),
163
+ eyesWeight: z.number().min(0).max(1).optional().describe("Eye look weight 0-1"),
164
+ headWeight: z.number().min(0).max(1).optional().describe("Head turn weight 0-1"),
165
+ bodyWeight: z.number().min(0).max(1).optional().describe("Body turn weight 0-1"),
166
+ }, async (params) => {
167
+ const res = await bridge.send("set_look_at", params);
168
+ if (!res.success) {
169
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
170
+ }
171
+ return {
172
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
173
+ };
174
+ });
175
+ // ── add_ragdoll ────────────────────────────────────────────────────
176
+ server.tool("add_ragdoll", "Add ModelPhysics to a skinned model so it becomes a ragdoll (physics-driven bones). NOTE: the ragdoll only flops in PLAY mode — it won't move in the static editor view, so this one is verified structurally, not by screenshot.", {
177
+ id: z.string().describe("GUID of the GameObject with a SkinnedModelRenderer"),
178
+ motionEnabled: z
179
+ .boolean()
180
+ .optional()
181
+ .describe("Whether physics bodies start with motion enabled"),
182
+ }, async (params) => {
183
+ const res = await bridge.send("add_ragdoll", params);
184
+ if (!res.success) {
185
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
186
+ }
187
+ return {
188
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
189
+ };
190
+ });
191
+ // ── set_expression ─────────────────────────────────────────────────
192
+ server.tool("set_expression", "Set a facial morph (blendshape) on a skinned model — e.g. smile, frown, blink. Call with NO morph to list the model's available morph names (returned as availableMorphs). weight is typically 0-1.", {
193
+ id: z.string().describe("GUID of the GameObject with a SkinnedModelRenderer"),
194
+ morph: z
195
+ .string()
196
+ .optional()
197
+ .describe("Morph/blendshape name (omit to list available morphs)"),
198
+ weight: z.number().optional().describe("Morph weight, typically 0-1 (default 1)"),
199
+ }, async (params) => {
200
+ const res = await bridge.send("set_expression", params);
201
+ if (!res.success) {
202
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
203
+ }
204
+ return {
205
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
206
+ };
207
+ });
208
+ }
209
+ //# sourceMappingURL=characters.js.map
@@ -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 registerLevelTools(server: McpServer, bridge: BridgeClient): void;
4
+ //# sourceMappingURL=leveltools.d.ts.map
@@ -0,0 +1,148 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Scene & level-building tools (Batch 21): snap-to-ground, align, distribute,
4
+ * grid-duplicate, and measure. Transform-level operations for arranging a
5
+ * scene — all verifiable via the editor (screenshot or hierarchy/state).
6
+ */
7
+ const Vector3Schema = z
8
+ .object({ x: z.number(), y: z.number(), z: z.number() })
9
+ .describe("Vector {x,y,z}");
10
+ export function registerLevelTools(server, bridge) {
11
+ // ── snap_to_ground ─────────────────────────────────────────────────
12
+ 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.", {
13
+ id: z.string().describe("GUID of the GameObject to snap"),
14
+ offset: z.number().optional().describe("Height above the surface to place it (default 0)"),
15
+ startHeight: z.number().optional().describe("How far above the object to start the trace (default 2000)"),
16
+ maxDistance: z.number().optional().describe("Max trace distance downward (default 20000)"),
17
+ }, async (params) => {
18
+ const res = await bridge.send("snap_to_ground", params);
19
+ if (!res.success) {
20
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
21
+ }
22
+ return {
23
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
24
+ };
25
+ });
26
+ // ── align_objects ──────────────────────────────────────────────────
27
+ 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.", {
28
+ ids: z.array(z.string()).describe("GUIDs of the GameObjects to align (>= 2)"),
29
+ axis: z.enum(["x", "y", "z"]).describe("Axis to align on"),
30
+ mode: z
31
+ .enum(["first", "min", "max", "average"])
32
+ .optional()
33
+ .describe("Target coordinate to align to (default first)"),
34
+ }, async (params) => {
35
+ const res = await bridge.send("align_objects", params);
36
+ if (!res.success) {
37
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
38
+ }
39
+ return {
40
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
41
+ };
42
+ });
43
+ // ── distribute_objects ─────────────────────────────────────────────
44
+ server.tool("distribute_objects", "Evenly space GameObjects along an axis between the lowest and highest (keeps the two ends fixed, spreads the rest evenly).", {
45
+ ids: z.array(z.string()).describe("GUIDs of the GameObjects to distribute (>= 3)"),
46
+ axis: z.enum(["x", "y", "z"]).describe("Axis to distribute along"),
47
+ }, async (params) => {
48
+ const res = await bridge.send("distribute_objects", params);
49
+ if (!res.success) {
50
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
51
+ }
52
+ return {
53
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
54
+ };
55
+ });
56
+ // ── grid_duplicate ─────────────────────────────────────────────────
57
+ server.tool("grid_duplicate", "Clone a GameObject into an X/Y/Z grid with fixed spacing (the original stays in place). Each count is clamped to 50 and total clones to 500. Great for fences, crates, pillars, foliage rows.", {
58
+ id: z.string().describe("GUID of the GameObject to clone"),
59
+ countX: z.number().int().optional().describe("Copies along X (default 1)"),
60
+ countY: z.number().int().optional().describe("Copies along Y (default 1)"),
61
+ countZ: z.number().int().optional().describe("Copies along Z (default 1)"),
62
+ spacing: Vector3Schema.optional().describe("Spacing between copies per axis (default 100,100,100)"),
63
+ }, async (params) => {
64
+ const res = await bridge.send("grid_duplicate", params);
65
+ if (!res.success) {
66
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
67
+ }
68
+ return {
69
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
70
+ };
71
+ });
72
+ // ── measure_distance ───────────────────────────────────────────────
73
+ server.tool("measure_distance", "Measure the distance between two points or two GameObjects. Provide a/b as {x,y,z} or idA/idB as GUIDs. Returns straight-line distance, horizontal (ground) distance, and the delta vector. Read-only (works during play).", {
74
+ a: Vector3Schema.optional().describe("First point {x,y,z}"),
75
+ b: Vector3Schema.optional().describe("Second point {x,y,z}"),
76
+ idA: z.string().optional().describe("First GameObject GUID (overrides a)"),
77
+ idB: z.string().optional().describe("Second GameObject GUID (overrides b)"),
78
+ }, async (params) => {
79
+ const res = await bridge.send("measure_distance", params);
80
+ if (!res.success) {
81
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
82
+ }
83
+ return {
84
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
85
+ };
86
+ });
87
+ // ── scatter_props ──────────────────────────────────────────────────
88
+ 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.", {
89
+ model: z.string().describe("Model path to scatter, e.g. 'models/dev/box.vmdl'"),
90
+ center: Vector3Schema.optional().describe("Centre of the scatter area (default origin)"),
91
+ radius: z.number().optional().describe("Scatter radius in units (default 256)"),
92
+ count: z.number().int().optional().describe("How many to place (default 10, max 300)"),
93
+ randomYaw: z.boolean().optional().describe("Randomly rotate each around Z (default true)"),
94
+ snapToGround: z.boolean().optional().describe("Raycast each onto the surface below (default true)"),
95
+ scaleMin: z.number().optional().describe("Min uniform scale (default 1)"),
96
+ scaleMax: z.number().optional().describe("Max uniform scale (default 1; set >min for size variation)"),
97
+ tint: z
98
+ .object({
99
+ r: z.number().min(0),
100
+ g: z.number().min(0),
101
+ b: z.number().min(0),
102
+ a: z.number().min(0).max(1).optional(),
103
+ })
104
+ .optional()
105
+ .describe("Tint applied to every copy"),
106
+ seed: z.number().int().optional().describe("PRNG seed for a reproducible layout (default 1)"),
107
+ group: z.boolean().optional().describe("Parent all copies under one group object (default true)"),
108
+ name: z.string().optional().describe("Base name for the props/group (default 'Prop')"),
109
+ }, async (params) => {
110
+ const res = await bridge.send("scatter_props", params);
111
+ if (!res.success) {
112
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
113
+ }
114
+ return {
115
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
116
+ };
117
+ });
118
+ // ── randomize_transforms ───────────────────────────────────────────
119
+ 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.", {
120
+ ids: z.array(z.string()).describe("GUIDs of the GameObjects to randomize"),
121
+ randomYaw: z.boolean().optional().describe("Randomize Z rotation (default true)"),
122
+ scaleMin: z.number().optional().describe("Min uniform scale (default 1)"),
123
+ scaleMax: z.number().optional().describe("Max uniform scale (default 1; set >min to vary)"),
124
+ seed: z.number().int().optional().describe("PRNG seed (default 1)"),
125
+ }, async (params) => {
126
+ const res = await bridge.send("randomize_transforms", params);
127
+ if (!res.success) {
128
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
129
+ }
130
+ return {
131
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
132
+ };
133
+ });
134
+ // ── group_objects ──────────────────────────────────────────────────
135
+ server.tool("group_objects", "Parent a set of GameObjects under a new empty group object (placed at their centroid) — tidies the hierarchy and lets you move/rotate them together.", {
136
+ ids: z.array(z.string()).describe("GUIDs of the GameObjects to group"),
137
+ name: z.string().optional().describe("Name for the group object (default 'Group')"),
138
+ }, async (params) => {
139
+ const res = await bridge.send("group_objects", params);
140
+ if (!res.success) {
141
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
142
+ }
143
+ return {
144
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
145
+ };
146
+ });
147
+ }
148
+ //# sourceMappingURL=leveltools.js.map
@@ -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 registerObjectTools(server: McpServer, bridge: BridgeClient): void;
4
+ //# sourceMappingURL=objecttools.d.ts.map
@@ -0,0 +1,80 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Object utility & query tools (Batch 23): find objects in the scene, and
4
+ * bulk-edit tint / model / tags across one or many objects. find_objects is
5
+ * the composable workhorse — query GUIDs, then feed them to align/distribute/
6
+ * set_tint/group/etc.
7
+ */
8
+ const ColorSchema = z
9
+ .object({
10
+ r: z.number().min(0).describe("Red, 0-1"),
11
+ g: z.number().min(0).describe("Green, 0-1"),
12
+ b: z.number().min(0).describe("Blue, 0-1"),
13
+ a: z.number().min(0).max(1).optional().describe("Alpha, 0-1 (default 1)"),
14
+ })
15
+ .describe("RGBA colour as 0-1 floats");
16
+ export function registerObjectTools(server, bridge) {
17
+ // ── find_objects ───────────────────────────────────────────────────
18
+ server.tool("find_objects", "Query the scene for GameObjects by name (case-insensitive substring), component type name, and/or tag — combine filters (AND). Returns {id,name} for matches (limit default 50, max 500). Read-only; works during play. Use it to get GUIDs to feed into align/distribute/set_tint/group/delete/etc.", {
19
+ name: z.string().optional().describe("Name substring (case-insensitive)"),
20
+ component: z
21
+ .string()
22
+ .optional()
23
+ .describe("Component type name, e.g. 'PointLight', 'SkinnedModelRenderer'"),
24
+ tag: z.string().optional().describe("Tag the object must have"),
25
+ limit: z.number().int().optional().describe("Max results (default 50, max 500)"),
26
+ }, async (params) => {
27
+ const res = await bridge.send("find_objects", params);
28
+ if (!res.success) {
29
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
30
+ }
31
+ return {
32
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
33
+ };
34
+ });
35
+ // ── set_tint ───────────────────────────────────────────────────────
36
+ server.tool("set_tint", "Set the renderer tint colour on one object (id) or many (ids) at once. Works on any ModelRenderer/SkinnedModelRenderer.", {
37
+ id: z.string().optional().describe("Single GameObject GUID"),
38
+ ids: z.array(z.string()).optional().describe("Multiple GameObject GUIDs"),
39
+ tint: ColorSchema.describe("Tint colour to apply"),
40
+ }, async (params) => {
41
+ const res = await bridge.send("set_tint", params);
42
+ if (!res.success) {
43
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
44
+ }
45
+ return {
46
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
47
+ };
48
+ });
49
+ // ── replace_model ──────────────────────────────────────────────────
50
+ server.tool("replace_model", "Swap the model on one object (id) or many (ids) — e.g. retheme a row of props in one call.", {
51
+ id: z.string().optional().describe("Single GameObject GUID"),
52
+ ids: z.array(z.string()).optional().describe("Multiple GameObject GUIDs"),
53
+ model: z.string().describe("New model path, e.g. 'models/dev/sphere.vmdl'"),
54
+ }, async (params) => {
55
+ const res = await bridge.send("replace_model", params);
56
+ if (!res.success) {
57
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
58
+ }
59
+ return {
60
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
61
+ };
62
+ });
63
+ // ── set_tags ───────────────────────────────────────────────────────
64
+ server.tool("set_tags", "Add, remove, and/or clear gameplay tags on one object (id) or many (ids). Tags drive collision groups, queries, and triggers.", {
65
+ id: z.string().optional().describe("Single GameObject GUID"),
66
+ ids: z.array(z.string()).optional().describe("Multiple GameObject GUIDs"),
67
+ add: z.array(z.string()).optional().describe("Tags to add"),
68
+ remove: z.array(z.string()).optional().describe("Tags to remove"),
69
+ clear: z.boolean().optional().describe("Remove all existing tags first"),
70
+ }, async (params) => {
71
+ const res = await bridge.send("set_tags", params);
72
+ if (!res.success) {
73
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
74
+ }
75
+ return {
76
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
77
+ };
78
+ });
79
+ }
80
+ //# sourceMappingURL=objecttools.js.map
@@ -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 registerVisualTools(server: McpServer, bridge: BridgeClient): void;
4
+ //# sourceMappingURL=visuals.d.ts.map
@@ -0,0 +1,259 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Visual & atmosphere tools (Batch 17): lighting, post-processing, fog, sky,
4
+ * reflection probes, and compose-it-all presets.
5
+ *
6
+ * These wrap s&box's visual components with sensible parameters so Claude can
7
+ * author scene mood directly, instead of hand-driving add_component_with_properties
8
+ * (which can't even set a Color). After any change, screenshot the scene and read
9
+ * the result — this layer is where the screenshot loop matters most.
10
+ */
11
+ const ColorSchema = z
12
+ .object({
13
+ r: z.number().min(0).describe("Red, 0-1"),
14
+ g: z.number().min(0).describe("Green, 0-1"),
15
+ b: z.number().min(0).describe("Blue, 0-1"),
16
+ a: z.number().min(0).max(1).optional().describe("Alpha, 0-1 (default 1)"),
17
+ })
18
+ .describe("RGBA colour as 0-1 floats");
19
+ const Vector3Schema = z
20
+ .object({ x: z.number(), y: z.number(), z: z.number() })
21
+ .describe("World position {x,y,z}");
22
+ const RotationSchema = z
23
+ .object({ pitch: z.number(), yaw: z.number(), roll: z.number() })
24
+ .describe("Rotation {pitch,yaw,roll} in degrees");
25
+ export function registerVisualTools(server, bridge) {
26
+ // ── add_light ──────────────────────────────────────────────────────
27
+ server.tool("add_light", "Add a light to the active scene. NOTE: s&box lights have no separate brightness field — intensity is the colour magnitude, so 'brightness' scales the colour (use >1 for bright/HDR). Types: directional = sun (aim it with rotation), point = omni-directional (range), spot = cone (range + coneInner/coneOuter degrees), ambient = global fill light.", {
28
+ type: z
29
+ .enum(["directional", "point", "spot", "ambient"])
30
+ .describe("Light type"),
31
+ name: z.string().optional().describe("GameObject name"),
32
+ color: ColorSchema.optional().describe("Light colour (default white)"),
33
+ brightness: z
34
+ .number()
35
+ .optional()
36
+ .describe("Intensity multiplier on the colour (default 1; try 2-10 for point/spot)"),
37
+ range: z
38
+ .number()
39
+ .optional()
40
+ .describe("point/spot only: falloff radius in units (maps to Radius)"),
41
+ coneInner: z
42
+ .number()
43
+ .optional()
44
+ .describe("spot only: inner cone angle in degrees"),
45
+ coneOuter: z
46
+ .number()
47
+ .optional()
48
+ .describe("spot only: outer cone angle in degrees"),
49
+ shadows: z.boolean().optional().describe("Cast shadows (default true)"),
50
+ skyColor: ColorSchema.optional().describe("directional only: ambient sky colour for the upper hemisphere"),
51
+ position: Vector3Schema.optional().describe("World position"),
52
+ rotation: RotationSchema.optional().describe("World rotation — sets the aim direction for directional/spot lights"),
53
+ parentId: z.string().optional().describe("GUID of a parent GameObject"),
54
+ }, async (params) => {
55
+ const res = await bridge.send("add_light", params);
56
+ if (!res.success) {
57
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
58
+ }
59
+ return {
60
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
61
+ };
62
+ });
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.", {
65
+ type: z
66
+ .enum(["gradient"])
67
+ .optional()
68
+ .describe("Fog type (default gradient; cubemap/volumetric coming later)"),
69
+ name: z.string().optional().describe("GameObject name when creating a new fog object"),
70
+ targetId: z
71
+ .string()
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()
86
+ .optional()
87
+ .describe("Distance falloff exponent (higher = sharper onset)"),
88
+ }, async (params) => {
89
+ const res = await bridge.send("set_fog", params);
90
+ if (!res.success) {
91
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
92
+ }
93
+ return {
94
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
95
+ };
96
+ });
97
+ // ── add_post_process ─────────────────────────────────────────────────
98
+ server.tool("add_post_process", "Add (or update) a post-processing effect on the scene's main camera (auto-enables post-processing). Generic: pass the effect component name + any of its properties. Examples — Bloom {Strength, Threshold, Tint}, Tonemapping, ColorAdjustments {Saturation, Brightness, Contrast}, Vignette {Intensity, Color}, FilmGrain, DepthOfField, ChromaticAberration, MotionBlur, Sharpen, AmbientOcclusion. Call describe_type <Effect> to discover a given effect's properties.", {
99
+ effect: z
100
+ .string()
101
+ .describe("Post-process component type name, e.g. 'Bloom', 'Vignette', 'ColorAdjustments'"),
102
+ properties: z
103
+ .record(z.any())
104
+ .optional()
105
+ .describe("Property name -> value. Floats/ints/bools as numbers/bools, colours as {r,g,b,a}, enums as their string name."),
106
+ cameraId: z
107
+ .string()
108
+ .optional()
109
+ .describe("GUID of a specific camera GameObject (default: the scene's main camera)"),
110
+ }, async (params) => {
111
+ const res = await bridge.send("add_post_process", params);
112
+ if (!res.success) {
113
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
114
+ }
115
+ return {
116
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
117
+ };
118
+ });
119
+ // ── set_skybox ───────────────────────────────────────────────────────
120
+ server.tool("set_skybox", "Set the scene's 2D skybox tint / indirect lighting (re-uses an existing SkyBox2D or creates one). Darken the tint for night/dusk. Optionally point it at a .vmat sky material.", {
121
+ tint: ColorSchema.optional().describe("Sky tint colour"),
122
+ indirectLighting: z
123
+ .boolean()
124
+ .optional()
125
+ .describe("Whether the sky contributes indirect/ambient light"),
126
+ material: z.string().optional().describe("Path to a .vmat sky material (optional)"),
127
+ name: z.string().optional().describe("Name for the sky GameObject if one is created"),
128
+ }, async (params) => {
129
+ const res = await bridge.send("set_skybox", params);
130
+ if (!res.success) {
131
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
132
+ }
133
+ return {
134
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
135
+ };
136
+ });
137
+ // ── apply_atmosphere (preset) ─────────────────────────────────────────
138
+ server.tool("apply_atmosphere", "One-call scene mood: composes ambient + directional light, gradient fog, and a camera post-fx stack (tonemap + colour grade + vignette) tuned for the chosen mood. Idempotent — re-runs update the same 'Atmosphere *' objects.", {
139
+ mood: z
140
+ .enum(["horror-night", "foggy-dawn", "overcast", "warm-interior"])
141
+ .describe("Atmosphere preset"),
142
+ }, async (params) => {
143
+ const res = await bridge.send("apply_atmosphere", params);
144
+ if (!res.success) {
145
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
146
+ }
147
+ return {
148
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
149
+ };
150
+ });
151
+ // ── apply_post_fx_look (preset) ───────────────────────────────────────
152
+ server.tool("apply_post_fx_look", "Apply just a camera post-processing look (no lights/fog): cinematic (tonemap + bloom + soft vignette), filmic-horror (desaturated, high-contrast, heavy vignette, film grain), or clean (tonemap only).", {
153
+ look: z
154
+ .enum(["cinematic", "filmic-horror", "clean"])
155
+ .describe("Post-fx look preset"),
156
+ }, async (params) => {
157
+ const res = await bridge.send("apply_post_fx_look", params);
158
+ if (!res.success) {
159
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
160
+ }
161
+ return {
162
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
163
+ };
164
+ });
165
+ // ── add_envmap_probe ─────────────────────────────────────────────────
166
+ server.tool("add_envmap_probe", "Add an environment reflection/ambient probe (EnvmapProbe) at a position with a cubic influence volume — captures local reflections and indirect light for nearby surfaces.", {
167
+ name: z.string().optional().describe("GameObject name"),
168
+ position: Vector3Schema.optional().describe("World position (centre of the probe)"),
169
+ size: z.number().optional().describe("Cubic influence size in units (default 1024)"),
170
+ tint: ColorSchema.optional().describe("Tint applied to the captured environment"),
171
+ feathering: z
172
+ .number()
173
+ .optional()
174
+ .describe("Edge feathering 0-1 for blending between overlapping probes"),
175
+ }, async (params) => {
176
+ const res = await bridge.send("add_envmap_probe", params);
177
+ if (!res.success) {
178
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
179
+ }
180
+ return {
181
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
182
+ };
183
+ });
184
+ // ── spawn_particle (Batch 18: VFX) ───────────────────────────────────
185
+ server.tool("spawn_particle", "Spawn an additive particle effect (no texture asset needed): kind = fire (rising flame), embers (slow drifting glow), or sparks (a one-shot burst). Renders as tinted glowing dots — great for campfires, torches, and impacts. (smoke needs a soft sprite; not in v1.)", {
186
+ kind: z
187
+ .enum(["fire", "embers", "sparks", "magic", "dust", "blood", "snow"])
188
+ .describe("Particle preset"),
189
+ position: Vector3Schema.optional().describe("World position"),
190
+ color: ColorSchema.optional().describe("Override the particle tint"),
191
+ name: z.string().optional().describe("GameObject name"),
192
+ }, async (params) => {
193
+ const res = await bridge.send("spawn_particle", params);
194
+ if (!res.success) {
195
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
196
+ }
197
+ return {
198
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
199
+ };
200
+ });
201
+ // ── add_trail ────────────────────────────────────────────────────────
202
+ server.tool("add_trail", "Attach a motion trail (TrailRenderer) to an existing GameObject (via targetId) so it leaves a trail as it moves — or create a standalone trail object. Only visible while the object is moving.", {
203
+ targetId: z.string().optional().describe("GUID of the GameObject to attach the trail to (else a new 'Trail' object is made)"),
204
+ position: Vector3Schema.optional().describe("World position when creating a standalone trail"),
205
+ lifetime: z.number().optional().describe("How long (seconds) trail points persist"),
206
+ maxPoints: z.number().optional().describe("Max points in the trail"),
207
+ pointDistance: z.number().optional().describe("Min distance between trail points"),
208
+ name: z.string().optional().describe("GameObject name when creating a new one"),
209
+ }, async (params) => {
210
+ const res = await bridge.send("add_trail", params);
211
+ if (!res.success) {
212
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
213
+ }
214
+ return {
215
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
216
+ };
217
+ });
218
+ // ── add_beam ─────────────────────────────────────────────────────────
219
+ server.tool("add_beam", "Create an energy/laser beam (BeamEffect) from a position to a target point — additive, tintable. Good for lasers, tracers, magic beams.", {
220
+ position: Vector3Schema.optional().describe("Beam start (world position of the beam object)"),
221
+ target: Vector3Schema.optional().describe("Beam end point in world space (default: 128u up)"),
222
+ width: z.number().optional().describe("Beam width/scale (default 4)"),
223
+ color: ColorSchema.optional().describe("Beam colour (default white)"),
224
+ name: z.string().optional().describe("GameObject name"),
225
+ }, async (params) => {
226
+ const res = await bridge.send("add_beam", params);
227
+ if (!res.success) {
228
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
229
+ }
230
+ return {
231
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
232
+ };
233
+ });
234
+ // ── create_particle_effect (generic / raw params) ────────────────────
235
+ server.tool("create_particle_effect", "Build a custom additive particle effect from raw params (ParticleEffect + cone emitter + sprite renderer). Use this when the spawn_particle presets aren't what you want. Texture-free (additive Texture.White glow).", {
236
+ position: Vector3Schema.optional().describe("World position"),
237
+ color: ColorSchema.optional().describe("Particle tint (default white)"),
238
+ rate: z.number().optional().describe("Particles per second when looping (default 30)"),
239
+ burst: z.number().optional().describe("Particle count for a one-shot burst when loop=false (default 30)"),
240
+ loop: z.boolean().optional().describe("Continuous emission (default true) vs a single burst"),
241
+ lifetime: z.number().optional().describe("Particle lifetime in seconds (default 2)"),
242
+ size: z.number().optional().describe("Particle size (default 4)"),
243
+ speed: z.number().optional().describe("Emission speed along the cone (default 100)"),
244
+ coneAngle: z.number().optional().describe("Cone half-angle in degrees; ~85 ≈ hemisphere (default 40)"),
245
+ gravity: z.number().optional().describe("Downward force (default 0 = none)"),
246
+ additive: z.boolean().optional().describe("Additive (glow) blending (default true)"),
247
+ maxParticles: z.number().optional().describe("Max live particles (default 500)"),
248
+ name: z.string().optional().describe("GameObject name"),
249
+ }, async (params) => {
250
+ const res = await bridge.send("create_particle_effect", params);
251
+ if (!res.success) {
252
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
253
+ }
254
+ return {
255
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
256
+ };
257
+ });
258
+ }
259
+ //# sourceMappingURL=visuals.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbox-mcp-server",
3
- "version": "1.3.2",
3
+ "version": "1.4.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",