sbox-mcp-server 1.20.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -59,6 +59,10 @@ import { registerInteractionPackTools } from "./tools/interactionpack.js";
59
59
  import { registerLootEconomyTools } from "./tools/looteconomy.js";
60
60
  import { registerUiFeedbackTools } from "./tools/uifeedback.js";
61
61
  import { registerCinematicsTools } from "./tools/cinematics.js";
62
+ import { registerAuditTools } from "./tools/audit.js";
63
+ import { registerBatchTools } from "./tools/batch.js";
64
+ import { registerWorkflowTools } from "./tools/workflow.js";
65
+ import { registerVehicleTools } from "./tools/vehicles.js";
62
66
  // ── CLI flags ──────────────────────────────────────────────────────
63
67
  const args = process.argv.slice(2);
64
68
  /** Read the package version from package.json, or return "unknown" on failure. */
@@ -78,68 +82,68 @@ if (args.includes("--version") || args.includes("-v")) {
78
82
  process.exit(0);
79
83
  }
80
84
  if (args.includes("--help") || args.includes("-h")) {
81
- console.log(`sbox-mcp ${getVersion()} — MCP Server for s&box game engine
82
-
83
- USAGE
84
- node dist/index.js Start the MCP server (stdio transport)
85
- node dist/index.js --help Show this help
86
- node dist/index.js --version Show version
87
-
88
- ENVIRONMENT VARIABLES
89
- SBOX_BRIDGE_IPC_DIR IPC directory — MUST match the s&box addon's dir.
90
- Default: <os tmpdir>/sbox-bridge-ipc
91
- SBOX_BRIDGE_HOST Legacy/cosmetic — shown in get_bridge_status only
92
- SBOX_BRIDGE_PORT Legacy/cosmetic — shown in get_bridge_status only
93
-
94
- CONNECT TO CLAUDE CODE
95
- claude mcp add sbox -- node /path/to/sbox-mcp-server/dist/index.js
96
-
97
- TOOLS (150 total / 142 s&box-editor handlers +16 in v1.5.0)
98
- Project: get_project_info, list_project_files, read_file, write_file
99
- Scripts: create_script, edit_script, delete_script, trigger_hotload
100
- Scenes: list_scenes, load_scene, save_scene, create_scene
101
- GameObjects: create/delete/duplicate/rename_gameobject, set_parent/enabled/transform
102
- Components: get/set_property, get_all_properties, list_available_components, add_component_with_properties, set_prefab_ref
103
- Hierarchy: get_scene_hierarchy (with maxDepth + rootId), get_selected_objects, select_object, focus_object
104
- Assets: search_assets, list_asset_library, install_asset, get_asset_info
105
- Materials: assign_model, create_material, assign_material, set_material_property
106
- Audio: list_sounds, create_sound_event, assign_sound, play_sound_preview
107
- Play Mode: start_play, stop_play, is_playing
108
- Runtime: get/set_runtime_property, take_screenshot
109
- Editor: undo, redo
110
- Prefabs: create_prefab, instantiate_prefab, list_prefabs, get_prefab_info
111
- Physics: add_physics, add_collider, add_joint, raycast
112
- UI: create_razor_ui, add_screen_panel, add_world_panel
113
- Templates: create_player_controller, create_npc_controller, create_game_manager, create_trigger_zone
114
- Networking: add_network_helper, configure_network, get_network_status, network_spawn, set_ownership
115
- Net Scripts: add_sync_property, add_rpc_method, create_networked_player, create_lobby_manager, create_network_events
116
- Publishing: get_project_config, set_project_config, validate_project, set_project_thumbnail, get_package_details
117
- World Gen: invoke_button, list_component_buttons, raycast_terrain, build_terrain_mesh
118
- Map Edit: add_terrain_hill/clearing/trail, clear_terrain_features, sculpt_terrain
119
- Caves: add_cave_waypoint, clear_cave_path
120
- Forest: add_forest_poi/trail, set_forest_seed, clear_forest_pois, paint_forest_density
121
- Placement: place_along_path
122
- Discovery: describe_type, search_types, get_method_signature, find_in_project
123
- Status: get_bridge_status
124
-
125
- ── New in v1.4.0 ───────────────────────────────────
126
- Visual: add_light, set_fog, add_post_process, set_skybox, add_envmap_probe, apply_atmosphere, apply_post_fx_look
127
- Characters: spawn_model, spawn_citizen, dress_citizen, set_bodygroup, pose_citizen, equip_model, set_look_at, add_ragdoll, set_expression
128
- Scene: snap_to_ground, align_objects, distribute_objects, grid_duplicate, measure_distance
129
- Environment: scatter_props, randomize_transforms, group_objects
130
- Utilities: find_objects, set_tint, replace_model, set_tags
131
- VFX (exp): spawn_particle, create_particle_effect, add_trail, add_beam
132
-
133
- ── New in v1.5.0 ───────────────────────────────────
134
- Diagnostics: read_log, get_compile_errors (MCP-server-side — work even if the editor crashed)
135
- Camera: screenshot_from (AIM a shot at an object/point — take_screenshot is fixed to the Main Camera), frame_camera
136
- Navigation: bake_navmesh, get_navmesh_path
137
- Spatial: physics_overlap (volume counterpart to raycast)
138
- Reflections: bake_reflections
139
- Particles: spawn_vpcf (compiled .vpcf via LegacyParticleSystem — the supported particle path)
140
- Console/Exec: console_run, execute_csharp (experimental)
141
- Object utils: remove_component, get_tags
142
- Docs search: search_docs, get_doc_page, list_doc_categories (MCP-server-side — official Facepunch/sbox-docs)
85
+ console.log(`sbox-mcp ${getVersion()} — MCP Server for s&box game engine
86
+
87
+ USAGE
88
+ node dist/index.js Start the MCP server (stdio transport)
89
+ node dist/index.js --help Show this help
90
+ node dist/index.js --version Show version
91
+
92
+ ENVIRONMENT VARIABLES
93
+ SBOX_BRIDGE_IPC_DIR IPC directory — MUST match the s&box addon's dir.
94
+ Default: <os tmpdir>/sbox-bridge-ipc
95
+ SBOX_BRIDGE_HOST Legacy/cosmetic — shown in get_bridge_status only
96
+ SBOX_BRIDGE_PORT Legacy/cosmetic — shown in get_bridge_status only
97
+
98
+ CONNECT TO CLAUDE CODE
99
+ claude mcp add sbox -- node /path/to/sbox-mcp-server/dist/index.js
100
+
101
+ TOOLS (245 total / 237 editor handlers · v2: the native editor MCP server at http://127.0.0.1:7269/mcp is the primary path — this stdio server is the full-surface fallback; --lifeline exposes just the editor-down diagnostics)
102
+ Project: get_project_info, list_project_files, read_file, write_file
103
+ Scripts: create_script, edit_script, delete_script, trigger_hotload
104
+ Scenes: list_scenes, load_scene, save_scene, create_scene
105
+ GameObjects: create/delete/duplicate/rename_gameobject, set_parent/enabled/transform
106
+ Components: get/set_property, get_all_properties, list_available_components, add_component_with_properties, set_prefab_ref
107
+ Hierarchy: get_scene_hierarchy (with maxDepth + rootId), get_selected_objects, select_object, focus_object
108
+ Assets: search_assets, list_asset_library, install_asset, get_asset_info
109
+ Materials: assign_model, create_material, assign_material, set_material_property
110
+ Audio: list_sounds, create_sound_event, assign_sound, play_sound_preview
111
+ Play Mode: start_play, stop_play, is_playing
112
+ Runtime: get/set_runtime_property, take_screenshot
113
+ Editor: undo, redo
114
+ Prefabs: create_prefab, instantiate_prefab, list_prefabs, get_prefab_info
115
+ Physics: add_physics, add_collider, add_joint, raycast
116
+ UI: create_razor_ui, add_screen_panel, add_world_panel
117
+ Templates: create_player_controller, create_npc_controller, create_game_manager, create_trigger_zone
118
+ Networking: add_network_helper, configure_network, get_network_status, network_spawn, set_ownership
119
+ Net Scripts: add_sync_property, add_rpc_method, create_networked_player, create_lobby_manager, create_network_events
120
+ Publishing: get_project_config, set_project_config, validate_project, set_project_thumbnail, get_package_details
121
+ World Gen: invoke_button, list_component_buttons, raycast_terrain, build_terrain_mesh
122
+ Map Edit: add_terrain_hill/clearing/trail, clear_terrain_features, sculpt_terrain
123
+ Caves: add_cave_waypoint, clear_cave_path
124
+ Forest: add_forest_poi/trail, set_forest_seed, clear_forest_pois, paint_forest_density
125
+ Placement: place_along_path
126
+ Discovery: describe_type, search_types, get_method_signature, find_in_project
127
+ Status: get_bridge_status
128
+
129
+ ── New in v1.4.0 ───────────────────────────────────
130
+ Visual: add_light, set_fog, add_post_process, set_skybox, add_envmap_probe, apply_atmosphere, apply_post_fx_look
131
+ Characters: spawn_model, spawn_citizen, dress_citizen, set_bodygroup, pose_citizen, equip_model, set_look_at, add_ragdoll, set_expression
132
+ Scene: snap_to_ground, align_objects, distribute_objects, grid_duplicate, measure_distance
133
+ Environment: scatter_props, randomize_transforms, group_objects
134
+ Utilities: find_objects, set_tint, replace_model, set_tags
135
+ VFX (exp): spawn_particle, create_particle_effect, add_trail, add_beam
136
+
137
+ ── New in v1.5.0 ───────────────────────────────────
138
+ Diagnostics: read_log, get_compile_errors (MCP-server-side — work even if the editor crashed)
139
+ Camera: screenshot_from (AIM a shot at an object/point — take_screenshot is fixed to the Main Camera), frame_camera
140
+ Navigation: bake_navmesh, get_navmesh_path
141
+ Spatial: physics_overlap (volume counterpart to raycast)
142
+ Reflections: bake_reflections
143
+ Particles: spawn_vpcf (compiled .vpcf via LegacyParticleSystem — the supported particle path)
144
+ Console/Exec: console_run, execute_csharp (experimental)
145
+ Object utils: remove_component, get_tags
146
+ Docs search: search_docs, get_doc_page, list_doc_categories (MCP-server-side — official Facepunch/sbox-docs)
143
147
  `);
144
148
  process.exit(0);
145
149
  }
@@ -152,30 +156,47 @@ const server = new McpServer({
152
156
  // server (the way other MCP servers like Supabase / TurboTax do). Use it to
153
157
  // tell Claude how to work effectively with the bridge — the disciplines that
154
158
  // are easy to skip without a reminder.
155
- instructions: `You are working with the s&box Claude Bridge — a file-based IPC bridge into the s&box game engine editor.
156
-
157
- To get good results:
158
-
159
- 1. Always call \`mcp__sbox__get_bridge_status\` first to confirm the bridge addon is connected and s&box is running. If ping responds but other tools time out, the editor side isn't processing requests.
160
-
161
- 2. For visual changes (models, positions, animations, UI panels, lighting), call \`mcp__sbox__take_screenshot\` after the change and READ THE PNG yourself. You're a multimodal model — you can see the result. Guessing about visual outcomes from code alone produces long iteration loops. The screenshot tool saves to <sbox-install>/screenshots/sbox.<timestamp>.png — list the newest file and read it.
162
-
163
- 3. Before writing code that touches an unfamiliar s&box type, call \`mcp__sbox__describe_type\` or \`mcp__sbox__search_types\`. s&box's API changes between SDK versions — reflection is the source of truth, not training data.
164
-
165
- 4. \`get_scene_hierarchy\` honors \`maxDepth\` (default 10) and accepts optional \`rootId\` to traverse from a specific GameObject. Use these to avoid dumping the entire scene into a tool result.
166
-
167
- 5. Scene-mutating tools (create_gameobject, set_property, etc.) refuse during play mode and return a clear error. Stop play before making scene edits.
168
-
169
- 6. First session with the bridge (or when the user asks "how do I start?" / "what can this do?")? Offer to run setup — invoke the \`sbox-setup\` skill: it verifies the connection, detects the user's installed libraries (\`list_libraries\`), recommends a first move, and points to help + feedback.
170
-
171
- If you're running inside Claude Code, install the companion plugin for the full workflow:
172
- /plugin marketplace add LouSputthole/Sbox-Claude
173
- /plugin install sbox-claude
174
-
159
+ instructions: `You are working with the s&box Claude Bridge — a file-based IPC bridge into the s&box game engine editor.
160
+
161
+ To get good results:
162
+
163
+ 1. Always call \`mcp__sbox__get_bridge_status\` first to confirm the bridge addon is connected and s&box is running. If ping responds but other tools time out, the editor side isn't processing requests.
164
+
165
+ 2. For visual changes (models, positions, animations, UI panels, lighting), call \`mcp__sbox__take_screenshot\` after the change and READ THE PNG yourself. You're a multimodal model — you can see the result. Guessing about visual outcomes from code alone produces long iteration loops. The screenshot tool saves to <sbox-install>/screenshots/sbox.<timestamp>.png — list the newest file and read it.
166
+
167
+ 3. Before writing code that touches an unfamiliar s&box type, call \`mcp__sbox__describe_type\` or \`mcp__sbox__search_types\`. s&box's API changes between SDK versions — reflection is the source of truth, not training data.
168
+
169
+ 4. \`get_scene_hierarchy\` honors \`maxDepth\` (default 10) and accepts optional \`rootId\` to traverse from a specific GameObject. Use these to avoid dumping the entire scene into a tool result.
170
+
171
+ 5. Scene-mutating tools (create_gameobject, set_property, etc.) refuse during play mode and return a clear error. Stop play before making scene edits.
172
+
173
+ 6. First session with the bridge (or when the user asks "how do I start?" / "what can this do?")? Offer to run setup — invoke the \`sbox-setup\` skill: it verifies the connection, detects the user's installed libraries (\`list_libraries\`), recommends a first move, and points to help + feedback.
174
+
175
+ If you're running inside Claude Code, install the companion plugin for the full workflow:
176
+ /plugin marketplace add LouSputthole/Sbox-Claude
177
+ /plugin install sbox-claude
178
+
175
179
  The plugin ships an \`sbox-build-feature\` skill that codifies the workflow above plus a list of common s&box gotchas (MathF not available in sandbox, Cloud assets ephemeral, head bone case-sensitive, CitizenAnimationHelper.IkRightHand works at runtime, etc.). Read its SKILL.md before starting non-trivial features.`,
176
180
  });
177
181
  // Bridge client talks to the s&box editor via file IPC. host/port are cosmetic.
178
182
  const bridge = new BridgeClient(process.env.SBOX_BRIDGE_HOST ?? "127.0.0.1", parseInt(process.env.SBOX_BRIDGE_PORT ?? "29015", 10));
183
+ // ── Lifeline mode (v2 migration) ───────────────────────────────────
184
+ // With the native editor MCP server carrying the full tool surface, this stdio
185
+ // server's long-term job is "editor-down diagnostics": the native server dies
186
+ // with the editor; these tools read the log / docs directly and don't. --lifeline
187
+ // registers ONLY that set by filtering server.tool() — everything else is a no-op.
188
+ const LIFELINE_TOOLS = new Set([
189
+ "read_log", "get_compile_errors", "search_docs", "get_doc_page",
190
+ "list_doc_categories", "run_self_test", "get_bridge_status",
191
+ ]);
192
+ if (args.includes("--lifeline")) {
193
+ const realTool = server.tool.bind(server);
194
+ server.tool = (...toolArgs) => {
195
+ if (LIFELINE_TOOLS.has(toolArgs[0]))
196
+ return realTool(...toolArgs);
197
+ return undefined;
198
+ };
199
+ }
179
200
  // Register all tools
180
201
  registerProjectTools(server, bridge);
181
202
  registerScriptTools(server, bridge);
@@ -221,6 +242,10 @@ registerInteractionPackTools(server, bridge);
221
242
  registerLootEconomyTools(server, bridge);
222
243
  registerUiFeedbackTools(server, bridge);
223
244
  registerCinematicsTools(server, bridge);
245
+ registerAuditTools(server, bridge);
246
+ registerBatchTools(server, bridge);
247
+ registerWorkflowTools(server, bridge);
248
+ registerVehicleTools(server, bridge);
224
249
  /** Start the MCP server on stdio and attempt initial Bridge connection. */
225
250
  async function main() {
226
251
  const transport = new StdioServerTransport();
@@ -28,19 +28,19 @@ export function registerAssetTools(server, bridge) {
28
28
  };
29
29
  });
30
30
  // ── list_asset_library ───────────────────────────────────────────
31
- server.tool("list_asset_library", "Browse the s&box community asset library. Search for packages by name, description, or type to find models, maps, and tools to install", {
31
+ server.tool("list_asset_library", "List assets visible to the editor's AssetSystem project assets plus mounted engine/installed-package content (it does NOT query the remote sbox.game library; use install_asset to pull in new packages). Returns { count, assets:[{ name, path, relativePath, assetType }] } pass a returned path to spawn_model, assign_model, or get_asset_info", {
32
32
  query: z
33
33
  .string()
34
34
  .optional()
35
- .describe("Search term for packages"),
35
+ .describe("Search term, case-insensitive substring match against asset NAMES"),
36
36
  type: z
37
37
  .string()
38
38
  .optional()
39
- .describe("Package type filter (e.g. 'model', 'map', 'library')"),
39
+ .describe("Asset type filter, substring-matched against the asset's type (e.g. 'model', 'material', 'sound')"),
40
40
  maxResults: z
41
41
  .number()
42
42
  .optional()
43
- .describe("Maximum results. Defaults to 25"),
43
+ .describe("Maximum results. Defaults to 200"),
44
44
  }, async (params) => {
45
45
  const res = await bridge.send("list_asset_library", params);
46
46
  if (!res.success) {
@@ -51,7 +51,7 @@ export function registerAssetTools(server, bridge) {
51
51
  };
52
52
  });
53
53
  // ── install_asset ────────────────────────────────────────────────
54
- server.tool("install_asset", "Install a community asset package into the project by its ident (e.g. 'facepunch.flatgrass'). Adds it as a project dependency", {
54
+ server.tool("install_asset", "Install a community asset package into the project by its ident (e.g. 'facepunch.flatgrass'). Adds it as a project dependency. Returns { installed, ident, name, path, relativePath, restartRecommended, note } — pass the returned path to spawn_model / assign_model. CAUTION: if the package is a code LIBRARY (adds a PackageReference), trigger_hotload will NOT surface its types — call restart_editor so the new package compiles into the project", {
55
55
  ident: z
56
56
  .string()
57
57
  .describe("Package identifier (e.g. 'facepunch.flatgrass', 'author.package_name')"),
@@ -5,15 +5,15 @@ import { z } from "zod";
5
5
  */
6
6
  export function registerAudioTools(server, bridge) {
7
7
  // ── list_sounds ──────────────────────────────────────────────────
8
- server.tool("list_sounds", "List available sound assets in the project and installed packages. Filter by name", {
8
+ server.tool("list_sounds", "List the project's .sound event files (recursive scan of the project root for *.sound). Returns { count, sounds } — project-relative paths ready to pass to assign_sound, play_sound_preview, or add_lipsync. NOTE: the current handler returns every match (filter/maxResults are not applied) and only covers .sound files in the project tree — use search_assets type='sound' for other sound assets", {
9
9
  filter: z
10
10
  .string()
11
11
  .optional()
12
- .describe("Search filter for sound name or path"),
12
+ .describe("Search filter for sound name or path (currently not applied by the handler — all .sound files are returned)"),
13
13
  maxResults: z
14
14
  .number()
15
15
  .optional()
16
- .describe("Maximum results. Defaults to 50"),
16
+ .describe("Maximum results. Defaults to 50 (currently not applied by the handler)"),
17
17
  }, async (params) => {
18
18
  const res = await bridge.send("list_sounds", params);
19
19
  if (!res.success) {
@@ -24,13 +24,14 @@ export function registerAudioTools(server, bridge) {
24
24
  };
25
25
  });
26
26
  // ── create_sound_event ───────────────────────────────────────────
27
- server.tool("create_sound_event", "Create a new sound event file (.sound) with volume, pitch, distance falloff, and looping settings", {
27
+ server.tool("create_sound_event", "Create a .sound event file wired to a source .vsnd. Returns { created, path, soundReferenced, note } (path is project-relative); errors if the file already exists. Preview the result with play_sound_preview or attach it to an object with assign_sound. Note: .sound events have no loop flag — looping lives on the SoundPointComponent that plays the event", {
28
28
  path: z
29
29
  .string()
30
- .describe("Path for the sound event file (e.g. 'sounds/footstep.sound')"),
30
+ .describe("Project-relative path for the sound event file (e.g. 'sounds/footstep.sound'; '.sound' appended if missing)"),
31
31
  sound: z
32
32
  .string()
33
- .describe("Path to the source sound asset (.vsnd)"),
33
+ .optional()
34
+ .describe("Path to the source sound asset (.vsnd) the event plays. Omit to create an empty event and wire it later"),
34
35
  volume: z
35
36
  .number()
36
37
  .optional()
@@ -39,18 +40,10 @@ export function registerAudioTools(server, bridge) {
39
40
  .number()
40
41
  .optional()
41
42
  .describe("Pitch multiplier. Defaults to 1.0"),
42
- minDistance: z
43
- .number()
44
- .optional()
45
- .describe("Minimum distance before falloff starts (units). Defaults to 100"),
46
43
  maxDistance: z
47
44
  .number()
48
45
  .optional()
49
- .describe("Maximum audible distance (units). Defaults to 2000"),
50
- loop: z
51
- .boolean()
52
- .optional()
53
- .describe("Whether the sound should loop. Defaults to false"),
46
+ .describe("Maximum audible distance in units (sets Distance + enables DistanceAttenuation). Omit for the engine default"),
54
47
  }, async (params) => {
55
48
  const res = await bridge.send("create_sound_event", params);
56
49
  if (!res.success) {
@@ -61,7 +54,7 @@ export function registerAudioTools(server, bridge) {
61
54
  };
62
55
  });
63
56
  // ── assign_sound ─────────────────────────────────────────────────
64
- server.tool("assign_sound", "Attach a sound event to a GameObject via SoundPointComponent. Creates the component if needed", {
57
+ server.tool("assign_sound", "Attach a sound event to a GameObject via SoundPointComponent. Creates the component if needed. Returns { assigned, id, sound, soundLoaded, playOnStart } — soundLoaded:false means the .sound path did not resolve (the component is still added with no event; verify the path with list_sounds)", {
65
58
  id: z.string().describe("GUID of the GameObject"),
66
59
  sound: z
67
60
  .string()
@@ -69,7 +62,7 @@ export function registerAudioTools(server, bridge) {
69
62
  playOnStart: z
70
63
  .boolean()
71
64
  .optional()
72
- .describe("Whether the sound plays automatically when the game starts"),
65
+ .describe("If true, the handler calls StartSound() immediately, so the sound starts playing right away (audible in the editor)"),
73
66
  }, async (params) => {
74
67
  const res = await bridge.send("assign_sound", params);
75
68
  if (!res.success) {
@@ -80,14 +73,14 @@ export function registerAudioTools(server, bridge) {
80
73
  };
81
74
  });
82
75
  // ── play_sound_preview ───────────────────────────────────────────
83
- server.tool("play_sound_preview", "Play a sound in the editor for testing without entering play mode", {
76
+ server.tool("play_sound_preview", "Play a sound in the editor for testing without entering play mode. Returns { playing, sound, volume }. Fire-and-forget via Sound.Play — there is no stop control, and the volume param is echoed back but not currently applied to playback", {
84
77
  sound: z
85
78
  .string()
86
79
  .describe("Sound event or asset path to preview"),
87
80
  volume: z
88
81
  .number()
89
82
  .optional()
90
- .describe("Preview volume (0-1). Defaults to 1.0"),
83
+ .describe("Preview volume (0-1). Defaults to 1.0 (echoed in the response but not currently applied to playback)"),
91
84
  }, async (params) => {
92
85
  const res = await bridge.send("play_sound_preview", params);
93
86
  if (!res.success) {
@@ -0,0 +1,8 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { BridgeClient } from "../transport/bridge-client.js";
3
+ /**
4
+ * Project audit & batch operation tools (v2 relaunch wave 1, Batch 51):
5
+ * find_broken_references, batch_set_property, describe_project.
6
+ */
7
+ export declare function registerAuditTools(server: McpServer, bridge: BridgeClient): void;
8
+ //# sourceMappingURL=audit.d.ts.map
@@ -0,0 +1,65 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Project audit & batch operation tools (v2 relaunch wave 1, Batch 51):
4
+ * find_broken_references, batch_set_property, describe_project.
5
+ */
6
+ export function registerAuditTools(server, bridge) {
7
+ // ── find_broken_references ───────────────────────────────────────
8
+ server.tool("find_broken_references", "Scan the project for broken references, two layers in one call: (1) every GameObject in the open scene — renderers with no Model (missing_model), component properties pointing at DESTROYED GameObjects/Components (dead_gameobject_ref / dead_component_ref), null component entries whose type no longer exists (missing_component); (2) every .scene/.prefab FILE — prefab references to deleted/renamed files (missing_prefab_file). Returns { total, showing, truncated, objectsScanned, filesScanned, issues } — each issue has { id, name, component, kind, detail } (file-level issues carry the file path in name). Fix missing models with assign_model, dead refs with set_property/set_component_reference, missing prefab files by fixing the path or recreating via create_prefab. Read-only; safe any time. Results cap at `limit` (default 100, max 500)", {
9
+ limit: z
10
+ .number()
11
+ .int()
12
+ .optional()
13
+ .describe("Max issues to return (default 100, max 500). total still counts everything"),
14
+ scanFiles: z
15
+ .boolean()
16
+ .optional()
17
+ .describe("Include the .scene/.prefab file scan for missing prefab references. Default true"),
18
+ }, async (params) => {
19
+ const res = await bridge.send("find_broken_references", params);
20
+ if (!res.success) {
21
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
22
+ }
23
+ return {
24
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
25
+ };
26
+ });
27
+ // ── batch_set_property ───────────────────────────────────────────
28
+ server.tool("batch_set_property", "Set ONE component property to the same value across MANY GameObjects in a single call (e.g. Tint on 40 props, Enabled on every light). Pass dryRun:true first to validate — it reports each object's current value and what would change WITHOUT applying anything. Returns { total, succeeded, failed, dryRun, results } with per-object ok/error and the previous value on success. Get ids from find_objects or get_selected_objects. Scene-mutating: refused during play mode. Value coercion matches set_property (numbers, bools, enums, 'x,y,z' vectors, colors, asset paths)", {
29
+ ids: z
30
+ .array(z.string())
31
+ .describe("GUIDs of the GameObjects to modify (from find_objects, get_scene_hierarchy, or get_selected_objects)"),
32
+ component: z
33
+ .string()
34
+ .describe("Component type name present on each object, e.g. 'ModelRenderer', 'PointLight'"),
35
+ property: z
36
+ .string()
37
+ .describe("Property name to set, e.g. 'Tint', 'Enabled', 'LightColor' (see get_all_properties)"),
38
+ value: z
39
+ .any()
40
+ .describe("New value — number, bool, string, enum name, 'x,y,z' vector, color, or asset path"),
41
+ dryRun: z
42
+ .boolean()
43
+ .optional()
44
+ .describe("true = validate and report current values without changing anything. Default false"),
45
+ }, async (params) => {
46
+ const res = await bridge.send("batch_set_property", params);
47
+ if (!res.success) {
48
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
49
+ }
50
+ return {
51
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
52
+ };
53
+ });
54
+ // ── describe_project ─────────────────────────────────────────────
55
+ server.tool("describe_project", "One-call project orientation: identity (name/ident/org/type), the open scene with object count, scene and prefab file lists (capped at 50 each, Libraries/.sbox excluded), code footprint (.cs/.razor counts), custom Component types (up to 100, engine types excluded), and installed libraries. Returns a structured summary — orient here first, then get_scene_hierarchy for the scene, describe_type for components, find_broken_references for project health. Read-only", {}, async (params) => {
56
+ const res = await bridge.send("describe_project", params);
57
+ if (!res.success) {
58
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
59
+ }
60
+ return {
61
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
62
+ };
63
+ });
64
+ }
65
+ //# sourceMappingURL=audit.js.map
@@ -0,0 +1,8 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { BridgeClient } from "../transport/bridge-client.js";
3
+ /**
4
+ * Batch operation tools (Batch 52): batch_delete, batch_add_component,
5
+ * batch_reparent. All follow the dryRun convention from batch_set_property.
6
+ */
7
+ export declare function registerBatchTools(server: McpServer, bridge: BridgeClient): void;
8
+ //# sourceMappingURL=batch.d.ts.map
@@ -0,0 +1,77 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Batch operation tools (Batch 52): batch_delete, batch_add_component,
4
+ * batch_reparent. All follow the dryRun convention from batch_set_property.
5
+ */
6
+ export function registerBatchTools(server, bridge) {
7
+ // ── batch_delete ─────────────────────────────────────────────────
8
+ server.tool("batch_delete", "Delete MANY GameObjects (and their entire subtrees) in one call. ALWAYS pass dryRun:true first — it reports each object's name and child count WITHOUT deleting, so you can confirm the target list. Returns { total, succeeded, failed, dryRun, results } with per-object ok/error. Destructive and NOT undoable — get ids from find_objects or get_selected_objects and verify the dry-run before applying. Scene-mutating: refused during play mode", {
9
+ ids: z
10
+ .array(z.string())
11
+ .describe("GUIDs of the GameObjects to delete (subtrees included)"),
12
+ dryRun: z
13
+ .boolean()
14
+ .optional()
15
+ .describe("true = report names/child counts without deleting anything. Default false — but run a dry pass first"),
16
+ }, async (params) => {
17
+ const res = await bridge.send("batch_delete", params);
18
+ if (!res.success) {
19
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
20
+ }
21
+ return {
22
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
23
+ };
24
+ });
25
+ // ── batch_add_component ──────────────────────────────────────────
26
+ server.tool("batch_add_component", "Add one component type to MANY GameObjects in one call (e.g. BoxCollider on every crate). Skips objects that already have the component unless skipExisting:false. dryRun:true validates the type and reports per-object alreadyHas WITHOUT adding. Returns { total, succeeded, failed, skipped, dryRun, results }. Follow with batch_set_property to configure the new components. Scene-mutating: refused during play mode", {
27
+ ids: z
28
+ .array(z.string())
29
+ .describe("GUIDs of the target GameObjects (from find_objects or get_selected_objects)"),
30
+ component: z
31
+ .string()
32
+ .describe("Component type name to add, e.g. 'BoxCollider', 'PointLight' (search with list_available_components)"),
33
+ skipExisting: z
34
+ .boolean()
35
+ .optional()
36
+ .describe("Skip objects that already have this component type. Default true"),
37
+ dryRun: z
38
+ .boolean()
39
+ .optional()
40
+ .describe("true = validate and report without adding anything. Default false"),
41
+ }, async (params) => {
42
+ const res = await bridge.send("batch_add_component", params);
43
+ if (!res.success) {
44
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
45
+ }
46
+ return {
47
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
48
+ };
49
+ });
50
+ // ── batch_reparent ───────────────────────────────────────────────
51
+ server.tool("batch_reparent", "Move MANY GameObjects under a new parent (or to the scene root) in one call — organize scattered props into a folder object, or regroup a level section. dryRun:true reports each object's current parent and destination WITHOUT moving. Returns { total, succeeded, failed, dryRun, keepWorldPosition, results }. Errors if parent is among ids (no cycles). Scene-mutating: refused during play mode", {
52
+ ids: z
53
+ .array(z.string())
54
+ .describe("GUIDs of the GameObjects to move"),
55
+ parent: z
56
+ .string()
57
+ .optional()
58
+ .describe("GUID of the new parent GameObject. Omit (or empty) to move to the scene root"),
59
+ keepWorldPosition: z
60
+ .boolean()
61
+ .optional()
62
+ .describe("Preserve each object's world position while reparenting. Default true"),
63
+ dryRun: z
64
+ .boolean()
65
+ .optional()
66
+ .describe("true = report current parents and the destination without moving anything. Default false"),
67
+ }, async (params) => {
68
+ const res = await bridge.send("batch_reparent", params);
69
+ if (!res.success) {
70
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
71
+ }
72
+ return {
73
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
74
+ };
75
+ });
76
+ }
77
+ //# sourceMappingURL=batch.js.map
@@ -56,7 +56,7 @@ export function registerCharacterTools(server, bridge) {
56
56
  };
57
57
  });
58
58
  // ── spawn_citizen ──────────────────────────────────────────────────
59
- 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.", {
59
+ 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). Returns { created, hasAnimator, gameObject:{ id, name, position, components, ... } } — pass gameObject.id to the follow-ups: dress_citizen, pose_citizen, set_expression, equip_model.", {
60
60
  name: z.string().optional().describe("GameObject name (default 'Citizen')"),
61
61
  model: z
62
62
  .string()
@@ -110,7 +110,7 @@ export function registerCharacterTools(server, bridge) {
110
110
  };
111
111
  });
112
112
  // ── set_bodygroup ──────────────────────────────────────────────────
113
- 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).", {
113
+ 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). Returns { set, bodygroup } on success (errors if the object has no SkinnedModelRenderer or neither value nor choice is given); take_screenshot to verify the visual change.", {
114
114
  id: z.string().describe("GUID of the GameObject with a SkinnedModelRenderer"),
115
115
  name: z.string().describe("Bodygroup name"),
116
116
  value: z.number().int().optional().describe("Bodygroup choice index"),
@@ -125,7 +125,7 @@ export function registerCharacterTools(server, bridge) {
125
125
  };
126
126
  });
127
127
  // ── pose_citizen ───────────────────────────────────────────────────
128
- 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).", {
128
+ 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). Returns { posed, changed:[...], gameObject } — changed lists which helper params were applied; take_screenshot to verify the pose.", {
129
129
  id: z.string().describe("GUID of the Citizen GameObject (must have a CitizenAnimationHelper)"),
130
130
  holdType: z.string().optional().describe("Hold pose, e.g. None, Pistol, Rifle, Shotgun, HoldItem"),
131
131
  moveStyle: z.string().optional().describe("Movement style, e.g. Auto, Walk, Run"),
@@ -184,7 +184,7 @@ export function registerCharacterTools(server, bridge) {
184
184
  };
185
185
  });
186
186
  // ── add_ragdoll ────────────────────────────────────────────────────
187
- 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.", {
187
+ 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. Returns { ragdoll, note, gameObject } — check gameObject.components for ModelPhysics, then verify at runtime with start_play + capture_view.", {
188
188
  id: z.string().describe("GUID of the GameObject with a SkinnedModelRenderer"),
189
189
  motionEnabled: z
190
190
  .boolean()
@@ -200,7 +200,7 @@ export function registerCharacterTools(server, bridge) {
200
200
  };
201
201
  });
202
202
  // ── set_expression ─────────────────────────────────────────────────
203
- 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.", {
203
+ 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. Returns { set, morph, weight, availableMorphs } — availableMorphs is included in every response, so a wrong name can be corrected without an extra listing call.", {
204
204
  id: z.string().describe("GUID of the GameObject with a SkinnedModelRenderer"),
205
205
  morph: z
206
206
  .string()
@@ -244,7 +244,7 @@ export function registerCharacterTools(server, bridge) {
244
244
  };
245
245
  });
246
246
  // ── set_animgraph_param ────────────────────────────────────────────
247
- server.tool("set_animgraph_param", "Set an AnimationGraph parameter on a GameObject's SkinnedModelRenderer (calls Set). This drives Citizen/animgraph motion — e.g. 'move_x'/'move_y' (float), 'b_grounded'/'b_ducked' (bool), or a Vector3. Pose previews in-editor when PlayAnimationsInEditorScene is on; screenshot to verify. Param names are defined by the model's animation graph.", {
247
+ server.tool("set_animgraph_param", "Set an AnimationGraph parameter on a GameObject's SkinnedModelRenderer (calls Set). This drives Citizen/animgraph motion — e.g. 'move_x'/'move_y' (float), 'b_grounded'/'b_ducked' (bool), or a Vector3. Pose previews in-editor when PlayAnimationsInEditorScene is on; screenshot to verify. Param names are defined by the model's animation graph — use list_animations to check whether the model is animgraph-driven. Returns { set, param, kind, note } — kind is the type actually applied (float/int/bool/vector).", {
248
248
  id: z.string().describe("GUID of the GameObject with a SkinnedModelRenderer"),
249
249
  param: z.string().describe("Animgraph parameter name, e.g. 'move_x', 'b_grounded'"),
250
250
  value: z
@@ -16,7 +16,7 @@ import { z } from "zod";
16
16
  */
17
17
  export function registerCinematicsTools(server, bridge) {
18
18
  // ── create_cutscene_director ──────────────────────────────────────
19
- server.tool("create_cutscene_director", "Generate a hand-authored cutscene player component that needs NO .movie asset — the zero-asset alternative to the MovieMaker family (add_movie_player/play_movie, which play keyframed .movie clips authored in the editor's Movie Maker dock). You author the shots directly in the inspector as parallel lists: ShotPositions (Vector3), ShotAngles (pitch/yaw/roll — Angles, not raw quaternions), ShotHoldSeconds, ShotBlendSeconds, and an optional per-shot ShotLookAt GameObject (aim at a target instead of using ShotAngles). At runtime it takes over the main camera (Scene.Camera) in OnPreRender ONLY while playing — smoothstep-eased Vector3.Lerp + Rotation.Slerp between shots — captures the camera's prior transform and restores it exactly when finished (same un-apply discipline as create_camera_shake). Play from any game code via the static <Name>.Play() (first director) or <Name>.Play(\"name\") (matches CutsceneName); subscribe the static <Name>.OnCutsceneFinished, or gate game logic on the static <Name>.IsCutscenePlaying. LockInput freezes player input each frame via Input.ClearActions() while still reading the SkipAction press first so the cutscene stays skippable. Optional letterbox generates a razor_lint-safe black-bars overlay panel (host under a ScreenPanel) shown while IsCutscenePlaying. LOCAL-only (each client renders its own view) — trigger inside an [Rpc.Broadcast] for all clients. Attach to ANY GameObject; it drives the camera itself and does not need to sit on the camera. Compile with trigger_hotload afterward.", {
19
+ server.tool("create_cutscene_director", "Generate a hand-authored cutscene player component that needs NO .movie asset — the zero-asset alternative to the MovieMaker family (add_movie_player/play_movie, which play keyframed .movie clips authored in the editor's Movie Maker dock). You author the shots directly in the inspector as parallel lists: ShotPositions (Vector3), ShotAngles (pitch/yaw/roll — Angles, not raw quaternions), ShotHoldSeconds, ShotBlendSeconds, and an optional per-shot ShotLookAt GameObject (aim at a target instead of using ShotAngles). At runtime it takes over the main camera (Scene.Camera) in OnPreRender ONLY while playing — smoothstep-eased Vector3.Lerp + Rotation.Slerp between shots — captures the camera's prior transform and restores it exactly when finished (same un-apply discipline as create_camera_shake). Play from any game code via the static <Name>.Play() (first director) or <Name>.Play(\"name\") (matches CutsceneName); subscribe the static <Name>.OnCutsceneFinished, or gate game logic on the static <Name>.IsCutscenePlaying. LockInput freezes player input each frame via Input.ClearActions() while still reading the SkipAction press first so the cutscene stays skippable. Optional letterbox generates a razor_lint-safe black-bars overlay panel (host under a ScreenPanel) shown while IsCutscenePlaying. LOCAL-only (each client renders its own view) — trigger inside an [Rpc.Broadcast] for all clients. Attach to ANY GameObject; it drives the camera itself and does not need to sit on the camera. Returns { created, path, className, skipAction, lockInput, letterbox, nextSteps } — path is the generated .cs (letterbox lists the overlay files when enabled). Follow with trigger_hotload, then get_compile_errors, then attach via add_component_with_properties (component=className).", {
20
20
  name: z
21
21
  .string()
22
22
  .optional()
@@ -45,7 +45,7 @@ export function registerCinematicsTools(server, bridge) {
45
45
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
46
46
  });
47
47
  // ── create_dialogue_system ────────────────────────────────────────
48
- server.tool("create_dialogue_system", "Generate an NPC/story dialogue system — a sealed state+data Component paired with a razor_lint-safe Razor HUD panel. Lines are authored in the inspector as a List<string> using the 'Speaker: text' convention (the part before the first colon is the speaker). The generated HUD panel binds to <Name>.Current automatically (no wiring) and renders the current line with a TimeSince-driven typewriter reveal at CharsPerSecond, folding the visible substring into BuildHash so it re-renders as characters appear. Press the AdvanceAction once to snap the whole line into view instantly, again to move to the next line; dismissing the last line ends the conversation. Start from any game code via the static <Name>.StartDialogue(string[] lines) or set Lines and call the instance Begin(). Static events for hooks: OnLineShown(index, speaker) — pair with add_lipsync to drive facial morphs / audio per line — and OnDialogueFinished when the conversation ends. Pairs with create_interactable to trigger dialogue on use. LOCAL-only (per-client HUD) — call StartDialogue inside an [Rpc.Broadcast] if every client should see it. Attach the panel under a ScreenPanel (add_screen_panel) so the HUD renders. Compile with trigger_hotload afterward.", {
48
+ server.tool("create_dialogue_system", "Generate an NPC/story dialogue system — a sealed state+data Component paired with a razor_lint-safe Razor HUD panel. Lines are authored in the inspector as a List<string> using the 'Speaker: text' convention (the part before the first colon is the speaker). The generated HUD panel binds to <Name>.Current automatically (no wiring) and renders the current line with a TimeSince-driven typewriter reveal at CharsPerSecond, folding the visible substring into BuildHash so it re-renders as characters appear. Press the AdvanceAction once to snap the whole line into view instantly, again to move to the next line; dismissing the last line ends the conversation. Start from any game code via the static <Name>.StartDialogue(string[] lines) or set Lines and call the instance Begin(). Static events for hooks: OnLineShown(index, speaker) — pair with add_lipsync to drive facial morphs / audio per line — and OnDialogueFinished when the conversation ends. Pairs with create_interactable to trigger dialogue on use. LOCAL-only (per-client HUD) — call StartDialogue inside an [Rpc.Broadcast] if every client should see it. Attach the panel under a ScreenPanel (add_screen_panel) so the HUD renders. Returns { created, path, className, charsPerSecond, advanceAction, panel, nextSteps } — panel lists the generated '<Name>Panel' HUD files. Follow with trigger_hotload, then get_compile_errors, then attach both components via add_component_with_properties.", {
49
49
  name: z
50
50
  .string()
51
51
  .optional()