sbox-mcp-server 1.17.1 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -4
- package/dist/index.js +10 -0
- package/dist/tools/characters.js +24 -0
- package/dist/tools/director.d.ts +14 -0
- package/dist/tools/director.js +48 -0
- package/dist/tools/gamefeel.d.ts +14 -0
- package/dist/tools/gamefeel.js +113 -0
- package/dist/tools/roundstate.d.ts +28 -0
- package/dist/tools/roundstate.js +67 -0
- package/dist/tools/saveslots.d.ts +15 -0
- package/dist/tools/saveslots.js +45 -0
- package/dist/tools/stations.d.ts +15 -0
- package/dist/tools/stations.js +47 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# sbox-mcp-server
|
|
2
2
|
|
|
3
|
-
MCP Server for the s&box game engine. Lets Claude Code build s&box games through conversation — **
|
|
3
|
+
MCP Server for the s&box game engine. Lets Claude Code build s&box games through conversation — **200+ tools** for scenes, scripts, GameObjects, components, assets, materials, audio, physics, UI, networking, publishing, world-gen, lighting & atmosphere, characters, lipsync, scene layout, navmesh & spatial queries, particles, animation, NPC brains, playable-game scaffolds, gameplay playtesting, networking & scene inspection/lint, save & services queries, self-diagnosis, console/C# execution, live docs search, and type discovery.
|
|
4
4
|
|
|
5
5
|
## Fastest install — the Claude Code plugin
|
|
6
6
|
|
|
@@ -54,9 +54,9 @@ Claude Code → (stdio) → sbox-mcp-server → (file IPC) → bridge addon →
|
|
|
54
54
|
|
|
55
55
|
Communication uses file-based IPC through `%TEMP%/sbox-bridge-ipc/`. The MCP server writes request JSON files, the bridge addon (running inside s&box) polls and processes on the main editor thread, then writes response files back. WebSocket is not used — s&box's sandboxed C# environment blocks `System.Net`.
|
|
56
56
|
|
|
57
|
-
## Tools
|
|
57
|
+
## Tools
|
|
58
58
|
|
|
59
|
-
`get_bridge_status` reports the `handlerCount` — that's the C# handlers compiled inside the editor.
|
|
59
|
+
`get_bridge_status` reports the `handlerCount` — that's the C# handlers compiled inside the editor. A handful of tools run **MCP-server-side** and need no editor handler (`read_log`, `get_compile_errors`, `execute_csharp`, `search_docs`, `get_doc_page`, `list_doc_categories`, `run_self_test`, `screenshot_orbit`). They read the log / hotload-eval / fetch docs / orchestrate other tools directly, so most keep working even when the editor has crashed or stalled.
|
|
60
60
|
|
|
61
61
|
| Category | Tools |
|
|
62
62
|
|----------|-------|
|
|
@@ -100,6 +100,16 @@ Communication uses file-based IPC through `%TEMP%/sbox-bridge-ipc/`. The MCP ser
|
|
|
100
100
|
| **Object utilities** *(v1.5.0)* | remove_component, get_tags |
|
|
101
101
|
| **Docs search** *(v1.5.0, MCP-server-side)* | search_docs, get_doc_page, list_doc_categories — official `Facepunch/sbox-docs` |
|
|
102
102
|
| **Inspection & validation** *(v1.9.0)* | inspect_networked_object (per-object `Network.*` + every component's `[Sync]` fields/values), networking_lint (static scan for `[Sync]`/RPC footguns), scene_validate (no-camera / stray root Rigidbody / trigger-vs-trace), save_inspect (list/read/diff `FileSystem.Data` saves), services_query (`Sandbox.Services` stats + leaderboards), simulate_input (drive named input actions in play mode) |
|
|
103
|
+
| **Editor & self-test** *(v1.5.1–v1.7)* | restart_editor (self-restart + reconnect — closes the C#-edit loop), list_libraries, recompile_asset, run_self_test (8-check end-to-end health gate), capture_view (**renders the RUNNING play scene** from any camera — the play-mode eyes) |
|
|
104
|
+
| **Animation & verification** *(v1.6.0)* | list_animations, play_animation, set_animgraph_param, get_bounds, screenshot_orbit (multi-angle orbit shots of any object) |
|
|
105
|
+
| **Call & input** *(v1.10.0)* | invoke_method (call a component method **with args**), ensure_input_action (add a `.sbproj` input action), drive_player / drive_player_status (play-mode input driver) |
|
|
106
|
+
| **Gameplay scaffolds** *(v1.7–v1.13)* | set_component_reference, add_component_to_new_object, create_objective_system, create_health_system, create_pickup, create_trigger_zone, create_economy_wallet, create_round_phase_machine, create_day_night_clock, create_interactable, create_weighted_loot_table, create_save_system, create_leaderboard_panel, create_inventory, create_stat_modifier_system, create_placement_mode |
|
|
107
|
+
| **NPC brains** *(v1.7.0)* | create_npc_brain (FSM: Idle/Patrol/Wander/Chase/Search/Flee/Ambush + FOV/LOS/hearing), place_patrol_route, assign_patrol_route, create_npc_spawner, simulate_npc_perception |
|
|
108
|
+
| **Lints & asset utils** *(v1.12.0)* | sandbox_lint (whitelist pre-compile scan), razor_lint (Razor/SCSS transpiler footguns), copy_asset_with_dependencies (asset + full dependency closure) |
|
|
109
|
+
| **Meta & debug-draw** *(v1.14–v1.15)* | set_time_scale (pause/slow-mo/fast-forward), get_profiler_stats (FPS/frame/GPU/allocations), debug_draw_line/ray/box/sphere, debug_clear |
|
|
110
|
+
| **Playtest harness** *(v1.17)* | playtest (scripted gameplay loop in PLAY MODE with **in-frame assertions** — move/look/action/jump/set/wait/capture/assert), playtest_status |
|
|
111
|
+
| **Lipsync** *(v1.18.0)* | add_lipsync — wires `Sandbox.LipSync` (new in the 2026-07-01 engine update) to a citizen/model: `SkinnedModelRenderer` + a `SoundPointComponent` bound to a `.sound` path, facial morphs animating while the sound plays |
|
|
112
|
+
| **Gameplay scaffolds** *(v1.18.0)* | create_round_state_machine (multi-state round manager + abstract `RoundState` lifecycle base, host-authoritative with late-joiner reconcile), add_interaction_station (one-occupant `IPressable` station, host-routed claims, reservation grace window, level gate), create_event_director (weighted-interval pacing/AI director, dedupe, `MaxActive` cap, timed self-destruct), create_save_slots (multi-slot save manager, versioned, optional GUID scene reconciliation — companion to create_save_system) |
|
|
103
113
|
|
|
104
114
|
## Working with Claude effectively
|
|
105
115
|
|
|
@@ -127,6 +137,6 @@ The companion plugin's `sbox-build-feature` skill encodes this workflow plus the
|
|
|
127
137
|
|
|
128
138
|
## License
|
|
129
139
|
|
|
130
|
-
**
|
|
140
|
+
**Source-available (no redistribution)** — see [LICENSE](../LICENSE) and [NOTICE](../NOTICE) for details. You may use and locally modify the bridge to build your own games, but you may not redistribute, fork, repackage, or re-host it. The "s&box Claude Bridge" / "sboxskins.gg" name and branding are trademarks and may not be reused.
|
|
131
141
|
|
|
132
142
|
Copyright (c) 2026 [sboxskins.gg](https://sboxskins.gg)
|
package/dist/index.js
CHANGED
|
@@ -48,6 +48,11 @@ import { registerInputTools } from "./tools/inputs.js";
|
|
|
48
48
|
import { registerDebugVizTools } from "./tools/debugviz.js";
|
|
49
49
|
import { registerDebugDrawTools } from "./tools/debugdraw.js";
|
|
50
50
|
import { registerPlaytestTools } from "./tools/playtest.js";
|
|
51
|
+
import { registerRoundStateTools } from "./tools/roundstate.js";
|
|
52
|
+
import { registerStationTools } from "./tools/stations.js";
|
|
53
|
+
import { registerDirectorTools } from "./tools/director.js";
|
|
54
|
+
import { registerSaveSlotsTools } from "./tools/saveslots.js";
|
|
55
|
+
import { registerGameFeelTools } from "./tools/gamefeel.js";
|
|
51
56
|
// ── CLI flags ──────────────────────────────────────────────────────
|
|
52
57
|
const args = process.argv.slice(2);
|
|
53
58
|
/** Read the package version from package.json, or return "unknown" on failure. */
|
|
@@ -199,6 +204,11 @@ registerInputTools(server, bridge);
|
|
|
199
204
|
registerDebugVizTools(server, bridge);
|
|
200
205
|
registerDebugDrawTools(server, bridge);
|
|
201
206
|
registerPlaytestTools(server, bridge);
|
|
207
|
+
registerRoundStateTools(server, bridge);
|
|
208
|
+
registerStationTools(server, bridge);
|
|
209
|
+
registerDirectorTools(server, bridge);
|
|
210
|
+
registerSaveSlotsTools(server, bridge);
|
|
211
|
+
registerGameFeelTools(server, bridge);
|
|
202
212
|
/** Start the MCP server on stdio and attempt initial Bridge connection. */
|
|
203
213
|
async function main() {
|
|
204
214
|
const transport = new StdioServerTransport();
|
package/dist/tools/characters.js
CHANGED
|
@@ -268,5 +268,29 @@ export function registerCharacterTools(server, bridge) {
|
|
|
268
268
|
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
269
269
|
};
|
|
270
270
|
});
|
|
271
|
+
// ── add_lipsync ────────────────────────────────────────────────────
|
|
272
|
+
server.tool("add_lipsync", "Add + wire s&box's LipSync component (new in the 2026-07 engine update): drives a SkinnedModelRenderer's facial morphs (visemes) from a playing sound. Wires the renderer on the target GameObject (or rendererId) and, if soundEvent is given, a SoundPointComponent bound to that .sound asset (see list_sounds / create_sound_event). Morphs animate at RUNTIME while the sound plays — verify in play mode with capture_view, not the static editor pose.", {
|
|
273
|
+
id: z.string().describe("GUID of the GameObject to add LipSync to (e.g. a spawn_citizen)"),
|
|
274
|
+
rendererId: z
|
|
275
|
+
.string()
|
|
276
|
+
.optional()
|
|
277
|
+
.describe("GUID of a different GameObject holding the SkinnedModelRenderer (default: same as id)"),
|
|
278
|
+
soundEvent: z
|
|
279
|
+
.string()
|
|
280
|
+
.optional()
|
|
281
|
+
.describe("Path to a .sound event to speak, e.g. 'sounds/dialogue/hello.sound' — creates/reuses a SoundPointComponent. Omit to wire an existing sound component on the GameObject (or wire audio later)."),
|
|
282
|
+
playOnStart: z.boolean().optional().describe("Play the sound on scene start (default true; only with soundEvent)"),
|
|
283
|
+
volume: z.number().optional().describe("SoundPointComponent volume (only with soundEvent)"),
|
|
284
|
+
morphScale: z.number().optional().describe("LipSync.MorphScale — exaggerate/dampen mouth movement (engine default when omitted)"),
|
|
285
|
+
morphSmoothTime: z.number().optional().describe("LipSync.MorphSmoothTime — morph smoothing seconds (engine default when omitted)"),
|
|
286
|
+
}, async (params) => {
|
|
287
|
+
const res = await bridge.send("add_lipsync", params);
|
|
288
|
+
if (!res.success) {
|
|
289
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
293
|
+
};
|
|
294
|
+
});
|
|
271
295
|
}
|
|
272
296
|
//# sourceMappingURL=characters.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BridgeClient } from "../transport/bridge-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* AI-director scaffold — generalized L4D-style event pacing.
|
|
5
|
+
*
|
|
6
|
+
* - create_event_director generate a host-authoritative "AI director" component that
|
|
7
|
+
* rolls on an interval, weighted-picks an event prefab,
|
|
8
|
+
* deduplicates against the active set, spawns it, and gives
|
|
9
|
+
* each spawned event a timed self-destruct.
|
|
10
|
+
*
|
|
11
|
+
* Code-gen + scene-mutating; refused during play mode by the bridge dispatch.
|
|
12
|
+
*/
|
|
13
|
+
export declare function registerDirectorTools(server: McpServer, bridge: BridgeClient): void;
|
|
14
|
+
//# sourceMappingURL=director.d.ts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* AI-director scaffold — generalized L4D-style event pacing.
|
|
4
|
+
*
|
|
5
|
+
* - create_event_director generate a host-authoritative "AI director" component that
|
|
6
|
+
* rolls on an interval, weighted-picks an event prefab,
|
|
7
|
+
* deduplicates against the active set, spawns it, and gives
|
|
8
|
+
* each spawned event a timed self-destruct.
|
|
9
|
+
*
|
|
10
|
+
* Code-gen + scene-mutating; refused during play mode by the bridge dispatch.
|
|
11
|
+
*/
|
|
12
|
+
export function registerDirectorTools(server, bridge) {
|
|
13
|
+
// ── create_event_director ─────────────────────────────────────────
|
|
14
|
+
server.tool("create_event_director", "Generate a generalized L4D-style AI/pacing director component (host-authoritative). On a configurable interval the host rolls a weighted pick over a [Property] List<GameObject> EventPrefabs (with a parallel List<float> Weights), skips any event already active (dedupe) and anything past a MaxActive concurrency cap, clones the chosen prefab, NetworkSpawns it, and attaches a generated {name}TimedEvent companion so each spawned event self-destructs after EventLifetime seconds. Great for ambient events, waves, and world events. Single-player safe (IsProxy guard; NetworkSpawn falls back to a local clone). Fill EventPrefabs/Weights in the inspector or via the bridge after a hotload; edit the RollInterval() stub to make pacing adaptive (player-count/inactivity/time-pressure factors) per the ai-director cookbook. Optionally attached to an existing GameObject by GUID (after a hotload). NOTE: emits ONE .cs file containing two classes ({name} + {name}TimedEvent); the type only resolves after trigger_hotload.", {
|
|
15
|
+
name: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("Class name for the director (a {name}TimedEvent companion is generated alongside it). Defaults to 'EventDirector'"),
|
|
19
|
+
path: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
|
|
23
|
+
intervalSeconds: z
|
|
24
|
+
.number()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Base seconds between director rolls. Defaults to 30 (clamped to >= 0.1)"),
|
|
27
|
+
maxActive: z
|
|
28
|
+
.number()
|
|
29
|
+
.int()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("Maximum number of concurrently-live events. Defaults to 3 (clamped to >= 1)"),
|
|
32
|
+
eventLifetime: z
|
|
33
|
+
.number()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("Seconds before each spawned event self-destructs. Defaults to 60 (clamped to >= 0.1)"),
|
|
36
|
+
targetId: z
|
|
37
|
+
.string()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe("GUID of an existing GameObject to attach the director to (only attaches if the type is already loaded — hotload first)"),
|
|
40
|
+
}, async (params) => {
|
|
41
|
+
const res = await bridge.send("create_event_director", params);
|
|
42
|
+
if (!res.success) {
|
|
43
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
44
|
+
}
|
|
45
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=director.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BridgeClient } from "../transport/bridge-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Game Feel pack — three "juice" scaffolds (v1.19.0):
|
|
5
|
+
*
|
|
6
|
+
* - create_camera_shake trauma-based Perlin camera shake
|
|
7
|
+
* - add_flicker_light flicker/pulse animator for an existing light
|
|
8
|
+
* - create_floating_combat_text rising/fading damage-number popups
|
|
9
|
+
*
|
|
10
|
+
* All generate a clean, self-contained .cs (LOCAL/visual-only — no [Sync]);
|
|
11
|
+
* file/scene-mutating, refused during play mode by the bridge dispatch.
|
|
12
|
+
*/
|
|
13
|
+
export declare function registerGameFeelTools(server: McpServer, bridge: BridgeClient): void;
|
|
14
|
+
//# sourceMappingURL=gamefeel.d.ts.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Game Feel pack — three "juice" scaffolds (v1.19.0):
|
|
4
|
+
*
|
|
5
|
+
* - create_camera_shake trauma-based Perlin camera shake
|
|
6
|
+
* - add_flicker_light flicker/pulse animator for an existing light
|
|
7
|
+
* - create_floating_combat_text rising/fading damage-number popups
|
|
8
|
+
*
|
|
9
|
+
* All generate a clean, self-contained .cs (LOCAL/visual-only — no [Sync]);
|
|
10
|
+
* file/scene-mutating, refused during play mode by the bridge dispatch.
|
|
11
|
+
*/
|
|
12
|
+
export function registerGameFeelTools(server, bridge) {
|
|
13
|
+
// ── create_camera_shake ───────────────────────────────────────────
|
|
14
|
+
server.tool("create_camera_shake", "Generate a trauma-based camera shake component (the standard game-feel model: events add Trauma 0..1, shake magnitude = Trauma², smooth Perlin offsets — not white-noise jitter — and Trauma decays every frame, so explosions slam and footsteps barely register). Attach the generated component to the CAMERA GameObject; fire from any game code via the static <Name>.Shake(0.4f). Applies in OnPreRender AFTER controllers position the camera, with an un-apply guard so it neither fights a controller-driven camera nor accumulates on a static one, and restores the camera when trauma hits zero. LOCAL-only (no [Sync]) — call Shake inside an [Rpc.Broadcast] handler if every client should feel it. Optionally attached to an existing GameObject by GUID (after a hotload).", {
|
|
15
|
+
name: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("Class name for the generated component. Defaults to 'CameraShake'"),
|
|
19
|
+
directory: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
|
|
23
|
+
maxOffset: z
|
|
24
|
+
.number()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Positional shake at full trauma, in world units. Defaults to 6"),
|
|
27
|
+
maxAngle: z
|
|
28
|
+
.number()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Rotational shake at full trauma, in degrees (applied to pitch/yaw/roll). Defaults to 4"),
|
|
31
|
+
frequency: z
|
|
32
|
+
.number()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Noise speed — higher = violent rattle, lower = drunken sway. Defaults to 10"),
|
|
35
|
+
decayPerSecond: z
|
|
36
|
+
.number()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("How much trauma drains per second. Defaults to 1.5"),
|
|
39
|
+
targetId: z
|
|
40
|
+
.string()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("GUID of the camera GameObject to attach to (only attaches if the type is already loaded — hotload first)"),
|
|
43
|
+
}, async (params) => {
|
|
44
|
+
const res = await bridge.send("create_camera_shake", params);
|
|
45
|
+
if (!res.success) {
|
|
46
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
47
|
+
}
|
|
48
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
49
|
+
});
|
|
50
|
+
// ── add_flicker_light ─────────────────────────────────────────────
|
|
51
|
+
server.tool("add_flicker_light", "Generate a light-flicker animator and optionally attach it to an existing light GameObject by GUID. Five presets: Candle (soft organic sway), Fluorescent (mostly steady with random dips), Faulty (hard on/off cuts), Pulse (slow sine breathing), Lightning (dim baseline with rare bright flashes). Modulates the sibling Light component's LightColor around the color it found on enable (works on PointLight / SpotLight / DirectionalLight) and restores it exactly on disable; intensity 0..1 sets flicker depth, speed scales the whole pattern. The single biggest atmosphere win per call for horror/night scenes — pairs with apply_atmosphere. LOCAL/visual-only.", {
|
|
52
|
+
name: z
|
|
53
|
+
.string()
|
|
54
|
+
.optional()
|
|
55
|
+
.describe("Class name for the generated component. Defaults to 'FlickerLight'"),
|
|
56
|
+
directory: z
|
|
57
|
+
.string()
|
|
58
|
+
.optional()
|
|
59
|
+
.describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
|
|
60
|
+
style: z
|
|
61
|
+
.enum(["Candle", "Fluorescent", "Faulty", "Pulse", "Lightning"])
|
|
62
|
+
.optional()
|
|
63
|
+
.describe("Default flicker preset baked into the component (editable per-instance in the inspector). Defaults to 'Candle'"),
|
|
64
|
+
intensity: z
|
|
65
|
+
.number()
|
|
66
|
+
.optional()
|
|
67
|
+
.describe("Flicker depth 0..1: 0 = steady, 1 = full blackouts / double-bright flashes. Defaults to 0.5"),
|
|
68
|
+
speed: z
|
|
69
|
+
.number()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe("Speed multiplier for the whole pattern. Defaults to 1"),
|
|
72
|
+
lightId: z
|
|
73
|
+
.string()
|
|
74
|
+
.optional()
|
|
75
|
+
.describe("GUID of a GameObject holding a light component to attach to (only attaches if the type is already loaded — hotload first)"),
|
|
76
|
+
}, async (params) => {
|
|
77
|
+
const res = await bridge.send("add_flicker_light", params);
|
|
78
|
+
if (!res.success) {
|
|
79
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
80
|
+
}
|
|
81
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
82
|
+
});
|
|
83
|
+
// ── create_floating_combat_text ───────────────────────────────────
|
|
84
|
+
server.tool("create_floating_combat_text", "Generate a floating combat text component — rising, fading, camera-billboarded world-space popups for damage numbers, '+10 gold', pickup names. TextRenderer-based: no Razor, no WorldPanel, zero UI setup. Nothing to place in the scene — the generated class carries a static factory: <Name>.Spawn(position, \"-25\", Color.Red[, sizeMultiplier]) spawns a popup that rises at RiseSpeed, fades over Lifetime, and destroys itself. Pairs with create_health_system (spawn from the damage path so every hit prints its number). LOCAL-only — spawn inside an [Rpc.Broadcast] handler if every client should see it.", {
|
|
85
|
+
name: z
|
|
86
|
+
.string()
|
|
87
|
+
.optional()
|
|
88
|
+
.describe("Class name for the generated component. Defaults to 'FloatingCombatText'"),
|
|
89
|
+
directory: z
|
|
90
|
+
.string()
|
|
91
|
+
.optional()
|
|
92
|
+
.describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
|
|
93
|
+
riseSpeed: z
|
|
94
|
+
.number()
|
|
95
|
+
.optional()
|
|
96
|
+
.describe("World units the popup rises per second. Defaults to 48"),
|
|
97
|
+
lifetime: z
|
|
98
|
+
.number()
|
|
99
|
+
.optional()
|
|
100
|
+
.describe("Seconds until the popup is fully faded and destroyed. Defaults to 1.1"),
|
|
101
|
+
fontSize: z
|
|
102
|
+
.number()
|
|
103
|
+
.optional()
|
|
104
|
+
.describe("Base font size baked into Spawn() (the optional Spawn size argument multiplies it). Defaults to 24"),
|
|
105
|
+
}, async (params) => {
|
|
106
|
+
const res = await bridge.send("create_floating_combat_text", params);
|
|
107
|
+
if (!res.success) {
|
|
108
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
109
|
+
}
|
|
110
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=gamefeel.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BridgeClient } from "../transport/bridge-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* create_round_state_machine — the COMPLEX, multi-state round machine scaffold.
|
|
5
|
+
*
|
|
6
|
+
* The sibling of create_round_phase_machine. Both are host-authoritative round
|
|
7
|
+
* flow, but they scaffold different shapes:
|
|
8
|
+
*
|
|
9
|
+
* create_round_phase_machine → ONE sealed component with a [Sync] enum cycled
|
|
10
|
+
* on a shared per-phase timer. Reach for it for 3–5 LIGHT phases whose
|
|
11
|
+
* only per-phase logic is "how long" (round/day-night/match phases).
|
|
12
|
+
*
|
|
13
|
+
* create_round_state_machine → a RoundManager singleton + an abstract
|
|
14
|
+
* RoundState base (Begin/Tick/OnTimeUp/Finish lifecycle + per-state
|
|
15
|
+
* [Sync] TimeUntil timer) + one sealed stub class PER state. Reach for it
|
|
16
|
+
* when each phase deserves its OWN file/behaviour: entry side-effects,
|
|
17
|
+
* per-frame Tick logic, a CanEnter() skip condition, copy-data-out-on-exit.
|
|
18
|
+
* This is the "state-as-component" pattern from the round-match cookbook.
|
|
19
|
+
*
|
|
20
|
+
* The generated manager auto-attaches the state components on start (so a
|
|
21
|
+
* non-coder only places the manager), index-wraps when advancing, skips states
|
|
22
|
+
* whose CanEnter() returns false, and announces transitions via a static
|
|
23
|
+
* OnStateChanged event mirrored by an [Rpc.Broadcast] so proxies converge
|
|
24
|
+
* instantly (the [Sync] index is the durable late-joiner reconcile path).
|
|
25
|
+
* Single-player safe. Scene/file-mutating; refused during play mode.
|
|
26
|
+
*/
|
|
27
|
+
export declare function registerRoundStateTools(server: McpServer, bridge: BridgeClient): void;
|
|
28
|
+
//# sourceMappingURL=roundstate.d.ts.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* create_round_state_machine — the COMPLEX, multi-state round machine scaffold.
|
|
4
|
+
*
|
|
5
|
+
* The sibling of create_round_phase_machine. Both are host-authoritative round
|
|
6
|
+
* flow, but they scaffold different shapes:
|
|
7
|
+
*
|
|
8
|
+
* create_round_phase_machine → ONE sealed component with a [Sync] enum cycled
|
|
9
|
+
* on a shared per-phase timer. Reach for it for 3–5 LIGHT phases whose
|
|
10
|
+
* only per-phase logic is "how long" (round/day-night/match phases).
|
|
11
|
+
*
|
|
12
|
+
* create_round_state_machine → a RoundManager singleton + an abstract
|
|
13
|
+
* RoundState base (Begin/Tick/OnTimeUp/Finish lifecycle + per-state
|
|
14
|
+
* [Sync] TimeUntil timer) + one sealed stub class PER state. Reach for it
|
|
15
|
+
* when each phase deserves its OWN file/behaviour: entry side-effects,
|
|
16
|
+
* per-frame Tick logic, a CanEnter() skip condition, copy-data-out-on-exit.
|
|
17
|
+
* This is the "state-as-component" pattern from the round-match cookbook.
|
|
18
|
+
*
|
|
19
|
+
* The generated manager auto-attaches the state components on start (so a
|
|
20
|
+
* non-coder only places the manager), index-wraps when advancing, skips states
|
|
21
|
+
* whose CanEnter() returns false, and announces transitions via a static
|
|
22
|
+
* OnStateChanged event mirrored by an [Rpc.Broadcast] so proxies converge
|
|
23
|
+
* instantly (the [Sync] index is the durable late-joiner reconcile path).
|
|
24
|
+
* Single-player safe. Scene/file-mutating; refused during play mode.
|
|
25
|
+
*/
|
|
26
|
+
export function registerRoundStateTools(server, bridge) {
|
|
27
|
+
server.tool("create_round_state_machine", "Generate a host-authoritative MULTI-STATE round machine (the complex variant of create_round_phase_machine). Produces one .cs file: a RoundManager singleton component + an abstract RoundState base (Begin/Tick/OnTimeUp/Finish lifecycle with a per-state [Sync(SyncFlags.FromHost)] TimeUntil timer) + one sealed stub class per named state. The manager auto-attaches the state components on start (you only place the manager), ticks ONLY the active state on the host, Advance()s on timeout with index-wrap, SKIPS any state whose CanEnter() returns false, and announces every transition via a static OnStateChanged event plus an [Rpc.Broadcast] mirror so the host fires immediately and proxies converge without waiting a snapshot (the [Sync] index reconciles late joiners). Single-player safe. USE THIS (not create_round_phase_machine) when each phase needs its OWN behaviour — entry side-effects, per-frame Tick logic, a skip condition, or copy-data-out-on-exit; use the phase machine for 3–5 light phases that differ only in duration. Optionally attached to an existing GameObject by GUID (after a hotload).", {
|
|
28
|
+
name: z
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("Manager class name. Defaults to 'RoundManager'. The abstract base is derived from it (RoundManager → RoundState)"),
|
|
32
|
+
directory: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("Subdirectory for the .cs file (path override). Defaults to 'Code'"),
|
|
36
|
+
states: z
|
|
37
|
+
.array(z.string())
|
|
38
|
+
.optional()
|
|
39
|
+
.describe('Ordered state names — each becomes a sealed {Name}State stub class. Defaults to ["Waiting","Active","PostRound"]'),
|
|
40
|
+
duration: z
|
|
41
|
+
.number()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("Default seconds each state lasts (each state also gets its own tunable [Property] Duration). 0 = no auto-advance for a state. Defaults to 30"),
|
|
44
|
+
durations: z
|
|
45
|
+
.union([
|
|
46
|
+
z.array(z.number()),
|
|
47
|
+
z.record(z.number()),
|
|
48
|
+
])
|
|
49
|
+
.optional()
|
|
50
|
+
.describe('Optional per-state duration override: an array aligned to `states` ([10,120,8]) OR an object keyed by state name ({"Waiting":10,"Active":120}). Any state not covered falls back to `duration`'),
|
|
51
|
+
loop: z
|
|
52
|
+
.boolean()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe("Loop back to the first state after the last (true) or hold on the last state (false). Defaults to true"),
|
|
55
|
+
targetId: z
|
|
56
|
+
.string()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe("GUID of an existing GameObject to attach the manager to (only if the type is already loaded — trigger_hotload first)"),
|
|
59
|
+
}, async (params) => {
|
|
60
|
+
const res = await bridge.send("create_round_state_machine", params);
|
|
61
|
+
if (!res.success) {
|
|
62
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
63
|
+
}
|
|
64
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=roundstate.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BridgeClient } from "../transport/bridge-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Multi-slot save manager scaffold — the slot-picker sibling of create_save_system.
|
|
5
|
+
*
|
|
6
|
+
* - create_save_slots generate a SaveSlotManager component: list / create /
|
|
7
|
+
* load / save / delete N save slots, each with picker
|
|
8
|
+
* metadata (name + timestamp + playtime), versioned
|
|
9
|
+
* payloads with delete-on-version-mismatch, and an
|
|
10
|
+
* optional GUID scene-object reconciliation on load.
|
|
11
|
+
*
|
|
12
|
+
* File/scene-mutating; refused during play mode by the bridge dispatch.
|
|
13
|
+
*/
|
|
14
|
+
export declare function registerSaveSlotsTools(server: McpServer, bridge: BridgeClient): void;
|
|
15
|
+
//# sourceMappingURL=saveslots.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Multi-slot save manager scaffold — the slot-picker sibling of create_save_system.
|
|
4
|
+
*
|
|
5
|
+
* - create_save_slots generate a SaveSlotManager component: list / create /
|
|
6
|
+
* load / save / delete N save slots, each with picker
|
|
7
|
+
* metadata (name + timestamp + playtime), versioned
|
|
8
|
+
* payloads with delete-on-version-mismatch, and an
|
|
9
|
+
* optional GUID scene-object reconciliation on load.
|
|
10
|
+
*
|
|
11
|
+
* File/scene-mutating; refused during play mode by the bridge dispatch.
|
|
12
|
+
*/
|
|
13
|
+
export function registerSaveSlotsTools(server, bridge) {
|
|
14
|
+
// ── create_save_slots ─────────────────────────────────────────────
|
|
15
|
+
server.tool("create_save_slots", "Generate a multi-slot save MANAGER component (the slot-picker sibling of create_save_system). Use this when the game needs SEVERAL named save slots the player chooses between (New Game / Load Game menu, per-character or per-run saves) — not one silent autosave. Use create_save_system instead when a single implicit save file is enough. Emits one sealed Component that lists / creates / loads / saves / deletes N slots: a lightweight manifest file (saveslots.json) holds per-slot metadata for the picker (Used flag + Name + SavedAtUnix timestamp + PlaytimeSeconds) so listing never loads a heavy payload, and each slot's game state lives in its own saveslot_<i>.json. Versioned SlotData POCO with clamp-on-load Sanitize() and delete-on-version-mismatch; runs only on the owning machine (IsProxy guard). Static OnSlotLoaded / OnSlotSaved / OnSlotDeleted hooks for HUD. Storage stays within the verified FileSystem.Data.ReadJsonOrDefault / WriteJson / DeleteFile surface (index-file pattern, no directory enumeration). Set sceneReconciliation:true to also reconcile scene objects by GameObject.Id on load — records the save marks destroyed are destroyed, survivors repositioned, missing skipped (good for a placeable-world tycoon). Optionally attached to an existing GameObject by GUID (after a hotload).", {
|
|
16
|
+
name: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Class name for the generated component. Defaults to 'SaveSlotManager'"),
|
|
20
|
+
directory: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
|
|
24
|
+
maxSlots: z
|
|
25
|
+
.number()
|
|
26
|
+
.int()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("How many save slots the manager manages (manifest is normalized to exactly this many, indexed 0..N-1). Clamped to 1..100. Defaults to 3"),
|
|
29
|
+
sceneReconciliation: z
|
|
30
|
+
.boolean()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("If true, saved records carry each object's GameObject.Id GUID and load reconciles the live scene against them (destroy the save's destroyed records via Scene.Directory.FindByGuid, reposition survivors, skip missing) — call RecordObject(go) to track a placeable. If false (default), the slot save is a plain payload with no scene reconciliation. Defaults to false"),
|
|
33
|
+
targetId: z
|
|
34
|
+
.string()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("GUID of an existing GameObject to attach the manager to (only attaches if the type is already loaded — hotload first)"),
|
|
37
|
+
}, async (params) => {
|
|
38
|
+
const res = await bridge.send("create_save_slots", params);
|
|
39
|
+
if (!res.success) {
|
|
40
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
41
|
+
}
|
|
42
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=saveslots.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BridgeClient } from "../transport/bridge-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Interaction-station scaffold — a Tier-1 backlog item (mined from 2 games).
|
|
5
|
+
*
|
|
6
|
+
* - add_interaction_station an IPressable "station" prop (crafting bench /
|
|
7
|
+
* shop till / arcade cabinet) with host-authoritative
|
|
8
|
+
* single-occupant occupancy, a reservation grace
|
|
9
|
+
* window, an unlock-level gate, and an overlay-open hook.
|
|
10
|
+
*
|
|
11
|
+
* Generates a clean, self-contained .cs and optionally attaches it to a live
|
|
12
|
+
* GameObject. Scene/file-mutating; refused during play mode by the bridge dispatch.
|
|
13
|
+
*/
|
|
14
|
+
export declare function registerStationTools(server: McpServer, bridge: BridgeClient): void;
|
|
15
|
+
//# sourceMappingURL=stations.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Interaction-station scaffold — a Tier-1 backlog item (mined from 2 games).
|
|
4
|
+
*
|
|
5
|
+
* - add_interaction_station an IPressable "station" prop (crafting bench /
|
|
6
|
+
* shop till / arcade cabinet) with host-authoritative
|
|
7
|
+
* single-occupant occupancy, a reservation grace
|
|
8
|
+
* window, an unlock-level gate, and an overlay-open hook.
|
|
9
|
+
*
|
|
10
|
+
* Generates a clean, self-contained .cs and optionally attaches it to a live
|
|
11
|
+
* GameObject. Scene/file-mutating; refused during play mode by the bridge dispatch.
|
|
12
|
+
*/
|
|
13
|
+
export function registerStationTools(server, bridge) {
|
|
14
|
+
// ── add_interaction_station ───────────────────────────────────────
|
|
15
|
+
server.tool("add_interaction_station", "Generate a Component.IPressable 'station' prop (crafting bench / shop till / arcade cabinet) that ONE user occupies at a time. Occupancy is host-authoritative: the occupant is a [Sync(SyncFlags.FromHost)] Guid (GameObject/Connection aren't [Sync]-able) and Press() routes the claim to the host via an [Rpc.Host] Occupy(). Includes a reservation grace window (the station stays reserved for its last user for graceSeconds after they leave, so a brief walk-away can't jump the queue), an optional unlock-level gate (users below requiredLevel can't use it — wire the static ResolveUserLevel hook to your progression system to activate it), and an overlay-open hook (a static OnStationOpened(GameObject) event to open your UI, plus an opt-in [Rpc.Broadcast] mirror). Single-player safe. Optionally attached to an existing GameObject by GUID (only after a trigger_hotload). Give the prop a Collider so the player's use key can raycast it. Mined from interaction-station patterns across shipped s&box games.", {
|
|
16
|
+
name: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Class name for the generated component. Defaults to 'InteractionStation'"),
|
|
20
|
+
directory: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Subdirectory for the .cs file (path override). Defaults to 'Code'"),
|
|
24
|
+
graceSeconds: z
|
|
25
|
+
.number()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("Seconds the station stays reserved for its last user after they leave, before anyone else can claim it. 0 = no grace window. Defaults to 5"),
|
|
28
|
+
requiredLevel: z
|
|
29
|
+
.number()
|
|
30
|
+
.int()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Unlock-level gate: users below this level can't use the station. 0 = no gate. The gate only bites once you wire the static ResolveUserLevel hook to your progression system. Defaults to 0"),
|
|
33
|
+
targetId: z
|
|
34
|
+
.string()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("GUID of an existing GameObject to attach the station component to (only attaches if the type is already loaded — generate, trigger_hotload, then it places; otherwise add it after the hotload)"),
|
|
37
|
+
}, async (params) => {
|
|
38
|
+
const res = await bridge.send("add_interaction_station", params);
|
|
39
|
+
if (!res.success) {
|
|
40
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=stations.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbox-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"description": "MCP Server for s&box game engine — enables Claude to build games through conversation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -51,5 +51,5 @@
|
|
|
51
51
|
"engines": {
|
|
52
52
|
"node": ">=18.0.0"
|
|
53
53
|
},
|
|
54
|
-
"license": "
|
|
54
|
+
"license": "SEE LICENSE IN LICENSE"
|
|
55
55
|
}
|