sbox-mcp-server 1.20.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/index.js +125 -82
  2. package/dist/tools/aisystems.d.ts +4 -0
  3. package/dist/tools/aisystems.js +236 -0
  4. package/dist/tools/assets.js +5 -5
  5. package/dist/tools/audio.js +12 -19
  6. package/dist/tools/audit.d.ts +8 -0
  7. package/dist/tools/audit.js +65 -0
  8. package/dist/tools/batch.d.ts +8 -0
  9. package/dist/tools/batch.js +77 -0
  10. package/dist/tools/characters.js +6 -6
  11. package/dist/tools/cinematicrecording.d.ts +31 -0
  12. package/dist/tools/cinematicrecording.js +107 -0
  13. package/dist/tools/cinematics.js +2 -2
  14. package/dist/tools/components.js +2 -2
  15. package/dist/tools/debugdraw.js +4 -4
  16. package/dist/tools/diagnostics.js +7 -5
  17. package/dist/tools/dialoguefx.d.ts +4 -0
  18. package/dist/tools/dialoguefx.js +144 -0
  19. package/dist/tools/discovery.js +6 -6
  20. package/dist/tools/economysave.d.ts +27 -0
  21. package/dist/tools/economysave.js +231 -0
  22. package/dist/tools/gameobjects.js +7 -7
  23. package/dist/tools/gameplay.js +4 -4
  24. package/dist/tools/gameplayrecorder.d.ts +18 -0
  25. package/dist/tools/gameplayrecorder.js +73 -0
  26. package/dist/tools/inspection.js +6 -6
  27. package/dist/tools/leveltools.js +5 -5
  28. package/dist/tools/materials.js +4 -4
  29. package/dist/tools/movieauthoring.d.ts +31 -0
  30. package/dist/tools/movieauthoring.js +84 -0
  31. package/dist/tools/moviemaker.js +1 -1
  32. package/dist/tools/multiplayertest.d.ts +23 -0
  33. package/dist/tools/multiplayertest.js +62 -0
  34. package/dist/tools/networking.js +22 -22
  35. package/dist/tools/physics.js +4 -4
  36. package/dist/tools/playmode.js +5 -5
  37. package/dist/tools/playtest.js +2 -1
  38. package/dist/tools/prefabs.js +7 -3
  39. package/dist/tools/project.js +2 -2
  40. package/dist/tools/publishing.js +3 -3
  41. package/dist/tools/roundui.d.ts +22 -0
  42. package/dist/tools/roundui.js +88 -0
  43. package/dist/tools/scenes.js +2 -2
  44. package/dist/tools/scripts.js +3 -3
  45. package/dist/tools/statsachievements.d.ts +4 -0
  46. package/dist/tools/statsachievements.js +243 -0
  47. package/dist/tools/status.js +1 -1
  48. package/dist/tools/templates.js +7 -7
  49. package/dist/tools/ui.js +5 -5
  50. package/dist/tools/uifeedback.js +1 -1
  51. package/dist/tools/vehicles.d.ts +8 -0
  52. package/dist/tools/vehicles.js +98 -0
  53. package/dist/tools/visuals.js +8 -8
  54. package/dist/tools/workflow.d.ts +8 -0
  55. package/dist/tools/workflow.js +107 -0
  56. package/dist/tools/world.js +61 -58
  57. package/dist/tools/worldrender.d.ts +4 -0
  58. package/dist/tools/worldrender.js +183 -0
  59. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -59,6 +59,19 @@ 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";
66
+ import { registerEconomySaveTools } from "./tools/economysave.js";
67
+ import { registerStatsAchievementsTools } from "./tools/statsachievements.js";
68
+ import { registerRoundUiTools } from "./tools/roundui.js";
69
+ import { registerWorldRenderTools } from "./tools/worldrender.js";
70
+ import { registerAiSystemsTools } from "./tools/aisystems.js";
71
+ import { registerGameplayRecorderTools } from "./tools/gameplayrecorder.js";
72
+ import { registerDialogueFxTools } from "./tools/dialoguefx.js";
73
+ import { registerMovieAuthoringTools } from "./tools/movieauthoring.js";
74
+ import { registerCinematicRecordingTools } from "./tools/cinematicrecording.js";
62
75
  // ── CLI flags ──────────────────────────────────────────────────────
63
76
  const args = process.argv.slice(2);
64
77
  /** Read the package version from package.json, or return "unknown" on failure. */
@@ -78,68 +91,68 @@ if (args.includes("--version") || args.includes("-v")) {
78
91
  process.exit(0);
79
92
  }
80
93
  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)
94
+ console.log(`sbox-mcp ${getVersion()} — MCP Server for s&box game engine
95
+
96
+ USAGE
97
+ node dist/index.js Start the MCP server (stdio transport)
98
+ node dist/index.js --help Show this help
99
+ node dist/index.js --version Show version
100
+
101
+ ENVIRONMENT VARIABLES
102
+ SBOX_BRIDGE_IPC_DIR IPC directory — MUST match the s&box addon's dir.
103
+ Default: <os tmpdir>/sbox-bridge-ipc
104
+ SBOX_BRIDGE_HOST Legacy/cosmetic — shown in get_bridge_status only
105
+ SBOX_BRIDGE_PORT Legacy/cosmetic — shown in get_bridge_status only
106
+
107
+ CONNECT TO CLAUDE CODE
108
+ claude mcp add sbox -- node /path/to/sbox-mcp-server/dist/index.js
109
+
110
+ 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)
111
+ Project: get_project_info, list_project_files, read_file, write_file
112
+ Scripts: create_script, edit_script, delete_script, trigger_hotload
113
+ Scenes: list_scenes, load_scene, save_scene, create_scene
114
+ GameObjects: create/delete/duplicate/rename_gameobject, set_parent/enabled/transform
115
+ Components: get/set_property, get_all_properties, list_available_components, add_component_with_properties, set_prefab_ref
116
+ Hierarchy: get_scene_hierarchy (with maxDepth + rootId), get_selected_objects, select_object, focus_object
117
+ Assets: search_assets, list_asset_library, install_asset, get_asset_info
118
+ Materials: assign_model, create_material, assign_material, set_material_property
119
+ Audio: list_sounds, create_sound_event, assign_sound, play_sound_preview
120
+ Play Mode: start_play, stop_play, is_playing
121
+ Runtime: get/set_runtime_property, take_screenshot
122
+ Editor: undo, redo
123
+ Prefabs: create_prefab, instantiate_prefab, list_prefabs, get_prefab_info
124
+ Physics: add_physics, add_collider, add_joint, raycast
125
+ UI: create_razor_ui, add_screen_panel, add_world_panel
126
+ Templates: create_player_controller, create_npc_controller, create_game_manager, create_trigger_zone
127
+ Networking: add_network_helper, configure_network, get_network_status, network_spawn, set_ownership
128
+ Net Scripts: add_sync_property, add_rpc_method, create_networked_player, create_lobby_manager, create_network_events
129
+ Publishing: get_project_config, set_project_config, validate_project, set_project_thumbnail, get_package_details
130
+ World Gen: invoke_button, list_component_buttons, raycast_terrain, build_terrain_mesh
131
+ Map Edit: add_terrain_hill/clearing/trail, clear_terrain_features, sculpt_terrain
132
+ Caves: add_cave_waypoint, clear_cave_path
133
+ Forest: add_forest_poi/trail, set_forest_seed, clear_forest_pois, paint_forest_density
134
+ Placement: place_along_path
135
+ Discovery: describe_type, search_types, get_method_signature, find_in_project
136
+ Status: get_bridge_status
137
+
138
+ ── New in v1.4.0 ───────────────────────────────────
139
+ Visual: add_light, set_fog, add_post_process, set_skybox, add_envmap_probe, apply_atmosphere, apply_post_fx_look
140
+ Characters: spawn_model, spawn_citizen, dress_citizen, set_bodygroup, pose_citizen, equip_model, set_look_at, add_ragdoll, set_expression
141
+ Scene: snap_to_ground, align_objects, distribute_objects, grid_duplicate, measure_distance
142
+ Environment: scatter_props, randomize_transforms, group_objects
143
+ Utilities: find_objects, set_tint, replace_model, set_tags
144
+ VFX (exp): spawn_particle, create_particle_effect, add_trail, add_beam
145
+
146
+ ── New in v1.5.0 ───────────────────────────────────
147
+ Diagnostics: read_log, get_compile_errors (MCP-server-side — work even if the editor crashed)
148
+ Camera: screenshot_from (AIM a shot at an object/point — take_screenshot is fixed to the Main Camera), frame_camera
149
+ Navigation: bake_navmesh, get_navmesh_path
150
+ Spatial: physics_overlap (volume counterpart to raycast)
151
+ Reflections: bake_reflections
152
+ Particles: spawn_vpcf (compiled .vpcf via LegacyParticleSystem — the supported particle path)
153
+ Console/Exec: console_run, execute_csharp (experimental)
154
+ Object utils: remove_component, get_tags
155
+ Docs search: search_docs, get_doc_page, list_doc_categories (MCP-server-side — official Facepunch/sbox-docs)
143
156
  `);
144
157
  process.exit(0);
145
158
  }
@@ -152,30 +165,47 @@ const server = new McpServer({
152
165
  // server (the way other MCP servers like Supabase / TurboTax do). Use it to
153
166
  // tell Claude how to work effectively with the bridge — the disciplines that
154
167
  // 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
-
168
+ instructions: `You are working with the s&box Claude Bridge — a file-based IPC bridge into the s&box game engine editor.
169
+
170
+ To get good results:
171
+
172
+ 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.
173
+
174
+ 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.
175
+
176
+ 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.
177
+
178
+ 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.
179
+
180
+ 5. Scene-mutating tools (create_gameobject, set_property, etc.) refuse during play mode and return a clear error. Stop play before making scene edits.
181
+
182
+ 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.
183
+
184
+ If you're running inside Claude Code, install the companion plugin for the full workflow:
185
+ /plugin marketplace add LouSputthole/Sbox-Claude
186
+ /plugin install sbox-claude
187
+
175
188
  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
189
  });
177
190
  // Bridge client talks to the s&box editor via file IPC. host/port are cosmetic.
178
191
  const bridge = new BridgeClient(process.env.SBOX_BRIDGE_HOST ?? "127.0.0.1", parseInt(process.env.SBOX_BRIDGE_PORT ?? "29015", 10));
192
+ // ── Lifeline mode (v2 migration) ───────────────────────────────────
193
+ // With the native editor MCP server carrying the full tool surface, this stdio
194
+ // server's long-term job is "editor-down diagnostics": the native server dies
195
+ // with the editor; these tools read the log / docs directly and don't. --lifeline
196
+ // registers ONLY that set by filtering server.tool() — everything else is a no-op.
197
+ const LIFELINE_TOOLS = new Set([
198
+ "read_log", "get_compile_errors", "search_docs", "get_doc_page",
199
+ "list_doc_categories", "run_self_test", "get_bridge_status",
200
+ ]);
201
+ if (args.includes("--lifeline")) {
202
+ const realTool = server.tool.bind(server);
203
+ server.tool = (...toolArgs) => {
204
+ if (LIFELINE_TOOLS.has(toolArgs[0]))
205
+ return realTool(...toolArgs);
206
+ return undefined;
207
+ };
208
+ }
179
209
  // Register all tools
180
210
  registerProjectTools(server, bridge);
181
211
  registerScriptTools(server, bridge);
@@ -221,6 +251,19 @@ registerInteractionPackTools(server, bridge);
221
251
  registerLootEconomyTools(server, bridge);
222
252
  registerUiFeedbackTools(server, bridge);
223
253
  registerCinematicsTools(server, bridge);
254
+ registerAuditTools(server, bridge);
255
+ registerBatchTools(server, bridge);
256
+ registerWorkflowTools(server, bridge);
257
+ registerVehicleTools(server, bridge);
258
+ registerEconomySaveTools(server, bridge);
259
+ registerStatsAchievementsTools(server, bridge);
260
+ registerRoundUiTools(server, bridge);
261
+ registerWorldRenderTools(server, bridge);
262
+ registerAiSystemsTools(server, bridge);
263
+ registerGameplayRecorderTools(server, bridge);
264
+ registerDialogueFxTools(server, bridge);
265
+ registerMovieAuthoringTools(server, bridge);
266
+ registerCinematicRecordingTools(server, bridge);
224
267
  /** Start the MCP server on stdio and attempt initial Bridge connection. */
225
268
  async function main() {
226
269
  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 registerAiSystemsTools(server: McpServer, bridge: BridgeClient): void;
4
+ //# sourceMappingURL=aisystems.d.ts.map
@@ -0,0 +1,236 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * AI & Systems pack — five scaffold generators:
4
+ *
5
+ * - create_needs_system sim/tycoon needs engine (decay -> Happiness + events)
6
+ * - create_utility_ai scored-action brain (abstract Action base + brain + 2 examples)
7
+ * - create_npc_schedule_brain daily-routine NPC bound to the day-night clock contract
8
+ * - create_event_bus typed LOCAL pub/sub static class (not a Component)
9
+ * - add_tts_voice TTS speaker over Sandbox.Speech.Synthesizer
10
+ *
11
+ * All write a .cs file into the project (scene/file-mutating; refused during play
12
+ * mode by the bridge dispatch). Every generated template was live-compile-verified
13
+ * (hotload + TypeLibrary-load) against the 2026-07 SDK on 2026-07-12.
14
+ *
15
+ * Mirrors the gamefeel.ts module shape: zod params, one bridge.send per tool,
16
+ * JSON.stringify(res.data) on success.
17
+ */
18
+ // A world-space Vector3 accepted as EITHER an object {x,y,z} OR a comma string
19
+ // "x,y,z", passed through unchanged. The C# handler parses both forms.
20
+ const Vec3 = z
21
+ .union([
22
+ z.object({
23
+ x: z.number().describe("X"),
24
+ y: z.number().describe("Y"),
25
+ z: z.number().describe("Z"),
26
+ }),
27
+ z.string().describe('Comma string "x,y,z", e.g. "0,0,200"'),
28
+ ])
29
+ .describe('A world-space Vector3 — object {x,y,z} OR comma string "x,y,z"');
30
+ export function registerAiSystemsTools(server, bridge) {
31
+ // ── create_needs_system ───────────────────────────────────────────
32
+ server.tool("create_needs_system", "Generate a sim/tycoon needs engine component: a [Property] list of need definitions (name, decay rate/s, critical threshold, weight) with per-need 0..100 values that decay over Time.Delta, Satisfy(name, amount) to restore, an aggregate Happiness (weighted mean, [Sync(FromHost)] when networked), and static OnNeedCritical (edge-triggered: fires once crossing below threshold, re-arms above) + OnHappinessChanged (>0.25-point moves) events. Returns {created, path, className, needs[], propertyNames[], note}. Next: trigger_hotload, get_compile_errors, then attach via targetId re-call or add_component_with_properties; drive from game code (e.g. a create_interactable that calls Satisfy). Limits: per-need values live on the simulating machine only (host) — sync per-need UI yourself via RPCs; events fire on the simulating machine only; networked default true means a no-session solo playtest won't tick (everything is a proxy) — pass networked:false to iterate solo. Refused during play mode; refuses to overwrite an existing file.", {
33
+ name: z
34
+ .string()
35
+ .optional()
36
+ .describe("Class/file name. Defaults to 'NeedsSystem'. Sanitized to a valid C# identifier."),
37
+ directory: z
38
+ .string()
39
+ .optional()
40
+ .describe("Subdirectory under the project root for the .cs file. Defaults to 'Code'."),
41
+ needs: z
42
+ .array(z.object({
43
+ name: z.string().describe("Need name, e.g. 'Hunger' (the key for Satisfy/GetNeed)."),
44
+ decayPerSecond: z.number().optional().describe("Points lost per second on the 0..100 scale. Defaults to 0.5."),
45
+ criticalThreshold: z.number().optional().describe("OnNeedCritical fires when the value falls below this. Defaults to 20."),
46
+ weight: z.number().optional().describe("Contribution to the Happiness weighted mean. Defaults to 1."),
47
+ }))
48
+ .optional()
49
+ .describe("Need definitions baked as inspector-editable defaults. Defaults to the classic sim trio: Hunger(0.8/s), Energy(0.5/s), Fun(0.3/s)."),
50
+ networked: z
51
+ .boolean()
52
+ .optional()
53
+ .describe("true (default): host-authoritative (IsProxy guard) + [Sync(FromHost)] Happiness — needs a host session. false: local build that ticks in a solo playtest."),
54
+ targetId: z
55
+ .string()
56
+ .optional()
57
+ .describe("GUID of a GameObject to attach the component to (only attaches if the type is already in the TypeLibrary — hotload first, then re-call or use add_component_with_properties)."),
58
+ }, async (params) => {
59
+ const res = await bridge.send("create_needs_system", params);
60
+ if (!res.success) {
61
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
62
+ }
63
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
64
+ });
65
+ // ── create_utility_ai ─────────────────────────────────────────────
66
+ server.tool("create_utility_ai", "Generate a utility-AI (scored-action) brain: one file with an abstract {name}Action : Component base (Score() 0..1 + Begin/Tick/End lifecycle), a sealed {name}Brain that every EvaluateInterval picks the highest-scoring sibling action (score × ScoreWeight, current action gets +HysteresisBonus so near-ties don't flip-flop), and two example actions — {name}IdleAction (constant fallback score) and {name}WanderAction (desire builds while idle, walks to random points by direct transform movement, no navmesh). How it differs from create_npc_brain: the FSM has a FIXED transition table; here behavior EMERGES from per-frame scores — add behaviors by subclassing the base on the same GameObject, no transition wiring. Returns {created, path, classNames[4], propertyNames[], note}. Next: trigger_hotload + get_compile_errors, attach the brain AND example actions to one GameObject (targetId attaches only the brain), verify in play mode via get_runtime_property CurrentActionName. Limits: networked default true = host-authoritative (won't tick in a no-session solo playtest — use networked:false); actions Tick on the simulating machine only. Refused during play mode; refuses to overwrite an existing file.", {
67
+ name: z
68
+ .string()
69
+ .optional()
70
+ .describe("System prefix — generates {name}Action / {name}Brain / {name}IdleAction / {name}WanderAction in {name}Ai.cs. Defaults to 'Utility'. Sanitized to a valid C# identifier."),
71
+ directory: z
72
+ .string()
73
+ .optional()
74
+ .describe("Subdirectory under the project root for the .cs file. Defaults to 'Code'."),
75
+ evaluateInterval: z
76
+ .number()
77
+ .optional()
78
+ .describe("Seconds between score evaluations (the active action still Ticks every frame). Defaults to 0.25."),
79
+ hysteresisBonus: z
80
+ .number()
81
+ .optional()
82
+ .describe("Score bonus the current action gets during evaluation — stickiness that prevents flip-flopping between near-tied actions. Defaults to 0.15."),
83
+ moveSpeed: z
84
+ .number()
85
+ .optional()
86
+ .describe("Example WanderAction walk speed in world units/s. Defaults to 80."),
87
+ wanderRadius: z
88
+ .number()
89
+ .optional()
90
+ .describe("Example WanderAction roam radius around its start position. Defaults to 300."),
91
+ networked: z
92
+ .boolean()
93
+ .optional()
94
+ .describe("true (default): host-authoritative brain (IsProxy guard) + [Sync(FromHost)] CurrentActionName. false: local build for solo iteration."),
95
+ targetId: z
96
+ .string()
97
+ .optional()
98
+ .describe("GUID of a GameObject to attach the BRAIN to (actions must be added separately; only attaches if the type is already in the TypeLibrary — hotload first)."),
99
+ }, async (params) => {
100
+ const res = await bridge.send("create_utility_ai", params);
101
+ if (!res.success) {
102
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
103
+ }
104
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
105
+ });
106
+ // ── create_npc_schedule_brain ─────────────────────────────────────
107
+ server.tool("create_npc_schedule_brain", "Generate a daily-routine NPC brain: a [Property] list of schedule entries (startHour/endHour 0..24, taskName, target = named scene GameObject or fixed position), the hour read from any create_day_night_clock component (capability match: a float TimeOfDay property, same GameObject first then scene-wide) with an HONEST fallback to its own internal clock when none exists (check the generated UsingClockComponent bool), walking the NPC to the active entry's target and idling outside the schedule, plus a static OnTaskChanged(brain, taskName) event and [Sync(FromHost)] CurrentTask. Entries with endHour < startHour wrap past midnight. Returns {created, path, className, tasks[], propertyNames[], note}. Next: trigger_hotload + get_compile_errors, attach (targetId or add_component_with_properties), create the named target GameObjects (e.g. 'WorkSpot'), pair with create_day_night_clock for shared time, verify via get_runtime_property CurrentTask in play mode. Limits: default movement is a direct transform walk (walks through walls) — pass useNavMeshAgent:true for pathfinding (then bake_navmesh is REQUIRED); a clock with a different shape (e.g. 0..1 DayProgress) will NOT bind; networked default true won't tick in a no-session solo playtest (networked:false to iterate). Refused during play mode; refuses to overwrite an existing file.", {
108
+ name: z
109
+ .string()
110
+ .optional()
111
+ .describe("Class/file name. Defaults to 'NpcScheduleBrain'. Sanitized to a valid C# identifier."),
112
+ directory: z
113
+ .string()
114
+ .optional()
115
+ .describe("Subdirectory under the project root for the .cs file. Defaults to 'Code'."),
116
+ schedule: z
117
+ .array(z.object({
118
+ startHour: z.number().describe("Entry start hour, 0..24 (inclusive)."),
119
+ endHour: z.number().describe("Entry end hour, 0..24 (exclusive). Smaller than startHour = wraps past midnight (e.g. 22 -> 6)."),
120
+ taskName: z.string().describe("Task label, e.g. 'Work' — surfaced via CurrentTask + OnTaskChanged."),
121
+ targetName: z.string().optional().describe("Named scene GameObject to walk to (case-insensitive; wins over targetPosition). Missing name = NPC idles."),
122
+ targetPosition: Vec3.optional().describe("Fixed world position to walk to, used when targetName is empty."),
123
+ }))
124
+ .optional()
125
+ .describe("Schedule entries baked as inspector-editable defaults. Defaults to Work 8-17 @ 'WorkSpot', Relax 17-22 @ 'HomeSpot' (idles/sleeps otherwise)."),
126
+ moveSpeed: z
127
+ .number()
128
+ .optional()
129
+ .describe("Walk speed in world units/s. Defaults to 100."),
130
+ arriveDistance: z
131
+ .number()
132
+ .optional()
133
+ .describe("Distance at which the NPC counts as arrived and idles at the spot. Defaults to 32."),
134
+ useNavMeshAgent: z
135
+ .boolean()
136
+ .optional()
137
+ .describe("true: move via NavMeshAgent.MoveTo (real pathfinding — REQUIRES bake_navmesh or the NPC won't move). Defaults to false (direct transform walk, no navmesh needed, walks through walls)."),
138
+ fallbackDayLengthSeconds: z
139
+ .number()
140
+ .optional()
141
+ .describe("Internal fallback clock only: real seconds per 24 in-game hours when NO TimeOfDay clock component exists. Defaults to 600."),
142
+ fallbackStartHour: z
143
+ .number()
144
+ .optional()
145
+ .describe("Internal fallback clock only: starting hour 0..24. Defaults to 8."),
146
+ networked: z
147
+ .boolean()
148
+ .optional()
149
+ .describe("true (default): host-authoritative (IsProxy guard) + [Sync(FromHost)] CurrentTask. false: local build for solo iteration."),
150
+ targetId: z
151
+ .string()
152
+ .optional()
153
+ .describe("GUID of the NPC GameObject to attach to (only attaches if the type is already in the TypeLibrary — hotload first)."),
154
+ }, async (params) => {
155
+ const res = await bridge.send("create_npc_schedule_brain", params);
156
+ if (!res.success) {
157
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
158
+ }
159
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
160
+ });
161
+ // ── create_event_bus ──────────────────────────────────────────────
162
+ server.tool("create_event_bus", "Generate a typed LOCAL pub/sub event bus: a pure STATIC class (NOT a Component — nothing to place in the scene) with Subscribe<T>(owner, Action<T>), Unsubscribe(owner) (removes all of that owner's handlers across every event type), Publish<T>(evt) (synchronous, exact-type-T subscribers only, snapshot-iterated so handlers may subscribe/unsubscribe mid-publish), Count<T>() and Clear(), keyed by a plain Dictionary<Type, List<(object, Delegate)>> — plus a tiny example event record ({name}Ping). Decouples game systems: the quest system publishes 'EnemyDied', UI and achievements subscribe, neither knows the other. Returns {created, path, className, exampleEvent, api[], note}. Next: trigger_hotload + get_compile_errors, then Subscribe in components' OnStart and — REQUIRED — Unsubscribe(this) in OnDestroy: handler lists hold PLAIN references (no weak refs), so a component that never unsubscribes leaks itself for the scene's life; call Clear() on scene teardown. Limits: LOCAL only — Publish reaches the calling machine's subscribers, NOT other clients; for networked events pair with [Rpc.Broadcast]/[Rpc.Host] methods that Publish on arrival. No base-type dispatch (Publish<Base> won't reach Subscribe<Derived>). Refuses to overwrite an existing file; refused during play mode.", {
163
+ name: z
164
+ .string()
165
+ .optional()
166
+ .describe("Static class/file name. Defaults to 'EventBus'. The example event record is named {name}Ping. Sanitized to a valid C# identifier."),
167
+ directory: z
168
+ .string()
169
+ .optional()
170
+ .describe("Subdirectory under the project root for the .cs file. Defaults to 'Code'."),
171
+ }, async (params) => {
172
+ const res = await bridge.send("create_event_bus", params);
173
+ if (!res.success) {
174
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
175
+ }
176
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
177
+ });
178
+ // ── add_tts_voice ─────────────────────────────────────────────────
179
+ server.tool("add_tts_voice", "Generate a text-to-speech speaker component over Sandbox.Speech.Synthesizer (the OS speech engine — dynamic NPC dialog with zero recorded VO): call <class>.Say(\"text\") from game code and it builds a Synthesizer (TrySetVoice by exact VoiceName, else gender/age hint, else OS default) -> WithText -> WithRate -> Play(), returning a tracked SoundHandle — positional 3D parented to the speaker (default) or flat 2D, with stop-previous-on-say interruption, IsSpeaking, StopSpeaking(), and LogVoices() to enumerate installed OS voices. Returns {created, path, className, propertyNames[], note}. Next: trigger_hotload + get_compile_errors, attach (targetId or add_component_with_properties), then Say from game code — LOCAL audio only, wrap the Say call in an [Rpc.Broadcast] handler for everyone to hear. Limits & honesty: the editor cannot playtest audio, so RUNTIME speech is UNVERIFIED (the API surface compiles — verify with your ears in play mode); voices are machine/OS-specific and TrySetVoice is best-effort; LIPSYNC IS NOT AUTO-WIRED — s&box's Sandbox.LipSync component consumes a BaseSoundComponent, not the raw SoundHandle TTS produces, and Synthesizer.OnVisemeReached's delegate arg types can't be confirmed via reflection; enableVisemeData:true enables Handle.LipSync.Visemes for your own mouth-drive code (runtime-unverified). Refuses to overwrite an existing file; refused during play mode.", {
180
+ name: z
181
+ .string()
182
+ .optional()
183
+ .describe("Class/file name. Defaults to 'TtsSpeaker'. Sanitized to a valid C# identifier."),
184
+ directory: z
185
+ .string()
186
+ .optional()
187
+ .describe("Subdirectory under the project root for the .cs file. Defaults to 'Code'."),
188
+ voiceName: z
189
+ .string()
190
+ .optional()
191
+ .describe("Exact installed OS voice name (machine-specific — the generated LogVoices() lists them at runtime). Empty = use voiceGender/voiceAge, or the OS default."),
192
+ voiceGender: z
193
+ .string()
194
+ .optional()
195
+ .describe("Voice gender hint used only when voiceName is empty (e.g. 'Female', 'Male'). Must be paired with voiceAge. Passed through unvalidated."),
196
+ voiceAge: z
197
+ .string()
198
+ .optional()
199
+ .describe("Voice age hint paired with voiceGender (e.g. 'Adult', 'Child', 'Senior'). Passed through unvalidated."),
200
+ rate: z
201
+ .number()
202
+ .optional()
203
+ .describe("Speaking rate offset (integer): negative = slower, positive = faster. Defaults to 0 (normal)."),
204
+ volume: z
205
+ .number()
206
+ .optional()
207
+ .describe("Playback volume for spoken lines. Defaults to 1."),
208
+ positional: z
209
+ .boolean()
210
+ .optional()
211
+ .describe("true (default): 3D sound parented to the speaker GameObject (follows it). false: flat 2D voice on the listener (narrator/UI style)."),
212
+ stopPreviousOnSay: z
213
+ .boolean()
214
+ .optional()
215
+ .describe("true (default): a new Say() fades out the still-playing previous line. false: lines overlap."),
216
+ stopFadeSeconds: z
217
+ .number()
218
+ .optional()
219
+ .describe("Fade-out duration used when interrupting/stopping a line. Defaults to 0.1."),
220
+ enableVisemeData: z
221
+ .boolean()
222
+ .optional()
223
+ .describe("true: sets Handle.LipSync.Enabled on each played line so custom mouth-drive code can read Handle.LipSync.Visemes. Runtime behavior unverified (editor can't playtest audio). Defaults to false."),
224
+ targetId: z
225
+ .string()
226
+ .optional()
227
+ .describe("GUID of the speaker GameObject to attach to (only attaches if the type is already in the TypeLibrary — hotload first)."),
228
+ }, async (params) => {
229
+ const res = await bridge.send("add_tts_voice", params);
230
+ if (!res.success) {
231
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
232
+ }
233
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
234
+ });
235
+ }
236
+ //# sourceMappingURL=aisystems.js.map
@@ -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