sbox-mcp-server 1.19.0 → 1.20.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
@@ -53,6 +53,12 @@ import { registerStationTools } from "./tools/stations.js";
53
53
  import { registerDirectorTools } from "./tools/director.js";
54
54
  import { registerSaveSlotsTools } from "./tools/saveslots.js";
55
55
  import { registerGameFeelTools } from "./tools/gamefeel.js";
56
+ import { registerMovieMakerTools } from "./tools/moviemaker.js";
57
+ import { registerNetPrimitivesTools } from "./tools/netprimitives.js";
58
+ import { registerInteractionPackTools } from "./tools/interactionpack.js";
59
+ import { registerLootEconomyTools } from "./tools/looteconomy.js";
60
+ import { registerUiFeedbackTools } from "./tools/uifeedback.js";
61
+ import { registerCinematicsTools } from "./tools/cinematics.js";
56
62
  // ── CLI flags ──────────────────────────────────────────────────────
57
63
  const args = process.argv.slice(2);
58
64
  /** Read the package version from package.json, or return "unknown" on failure. */
@@ -209,6 +215,12 @@ registerStationTools(server, bridge);
209
215
  registerDirectorTools(server, bridge);
210
216
  registerSaveSlotsTools(server, bridge);
211
217
  registerGameFeelTools(server, bridge);
218
+ registerMovieMakerTools(server, bridge);
219
+ registerNetPrimitivesTools(server, bridge);
220
+ registerInteractionPackTools(server, bridge);
221
+ registerLootEconomyTools(server, bridge);
222
+ registerUiFeedbackTools(server, bridge);
223
+ registerCinematicsTools(server, bridge);
212
224
  /** Start the MCP server on stdio and attempt initial Bridge connection. */
213
225
  async function main() {
214
226
  const transport = new StdioServerTransport();
@@ -0,0 +1,19 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { BridgeClient } from "../transport/bridge-client.js";
3
+ /**
4
+ * Cinematics & Dialogue pack — two engine-proof, no-asset scaffolds (v1.20.0):
5
+ *
6
+ * - create_cutscene_director hand-authored camera-shot cutscene player
7
+ * - create_dialogue_system typewriter NPC/story dialogue (Component + Razor HUD)
8
+ *
9
+ * These are the HAND-AUTHORED cinematic path (zero assets, full C# control). The
10
+ * keyframed/timeline path is the MovieMaker family (add_movie_player / play_movie
11
+ * / stop_movie / list_movies), which wires a Sandbox.MovieMaker MoviePlayer to a
12
+ * .movie clip authored in the editor's Movie Maker dock.
13
+ *
14
+ * All generate self-contained, sandbox-safe, LOCAL/visual-only code; the Razor
15
+ * output is razor_lint-safe by construction. File/scene-mutating, so refused
16
+ * during play mode by the bridge dispatch.
17
+ */
18
+ export declare function registerCinematicsTools(server: McpServer, bridge: BridgeClient): void;
19
+ //# sourceMappingURL=cinematics.d.ts.map
@@ -0,0 +1,73 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Cinematics & Dialogue pack — two engine-proof, no-asset scaffolds (v1.20.0):
4
+ *
5
+ * - create_cutscene_director hand-authored camera-shot cutscene player
6
+ * - create_dialogue_system typewriter NPC/story dialogue (Component + Razor HUD)
7
+ *
8
+ * These are the HAND-AUTHORED cinematic path (zero assets, full C# control). The
9
+ * keyframed/timeline path is the MovieMaker family (add_movie_player / play_movie
10
+ * / stop_movie / list_movies), which wires a Sandbox.MovieMaker MoviePlayer to a
11
+ * .movie clip authored in the editor's Movie Maker dock.
12
+ *
13
+ * All generate self-contained, sandbox-safe, LOCAL/visual-only code; the Razor
14
+ * output is razor_lint-safe by construction. File/scene-mutating, so refused
15
+ * during play mode by the bridge dispatch.
16
+ */
17
+ export function registerCinematicsTools(server, bridge) {
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.", {
20
+ name: z
21
+ .string()
22
+ .optional()
23
+ .describe("Class name for the generated component. Defaults to 'CutsceneDirector'"),
24
+ directory: z
25
+ .string()
26
+ .optional()
27
+ .describe("Subdirectory for the generated files. Defaults to 'Code'"),
28
+ skipAction: z
29
+ .string()
30
+ .optional()
31
+ .describe("Input action that ends the cutscene early (read before input is cleared, so a locked cutscene is still skippable). Sanitized to a safe token. Empty = not skippable. Defaults to 'jump'"),
32
+ lockInput: z
33
+ .boolean()
34
+ .optional()
35
+ .describe("Freeze player input during playback via Input.ClearActions() each frame. Defaults to true"),
36
+ letterbox: z
37
+ .boolean()
38
+ .optional()
39
+ .describe("Also generate a razor_lint-safe letterbox overlay (two black bars, shown while IsCutscenePlaying — host it under a ScreenPanel). Defaults to false"),
40
+ }, async (params) => {
41
+ const res = await bridge.send("create_cutscene_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
+ // ── 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.", {
49
+ name: z
50
+ .string()
51
+ .optional()
52
+ .describe("Class name for the generated dialogue component (the HUD panel is generated as '<Name>Panel'). Defaults to 'DialogueSystem'"),
53
+ directory: z
54
+ .string()
55
+ .optional()
56
+ .describe("Subdirectory for the generated files. Defaults to 'Code'"),
57
+ charsPerSecond: z
58
+ .number()
59
+ .optional()
60
+ .describe("Typewriter reveal speed in characters per second (clamped to >= 1). Defaults to 40"),
61
+ advanceAction: z
62
+ .string()
63
+ .optional()
64
+ .describe("Input action that completes the reveal / advances to the next line. Sanitized to a safe token. Defaults to 'use'"),
65
+ }, async (params) => {
66
+ const res = await bridge.send("create_dialogue_system", params);
67
+ if (!res.success) {
68
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
69
+ }
70
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
71
+ });
72
+ }
73
+ //# sourceMappingURL=cinematics.js.map
@@ -0,0 +1,16 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { BridgeClient } from "../transport/bridge-client.js";
3
+ /**
4
+ * Interaction pack + Carry (v1.20.0) — three interaction scaffolds:
5
+ *
6
+ * - add_interaction_prompt eye-traced "Press E" HUD bound to IPressable targets
7
+ * (generates a .razor + .razor.scss pair, razor_lint-safe)
8
+ * - create_hold_to_confirm hold-to-fill progress action + static OnConfirmed
9
+ * - create_carry_system pickup / carry / throw with host-routed RPCs + ownership
10
+ *
11
+ * All are file/scene-mutating (refused during play mode by the bridge dispatch).
12
+ * The generated game code was authored against live reflection on this SDK
13
+ * (Scene.Trace, Component.IPressable, Rigidbody, GameObject.Network) — not memory.
14
+ */
15
+ export declare function registerInteractionPackTools(server: McpServer, bridge: BridgeClient): void;
16
+ //# sourceMappingURL=interactionpack.d.ts.map
@@ -0,0 +1,107 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Interaction pack + Carry (v1.20.0) — three interaction scaffolds:
4
+ *
5
+ * - add_interaction_prompt eye-traced "Press E" HUD bound to IPressable targets
6
+ * (generates a .razor + .razor.scss pair, razor_lint-safe)
7
+ * - create_hold_to_confirm hold-to-fill progress action + static OnConfirmed
8
+ * - create_carry_system pickup / carry / throw with host-routed RPCs + ownership
9
+ *
10
+ * All are file/scene-mutating (refused during play mode by the bridge dispatch).
11
+ * The generated game code was authored against live reflection on this SDK
12
+ * (Scene.Trace, Component.IPressable, Rigidbody, GameObject.Network) — not memory.
13
+ */
14
+ export function registerInteractionPackTools(server, bridge) {
15
+ // ── add_interaction_prompt ────────────────────────────────────────
16
+ server.tool("add_interaction_prompt", 'Generate an eye-traced interaction-prompt HUD — a PanelComponent (.razor + .razor.scss pair, like create_leaderboard_panel) that every frame traces a ray from the scene camera (Scene.Trace.Ray, out to [Property] float Range) and, when the crosshair is on a component implementing Component.IPressable, shows a centered "Press E"-style pill. The prompt text comes from the target\'s IPressable.GetTooltip() when it overrides it (most don\'t), else a [Property] DefaultPrompt built from the action. This is the visible half of the interaction loop: it PAIRS with create_interactable / add_interaction_station (which implement IPressable) — this tool tells the player they CAN press, those tools handle the press. Host it under a ScreenPanel (add_screen_panel), then add the component to that panel object. The generated Razor is razor_lint-safe by construction: PanelComponent + BuildHash override folding the visible state, no switch-expressions and no non-ASCII in @code, and a class root selector in the SCSS. LOCAL/visual-only (no [Sync]).', {
17
+ name: z
18
+ .string()
19
+ .optional()
20
+ .describe("Class/file name for the generated .razor. Defaults to 'InteractionPrompt'"),
21
+ directory: z
22
+ .string()
23
+ .optional()
24
+ .describe("Subdirectory for the generated .razor + .razor.scss. Defaults to 'Code/UI'"),
25
+ action: z
26
+ .string()
27
+ .optional()
28
+ .describe("Verb woven into the default prompt text ('Press E to <action>'). Defaults to 'use'"),
29
+ range: z
30
+ .number()
31
+ .optional()
32
+ .describe("Eye-trace reach in world units — how close the crosshair must be to a pressable to show the prompt. Defaults to 120"),
33
+ }, async (params) => {
34
+ const res = await bridge.send("add_interaction_prompt", params);
35
+ if (!res.success) {
36
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
37
+ }
38
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
39
+ });
40
+ // ── create_hold_to_confirm ────────────────────────────────────────
41
+ server.tool("create_hold_to_confirm", "Generate a hold-to-confirm action component (sealed Component). While a named input action is held (Input.Down), a public Progress value fills 0→1 over [Property] float HoldSeconds; releasing early snaps back to 0, or drains down if [Property] bool DecayOnRelease. Reaching 1 fires the static OnConfirmed(GameObject) event, then a short CooldownSeconds blocks re-triggering. The classic 'hold E to disarm / open / revive' interaction. No UI is generated — read the public Progress (0..1) from your own HUD to draw a radial or bar; a #region Feedback hook marks where to tie in a sound/effect. LOCAL/owner-only: input is IsProxy-guarded so it never fires on proxies and is single-player safe. For a host-authoritative outcome, call an [Rpc.Host] from inside the OnConfirmed subscriber. Attach to the player (or any owned object that reads input); optionally attach to an existing GameObject by GUID after a hotload.", {
42
+ name: z
43
+ .string()
44
+ .optional()
45
+ .describe("Class name for the generated component. Defaults to 'HoldToConfirm'"),
46
+ directory: z
47
+ .string()
48
+ .optional()
49
+ .describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
50
+ action: z
51
+ .string()
52
+ .optional()
53
+ .describe("Input action name that must be held (must exist in the project's Input settings — see ensure_input_action). Defaults to 'use'"),
54
+ holdSeconds: z
55
+ .number()
56
+ .optional()
57
+ .describe("Seconds of continuous hold required to confirm. Defaults to 1.5"),
58
+ decayOnRelease: z
59
+ .boolean()
60
+ .optional()
61
+ .describe("Baked default for DecayOnRelease: if true, releasing early drains Progress back down instead of snapping to 0 (editable per-instance). Defaults to false"),
62
+ targetId: z
63
+ .string()
64
+ .optional()
65
+ .describe("GUID of a GameObject to attach the component to (only attaches if the type is already loaded — hotload first)"),
66
+ }, async (params) => {
67
+ const res = await bridge.send("create_hold_to_confirm", params);
68
+ if (!res.success) {
69
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
70
+ }
71
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
72
+ });
73
+ // ── create_carry_system ───────────────────────────────────────────
74
+ server.tool("create_carry_system", "Generate a first-person pickup / carry / throw component (sealed Component) for physics props. Attach it to the PLAYER (the object that owns the camera). It eye-traces from Scene.Camera for a Rigidbody-bearing GameObject tagged [Property] CarryTag (default 'carryable') within [Property] Range; grabbing routes a host-authoritative [Rpc.Host] request that re-validates the target and caller, hands the object's network ownership to the carrier (GameObject.Network.AssignOwnership), and disables the rigidbody's MotionEnabled while held. The held object follows a hold point ([Property] Vector3 HoldOffset in front of the camera) each FixedUpdate; dropping restores physics, throwing applies an impulse ([Property] float ThrowForce). The held-object id is [Sync(SyncFlags.FromHost)] so proxies see the carrying state, and static OnPickedUp / OnDropped events fire uniformly for SFX/VFX. PAIRS with physics props — give each carryable a Rigidbody + Collider and the CarryTag (set_tags); network-spawn them for multiplayer so ownership + transform replicate. Single-player safe (IsProxy is false and RPCs run locally with no session). Inputs: GrabAction (default 'use') grabs/drops, ThrowAction (default 'attack1') throws. Optionally attach to an existing player GameObject by GUID after a hotload.", {
75
+ name: z
76
+ .string()
77
+ .optional()
78
+ .describe("Class name for the generated component. Defaults to 'CarrySystem'"),
79
+ directory: z
80
+ .string()
81
+ .optional()
82
+ .describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
83
+ range: z
84
+ .number()
85
+ .optional()
86
+ .describe("Eye-trace reach for grabbing a carryable, in world units. Defaults to 130"),
87
+ throwForce: z
88
+ .number()
89
+ .optional()
90
+ .describe("Impulse magnitude applied on throw (scales with the prop's mass — tune per game). Defaults to 20000"),
91
+ carryTag: z
92
+ .string()
93
+ .optional()
94
+ .describe("Only objects with this tag (and a Rigidbody) can be picked up; lower-cased/underscored to match s&box tag convention. Defaults to 'carryable'"),
95
+ targetId: z
96
+ .string()
97
+ .optional()
98
+ .describe("GUID of the PLAYER GameObject (the one with the camera) to attach to (only attaches if the type is already loaded — hotload first)"),
99
+ }, async (params) => {
100
+ const res = await bridge.send("create_carry_system", 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
+ }
107
+ //# sourceMappingURL=interactionpack.js.map
@@ -0,0 +1,18 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { BridgeClient } from "../transport/bridge-client.js";
3
+ /**
4
+ * Loot / Economy variants pack — three Tier-2 scaffolds (v1.20.0, Track D):
5
+ *
6
+ * - create_gacha_drop_table per-rarity roll + pity counter + duplicate detection,
7
+ * host-authoritative ([Rpc.Host] roll → [Rpc.Broadcast])
8
+ * - create_currency_pickup networked coin: optional magnet + host-validated grant
9
+ * into create_economy_wallet's AddMoney + replicated despawn
10
+ * - create_offline_progress DateTime delta on enable + clamp + deterministic tick replay
11
+ *
12
+ * All generate a clean, self-contained sealed Component .cs; file/scene-mutating, refused
13
+ * during play mode by the bridge dispatch. create_currency_pickup + create_offline_progress
14
+ * emit host/owner-authoritative code ([Sync]-free state stays host-side); create_gacha_drop_table
15
+ * routes the roll through [Rpc.Host] and announces via [Rpc.Broadcast].
16
+ */
17
+ export declare function registerLootEconomyTools(server: McpServer, bridge: BridgeClient): void;
18
+ //# sourceMappingURL=looteconomy.d.ts.map
@@ -0,0 +1,107 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Loot / Economy variants pack — three Tier-2 scaffolds (v1.20.0, Track D):
4
+ *
5
+ * - create_gacha_drop_table per-rarity roll + pity counter + duplicate detection,
6
+ * host-authoritative ([Rpc.Host] roll → [Rpc.Broadcast])
7
+ * - create_currency_pickup networked coin: optional magnet + host-validated grant
8
+ * into create_economy_wallet's AddMoney + replicated despawn
9
+ * - create_offline_progress DateTime delta on enable + clamp + deterministic tick replay
10
+ *
11
+ * All generate a clean, self-contained sealed Component .cs; file/scene-mutating, refused
12
+ * during play mode by the bridge dispatch. create_currency_pickup + create_offline_progress
13
+ * emit host/owner-authoritative code ([Sync]-free state stays host-side); create_gacha_drop_table
14
+ * routes the roll through [Rpc.Host] and announces via [Rpc.Broadcast].
15
+ */
16
+ export function registerLootEconomyTools(server, bridge) {
17
+ // ── create_gacha_drop_table ───────────────────────────────────────
18
+ server.tool("create_gacha_drop_table", "Generate a host-authoritative gacha / loot-box roller component. Two-level pick: parallel [Property] lists RarityNames + RarityWeights select a RARITY by cumulative weight (the create_weighted_loot_table shape), then a flat 'Rarity:Item' [Property] list (e.g. 'Legendary:Dragon Fang') picks an ITEM uniformly within that rarity — simple and inspector-editable. A pity counter (PityAfter, default 50) guarantees the rarest tier (the LAST entry in RarityNames) after N rolls without it and resets on a hit. Duplicate detection against an owned-items set fires a host-side OnDuplicate hook (marked TODO: convert dupes to shards/currency). Roll() routes to the host via an [Rpc.Host] RequestRoll (Rpc.Caller re-validated — NetFlags is not security) and the result fans out via [Rpc.Broadcast] so every machine fires the static OnRolled(rarity, item, isDuplicate) event; single-player safe (RPCs run locally). Use create_weighted_loot_table instead for a simpler single-tier weighted pick with no pity/dupe/networking. Pairs with create_economy_wallet (spend currency to roll) and create_inventory (store the pulls).", {
19
+ name: z
20
+ .string()
21
+ .optional()
22
+ .describe("Class name for the generated component. Defaults to 'GachaDropTable'"),
23
+ directory: z
24
+ .string()
25
+ .optional()
26
+ .describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
27
+ pityAfter: z
28
+ .number()
29
+ .int()
30
+ .optional()
31
+ .describe("Rolls without a rarest-tier hit before the next roll is guaranteed rarest. 0 disables pity. Defaults to 50"),
32
+ targetId: z
33
+ .string()
34
+ .optional()
35
+ .describe("GUID of a per-player/manager GameObject to attach to (only attaches if the type is already loaded — hotload first)"),
36
+ }, async (params) => {
37
+ const res = await bridge.send("create_gacha_drop_table", params);
38
+ if (!res.success) {
39
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
40
+ }
41
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
42
+ });
43
+ // ── create_currency_pickup ────────────────────────────────────────
44
+ server.tool("create_currency_pickup", "Generate a networked coin / currency pickup component (sealed, Component.ITriggerListener). Host-spawned; when a GameObject carrying PlayerTag ('player') enters its trigger the HOST validates and grants Value (default 1) into a wallet on the player, then destroys the pickup network-wide (the host Destroy() replicates — there is no NetworkDestroy on this SDK). Optional magnet: while MagnetRadius (default 0 = off) is > 0 the coin accelerates toward the nearest player each FixedUpdate (host-side, capped by MaxMagnetSpeed). IsProxy guards keep the grant + despawn host-only in multiplayer (NetworkSpawn the coin on the host); single-player works with no networking. The deposit is reflection-free and dependency-free: a static Grant seam is wired ONCE to the direct typed call — player.Components.Get<EconomyWallet>()?.AddMoney(amount) — so the component compiles with NO hard reference to a specific wallet class (rename the wallet type if yours differs; mirrors create_pickup's self-contained convention). WalletComponentName (default 'EconomyWallet') is used to locate the wallet and name the fix if Grant is left unwired (never silent). Pairs with create_economy_wallet (AddMoney/TrySpend/CanAfford) and create_floating_combat_text (spawn a '+N' popup from OnCollected).", {
45
+ name: z
46
+ .string()
47
+ .optional()
48
+ .describe("Class name for the generated component. Defaults to 'CurrencyPickup'"),
49
+ directory: z
50
+ .string()
51
+ .optional()
52
+ .describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
53
+ value: z
54
+ .number()
55
+ .int()
56
+ .optional()
57
+ .describe("How much currency the pickup grants into the wallet. Defaults to 1"),
58
+ magnetRadius: z
59
+ .number()
60
+ .optional()
61
+ .describe("Magnet range in world units — within it the coin flies to the nearest player each FixedUpdate. 0 = magnet off. Defaults to 0"),
62
+ walletComponentName: z
63
+ .string()
64
+ .optional()
65
+ .describe("Type name of the wallet component to deposit into (used to locate it and to name the fix if the Grant seam is left unwired). Defaults to 'EconomyWallet'"),
66
+ targetId: z
67
+ .string()
68
+ .optional()
69
+ .describe("GUID of a coin GameObject to attach to — give it a trigger Collider (SphereCollider, IsTrigger=true). Only attaches if the type is already loaded — hotload first"),
70
+ }, async (params) => {
71
+ const res = await bridge.send("create_currency_pickup", params);
72
+ if (!res.success) {
73
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
74
+ }
75
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
76
+ });
77
+ // ── create_offline_progress ───────────────────────────────────────
78
+ server.tool("create_offline_progress", "Generate an offline / idle-progress component (sealed, owner/host-only) — the idle-game staple. Persists LastSeenUtc (DateTime) to FileSystem.Data JSON on a dirty-flag autosave heartbeat (AutosaveSeconds) and on OnDisabled, copying create_save_system's persistence patterns. On enable it computes elapsed = now − LastSeenUtc, guards a clock rollback (negative → 0), clamps to MaxOfflineHours (default 8), then replays that time through a SimulateOffline(double seconds) TODO hook in fixed TickSeconds chunks (default 1) so idle accumulation is deterministic (frame-rate independent), and fires the static OnOfflineProgressApplied(seconds) event (drive a 'welcome back, you earned X' screen). IsProxy-guarded so a client can't author their own offline earnings. Fill in the SimulateOffline hook with your idle math (e.g. wallet.AddMoney(rate*seconds)). Pairs with create_economy_wallet / create_save_system / create_stat_modifier_system.", {
79
+ name: z
80
+ .string()
81
+ .optional()
82
+ .describe("Class name for the generated component. Defaults to 'OfflineProgress'"),
83
+ directory: z
84
+ .string()
85
+ .optional()
86
+ .describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
87
+ maxOfflineHours: z
88
+ .number()
89
+ .optional()
90
+ .describe("Offline time is clamped to this many hours (stops a week-away paying out a week). Defaults to 8"),
91
+ tickSeconds: z
92
+ .number()
93
+ .optional()
94
+ .describe("SimulateOffline chunk size in seconds — smaller = finer-grained deterministic replay (floored at 0.1). Defaults to 1"),
95
+ targetId: z
96
+ .string()
97
+ .optional()
98
+ .describe("GUID of an idle/save-manager GameObject to attach to (only attaches if the type is already loaded — hotload first)"),
99
+ }, async (params) => {
100
+ const res = await bridge.send("create_offline_progress", 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
+ }
107
+ //# sourceMappingURL=looteconomy.js.map
@@ -0,0 +1,17 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { BridgeClient } from "../transport/bridge-client.js";
3
+ /**
4
+ * MovieMaker / cutscene family (v1.20.0) — first bridge coverage of
5
+ * Sandbox.MovieMaker (landed in the shipping build; verified live 2026-07-08):
6
+ *
7
+ * - list_movies enumerate the project's .movie resources
8
+ * - add_movie_player wire a MoviePlayer component + MovieResource
9
+ * - play_movie start playback (real playback advances in play mode)
10
+ * - stop_movie stop playback (optionally rewind)
11
+ *
12
+ * Movies are AUTHORED in the editor's Movie Maker dock — the bridge wires and
13
+ * plays them; it does not author keyframes. Only add_movie_player is
14
+ * scene-mutating; play/stop stay callable during play mode.
15
+ */
16
+ export declare function registerMovieMakerTools(server: McpServer, bridge: BridgeClient): void;
17
+ //# sourceMappingURL=moviemaker.d.ts.map
@@ -0,0 +1,98 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * MovieMaker / cutscene family (v1.20.0) — first bridge coverage of
4
+ * Sandbox.MovieMaker (landed in the shipping build; verified live 2026-07-08):
5
+ *
6
+ * - list_movies enumerate the project's .movie resources
7
+ * - add_movie_player wire a MoviePlayer component + MovieResource
8
+ * - play_movie start playback (real playback advances in play mode)
9
+ * - stop_movie stop playback (optionally rewind)
10
+ *
11
+ * Movies are AUTHORED in the editor's Movie Maker dock — the bridge wires and
12
+ * plays them; it does not author keyframes. Only add_movie_player is
13
+ * scene-mutating; play/stop stay callable during play mode.
14
+ */
15
+ export function registerMovieMakerTools(server, bridge) {
16
+ // ── list_movies ───────────────────────────────────────────────────
17
+ server.tool("list_movies", "List the project's .movie resources (Sandbox.MovieMaker clips authored in the editor's Movie Maker dock: Window → Movie Maker). Returns each movie's asset-relative path, whether it currently loads via ResourceLibrary, and whether it has a compiled clip. Start here before add_movie_player / play_movie — and if the list is empty, the movie has to be authored in the dock first (the bridge plays movies; it doesn't author keyframes).", {}, async () => {
18
+ const res = await bridge.send("list_movies", {});
19
+ if (!res.success) {
20
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
21
+ }
22
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
23
+ });
24
+ // ── add_movie_player ──────────────────────────────────────────────
25
+ server.tool("add_movie_player", "Add a Sandbox.MovieMaker.MoviePlayer component and optionally wire a .movie resource into it — the cutscene playback primitive. Creates a new 'Movie Player' GameObject when no id is given. Set playOnStart to begin playback the moment play mode starts (intro cinematics), or leave it and trigger via play_movie (scripted cutscenes — call it from a trigger zone or dialogue beat). isLooping + timeScale map straight onto the component. Movies must already exist as .movie assets (list_movies; author in the Movie Maker dock). Scene-mutating — refused during play mode.", {
26
+ id: z
27
+ .string()
28
+ .optional()
29
+ .describe("GameObject GUID to attach to. Omit to create a new 'Movie Player' object"),
30
+ moviePath: z
31
+ .string()
32
+ .optional()
33
+ .describe("Asset-relative path of the .movie resource to wire (see list_movies)"),
34
+ isLooping: z.boolean().optional().describe("Loop playback"),
35
+ timeScale: z
36
+ .number()
37
+ .optional()
38
+ .describe("Playback speed multiplier (1 = normal)"),
39
+ createTargets: z
40
+ .boolean()
41
+ .optional()
42
+ .describe("Let the player create missing track-target objects on play"),
43
+ playOnStart: z
44
+ .boolean()
45
+ .optional()
46
+ .describe("Begin playing as soon as play mode starts (intro cinematic)"),
47
+ }, async (params) => {
48
+ const res = await bridge.send("add_movie_player", params);
49
+ if (!res.success) {
50
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
51
+ }
52
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
53
+ });
54
+ // ── play_movie ────────────────────────────────────────────────────
55
+ server.tool("play_movie", "Start MoviePlayer playback. Targets the MoviePlayer on the given GameObject, or the first MoviePlayer in the scene when id is omitted. Pass moviePath to load-and-play a different .movie on the same player; positionSeconds seeks before playing; isLooping/timeScale apply immediately. Clips genuinely advance in PLAY MODE (start_play first, then verify with capture_view) — in edit mode this only sets state, which the response calls out. NOT scene-mutating, so it works during play mode.", {
56
+ id: z
57
+ .string()
58
+ .optional()
59
+ .describe("GameObject GUID holding the MoviePlayer. Omit to use the first MoviePlayer in the scene"),
60
+ moviePath: z
61
+ .string()
62
+ .optional()
63
+ .describe("Asset-relative .movie path to load and play (otherwise plays the wired Resource)"),
64
+ positionSeconds: z
65
+ .number()
66
+ .optional()
67
+ .describe("Seek to this time (seconds) before playing"),
68
+ timeScale: z
69
+ .number()
70
+ .optional()
71
+ .describe("Playback speed multiplier (1 = normal)"),
72
+ isLooping: z.boolean().optional().describe("Loop playback"),
73
+ }, async (params) => {
74
+ const res = await bridge.send("play_movie", params);
75
+ if (!res.success) {
76
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
77
+ }
78
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
79
+ });
80
+ // ── stop_movie ────────────────────────────────────────────────────
81
+ server.tool("stop_movie", "Stop MoviePlayer playback (the counterpart to play_movie). Targets the MoviePlayer on the given GameObject, or the first MoviePlayer in the scene when id is omitted. Pass rewind to also reset the playhead to 0 so the next play_movie starts from the top. Works during play mode.", {
82
+ id: z
83
+ .string()
84
+ .optional()
85
+ .describe("GameObject GUID holding the MoviePlayer. Omit to use the first MoviePlayer in the scene"),
86
+ rewind: z
87
+ .boolean()
88
+ .optional()
89
+ .describe("Also reset the playhead to 0"),
90
+ }, async (params) => {
91
+ const res = await bridge.send("stop_movie", params);
92
+ if (!res.success) {
93
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
94
+ }
95
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
96
+ });
97
+ }
98
+ //# sourceMappingURL=moviemaker.js.map
@@ -0,0 +1,18 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { BridgeClient } from "../transport/bridge-client.js";
3
+ /**
4
+ * Networking primitives pack — four multiplayer scaffolds (v1.20.0, Track B):
5
+ *
6
+ * - create_host_rpc_action validated + rate-limited [Rpc.Host] action skeleton
7
+ * - add_targeted_rpc Rpc.FilterInclude single-client (unicast) side-effect
8
+ * - create_local_player_resolver proxy-safe "who is MY player" resolver (online + offline)
9
+ * - add_host_migration_recovery proxy→authority transition detector + OnBecameHost hook
10
+ *
11
+ * Each generates ONE self-contained sealed Component .cs into the project via
12
+ * ScaffoldHelpers. These are the correctness primitives every networked s&box
13
+ * game hand-rolls (and usually gets wrong): host-authoritative validated actions,
14
+ * unicast RPCs, proxy-safe local-player resolution, and host-migration recovery.
15
+ * All are file/scene-mutating and refused during play mode by the bridge dispatch.
16
+ */
17
+ export declare function registerNetPrimitivesTools(server: McpServer, bridge: BridgeClient): void;
18
+ //# sourceMappingURL=netprimitives.d.ts.map
@@ -0,0 +1,110 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Networking primitives pack — four multiplayer scaffolds (v1.20.0, Track B):
4
+ *
5
+ * - create_host_rpc_action validated + rate-limited [Rpc.Host] action skeleton
6
+ * - add_targeted_rpc Rpc.FilterInclude single-client (unicast) side-effect
7
+ * - create_local_player_resolver proxy-safe "who is MY player" resolver (online + offline)
8
+ * - add_host_migration_recovery proxy→authority transition detector + OnBecameHost hook
9
+ *
10
+ * Each generates ONE self-contained sealed Component .cs into the project via
11
+ * ScaffoldHelpers. These are the correctness primitives every networked s&box
12
+ * game hand-rolls (and usually gets wrong): host-authoritative validated actions,
13
+ * unicast RPCs, proxy-safe local-player resolution, and host-migration recovery.
14
+ * All are file/scene-mutating and refused during play mode by the bridge dispatch.
15
+ */
16
+ export function registerNetPrimitivesTools(server, bridge) {
17
+ // ── create_host_rpc_action ────────────────────────────────────────
18
+ server.tool("create_host_rpc_action", "Generate a validated, rate-limited host-action component — the SAFE skeleton for 'a client asks the host to DO something' (buy, use, vote, interact, spend). The generated sealed Component exposes a client-callable Request() that forwards to an [Rpc.Host] SubmitRequest() which runs ON THE HOST: it re-resolves WHO called it via Rpc.Caller (never trusting client args for identity), enforces a per-SteamId cooldown backed by a Dictionary<ulong, TimeSince>, runs a clearly-marked TODO block for your authoritative action, and fires the static OnActionExecuted(Connection) event. This is the correct answer to the #1 multiplayer exploit class: [Rpc.Host] is callable by ANY client with forged args — NetFlags restrict who may INVOKE, which is not security — so identity + rate-limit + validation all live inside the host body. Single-player safe (no session → the RPC runs locally, caller falls back to Connection.Local). Attach it to the object that owns the action (a player, a station, or your game manager). Next steps: hotload, tag/attach, call Request() from input or a UI button, fill the TODO host block, and subscribe to OnActionExecuted. Covers the backlog's add_rate_limited_rpc. Optionally attached to an existing GameObject by GUID (after a hotload).", {
19
+ name: z
20
+ .string()
21
+ .optional()
22
+ .describe("Class name for the generated component. Defaults to 'HostRpcAction'"),
23
+ directory: z
24
+ .string()
25
+ .optional()
26
+ .describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
27
+ cooldownSeconds: z
28
+ .number()
29
+ .optional()
30
+ .describe("Minimum seconds between accepted requests, per calling player (the per-SteamId rate limit). Defaults to 1"),
31
+ targetId: z
32
+ .string()
33
+ .optional()
34
+ .describe("GUID of a GameObject to attach to (only attaches if the type is already loaded — hotload first)"),
35
+ }, async (params) => {
36
+ const res = await bridge.send("create_host_rpc_action", params);
37
+ if (!res.success) {
38
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
39
+ }
40
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
41
+ });
42
+ // ── add_targeted_rpc ──────────────────────────────────────────────
43
+ server.tool("add_targeted_rpc", "Generate a component demonstrating the unicast (single-client) RPC pattern via Rpc.FilterInclude. A normal [Rpc.Broadcast] runs on EVERY machine; the generated component's host-side SendTo(Connection target, string message) wraps its [Rpc.Broadcast] call in `using ( Rpc.FilterInclude( target ) )` so ONLY the target connection executes the body, which raises the static OnReceived(string) event. This is the RIGHT way to send something to one player — a private prompt, a personal reward toast, a per-player cutscene cue — instead of broadcasting to everyone and filtering on the client (which leaks data and wastes bandwidth). Call SendTo on the host (guarded behind Networking.IsActive so Networking.IsHost can't throw with no session; runs locally in solo). Attach to a networked manager object that is NetworkSpawn'd. Next steps: hotload, attach, call SendTo(player.Network.Owner, \"...\") from the host, and subscribe to OnReceived on the target client. Optionally attached to an existing GameObject by GUID (after a hotload).", {
44
+ name: z
45
+ .string()
46
+ .optional()
47
+ .describe("Class name for the generated component. Defaults to 'TargetedRpc'"),
48
+ directory: z
49
+ .string()
50
+ .optional()
51
+ .describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
52
+ targetId: z
53
+ .string()
54
+ .optional()
55
+ .describe("GUID of a GameObject to attach to (only attaches if the type is already loaded — hotload first)"),
56
+ }, async (params) => {
57
+ const res = await bridge.send("add_targeted_rpc", params);
58
+ if (!res.success) {
59
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
60
+ }
61
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
62
+ });
63
+ // ── create_local_player_resolver ──────────────────────────────────
64
+ server.tool("create_local_player_resolver", "Generate a proxy-safe 'who is MY player?' resolver — the corpus footgun-killer that stops you running local-player logic against a proxy of someone else's player. The generated sealed Component exposes a static Local property that lazily finds the player GameObject belonging to THIS machine: online it's the tagged object whose Network.Owner is the local connection (Network.Owner == Connection.Local, or Network.IsOwner); offline/solo (no session) it's the first/only tagged player. The result is cached and revalidated with IsValid() so a destroyed or respawned player is re-resolved automatically. Also exposes a static IsLocal(GameObject) helper for filtering events (e.g. only open a station overlay for your own player). Works identically online and offline — no `if (Network.IsOwner)` guard that silently disables everything in a solo playtest. Attach ONE to a persistent object (your game manager) so PlayerTag is configurable in the inspector; the resolver is static and callable from anywhere. Next steps: hotload, attach, tag each player GameObject with the PlayerTag, then read <Name>.Local / filter with <Name>.IsLocal(go). Optionally attached to an existing GameObject by GUID (after a hotload).", {
65
+ name: z
66
+ .string()
67
+ .optional()
68
+ .describe("Class name for the generated component. Defaults to 'LocalPlayerResolver'"),
69
+ directory: z
70
+ .string()
71
+ .optional()
72
+ .describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
73
+ playerTag: z
74
+ .string()
75
+ .optional()
76
+ .describe("Tag that marks a player GameObject (players must carry this tag). Baked as the default PlayerTag, editable per-instance in the inspector. Defaults to 'player'"),
77
+ targetId: z
78
+ .string()
79
+ .optional()
80
+ .describe("GUID of a GameObject to attach to (only attaches if the type is already loaded — hotload first)"),
81
+ }, async (params) => {
82
+ const res = await bridge.send("create_local_player_resolver", params);
83
+ if (!res.success) {
84
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
85
+ }
86
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
87
+ });
88
+ // ── add_host_migration_recovery ───────────────────────────────────
89
+ server.tool("add_host_migration_recovery", "Generate a host-migration recovery component — it detects when THIS machine takes authority over its GameObject (the proxy→authority transition that promotes a client to host when the old host leaves) and gives you a clean hook to rebuild host-only state. It tracks IsProxy each frame; when it flips from true (someone else was the authority) to false (now it's us), it fires the static OnBecameHost(GameObject) event and runs a virtual-style TODO rebuild region, then — after a tunable SettleSeconds delay so in-flight packets can land — runs a deferred validation region. s&box has a known bug where networked objects are often destroyed during host migration and the new host inherits stale transient state, so the recommended shape is: detect becoming host, aggressively rebuild (re-arm host-only loops/TimeUntil timers against your clock, TakeOwnership of orphans, rebuild handle maps by world position, reconcile the [Sync] registry against the real scene), and defer the sanity check ~1s. Inert offline (IsProxy is always false with no session). Attach to your host-authoritative manager object (NetworkSpawn'd). Next steps: hotload, attach, subscribe to OnBecameHost, and fill the two TODO regions. Requires a real host migration (a second client that becomes host) to fire — it can't be exercised in a solo playtest. Optionally attached to an existing GameObject by GUID (after a hotload).", {
90
+ name: z
91
+ .string()
92
+ .optional()
93
+ .describe("Class name for the generated component. Defaults to 'HostMigrationRecovery'"),
94
+ directory: z
95
+ .string()
96
+ .optional()
97
+ .describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
98
+ targetId: z
99
+ .string()
100
+ .optional()
101
+ .describe("GUID of a GameObject to attach to (only attaches if the type is already loaded — hotload first)"),
102
+ }, async (params) => {
103
+ const res = await bridge.send("add_host_migration_recovery", params);
104
+ if (!res.success) {
105
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
106
+ }
107
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
108
+ });
109
+ }
110
+ //# sourceMappingURL=netprimitives.js.map
@@ -0,0 +1,16 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { BridgeClient } from "../transport/bridge-client.js";
3
+ /**
4
+ * UI / Feedback pack — three feedback scaffolds (v1.20.0, Track C):
5
+ *
6
+ * - create_worldpanel_ui diegetic clickable WorldPanel Razor UI (+ scss)
7
+ * - create_proxy_nametag billboarded owner-name tag above a networked player
8
+ * - create_combo_meter combo counter + decay + multiplier (.cs) + Razor HUD
9
+ *
10
+ * All are file/scene-mutating (refused during play mode by the bridge dispatch).
11
+ * The Razor output is razor_lint-safe by construction (BuildHash override, no
12
+ * switch-expressions or non-ASCII in @code, class-selector SCSS roots), modeled
13
+ * on create_leaderboard_panel.
14
+ */
15
+ export declare function registerUiFeedbackTools(server: McpServer, bridge: BridgeClient): void;
16
+ //# sourceMappingURL=uifeedback.d.ts.map
@@ -0,0 +1,83 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * UI / Feedback pack — three feedback scaffolds (v1.20.0, Track C):
4
+ *
5
+ * - create_worldpanel_ui diegetic clickable WorldPanel Razor UI (+ scss)
6
+ * - create_proxy_nametag billboarded owner-name tag above a networked player
7
+ * - create_combo_meter combo counter + decay + multiplier (.cs) + Razor HUD
8
+ *
9
+ * All are file/scene-mutating (refused during play mode by the bridge dispatch).
10
+ * The Razor output is razor_lint-safe by construction (BuildHash override, no
11
+ * switch-expressions or non-ASCII in @code, class-selector SCSS roots), modeled
12
+ * on create_leaderboard_panel.
13
+ */
14
+ export function registerUiFeedbackTools(server, bridge) {
15
+ // ── create_worldpanel_ui ──────────────────────────────────────────
16
+ server.tool("create_worldpanel_ui", "Generate a diegetic, clickable world-space UI: a Razor PanelComponent (+ .razor.scss) meant to sit on a GameObject that ALSO carries a Sandbox.WorldPanel — the WorldPanel is the world-space render surface (PanelSize / RenderScale / InteractionRange), this component is the actual UI it renders (PanelComponent has no world-panel mode of its own). Ships two example buttons wired to @onclick that raise a static event OnButtonPressed(string id), so game code reacts WITHOUT editing the panel (subscribe: <Name>.OnButtonPressed += id => ...). razor_lint-safe by construction (BuildHash override, no switch-expressions / non-ASCII in @code, class-selector SCSS root). SCENE PREREQUISITE FOR CLICKS: a WorldPanel's buttons only fire when a Sandbox.WorldInput exists in the scene (typically on the camera or the player) with its LeftMouseAction set to your click input action (e.g. \"attack1\"); on this SDK WorldInput drives itself from the camera + that action — there is no manual ray to feed, and WorldInput.Hovered (read-only) reflects the panel under the cursor. Without a WorldInput present, @onclick never fires. Setup: add_world_panel to a GameObject, add this component to the same object, add a WorldInput to the scene.", {
17
+ name: z
18
+ .string()
19
+ .optional()
20
+ .describe("Class name for the generated panel. Defaults to 'WorldPanelUi'"),
21
+ directory: z
22
+ .string()
23
+ .optional()
24
+ .describe("Subdirectory for the generated .razor + .razor.scss. Defaults to 'Code/UI'"),
25
+ title: z
26
+ .string()
27
+ .optional()
28
+ .describe("Heading text baked into the panel (editable per-instance via the Title [Property]). Defaults to 'Interact'"),
29
+ }, async (params) => {
30
+ const res = await bridge.send("create_worldpanel_ui", params);
31
+ if (!res.success) {
32
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
33
+ }
34
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
35
+ });
36
+ // ── create_proxy_nametag ──────────────────────────────────────────
37
+ server.tool("create_proxy_nametag", "Generate a sealed Component that floats the OWNER'S display name above a networked player. TextRenderer-based (not WorldPanel): a nametag is one short string with a distance fade, so a TextRenderer on a managed child object is far simpler than a WorldPanel + Razor + WorldInput stack — no UI assets, no panel host, per-frame alpha is a one-liner (mirrors create_floating_combat_text). Reads GameObject.Network.Owner.DisplayName (Owner and OwnerConnection are the same Connection on this SDK; falls back to the object name offline). Visibility is the INVERSE of the usual proxy guard: it renders only when GameObject.Network.IsProxy is true — i.e. on OTHER clients' copies of the player — so you never see a tag over your own head (offline / no networking => IsProxy false everywhere => no tags, expected). Spawns a CHILD GameObject for the text so billboarding never rotates the player model; cleaned up on disable. [Property] MaxDistance fades the tag out with distance, HeightOffset floats it above the head, FontSize sizes it. Attach to the ROOT of your networked player object. Pairs with create_networked_player (add it to the generated player prefab so every remote player is labeled).", {
38
+ name: z
39
+ .string()
40
+ .optional()
41
+ .describe("Class name for the generated component. Defaults to 'ProxyNametag'"),
42
+ directory: z
43
+ .string()
44
+ .optional()
45
+ .describe("Subdirectory for the generated .cs file. Defaults to 'Code'"),
46
+ maxDistance: z
47
+ .number()
48
+ .optional()
49
+ .describe("Full alpha up close; fades to zero as the camera approaches this distance and is hidden past it, in world units. Defaults to 2000"),
50
+ heightOffset: z
51
+ .number()
52
+ .optional()
53
+ .describe("Height above the object's origin to float the tag, in world units (~72 clears a Citizen's head). Defaults to 72"),
54
+ }, async (params) => {
55
+ const res = await bridge.send("create_proxy_nametag", params);
56
+ if (!res.success) {
57
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
58
+ }
59
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
60
+ });
61
+ // ── create_combo_meter ────────────────────────────────────────────
62
+ server.tool("create_combo_meter", "Generate a combo system: a sealed Component (the authoritative, headless state) PLUS a small Razor HUD (PanelComponent + scss). Three files: <Name>.cs + <Name>Hud.razor + <Name>Hud.razor.scss. The component exposes a static Bump() — call it on every hit and the Count rises; an idle window (ComboWindowSeconds, tracked with TimeSince) resets it; the Multiplier steps up through [Property] tier thresholds (Tier2Hits/Tier3Hits/Tier4Hits => 2x/3x/4x); and a static OnComboChanged(int count, float multiplier) event fires so any HUD/audio reacts without a reference (Bump() targets the active instance, so callers never need a handle — attach ONE to a persistent object). The HUD subscribes to OnComboChanged, shows \"<count> HITS x<mult>\", and pulses via a CSS animation on every change (razor_lint-safe: BuildHash folds count/mult/pulse, no switch-expressions / non-ASCII in @code, class-selector SCSS root; host it under a ScreenPanel via add_screen_panel). Pairs with create_health_system / create_floating_combat_text — call Bump() from the damage path and Spawn a popup that reflects the multiplier.", {
63
+ name: z
64
+ .string()
65
+ .optional()
66
+ .describe("Class name for the combo component; the HUD is generated as <Name>Hud. Defaults to 'ComboMeter'"),
67
+ directory: z
68
+ .string()
69
+ .optional()
70
+ .describe("Subdirectory for the generated .cs + .razor + .razor.scss. Defaults to 'Code'"),
71
+ comboWindowSeconds: z
72
+ .number()
73
+ .optional()
74
+ .describe("Idle seconds before the combo resets back to zero (clamped to >= 0.25). Defaults to 3"),
75
+ }, async (params) => {
76
+ const res = await bridge.send("create_combo_meter", params);
77
+ if (!res.success) {
78
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
79
+ }
80
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
81
+ });
82
+ }
83
+ //# sourceMappingURL=uifeedback.js.map
package/package.json CHANGED
@@ -1,55 +1,55 @@
1
- {
2
- "name": "sbox-mcp-server",
3
- "version": "1.19.0",
4
- "description": "MCP Server for s&box game engine — enables Claude to build games through conversation",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "bin": {
8
- "sbox-mcp-server": "dist/index.js",
9
- "sbox-mcp": "dist/index.js"
10
- },
11
- "files": [
12
- "dist/**/*.js",
13
- "dist/**/*.d.ts",
14
- "README.md",
15
- "LICENSE"
16
- ],
17
- "scripts": {
18
- "build": "tsc",
19
- "test": "npm run build && node --test",
20
- "start": "node dist/index.js",
21
- "dev": "tsc --watch",
22
- "prepublishOnly": "npm run build"
23
- },
24
- "keywords": [
25
- "mcp",
26
- "sbox",
27
- "s&box",
28
- "game-engine",
29
- "claude",
30
- "ai",
31
- "source2",
32
- "model-context-protocol"
33
- ],
34
- "repository": {
35
- "type": "git",
36
- "url": "git+https://github.com/LouSputthole/Sbox-Claude.git"
37
- },
38
- "author": "sboxskins.gg (https://sboxskins.gg)",
39
- "homepage": "https://sboxskins.gg",
40
- "bugs": {
41
- "url": "https://github.com/LouSputthole/Sbox-Claude/issues"
42
- },
43
- "dependencies": {
44
- "@modelcontextprotocol/sdk": "^1.12.1",
45
- "zod": "^3.24.0"
46
- },
47
- "devDependencies": {
48
- "@types/node": "^22.0.0",
49
- "typescript": "^5.7.0"
50
- },
51
- "engines": {
52
- "node": ">=18.0.0"
53
- },
54
- "license": "SEE LICENSE IN LICENSE"
55
- }
1
+ {
2
+ "name": "sbox-mcp-server",
3
+ "version": "1.20.0",
4
+ "description": "MCP Server for s&box game engine — enables Claude to build games through conversation",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "sbox-mcp-server": "dist/index.js",
9
+ "sbox-mcp": "dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist/**/*.js",
13
+ "dist/**/*.d.ts",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "test": "npm run build && node --test",
20
+ "start": "node dist/index.js",
21
+ "dev": "tsc --watch",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "keywords": [
25
+ "mcp",
26
+ "sbox",
27
+ "s&box",
28
+ "game-engine",
29
+ "claude",
30
+ "ai",
31
+ "source2",
32
+ "model-context-protocol"
33
+ ],
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/LouSputthole/Sbox-Claude.git"
37
+ },
38
+ "author": "sboxskins.gg (https://sboxskins.gg)",
39
+ "homepage": "https://sboxskins.gg",
40
+ "bugs": {
41
+ "url": "https://github.com/LouSputthole/Sbox-Claude/issues"
42
+ },
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.12.1",
45
+ "zod": "^3.24.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^22.0.0",
49
+ "typescript": "^5.7.0"
50
+ },
51
+ "engines": {
52
+ "node": ">=18.0.0"
53
+ },
54
+ "license": "SEE LICENSE IN LICENSE"
55
+ }