@chrrxs/robloxstudio-mcp-inspector 2.10.0 → 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 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,
@@ -1212,6 +1215,37 @@ function luaLongQuote(s) {
1212
1215
  ${s}
1213
1216
  ]${eq}]`;
1214
1217
  }
1218
+ function buildModuleScriptInvokeWrapper(opts) {
1219
+ const wrapped = `return ((function()
1220
+ ${opts.userCode}
1221
+ end)())`;
1222
+ return `
1223
+ local HttpService = game:GetService("HttpService")
1224
+ local bf = game:GetService("${opts.service}"):FindFirstChild("${opts.bridgeName}")
1225
+ if not bf then
1226
+ return HttpService:JSONEncode({
1227
+ bridge = "missing",
1228
+ error = ${luaLongQuote(opts.missingError)},
1229
+ })
1230
+ end
1231
+ local USER_CODE = ${luaLongQuote(wrapped)}
1232
+ local m = Instance.new("ModuleScript")
1233
+ m.Name = "__MCPEvalPayload"
1234
+ local okSet, setErr = pcall(function() m.Source = USER_CODE end)
1235
+ if not okSet then
1236
+ m:Destroy()
1237
+ return HttpService:JSONEncode({ bridge = "ok", ok = false, result = "ModuleScript Source set failed: " .. tostring(setErr) })
1238
+ end
1239
+ m.Parent = workspace
1240
+ local ok, result = bf:Invoke(m)
1241
+ m:Destroy()
1242
+ return HttpService:JSONEncode({
1243
+ bridge = "ok",
1244
+ ok = ok,
1245
+ result = if result == nil then nil else tostring(result),
1246
+ })
1247
+ `;
1248
+ }
1215
1249
  function parseBridgeResponse(response) {
1216
1250
  const r = response;
1217
1251
  if (r && typeof r.returnValue === "string") {
@@ -1771,23 +1805,12 @@ ${code}`
1771
1805
  if (!code) {
1772
1806
  throw new Error("Code is required for eval_server_runtime");
1773
1807
  }
1774
- const wrapper = `
1775
- local HttpService = game:GetService("HttpService")
1776
- local bf = game:GetService("ServerScriptService"):FindFirstChild("${SERVER_LOCAL_NAME}")
1777
- if not bf then
1778
- return HttpService:JSONEncode({
1779
- bridge = "missing",
1780
- error = "ServerEvalBridge not installed. Bridges are auto-installed at start_playtest and removed at stop_playtest. Start a playtest before calling eval_server_runtime.",
1781
- })
1782
- end
1783
- local USER_CODE = ${luaLongQuote(code)}
1784
- local ok, result = bf:Invoke(USER_CODE)
1785
- return HttpService:JSONEncode({
1786
- bridge = "ok",
1787
- ok = ok,
1788
- result = if result == nil then nil else tostring(result),
1789
- })
1790
- `;
1808
+ const wrapper = buildModuleScriptInvokeWrapper({
1809
+ service: "ServerScriptService",
1810
+ bridgeName: SERVER_LOCAL_NAME,
1811
+ missingError: "ServerEvalBridge not installed. Bridges are auto-installed at start_playtest and removed at stop_playtest. Start a playtest before calling eval_server_runtime.",
1812
+ userCode: code
1813
+ });
1791
1814
  const response = await this.client.request("/api/execute-luau", { code: wrapper }, "server");
1792
1815
  return {
1793
1816
  content: [
@@ -1806,32 +1829,12 @@ return HttpService:JSONEncode({
1806
1829
  if (!clientTarget.startsWith("client-")) {
1807
1830
  throw new Error(`eval_client_runtime requires target=client-N (got: ${clientTarget})`);
1808
1831
  }
1809
- const wrapper = `
1810
- local HttpService = game:GetService("HttpService")
1811
- local bf = game:GetService("ReplicatedStorage"):FindFirstChild("${CLIENT_LOCAL_NAME}")
1812
- if not bf then
1813
- return HttpService:JSONEncode({
1814
- bridge = "missing",
1815
- error = "ClientEvalBridge not installed. Bridges are auto-installed at start_playtest and removed at stop_playtest. Start a playtest before calling eval_client_runtime.",
1816
- })
1817
- end
1818
- local USER_CODE = ${luaLongQuote(code)}
1819
- local m = Instance.new("ModuleScript")
1820
- m.Name = "__MCPEvalPayload"
1821
- local okSet, setErr = pcall(function() m.Source = USER_CODE end)
1822
- if not okSet then
1823
- m:Destroy()
1824
- return HttpService:JSONEncode({ bridge = "ok", ok = false, result = "ModuleScript Source set failed: " .. tostring(setErr) })
1825
- end
1826
- m.Parent = workspace
1827
- local ok, result = bf:Invoke(m)
1828
- m:Destroy()
1829
- return HttpService:JSONEncode({
1830
- bridge = "ok",
1831
- ok = ok,
1832
- result = if result == nil then nil else tostring(result),
1833
- })
1834
- `;
1832
+ const wrapper = buildModuleScriptInvokeWrapper({
1833
+ service: "ReplicatedStorage",
1834
+ bridgeName: CLIENT_LOCAL_NAME,
1835
+ missingError: "ClientEvalBridge not installed. Bridges are auto-installed at start_playtest and removed at stop_playtest. Start a playtest before calling eval_client_runtime.",
1836
+ userCode: code
1837
+ });
1835
1838
  const response = await this.client.request("/api/execute-luau", { code: wrapper }, clientTarget);
1836
1839
  return {
1837
1840
  content: [
@@ -2777,6 +2780,139 @@ return HttpService:JSONEncode({
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) {
@@ -3909,7 +4045,7 @@ var init_definitions = __esm({
3909
4045
  {
3910
4046
  name: "eval_server_runtime",
3911
4047
  category: "write",
3912
- description: "Execute Luau on the server peer in the running game's Script VM (shares require cache with user game scripts). Use this instead of execute_luau target=server when you need to see runtime-mutated module state. Auto-installed at start_playtest, removed at stop_playtest. Requires ServerScriptService.LoadStringEnabled=true.",
4048
+ description: "Execute Luau on the server peer in the running game's Script VM (shares require cache with user game scripts). Use this instead of execute_luau target=server when you need to see runtime-mutated module state. Auto-installed at start_playtest, removed at stop_playtest.",
3913
4049
  inputSchema: {
3914
4050
  type: "object",
3915
4051
  properties: {
@@ -3924,7 +4060,7 @@ var init_definitions = __esm({
3924
4060
  {
3925
4061
  name: "eval_client_runtime",
3926
4062
  category: "write",
3927
- description: "Execute Luau on a client peer in the running game's LocalScript VM (shares require cache with user game scripts). Use this instead of execute_luau target=client-N when you need to see runtime-mutated module state. Auto-installed at start_playtest, removed at stop_playtest. Does not require LoadStringEnabled.",
4063
+ description: "Execute Luau on a client peer in the running game's LocalScript VM (shares require cache with user game scripts). Use this instead of execute_luau target=client-N when you need to see runtime-mutated module state. Auto-installed at start_playtest, removed at stop_playtest.",
3928
4064
  inputSchema: {
3929
4065
  type: "object",
3930
4066
  properties: {
@@ -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.10.0",
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",