sbox-mcp-server 1.11.0 → 1.13.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.
@@ -76,5 +76,27 @@ export function registerAssetTools(server, bridge) {
76
76
  content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
77
77
  };
78
78
  });
79
+ // ── copy_asset_with_dependencies ─────────────────────────────────
80
+ server.tool("copy_asset_with_dependencies", "Copy a project asset and its full dependency closure (via Asset.GetReferences(deep:true)) into a target directory, preserving relative path structure so material references to textures keep resolving. SHADOW GUARD: refuses to write under core engine trees (models/citizen, models/dev, materials/dev, materials/default) -- copying there triggers an infinite asset-recompile loop (BRIDGE_GOTCHAS #5). Cloud/procedural/transient assets are skipped with a reason. Returns { copied:[{from,to}], skipped:[{path,reason}], count, note }.", {
81
+ sourcePath: z
82
+ .string()
83
+ .describe("Absolute path OR project-relative path of the source asset (e.g. 'models/props/crate.vmdl'). Use search_assets to find the exact path."),
84
+ targetDir: z
85
+ .string()
86
+ .optional()
87
+ .describe("Project-relative destination directory (e.g. 'Assets/library'). Defaults to 'Assets/library'"),
88
+ overwrite: z
89
+ .boolean()
90
+ .optional()
91
+ .describe("Overwrite existing files at the destination. Defaults to false"),
92
+ }, async (params) => {
93
+ const res = await bridge.send("copy_asset_with_dependencies", params);
94
+ if (!res.success) {
95
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
96
+ }
97
+ return {
98
+ content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
99
+ };
100
+ });
79
101
  }
80
102
  //# sourceMappingURL=assets.js.map
@@ -24,9 +24,8 @@ export function registerComponentTools(server, bridge) {
24
24
  };
25
25
  });
26
26
  // ── get_all_properties ───────────────────────────────────────────
27
- server.tool("get_all_properties", "Dump all public properties of a component as JSON names, types, and current values", {
27
+ server.tool("get_all_properties", "Dump all public properties of every component on a GameObject as JSON -- names, types, and current values", {
28
28
  id: z.string().describe("GUID of the GameObject"),
29
- component: z.string().describe("Component type name"),
30
29
  }, async (params) => {
31
30
  const res = await bridge.send("get_all_properties", params);
32
31
  if (!res.success) {
@@ -257,7 +257,59 @@ export function registerGameplayTools(server, bridge) {
257
257
  }
258
258
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
259
259
  });
260
- // ── create_pickup ─────────────────────────────────────────────────
260
+ // ── create_interactable ───────────────────────────────────────────
261
+ server.tool("create_interactable", "Generate a Component.IPressable interactable: the built-in PlayerController 'use' key drives Press()/Hover()/Blur() with no custom player code. Includes a static OnPressed event, an optional cooldown (TimeUntil), and a private OnPress() extensionpoint for effects. For host-authoritative side-effects call an [Rpc.Host] from OnPress(). The Prompt property is left to your game's HUD. Optionally attached to an existing GameObject by GUID (after a hotload).", {
262
+ name: z.string().optional().describe("Class name. Defaults to 'Interactable'"),
263
+ directory: z.string().optional().describe("Subdirectory for the .cs file. Defaults to 'Code'"),
264
+ prompt: z
265
+ .string()
266
+ .optional()
267
+ .describe("Prompt string shown by the game's HUD when hovering. Defaults to 'Press'"),
268
+ cooldownSeconds: z
269
+ .number()
270
+ .optional()
271
+ .describe("Seconds before the interactable can be pressed again. 0 = no cooldown. Defaults to 0"),
272
+ targetId: z
273
+ .string()
274
+ .optional()
275
+ .describe("GUID of an existing GameObject to attach the component to (only attaches if the type is already loaded — hotload first)"),
276
+ }, async (params) => {
277
+ const res = await bridge.send("create_interactable", params);
278
+ if (!res.success) {
279
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
280
+ }
281
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
282
+ });
283
+ // -- create_weighted_loot_table ----------------------------------------
284
+ server.tool("create_weighted_loot_table", "Generate a cumulative-weight random loot picker: parallel Name/Weight lists (inspector-editable), a Roll() method that returns a winning entry name and fires a static OnLoot event, and optional pity (guarantee the last/rarest entry after PityAfter consecutive non-rare rolls). Roll() is host-authoritative -- only call it on the host and replicate the result (clients rolling their own loot is equivalent to clients writing their own money balance). Optionally attached to an existing GameObject by GUID (after a hotload).", {
285
+ name: z.string().optional().describe("Class name. Defaults to 'LootTable'"),
286
+ directory: z
287
+ .string()
288
+ .optional()
289
+ .describe("Subdirectory for the .cs file. Defaults to 'Code'"),
290
+ entries: z
291
+ .union([
292
+ z.array(z.object({ name: z.string(), weight: z.number() })),
293
+ z.string().describe('Compact "name:weight,name:weight" string, e.g. "common:70,uncommon:25,rare:5"'),
294
+ ])
295
+ .optional()
296
+ .describe("Loot table entries. Defaults to common:70 / uncommon:25 / rare:5"),
297
+ pity: z
298
+ .boolean()
299
+ .optional()
300
+ .describe("If true, guarantee the last (rarest) entry after PityAfter consecutive non-rare rolls. Defaults to false"),
301
+ targetId: z
302
+ .string()
303
+ .optional()
304
+ .describe("GUID of an existing GameObject to attach to (only if the type is already loaded -- hotload first)"),
305
+ }, async (params) => {
306
+ const res = await bridge.send("create_weighted_loot_table", params);
307
+ if (!res.success) {
308
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
309
+ }
310
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
311
+ });
312
+ // -- create_pickup -----------------------------------------------------
261
313
  server.tool("create_pickup", "Generate a trigger-based collectible component. On enter by a tagged object it raises OnCollected (wire it to your objective/score system) and despawns. Optionally builds a visible pickup GameObject with a trigger SphereCollider (+ a model) in one call", {
262
314
  name: z.string().optional().describe("Class name. Defaults to 'Pickup'"),
263
315
  directory: z
@@ -298,5 +350,87 @@ export function registerGameplayTools(server, bridge) {
298
350
  content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
299
351
  };
300
352
  });
353
+ // -- create_save_system ------------------------------------------------
354
+ server.tool("create_save_system", "Generate a versioned save-system component: a SaveData POCO with Version bump on schema change, dirty-flag autosave on a TimeUntil timer, clamp-on-load Sanitize() for corrupt/hand-edited saves, and delete-on-version-mismatch to start fresh instead of crashing. Runs only on the owning machine (IsProxy guard). Fires static OnLoaded/OnSaved hooks for HUD and analytics. FileSystem.Data.ReadJsonOrDefault/WriteJson verified live on the current SDK. Optionally attached to an existing GameObject by GUID (after a hotload).", {
355
+ name: z.string().optional().describe("Class name. Defaults to 'SaveSystem'"),
356
+ directory: z.string().optional().describe("Subdirectory for the .cs file. Defaults to 'Code'"),
357
+ fileName: z
358
+ .string()
359
+ .optional()
360
+ .describe("Save file name under FileSystem.Data (e.g. 'save.json'). Defaults to 'save.json'"),
361
+ version: z
362
+ .number()
363
+ .int()
364
+ .optional()
365
+ .describe("Schema version embedded in SaveData. Old saves with a different version start fresh. Defaults to 1"),
366
+ autosaveSeconds: z
367
+ .number()
368
+ .optional()
369
+ .describe("Seconds between autosave ticks (0 disables autosave). Defaults to 10"),
370
+ targetId: z
371
+ .string()
372
+ .optional()
373
+ .describe("GUID of an existing GameObject to attach to (only if the type is already loaded -- hotload first)"),
374
+ }, async (params) => {
375
+ const res = await bridge.send("create_save_system", params);
376
+ if (!res.success) {
377
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
378
+ }
379
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
380
+ });
381
+ // -- create_leaderboard_panel ------------------------------------------
382
+ server.tool("create_leaderboard_panel", "Generate a Razor PanelComponent that fetches and displays a Sandbox.Services leaderboard derived from a stat name. Produces TWO files: {name}.razor and {name}.razor.scss. The panel auto-refreshes every 30 s, shows rank/displayName/value rows, handles loading state, and includes a BuildHash() override (razor-lint clean). Must be hosted under a ScreenPanel or WorldPanel. Stats must be configured for the project ident on sbox.game. Uses Leaderboards.Get(statName) + board.Refresh() -- the exact API from ServicesQueryHandler.", {
383
+ name: z.string().optional().describe("Class name for the panel component. Defaults to 'LeaderboardPanel'"),
384
+ directory: z.string().optional().describe("Subdirectory for the generated files. Defaults to 'Code/UI'"),
385
+ statName: z.string().optional().describe("Sandbox.Services stat name the leaderboard is derived from. Defaults to 'score'"),
386
+ title: z.string().optional().describe("Display title shown at the top of the panel. Defaults to 'Leaderboard'"),
387
+ maxRows: z.number().int().optional().describe("Maximum leaderboard rows to fetch and display. Defaults to 10"),
388
+ }, async (params) => {
389
+ const res = await bridge.send("create_leaderboard_panel", params);
390
+ if (!res.success) {
391
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
392
+ }
393
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
394
+ });
395
+ // -- create_inventory --------------------------------------------------
396
+ server.tool("create_inventory", "Generate a slot-based inventory component using parallel List<string> ItemIds / List<int> Counts (serialization-safe, inspector-editable). Includes TryAdd (stack-first, partial-add rejected), TryRemove, CountOf, Move (swap or merge same-id slots), and Clear. Static OnChanged event fires after every successful mutation. Host-authoritative usage note: mutate on the host in multiplayer, replicate via your own [Sync]/RPC. Pairs with create_pickup.", {
397
+ name: z.string().optional().describe("Class name. Defaults to 'Inventory'"),
398
+ directory: z.string().optional().describe("Subdirectory for the .cs file. Defaults to 'Code'"),
399
+ capacity: z.number().int().optional().describe("Total slot count. Defaults to 24"),
400
+ maxStack: z.number().int().optional().describe("Maximum items per slot (stack cap). Defaults to 99"),
401
+ targetId: z.string().optional().describe("GUID of an existing GameObject to attach to (hotload first)"),
402
+ }, async (params) => {
403
+ const res = await bridge.send("create_inventory", params);
404
+ if (!res.success) {
405
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
406
+ }
407
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
408
+ });
409
+ // -- create_stat_modifier_system ---------------------------------------
410
+ server.tool("create_stat_modifier_system", "Generate an enum-keyed stat modifier system with three modifier layers: SET (highest-priority-wins hard override), ADD (summed bonuses), MULT (multiplied factors applied last). Modifier storage uses parallel private Lists of primitive types (serialization-safe). RemoveModifiersFrom(source) cleans up all mods from a buff/debuff source by reference. Static OnStatChanged(stat, value) event fires after every add/remove. Mined from RPG/buff/debuff patterns across shipped s&box games.", {
411
+ name: z.string().optional().describe("Class name prefix -- generates {name}Stat enum + {name} Component. Defaults to 'StatSystem'"),
412
+ directory: z.string().optional().describe("Subdirectory for the .cs file. Defaults to 'Code'"),
413
+ stats: z.union([z.array(z.string()), z.string()]).optional().describe("Stat names as a JSON array or comma-separated string. Defaults to 'Health,Speed,Damage'"),
414
+ targetId: z.string().optional().describe("GUID of an existing GameObject to attach to (hotload first)"),
415
+ }, async (params) => {
416
+ const res = await bridge.send("create_stat_modifier_system", params);
417
+ if (!res.success) {
418
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
419
+ }
420
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
421
+ });
422
+ // -- create_placement_mode ---------------------------------------------
423
+ server.tool("create_placement_mode", "Generate a ghost-preview + commit placement component (single class). StartPlacing() clones GhostPrefab as a NetworkMode.Never preview with colliders disabled and ModelRenderers tinted semi-transparent. Each frame while placing: ray from Scene.Camera.GetMouseRay(), IgnoreGameObjectHierarchy(ghost), snap hit position to GridSize (0 = freeform), move ghost. On Input.Pressed('attack1') TryPlace() re-validates distance and commits a real clone. StopPlacing() destroys the ghost. Static OnPlaced(GameObject, Vector3) event. Includes a multiplayer RPC note. API grounded in building-placement cookbook (enifun.shop_manager pattern).", {
424
+ name: z.string().optional().describe("Class name. Defaults to 'PlacementMode'"),
425
+ directory: z.string().optional().describe("Subdirectory for the .cs file. Defaults to 'Code'"),
426
+ gridSize: z.number().optional().describe("Snap grid size in world units (0 = freeform placement). Defaults to 0"),
427
+ targetId: z.string().optional().describe("GUID of an existing GameObject to attach to (hotload first)"),
428
+ }, async (params) => {
429
+ const res = await bridge.send("create_placement_mode", params);
430
+ if (!res.success) {
431
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
432
+ }
433
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
434
+ });
301
435
  }
302
436
  //# sourceMappingURL=gameplay.js.map
@@ -38,6 +38,30 @@ export function registerInspectionTools(server, bridge) {
38
38
  return { content: [{ type: "text", text: `Error: ${res.error}` }] };
39
39
  return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
40
40
  });
41
+ // ── sandbox_lint ──────────────────────────────────────────────────
42
+ server.tool("sandbox_lint", "Static-scan the project's C# for s&box sandbox whitelist violations BEFORE they cause compile errors: System.MathF (use MathX), System.Math (use MathX), Array.Clone() (use .ToArray()), System.Net / raw sockets (use Sandbox.Http), System.IO.File (use FileSystem.Data), and raw System.Threading.Thread (use async/Task or GameTask). Returns { scanned, findings: [{file, line, match, advice}], clean }. Scope to a subdirectory with the directory param.", {
43
+ directory: z
44
+ .string()
45
+ .optional()
46
+ .describe("Subdirectory under the project root to scan (e.g. 'Code', 'Code/Player'). Defaults to 'Code'"),
47
+ }, async (params) => {
48
+ const res = await bridge.send("sandbox_lint", params);
49
+ if (!res.success)
50
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
51
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
52
+ });
53
+ // ── razor_lint ────────────────────────────────────────────────────
54
+ server.tool("razor_lint", "Static-scan .razor and .razor.scss files for the silent footguns that crash the Razor transpiler or stylesheet engine with no useful error message: switch expressions inside @code blocks (use if/else instead), non-ASCII/emoji inside @code (move to markup or a string constant), PanelComponent subclasses missing a BuildHash override (panel never re-renders), and root uppercase type-selector rules in .razor.scss (silently skipped -- use a class selector like .my-panel). Returns { scanned, findings: [{file, line, match, advice}], clean } matching the sandbox_lint shape.", {
55
+ directory: z
56
+ .string()
57
+ .optional()
58
+ .describe("Subdirectory under the project root to scan (e.g. 'UI', 'Code'). Defaults to 'Code'"),
59
+ }, async (params) => {
60
+ const res = await bridge.send("razor_lint", params);
61
+ if (!res.success)
62
+ return { content: [{ type: "text", text: `Error: ${res.error}` }] };
63
+ return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
64
+ });
41
65
  // ── scene_validate ────────────────────────────────────────────────
42
66
  server.tool("scene_validate", "Validate the active scene for the silent setup footguns that break controllers/physics/cameras: no CameraComponent, no player controller, multiple root Rigidbodies, a Rigidbody with MotionEnabled=false fighting a kinematic root, IsTrigger colliders that Scene.Trace will ignore, child Rigidbodies breaking collider binding, and missing required child anchors. Returns each issue with the GameObject and the exact fix.", {}, async () => {
43
67
  const res = await bridge.send("scene_validate", {});
@@ -92,7 +116,7 @@ export function registerInspectionTools(server, bridge) {
92
116
  z.string().describe('Comma string "x,y,z", e.g. "1,0,0"'),
93
117
  ])
94
118
  .optional()
95
- .describe('AnalogMove vector to apply (forward/left) object {x,y,z} or comma string "x,y,z"'),
119
+ .describe('AnalogMove vector to apply (forward/left) -- object {x,y,z} or comma string "x,y,z"'),
96
120
  analogLook: z
97
121
  .object({ pitch: z.number(), yaw: z.number(), roll: z.number().default(0) })
98
122
  .optional()
package/package.json CHANGED
@@ -1,55 +1,55 @@
1
- {
2
- "name": "sbox-mcp-server",
3
- "version": "1.11.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": "AGPL-3.0-or-later"
55
- }
1
+ {
2
+ "name": "sbox-mcp-server",
3
+ "version": "1.13.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": "AGPL-3.0-or-later"
55
+ }