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 +173 -10
- package/dist/install-plugin-cli.js +87 -0
- package/package.json +3 -2
- package/studio-plugin/MCPPlugin.rbxmx +1522 -183
- package/studio-plugin/src/modules/Communication.ts +3 -0
- package/studio-plugin/src/modules/Utils.ts +1 -1
- package/studio-plugin/src/modules/handlers/AssetHandlers.ts +79 -68
- package/studio-plugin/src/modules/handlers/BuildHandlers.ts +36 -31
- package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +118 -0
- package/studio-plugin/src/modules/handlers/InstanceHandlers.ts +125 -72
- package/studio-plugin/src/modules/handlers/QueryHandlers.ts +25 -8
- package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +1 -0
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
|
-
|
|
1660
|
-
|
|
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
|
-
|
|
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
|
|
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: {
|
|
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.
|
|
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/**/*",
|