@el4cteo/rbx-studio-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +203 -0
  3. package/dist/bridge/rpc.js +243 -0
  4. package/dist/bridge/rpc.js.map +1 -0
  5. package/dist/bridge/server.js +281 -0
  6. package/dist/bridge/server.js.map +1 -0
  7. package/dist/index.js +104 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/lib/apidump.js +269 -0
  10. package/dist/lib/apidump.js.map +1 -0
  11. package/dist/lib/errors.js +38 -0
  12. package/dist/lib/errors.js.map +1 -0
  13. package/dist/lib/format.js +191 -0
  14. package/dist/lib/format.js.map +1 -0
  15. package/dist/lib/pluginbuild.js +83 -0
  16. package/dist/lib/pluginbuild.js.map +1 -0
  17. package/dist/lib/png.js +84 -0
  18. package/dist/lib/png.js.map +1 -0
  19. package/dist/lib/protocol.js +22 -0
  20. package/dist/lib/protocol.js.map +1 -0
  21. package/dist/lib/tool.js +27 -0
  22. package/dist/lib/tool.js.map +1 -0
  23. package/dist/resources.js +70 -0
  24. package/dist/resources.js.map +1 -0
  25. package/dist/tools/api.js +78 -0
  26. package/dist/tools/api.js.map +1 -0
  27. package/dist/tools/character.js +94 -0
  28. package/dist/tools/character.js.map +1 -0
  29. package/dist/tools/debug.js +211 -0
  30. package/dist/tools/debug.js.map +1 -0
  31. package/dist/tools/device.js +74 -0
  32. package/dist/tools/device.js.map +1 -0
  33. package/dist/tools/discover.js +217 -0
  34. package/dist/tools/discover.js.map +1 -0
  35. package/dist/tools/exec.js +191 -0
  36. package/dist/tools/exec.js.map +1 -0
  37. package/dist/tools/input.js +96 -0
  38. package/dist/tools/input.js.map +1 -0
  39. package/dist/tools/instances.js +261 -0
  40. package/dist/tools/instances.js.map +1 -0
  41. package/dist/tools/perf.js +367 -0
  42. package/dist/tools/perf.js.map +1 -0
  43. package/dist/tools/playtest.js +153 -0
  44. package/dist/tools/playtest.js.map +1 -0
  45. package/dist/tools/screenshot.js +75 -0
  46. package/dist/tools/screenshot.js.map +1 -0
  47. package/dist/tools/scripts.js +316 -0
  48. package/dist/tools/scripts.js.map +1 -0
  49. package/dist/tools/session.js +152 -0
  50. package/dist/tools/session.js.map +1 -0
  51. package/dist/tools/world.js +281 -0
  52. package/dist/tools/world.js.map +1 -0
  53. package/package.json +62 -0
  54. package/plugin/default.project.json +6 -0
  55. package/plugin/src/Config.luau +59 -0
  56. package/plugin/src/Console.luau +657 -0
  57. package/plugin/src/Context.luau +35 -0
  58. package/plugin/src/Dispatch.luau +90 -0
  59. package/plugin/src/Editor.luau +142 -0
  60. package/plugin/src/Emulation.luau +151 -0
  61. package/plugin/src/LogBuffer.luau +277 -0
  62. package/plugin/src/Net.luau +102 -0
  63. package/plugin/src/Paths.luau +255 -0
  64. package/plugin/src/Phrase.luau +465 -0
  65. package/plugin/src/Png.luau +238 -0
  66. package/plugin/src/Scope.luau +78 -0
  67. package/plugin/src/ScriptEdit.luau +100 -0
  68. package/plugin/src/Serialize.luau +287 -0
  69. package/plugin/src/TextEdit.luau +296 -0
  70. package/plugin/src/Transport.luau +328 -0
  71. package/plugin/src/Undo.luau +72 -0
  72. package/plugin/src/Visuals.luau +710 -0
  73. package/plugin/src/handlers/Api.luau +242 -0
  74. package/plugin/src/handlers/Assets.luau +145 -0
  75. package/plugin/src/handlers/Capture.luau +187 -0
  76. package/plugin/src/handlers/Character.luau +361 -0
  77. package/plugin/src/handlers/Debug.luau +391 -0
  78. package/plugin/src/handlers/Device.luau +119 -0
  79. package/plugin/src/handlers/Discover.luau +289 -0
  80. package/plugin/src/handlers/Exec.luau +270 -0
  81. package/plugin/src/handlers/Geometry.luau +261 -0
  82. package/plugin/src/handlers/Input.luau +287 -0
  83. package/plugin/src/handlers/Instances.luau +389 -0
  84. package/plugin/src/handlers/Perf.luau +645 -0
  85. package/plugin/src/handlers/Playtest.luau +205 -0
  86. package/plugin/src/handlers/Scripts.luau +387 -0
  87. package/plugin/src/handlers/Session.luau +168 -0
  88. package/plugin/src/handlers/Viewport.luau +302 -0
  89. package/plugin/src/handlers/World.luau +176 -0
  90. package/plugin/src/init.server.luau +317 -0
  91. package/scripts/build-plugin.mjs +157 -0
  92. package/scripts/check-plugin.mjs +97 -0
  93. package/scripts/install-plugin.mjs +39 -0
  94. package/scripts/latency.mjs +201 -0
  95. package/scripts/locate-luau.mjs +51 -0
  96. package/scripts/sourcemap.mjs +58 -0
  97. package/scripts/test-plugin.mjs +82 -0
@@ -0,0 +1,84 @@
1
+ import { deflateSync } from "node:zlib";
2
+ /**
3
+ * Writes a PNG from raw RGB bytes.
4
+ *
5
+ * The plugin used to do this itself, and could only do it badly. Studio has no
6
+ * deflate, so the Luau encoder emitted zlib *stored* blocks -- the format's
7
+ * escape hatch for "compression not applied" -- which meant every screenshot
8
+ * travelled and landed at full uncompressed size. For a screen of mostly flat
9
+ * colour that is several times larger than it needs to be, and it is spent
10
+ * twice: once over the bridge and again as base64 in the conversation.
11
+ *
12
+ * Node has real zlib. So the plugin now sends pixels and this writes the file,
13
+ * which is the right division: the side with the compressor does the
14
+ * compressing.
15
+ */
16
+ const CRC_TABLE = (() => {
17
+ const table = new Int32Array(256);
18
+ for (let index = 0; index < 256; index += 1) {
19
+ let value = index;
20
+ for (let bit = 0; bit < 8; bit += 1) {
21
+ // 0xEDB88320: the reversed CRC-32 polynomial. Worth writing out, because
22
+ // a single wrong digit here (0xED888320) still produces a table, still
23
+ // produces a checksum, and still produces a PNG that every decoder
24
+ // rejects — with nothing in the file to say which byte was wrong.
25
+ value = value & 1 ? 0xedb8_8320 ^ (value >>> 1) : value >>> 1;
26
+ }
27
+ table[index] = value;
28
+ }
29
+ return table;
30
+ })();
31
+ function crc32(data) {
32
+ let crc = -1;
33
+ for (const byte of data) {
34
+ crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
35
+ }
36
+ return (crc ^ -1) >>> 0;
37
+ }
38
+ /** One PNG chunk: length, type, payload, CRC over type+payload. */
39
+ function chunk(kind, payload) {
40
+ const length = Buffer.alloc(4);
41
+ length.writeUInt32BE(payload.length, 0);
42
+ const body = Buffer.concat([Buffer.from(kind, "ascii"), payload]);
43
+ const crc = Buffer.alloc(4);
44
+ crc.writeUInt32BE(crc32(body), 0);
45
+ return Buffer.concat([length, body, crc]);
46
+ }
47
+ const SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
48
+ /**
49
+ * Encodes `width` x `height` RGB triples as a PNG.
50
+ *
51
+ * Filter type 0 (None) on every scanline. The adaptive filters PNG allows would
52
+ * compress better, but choosing between them well is its own problem, and
53
+ * deflate over unfiltered rows already recovers almost all of what the old
54
+ * stored-block encoder was throwing away.
55
+ */
56
+ export function encodePng(rgb, width, height) {
57
+ const stride = width * 3;
58
+ const expected = stride * height;
59
+ if (rgb.length < expected) {
60
+ throw new Error(`pixel data is ${rgb.length} bytes, short of the ${expected} needed for ${width}x${height}`);
61
+ }
62
+ // One leading filter byte per row is what separates the raw pixels from a
63
+ // PNG's idea of a scanline.
64
+ const raw = Buffer.alloc((stride + 1) * height);
65
+ for (let row = 0; row < height; row += 1) {
66
+ raw[row * (stride + 1)] = 0;
67
+ rgb.copy(raw, row * (stride + 1) + 1, row * stride, row * stride + stride);
68
+ }
69
+ const header = Buffer.alloc(13);
70
+ header.writeUInt32BE(width, 0);
71
+ header.writeUInt32BE(height, 4);
72
+ header[8] = 8; // bit depth
73
+ header[9] = 2; // colour type 2: truecolour RGB
74
+ header[10] = 0; // deflate
75
+ header[11] = 0; // adaptive filtering
76
+ header[12] = 0; // no interlace
77
+ return Buffer.concat([
78
+ SIGNATURE,
79
+ chunk("IHDR", header),
80
+ chunk("IDAT", deflateSync(raw, { level: 9 })),
81
+ chunk("IEND", Buffer.alloc(0)),
82
+ ]);
83
+ }
84
+ //# sourceMappingURL=png.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"png.js","sourceRoot":"","sources":["../../src/lib/png.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Wire protocol shared between the Node bridge and the Studio plugin.
3
+ *
4
+ * Transport is deliberately dumb: the server pushes `Command` frames down an
5
+ * SSE stream (or hands them out via long-poll when SSE is unavailable) and the
6
+ * plugin POSTs a matching `CommandResult` back to /result. Every frame is
7
+ * correlated by `id`, so both transports behave identically from the tool side.
8
+ */
9
+ /**
10
+ * Bumped whenever `Command`/`CommandResult` shapes change incompatibly. The
11
+ * plugin sends its own value at handshake so we can tell the user to update
12
+ * rather than failing with a confusing schema error later.
13
+ */
14
+ export const PROTOCOL_VERSION = 1;
15
+ /**
16
+ * Custom header every plugin request must carry. Browsers cannot set it on a
17
+ * cross-origin request without a preflight the bridge never answers, which is
18
+ * what stops a malicious web page from driving the user's Studio via DNS
19
+ * rebinding. Cheaper and less fragile than a token the user has to copy.
20
+ */
21
+ export const CLIENT_HEADER = "x-roblox-studio-mcp";
22
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/lib/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAqDH;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAElC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,qBAAqB,CAAC"}
@@ -0,0 +1,27 @@
1
+ import { toToolError } from "./errors.js";
2
+ import { errorText } from "./format.js";
3
+ export function defineTool(context, spec, handler) {
4
+ const readOnly = spec.readOnly ?? false;
5
+ context.server.registerTool(spec.name, {
6
+ title: spec.title,
7
+ description: spec.description,
8
+ inputSchema: spec.inputSchema,
9
+ annotations: {
10
+ title: spec.title,
11
+ readOnlyHint: readOnly,
12
+ destructiveHint: spec.destructive ?? !readOnly,
13
+ idempotentHint: spec.idempotent ?? readOnly,
14
+ // Studio is a live external process whose state we do not control.
15
+ openWorldHint: true,
16
+ },
17
+ }, (async (args) => {
18
+ try {
19
+ return await handler(args);
20
+ }
21
+ catch (cause) {
22
+ const error = toToolError(cause);
23
+ return errorText(`[${error.code}] ${error.message}`);
24
+ }
25
+ }));
26
+ }
27
+ //# sourceMappingURL=tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool.js","sourceRoot":"","sources":["../../src/lib/tool.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAmB,MAAM,aAAa,CAAC;AAwCzD,MAAM,UAAU,UAAU,CACxB,OAAoB,EACpB,IAAqB,EACrB,OAAuD;IAEvD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,KAAK,CAAC;IACxC,OAAO,CAAC,MAAM,CAAC,YAAY,CACzB,IAAI,CAAC,IAAI,EACT;QACE,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,WAAW,EAAE;YACX,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,YAAY,EAAE,QAAQ;YACtB,eAAe,EAAE,IAAI,CAAC,WAAW,IAAI,CAAC,QAAQ;YAC9C,cAAc,EAAE,IAAI,CAAC,UAAU,IAAI,QAAQ;YAC3C,mEAAmE;YACnE,aAAa,EAAE,IAAI;SACpB;KACF,EACD,CAAC,KAAK,EAAE,IAAqB,EAAE,EAAE;QAC/B,IAAI,CAAC;YACH,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;QAC7B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;YACjC,OAAO,SAAS,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACvD,CAAC;IACH,CAAC,CAAU,CACZ,CAAC;AACJ,CAAC"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * MCP resources: place state an agent can read without spending a tool call.
3
+ *
4
+ * Tools cost schema tokens in every request, whether or not they are used.
5
+ * Resources cost nothing until read, which makes them the right home for the
6
+ * three questions asked at the start of almost every session — what is
7
+ * connected, what is in the place, what is selected — and lets a client attach
8
+ * them as context rather than making the model ask.
9
+ *
10
+ * Everything here is read-only by construction: resources have no parameters to
11
+ * validate and no way to express a mutation, which is exactly why the state
12
+ * queries belong here and the actions do not.
13
+ */
14
+ export function registerResources(context) {
15
+ const { server, bridge } = context;
16
+ const readJson = async (op, params = {}) => {
17
+ // Resources have nowhere to put an error, so a failure is returned as the
18
+ // content itself rather than thrown: a client attaching this to a prompt
19
+ // should see why it is empty, not a blank block.
20
+ try {
21
+ const result = await bridge.call(op, params, {});
22
+ return JSON.stringify(result, null, 2);
23
+ }
24
+ catch (cause) {
25
+ const message = cause instanceof Error ? cause.message : String(cause);
26
+ return JSON.stringify({ unavailable: message }, null, 2);
27
+ }
28
+ };
29
+ server.registerResource("studio-status", "studio://status", {
30
+ title: "Studio session",
31
+ description: "Which place is open, whether it is playing, what is selected, and " +
32
+ "which scripts the user has on screen. The cheapest orientation there is.",
33
+ mimeType: "application/json",
34
+ }, async (uri) => ({
35
+ contents: [{ uri: uri.href, mimeType: "application/json", text: await readJson("studio.status") }],
36
+ }));
37
+ server.registerResource("studio-tree", "studio://tree", {
38
+ title: "Place hierarchy",
39
+ description: "The authored containers and what is directly inside them, two levels " +
40
+ "deep. A map of the place, without the ~120 engine services that would " +
41
+ "bury it.",
42
+ mimeType: "application/json",
43
+ }, async (uri) => ({
44
+ contents: [
45
+ {
46
+ uri: uri.href,
47
+ mimeType: "application/json",
48
+ // Two levels and a modest cap: this is orientation, not an inventory,
49
+ // and a resource that dumps a large place would cost more than the
50
+ // tool call it saves.
51
+ text: await readJson("discover.tree", { depth: 2, limit: 150, detail: "concise" }),
52
+ },
53
+ ],
54
+ }));
55
+ server.registerResource("studio-console", "studio://console", {
56
+ title: "Recent output",
57
+ description: "The last 100 lines Studio printed, warnings and errors included. " +
58
+ "Reading this after a playtest is usually the first useful thing to do.",
59
+ mimeType: "application/json",
60
+ }, async (uri) => ({
61
+ contents: [
62
+ {
63
+ uri: uri.href,
64
+ mimeType: "application/json",
65
+ text: await readJson("perf.console", { limit: 100 }),
66
+ },
67
+ ],
68
+ }));
69
+ }
70
+ //# sourceMappingURL=resources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resources.js","sourceRoot":"","sources":["../src/resources.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAoB;IACpD,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAEnC,MAAM,QAAQ,GAAG,KAAK,EAAE,EAAU,EAAE,MAAM,GAA4B,EAAE,EAAmB,EAAE;QAC3F,0EAA0E;QAC1E,yEAAyE;QACzE,iDAAiD;QACjD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,CAAU,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;YAC1D,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QACzC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvE,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,CAAC,gBAAgB,CACrB,eAAe,EACf,iBAAiB,EACjB;QACE,KAAK,EAAE,gBAAgB;QACvB,WAAW,EACT,oEAAoE;YACpE,0EAA0E;QAC5E,QAAQ,EAAE,kBAAkB;KAC7B,EACD,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;QACd,QAAQ,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC;KACnG,CAAC,CACH,CAAC;IAEF,MAAM,CAAC,gBAAgB,CACrB,aAAa,EACb,eAAe,EACf;QACE,KAAK,EAAE,iBAAiB;QACxB,WAAW,EACT,uEAAuE;YACvE,wEAAwE;YACxE,UAAU;QACZ,QAAQ,EAAE,kBAAkB;KAC7B,EACD,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;QACd,QAAQ,EAAE;YACR;gBACE,GAAG,EAAE,GAAG,CAAC,IAAI;gBACb,QAAQ,EAAE,kBAAkB;gBAC5B,sEAAsE;gBACtE,mEAAmE;gBACnE,sBAAsB;gBACtB,IAAI,EAAE,MAAM,QAAQ,CAAC,eAAe,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;aACnF;SACF;KACF,CAAC,CACH,CAAC;IAEF,MAAM,CAAC,gBAAgB,CACrB,gBAAgB,EAChB,kBAAkB,EAClB;QACE,KAAK,EAAE,eAAe;QACtB,WAAW,EACT,mEAAmE;YACnE,wEAAwE;QAC1E,QAAQ,EAAE,kBAAkB;KAC7B,EACD,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;QACd,QAAQ,EAAE;YACR;gBACE,GAAG,EAAE,GAAG,CAAC,IAAI;gBACb,QAAQ,EAAE,kBAAkB;gBAC5B,IAAI,EAAE,MAAM,QAAQ,CAAC,cAAc,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;aACrD;SACF;KACF,CAAC,CACH,CAAC;AACJ,CAAC"}
@@ -0,0 +1,78 @@
1
+ import { z } from "zod";
2
+ import { json, text } from "../lib/format.js";
3
+ import { defineTool } from "../lib/tool.js";
4
+ export function registerApiTools(context) {
5
+ const { bridge } = context;
6
+ defineTool(context, {
7
+ name: "api",
8
+ title: "What a class can do",
9
+ description: "Lists the properties, methods and events of any Roblox class, read " +
10
+ "from the engine that is running.\n\n" +
11
+ "Use it before writing Luau against a class you are not certain of. " +
12
+ "Guessing a method name costs a runtime error and a round trip; this " +
13
+ "costs one call and is never out of date, because the answer comes from " +
14
+ "the running binary rather than from a published dump or from training " +
15
+ "data. That matters most for exactly the classes worth checking — new " +
16
+ "ones, and ones that changed recently.\n\n" +
17
+ "Members come back as signatures rather than bare names — " +
18
+ "`AddAccessory(accessory: Instance)`, `HoldDuration: number` — because a " +
19
+ "name tells you something exists and a signature tells you how to call " +
20
+ "it, which is the actual question.\n\n" +
21
+ "`describe` takes a class name and gives the members it declares itself, " +
22
+ "counting the inherited ones separately. `classes` searches class names, " +
23
+ "which is how to find one whose exact spelling you do not have.\n\n" +
24
+ "Deprecated members are never listed, only counted — `Instance` has " +
25
+ "eight, including `clone`, `remove` and `getChildren`. They still run, " +
26
+ "so picking one from a list gives you working code and a deprecation " +
27
+ "warning in the user's output.\n\n" +
28
+ "This is not the same as `inspect`. `inspect` reads the values on an " +
29
+ "instance that exists; this reads the shape of a class whether or not " +
30
+ "anything in the place is one — which is what you need when deciding " +
31
+ "what to create in the first place.",
32
+ inputSchema: {
33
+ op: z
34
+ .enum(["describe", "classes"])
35
+ .default("describe")
36
+ .describe("'describe' details one class, 'classes' searches class names."),
37
+ className: z
38
+ .string()
39
+ .optional()
40
+ .describe('describe only: the class, e.g. "TweenService", "ProximityPrompt", "Humanoid". Case-sensitive.'),
41
+ contains: z
42
+ .string()
43
+ .optional()
44
+ .describe('classes only: substring to match, case-insensitive, e.g. "constraint" or "gui". Omit to list everything.'),
45
+ include: z
46
+ .array(z.enum(["properties", "methods", "events"]))
47
+ .optional()
48
+ .describe("describe only: which member kinds to return. Defaults to all " +
49
+ "three; narrow it when you only need one and the class is large."),
50
+ inherited: z
51
+ .boolean()
52
+ .default(false)
53
+ .describe("describe only: include members inherited from Instance and Object. " +
54
+ "Off by default because they swamp the answer — ProximityPrompt has " +
55
+ "2 methods of its own and 42 inherited, and the two you want are not " +
56
+ "the ones you already know. The inherited count is reported either way."),
57
+ studioId: z.string().optional().describe("Target Studio; omit for the active one."),
58
+ },
59
+ readOnly: true,
60
+ }, async (args) => {
61
+ if (args.op === "classes") {
62
+ const response = await bridge.call("api.classes", { contains: args.contains }, { studioId: args.studioId });
63
+ if (response.matched === 0) {
64
+ return text(`No class name contains ${JSON.stringify(args.contains ?? "")}.\n` +
65
+ "The match is a plain substring, case-insensitive — try a shorter one.");
66
+ }
67
+ return json(response.classes, response.truncated
68
+ ? `${response.matched} matched; ${response.truncated} not shown. Narrow with \`contains\`.`
69
+ : `${response.matched} matched.`);
70
+ }
71
+ if (args.className === undefined || args.className === "") {
72
+ return text('describe needs a `className`. Use `op: "classes"` to search for one.');
73
+ }
74
+ const response = await bridge.call("api.describe", { className: args.className, include: args.include, inherited: args.inherited }, { studioId: args.studioId });
75
+ return json(response);
76
+ });
77
+ }
78
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/tools/api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ import { z } from "zod";
2
+ import { json } from "../lib/format.js";
3
+ import { defineTool } from "../lib/tool.js";
4
+ export function registerCharacterTools(context) {
5
+ const { bridge } = context;
6
+ defineTool(context, {
7
+ name: "character",
8
+ title: "Drive the player during a playtest",
9
+ description: "Moves and acts as the player character in a running playtest, so " +
10
+ "gameplay can be tested without asking the user to play it.\n\n" +
11
+ "`moveTo` walks to a position or to an instance, following a path " +
12
+ "computed around walls and gaps rather than a straight line into them. " +
13
+ "It reports whether it ACTUALLY ARRIVED and how far short it stopped — " +
14
+ "a route blocked by something you did not know about otherwise looks " +
15
+ "identical to a successful walk.\n\n" +
16
+ "`act` does the one-shot things worth testing: jump, sit, stand, " +
17
+ "respawn, kill (to exercise the death and respawn path), teleport, and " +
18
+ "`equip`/`activate` to use a Tool — which is how combat gets tested, " +
19
+ "since Activate is exactly what a mouse click triggers. Note " +
20
+ "that teleport skips everything in between, so triggers and collisions " +
21
+ "along the route do not fire — walk if you are testing those.\n\n" +
22
+ "`state` reports position, health, walk speed and what the humanoid is " +
23
+ "doing. Call it before and after anything else here.\n\n" +
24
+ "This drives the Humanoid directly rather than simulating keystrokes, " +
25
+ "which is the right tool for going places: pathfinding around a wall is " +
26
+ "one call here and a sequence of guessed key presses otherwise. For " +
27
+ "anything bound to a control rather than to movement — does E open the " +
28
+ "door, does the sprint key work, does Escape close the menu — use " +
29
+ "`input`, which sends real key and mouse events.\n\n" +
30
+ "REQUIRES A RUNNING PLAYTEST, and the character lives in the playtest's " +
31
+ "data model — address these to the playtest's studioId from " +
32
+ "`list_studios`, not the editor's. Run mode has no character at all; " +
33
+ "use `playtest op=play`.",
34
+ inputSchema: {
35
+ op: z
36
+ .enum(["moveTo", "act", "state"])
37
+ .describe("'moveTo' walks somewhere, 'act' performs an action, 'state' only reports."),
38
+ to: z
39
+ .string()
40
+ .optional()
41
+ .describe('Target position, e.g. "25, 5, -10". Used by moveTo and by teleport.'),
42
+ path: z
43
+ .string()
44
+ .optional()
45
+ .describe("moveTo only: walk to this instance instead of a coordinate."),
46
+ direct: z
47
+ .boolean()
48
+ .default(false)
49
+ .describe("moveTo only: walk straight at the target without pathfinding. Use " +
50
+ "when a route is reported unreachable but you want to see what happens."),
51
+ canJump: z.boolean().default(true).describe("moveTo only: allow the path to include jumps."),
52
+ action: z
53
+ .enum([
54
+ "jump", "stop", "sit", "stand", "respawn", "kill", "teleport",
55
+ "equip", "activate", "unequip",
56
+ ])
57
+ .optional()
58
+ .describe("act only: what to do. 'equip' takes a Tool from the Backpack or " +
59
+ "StarterPack, 'activate' uses it (what a mouse click triggers)."),
60
+ tool: z.string().optional().describe("equip only: the Tool's name."),
61
+ player: z
62
+ .string()
63
+ .optional()
64
+ .describe("Which player, by name. Omit for the only one; needed in a multiplayer test."),
65
+ studioId: z
66
+ .string()
67
+ .optional()
68
+ .describe("The PLAYTEST session's id — not the editor's. See list_studios."),
69
+ },
70
+ destructive: false,
71
+ }, async (args) => {
72
+ if (args.op === "moveTo") {
73
+ const response = await bridge.call("character.moveTo", { to: args.to, path: args.path, direct: args.direct, canJump: args.canJump, player: args.player },
74
+ // Walking a long route is genuinely slow, and the handler waits for
75
+ // each waypoint rather than returning before it arrives.
76
+ { studioId: args.studioId, timeoutMs: 60_000 });
77
+ const notes = [];
78
+ if (response.note)
79
+ notes.push(response.note);
80
+ if (!response.arrived && response.pathStatus === "Enum.PathStatus.Success") {
81
+ notes.push("The path was valid but the character did not reach the end — something " +
82
+ "is physically in the way, or it fell. Take a `screenshot` to see where it stopped.");
83
+ }
84
+ return json(response, notes.length > 0 ? notes.join(" ") : undefined);
85
+ }
86
+ if (args.op === "act") {
87
+ const response = await bridge.call("character.act", { action: args.action ?? "jump", to: args.to, tool: args.tool, player: args.player }, { studioId: args.studioId });
88
+ return json(response);
89
+ }
90
+ const response = await bridge.call("character.state", { player: args.player }, { studioId: args.studioId });
91
+ return json(response);
92
+ });
93
+ }
94
+ //# sourceMappingURL=character.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"character.js","sourceRoot":"","sources":["../../src/tools/character.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,211 @@
1
+ import { z } from "zod";
2
+ import { json, text } from "../lib/format.js";
3
+ import { defineTool } from "../lib/tool.js";
4
+ /**
5
+ * Locals the debugger reports in every frame regardless of the code.
6
+ *
7
+ * `_G`, `shared` and `script` are in scope everywhere and `...` is the empty
8
+ * varargs tuple; none of them is ever the value someone set a breakpoint to see.
9
+ * Left in, they were four of the six rows in each frame -- so on a loop with two
10
+ * locals, two thirds of a capture was boilerplate repeated once per hit.
11
+ */
12
+ const AMBIENT_LOCALS = new Set(["_G", "shared", "script", "..."]);
13
+ /** Trims a debugger value string to something readable in a list. */
14
+ function briefly(value) {
15
+ const single = value.replace(/\s+/g, " ").trim();
16
+ return single.length > 160 ? `${single.slice(0, 159)}…` : single;
17
+ }
18
+ function frameLabel(frame) {
19
+ if (!frame) {
20
+ return "(unknown)";
21
+ }
22
+ const where = `${frame.ScriptPath ?? "?"}:${frame.Line ?? "?"}`;
23
+ return frame.Name ? `${where} in ${frame.Name}` : where;
24
+ }
25
+ /**
26
+ * Renders captured frames as a readable outline rather than raw JSON.
27
+ *
28
+ * The raw payload is Roblox's, and it is shaped for a debugger UI: variables are
29
+ * a numbered map rather than a list, the call stack is repeated both inside each
30
+ * frame and again alongside it, and every variable carries a reference id the
31
+ * caller has no way to follow. Passed straight through, nine hits of a two-line
32
+ * loop came back as roughly eight thousand tokens. This keeps what a person
33
+ * reading the capture is actually after -- where it stopped, and what the values
34
+ * were -- and drops the rest.
35
+ */
36
+ function renderSnapshots(items) {
37
+ const blocks = [];
38
+ let droppedAmbient = false;
39
+ items.forEach((item, index) => {
40
+ const frames = (item.frames ?? []);
41
+ const stack = item.stack;
42
+ const reason = item.stopped?.Reason;
43
+ const lines = [];
44
+ const head = frameLabel(frames[0]?.frame);
45
+ const depth = stack?.TotalFrames ?? frames.length;
46
+ lines.push(`#${index + 1} ${head}${depth > 1 ? ` (${depth} frames)` : ""}` +
47
+ (reason && !reason.endsWith("Breakpoint") ? ` [${reason}]` : ""));
48
+ for (const entry of frames) {
49
+ if (frames.length > 1) {
50
+ lines.push(` ${frameLabel(entry.frame)}`);
51
+ }
52
+ const variables = Object.values(entry.variables ?? {});
53
+ const shown = variables.filter((variable) => {
54
+ if (variable.Name !== undefined && AMBIENT_LOCALS.has(variable.Name)) {
55
+ droppedAmbient = true;
56
+ return false;
57
+ }
58
+ return true;
59
+ });
60
+ if (shown.length === 0) {
61
+ lines.push(" (no locals in scope)");
62
+ continue;
63
+ }
64
+ for (const variable of shown) {
65
+ const type = variable.Type ? ` : ${variable.Type}` : "";
66
+ lines.push(` ${variable.Name ?? "?"} = ${briefly(variable.Value ?? "nil")}${type}`);
67
+ }
68
+ }
69
+ // Frames below the top one, when the capture only carried the stack for them.
70
+ if (stack?.Frames && frames.length <= 1 && (stack.TotalFrames ?? 0) > 1) {
71
+ const rest = Object.values(stack.Frames).slice(1);
72
+ for (const frame of rest) {
73
+ lines.push(` called from ${frameLabel(frame)}`);
74
+ }
75
+ }
76
+ blocks.push(lines.join("\n"));
77
+ });
78
+ if (droppedAmbient) {
79
+ blocks.push("(_G, shared, script and ... are omitted -- they are in scope everywhere.)");
80
+ }
81
+ return blocks.join("\n\n");
82
+ }
83
+ export function registerDebugTools(context) {
84
+ const { bridge } = context;
85
+ defineTool(context, {
86
+ name: "debug",
87
+ title: "Breakpoints and runtime inspection",
88
+ description: "Sets breakpoints that record the stack and variables when they are hit, " +
89
+ "then reads back what they caught.\n\n" +
90
+ "These are tracepoints, not a step debugger. A breakpoint fires, captures " +
91
+ "the call stack and the variables in scope, and lets execution continue; " +
92
+ "`op: \"snapshots\"` returns what was captured. Studio's debugger has to " +
93
+ "decide whether to resume the instant it stops, and cannot wait for a " +
94
+ "tool call to come back with an answer, so stepping through code line by " +
95
+ "line is not possible this way — but 'what was this value when it got " +
96
+ "here' is, which is usually the actual question.\n\n" +
97
+ "`condition` is a Luau expression evaluated where the breakpoint sits, so " +
98
+ "a breakpoint can fire only on the case that matters — `health < 0`, " +
99
+ "`player.Name == \"someone\"`.\n\n" +
100
+ "`logMessage` is ALSO a Luau expression, not a template string: its value " +
101
+ 'is printed when the breakpoint is hit, so write `"index=" .. index` ' +
102
+ "rather than `index={index}`. Prose is a syntax error and the breakpoint " +
103
+ "is skipped. The engine prints it without stopping the thread at all, " +
104
+ "which makes it the cheapest way to watch a value change on a hot path " +
105
+ "or inside a tight loop — read the lines back with `console`.\n\n" +
106
+ "So the two kinds cost different things: a `logMessage` breakpoint never " +
107
+ "stops and gives you one line you composed in advance, while one without " +
108
+ "it stops briefly and gives you the whole frame — every local and its " +
109
+ "type, without having to guess beforehand which value would matter. " +
110
+ "Reach for the log when you know what to watch, the capture when you do " +
111
+ "not.\n\n" +
112
+ "Only one breakpoint exists per line, so the same line cannot both log " +
113
+ "and capture.\n\n" +
114
+ "Put the breakpoint on a line that does something. A `return`, an `end` " +
115
+ "or a bare declaration can verify and then never fire — measured, not " +
116
+ "guessed: the same breakpoint moved from `return squared, tag` to the " +
117
+ "assignment above it went from silent to firing on every pass. If one " +
118
+ "verifies but catches nothing, suspect the line before suspecting the " +
119
+ "condition.\n\n" +
120
+ "Breakpoints belong to the session that holds them. Set them in the " +
121
+ "editor session BEFORE starting a playtest, since code that already ran " +
122
+ "cannot be caught retroactively.\n\n" +
123
+ "Nothing here leaves a thread stopped waiting for you. A capture " +
124
+ "breakpoint stops for as long as it takes to read the frame and then " +
125
+ "resumes itself, so a script with one mid-loop still runs to its last " +
126
+ "line, and the user is never left with a frozen Studio to rescue.",
127
+ inputSchema: {
128
+ op: z
129
+ .enum(["set", "clear", "snapshots", "exceptions"])
130
+ .describe("'set' adds breakpoints, 'clear' removes one or all, 'snapshots' " +
131
+ "reads what has been captured, 'exceptions' controls breaking on errors."),
132
+ breakpoints: z
133
+ .array(z.object({
134
+ path: z.string().describe('Script path, e.g. "ServerScriptService.Combat".'),
135
+ line: z.number().int().min(1).describe("Line to break on."),
136
+ condition: z
137
+ .string()
138
+ .optional()
139
+ .describe("Luau expression; the breakpoint only fires when it is true, " +
140
+ 'evaluated in scope at that line, e.g. "count > 100".'),
141
+ logMessage: z
142
+ .string()
143
+ .optional()
144
+ .describe("Write this to the output when hit, instead of capturing a snapshot."),
145
+ }))
146
+ .max(50)
147
+ .optional()
148
+ .describe("set only: breakpoints to add."),
149
+ path: z
150
+ .string()
151
+ .optional()
152
+ .describe("clear only: remove breakpoints from this script. Omit to clear everything."),
153
+ line: z.number().int().min(1).optional().describe("clear only: which line to remove."),
154
+ mode: z
155
+ .enum(["Never", "Always", "Unhandled"])
156
+ .optional()
157
+ .describe("exceptions only: break on every error, only unhandled ones, or never. " +
158
+ "Defaults to Unhandled."),
159
+ limit: z
160
+ .number()
161
+ .int()
162
+ .min(1)
163
+ .max(40)
164
+ .default(10)
165
+ .describe("snapshots only: how many of the most recent to return."),
166
+ clear: z
167
+ .boolean()
168
+ .default(false)
169
+ .describe("snapshots only: discard what is returned, so the next read starts fresh."),
170
+ studioId: z.string().optional().describe("Target Studio; omit for the active one."),
171
+ },
172
+ destructive: false,
173
+ }, async (args) => {
174
+ if (args.op === "snapshots") {
175
+ const response = await bridge.call("debug.snapshots", { limit: args.limit, clear: args.clear }, { studioId: args.studioId });
176
+ if (response.items.length === 0) {
177
+ return text("No breakpoints have been hit.\n" +
178
+ "Breakpoints only catch code that runs after they are set, and they " +
179
+ "belong to one session — if the code runs in a playtest, set them " +
180
+ "before starting it and read them back from that playtest's studioId.");
181
+ }
182
+ const shown = response.items.length;
183
+ const footer = [
184
+ `${shown} of ${response.total} captured`,
185
+ response.overflow ? `${response.overflow} older snapshots were dropped` : undefined,
186
+ ]
187
+ .filter(Boolean)
188
+ .join(", ");
189
+ return text(`${renderSnapshots(response.items)}\n\n[${footer}]`);
190
+ }
191
+ if (args.op === "set") {
192
+ if (!args.breakpoints || args.breakpoints.length === 0) {
193
+ return text("set needs a `breakpoints` array.");
194
+ }
195
+ const response = await bridge.call("debug.set", { breakpoints: args.breakpoints }, { studioId: args.studioId });
196
+ const notes = [];
197
+ if (response.failed.length > 0) {
198
+ notes.push("Refused:\n" +
199
+ response.failed.map((f) => ` ${f.path}:${f.line} — ${f.error}`).join("\n"));
200
+ }
201
+ return json(response.added, notes.length > 0 ? notes.join("\n\n") : undefined);
202
+ }
203
+ if (args.op === "exceptions") {
204
+ const response = await bridge.call("debug.exceptions", { mode: args.mode ?? "Unhandled" }, { studioId: args.studioId });
205
+ return text(`Breaking on exceptions: ${response.mode}.`);
206
+ }
207
+ const response = await bridge.call("debug.clear", { path: args.path, line: args.line }, { studioId: args.studioId });
208
+ return json(response);
209
+ });
210
+ }
211
+ //# sourceMappingURL=debug.js.map