@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,386 @@
1
+ const HttpService = game.GetService("HttpService");
2
+ const Players = game.GetService("Players");
3
+ const RunService = game.GetService("RunService");
4
+
5
+ interface ScriptProfilerServiceLike extends Instance {
6
+ ServerStart(this: ScriptProfilerServiceLike, frequency?: number): void;
7
+ ServerStop(this: ScriptProfilerServiceLike): void;
8
+ ServerRequestData(this: ScriptProfilerServiceLike): void;
9
+ ClientStart(this: ScriptProfilerServiceLike, player: Player, frequency?: number): void;
10
+ ClientStop(this: ScriptProfilerServiceLike, player: Player): void;
11
+ ClientRequestData(this: ScriptProfilerServiceLike, player: Player): void;
12
+ OnNewData: RBXScriptSignal<(player: Player | undefined, jsonString: string) => void>;
13
+ }
14
+
15
+ interface CategoryInfo {
16
+ Name?: string;
17
+ NodeId?: number;
18
+ }
19
+
20
+ interface FunctionInfo {
21
+ Source?: string;
22
+ Name?: string;
23
+ Line?: number;
24
+ TotalDuration?: number;
25
+ Flags?: number;
26
+ }
27
+
28
+ interface ProfilingInfo {
29
+ Version?: number;
30
+ SessionStartTime?: number;
31
+ SessionEndTime?: number;
32
+ Categories?: CategoryInfo[];
33
+ Nodes?: unknown[];
34
+ Functions?: FunctionInfo[];
35
+ }
36
+
37
+ interface FunctionRow {
38
+ function_index: number;
39
+ name: string;
40
+ source?: string;
41
+ line?: number;
42
+ total_us: number;
43
+ is_native?: boolean;
44
+ is_plugin?: boolean;
45
+ is_debug_label?: boolean;
46
+ }
47
+
48
+ const DEFAULT_DURATION_MS = 1000;
49
+ const MIN_DURATION_MS = 100;
50
+ const MAX_DURATION_MS = 15000;
51
+ const DEFAULT_FREQUENCY = 1000;
52
+ const DEFAULT_MAX_FUNCTIONS = 20;
53
+
54
+ function getProfilerService(): ScriptProfilerServiceLike | Record<string, unknown> {
55
+ const provider = game as unknown as { GetService(serviceName: string): Instance };
56
+ const [ok, service] = pcall(() => provider.GetService("ScriptProfilerService") as ScriptProfilerServiceLike);
57
+ if (!ok || !service) {
58
+ return {
59
+ error: "script_profiler_unavailable",
60
+ message: `ScriptProfilerService is unavailable: ${tostring(service)}`,
61
+ };
62
+ }
63
+ return service;
64
+ }
65
+
66
+ function normalizeDurationMs(value: unknown): number {
67
+ if (!typeIs(value, "number")) return DEFAULT_DURATION_MS;
68
+ return math.clamp(math.floor(value), MIN_DURATION_MS, MAX_DURATION_MS);
69
+ }
70
+
71
+ function normalizeFrequency(value: unknown): number {
72
+ if (!typeIs(value, "number")) return DEFAULT_FREQUENCY;
73
+ return math.clamp(math.floor(value), 1, 10000);
74
+ }
75
+
76
+ function normalizeMaxFunctions(value: unknown): number {
77
+ if (!typeIs(value, "number")) return DEFAULT_MAX_FUNCTIONS;
78
+ return math.clamp(math.floor(value), 1, 100);
79
+ }
80
+
81
+ function normalizeMinTotalUs(requestData: Record<string, unknown>): number {
82
+ const value = requestData.min_total_us;
83
+ if (typeIs(value, "number")) return math.max(0, value);
84
+ return 0;
85
+ }
86
+
87
+ function stringContains(haystack: string, needle: string): boolean {
88
+ return string.find(string.lower(haystack), string.lower(needle), 1, true)[0] !== undefined;
89
+ }
90
+
91
+ function localPlayer(): Player | undefined {
92
+ let player = Players.LocalPlayer;
93
+ const started = tick();
94
+ while (!player && tick() - started < 5) {
95
+ task.wait(0.05);
96
+ player = Players.LocalPlayer;
97
+ }
98
+ return player;
99
+ }
100
+
101
+ function functionDisplayName(func: FunctionInfo): string {
102
+ if (typeIs(func.Name, "string") && func.Name !== "") return func.Name;
103
+ if (typeIs(func.Source, "string") && func.Source !== "") {
104
+ if (typeIs(func.Line, "number") && func.Line > 0) return `${func.Source}:${func.Line}`;
105
+ return func.Source;
106
+ }
107
+ return "<anonymous>";
108
+ }
109
+
110
+ function flagsOf(func: FunctionInfo): number {
111
+ return typeIs(func.Flags, "number") ? func.Flags : 0;
112
+ }
113
+
114
+ function isNativeFunction(func: FunctionInfo): boolean {
115
+ return bit32.band(flagsOf(func), 1) !== 0;
116
+ }
117
+
118
+ function isPluginFunction(func: FunctionInfo): boolean {
119
+ if (bit32.band(flagsOf(func), 2) !== 0) return true;
120
+ return typeIs(func.Source, "string") && string.find(func.Source, "MCPPlugin", 1, true)[0] !== undefined;
121
+ }
122
+
123
+ function isDebugLabel(func: FunctionInfo, filter: string | undefined): boolean {
124
+ if (filter === undefined) return false;
125
+ if (!typeIs(func.Name, "string") || func.Name === "") return false;
126
+ if (!typeIs(func.Source, "string") || func.Source === "" || func.Source === "[C]" || func.Source === "GC") return false;
127
+ if (func.Line !== undefined && func.Line !== 0) return false;
128
+ if (isNativeFunction(func) || isPluginFunction(func)) return false;
129
+ return stringContains(func.Name, filter) || stringContains(func.Source, filter);
130
+ }
131
+
132
+ function pctOfCapture(row: FunctionRow, durationMs: number): number | undefined {
133
+ const captureUs = durationMs * 1000;
134
+ if (captureUs <= 0) return undefined;
135
+ return math.floor((row.total_us / captureUs) * 10000 + 0.5) / 100;
136
+ }
137
+
138
+ function compactFunction(row: FunctionRow, rank: number, durationMs: number): Record<string, unknown> {
139
+ const out: Record<string, unknown> = {
140
+ rank,
141
+ function_index: row.function_index,
142
+ name: row.name,
143
+ total_us: math.floor(row.total_us + 0.5),
144
+ };
145
+ const pct = pctOfCapture(row, durationMs);
146
+ if (pct !== undefined) out.pct_of_capture = pct;
147
+ if (row.source !== undefined) out.source = row.source;
148
+ if (row.line !== undefined) out.line = row.line;
149
+ if (row.is_native === true) out.is_native = true;
150
+ if (row.is_plugin === true) out.is_plugin = true;
151
+ if (row.is_debug_label === true) out.is_debug_label = true;
152
+ return out;
153
+ }
154
+
155
+ function summarizeProfile(
156
+ rawJson: string,
157
+ profile: ProfilingInfo,
158
+ requestData: Record<string, unknown>,
159
+ durationMs: number,
160
+ frequency: number,
161
+ eventPlayerName: string | undefined,
162
+ ): Record<string, unknown> {
163
+ const funcs = typeIs(profile.Functions, "table") ? profile.Functions : [];
164
+ const nodes = typeIs(profile.Nodes, "table") ? profile.Nodes : [];
165
+ const categories = typeIs(profile.Categories, "table") ? profile.Categories : [];
166
+ const maxFunctions = normalizeMaxFunctions(requestData.max_functions);
167
+ const minTotalUs = normalizeMinTotalUs(requestData);
168
+ const includeNative = requestData.include_native === true;
169
+ const includePlugin = requestData.include_plugin === true;
170
+ const filter = typeIs(requestData.filter, "string") && requestData.filter !== "" ? requestData.filter as string : undefined;
171
+
172
+ const rows: FunctionRow[] = [];
173
+ const debugRows: FunctionRow[] = [];
174
+ let omittedNative = 0;
175
+ let omittedPlugin = 0;
176
+ let omittedBelowThreshold = 0;
177
+ let omittedByFilter = 0;
178
+
179
+ for (let i = 0; i < funcs.size(); i++) {
180
+ const func = funcs[i];
181
+ if (!typeIs(func, "table")) continue;
182
+ const info = func as FunctionInfo;
183
+ const totalUs = typeIs(info.TotalDuration, "number") ? info.TotalDuration : 0;
184
+ const name = functionDisplayName(info);
185
+ const row: FunctionRow = {
186
+ function_index: i + 1,
187
+ name,
188
+ source: typeIs(info.Source, "string") ? info.Source : undefined,
189
+ line: typeIs(info.Line, "number") ? info.Line : undefined,
190
+ total_us: totalUs,
191
+ is_native: isNativeFunction(info) ? true : undefined,
192
+ is_plugin: isPluginFunction(info) ? true : undefined,
193
+ is_debug_label: isDebugLabel(info, filter) ? true : undefined,
194
+ };
195
+
196
+ if (!includeNative && row.is_native === true) {
197
+ omittedNative += 1;
198
+ continue;
199
+ }
200
+ if (!includePlugin && row.is_plugin === true) {
201
+ omittedPlugin += 1;
202
+ continue;
203
+ }
204
+ if (totalUs < minTotalUs) {
205
+ omittedBelowThreshold += 1;
206
+ continue;
207
+ }
208
+ if (filter !== undefined) {
209
+ const text = `${row.name} ${row.source ?? ""}`;
210
+ if (!stringContains(text, filter)) {
211
+ omittedByFilter += 1;
212
+ continue;
213
+ }
214
+ }
215
+ rows.push(row);
216
+ if (row.is_debug_label === true) debugRows.push(row);
217
+ }
218
+
219
+ rows.sort((a, b) => a.total_us > b.total_us);
220
+ debugRows.sort((a, b) => a.total_us > b.total_us);
221
+
222
+ const topFunctions: Record<string, unknown>[] = [];
223
+ for (let i = 0; i < math.min(maxFunctions, rows.size()); i++) {
224
+ topFunctions.push(compactFunction(rows[i], i + 1, durationMs));
225
+ }
226
+
227
+ const debugLabels: Record<string, unknown>[] = [];
228
+ for (let i = 0; i < math.min(maxFunctions, debugRows.size()); i++) {
229
+ debugLabels.push(compactFunction(debugRows[i], i + 1, durationMs));
230
+ }
231
+
232
+ const categoryNames: string[] = [];
233
+ for (let i = 0; i < categories.size(); i++) {
234
+ const category = categories[i];
235
+ if (typeIs(category, "table")) {
236
+ const name = (category as CategoryInfo).Name;
237
+ if (typeIs(name, "string")) categoryNames.push(name);
238
+ }
239
+ }
240
+
241
+ const omitted: Record<string, number> = {};
242
+ let hasOmitted = false;
243
+ if (omittedNative > 0) {
244
+ omitted.native = omittedNative;
245
+ hasOmitted = true;
246
+ }
247
+ if (omittedPlugin > 0) {
248
+ omitted.plugin = omittedPlugin;
249
+ hasOmitted = true;
250
+ }
251
+ if (omittedBelowThreshold > 0) {
252
+ omitted.below_min_total_us = omittedBelowThreshold;
253
+ hasOmitted = true;
254
+ }
255
+ if (omittedByFilter > 0) {
256
+ omitted.filtered_out = omittedByFilter;
257
+ hasOmitted = true;
258
+ }
259
+
260
+ const result: Record<string, unknown> = {
261
+ ok: true,
262
+ duration_ms: durationMs,
263
+ frequency,
264
+ applied: {
265
+ filter: filter ?? undefined,
266
+ min_total_us: minTotalUs,
267
+ include_native: includeNative,
268
+ include_plugin: includePlugin,
269
+ max_functions: maxFunctions,
270
+ sort: "total_us_desc",
271
+ },
272
+ json_bytes: rawJson.size(),
273
+ counts: {
274
+ categories: categories.size(),
275
+ nodes: nodes.size(),
276
+ functions: funcs.size(),
277
+ },
278
+ top_functions: topFunctions,
279
+ debug_labels: debugLabels,
280
+ };
281
+ if (categoryNames.size() > 0) result.categories = categoryNames;
282
+ if (hasOmitted) result.omitted = omitted;
283
+ if (profile.Version !== undefined) result.version = profile.Version;
284
+ if (profile.SessionStartTime !== undefined || profile.SessionEndTime !== undefined) {
285
+ result.session = {
286
+ start_time: profile.SessionStartTime,
287
+ end_time: profile.SessionEndTime,
288
+ };
289
+ }
290
+ if (eventPlayerName !== undefined) result.player = eventPlayerName;
291
+ if (requestData.__mcp_include_raw_json === true) result.raw_json = rawJson;
292
+ return result;
293
+ }
294
+
295
+ function captureScriptProfiler(requestData: Record<string, unknown>): unknown {
296
+ if (!RunService.IsRunning()) {
297
+ return {
298
+ error: "runtime_target_required",
299
+ message: "Script profiler capture requires a running playtest target such as target=\"server\" or target=\"client-1\".",
300
+ };
301
+ }
302
+
303
+ const serviceOrError = getProfilerService();
304
+ if (!serviceOrError.IsA) return serviceOrError;
305
+ const service = serviceOrError as ScriptProfilerServiceLike;
306
+
307
+ const durationMs = normalizeDurationMs(requestData.duration_ms);
308
+ const frequency = normalizeFrequency(requestData.frequency);
309
+ const isServer = RunService.IsServer();
310
+ const isClient = RunService.IsClient() && !isServer;
311
+ const player = isClient ? localPlayer() : undefined;
312
+ if (!isServer && !player) {
313
+ return {
314
+ error: "client_player_unavailable",
315
+ message: "Could not resolve Players.LocalPlayer for client profiling.",
316
+ };
317
+ }
318
+
319
+ let rawJson: string | undefined;
320
+ let eventPlayerName: string | undefined;
321
+ const connection = service.OnNewData.Connect((playerArg: Player | undefined, jsonString: string) => {
322
+ if (rawJson !== undefined) return;
323
+ rawJson = jsonString;
324
+ if (playerArg) eventPlayerName = playerArg.Name;
325
+ });
326
+
327
+ const [startOk, startErr] = pcall(() => {
328
+ if (isServer) {
329
+ service.ServerStart(frequency);
330
+ } else {
331
+ service.ClientStart(player!, frequency);
332
+ }
333
+ });
334
+ if (!startOk) {
335
+ connection.Disconnect();
336
+ return {
337
+ error: "script_profiler_start_failed",
338
+ message: tostring(startErr),
339
+ };
340
+ }
341
+
342
+ task.wait(durationMs / 1000);
343
+
344
+ const [stopOk, stopErr] = pcall(() => {
345
+ if (isServer) {
346
+ service.ServerStop();
347
+ service.ServerRequestData();
348
+ } else {
349
+ service.ClientStop(player!);
350
+ service.ClientRequestData(player!);
351
+ }
352
+ });
353
+ if (!stopOk) {
354
+ connection.Disconnect();
355
+ return {
356
+ error: "script_profiler_stop_failed",
357
+ message: tostring(stopErr),
358
+ };
359
+ }
360
+
361
+ const requestedAt = tick();
362
+ while (rawJson === undefined && tick() - requestedAt < 5) {
363
+ task.wait(0.05);
364
+ }
365
+ connection.Disconnect();
366
+
367
+ if (rawJson === undefined) {
368
+ return {
369
+ error: "script_profiler_data_timeout",
370
+ message: "ScriptProfilerService did not emit OnNewData after requesting profiler data.",
371
+ };
372
+ }
373
+
374
+ const [decodeOk, decoded] = pcall(() => HttpService.JSONDecode(rawJson!));
375
+ if (!decodeOk) {
376
+ return {
377
+ error: "script_profiler_decode_failed",
378
+ message: tostring(decoded),
379
+ json_bytes: rawJson.size(),
380
+ };
381
+ }
382
+
383
+ return summarizeProfile(rawJson, decoded as ProfilingInfo, requestData, durationMs, frequency, eventPlayerName);
384
+ }
385
+
386
+ export = { captureScriptProfiler };
@@ -0,0 +1,172 @@
1
+ import { RunService } from "@rbxts/services";
2
+ import Utils from "../Utils";
3
+ import Recording from "../Recording";
4
+
5
+ // SerializationService:SerializeInstancesAsync / DeserializeInstancesAsync were
6
+ // added in engine v668 and are PluginSecurity. They are not in @rbxts/types yet,
7
+ // so we resolve the service through an untyped GetService cast and treat the
8
+ // methods as opaque (buffer in / buffer out).
9
+ type SerializationServiceShape = {
10
+ SerializeInstancesAsync(this: SerializationServiceShape, instances: Instance[]): buffer;
11
+ DeserializeInstancesAsync(this: SerializationServiceShape, b: buffer): Instance[];
12
+ };
13
+
14
+ const SerializationService = (game as unknown as {
15
+ GetService(name: string): SerializationServiceShape;
16
+ }).GetService("SerializationService");
17
+
18
+ // EncodingService:Base64Encode / Base64Decode take and return `buffer` (not
19
+ // `string`). The signature is in @rbxts/types under None.d.ts so a normal
20
+ // GetService("EncodingService") would already give correct types, but @rbxts
21
+ // generates a per-service nominal interface and roblox.d.ts doesn't re-export
22
+ // EncodingService from the services barrel module - so the typed cast below
23
+ // matches what GetService would give us if it did.
24
+ type EncodingServiceShape = {
25
+ Base64Encode(this: EncodingServiceShape, input: buffer): buffer;
26
+ Base64Decode(this: EncodingServiceShape, input: buffer): buffer;
27
+ };
28
+
29
+ const EncodingService = (game as unknown as {
30
+ GetService(name: string): EncodingServiceShape;
31
+ }).GetService("EncodingService");
32
+
33
+ const { getInstanceByPath, getInstancePath } = Utils;
34
+ const { beginRecording, finishRecording } = Recording;
35
+
36
+ function exportRbxm(requestData: Record<string, unknown>): unknown {
37
+ const instancePaths = requestData.instance_paths as string[] | undefined;
38
+ if (!instancePaths || !typeIs(instancePaths, "table") || instancePaths.size() === 0) {
39
+ return { error: "instance_paths must be a non-empty array" };
40
+ }
41
+
42
+ const instances: Instance[] = [];
43
+ for (const p of instancePaths) {
44
+ const inst = getInstanceByPath(p);
45
+ if (!inst) {
46
+ return { error: `instance not found: ${p}` };
47
+ }
48
+ instances.push(inst);
49
+ }
50
+
51
+ const [serializeOk, serializeResult] = pcall(() => {
52
+ return SerializationService.SerializeInstancesAsync(instances);
53
+ });
54
+ if (!serializeOk) {
55
+ return { error: `SerializeInstancesAsync failed: ${tostring(serializeResult)}` };
56
+ }
57
+
58
+ const buf = serializeResult as buffer;
59
+ const [encodeOk, encodeResult] = pcall(() => EncodingService.Base64Encode(buf));
60
+ if (!encodeOk) {
61
+ return { error: `EncodingService:Base64Encode failed: ${tostring(encodeResult)}` };
62
+ }
63
+ // Base64Encode returns a buffer of ASCII bytes; convert to a Lua string so
64
+ // HttpService:JSONEncode (called by the harness in Communication.ts) accepts
65
+ // it. Base64 is by definition pure ASCII so this round-trips cleanly.
66
+ const base64Str = buffer.tostring(encodeResult as buffer);
67
+
68
+ return {
69
+ base64: base64Str,
70
+ instance_count: instances.size(),
71
+ };
72
+ }
73
+
74
+ function importRbxm(requestData: Record<string, unknown>): unknown {
75
+ const b64 = requestData.base64 as string | undefined;
76
+ const parentPath = requestData.parent_path as string | undefined;
77
+ const sourceLabel = (requestData.source_label as string | undefined) ?? "unknown";
78
+
79
+ if (!b64 || !typeIs(b64, "string")) {
80
+ return { error: "base64 payload is required" };
81
+ }
82
+ if (!parentPath || !typeIs(parentPath, "string")) {
83
+ return { error: "parent_path is required" };
84
+ }
85
+
86
+ const parentInstance = getInstanceByPath(parentPath);
87
+ if (!parentInstance) {
88
+ return { error: `parent instance not found: ${parentPath}` };
89
+ }
90
+
91
+ // b64 is an ASCII-only Lua string from the wire; lift it into a buffer for
92
+ // EncodingService:Base64Decode, which returns a buffer of raw rbxm bytes
93
+ // ready for DeserializeInstancesAsync.
94
+ const [b64BufOk, b64BufResult] = pcall(() => buffer.fromstring(b64));
95
+ if (!b64BufOk) {
96
+ return { error: `buffer.fromstring(base64) failed: ${tostring(b64BufResult)}` };
97
+ }
98
+ const [decodeOk, decodeResult] = pcall(() => EncodingService.Base64Decode(b64BufResult as buffer));
99
+ if (!decodeOk) {
100
+ return { error: `EncodingService:Base64Decode failed: ${tostring(decodeResult)}` };
101
+ }
102
+ const buf = decodeResult as buffer;
103
+
104
+ const [deserOk, deserResult] = pcall(() => {
105
+ return SerializationService.DeserializeInstancesAsync(buf);
106
+ });
107
+ if (!deserOk) {
108
+ return { error: `DeserializeInstancesAsync failed: ${tostring(deserResult)}` };
109
+ }
110
+ const deserialized = deserResult as Instance[];
111
+
112
+ // All-or-nothing parenting. Track every instance we've attached and roll back
113
+ // (unparent + Destroy) if any later one fails - partial imports leave the DM
114
+ // in a worse state than failing cleanly.
115
+ const isEdit = !RunService.IsRunning();
116
+ const recordingId = isEdit ? beginRecording(`Import rbxm`) : undefined;
117
+
118
+ const attached: Instance[] = [];
119
+ let failureMessage: string | undefined;
120
+ for (const inst of deserialized) {
121
+ const [parentOk, parentErr] = pcall(() => {
122
+ inst.Parent = parentInstance;
123
+ });
124
+ if (!parentOk) {
125
+ failureMessage = `failed to parent ${inst.Name} (${inst.ClassName}) under ${parentPath}: ${tostring(parentErr)}`;
126
+ break;
127
+ }
128
+ attached.push(inst);
129
+ }
130
+
131
+ if (failureMessage !== undefined) {
132
+ for (const inst of attached) {
133
+ pcall(() => {
134
+ inst.Parent = undefined;
135
+ inst.Destroy();
136
+ });
137
+ }
138
+ // Also destroy any unparented deserialized instances so they don't leak.
139
+ for (const inst of deserialized) {
140
+ if (inst.Parent === undefined) {
141
+ pcall(() => inst.Destroy());
142
+ }
143
+ }
144
+ finishRecording(recordingId, false);
145
+ return { error: failureMessage };
146
+ }
147
+
148
+ const names: string[] = [];
149
+ const paths: string[] = [];
150
+ for (const inst of attached) {
151
+ names.push(inst.Name);
152
+ paths.push(getInstancePath(inst));
153
+ }
154
+
155
+ // The recording shows "MCP: Import rbxm" in Studio's undo stack -
156
+ // ChangeHistoryService doesn't expose a way to set a richer displayName
157
+ // after TryBeginRecording, so the count/source only land in the JSON response.
158
+ finishRecording(recordingId, true);
159
+
160
+ return {
161
+ instance_count: attached.size(),
162
+ instance_names: names,
163
+ instance_paths: paths,
164
+ parent_path: parentPath,
165
+ source: sourceLabel,
166
+ };
167
+ }
168
+
169
+ export = {
170
+ exportRbxm,
171
+ importRbxm,
172
+ };