@chrrxs/robloxstudio-mcp-inspector 2.10.1 → 2.11.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.
- package/dist/index.js +216 -0
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +280 -16
- package/studio-plugin/MCPPlugin.rbxmx +280 -16
- package/studio-plugin/src/modules/ClientBroker.ts +5 -0
- package/studio-plugin/src/modules/Communication.ts +7 -0
- package/studio-plugin/src/modules/handlers/MemoryHandlers.ts +44 -0
- package/studio-plugin/src/modules/handlers/SerializationHandlers.ts +172 -0
package/dist/index.js
CHANGED
|
@@ -372,6 +372,9 @@ var init_http_server = __esm({
|
|
|
372
372
|
simulate_mouse_input: (tools, body) => tools.simulateMouseInput(body.action, body.x, body.y, body.button, body.scrollDirection, body.target),
|
|
373
373
|
simulate_keyboard_input: (tools, body) => tools.simulateKeyboardInput(body.keyCode, body.action, body.duration, body.target),
|
|
374
374
|
character_navigation: (tools, body) => tools.characterNavigation(body.position, body.instancePath, body.waitForCompletion, body.timeout, body.target),
|
|
375
|
+
get_memory_breakdown: (tools, body) => tools.getMemoryBreakdown(body.target, body.tags),
|
|
376
|
+
export_rbxm: (tools, body) => tools.exportRbxm(body.instance_paths, body.output_path, body.target),
|
|
377
|
+
import_rbxm: (tools, body) => tools.importRbxm(body.source, body.parent_path, body.target),
|
|
375
378
|
find_and_replace_in_scripts: (tools, body) => tools.findAndReplaceInScripts(body.pattern, body.replacement, {
|
|
376
379
|
caseSensitive: body.caseSensitive,
|
|
377
380
|
usePattern: body.usePattern,
|
|
@@ -2777,6 +2780,139 @@ ${code}`
|
|
|
2777
2780
|
}]
|
|
2778
2781
|
};
|
|
2779
2782
|
}
|
|
2783
|
+
async getMemoryBreakdown(target, tags) {
|
|
2784
|
+
const tgt = target ?? "all";
|
|
2785
|
+
const data = {};
|
|
2786
|
+
if (tags !== void 0)
|
|
2787
|
+
data.tags = tags;
|
|
2788
|
+
if (tgt !== "all") {
|
|
2789
|
+
const response = await this.client.request("/api/get-memory-breakdown", data, tgt);
|
|
2790
|
+
return { content: [{ type: "text", text: JSON.stringify(response) }] };
|
|
2791
|
+
}
|
|
2792
|
+
const targets = this.bridge.getInstances().filter((i) => i.role !== "edit-proxy").map((i) => i.role);
|
|
2793
|
+
const responses = await Promise.allSettled(targets.map(async (t) => ({
|
|
2794
|
+
peer: t,
|
|
2795
|
+
result: await this.client.request("/api/get-memory-breakdown", data, t)
|
|
2796
|
+
})));
|
|
2797
|
+
const body = {};
|
|
2798
|
+
for (let i = 0; i < responses.length; i++) {
|
|
2799
|
+
const r = responses[i];
|
|
2800
|
+
const peer = targets[i];
|
|
2801
|
+
if (r.status === "fulfilled") {
|
|
2802
|
+
body[peer] = r.value.result;
|
|
2803
|
+
} else {
|
|
2804
|
+
body[peer] = { error: "disconnected" };
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
return { content: [{ type: "text", text: JSON.stringify(body) }] };
|
|
2808
|
+
}
|
|
2809
|
+
async exportRbxm(instancePaths, outputPath, target) {
|
|
2810
|
+
if (!Array.isArray(instancePaths) || instancePaths.length === 0) {
|
|
2811
|
+
throw new Error("instance_paths must be a non-empty array for export_rbxm");
|
|
2812
|
+
}
|
|
2813
|
+
if (!outputPath || typeof outputPath !== "string") {
|
|
2814
|
+
throw new Error("output_path is required for export_rbxm");
|
|
2815
|
+
}
|
|
2816
|
+
const tgt = target || "edit";
|
|
2817
|
+
if (tgt !== "edit" && tgt !== "server") {
|
|
2818
|
+
throw new Error(`export_rbxm target must be "edit" or "server" (got: ${tgt})`);
|
|
2819
|
+
}
|
|
2820
|
+
const response = await this.client.request("/api/export-rbxm", { instance_paths: instancePaths }, tgt);
|
|
2821
|
+
if (response.error) {
|
|
2822
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: response.error }) }] };
|
|
2823
|
+
}
|
|
2824
|
+
if (!response.base64) {
|
|
2825
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "plugin returned no base64 payload" }) }] };
|
|
2826
|
+
}
|
|
2827
|
+
const bytes = Buffer.from(response.base64, "base64");
|
|
2828
|
+
const resolved = path.resolve(outputPath);
|
|
2829
|
+
try {
|
|
2830
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
2831
|
+
fs.writeFileSync(resolved, bytes);
|
|
2832
|
+
} catch (err) {
|
|
2833
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `failed to write ${resolved}: ${err.message}` }) }] };
|
|
2834
|
+
}
|
|
2835
|
+
return {
|
|
2836
|
+
content: [{
|
|
2837
|
+
type: "text",
|
|
2838
|
+
text: JSON.stringify({
|
|
2839
|
+
bytes_written: bytes.length,
|
|
2840
|
+
instance_count: response.instance_count ?? instancePaths.length,
|
|
2841
|
+
output_path: resolved
|
|
2842
|
+
})
|
|
2843
|
+
}]
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
2846
|
+
async importRbxm(source, parentPath, target) {
|
|
2847
|
+
if (!source || typeof source !== "object") {
|
|
2848
|
+
throw new Error("source is required for import_rbxm");
|
|
2849
|
+
}
|
|
2850
|
+
if (!parentPath || typeof parentPath !== "string") {
|
|
2851
|
+
throw new Error("parent_path is required for import_rbxm");
|
|
2852
|
+
}
|
|
2853
|
+
const tgt = target || "edit";
|
|
2854
|
+
if (tgt !== "edit" && tgt !== "server") {
|
|
2855
|
+
throw new Error(`import_rbxm target must be "edit" or "server" (got: ${tgt})`);
|
|
2856
|
+
}
|
|
2857
|
+
const modes = ["path", "url", "base64"].filter((k) => source[k] !== void 0);
|
|
2858
|
+
if (modes.length !== 1) {
|
|
2859
|
+
throw new Error(`source must contain exactly one of { path, url, base64 } (got: ${modes.join(", ") || "none"})`);
|
|
2860
|
+
}
|
|
2861
|
+
let bytes;
|
|
2862
|
+
let sourceLabel;
|
|
2863
|
+
if (source.path !== void 0) {
|
|
2864
|
+
const resolved = path.resolve(source.path);
|
|
2865
|
+
try {
|
|
2866
|
+
bytes = fs.readFileSync(resolved);
|
|
2867
|
+
} catch (err) {
|
|
2868
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `failed to read ${resolved}: ${err.message}` }) }] };
|
|
2869
|
+
}
|
|
2870
|
+
sourceLabel = resolved;
|
|
2871
|
+
} else if (source.url !== void 0) {
|
|
2872
|
+
let parsedUrl;
|
|
2873
|
+
try {
|
|
2874
|
+
parsedUrl = new URL(source.url);
|
|
2875
|
+
} catch {
|
|
2876
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `import_rbxm url is not a valid URL: ${source.url}` }) }] };
|
|
2877
|
+
}
|
|
2878
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
2879
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `import_rbxm url must use http(s); got ${parsedUrl.protocol}` }) }] };
|
|
2880
|
+
}
|
|
2881
|
+
const MAX_IMPORT_BYTES = 50 * 1024 * 1024;
|
|
2882
|
+
try {
|
|
2883
|
+
const res = await fetch(source.url);
|
|
2884
|
+
if (!res.ok) {
|
|
2885
|
+
const snippet = (await res.text()).slice(0, 500);
|
|
2886
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `fetch ${source.url} returned ${res.status}: ${snippet}` }) }] };
|
|
2887
|
+
}
|
|
2888
|
+
const claimed = Number(res.headers.get("content-length") ?? "0");
|
|
2889
|
+
if (claimed > MAX_IMPORT_BYTES) {
|
|
2890
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `fetch ${source.url}: content-length ${claimed} exceeds ${MAX_IMPORT_BYTES} byte cap` }) }] };
|
|
2891
|
+
}
|
|
2892
|
+
const arr = await res.arrayBuffer();
|
|
2893
|
+
if (arr.byteLength > MAX_IMPORT_BYTES) {
|
|
2894
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `fetch ${source.url}: downloaded ${arr.byteLength} bytes exceeds ${MAX_IMPORT_BYTES} byte cap` }) }] };
|
|
2895
|
+
}
|
|
2896
|
+
bytes = Buffer.from(arr);
|
|
2897
|
+
} catch (err) {
|
|
2898
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `fetch ${source.url} failed: ${err.message}` }) }] };
|
|
2899
|
+
}
|
|
2900
|
+
sourceLabel = source.url;
|
|
2901
|
+
} else {
|
|
2902
|
+
try {
|
|
2903
|
+
bytes = Buffer.from(source.base64, "base64");
|
|
2904
|
+
} catch (err) {
|
|
2905
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `base64 decode failed: ${err.message}` }) }] };
|
|
2906
|
+
}
|
|
2907
|
+
sourceLabel = `base64(${bytes.length}B)`;
|
|
2908
|
+
}
|
|
2909
|
+
const response = await this.client.request("/api/import-rbxm", {
|
|
2910
|
+
base64: bytes.toString("base64"),
|
|
2911
|
+
parent_path: parentPath,
|
|
2912
|
+
source_label: sourceLabel
|
|
2913
|
+
}, tgt);
|
|
2914
|
+
return { content: [{ type: "text", text: JSON.stringify(response) }] };
|
|
2915
|
+
}
|
|
2780
2916
|
async captureScreenshot() {
|
|
2781
2917
|
const response = await this.client.request("/api/capture-screenshot", {});
|
|
2782
2918
|
if (response.error) {
|
|
@@ -4746,6 +4882,86 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
4746
4882
|
required: ["instancePath", "attributes"]
|
|
4747
4883
|
}
|
|
4748
4884
|
},
|
|
4885
|
+
// === Per-peer memory breakdown ===
|
|
4886
|
+
{
|
|
4887
|
+
name: "get_memory_breakdown",
|
|
4888
|
+
category: "read",
|
|
4889
|
+
description: `Read per-category memory usage by iterating Enum.DeveloperMemoryTag and calling Stats:GetMemoryUsageMbForTag per item (workaround for Stats:GetMemoryUsageMbAllCategories being gated by Capabilities: InternalTest and not callable from plugin context), plus Stats:GetTotalMemoryUsageMb for the rollup. target="all" (default) returns { peer: { total_mb, categories, timestamp } } for every connected peer except edit-proxy; single-peer targets return that peer's object directly. Optional tags whitelist filters to only those DeveloperMemoryTag entries; unknown tags come back with value 0 and are listed in unknown_tags so cross-version drift doesn't error. timestamp is Unix milliseconds (DateTime.now().UnixTimestampMillis). Per-peer MemoryTrackingEnabled=false surfaces as { error } on that peer only.`,
|
|
4890
|
+
inputSchema: {
|
|
4891
|
+
type: "object",
|
|
4892
|
+
properties: {
|
|
4893
|
+
target: {
|
|
4894
|
+
type: "string",
|
|
4895
|
+
description: 'Peer to read from: "edit", "server", "client-N", or "all" (default).'
|
|
4896
|
+
},
|
|
4897
|
+
tags: {
|
|
4898
|
+
type: "array",
|
|
4899
|
+
items: { type: "string" },
|
|
4900
|
+
description: "Optional DeveloperMemoryTag whitelist. Unknown tag names return 0 + unknown_tags list."
|
|
4901
|
+
}
|
|
4902
|
+
}
|
|
4903
|
+
}
|
|
4904
|
+
},
|
|
4905
|
+
// === SerializationService round-trip ===
|
|
4906
|
+
{
|
|
4907
|
+
name: "export_rbxm",
|
|
4908
|
+
category: "read",
|
|
4909
|
+
description: "Serialize one or more instances to a .rbxm file on disk via SerializationService:SerializeInstancesAsync (engine v668+, PluginSecurity). Throws if any path resolves to nil, a service, or a non-creatable instance.",
|
|
4910
|
+
inputSchema: {
|
|
4911
|
+
type: "object",
|
|
4912
|
+
properties: {
|
|
4913
|
+
instance_paths: {
|
|
4914
|
+
type: "array",
|
|
4915
|
+
items: { type: "string" },
|
|
4916
|
+
description: 'DataModel paths to serialize (e.g. ["Workspace.TestRig", "ServerStorage.Templates.NPC"])'
|
|
4917
|
+
},
|
|
4918
|
+
output_path: {
|
|
4919
|
+
type: "string",
|
|
4920
|
+
description: "Absolute filesystem path where the .rbxm should be written"
|
|
4921
|
+
},
|
|
4922
|
+
target: {
|
|
4923
|
+
type: "string",
|
|
4924
|
+
enum: ["edit", "server"],
|
|
4925
|
+
description: 'Which DataModel to read from (default: "edit"). "server" serializes live runtime state during a playtest.'
|
|
4926
|
+
}
|
|
4927
|
+
},
|
|
4928
|
+
required: ["instance_paths", "output_path"]
|
|
4929
|
+
}
|
|
4930
|
+
},
|
|
4931
|
+
{
|
|
4932
|
+
name: "import_rbxm",
|
|
4933
|
+
category: "write",
|
|
4934
|
+
description: "Deserialize a .rbxm via SerializationService:DeserializeInstancesAsync (engine v668+, PluginSecurity) and parent the resulting instances under parent_path. All-or-nothing parenting: if any single instance fails to parent, every already-parented sibling is unparented and the call errors. Wrapped in ChangeHistoryService for edit target so one Ctrl+Z reverses the whole import.",
|
|
4935
|
+
inputSchema: {
|
|
4936
|
+
type: "object",
|
|
4937
|
+
properties: {
|
|
4938
|
+
source: {
|
|
4939
|
+
type: "object",
|
|
4940
|
+
description: "Exactly one of { path }, { url }, or { base64 }. path = read from local disk; url = http(s) only, fetched by the MCP server process, capped at 50 MiB; base64 = raw bytes inline.",
|
|
4941
|
+
properties: {
|
|
4942
|
+
path: { type: "string" },
|
|
4943
|
+
url: { type: "string" },
|
|
4944
|
+
base64: { type: "string" }
|
|
4945
|
+
},
|
|
4946
|
+
oneOf: [
|
|
4947
|
+
{ required: ["path"] },
|
|
4948
|
+
{ required: ["url"] },
|
|
4949
|
+
{ required: ["base64"] }
|
|
4950
|
+
]
|
|
4951
|
+
},
|
|
4952
|
+
parent_path: {
|
|
4953
|
+
type: "string",
|
|
4954
|
+
description: 'DataModel path of the Instance to parent imported instances under (e.g. "ServerStorage.Imported")'
|
|
4955
|
+
},
|
|
4956
|
+
target: {
|
|
4957
|
+
type: "string",
|
|
4958
|
+
enum: ["edit", "server"],
|
|
4959
|
+
description: 'Which DataModel to import into (default: "edit"). "server" parents into the live play-server DM.'
|
|
4960
|
+
}
|
|
4961
|
+
},
|
|
4962
|
+
required: ["source", "parent_path"]
|
|
4963
|
+
}
|
|
4964
|
+
},
|
|
4749
4965
|
// === Find and Replace ===
|
|
4750
4966
|
{
|
|
4751
4967
|
name: "find_and_replace_in_scripts",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrrxs/robloxstudio-mcp-inspector",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"description": "Read-only MCP Server for Roblox Studio (fork of boshyxd/robloxstudio-mcp-inspector with per-peer execute_luau fixes baked in)",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -86,6 +86,7 @@ local Players = _services.Players
|
|
|
86
86
|
local ReplicatedStorage = _services.ReplicatedStorage
|
|
87
87
|
local RunService = _services.RunService
|
|
88
88
|
local RuntimeLogBuffer = TS.import(script, script.Parent, "RuntimeLogBuffer")
|
|
89
|
+
local MemoryHandlers = TS.import(script, script.Parent, "handlers", "MemoryHandlers")
|
|
89
90
|
-- The client peer cannot reach the MCP HTTP server - Roblox forbids
|
|
90
91
|
-- HttpService:RequestAsync from the client DM even under PluginSecurity, and
|
|
91
92
|
-- HttpEnabled reads as false there regardless of identity. So the server peer
|
|
@@ -107,6 +108,7 @@ local BROKER_NAME = "__MCPClientBroker"
|
|
|
107
108
|
local CLIENT_BROKER_ALLOWED_ENDPOINTS = {
|
|
108
109
|
["/api/execute-luau"] = true,
|
|
109
110
|
["/api/get-runtime-logs"] = true,
|
|
111
|
+
["/api/get-memory-breakdown"] = true,
|
|
110
112
|
}
|
|
111
113
|
-- Throttle re-ready calls per proxyId so a brief window of unknownInstance
|
|
112
114
|
-- polls doesn't cause a re-register stampede.
|
|
@@ -219,6 +221,9 @@ local function setupClientBroker()
|
|
|
219
221
|
if payload and payload.endpoint == "/api/get-runtime-logs" then
|
|
220
222
|
return handleGetRuntimeLogs(payload.data)
|
|
221
223
|
end
|
|
224
|
+
if payload and payload.endpoint == "/api/get-memory-breakdown" then
|
|
225
|
+
return MemoryHandlers.getMemoryBreakdown(payload.data or {})
|
|
226
|
+
end
|
|
222
227
|
if payload and payload.endpoint == "/api/execute-luau" then
|
|
223
228
|
return handleExecuteLuau(payload.data)
|
|
224
229
|
end
|
|
@@ -443,6 +448,8 @@ local AssetHandlers = TS.import(script, script.Parent, "handlers", "AssetHandler
|
|
|
443
448
|
local CaptureHandlers = TS.import(script, script.Parent, "handlers", "CaptureHandlers")
|
|
444
449
|
local InputHandlers = TS.import(script, script.Parent, "handlers", "InputHandlers")
|
|
445
450
|
local LogHandlers = TS.import(script, script.Parent, "handlers", "LogHandlers")
|
|
451
|
+
local SerializationHandlers = TS.import(script, script.Parent, "handlers", "SerializationHandlers")
|
|
452
|
+
local MemoryHandlers = TS.import(script, script.Parent, "handlers", "MemoryHandlers")
|
|
446
453
|
local instanceId = HttpService:GenerateGUID(false)
|
|
447
454
|
local assignedRole
|
|
448
455
|
local function detectRole()
|
|
@@ -512,6 +519,9 @@ local routeMap = {
|
|
|
512
519
|
["/api/simulate-keyboard-input"] = InputHandlers.simulateKeyboardInput,
|
|
513
520
|
["/api/find-and-replace-in-scripts"] = ScriptHandlers.findAndReplaceInScripts,
|
|
514
521
|
["/api/get-runtime-logs"] = LogHandlers.getRuntimeLogs,
|
|
522
|
+
["/api/export-rbxm"] = SerializationHandlers.exportRbxm,
|
|
523
|
+
["/api/import-rbxm"] = SerializationHandlers.importRbxm,
|
|
524
|
+
["/api/get-memory-breakdown"] = MemoryHandlers.getMemoryBreakdown,
|
|
515
525
|
}
|
|
516
526
|
local function processRequest(request)
|
|
517
527
|
local endpoint = request.endpoint
|
|
@@ -2788,6 +2798,74 @@ return {
|
|
|
2788
2798
|
</Properties>
|
|
2789
2799
|
</Item>
|
|
2790
2800
|
<Item class="ModuleScript" referent="12">
|
|
2801
|
+
<Properties>
|
|
2802
|
+
<string name="Name">MemoryHandlers</string>
|
|
2803
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
2804
|
+
local Stats = game:GetService("Stats")
|
|
2805
|
+
-- GetMemoryUsageMbAllCategories is gated by capability "InternalTest" and not
|
|
2806
|
+
-- callable from plugin context. GetMemoryUsageMbForTag is not - so we iterate
|
|
2807
|
+
-- Enum.DeveloperMemoryTag and ask per-tag.
|
|
2808
|
+
local function getMemoryBreakdown(requestData)
|
|
2809
|
+
if not Stats.MemoryTrackingEnabled then
|
|
2810
|
+
return {
|
|
2811
|
+
error = "MemoryTrackingEnabled is false on this peer",
|
|
2812
|
+
}
|
|
2813
|
+
end
|
|
2814
|
+
local requested = requestData.tags
|
|
2815
|
+
local _result
|
|
2816
|
+
if requested and #requested > 0 then
|
|
2817
|
+
local _set = {}
|
|
2818
|
+
for _, _v in requested do
|
|
2819
|
+
_set[_v] = true
|
|
2820
|
+
end
|
|
2821
|
+
_result = _set
|
|
2822
|
+
else
|
|
2823
|
+
_result = nil
|
|
2824
|
+
end
|
|
2825
|
+
local requestedSet = _result
|
|
2826
|
+
local categories = {}
|
|
2827
|
+
for _, item in Enum.DeveloperMemoryTag:GetEnumItems() do
|
|
2828
|
+
local name = item.Name
|
|
2829
|
+
if requestedSet and not (requestedSet[name] ~= nil) then
|
|
2830
|
+
continue
|
|
2831
|
+
end
|
|
2832
|
+
local ok, mb = pcall(function()
|
|
2833
|
+
return Stats:GetMemoryUsageMbForTag(item)
|
|
2834
|
+
end)
|
|
2835
|
+
categories[name] = if ok then mb else 0
|
|
2836
|
+
end
|
|
2837
|
+
local unknownTags = {}
|
|
2838
|
+
if requestedSet then
|
|
2839
|
+
local known = {}
|
|
2840
|
+
for _, i in Enum.DeveloperMemoryTag:GetEnumItems() do
|
|
2841
|
+
local _name = i.Name
|
|
2842
|
+
known[_name] = true
|
|
2843
|
+
end
|
|
2844
|
+
for t in requestedSet do
|
|
2845
|
+
if not (known[t] ~= nil) then
|
|
2846
|
+
table.insert(unknownTags, t)
|
|
2847
|
+
categories[t] = 0
|
|
2848
|
+
end
|
|
2849
|
+
end
|
|
2850
|
+
end
|
|
2851
|
+
local result = {
|
|
2852
|
+
total_mb = Stats:GetTotalMemoryUsageMb(),
|
|
2853
|
+
categories = categories,
|
|
2854
|
+
memory_tracking_enabled = true,
|
|
2855
|
+
timestamp = DateTime.now().UnixTimestampMillis,
|
|
2856
|
+
}
|
|
2857
|
+
if #unknownTags > 0 then
|
|
2858
|
+
result.unknown_tags = unknownTags
|
|
2859
|
+
end
|
|
2860
|
+
return result
|
|
2861
|
+
end
|
|
2862
|
+
return {
|
|
2863
|
+
getMemoryBreakdown = getMemoryBreakdown,
|
|
2864
|
+
}
|
|
2865
|
+
]]></string>
|
|
2866
|
+
</Properties>
|
|
2867
|
+
</Item>
|
|
2868
|
+
<Item class="ModuleScript" referent="13">
|
|
2791
2869
|
<Properties>
|
|
2792
2870
|
<string name="Name">MetadataHandlers</string>
|
|
2793
2871
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -3424,7 +3502,7 @@ return {
|
|
|
3424
3502
|
]]></string>
|
|
3425
3503
|
</Properties>
|
|
3426
3504
|
</Item>
|
|
3427
|
-
<Item class="ModuleScript" referent="
|
|
3505
|
+
<Item class="ModuleScript" referent="14">
|
|
3428
3506
|
<Properties>
|
|
3429
3507
|
<string name="Name">PropertyHandlers</string>
|
|
3430
3508
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -3676,7 +3754,7 @@ return {
|
|
|
3676
3754
|
]]></string>
|
|
3677
3755
|
</Properties>
|
|
3678
3756
|
</Item>
|
|
3679
|
-
<Item class="ModuleScript" referent="
|
|
3757
|
+
<Item class="ModuleScript" referent="15">
|
|
3680
3758
|
<Properties>
|
|
3681
3759
|
<string name="Name">QueryHandlers</string>
|
|
3682
3760
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -4718,7 +4796,7 @@ return {
|
|
|
4718
4796
|
]]></string>
|
|
4719
4797
|
</Properties>
|
|
4720
4798
|
</Item>
|
|
4721
|
-
<Item class="ModuleScript" referent="
|
|
4799
|
+
<Item class="ModuleScript" referent="16">
|
|
4722
4800
|
<Properties>
|
|
4723
4801
|
<string name="Name">ScriptHandlers</string>
|
|
4724
4802
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5414,7 +5492,193 @@ return {
|
|
|
5414
5492
|
]]></string>
|
|
5415
5493
|
</Properties>
|
|
5416
5494
|
</Item>
|
|
5417
|
-
<Item class="ModuleScript" referent="
|
|
5495
|
+
<Item class="ModuleScript" referent="17">
|
|
5496
|
+
<Properties>
|
|
5497
|
+
<string name="Name">SerializationHandlers</string>
|
|
5498
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
5499
|
+
local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
|
|
5500
|
+
local RunService = TS.import(script, script.Parent.Parent.Parent, "node_modules", "@rbxts", "services").RunService
|
|
5501
|
+
local Utils = TS.import(script, script.Parent.Parent, "Utils")
|
|
5502
|
+
local Recording = TS.import(script, script.Parent.Parent, "Recording")
|
|
5503
|
+
-- SerializationService:SerializeInstancesAsync / DeserializeInstancesAsync were
|
|
5504
|
+
-- added in engine v668 and are PluginSecurity. They are not in @rbxts/types yet,
|
|
5505
|
+
-- so we resolve the service through an untyped GetService cast and treat the
|
|
5506
|
+
-- methods as opaque (buffer in / buffer out).
|
|
5507
|
+
local SerializationService = game:GetService("SerializationService")
|
|
5508
|
+
-- EncodingService:Base64Encode / Base64Decode take and return `buffer` (not
|
|
5509
|
+
-- `string`). The signature is in @rbxts/types under None.d.ts so a normal
|
|
5510
|
+
-- GetService("EncodingService") would already give correct types, but @rbxts
|
|
5511
|
+
-- generates a per-service nominal interface and roblox.d.ts doesn't re-export
|
|
5512
|
+
-- EncodingService from the services barrel module - so the typed cast below
|
|
5513
|
+
-- matches what GetService would give us if it did.
|
|
5514
|
+
local EncodingService = game:GetService("EncodingService")
|
|
5515
|
+
local _binding = Utils
|
|
5516
|
+
local getInstanceByPath = _binding.getInstanceByPath
|
|
5517
|
+
local getInstancePath = _binding.getInstancePath
|
|
5518
|
+
local _binding_1 = Recording
|
|
5519
|
+
local beginRecording = _binding_1.beginRecording
|
|
5520
|
+
local finishRecording = _binding_1.finishRecording
|
|
5521
|
+
local function exportRbxm(requestData)
|
|
5522
|
+
local instancePaths = requestData.instance_paths
|
|
5523
|
+
if not instancePaths or not (type(instancePaths) == "table") or #instancePaths == 0 then
|
|
5524
|
+
return {
|
|
5525
|
+
error = "instance_paths must be a non-empty array",
|
|
5526
|
+
}
|
|
5527
|
+
end
|
|
5528
|
+
local instances = {}
|
|
5529
|
+
for _, p in instancePaths do
|
|
5530
|
+
local inst = getInstanceByPath(p)
|
|
5531
|
+
if not inst then
|
|
5532
|
+
return {
|
|
5533
|
+
error = `instance not found: {p}`,
|
|
5534
|
+
}
|
|
5535
|
+
end
|
|
5536
|
+
table.insert(instances, inst)
|
|
5537
|
+
end
|
|
5538
|
+
local serializeOk, serializeResult = pcall(function()
|
|
5539
|
+
return SerializationService:SerializeInstancesAsync(instances)
|
|
5540
|
+
end)
|
|
5541
|
+
if not serializeOk then
|
|
5542
|
+
return {
|
|
5543
|
+
error = `SerializeInstancesAsync failed: {tostring(serializeResult)}`,
|
|
5544
|
+
}
|
|
5545
|
+
end
|
|
5546
|
+
local buf = serializeResult
|
|
5547
|
+
local encodeOk, encodeResult = pcall(function()
|
|
5548
|
+
return EncodingService:Base64Encode(buf)
|
|
5549
|
+
end)
|
|
5550
|
+
if not encodeOk then
|
|
5551
|
+
return {
|
|
5552
|
+
error = `EncodingService:Base64Encode failed: {tostring(encodeResult)}`,
|
|
5553
|
+
}
|
|
5554
|
+
end
|
|
5555
|
+
-- Base64Encode returns a buffer of ASCII bytes; convert to a Lua string so
|
|
5556
|
+
-- HttpService:JSONEncode (called by the harness in Communication.ts) accepts
|
|
5557
|
+
-- it. Base64 is by definition pure ASCII so this round-trips cleanly.
|
|
5558
|
+
local base64Str = buffer.tostring(encodeResult)
|
|
5559
|
+
return {
|
|
5560
|
+
base64 = base64Str,
|
|
5561
|
+
instance_count = #instances,
|
|
5562
|
+
}
|
|
5563
|
+
end
|
|
5564
|
+
local function importRbxm(requestData)
|
|
5565
|
+
local b64 = requestData.base64
|
|
5566
|
+
local parentPath = requestData.parent_path
|
|
5567
|
+
local _condition = (requestData.source_label)
|
|
5568
|
+
if _condition == nil then
|
|
5569
|
+
_condition = "unknown"
|
|
5570
|
+
end
|
|
5571
|
+
local sourceLabel = _condition
|
|
5572
|
+
if not (b64 ~= "" and b64) or not (type(b64) == "string") then
|
|
5573
|
+
return {
|
|
5574
|
+
error = "base64 payload is required",
|
|
5575
|
+
}
|
|
5576
|
+
end
|
|
5577
|
+
if not (parentPath ~= "" and parentPath) or not (type(parentPath) == "string") then
|
|
5578
|
+
return {
|
|
5579
|
+
error = "parent_path is required",
|
|
5580
|
+
}
|
|
5581
|
+
end
|
|
5582
|
+
local parentInstance = getInstanceByPath(parentPath)
|
|
5583
|
+
if not parentInstance then
|
|
5584
|
+
return {
|
|
5585
|
+
error = `parent instance not found: {parentPath}`,
|
|
5586
|
+
}
|
|
5587
|
+
end
|
|
5588
|
+
-- b64 is an ASCII-only Lua string from the wire; lift it into a buffer for
|
|
5589
|
+
-- EncodingService:Base64Decode, which returns a buffer of raw rbxm bytes
|
|
5590
|
+
-- ready for DeserializeInstancesAsync.
|
|
5591
|
+
local b64BufOk, b64BufResult = pcall(function()
|
|
5592
|
+
return buffer.fromstring(b64)
|
|
5593
|
+
end)
|
|
5594
|
+
if not b64BufOk then
|
|
5595
|
+
return {
|
|
5596
|
+
error = `buffer.fromstring(base64) failed: {tostring(b64BufResult)}`,
|
|
5597
|
+
}
|
|
5598
|
+
end
|
|
5599
|
+
local decodeOk, decodeResult = pcall(function()
|
|
5600
|
+
return EncodingService:Base64Decode(b64BufResult)
|
|
5601
|
+
end)
|
|
5602
|
+
if not decodeOk then
|
|
5603
|
+
return {
|
|
5604
|
+
error = `EncodingService:Base64Decode failed: {tostring(decodeResult)}`,
|
|
5605
|
+
}
|
|
5606
|
+
end
|
|
5607
|
+
local buf = decodeResult
|
|
5608
|
+
local deserOk, deserResult = pcall(function()
|
|
5609
|
+
return SerializationService:DeserializeInstancesAsync(buf)
|
|
5610
|
+
end)
|
|
5611
|
+
if not deserOk then
|
|
5612
|
+
return {
|
|
5613
|
+
error = `DeserializeInstancesAsync failed: {tostring(deserResult)}`,
|
|
5614
|
+
}
|
|
5615
|
+
end
|
|
5616
|
+
local deserialized = deserResult
|
|
5617
|
+
-- All-or-nothing parenting. Track every instance we've attached and roll back
|
|
5618
|
+
-- (unparent + Destroy) if any later one fails - partial imports leave the DM
|
|
5619
|
+
-- in a worse state than failing cleanly.
|
|
5620
|
+
local isEdit = not RunService:IsRunning()
|
|
5621
|
+
local recordingId = if isEdit then beginRecording(`Import rbxm`) else nil
|
|
5622
|
+
local attached = {}
|
|
5623
|
+
local failureMessage
|
|
5624
|
+
for _, inst in deserialized do
|
|
5625
|
+
local parentOk, parentErr = pcall(function()
|
|
5626
|
+
inst.Parent = parentInstance
|
|
5627
|
+
end)
|
|
5628
|
+
if not parentOk then
|
|
5629
|
+
failureMessage = `failed to parent {inst.Name} ({inst.ClassName}) under {parentPath}: {tostring(parentErr)}`
|
|
5630
|
+
break
|
|
5631
|
+
end
|
|
5632
|
+
table.insert(attached, inst)
|
|
5633
|
+
end
|
|
5634
|
+
if failureMessage ~= nil then
|
|
5635
|
+
for _, inst in attached do
|
|
5636
|
+
pcall(function()
|
|
5637
|
+
inst.Parent = nil
|
|
5638
|
+
inst:Destroy()
|
|
5639
|
+
end)
|
|
5640
|
+
end
|
|
5641
|
+
-- Also destroy any unparented deserialized instances so they don't leak.
|
|
5642
|
+
for _, inst in deserialized do
|
|
5643
|
+
if inst.Parent == nil then
|
|
5644
|
+
pcall(function()
|
|
5645
|
+
return inst:Destroy()
|
|
5646
|
+
end)
|
|
5647
|
+
end
|
|
5648
|
+
end
|
|
5649
|
+
finishRecording(recordingId, false)
|
|
5650
|
+
return {
|
|
5651
|
+
error = failureMessage,
|
|
5652
|
+
}
|
|
5653
|
+
end
|
|
5654
|
+
local names = {}
|
|
5655
|
+
local paths = {}
|
|
5656
|
+
for _, inst in attached do
|
|
5657
|
+
local _name = inst.Name
|
|
5658
|
+
table.insert(names, _name)
|
|
5659
|
+
local _arg0 = getInstancePath(inst)
|
|
5660
|
+
table.insert(paths, _arg0)
|
|
5661
|
+
end
|
|
5662
|
+
-- The recording shows "MCP: Import rbxm" in Studio's undo stack -
|
|
5663
|
+
-- ChangeHistoryService doesn't expose a way to set a richer displayName
|
|
5664
|
+
-- after TryBeginRecording, so the count/source only land in the JSON response.
|
|
5665
|
+
finishRecording(recordingId, true)
|
|
5666
|
+
return {
|
|
5667
|
+
instance_count = #attached,
|
|
5668
|
+
instance_names = names,
|
|
5669
|
+
instance_paths = paths,
|
|
5670
|
+
parent_path = parentPath,
|
|
5671
|
+
source = sourceLabel,
|
|
5672
|
+
}
|
|
5673
|
+
end
|
|
5674
|
+
return {
|
|
5675
|
+
exportRbxm = exportRbxm,
|
|
5676
|
+
importRbxm = importRbxm,
|
|
5677
|
+
}
|
|
5678
|
+
]]></string>
|
|
5679
|
+
</Properties>
|
|
5680
|
+
</Item>
|
|
5681
|
+
<Item class="ModuleScript" referent="18">
|
|
5418
5682
|
<Properties>
|
|
5419
5683
|
<string name="Name">TestHandlers</string>
|
|
5420
5684
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5724,7 +5988,7 @@ return {
|
|
|
5724
5988
|
</Properties>
|
|
5725
5989
|
</Item>
|
|
5726
5990
|
</Item>
|
|
5727
|
-
<Item class="ModuleScript" referent="
|
|
5991
|
+
<Item class="ModuleScript" referent="19">
|
|
5728
5992
|
<Properties>
|
|
5729
5993
|
<string name="Name">Recording</string>
|
|
5730
5994
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5754,7 +6018,7 @@ return {
|
|
|
5754
6018
|
]]></string>
|
|
5755
6019
|
</Properties>
|
|
5756
6020
|
</Item>
|
|
5757
|
-
<Item class="ModuleScript" referent="
|
|
6021
|
+
<Item class="ModuleScript" referent="20">
|
|
5758
6022
|
<Properties>
|
|
5759
6023
|
<string name="Name">RuntimeLogBuffer</string>
|
|
5760
6024
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5935,11 +6199,11 @@ return {
|
|
|
5935
6199
|
]]></string>
|
|
5936
6200
|
</Properties>
|
|
5937
6201
|
</Item>
|
|
5938
|
-
<Item class="ModuleScript" referent="
|
|
6202
|
+
<Item class="ModuleScript" referent="21">
|
|
5939
6203
|
<Properties>
|
|
5940
6204
|
<string name="Name">State</string>
|
|
5941
6205
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
5942
|
-
local CURRENT_VERSION = "2.
|
|
6206
|
+
local CURRENT_VERSION = "2.11.0"
|
|
5943
6207
|
local MAX_CONNECTIONS = 5
|
|
5944
6208
|
local BASE_PORT = 58741
|
|
5945
6209
|
local activeTabIndex = 0
|
|
@@ -6031,7 +6295,7 @@ return {
|
|
|
6031
6295
|
]]></string>
|
|
6032
6296
|
</Properties>
|
|
6033
6297
|
</Item>
|
|
6034
|
-
<Item class="ModuleScript" referent="
|
|
6298
|
+
<Item class="ModuleScript" referent="22">
|
|
6035
6299
|
<Properties>
|
|
6036
6300
|
<string name="Name">UI</string>
|
|
6037
6301
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -6782,7 +7046,7 @@ return {
|
|
|
6782
7046
|
]]></string>
|
|
6783
7047
|
</Properties>
|
|
6784
7048
|
</Item>
|
|
6785
|
-
<Item class="ModuleScript" referent="
|
|
7049
|
+
<Item class="ModuleScript" referent="23">
|
|
6786
7050
|
<Properties>
|
|
6787
7051
|
<string name="Name">Utils</string>
|
|
6788
7052
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -7312,11 +7576,11 @@ return {
|
|
|
7312
7576
|
</Properties>
|
|
7313
7577
|
</Item>
|
|
7314
7578
|
</Item>
|
|
7315
|
-
<Item class="Folder" referent="
|
|
7579
|
+
<Item class="Folder" referent="27">
|
|
7316
7580
|
<Properties>
|
|
7317
7581
|
<string name="Name">include</string>
|
|
7318
7582
|
</Properties>
|
|
7319
|
-
<Item class="ModuleScript" referent="
|
|
7583
|
+
<Item class="ModuleScript" referent="24">
|
|
7320
7584
|
<Properties>
|
|
7321
7585
|
<string name="Name">Promise</string>
|
|
7322
7586
|
<string name="Source"><![CDATA[--[[
|
|
@@ -9390,7 +9654,7 @@ return Promise
|
|
|
9390
9654
|
]]></string>
|
|
9391
9655
|
</Properties>
|
|
9392
9656
|
</Item>
|
|
9393
|
-
<Item class="ModuleScript" referent="
|
|
9657
|
+
<Item class="ModuleScript" referent="25">
|
|
9394
9658
|
<Properties>
|
|
9395
9659
|
<string name="Name">RuntimeLib</string>
|
|
9396
9660
|
<string name="Source"><![CDATA[local Promise = require(script.Parent.Promise)
|
|
@@ -9657,15 +9921,15 @@ return TS
|
|
|
9657
9921
|
</Properties>
|
|
9658
9922
|
</Item>
|
|
9659
9923
|
</Item>
|
|
9660
|
-
<Item class="Folder" referent="
|
|
9924
|
+
<Item class="Folder" referent="28">
|
|
9661
9925
|
<Properties>
|
|
9662
9926
|
<string name="Name">node_modules</string>
|
|
9663
9927
|
</Properties>
|
|
9664
|
-
<Item class="Folder" referent="
|
|
9928
|
+
<Item class="Folder" referent="29">
|
|
9665
9929
|
<Properties>
|
|
9666
9930
|
<string name="Name">@rbxts</string>
|
|
9667
9931
|
</Properties>
|
|
9668
|
-
<Item class="ModuleScript" referent="
|
|
9932
|
+
<Item class="ModuleScript" referent="26">
|
|
9669
9933
|
<Properties>
|
|
9670
9934
|
<string name="Name">services</string>
|
|
9671
9935
|
<string name="Source"><![CDATA[return setmetatable({}, {
|
|
@@ -86,6 +86,7 @@ local Players = _services.Players
|
|
|
86
86
|
local ReplicatedStorage = _services.ReplicatedStorage
|
|
87
87
|
local RunService = _services.RunService
|
|
88
88
|
local RuntimeLogBuffer = TS.import(script, script.Parent, "RuntimeLogBuffer")
|
|
89
|
+
local MemoryHandlers = TS.import(script, script.Parent, "handlers", "MemoryHandlers")
|
|
89
90
|
-- The client peer cannot reach the MCP HTTP server - Roblox forbids
|
|
90
91
|
-- HttpService:RequestAsync from the client DM even under PluginSecurity, and
|
|
91
92
|
-- HttpEnabled reads as false there regardless of identity. So the server peer
|
|
@@ -107,6 +108,7 @@ local BROKER_NAME = "__MCPClientBroker"
|
|
|
107
108
|
local CLIENT_BROKER_ALLOWED_ENDPOINTS = {
|
|
108
109
|
["/api/execute-luau"] = true,
|
|
109
110
|
["/api/get-runtime-logs"] = true,
|
|
111
|
+
["/api/get-memory-breakdown"] = true,
|
|
110
112
|
}
|
|
111
113
|
-- Throttle re-ready calls per proxyId so a brief window of unknownInstance
|
|
112
114
|
-- polls doesn't cause a re-register stampede.
|
|
@@ -219,6 +221,9 @@ local function setupClientBroker()
|
|
|
219
221
|
if payload and payload.endpoint == "/api/get-runtime-logs" then
|
|
220
222
|
return handleGetRuntimeLogs(payload.data)
|
|
221
223
|
end
|
|
224
|
+
if payload and payload.endpoint == "/api/get-memory-breakdown" then
|
|
225
|
+
return MemoryHandlers.getMemoryBreakdown(payload.data or {})
|
|
226
|
+
end
|
|
222
227
|
if payload and payload.endpoint == "/api/execute-luau" then
|
|
223
228
|
return handleExecuteLuau(payload.data)
|
|
224
229
|
end
|
|
@@ -443,6 +448,8 @@ local AssetHandlers = TS.import(script, script.Parent, "handlers", "AssetHandler
|
|
|
443
448
|
local CaptureHandlers = TS.import(script, script.Parent, "handlers", "CaptureHandlers")
|
|
444
449
|
local InputHandlers = TS.import(script, script.Parent, "handlers", "InputHandlers")
|
|
445
450
|
local LogHandlers = TS.import(script, script.Parent, "handlers", "LogHandlers")
|
|
451
|
+
local SerializationHandlers = TS.import(script, script.Parent, "handlers", "SerializationHandlers")
|
|
452
|
+
local MemoryHandlers = TS.import(script, script.Parent, "handlers", "MemoryHandlers")
|
|
446
453
|
local instanceId = HttpService:GenerateGUID(false)
|
|
447
454
|
local assignedRole
|
|
448
455
|
local function detectRole()
|
|
@@ -512,6 +519,9 @@ local routeMap = {
|
|
|
512
519
|
["/api/simulate-keyboard-input"] = InputHandlers.simulateKeyboardInput,
|
|
513
520
|
["/api/find-and-replace-in-scripts"] = ScriptHandlers.findAndReplaceInScripts,
|
|
514
521
|
["/api/get-runtime-logs"] = LogHandlers.getRuntimeLogs,
|
|
522
|
+
["/api/export-rbxm"] = SerializationHandlers.exportRbxm,
|
|
523
|
+
["/api/import-rbxm"] = SerializationHandlers.importRbxm,
|
|
524
|
+
["/api/get-memory-breakdown"] = MemoryHandlers.getMemoryBreakdown,
|
|
515
525
|
}
|
|
516
526
|
local function processRequest(request)
|
|
517
527
|
local endpoint = request.endpoint
|
|
@@ -2788,6 +2798,74 @@ return {
|
|
|
2788
2798
|
</Properties>
|
|
2789
2799
|
</Item>
|
|
2790
2800
|
<Item class="ModuleScript" referent="12">
|
|
2801
|
+
<Properties>
|
|
2802
|
+
<string name="Name">MemoryHandlers</string>
|
|
2803
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
2804
|
+
local Stats = game:GetService("Stats")
|
|
2805
|
+
-- GetMemoryUsageMbAllCategories is gated by capability "InternalTest" and not
|
|
2806
|
+
-- callable from plugin context. GetMemoryUsageMbForTag is not - so we iterate
|
|
2807
|
+
-- Enum.DeveloperMemoryTag and ask per-tag.
|
|
2808
|
+
local function getMemoryBreakdown(requestData)
|
|
2809
|
+
if not Stats.MemoryTrackingEnabled then
|
|
2810
|
+
return {
|
|
2811
|
+
error = "MemoryTrackingEnabled is false on this peer",
|
|
2812
|
+
}
|
|
2813
|
+
end
|
|
2814
|
+
local requested = requestData.tags
|
|
2815
|
+
local _result
|
|
2816
|
+
if requested and #requested > 0 then
|
|
2817
|
+
local _set = {}
|
|
2818
|
+
for _, _v in requested do
|
|
2819
|
+
_set[_v] = true
|
|
2820
|
+
end
|
|
2821
|
+
_result = _set
|
|
2822
|
+
else
|
|
2823
|
+
_result = nil
|
|
2824
|
+
end
|
|
2825
|
+
local requestedSet = _result
|
|
2826
|
+
local categories = {}
|
|
2827
|
+
for _, item in Enum.DeveloperMemoryTag:GetEnumItems() do
|
|
2828
|
+
local name = item.Name
|
|
2829
|
+
if requestedSet and not (requestedSet[name] ~= nil) then
|
|
2830
|
+
continue
|
|
2831
|
+
end
|
|
2832
|
+
local ok, mb = pcall(function()
|
|
2833
|
+
return Stats:GetMemoryUsageMbForTag(item)
|
|
2834
|
+
end)
|
|
2835
|
+
categories[name] = if ok then mb else 0
|
|
2836
|
+
end
|
|
2837
|
+
local unknownTags = {}
|
|
2838
|
+
if requestedSet then
|
|
2839
|
+
local known = {}
|
|
2840
|
+
for _, i in Enum.DeveloperMemoryTag:GetEnumItems() do
|
|
2841
|
+
local _name = i.Name
|
|
2842
|
+
known[_name] = true
|
|
2843
|
+
end
|
|
2844
|
+
for t in requestedSet do
|
|
2845
|
+
if not (known[t] ~= nil) then
|
|
2846
|
+
table.insert(unknownTags, t)
|
|
2847
|
+
categories[t] = 0
|
|
2848
|
+
end
|
|
2849
|
+
end
|
|
2850
|
+
end
|
|
2851
|
+
local result = {
|
|
2852
|
+
total_mb = Stats:GetTotalMemoryUsageMb(),
|
|
2853
|
+
categories = categories,
|
|
2854
|
+
memory_tracking_enabled = true,
|
|
2855
|
+
timestamp = DateTime.now().UnixTimestampMillis,
|
|
2856
|
+
}
|
|
2857
|
+
if #unknownTags > 0 then
|
|
2858
|
+
result.unknown_tags = unknownTags
|
|
2859
|
+
end
|
|
2860
|
+
return result
|
|
2861
|
+
end
|
|
2862
|
+
return {
|
|
2863
|
+
getMemoryBreakdown = getMemoryBreakdown,
|
|
2864
|
+
}
|
|
2865
|
+
]]></string>
|
|
2866
|
+
</Properties>
|
|
2867
|
+
</Item>
|
|
2868
|
+
<Item class="ModuleScript" referent="13">
|
|
2791
2869
|
<Properties>
|
|
2792
2870
|
<string name="Name">MetadataHandlers</string>
|
|
2793
2871
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -3424,7 +3502,7 @@ return {
|
|
|
3424
3502
|
]]></string>
|
|
3425
3503
|
</Properties>
|
|
3426
3504
|
</Item>
|
|
3427
|
-
<Item class="ModuleScript" referent="
|
|
3505
|
+
<Item class="ModuleScript" referent="14">
|
|
3428
3506
|
<Properties>
|
|
3429
3507
|
<string name="Name">PropertyHandlers</string>
|
|
3430
3508
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -3676,7 +3754,7 @@ return {
|
|
|
3676
3754
|
]]></string>
|
|
3677
3755
|
</Properties>
|
|
3678
3756
|
</Item>
|
|
3679
|
-
<Item class="ModuleScript" referent="
|
|
3757
|
+
<Item class="ModuleScript" referent="15">
|
|
3680
3758
|
<Properties>
|
|
3681
3759
|
<string name="Name">QueryHandlers</string>
|
|
3682
3760
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -4718,7 +4796,7 @@ return {
|
|
|
4718
4796
|
]]></string>
|
|
4719
4797
|
</Properties>
|
|
4720
4798
|
</Item>
|
|
4721
|
-
<Item class="ModuleScript" referent="
|
|
4799
|
+
<Item class="ModuleScript" referent="16">
|
|
4722
4800
|
<Properties>
|
|
4723
4801
|
<string name="Name">ScriptHandlers</string>
|
|
4724
4802
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5414,7 +5492,193 @@ return {
|
|
|
5414
5492
|
]]></string>
|
|
5415
5493
|
</Properties>
|
|
5416
5494
|
</Item>
|
|
5417
|
-
<Item class="ModuleScript" referent="
|
|
5495
|
+
<Item class="ModuleScript" referent="17">
|
|
5496
|
+
<Properties>
|
|
5497
|
+
<string name="Name">SerializationHandlers</string>
|
|
5498
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
5499
|
+
local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
|
|
5500
|
+
local RunService = TS.import(script, script.Parent.Parent.Parent, "node_modules", "@rbxts", "services").RunService
|
|
5501
|
+
local Utils = TS.import(script, script.Parent.Parent, "Utils")
|
|
5502
|
+
local Recording = TS.import(script, script.Parent.Parent, "Recording")
|
|
5503
|
+
-- SerializationService:SerializeInstancesAsync / DeserializeInstancesAsync were
|
|
5504
|
+
-- added in engine v668 and are PluginSecurity. They are not in @rbxts/types yet,
|
|
5505
|
+
-- so we resolve the service through an untyped GetService cast and treat the
|
|
5506
|
+
-- methods as opaque (buffer in / buffer out).
|
|
5507
|
+
local SerializationService = game:GetService("SerializationService")
|
|
5508
|
+
-- EncodingService:Base64Encode / Base64Decode take and return `buffer` (not
|
|
5509
|
+
-- `string`). The signature is in @rbxts/types under None.d.ts so a normal
|
|
5510
|
+
-- GetService("EncodingService") would already give correct types, but @rbxts
|
|
5511
|
+
-- generates a per-service nominal interface and roblox.d.ts doesn't re-export
|
|
5512
|
+
-- EncodingService from the services barrel module - so the typed cast below
|
|
5513
|
+
-- matches what GetService would give us if it did.
|
|
5514
|
+
local EncodingService = game:GetService("EncodingService")
|
|
5515
|
+
local _binding = Utils
|
|
5516
|
+
local getInstanceByPath = _binding.getInstanceByPath
|
|
5517
|
+
local getInstancePath = _binding.getInstancePath
|
|
5518
|
+
local _binding_1 = Recording
|
|
5519
|
+
local beginRecording = _binding_1.beginRecording
|
|
5520
|
+
local finishRecording = _binding_1.finishRecording
|
|
5521
|
+
local function exportRbxm(requestData)
|
|
5522
|
+
local instancePaths = requestData.instance_paths
|
|
5523
|
+
if not instancePaths or not (type(instancePaths) == "table") or #instancePaths == 0 then
|
|
5524
|
+
return {
|
|
5525
|
+
error = "instance_paths must be a non-empty array",
|
|
5526
|
+
}
|
|
5527
|
+
end
|
|
5528
|
+
local instances = {}
|
|
5529
|
+
for _, p in instancePaths do
|
|
5530
|
+
local inst = getInstanceByPath(p)
|
|
5531
|
+
if not inst then
|
|
5532
|
+
return {
|
|
5533
|
+
error = `instance not found: {p}`,
|
|
5534
|
+
}
|
|
5535
|
+
end
|
|
5536
|
+
table.insert(instances, inst)
|
|
5537
|
+
end
|
|
5538
|
+
local serializeOk, serializeResult = pcall(function()
|
|
5539
|
+
return SerializationService:SerializeInstancesAsync(instances)
|
|
5540
|
+
end)
|
|
5541
|
+
if not serializeOk then
|
|
5542
|
+
return {
|
|
5543
|
+
error = `SerializeInstancesAsync failed: {tostring(serializeResult)}`,
|
|
5544
|
+
}
|
|
5545
|
+
end
|
|
5546
|
+
local buf = serializeResult
|
|
5547
|
+
local encodeOk, encodeResult = pcall(function()
|
|
5548
|
+
return EncodingService:Base64Encode(buf)
|
|
5549
|
+
end)
|
|
5550
|
+
if not encodeOk then
|
|
5551
|
+
return {
|
|
5552
|
+
error = `EncodingService:Base64Encode failed: {tostring(encodeResult)}`,
|
|
5553
|
+
}
|
|
5554
|
+
end
|
|
5555
|
+
-- Base64Encode returns a buffer of ASCII bytes; convert to a Lua string so
|
|
5556
|
+
-- HttpService:JSONEncode (called by the harness in Communication.ts) accepts
|
|
5557
|
+
-- it. Base64 is by definition pure ASCII so this round-trips cleanly.
|
|
5558
|
+
local base64Str = buffer.tostring(encodeResult)
|
|
5559
|
+
return {
|
|
5560
|
+
base64 = base64Str,
|
|
5561
|
+
instance_count = #instances,
|
|
5562
|
+
}
|
|
5563
|
+
end
|
|
5564
|
+
local function importRbxm(requestData)
|
|
5565
|
+
local b64 = requestData.base64
|
|
5566
|
+
local parentPath = requestData.parent_path
|
|
5567
|
+
local _condition = (requestData.source_label)
|
|
5568
|
+
if _condition == nil then
|
|
5569
|
+
_condition = "unknown"
|
|
5570
|
+
end
|
|
5571
|
+
local sourceLabel = _condition
|
|
5572
|
+
if not (b64 ~= "" and b64) or not (type(b64) == "string") then
|
|
5573
|
+
return {
|
|
5574
|
+
error = "base64 payload is required",
|
|
5575
|
+
}
|
|
5576
|
+
end
|
|
5577
|
+
if not (parentPath ~= "" and parentPath) or not (type(parentPath) == "string") then
|
|
5578
|
+
return {
|
|
5579
|
+
error = "parent_path is required",
|
|
5580
|
+
}
|
|
5581
|
+
end
|
|
5582
|
+
local parentInstance = getInstanceByPath(parentPath)
|
|
5583
|
+
if not parentInstance then
|
|
5584
|
+
return {
|
|
5585
|
+
error = `parent instance not found: {parentPath}`,
|
|
5586
|
+
}
|
|
5587
|
+
end
|
|
5588
|
+
-- b64 is an ASCII-only Lua string from the wire; lift it into a buffer for
|
|
5589
|
+
-- EncodingService:Base64Decode, which returns a buffer of raw rbxm bytes
|
|
5590
|
+
-- ready for DeserializeInstancesAsync.
|
|
5591
|
+
local b64BufOk, b64BufResult = pcall(function()
|
|
5592
|
+
return buffer.fromstring(b64)
|
|
5593
|
+
end)
|
|
5594
|
+
if not b64BufOk then
|
|
5595
|
+
return {
|
|
5596
|
+
error = `buffer.fromstring(base64) failed: {tostring(b64BufResult)}`,
|
|
5597
|
+
}
|
|
5598
|
+
end
|
|
5599
|
+
local decodeOk, decodeResult = pcall(function()
|
|
5600
|
+
return EncodingService:Base64Decode(b64BufResult)
|
|
5601
|
+
end)
|
|
5602
|
+
if not decodeOk then
|
|
5603
|
+
return {
|
|
5604
|
+
error = `EncodingService:Base64Decode failed: {tostring(decodeResult)}`,
|
|
5605
|
+
}
|
|
5606
|
+
end
|
|
5607
|
+
local buf = decodeResult
|
|
5608
|
+
local deserOk, deserResult = pcall(function()
|
|
5609
|
+
return SerializationService:DeserializeInstancesAsync(buf)
|
|
5610
|
+
end)
|
|
5611
|
+
if not deserOk then
|
|
5612
|
+
return {
|
|
5613
|
+
error = `DeserializeInstancesAsync failed: {tostring(deserResult)}`,
|
|
5614
|
+
}
|
|
5615
|
+
end
|
|
5616
|
+
local deserialized = deserResult
|
|
5617
|
+
-- All-or-nothing parenting. Track every instance we've attached and roll back
|
|
5618
|
+
-- (unparent + Destroy) if any later one fails - partial imports leave the DM
|
|
5619
|
+
-- in a worse state than failing cleanly.
|
|
5620
|
+
local isEdit = not RunService:IsRunning()
|
|
5621
|
+
local recordingId = if isEdit then beginRecording(`Import rbxm`) else nil
|
|
5622
|
+
local attached = {}
|
|
5623
|
+
local failureMessage
|
|
5624
|
+
for _, inst in deserialized do
|
|
5625
|
+
local parentOk, parentErr = pcall(function()
|
|
5626
|
+
inst.Parent = parentInstance
|
|
5627
|
+
end)
|
|
5628
|
+
if not parentOk then
|
|
5629
|
+
failureMessage = `failed to parent {inst.Name} ({inst.ClassName}) under {parentPath}: {tostring(parentErr)}`
|
|
5630
|
+
break
|
|
5631
|
+
end
|
|
5632
|
+
table.insert(attached, inst)
|
|
5633
|
+
end
|
|
5634
|
+
if failureMessage ~= nil then
|
|
5635
|
+
for _, inst in attached do
|
|
5636
|
+
pcall(function()
|
|
5637
|
+
inst.Parent = nil
|
|
5638
|
+
inst:Destroy()
|
|
5639
|
+
end)
|
|
5640
|
+
end
|
|
5641
|
+
-- Also destroy any unparented deserialized instances so they don't leak.
|
|
5642
|
+
for _, inst in deserialized do
|
|
5643
|
+
if inst.Parent == nil then
|
|
5644
|
+
pcall(function()
|
|
5645
|
+
return inst:Destroy()
|
|
5646
|
+
end)
|
|
5647
|
+
end
|
|
5648
|
+
end
|
|
5649
|
+
finishRecording(recordingId, false)
|
|
5650
|
+
return {
|
|
5651
|
+
error = failureMessage,
|
|
5652
|
+
}
|
|
5653
|
+
end
|
|
5654
|
+
local names = {}
|
|
5655
|
+
local paths = {}
|
|
5656
|
+
for _, inst in attached do
|
|
5657
|
+
local _name = inst.Name
|
|
5658
|
+
table.insert(names, _name)
|
|
5659
|
+
local _arg0 = getInstancePath(inst)
|
|
5660
|
+
table.insert(paths, _arg0)
|
|
5661
|
+
end
|
|
5662
|
+
-- The recording shows "MCP: Import rbxm" in Studio's undo stack -
|
|
5663
|
+
-- ChangeHistoryService doesn't expose a way to set a richer displayName
|
|
5664
|
+
-- after TryBeginRecording, so the count/source only land in the JSON response.
|
|
5665
|
+
finishRecording(recordingId, true)
|
|
5666
|
+
return {
|
|
5667
|
+
instance_count = #attached,
|
|
5668
|
+
instance_names = names,
|
|
5669
|
+
instance_paths = paths,
|
|
5670
|
+
parent_path = parentPath,
|
|
5671
|
+
source = sourceLabel,
|
|
5672
|
+
}
|
|
5673
|
+
end
|
|
5674
|
+
return {
|
|
5675
|
+
exportRbxm = exportRbxm,
|
|
5676
|
+
importRbxm = importRbxm,
|
|
5677
|
+
}
|
|
5678
|
+
]]></string>
|
|
5679
|
+
</Properties>
|
|
5680
|
+
</Item>
|
|
5681
|
+
<Item class="ModuleScript" referent="18">
|
|
5418
5682
|
<Properties>
|
|
5419
5683
|
<string name="Name">TestHandlers</string>
|
|
5420
5684
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5724,7 +5988,7 @@ return {
|
|
|
5724
5988
|
</Properties>
|
|
5725
5989
|
</Item>
|
|
5726
5990
|
</Item>
|
|
5727
|
-
<Item class="ModuleScript" referent="
|
|
5991
|
+
<Item class="ModuleScript" referent="19">
|
|
5728
5992
|
<Properties>
|
|
5729
5993
|
<string name="Name">Recording</string>
|
|
5730
5994
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5754,7 +6018,7 @@ return {
|
|
|
5754
6018
|
]]></string>
|
|
5755
6019
|
</Properties>
|
|
5756
6020
|
</Item>
|
|
5757
|
-
<Item class="ModuleScript" referent="
|
|
6021
|
+
<Item class="ModuleScript" referent="20">
|
|
5758
6022
|
<Properties>
|
|
5759
6023
|
<string name="Name">RuntimeLogBuffer</string>
|
|
5760
6024
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5935,11 +6199,11 @@ return {
|
|
|
5935
6199
|
]]></string>
|
|
5936
6200
|
</Properties>
|
|
5937
6201
|
</Item>
|
|
5938
|
-
<Item class="ModuleScript" referent="
|
|
6202
|
+
<Item class="ModuleScript" referent="21">
|
|
5939
6203
|
<Properties>
|
|
5940
6204
|
<string name="Name">State</string>
|
|
5941
6205
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
5942
|
-
local CURRENT_VERSION = "2.
|
|
6206
|
+
local CURRENT_VERSION = "2.11.0"
|
|
5943
6207
|
local MAX_CONNECTIONS = 5
|
|
5944
6208
|
local BASE_PORT = 58741
|
|
5945
6209
|
local activeTabIndex = 0
|
|
@@ -6031,7 +6295,7 @@ return {
|
|
|
6031
6295
|
]]></string>
|
|
6032
6296
|
</Properties>
|
|
6033
6297
|
</Item>
|
|
6034
|
-
<Item class="ModuleScript" referent="
|
|
6298
|
+
<Item class="ModuleScript" referent="22">
|
|
6035
6299
|
<Properties>
|
|
6036
6300
|
<string name="Name">UI</string>
|
|
6037
6301
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -6782,7 +7046,7 @@ return {
|
|
|
6782
7046
|
]]></string>
|
|
6783
7047
|
</Properties>
|
|
6784
7048
|
</Item>
|
|
6785
|
-
<Item class="ModuleScript" referent="
|
|
7049
|
+
<Item class="ModuleScript" referent="23">
|
|
6786
7050
|
<Properties>
|
|
6787
7051
|
<string name="Name">Utils</string>
|
|
6788
7052
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -7312,11 +7576,11 @@ return {
|
|
|
7312
7576
|
</Properties>
|
|
7313
7577
|
</Item>
|
|
7314
7578
|
</Item>
|
|
7315
|
-
<Item class="Folder" referent="
|
|
7579
|
+
<Item class="Folder" referent="27">
|
|
7316
7580
|
<Properties>
|
|
7317
7581
|
<string name="Name">include</string>
|
|
7318
7582
|
</Properties>
|
|
7319
|
-
<Item class="ModuleScript" referent="
|
|
7583
|
+
<Item class="ModuleScript" referent="24">
|
|
7320
7584
|
<Properties>
|
|
7321
7585
|
<string name="Name">Promise</string>
|
|
7322
7586
|
<string name="Source"><![CDATA[--[[
|
|
@@ -9390,7 +9654,7 @@ return Promise
|
|
|
9390
9654
|
]]></string>
|
|
9391
9655
|
</Properties>
|
|
9392
9656
|
</Item>
|
|
9393
|
-
<Item class="ModuleScript" referent="
|
|
9657
|
+
<Item class="ModuleScript" referent="25">
|
|
9394
9658
|
<Properties>
|
|
9395
9659
|
<string name="Name">RuntimeLib</string>
|
|
9396
9660
|
<string name="Source"><![CDATA[local Promise = require(script.Parent.Promise)
|
|
@@ -9657,15 +9921,15 @@ return TS
|
|
|
9657
9921
|
</Properties>
|
|
9658
9922
|
</Item>
|
|
9659
9923
|
</Item>
|
|
9660
|
-
<Item class="Folder" referent="
|
|
9924
|
+
<Item class="Folder" referent="28">
|
|
9661
9925
|
<Properties>
|
|
9662
9926
|
<string name="Name">node_modules</string>
|
|
9663
9927
|
</Properties>
|
|
9664
|
-
<Item class="Folder" referent="
|
|
9928
|
+
<Item class="Folder" referent="29">
|
|
9665
9929
|
<Properties>
|
|
9666
9930
|
<string name="Name">@rbxts</string>
|
|
9667
9931
|
</Properties>
|
|
9668
|
-
<Item class="ModuleScript" referent="
|
|
9932
|
+
<Item class="ModuleScript" referent="26">
|
|
9669
9933
|
<Properties>
|
|
9670
9934
|
<string name="Name">services</string>
|
|
9671
9935
|
<string name="Source"><![CDATA[return setmetatable({}, {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HttpService, Players, ReplicatedStorage, RunService } from "@rbxts/services";
|
|
2
2
|
import RuntimeLogBuffer from "./RuntimeLogBuffer";
|
|
3
|
+
import MemoryHandlers from "./handlers/MemoryHandlers";
|
|
3
4
|
|
|
4
5
|
// The client peer cannot reach the MCP HTTP server - Roblox forbids
|
|
5
6
|
// HttpService:RequestAsync from the client DM even under PluginSecurity, and
|
|
@@ -45,6 +46,7 @@ interface ExecuteResult {
|
|
|
45
46
|
const CLIENT_BROKER_ALLOWED_ENDPOINTS = new Set<string>([
|
|
46
47
|
"/api/execute-luau",
|
|
47
48
|
"/api/get-runtime-logs",
|
|
49
|
+
"/api/get-memory-breakdown",
|
|
48
50
|
]);
|
|
49
51
|
|
|
50
52
|
interface ReadyResponseBody {
|
|
@@ -145,6 +147,9 @@ function setupClientBroker() {
|
|
|
145
147
|
if (payload && payload.endpoint === "/api/get-runtime-logs") {
|
|
146
148
|
return handleGetRuntimeLogs(payload.data);
|
|
147
149
|
}
|
|
150
|
+
if (payload && payload.endpoint === "/api/get-memory-breakdown") {
|
|
151
|
+
return MemoryHandlers.getMemoryBreakdown(payload.data ?? {});
|
|
152
|
+
}
|
|
148
153
|
if (payload && payload.endpoint === "/api/execute-luau") {
|
|
149
154
|
return handleExecuteLuau(payload.data);
|
|
150
155
|
}
|
|
@@ -13,6 +13,8 @@ import AssetHandlers from "./handlers/AssetHandlers";
|
|
|
13
13
|
import CaptureHandlers from "./handlers/CaptureHandlers";
|
|
14
14
|
import InputHandlers from "./handlers/InputHandlers";
|
|
15
15
|
import LogHandlers from "./handlers/LogHandlers";
|
|
16
|
+
import SerializationHandlers from "./handlers/SerializationHandlers";
|
|
17
|
+
import MemoryHandlers from "./handlers/MemoryHandlers";
|
|
16
18
|
import { Connection, RequestPayload, PollResponse, ReadyResponse } from "../types";
|
|
17
19
|
|
|
18
20
|
const instanceId = HttpService.GenerateGUID(false);
|
|
@@ -95,6 +97,11 @@ const routeMap: Record<string, Handler> = {
|
|
|
95
97
|
"/api/find-and-replace-in-scripts": ScriptHandlers.findAndReplaceInScripts,
|
|
96
98
|
|
|
97
99
|
"/api/get-runtime-logs": LogHandlers.getRuntimeLogs,
|
|
100
|
+
|
|
101
|
+
"/api/export-rbxm": SerializationHandlers.exportRbxm,
|
|
102
|
+
"/api/import-rbxm": SerializationHandlers.importRbxm,
|
|
103
|
+
|
|
104
|
+
"/api/get-memory-breakdown": MemoryHandlers.getMemoryBreakdown,
|
|
98
105
|
};
|
|
99
106
|
|
|
100
107
|
function processRequest(request: RequestPayload): unknown {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const Stats = game.GetService("Stats");
|
|
2
|
+
|
|
3
|
+
// GetMemoryUsageMbAllCategories is gated by capability "InternalTest" and not
|
|
4
|
+
// callable from plugin context. GetMemoryUsageMbForTag is not - so we iterate
|
|
5
|
+
// Enum.DeveloperMemoryTag and ask per-tag.
|
|
6
|
+
function getMemoryBreakdown(requestData: Record<string, unknown>): unknown {
|
|
7
|
+
if (!Stats.MemoryTrackingEnabled) {
|
|
8
|
+
return { error: "MemoryTrackingEnabled is false on this peer" };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const requested = requestData.tags as string[] | undefined;
|
|
12
|
+
const requestedSet = requested && requested.size() > 0 ? new Set(requested) : undefined;
|
|
13
|
+
|
|
14
|
+
const categories: Record<string, number> = {};
|
|
15
|
+
for (const item of Enum.DeveloperMemoryTag.GetEnumItems()) {
|
|
16
|
+
const name = item.Name;
|
|
17
|
+
if (requestedSet && !requestedSet.has(name)) continue;
|
|
18
|
+
const [ok, mb] = pcall(() => Stats.GetMemoryUsageMbForTag(item));
|
|
19
|
+
categories[name] = ok ? (mb as number) : 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const unknownTags: string[] = [];
|
|
23
|
+
if (requestedSet) {
|
|
24
|
+
const known = new Set<string>();
|
|
25
|
+
for (const i of Enum.DeveloperMemoryTag.GetEnumItems()) known.add(i.Name);
|
|
26
|
+
for (const t of requestedSet) {
|
|
27
|
+
if (!known.has(t)) {
|
|
28
|
+
unknownTags.push(t);
|
|
29
|
+
categories[t] = 0;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const result: Record<string, unknown> = {
|
|
35
|
+
total_mb: Stats.GetTotalMemoryUsageMb(),
|
|
36
|
+
categories,
|
|
37
|
+
memory_tracking_enabled: true,
|
|
38
|
+
timestamp: DateTime.now().UnixTimestampMillis,
|
|
39
|
+
};
|
|
40
|
+
if (unknownTags.size() > 0) result.unknown_tags = unknownTags;
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export = { getMemoryBreakdown };
|
|
@@ -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
|
+
};
|