@el4cteo/rbx-studio-mcp 0.6.1 → 0.6.5

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 (47) hide show
  1. package/dist/index.js +4 -0
  2. package/dist/index.js.map +1 -1
  3. package/dist/tools/anim.js +159 -0
  4. package/dist/tools/anim.js.map +1 -0
  5. package/dist/tools/character.js +95 -5
  6. package/dist/tools/character.js.map +1 -1
  7. package/dist/tools/data.js +173 -0
  8. package/dist/tools/data.js.map +1 -0
  9. package/dist/tools/device.js +77 -7
  10. package/dist/tools/device.js.map +1 -1
  11. package/dist/tools/discover.js +80 -4
  12. package/dist/tools/discover.js.map +1 -1
  13. package/dist/tools/exec.js +48 -1
  14. package/dist/tools/exec.js.map +1 -1
  15. package/dist/tools/input.js +35 -9
  16. package/dist/tools/input.js.map +1 -1
  17. package/dist/tools/perf.js +74 -7
  18. package/dist/tools/perf.js.map +1 -1
  19. package/dist/tools/scripts.js +51 -3
  20. package/dist/tools/scripts.js.map +1 -1
  21. package/dist/tools/world.js +317 -44
  22. package/dist/tools/world.js.map +1 -1
  23. package/package.json +74 -74
  24. package/plugin/src/Commands.luau +622 -622
  25. package/plugin/src/Config.luau +65 -65
  26. package/plugin/src/Console.luau +1909 -1843
  27. package/plugin/src/Emulation.luau +172 -0
  28. package/plugin/src/Phrase.luau +164 -16
  29. package/plugin/src/Png.luau +8 -4
  30. package/plugin/src/Serialize.luau +499 -327
  31. package/plugin/src/Undo.luau +94 -6
  32. package/plugin/src/handlers/Anim.luau +897 -0
  33. package/plugin/src/handlers/Assets.luau +587 -352
  34. package/plugin/src/handlers/Capture.luau +155 -20
  35. package/plugin/src/handlers/Character.luau +823 -361
  36. package/plugin/src/handlers/Data.luau +539 -0
  37. package/plugin/src/handlers/Device.luau +394 -139
  38. package/plugin/src/handlers/Discover.luau +685 -363
  39. package/plugin/src/handlers/Geometry.luau +127 -0
  40. package/plugin/src/handlers/Perf.luau +227 -0
  41. package/plugin/src/handlers/Scripts.luau +673 -539
  42. package/plugin/src/handlers/Session.luau +3 -0
  43. package/plugin/src/handlers/Viewport.luau +268 -0
  44. package/plugin/src/handlers/World.luau +89 -15
  45. package/plugin/src/init.server.luau +879 -875
  46. package/scripts/build-plugin.mjs +20 -0
  47. package/scripts/check-plugin.mjs +171 -124
@@ -17,6 +17,19 @@ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
17
17
  const sourceDir = join(root, "plugin", "src");
18
18
  const outputPath = resolve(process.argv[2] ?? join(root, "build", "StudioMCP.rbxmx"));
19
19
 
20
+ /**
21
+ * The package's version, stamped into the plugin the same way the build id is.
22
+ *
23
+ * `Config.PLUGIN_VERSION` used to be a literal that someone had to remember to
24
+ * edit alongside package.json, and nothing checked. A release that bumped the
25
+ * package and not the literal shipped a plugin reporting the previous version
26
+ * in its own `version` and `status` output, in `list_studios`, and in the
27
+ * mismatch warnings the server prints when a plugin looks out of date -- which
28
+ * is the one place a wrong version number does real damage, because it is read
29
+ * as evidence about which build is running.
30
+ */
31
+ const packageVersion = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version;
32
+
20
33
  const NEWLINE = "\n";
21
34
 
22
35
  /**
@@ -121,6 +134,13 @@ function buildTree(dir, name) {
121
134
  // Stamped with the fingerprint of the pre-injection sources, which is
122
135
  // exactly what the server recomputes at runtime.
123
136
  source = source.replace('Config.BUILD_ID = "dev"', `Config.BUILD_ID = "${stamp}"`);
137
+ // The literal in the source is the fallback for anyone loading
138
+ // plugin/src directly through Rojo; a built plugin always carries the
139
+ // package's own version.
140
+ source = source.replace(
141
+ /Config\.PLUGIN_VERSION = "[^"]*"/,
142
+ `Config.PLUGIN_VERSION = "${packageVersion}"`,
143
+ );
124
144
  }
125
145
 
126
146
  if (entry.name === "init.server.luau") {
@@ -1,125 +1,172 @@
1
- /**
2
- * Compiles every plugin source file, so a broken one is caught here rather than
3
- * in Studio.
4
- *
5
- * This exists because the feedback loop without it is terrible. Nothing in the
6
- * Node build reads the Luau at all -- `build:plugin` packs the files into an
7
- * .rbxmx as text -- so a syntax error ships, installs, and is only discovered
8
- * when the user focuses Studio and the plugin fails to load. It cost two
9
- * sessions in one afternoon: a literal newline written into a string where an
10
- * escape was meant, and a closure that captured a nil global because it sat
11
- * above the forward declaration of the local it meant to call. The first is a
12
- * compile error and would have been caught instantly by this. The second is not,
13
- * which is why `--!strict` analysis runs too when the analyser is available:
14
- * an unknown global is exactly what it flags.
15
- *
16
- * Needs `luau-compile` (and ideally `luau-analyze`) on PATH, in ./tools, or
17
- * named by the LUAU_COMPILE / LUAU_ANALYZE environment variables. Get them from
18
- * https://github.com/luau-lang/luau/releases.
19
- *
20
- * Silent and exit 0 when everything compiles; prints what failed otherwise.
21
- *
22
- * Usage: node scripts/check-plugin.mjs
23
- */
24
- import { spawnSync } from "node:child_process";
25
- import { readdirSync, statSync } from "node:fs";
26
- import { dirname, join, resolve } from "node:path";
27
- import { fileURLToPath } from "node:url";
28
- import { locateLuau, missingLuau } from "./locate-luau.mjs";
29
-
30
- const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
31
-
32
- function luauFiles(directory) {
33
- const found = [];
34
- for (const entry of readdirSync(directory)) {
35
- const path = join(directory, entry);
36
- if (statSync(path).isDirectory()) found.push(...luauFiles(path));
37
- else if (entry.endsWith(".luau")) found.push(path);
38
- }
39
- return found;
40
- }
41
-
42
- const compiler = locateLuau("LUAU_COMPILE", ["luau-compile.exe", "luau-compile"]);
43
- if (compiler === null) {
44
- process.stderr.write(
45
- "No luau-compile found. Put it on PATH or in ./tools, or set LUAU_COMPILE.\n" +
46
- "Download: https://github.com/luau-lang/luau/releases\n",
47
- );
48
- process.exit(1);
49
- }
50
-
51
- const files = luauFiles(join(root, "plugin", "src"));
52
- const failures = [];
53
-
54
- for (const file of files) {
55
- // --binary throws the bytecode away; only the exit status and diagnostics
56
- // matter, and writing it anywhere would just be litter to clean up.
57
- const result = spawnSync(compiler, ["--binary", file], { encoding: "utf8" });
58
- const diagnostics = `${result.stderr ?? ""}${result.status === 0 ? "" : (result.stdout ?? "")}`.trim();
59
- if (result.status !== 0 || diagnostics.length > 0) {
60
- failures.push(`${file.slice(root.length + 1)}\n${diagnostics}`);
61
- }
62
- }
63
-
64
- /*
65
- * One diagnostic from the analyser, deliberately.
66
- *
67
- * `LocalShadow` is reported when a name is used as a global and a local of that
68
- * same name is declared later in the file -- which is the shape of the bug this
69
- * check was written for, and is unambiguous. Running the analyser without
70
- * Roblox's type definitions also reports every engine global as unknown, so
71
- * `script`, `task`, `Color3` and friends produce hundreds of lines of noise;
72
- * filtering to this one diagnostic gets the signal without needing a
73
- * definitions file that would then have to be kept current with the engine.
74
- */
75
- /*
76
- * Diagnostics about a table this file declares its own type for.
77
- *
78
- * The LocalShadow filter was the only thing let through, and that let a whole
79
- * class of error ship: a field read or written on a `--!strict` table type that
80
- * does not declare it. The plugin failed to load on the first line it logged --
81
- * "attempt to perform arithmetic on nil" -- because a batch of edits added five
82
- * uses of `state.entries` and the edit declaring it never landed. The analyser
83
- * had said so, four times, and this script threw it away.
84
- *
85
- * These two patterns are safe to surface where the rest is not. Without Roblox
86
- * type definitions the analyser cannot know what `Instance` or `Color3` are, so
87
- * it reports engine globals in their hundreds -- but those come out as unknown
88
- * *globals* and unknown *types*. A key missing from a named table type can only
89
- * be a table this file declared itself.
90
- */
91
- const TYPE_PATTERNS = [/Key '[^']+' not found in table/, /Cannot add property '[^']+' to table/];
92
-
93
- const analyser = locateLuau("LUAU_ANALYZE", ["luau-analyze.exe", "luau-analyze"]);
94
- const shadowed = [];
95
- const mistyped = [];
96
- if (analyser !== null) {
97
- for (const file of files) {
98
- const result = spawnSync(analyser, [file], { encoding: "utf8" });
99
- const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
100
- for (const line of output.split("\n")) {
101
- if (line.includes("LocalShadow:")) shadowed.push(line.trim());
102
- else if (TYPE_PATTERNS.some((pattern) => pattern.test(line))) mistyped.push(line.trim());
103
- }
104
- }
105
- }
106
-
107
- if (failures.length > 0) {
108
- process.stderr.write(`${failures.join("\n\n")}\n`);
109
- }
110
- if (shadowed.length > 0) {
111
- process.stderr.write(
112
- "\nA local is used before it is declared, so the call reaches a nil global " +
113
- "instead:\n" +
114
- `${shadowed.join("\n")}\n`,
115
- );
116
- }
117
- if (mistyped.length > 0) {
118
- process.stderr.write(
119
- "\nA field is used on a table whose type does not declare it. It is nil at " +
120
- "runtime, and the plugin fails the first time that line runs:\n" +
1
+ /**
2
+ * Compiles every plugin source file, so a broken one is caught here rather than
3
+ * in Studio.
4
+ *
5
+ * This exists because the feedback loop without it is terrible. Nothing in the
6
+ * Node build reads the Luau at all -- `build:plugin` packs the files into an
7
+ * .rbxmx as text -- so a syntax error ships, installs, and is only discovered
8
+ * when the user focuses Studio and the plugin fails to load. It cost two
9
+ * sessions in one afternoon: a literal newline written into a string where an
10
+ * escape was meant, and a closure that captured a nil global because it sat
11
+ * above the forward declaration of the local it meant to call. The first is a
12
+ * compile error and would have been caught instantly by this. The second is not,
13
+ * which is why `--!strict` analysis runs too when the analyser is available:
14
+ * an unknown global is exactly what it flags.
15
+ *
16
+ * Needs `luau-compile` (and ideally `luau-analyze`) on PATH, in ./tools, or
17
+ * named by the LUAU_COMPILE / LUAU_ANALYZE environment variables. Get them from
18
+ * https://github.com/luau-lang/luau/releases.
19
+ *
20
+ * Silent and exit 0 when everything compiles; prints what failed otherwise.
21
+ *
22
+ * Usage: node scripts/check-plugin.mjs
23
+ */
24
+ import { spawnSync } from "node:child_process";
25
+ import { readdirSync, readFileSync, statSync } from "node:fs";
26
+ import { dirname, join, resolve } from "node:path";
27
+ import { fileURLToPath } from "node:url";
28
+ import { locateLuau, missingLuau } from "./locate-luau.mjs";
29
+
30
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
31
+
32
+ function luauFiles(directory) {
33
+ const found = [];
34
+ for (const entry of readdirSync(directory)) {
35
+ const path = join(directory, entry);
36
+ if (statSync(path).isDirectory()) found.push(...luauFiles(path));
37
+ else if (entry.endsWith(".luau")) found.push(path);
38
+ }
39
+ return found;
40
+ }
41
+
42
+ const compiler = locateLuau("LUAU_COMPILE", ["luau-compile.exe", "luau-compile"]);
43
+ if (compiler === null) {
44
+ process.stderr.write(
45
+ "No luau-compile found. Put it on PATH or in ./tools, or set LUAU_COMPILE.\n" +
46
+ "Download: https://github.com/luau-lang/luau/releases\n",
47
+ );
48
+ process.exit(1);
49
+ }
50
+
51
+ const files = luauFiles(join(root, "plugin", "src"));
52
+ const failures = [];
53
+
54
+ for (const file of files) {
55
+ // --binary throws the bytecode away; only the exit status and diagnostics
56
+ // matter, and writing it anywhere would just be litter to clean up.
57
+ const result = spawnSync(compiler, ["--binary", file], { encoding: "utf8" });
58
+ const diagnostics = `${result.stderr ?? ""}${result.status === 0 ? "" : (result.stdout ?? "")}`.trim();
59
+ if (result.status !== 0 || diagnostics.length > 0) {
60
+ failures.push(`${file.slice(root.length + 1)}\n${diagnostics}`);
61
+ }
62
+ }
63
+
64
+ /*
65
+ * One diagnostic from the analyser, deliberately.
66
+ *
67
+ * `LocalShadow` is reported when a name is used as a global and a local of that
68
+ * same name is declared later in the file -- which is the shape of the bug this
69
+ * check was written for, and is unambiguous. Running the analyser without
70
+ * Roblox's type definitions also reports every engine global as unknown, so
71
+ * `script`, `task`, `Color3` and friends produce hundreds of lines of noise;
72
+ * filtering to this one diagnostic gets the signal without needing a
73
+ * definitions file that would then have to be kept current with the engine.
74
+ */
75
+ /*
76
+ * Diagnostics about a table this file declares its own type for.
77
+ *
78
+ * The LocalShadow filter was the only thing let through, and that let a whole
79
+ * class of error ship: a field read or written on a `--!strict` table type that
80
+ * does not declare it. The plugin failed to load on the first line it logged --
81
+ * "attempt to perform arithmetic on nil" -- because a batch of edits added five
82
+ * uses of `state.entries` and the edit declaring it never landed. The analyser
83
+ * had said so, four times, and this script threw it away.
84
+ *
85
+ * These two patterns are safe to surface where the rest is not. Without Roblox
86
+ * type definitions the analyser cannot know what `Instance` or `Color3` are, so
87
+ * it reports engine globals in their hundreds -- but those come out as unknown
88
+ * *globals* and unknown *types*. A key missing from a named table type can only
89
+ * be a table this file declared itself.
90
+ */
91
+ const TYPE_PATTERNS = [/Key '[^']+' not found in table/, /Cannot add property '[^']+' to table/];
92
+
93
+ const analyser = locateLuau("LUAU_ANALYZE", ["luau-analyze.exe", "luau-analyze"]);
94
+ const shadowed = [];
95
+ const mistyped = [];
96
+ if (analyser !== null) {
97
+ for (const file of files) {
98
+ const result = spawnSync(analyser, [file], { encoding: "utf8" });
99
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
100
+ for (const line of output.split("\n")) {
101
+ if (line.includes("LocalShadow:")) shadowed.push(line.trim());
102
+ else if (TYPE_PATTERNS.some((pattern) => pattern.test(line))) mistyped.push(line.trim());
103
+ }
104
+ }
105
+ }
106
+
107
+ /*
108
+ * Every operation the plugin answers has to have a readable name in the panel.
109
+ *
110
+ * `Phrase` falls back to tidying the wire name, so a missing entry is invisible
111
+ * in testing and only shows up as "Data set" where "SAVE over 4212 in PlayerData"
112
+ * belonged. Left unchecked it rots by default: 26 of 72 operations had drifted
113
+ * out of the table, which is every tool added after the table was written. A
114
+ * missing KIND is worse than cosmetic -- the fallback is "read", so an
115
+ * unregistered terrain wipe was announced with the weight of an inspect.
116
+ */
117
+ const unnamed = [];
118
+ const kindless = new Set();
119
+ {
120
+ const phrase = readFileSync(join(root, "plugin", "src", "Phrase.luau"), "utf8");
121
+ const described = new Set([...phrase.matchAll(/\["([^"]+)"\]\s*=\s*function/g)].map((m) => m[1]));
122
+ const kinds = new Set([...phrase.matchAll(/^\t([a-z]+) = "/gm)].map((m) => m[1]));
123
+ // `script` is split by action inside Phrase.kindOf rather than by a table row.
124
+ kinds.add("script");
125
+
126
+ for (const file of files) {
127
+ const source = readFileSync(file, "utf8");
128
+ for (const block of source.matchAll(/Dispatch\.registerAll\("([^"]+)",\s*\{([\s\S]*?)\n\t\}\)/g)) {
129
+ const group = block[1];
130
+ if (!kinds.has(group)) kindless.add(group);
131
+ for (const entry of block[2].matchAll(/^\s*([A-Za-z0-9_]+)\s*=/gm)) {
132
+ const op = `${group}.${entry[1]}`;
133
+ if (!described.has(op)) unnamed.push(op);
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ if (unnamed.length > 0) {
140
+ failures.push(
141
+ "These operations have no entry in Phrase.luau, so the Studio panel shows " +
142
+ "the wire name instead of saying what they touch:\n " +
143
+ unnamed.sort().join("\n "),
144
+ );
145
+ }
146
+ if (kindless.size > 0) {
147
+ failures.push(
148
+ "These operation groups have no entry in Phrase KINDS, so they are announced " +
149
+ 'as "read" whatever they do:\n ' +
150
+ [...kindless].sort().join("\n "),
151
+ );
152
+ }
153
+
154
+ if (failures.length > 0) {
155
+ process.stderr.write(`${failures.join("\n\n")}\n`);
156
+ }
157
+ if (shadowed.length > 0) {
158
+ process.stderr.write(
159
+ "\nA local is used before it is declared, so the call reaches a nil global " +
160
+ "instead:\n" +
161
+ `${shadowed.join("\n")}\n`,
162
+ );
163
+ }
164
+ if (mistyped.length > 0) {
165
+ process.stderr.write(
166
+ "\nA field is used on a table whose type does not declare it. It is nil at " +
167
+ "runtime, and the plugin fails the first time that line runs:\n" +
121
168
  `${mistyped.join("\n")}
122
- `,
123
- );
124
- }
125
- process.exit(failures.length > 0 || shadowed.length > 0 || mistyped.length > 0 ? 1 : 0);
169
+ `,
170
+ );
171
+ }
172
+ process.exit(failures.length > 0 || shadowed.length > 0 || mistyped.length > 0 ? 1 : 0);