sbox-mcp-server 1.16.0 → 1.17.1
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 +2 -0
- package/dist/tools/playtest.d.ts +16 -0
- package/dist/tools/playtest.js +39 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -47,6 +47,7 @@ import { registerInspectionTools } from "./tools/inspection.js";
|
|
|
47
47
|
import { registerInputTools } from "./tools/inputs.js";
|
|
48
48
|
import { registerDebugVizTools } from "./tools/debugviz.js";
|
|
49
49
|
import { registerDebugDrawTools } from "./tools/debugdraw.js";
|
|
50
|
+
import { registerPlaytestTools } from "./tools/playtest.js";
|
|
50
51
|
// ── CLI flags ──────────────────────────────────────────────────────
|
|
51
52
|
const args = process.argv.slice(2);
|
|
52
53
|
/** Read the package version from package.json, or return "unknown" on failure. */
|
|
@@ -197,6 +198,7 @@ registerInspectionTools(server, bridge);
|
|
|
197
198
|
registerInputTools(server, bridge);
|
|
198
199
|
registerDebugVizTools(server, bridge);
|
|
199
200
|
registerDebugDrawTools(server, bridge);
|
|
201
|
+
registerPlaytestTools(server, bridge);
|
|
200
202
|
/** Start the MCP server on stdio and attempt initial Bridge connection. */
|
|
201
203
|
async function main() {
|
|
202
204
|
const transport = new StdioServerTransport();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BridgeClient } from "../transport/bridge-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* playtest / playtest_status — the gameplay-VERIFICATION harness.
|
|
5
|
+
*
|
|
6
|
+
* Runs a scripted step list inside the editor frame loop (async, like drive_player)
|
|
7
|
+
* so input, state reads, and assertions time-align with the running game. This is the
|
|
8
|
+
* only way to verify a *playable loop* (not just a static scene): TS round-trips can't
|
|
9
|
+
* catch transient state (e.g. a jump's airborne frame). The job auto-disables the
|
|
10
|
+
* controller's input read for `move` steps, zeros WishVelocity between steps, releases
|
|
11
|
+
* held actions, and restores everything on teardown.
|
|
12
|
+
*
|
|
13
|
+
* Requires play mode (start_play first). One playtest at a time — poll playtest_status.
|
|
14
|
+
*/
|
|
15
|
+
export declare function registerPlaytestTools(server: McpServer, bridge: BridgeClient): void;
|
|
16
|
+
//# sourceMappingURL=playtest.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* playtest / playtest_status — the gameplay-VERIFICATION harness.
|
|
4
|
+
*
|
|
5
|
+
* Runs a scripted step list inside the editor frame loop (async, like drive_player)
|
|
6
|
+
* so input, state reads, and assertions time-align with the running game. This is the
|
|
7
|
+
* only way to verify a *playable loop* (not just a static scene): TS round-trips can't
|
|
8
|
+
* catch transient state (e.g. a jump's airborne frame). The job auto-disables the
|
|
9
|
+
* controller's input read for `move` steps, zeros WishVelocity between steps, releases
|
|
10
|
+
* held actions, and restores everything on teardown.
|
|
11
|
+
*
|
|
12
|
+
* Requires play mode (start_play first). One playtest at a time — poll playtest_status.
|
|
13
|
+
*/
|
|
14
|
+
export function registerPlaytestTools(server, bridge) {
|
|
15
|
+
const reply = (res) => res.success
|
|
16
|
+
? { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }
|
|
17
|
+
: { content: [{ type: "text", text: `Error: ${res.error}` }] };
|
|
18
|
+
server.tool("playtest", [
|
|
19
|
+
"Run a scripted gameplay-verification sequence in PLAY MODE and get a pass/fail transcript (start_play first; poll playtest_status until finished).",
|
|
20
|
+
"Each step is an object with ONE verb:",
|
|
21
|
+
'• { "move": {"x":1,"y":0}, "frames":60 } — analog move in the controller frame (x=fwd/back, y=left/right); auto-sets UseInputControls=false and zeroes WishVelocity after. Movement is controller-specific/best-effort.',
|
|
22
|
+
'• { "look": {"pitch":0,"yaw":90,"roll":0} } / { "lookDelta": {"yaw":2}, "frames":30 } — set/sweep EyeAngles.',
|
|
23
|
+
'• { "action": "use", "frames":20 } — hold a named input action down (rising-edge safe; for use/dig/attack handled by gameplay components).',
|
|
24
|
+
'• { "jump": "0,0,400" } — invoke the controller\'s Jump(velocity).',
|
|
25
|
+
'• { "set": {"component":"PlayerController","property":"WorldPosition","to":"100,0,0"} } — set a runtime property (toggles, or teleport via WorldPosition — the robust positioning fallback).',
|
|
26
|
+
'• { "wait": 10 } — advance N frames.',
|
|
27
|
+
'• { "capture": "after-jump" } — screenshot the live player-POV camera at this frame; the PNG path is recorded in the transcript (diagnostic, never pass/fail). Pass true for no label.',
|
|
28
|
+
'• { "assert": {"read":"Displacement","op":">","value":50,"desc":"moved >50u"} } — read a value and compare IN-FRAME. read = "Displacement" (scalar distance moved from job start — the clean facing-independent movement proof), "WorldPosition[.x|.y|.z]", or "<Component>.<Property>[.x|.y|.z|.Count]". op = > < >= <= == != changed. Records pass/fail.',
|
|
29
|
+
"Tip: prove movement with read:'Displacement' op:'>' (facing-independent, unambiguous), and catch transient state (IsAirborne) right after the action.",
|
|
30
|
+
].join("\n"), {
|
|
31
|
+
steps: z
|
|
32
|
+
.array(z.record(z.string(), z.any()))
|
|
33
|
+
.describe("Ordered step objects (see verbs above). Runs top-to-bottom in the frame loop."),
|
|
34
|
+
id: z.string().optional().describe("GUID of the player/controller GameObject. Omit to auto-resolve the first PlayerController."),
|
|
35
|
+
component: z.string().optional().describe("Controller component type to target (e.g. 'PlayerController'). Omit to auto-detect."),
|
|
36
|
+
}, async (p) => reply(await bridge.send("playtest", p)));
|
|
37
|
+
server.tool("playtest_status", "Poll the running/finished playtest. While running: { active:true, step, totalSteps, passed, failed }. When done: { finished:true, verdict:'PASS'|'FAIL', passed, failed, transcript:[...] } — the full per-step pass/fail record.", {}, async () => reply(await bridge.send("playtest_status", {})));
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=playtest.js.map
|