@princeofscale/bloxforge 2.20.2

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 (49) hide show
  1. package/assets/Baseplate.rbxl +0 -0
  2. package/dist/index.js +19391 -0
  3. package/package.json +62 -0
  4. package/studio-plugin/INSTALLATION.md +161 -0
  5. package/studio-plugin/MCPInspectorPlugin.rbxmx +171699 -0
  6. package/studio-plugin/MCPPlugin.rbxmx +171699 -0
  7. package/studio-plugin/default.project.json +19 -0
  8. package/studio-plugin/dev.project.json +23 -0
  9. package/studio-plugin/include/LibMP.lua +156378 -0
  10. package/studio-plugin/inspector-icon.png +0 -0
  11. package/studio-plugin/package-lock.json +692 -0
  12. package/studio-plugin/package.json +19 -0
  13. package/studio-plugin/plugin.json +10 -0
  14. package/studio-plugin/src/modules/ClientBroker.ts +447 -0
  15. package/studio-plugin/src/modules/Communication.ts +697 -0
  16. package/studio-plugin/src/modules/EvalBridges.ts +255 -0
  17. package/studio-plugin/src/modules/HttpDiagnostics.ts +50 -0
  18. package/studio-plugin/src/modules/JobRegistry.ts +77 -0
  19. package/studio-plugin/src/modules/LuauExec.ts +465 -0
  20. package/studio-plugin/src/modules/Recording.ts +28 -0
  21. package/studio-plugin/src/modules/RenderMonitor.ts +60 -0
  22. package/studio-plugin/src/modules/RuntimeLogBuffer.ts +152 -0
  23. package/studio-plugin/src/modules/ServerUrlSettings.ts +114 -0
  24. package/studio-plugin/src/modules/State.ts +101 -0
  25. package/studio-plugin/src/modules/StopPlayMonitor.ts +276 -0
  26. package/studio-plugin/src/modules/UI.ts +790 -0
  27. package/studio-plugin/src/modules/Utils.ts +318 -0
  28. package/studio-plugin/src/modules/handlers/AssetHandlers.ts +241 -0
  29. package/studio-plugin/src/modules/handlers/BreakpointHandlers.ts +460 -0
  30. package/studio-plugin/src/modules/handlers/BuildHandlers.ts +481 -0
  31. package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +203 -0
  32. package/studio-plugin/src/modules/handlers/EvalRuntimeHandlers.ts +149 -0
  33. package/studio-plugin/src/modules/handlers/InputHandlers.ts +160 -0
  34. package/studio-plugin/src/modules/handlers/InstanceHandlers.ts +380 -0
  35. package/studio-plugin/src/modules/handlers/JobHandlers.ts +111 -0
  36. package/studio-plugin/src/modules/handlers/LogHandlers.ts +14 -0
  37. package/studio-plugin/src/modules/handlers/MemoryHandlers.ts +44 -0
  38. package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +354 -0
  39. package/studio-plugin/src/modules/handlers/MicroProfilerHandlers.ts +1263 -0
  40. package/studio-plugin/src/modules/handlers/PropertyHandlers.ts +191 -0
  41. package/studio-plugin/src/modules/handlers/QueryHandlers.ts +809 -0
  42. package/studio-plugin/src/modules/handlers/SceneAnalysisHandlers.ts +216 -0
  43. package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +530 -0
  44. package/studio-plugin/src/modules/handlers/ScriptProfilerHandlers.ts +386 -0
  45. package/studio-plugin/src/modules/handlers/SerializationHandlers.ts +172 -0
  46. package/studio-plugin/src/modules/handlers/TestHandlers.ts +535 -0
  47. package/studio-plugin/src/server/index.server.ts +134 -0
  48. package/studio-plugin/src/types/index.d.ts +83 -0
  49. package/studio-plugin/tsconfig.json +21 -0
@@ -0,0 +1,255 @@
1
+ // Game-VM eval bridges, ported from chrrxs/roblox-mcp-primitives.
2
+ //
3
+ // Our standard `execute_luau target=server/client-N` runs in the plugin VM
4
+ // with a fresh ModuleScript per call. That gives a clean sandbox but means
5
+ // `require(SomeModule)` returns a fresh copy, not the one the running game
6
+ // scripts hold. So runtime-mutated module state is invisible to probes.
7
+ //
8
+ // These bridges fix that by living inside the user's game scripts. Both
9
+ // peers use the same symmetric shape:
10
+ // - Server: a Script in ServerScriptService that creates a BindableFunction.
11
+ // Plugin (server peer) invokes it with a fresh ModuleScript payload;
12
+ // require() runs inside the Script VM so it shares the running server's
13
+ // require cache.
14
+ // - Client: a LocalScript in StarterPlayer.StarterPlayerScripts that
15
+ // creates a BindableFunction. Plugin invokes it with a fresh ModuleScript
16
+ // payload; require() runs inside the LocalScript VM so it shares the
17
+ // game's require cache.
18
+ //
19
+ // Why ModuleScript+require on both sides (no loadstring): require'd modules
20
+ // run with the security level they were created at and don't need
21
+ // ServerScriptService.LoadStringEnabled, so eval_server_runtime works even
22
+ // when LoadStringEnabled=false (the default in fresh places).
23
+ //
24
+ // Lifecycle: bridge scripts are created only in running play DataModels.
25
+ // The server plugin peer creates the Script in runtime ServerScriptService;
26
+ // each client plugin peer creates its LocalScript in that client's
27
+ // PlayerScripts. Nothing is installed into the edit DataModel anymore.
28
+ // Runtime-created scripts disappear naturally when the playtest stops.
29
+
30
+ import { Players, ReplicatedStorage, RunService, ServerScriptService, StarterPlayer } from "@rbxts/services";
31
+
32
+ const ScriptEditorService = game.GetService("ScriptEditorService");
33
+
34
+ function getStarterPlayerScripts(): Instance | undefined {
35
+ return StarterPlayer.FindFirstChild("StarterPlayerScripts");
36
+ }
37
+
38
+ const SERVER_SCRIPT_NAME = "__MCP_ServerEvalBridge";
39
+ const CLIENT_SCRIPT_NAME = "__MCP_ClientEvalBridge";
40
+
41
+ // Public so the eval_*_runtime tool wrappers can reference the same names.
42
+ export const BRIDGE_NAMES = {
43
+ serverScript: SERVER_SCRIPT_NAME,
44
+ clientScript: CLIENT_SCRIPT_NAME,
45
+ serverLocal: "__MCP_ServerEvalLocal",
46
+ clientLocal: "__MCP_ClientEvalBridge",
47
+ } as const;
48
+
49
+ // Embedded Luau. The double `${...}` references our exported names so a
50
+ // rename here propagates to both the script source and the tool wrappers.
51
+ const SERVER_BRIDGE_SOURCE = `
52
+ -- Installed by @princeofscale/bloxforge to power the eval_server_runtime MCP
53
+ -- tool (shared-require-cache eval on the server during playtests). Inert
54
+ -- outside Studio (no-ops in live games); safe to leave in place.
55
+
56
+ local ServerScriptService = game:GetService("ServerScriptService")
57
+ local RunService = game:GetService("RunService")
58
+
59
+ if not RunService:IsStudio() then
60
+ return
61
+ end
62
+
63
+ local prevBf = ServerScriptService:FindFirstChild("${BRIDGE_NAMES.serverLocal}")
64
+ if prevBf then prevBf:Destroy() end
65
+
66
+ local bf = Instance.new("BindableFunction")
67
+ bf.Name = "${BRIDGE_NAMES.serverLocal}"
68
+ bf.Archivable = false
69
+ bf.Parent = ServerScriptService
70
+ bf.OnInvoke = function(payload)
71
+ if typeof(payload) ~= "Instance" or not payload:IsA("ModuleScript") then
72
+ return { ok = false, value = "payload must be a ModuleScript instance" }
73
+ end
74
+ local ok, value = pcall(require, payload)
75
+ return { ok = ok, value = value }
76
+ end
77
+ `;
78
+
79
+ const CLIENT_BRIDGE_SOURCE = `
80
+ -- Installed by @princeofscale/bloxforge to power the eval_client_runtime MCP
81
+ -- tool (shared-require-cache eval on the client during playtests). Inert
82
+ -- outside Studio (no-ops in live games); safe to leave in place.
83
+
84
+ local ReplicatedStorage = game:GetService("ReplicatedStorage")
85
+ local RunService = game:GetService("RunService")
86
+
87
+ if not RunService:IsStudio() then
88
+ return
89
+ end
90
+
91
+ local prevBf = ReplicatedStorage:FindFirstChild("${BRIDGE_NAMES.clientLocal}")
92
+ if prevBf then prevBf:Destroy() end
93
+
94
+ local bf = Instance.new("BindableFunction")
95
+ bf.Name = "${BRIDGE_NAMES.clientLocal}"
96
+ bf.Archivable = false
97
+ bf.Parent = ReplicatedStorage
98
+ bf.OnInvoke = function(payload)
99
+ if typeof(payload) ~= "Instance" or not payload:IsA("ModuleScript") then
100
+ return { ok = false, value = "payload must be a ModuleScript instance" }
101
+ end
102
+ local ok, value = pcall(require, payload)
103
+ return { ok = ok, value = value }
104
+ end
105
+ `;
106
+
107
+ // Stamp written onto each installed bridge Script so we can tell whether the
108
+ // runtime bridge currently in the play DM was produced by THIS plugin build.
109
+ // It's a djb2 hash of the actual bridge source plus the plugin version, so ANY
110
+ // change to the source (or a version bump) yields a new stamp and triggers a
111
+ // runtime refresh instead of keeping a stale bridge.
112
+ const STAMP_ATTR = "__MCPBridgeStamp";
113
+
114
+ function computeBridgeStamp(): string {
115
+ const combined = `${SERVER_BRIDGE_SOURCE}|${CLIENT_BRIDGE_SOURCE}`;
116
+ let h = 5381;
117
+ for (let i = 1; i <= combined.size(); i++) {
118
+ h = (h * 33 + string.byte(combined, i)[0]) % 2147483647;
119
+ }
120
+ // "__VERSION__" is replaced with the package version at package time
121
+ // (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
122
+ return `${tostring(h)}-__VERSION__`;
123
+ }
124
+
125
+ const BRIDGE_STAMP = computeBridgeStamp();
126
+
127
+ function setSource(scriptInst: Script | LocalScript, source: string): void {
128
+ // ScriptEditorService is the cleaner API and integrates with Studio's
129
+ // edit history; fall back to direct Source mutation (allowed in plugin
130
+ // context with PluginSecurity) if the edit service rejects the call.
131
+ const [seOk] = pcall(() => {
132
+ ScriptEditorService.UpdateSourceAsync(scriptInst, () => source);
133
+ });
134
+ if (!seOk) {
135
+ (scriptInst as unknown as { Source: string }).Source = source;
136
+ }
137
+ }
138
+
139
+ interface InstallResult {
140
+ installed: boolean;
141
+ error?: string;
142
+ }
143
+
144
+ function findLegacyEditBridges(): { server?: Instance; client?: Instance } {
145
+ const sps = getStarterPlayerScripts();
146
+ return {
147
+ server: ServerScriptService.FindFirstChild(SERVER_SCRIPT_NAME),
148
+ client: sps ? sps.FindFirstChild(CLIENT_SCRIPT_NAME) : undefined,
149
+ };
150
+ }
151
+
152
+ function destroyIfPresent(parent: Instance, name: string): void {
153
+ const existing = parent.FindFirstChild(name);
154
+ if (existing) {
155
+ pcall(() => existing.Destroy());
156
+ }
157
+ }
158
+
159
+ export function cleanupLegacyEditBridges(): void {
160
+ if (RunService.IsRunning()) return;
161
+ const { server, client } = findLegacyEditBridges();
162
+ if (server) {
163
+ pcall(() => server.Destroy());
164
+ }
165
+ if (client) {
166
+ pcall(() => client.Destroy());
167
+ }
168
+ }
169
+
170
+ function serverRuntimeBridgeReady(): boolean {
171
+ const scriptInst = ServerScriptService.FindFirstChild(SERVER_SCRIPT_NAME);
172
+ const bindable = ServerScriptService.FindFirstChild(BRIDGE_NAMES.serverLocal);
173
+ return scriptInst !== undefined &&
174
+ scriptInst.GetAttribute(STAMP_ATTR) === BRIDGE_STAMP &&
175
+ bindable !== undefined &&
176
+ bindable.IsA("BindableFunction");
177
+ }
178
+
179
+ function getPlayerScripts(): Instance | undefined {
180
+ const localPlayer = Players.LocalPlayer;
181
+ if (!localPlayer) return undefined;
182
+ let playerScripts = localPlayer.FindFirstChild("PlayerScripts");
183
+ if (!playerScripts) {
184
+ playerScripts = localPlayer.WaitForChild("PlayerScripts", 5);
185
+ }
186
+ return playerScripts;
187
+ }
188
+
189
+ function clientRuntimeBridgeReady(): boolean {
190
+ const playerScripts = getPlayerScripts();
191
+ if (!playerScripts) return false;
192
+ const scriptInst = playerScripts.FindFirstChild(CLIENT_SCRIPT_NAME);
193
+ const bindable = ReplicatedStorage.FindFirstChild(BRIDGE_NAMES.clientLocal);
194
+ return scriptInst !== undefined &&
195
+ scriptInst.GetAttribute(STAMP_ATTR) === BRIDGE_STAMP &&
196
+ bindable !== undefined &&
197
+ bindable.IsA("BindableFunction");
198
+ }
199
+
200
+ function installServerRuntimeBridge(): InstallResult {
201
+ if (serverRuntimeBridgeReady()) return { installed: true };
202
+
203
+ const [ok, err] = pcall(() => {
204
+ destroyIfPresent(ServerScriptService, SERVER_SCRIPT_NAME);
205
+ destroyIfPresent(ServerScriptService, BRIDGE_NAMES.serverLocal);
206
+
207
+ const serverScript = new Instance("Script");
208
+ serverScript.Name = SERVER_SCRIPT_NAME;
209
+ serverScript.Archivable = false;
210
+ setSource(serverScript, SERVER_BRIDGE_SOURCE);
211
+ serverScript.SetAttribute(STAMP_ATTR, BRIDGE_STAMP);
212
+ serverScript.Parent = ServerScriptService;
213
+ });
214
+
215
+ if (!ok) {
216
+ return { installed: false, error: tostring(err) };
217
+ }
218
+ return { installed: true };
219
+ }
220
+
221
+ function installClientRuntimeBridge(): InstallResult {
222
+ if (clientRuntimeBridgeReady()) return { installed: true };
223
+
224
+ const playerScripts = getPlayerScripts();
225
+ if (!playerScripts) {
226
+ return { installed: false, error: "Players.LocalPlayer.PlayerScripts not found - cannot install client eval bridge" };
227
+ }
228
+
229
+ const [ok, err] = pcall(() => {
230
+ destroyIfPresent(playerScripts, CLIENT_SCRIPT_NAME);
231
+ destroyIfPresent(ReplicatedStorage, BRIDGE_NAMES.clientLocal);
232
+
233
+ const clientScript = new Instance("LocalScript");
234
+ clientScript.Name = CLIENT_SCRIPT_NAME;
235
+ clientScript.Archivable = false;
236
+ setSource(clientScript, CLIENT_BRIDGE_SOURCE);
237
+ clientScript.SetAttribute(STAMP_ATTR, BRIDGE_STAMP);
238
+ clientScript.Parent = playerScripts;
239
+ });
240
+
241
+ if (!ok) {
242
+ return { installed: false, error: tostring(err) };
243
+ }
244
+ return { installed: true };
245
+ }
246
+
247
+ export function ensureRuntimeBridgeInstalled(): InstallResult {
248
+ if (!RunService.IsRunning()) {
249
+ return { installed: false, error: "Eval bridges are installed only in running play DataModels" };
250
+ }
251
+ if (RunService.IsServer()) {
252
+ return installServerRuntimeBridge();
253
+ }
254
+ return installClientRuntimeBridge();
255
+ }
@@ -0,0 +1,50 @@
1
+ import { HttpService } from "@rbxts/services";
2
+
3
+ interface FailureBody {
4
+ error?: string;
5
+ message?: string;
6
+ missingFields?: unknown;
7
+ request?: unknown;
8
+ existing?: unknown;
9
+ details?: unknown;
10
+ }
11
+
12
+ function encodeForLog(value: unknown): string {
13
+ const [ok, encoded] = pcall(() => HttpService.JSONEncode(value));
14
+ return ok ? encoded : tostring(value);
15
+ }
16
+
17
+ function formatBody(body: string): string {
18
+ if (body === "") return "";
19
+ const [ok, decoded] = pcall(() => HttpService.JSONDecode(body));
20
+ if (ok && typeIs(decoded, "table")) {
21
+ const data = decoded as FailureBody;
22
+ const parts: string[] = [];
23
+ if (typeIs(data.error, "string") && data.error !== "") parts.push(`error=${data.error}`);
24
+ if (typeIs(data.message, "string") && data.message !== "") parts.push(`message=${data.message}`);
25
+ if (data.missingFields !== undefined) parts.push(`missingFields=${encodeForLog(data.missingFields)}`);
26
+ if (data.request !== undefined) parts.push(`request=${encodeForLog(data.request)}`);
27
+ if (data.existing !== undefined) parts.push(`existing=${encodeForLog(data.existing)}`);
28
+ if (data.details !== undefined) parts.push(`details=${encodeForLog(data.details)}`);
29
+ if (parts.size() > 0) return parts.join(" ");
30
+ }
31
+ return `body=${body}`;
32
+ }
33
+
34
+ function formatRequestFailure(url: string, ok: boolean, res: unknown): string {
35
+ if (!ok) {
36
+ return `RequestAsync threw for ${url}: ${tostring(res)}`;
37
+ }
38
+ if (res === undefined) {
39
+ return `RequestAsync returned no response for ${url}`;
40
+ }
41
+ const response = res as RequestAsyncResponse;
42
+ const statusMessage = response.StatusMessage !== "" ? ` ${response.StatusMessage}` : "";
43
+ const body = formatBody(response.Body);
44
+ const suffix = body !== "" ? `: ${body}` : "";
45
+ return `HTTP ${response.StatusCode}${statusMessage} from ${url}${suffix}`;
46
+ }
47
+
48
+ export = {
49
+ formatRequestFailure,
50
+ };
@@ -0,0 +1,77 @@
1
+ // Async-job registry for long-running execute_luau. The poll loop already runs
2
+ // each request in its own task.spawn coroutine, and plugin modules are
3
+ // singletons that persist across poll cycles — so a job started by
4
+ // /api/execute-luau-async can keep running while later /api/get-job-status polls
5
+ // read its state here. This removes the false-timeout class: every individual
6
+ // MCP call returns fast; the heavy work happens between polls.
7
+
8
+ import { Job } from "../types";
9
+
10
+ const jobs = new Map<string, Job>();
11
+ const MAX_JOBS = 50;
12
+
13
+ // Maps a running coroutine to its job id, so the global _G.__mcp helpers can find
14
+ // "which job am I in" via coroutine.running() — concurrency-safe across jobs.
15
+ const threadJobs = new Map<thread, string>();
16
+
17
+ // Keep the registry bounded: once it grows past MAX_JOBS, drop the oldest
18
+ // finished jobs (never running ones).
19
+ function prune(): void {
20
+ if (jobs.size() <= MAX_JOBS) return;
21
+ const finished: Job[] = [];
22
+ for (const [, j] of jobs) {
23
+ if (j.status !== "running") finished.push(j);
24
+ }
25
+ finished.sort((a, b) => (a.finishedAt ?? 0) < (b.finishedAt ?? 0));
26
+ let toRemove = jobs.size() - MAX_JOBS;
27
+ for (const j of finished) {
28
+ if (toRemove <= 0) break;
29
+ jobs.delete(j.id);
30
+ toRemove--;
31
+ }
32
+ }
33
+
34
+ function create(): Job {
35
+ const job: Job = {
36
+ id: game.GetService("HttpService").GenerateGUID(false),
37
+ status: "running",
38
+ startedAt: tick(),
39
+ };
40
+ jobs.set(job.id, job);
41
+ prune();
42
+ return job;
43
+ }
44
+
45
+ function get(id: string): Job | undefined {
46
+ return jobs.get(id);
47
+ }
48
+
49
+ function bindThread(co: thread, jobId: string): void {
50
+ threadJobs.set(co, jobId);
51
+ }
52
+
53
+ function unbindThread(co: thread): void {
54
+ threadJobs.delete(co);
55
+ }
56
+
57
+ // Called by _G.__mcp.progress(done, total, msg) from server-generated code.
58
+ function reportProgress(co: thread, done: number, total?: number, message?: string, stage?: string): void {
59
+ const jobId = threadJobs.get(co);
60
+ if (jobId === undefined) return;
61
+ const job = jobs.get(jobId);
62
+ if (!job || job.status !== "running") return;
63
+ job.progress = done;
64
+ if (total !== undefined) job.total = total;
65
+ if (message !== undefined) job.message = message;
66
+ if (stage !== undefined) job.stage = stage;
67
+ }
68
+
69
+ // Called by _G.__mcp.checkCancelled() so long server-generated loops can bail early.
70
+ function isCancelledForThread(co: thread): boolean {
71
+ const jobId = threadJobs.get(co);
72
+ if (jobId === undefined) return false;
73
+ const job = jobs.get(jobId);
74
+ return job?.cancelled === true;
75
+ }
76
+
77
+ export = { create, get, bindThread, unbindThread, reportProgress, isCancelledForThread };