robloxstudio-mcp 2.5.0-next.0 → 2.5.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.
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/install-plugin.ts
4
+ import { createWriteStream, existsSync, mkdirSync, unlinkSync } from "fs";
5
+ import { join } from "path";
6
+ import { homedir } from "os";
7
+ import { get } from "https";
8
+ var REPO = "boshyxd/robloxstudio-mcp";
9
+ var ASSET_NAME = "MCPPlugin.rbxmx";
10
+ var TIMEOUT_MS = 3e4;
11
+ var MAX_REDIRECTS = 5;
12
+ function getPluginsFolder() {
13
+ if (process.platform === "win32") {
14
+ return join(process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local"), "Roblox", "Plugins");
15
+ }
16
+ return join(homedir(), "Documents", "Roblox", "Plugins");
17
+ }
18
+ function httpsGet(url) {
19
+ return new Promise((resolve, reject) => {
20
+ const req = get(url, { headers: { "User-Agent": "robloxstudio-mcp" } }, resolve);
21
+ req.on("error", reject);
22
+ req.setTimeout(TIMEOUT_MS, () => {
23
+ req.destroy(new Error(`Request timed out after ${TIMEOUT_MS}ms`));
24
+ });
25
+ });
26
+ }
27
+ async function download(url, dest, redirects = 0) {
28
+ const res = await httpsGet(url);
29
+ if (res.statusCode === 301 || res.statusCode === 302) {
30
+ if (redirects >= MAX_REDIRECTS) throw new Error(`Too many redirects (max ${MAX_REDIRECTS})`);
31
+ const location = res.headers.location;
32
+ if (!location) throw new Error("Redirect with no location header");
33
+ return download(location, dest, redirects + 1);
34
+ }
35
+ if (res.statusCode !== 200) {
36
+ throw new Error(`Download failed: HTTP ${res.statusCode}`);
37
+ }
38
+ return new Promise((resolve, reject) => {
39
+ const file = createWriteStream(dest);
40
+ const cleanup = (err) => {
41
+ file.close(() => {
42
+ try {
43
+ unlinkSync(dest);
44
+ } catch {
45
+ }
46
+ reject(err);
47
+ });
48
+ };
49
+ res.pipe(file);
50
+ file.on("finish", () => {
51
+ file.close();
52
+ resolve();
53
+ });
54
+ file.on("error", cleanup);
55
+ res.on("error", cleanup);
56
+ });
57
+ }
58
+ async function installPlugin() {
59
+ const pluginsFolder = getPluginsFolder();
60
+ if (!existsSync(pluginsFolder)) {
61
+ mkdirSync(pluginsFolder, { recursive: true });
62
+ }
63
+ console.log("Fetching latest release...");
64
+ const res = await httpsGet(`https://api.github.com/repos/${REPO}/releases/latest`);
65
+ if (res.statusCode !== 200) {
66
+ throw new Error(`GitHub API returned HTTP ${res.statusCode}`);
67
+ }
68
+ const chunks = [];
69
+ for await (const chunk of res) {
70
+ chunks.push(chunk);
71
+ }
72
+ const release = JSON.parse(Buffer.concat(chunks).toString());
73
+ const asset = release.assets?.find((a) => a.name === ASSET_NAME);
74
+ if (!asset) {
75
+ throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
76
+ }
77
+ const dest = join(pluginsFolder, ASSET_NAME);
78
+ console.log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
79
+ await download(asset.browser_download_url, dest);
80
+ console.log(`Installed to ${dest}`);
81
+ }
82
+
83
+ // src/install-plugin-cli.ts
84
+ installPlugin().catch((err) => {
85
+ console.error(err instanceof Error ? err.message : String(err));
86
+ process.exitCode = 1;
87
+ });
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "robloxstudio-mcp",
3
- "version": "2.5.0-next.0",
3
+ "version": "2.5.0",
4
4
  "description": "MCP Server for Roblox Studio Integration - Access Studio data, scripts, and objects through AI tools",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
7
7
  "bin": {
8
- "robloxstudio-mcp": "dist/index.js"
8
+ "robloxstudio-mcp": "dist/index.js",
9
+ "robloxstudio-mcp-install-plugin": "dist/install-plugin-cli.js"
9
10
  },
10
11
  "files": [
11
12
  "dist/**/*",
@@ -1333,6 +1333,7 @@ return {
1333
1333
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
1334
1334
  local CaptureService = game:GetService("CaptureService")
1335
1335
  local AssetService = game:GetService("AssetService")
1336
+ local MAX_TILE_SIZE = 1024
1336
1337
  local BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
1337
1338
  local function encodeBase64(buf)
1338
1339
  local len = buffer.len(buf)
@@ -1362,6 +1363,59 @@ local function encodeBase64(buf)
1362
1363
  end
1363
1364
  return table.concat(parts, "")
1364
1365
  end
1366
+ local function readPixelsTiled(img, w, h)
1367
+ local BYTES_PER_PIXEL = 4
1368
+ local fullBuf = buffer.create(w * h * BYTES_PER_PIXEL)
1369
+ local fullRowBytes = w * BYTES_PER_PIXEL
1370
+ do
1371
+ local ty = 0
1372
+ local _shouldIncrement = false
1373
+ while true do
1374
+ if _shouldIncrement then
1375
+ ty += MAX_TILE_SIZE
1376
+ else
1377
+ _shouldIncrement = true
1378
+ end
1379
+ if not (ty < h) then
1380
+ break
1381
+ end
1382
+ local tileH = math.min(MAX_TILE_SIZE, h - ty)
1383
+ do
1384
+ local tx = 0
1385
+ local _shouldIncrement_1 = false
1386
+ while true do
1387
+ if _shouldIncrement_1 then
1388
+ tx += MAX_TILE_SIZE
1389
+ else
1390
+ _shouldIncrement_1 = true
1391
+ end
1392
+ if not (tx < w) then
1393
+ break
1394
+ end
1395
+ local tileW = math.min(MAX_TILE_SIZE, w - tx)
1396
+ local tileBuf = img:ReadPixelsBuffer(Vector2.new(tx, ty), Vector2.new(tileW, tileH))
1397
+ local tileRowBytes = tileW * BYTES_PER_PIXEL
1398
+ do
1399
+ local row = 0
1400
+ local _shouldIncrement_2 = false
1401
+ while true do
1402
+ if _shouldIncrement_2 then
1403
+ row += 1
1404
+ else
1405
+ _shouldIncrement_2 = true
1406
+ end
1407
+ if not (row < tileH) then
1408
+ break
1409
+ end
1410
+ buffer.copy(fullBuf, (ty + row) * fullRowBytes + tx * BYTES_PER_PIXEL, tileBuf, row * tileRowBytes, tileRowBytes)
1411
+ end
1412
+ end
1413
+ end
1414
+ end
1415
+ end
1416
+ end
1417
+ return fullBuf
1418
+ end
1365
1419
  local function captureScreenshot()
1366
1420
  local contentId
1367
1421
  CaptureService:CaptureScreenshot(function(id)
@@ -1385,11 +1439,11 @@ local function captureScreenshot()
1385
1439
  }
1386
1440
  end
1387
1441
  local editableImage = editableResult
1388
- local size = editableImage.Size
1389
- local w = math.floor(size.X)
1390
- local h = math.floor(size.Y)
1442
+ local imgSize = editableImage.Size
1443
+ local w = math.floor(imgSize.X)
1444
+ local h = math.floor(imgSize.Y)
1391
1445
  local readOk, pixelBuffer = pcall(function()
1392
- return editableImage:ReadPixelsBuffer(Vector2.zero, editableImage.Size)
1446
+ return readPixelsTiled(editableImage, w, h)
1393
1447
  end)
1394
1448
  editableImage:Destroy()
1395
1449
  if not readOk then
@@ -2937,6 +2991,9 @@ local function getFileTree(requestData)
2937
2991
  if instance:IsA("LuaSourceContainer") then
2938
2992
  node.hasSource = true
2939
2993
  node.scriptType = instance.ClassName
2994
+ if instance:IsA("BaseScript") then
2995
+ node.enabled = instance.Enabled
2996
+ end
2940
2997
  end
2941
2998
  for _, child in instance:GetChildren() do
2942
2999
  local _children = node.children
@@ -2979,13 +3036,16 @@ local function searchFiles(requestData)
2979
3036
  match = (string.find(_exp, _arg0)) ~= nil
2980
3037
  end
2981
3038
  if match then
2982
- local _arg0 = {
3039
+ local entry = {
2983
3040
  name = instance.Name,
2984
3041
  className = instance.ClassName,
2985
3042
  path = getInstancePath(instance),
2986
3043
  hasSource = instance:IsA("LuaSourceContainer"),
2987
3044
  }
2988
- table.insert(results, _arg0)
3045
+ if instance:IsA("BaseScript") then
3046
+ entry.enabled = instance.Enabled
3047
+ end
3048
+ table.insert(results, entry)
2989
3049
  end
2990
3050
  for _, child in instance:GetChildren() do
2991
3051
  searchRecursive(child)
@@ -3246,14 +3306,17 @@ local function getInstanceChildren(requestData)
3246
3306
  end
3247
3307
  local children = {}
3248
3308
  for _, child in instance:GetChildren() do
3249
- local _arg0 = {
3309
+ local entry = {
3250
3310
  name = child.Name,
3251
3311
  className = child.ClassName,
3252
3312
  path = getInstancePath(child),
3253
3313
  hasChildren = #child:GetChildren() > 0,
3254
3314
  hasSource = child:IsA("LuaSourceContainer"),
3255
3315
  }
3256
- table.insert(children, _arg0)
3316
+ if child:IsA("BaseScript") then
3317
+ entry.enabled = child.Enabled
3318
+ end
3319
+ table.insert(children, entry)
3257
3320
  end
3258
3321
  return {
3259
3322
  instancePath = instancePath,
@@ -3694,13 +3757,16 @@ local function grepScripts(requestData)
3694
3757
  end
3695
3758
  end
3696
3759
  if scriptMatchCount > 0 then
3697
- local _arg0 = {
3760
+ local scriptResult = {
3698
3761
  instancePath = getInstancePath(instance),
3699
3762
  name = instance.Name,
3700
3763
  className = instance.ClassName,
3701
3764
  matches = scriptMatches,
3702
3765
  }
3703
- table.insert(results, _arg0)
3766
+ if instance:IsA("BaseScript") then
3767
+ scriptResult.enabled = instance.Enabled
3768
+ end
3769
+ table.insert(results, scriptResult)
3704
3770
  end
3705
3771
  end
3706
3772
  for _, child in instance:GetChildren() do
@@ -3766,6 +3832,7 @@ local function normalizeEscapes(s)
3766
3832
  result = (string.gsub(result, "\\n", "\n"))
3767
3833
  result = (string.gsub(result, "\\t", "\t"))
3768
3834
  result = (string.gsub(result, "\\r", "\r"))
3835
+ result = (string.gsub(result, '\\"', '"'))
3769
3836
  result = (string.gsub(result, "\\\\", "\\"))
3770
3837
  return result
3771
3838
  end
@@ -4449,7 +4516,7 @@ return {
4449
4516
  <Properties>
4450
4517
  <string name="Name">State</string>
4451
4518
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
4452
- local CURRENT_VERSION = "2.5.0-next.0"
4519
+ local CURRENT_VERSION = "2.5.0"
4453
4520
  local MAX_CONNECTIONS = 5
4454
4521
  local BASE_PORT = 58741
4455
4522
  local activeTabIndex = 0
@@ -1,6 +1,7 @@
1
1
  const CaptureService = game.GetService("CaptureService");
2
2
  const AssetService = game.GetService("AssetService");
3
3
 
4
+ const MAX_TILE_SIZE = 1024;
4
5
  const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
5
6
 
6
7
  function encodeBase64(buf: buffer): string {
@@ -46,6 +47,25 @@ function encodeBase64(buf: buffer): string {
46
47
  return parts.join("");
47
48
  }
48
49
 
50
+ function readPixelsTiled(img: EditableImage, w: number, h: number): buffer {
51
+ const BYTES_PER_PIXEL = 4;
52
+ const fullBuf = buffer.create(w * h * BYTES_PER_PIXEL);
53
+ const fullRowBytes = w * BYTES_PER_PIXEL;
54
+
55
+ for (let ty = 0; ty < h; ty += MAX_TILE_SIZE) {
56
+ const tileH = math.min(MAX_TILE_SIZE, h - ty);
57
+ for (let tx = 0; tx < w; tx += MAX_TILE_SIZE) {
58
+ const tileW = math.min(MAX_TILE_SIZE, w - tx);
59
+ const tileBuf = img.ReadPixelsBuffer(new Vector2(tx, ty), new Vector2(tileW, tileH));
60
+ const tileRowBytes = tileW * BYTES_PER_PIXEL;
61
+ for (let row = 0; row < tileH; row++) {
62
+ buffer.copy(fullBuf, (ty + row) * fullRowBytes + tx * BYTES_PER_PIXEL, tileBuf, row * tileRowBytes, tileRowBytes);
63
+ }
64
+ }
65
+ }
66
+ return fullBuf;
67
+ }
68
+
49
69
  function captureScreenshot(): unknown {
50
70
  let contentId: string | undefined;
51
71
 
@@ -74,12 +94,12 @@ function captureScreenshot(): unknown {
74
94
  }
75
95
 
76
96
  const editableImage = editableResult as EditableImage;
77
- const size = editableImage.Size;
78
- const w = math.floor(size.X);
79
- const h = math.floor(size.Y);
97
+ const imgSize = editableImage.Size;
98
+ const w = math.floor(imgSize.X);
99
+ const h = math.floor(imgSize.Y);
80
100
 
81
101
  const [readOk, pixelBuffer] = pcall(() => {
82
- return editableImage.ReadPixelsBuffer(Vector2.zero, editableImage.Size);
102
+ return readPixelsTiled(editableImage, w, h);
83
103
  });
84
104
 
85
105
  editableImage.Destroy();
@@ -9,6 +9,7 @@ interface TreeNode {
9
9
  children: TreeNode[];
10
10
  hasSource?: boolean;
11
11
  scriptType?: string;
12
+ enabled?: boolean;
12
13
  }
13
14
 
14
15
  function getFileTree(requestData: Record<string, unknown>) {
@@ -34,6 +35,9 @@ function getFileTree(requestData: Record<string, unknown>) {
34
35
  if (instance.IsA("LuaSourceContainer")) {
35
36
  node.hasSource = true;
36
37
  node.scriptType = instance.ClassName;
38
+ if (instance.IsA("BaseScript")) {
39
+ node.enabled = instance.Enabled;
40
+ }
37
41
  }
38
42
 
39
43
  for (const child of instance.GetChildren()) {
@@ -55,7 +59,7 @@ function searchFiles(requestData: Record<string, unknown>) {
55
59
 
56
60
  if (!query) return { error: "Query is required" };
57
61
 
58
- const results: { name: string; className: string; path: string; hasSource: boolean }[] = [];
62
+ const results: { name: string; className: string; path: string; hasSource: boolean; enabled?: boolean }[] = [];
59
63
 
60
64
  function searchRecursive(instance: Instance) {
61
65
  let match = false;
@@ -69,12 +73,16 @@ function searchFiles(requestData: Record<string, unknown>) {
69
73
  }
70
74
 
71
75
  if (match) {
72
- results.push({
76
+ const entry: { name: string; className: string; path: string; hasSource: boolean; enabled?: boolean } = {
73
77
  name: instance.Name,
74
78
  className: instance.ClassName,
75
79
  path: getInstancePath(instance),
76
80
  hasSource: instance.IsA("LuaSourceContainer"),
77
- });
81
+ };
82
+ if (instance.IsA("BaseScript")) {
83
+ entry.enabled = instance.Enabled;
84
+ }
85
+ results.push(entry);
78
86
  }
79
87
 
80
88
  for (const child of instance.GetChildren()) {
@@ -300,15 +308,19 @@ function getInstanceChildren(requestData: Record<string, unknown>) {
300
308
  const instance = getInstanceByPath(instancePath);
301
309
  if (!instance) return { error: `Instance not found: ${instancePath}` };
302
310
 
303
- const children: { name: string; className: string; path: string; hasChildren: boolean; hasSource: boolean }[] = [];
311
+ const children: { name: string; className: string; path: string; hasChildren: boolean; hasSource: boolean; enabled?: boolean }[] = [];
304
312
  for (const child of instance.GetChildren()) {
305
- children.push({
313
+ const entry: { name: string; className: string; path: string; hasChildren: boolean; hasSource: boolean; enabled?: boolean } = {
306
314
  name: child.Name,
307
315
  className: child.ClassName,
308
316
  path: getInstancePath(child),
309
317
  hasChildren: child.GetChildren().size() > 0,
310
318
  hasSource: child.IsA("LuaSourceContainer"),
311
- });
319
+ };
320
+ if (child.IsA("BaseScript")) {
321
+ entry.enabled = child.Enabled;
322
+ }
323
+ children.push(entry);
312
324
  }
313
325
 
314
326
  return { instancePath, children, count: children.size() };
@@ -568,6 +580,7 @@ function grepScripts(requestData: Record<string, unknown>) {
568
580
  instancePath: string;
569
581
  name: string;
570
582
  className: string;
583
+ enabled?: boolean;
571
584
  matches: LineMatch[];
572
585
  }
573
586
 
@@ -644,12 +657,16 @@ function grepScripts(requestData: Record<string, unknown>) {
644
657
  }
645
658
 
646
659
  if (scriptMatchCount > 0) {
647
- results.push({
660
+ const scriptResult: ScriptResult = {
648
661
  instancePath: getInstancePath(instance),
649
662
  name: instance.Name,
650
663
  className: instance.ClassName,
651
664
  matches: scriptMatches,
652
- });
665
+ };
666
+ if (instance.IsA("BaseScript")) {
667
+ scriptResult.enabled = instance.Enabled;
668
+ }
669
+ results.push(scriptResult);
653
670
  }
654
671
  }
655
672
 
@@ -11,6 +11,7 @@ function normalizeEscapes(s: string): string {
11
11
  result = result.gsub("\\n", "\n")[0];
12
12
  result = result.gsub("\\t", "\t")[0];
13
13
  result = result.gsub("\\r", "\r")[0];
14
+ result = result.gsub('\\"', '"')[0];
14
15
  result = result.gsub("\\\\", "\\")[0];
15
16
  return result;
16
17
  }