robloxstudio-mcp 2.4.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.
package/dist/index.js CHANGED
@@ -74,7 +74,8 @@ var TOOL_HANDLERS = {
74
74
  get_asset_details: (tools, body) => tools.getAssetDetails(body.assetId),
75
75
  get_asset_thumbnail: (tools, body) => tools.getAssetThumbnail(body.assetId, body.size),
76
76
  insert_asset: (tools, body) => tools.insertAsset(body.assetId, body.parentPath, body.position),
77
- preview_asset: (tools, body) => tools.previewAsset(body.assetId, body.includeProperties, body.maxDepth)
77
+ preview_asset: (tools, body) => tools.previewAsset(body.assetId, body.includeProperties, body.maxDepth),
78
+ capture_screenshot: (tools) => tools.captureScreenshot()
78
79
  };
79
80
  function createHttpServer(tools, bridge, allowedTools) {
80
81
  const app = express();
@@ -805,6 +806,41 @@ var OpenCloudClient = class {
805
806
  }
806
807
  };
807
808
 
809
+ // ../../node_modules/@robloxstudio-mcp/core/dist/png-encoder.js
810
+ import { deflateSync, crc32 } from "zlib";
811
+ var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
812
+ function writeChunk(type, data) {
813
+ const typeBytes = Buffer.from(type, "ascii");
814
+ const length = Buffer.alloc(4);
815
+ length.writeUInt32BE(data.length);
816
+ const crcInput = Buffer.concat([typeBytes, data]);
817
+ const checksum = Buffer.alloc(4);
818
+ checksum.writeUInt32BE(crc32(crcInput) >>> 0);
819
+ return Buffer.concat([length, typeBytes, data, checksum]);
820
+ }
821
+ function rgbaToPng(rgba, width, height) {
822
+ const stride = width * 4;
823
+ const filtered = Buffer.alloc(height * (1 + stride));
824
+ for (let y = 0; y < height; y++) {
825
+ filtered[y * (1 + stride)] = 0;
826
+ rgba.copy(filtered, y * (1 + stride) + 1, y * stride, (y + 1) * stride);
827
+ }
828
+ const ihdr = Buffer.alloc(13);
829
+ ihdr.writeUInt32BE(width, 0);
830
+ ihdr.writeUInt32BE(height, 4);
831
+ ihdr[8] = 8;
832
+ ihdr[9] = 6;
833
+ ihdr[10] = 0;
834
+ ihdr[11] = 0;
835
+ ihdr[12] = 0;
836
+ return Buffer.concat([
837
+ PNG_SIGNATURE,
838
+ writeChunk("IHDR", ihdr),
839
+ writeChunk("IDAT", deflateSync(filtered)),
840
+ writeChunk("IEND", Buffer.alloc(0))
841
+ ]);
842
+ }
843
+
808
844
  // ../../node_modules/@robloxstudio-mcp/core/dist/tools/index.js
809
845
  import * as fs from "fs";
810
846
  import * as path from "path";
@@ -1404,7 +1440,7 @@ var RobloxStudioTools = class _RobloxStudioTools {
1404
1440
  };
1405
1441
  }
1406
1442
  static findLibraryPath() {
1407
- let dir = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1"));
1443
+ let dir = path.dirname(decodeURIComponent(new URL(import.meta.url).pathname).replace(/^\/([A-Z]:)/, "$1"));
1408
1444
  for (let i = 0; i < 6; i++) {
1409
1445
  const candidate = path.join(dir, "build-library");
1410
1446
  if (fs.existsSync(candidate))
@@ -1656,11 +1692,59 @@ var RobloxStudioTools = class _RobloxStudioTools {
1656
1692
  const expandedBuilds = [];
1657
1693
  const modelMap = sceneData.models || {};
1658
1694
  const placements = sceneData.place || [];
1659
- for (const placement of placements) {
1660
- const [modelKey, position, rotation] = placement;
1695
+ const isVec3Tuple = (value) => {
1696
+ return Array.isArray(value) && value.length === 3 && value.every((component) => typeof component === "number" && Number.isFinite(component));
1697
+ };
1698
+ for (const [placementIndex, placement] of placements.entries()) {
1699
+ let modelKey;
1700
+ let position;
1701
+ let rotation;
1702
+ let validatedKeyPath;
1703
+ if (Array.isArray(placement)) {
1704
+ if (placement.length < 2 || placement.length > 3) {
1705
+ throw new Error(`Invalid sceneData.place[${placementIndex}]: expected [modelKey, [x,y,z], [rotX?,rotY?,rotZ?]]`);
1706
+ }
1707
+ const [tupleModelKey, tuplePosition, tupleRotation] = placement;
1708
+ if (typeof tupleModelKey !== "string" || tupleModelKey.trim() === "") {
1709
+ throw new Error(`Invalid sceneData.place[${placementIndex}][0]: model key must be a non-empty string`);
1710
+ }
1711
+ modelKey = tupleModelKey.trim();
1712
+ validatedKeyPath = `sceneData.place[${placementIndex}][0]`;
1713
+ if (!isVec3Tuple(tuplePosition)) {
1714
+ throw new Error(`Invalid sceneData.place[${placementIndex}][1]: position must be a numeric [x,y,z] tuple`);
1715
+ }
1716
+ position = tuplePosition;
1717
+ if (tupleRotation !== void 0) {
1718
+ if (!isVec3Tuple(tupleRotation)) {
1719
+ throw new Error(`Invalid sceneData.place[${placementIndex}][2]: rotation must be a numeric [x,y,z] tuple when provided`);
1720
+ }
1721
+ rotation = tupleRotation;
1722
+ }
1723
+ } else if (placement && typeof placement === "object") {
1724
+ const placementRecord = placement;
1725
+ const objectModelKey = placementRecord.modelKey;
1726
+ const objectPosition = placementRecord.position;
1727
+ const objectRotation = placementRecord.rotation;
1728
+ if (typeof objectModelKey !== "string" || objectModelKey.trim() === "") {
1729
+ throw new Error(`Invalid sceneData.place[${placementIndex}].modelKey: model key must be a non-empty string`);
1730
+ }
1731
+ if (!isVec3Tuple(objectPosition)) {
1732
+ throw new Error(`Invalid sceneData.place[${placementIndex}].position: must be a numeric [x,y,z] tuple`);
1733
+ }
1734
+ if (objectRotation !== void 0 && !isVec3Tuple(objectRotation)) {
1735
+ throw new Error(`Invalid sceneData.place[${placementIndex}].rotation: must be a numeric [x,y,z] tuple when provided`);
1736
+ }
1737
+ modelKey = objectModelKey.trim();
1738
+ validatedKeyPath = `sceneData.place[${placementIndex}].modelKey`;
1739
+ position = objectPosition;
1740
+ rotation = objectRotation;
1741
+ } else {
1742
+ throw new Error(`Invalid sceneData.place[${placementIndex}]: expected an object placement or [modelKey, [x,y,z], [rotX?,rotY?,rotZ?]] tuple`);
1743
+ }
1661
1744
  const buildId = modelMap[modelKey];
1662
- if (!buildId)
1663
- continue;
1745
+ if (!buildId) {
1746
+ throw new Error(`Invalid ${validatedKeyPath}: model key "${modelKey}" is not defined in sceneData.models`);
1747
+ }
1664
1748
  const filePath = path.join(libraryPath, `${buildId}.json`);
1665
1749
  if (!fs.existsSync(filePath)) {
1666
1750
  throw new Error(`Build not found in library: ${buildId}`);
@@ -1670,7 +1754,7 @@ var RobloxStudioTools = class _RobloxStudioTools {
1670
1754
  const buildName = buildId.split("/").pop() || buildId;
1671
1755
  expandedBuilds.push({
1672
1756
  buildData,
1673
- position: position || [0, 0, 0],
1757
+ position,
1674
1758
  rotation: rotation || [0, 0, 0],
1675
1759
  name: buildName
1676
1760
  });
@@ -1808,6 +1892,26 @@ var RobloxStudioTools = class _RobloxStudioTools {
1808
1892
  }]
1809
1893
  };
1810
1894
  }
1895
+ async captureScreenshot() {
1896
+ const response = await this.client.request("/api/capture-screenshot", {});
1897
+ if (response.error) {
1898
+ return {
1899
+ content: [{
1900
+ type: "text",
1901
+ text: response.error
1902
+ }]
1903
+ };
1904
+ }
1905
+ const rgbaBuffer = Buffer.from(response.data, "base64");
1906
+ const pngBuffer = rgbaToPng(rgbaBuffer, response.width, response.height);
1907
+ return {
1908
+ content: [{
1909
+ type: "image",
1910
+ data: pngBuffer.toString("base64"),
1911
+ mimeType: "image/png"
1912
+ }]
1913
+ };
1914
+ }
1811
1915
  };
1812
1916
 
1813
1917
  // ../../node_modules/@robloxstudio-mcp/core/dist/bridge-service.js
@@ -2092,6 +2196,8 @@ var RobloxStudioMCPServer = class {
2092
2196
  return await this.tools.insertAsset(args?.assetId, args?.parentPath, args?.position);
2093
2197
  case "preview_asset":
2094
2198
  return await this.tools.previewAsset(args?.assetId, args?.includeProperties, args?.maxDepth);
2199
+ case "capture_screenshot":
2200
+ return await this.tools.captureScreenshot();
2095
2201
  default:
2096
2202
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
2097
2203
  }
@@ -3177,7 +3283,10 @@ var TOOL_DEFINITIONS = [
3177
3283
  description: "Array of part arrays. Each: [posX, posY, posZ, sizeX, sizeY, sizeZ, rotX, rotY, rotZ, paletteKey, shape?, transparency?]. Shapes: Block (default), Wedge, Cylinder, Ball, CornerWedge.",
3178
3284
  items: {
3179
3285
  type: "array",
3180
- minItems: 10
3286
+ minItems: 10,
3287
+ items: {
3288
+ anyOf: [{ type: "number" }, { type: "string" }]
3289
+ }
3181
3290
  }
3182
3291
  },
3183
3292
  bounds: {
@@ -3353,8 +3462,53 @@ part(0,2,0,2,1,1,"b")`,
3353
3462
  },
3354
3463
  place: {
3355
3464
  type: "array",
3356
- description: "Array of placements: [modelKey, [x,y,z], [rotX?,rotY?,rotZ?]]",
3357
- items: { type: "array" }
3465
+ description: "Array of placements. Preferred format: {modelKey, position:[x,y,z], rotation?:[x,y,z]}. Legacy tuple format [modelKey, [x,y,z], [rotX?,rotY?,rotZ?]] is also accepted.",
3466
+ items: {
3467
+ anyOf: [
3468
+ {
3469
+ type: "object",
3470
+ additionalProperties: false,
3471
+ required: ["modelKey", "position"],
3472
+ properties: {
3473
+ modelKey: {
3474
+ type: "string",
3475
+ minLength: 1
3476
+ },
3477
+ position: {
3478
+ type: "array",
3479
+ items: { type: "number" },
3480
+ minItems: 3,
3481
+ maxItems: 3
3482
+ },
3483
+ rotation: {
3484
+ type: "array",
3485
+ items: { type: "number" },
3486
+ minItems: 3,
3487
+ maxItems: 3
3488
+ }
3489
+ }
3490
+ },
3491
+ {
3492
+ type: "array",
3493
+ minItems: 2,
3494
+ maxItems: 3,
3495
+ items: {
3496
+ anyOf: [
3497
+ {
3498
+ type: "string",
3499
+ minLength: 1
3500
+ },
3501
+ {
3502
+ type: "array",
3503
+ items: { type: "number" },
3504
+ minItems: 3,
3505
+ maxItems: 3
3506
+ }
3507
+ ]
3508
+ }
3509
+ }
3510
+ ]
3511
+ }
3358
3512
  },
3359
3513
  custom: {
3360
3514
  type: "array",
@@ -3498,6 +3652,15 @@ part(0,2,0,2,1,1,"b")`,
3498
3652
  },
3499
3653
  required: ["assetId"]
3500
3654
  }
3655
+ },
3656
+ {
3657
+ name: "capture_screenshot",
3658
+ category: "read",
3659
+ description: 'Capture a screenshot of the Roblox Studio viewport and return it as a PNG image. Requires EditableImage API to be enabled: Game Settings > Security > "Allow Mesh / Image APIs". Only works in Edit mode with the viewport visible.',
3660
+ inputSchema: {
3661
+ type: "object",
3662
+ properties: {}
3663
+ }
3501
3664
  }
3502
3665
  ];
3503
3666
  var getAllTools = () => [...TOOL_DEFINITIONS];
@@ -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.4.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/**/*",