@chrrxs/robloxstudio-mcp-inspector 2.8.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.
Files changed (30) hide show
  1. package/dist/index.js +4483 -0
  2. package/package.json +50 -0
  3. package/studio-plugin/INSTALLATION.md +150 -0
  4. package/studio-plugin/MCPInspectorPlugin.rbxmx +9074 -0
  5. package/studio-plugin/MCPPlugin.rbxmx +9074 -0
  6. package/studio-plugin/default.project.json +19 -0
  7. package/studio-plugin/dev.project.json +23 -0
  8. package/studio-plugin/inspector-icon.png +0 -0
  9. package/studio-plugin/package-lock.json +706 -0
  10. package/studio-plugin/package.json +19 -0
  11. package/studio-plugin/plugin.json +10 -0
  12. package/studio-plugin/src/modules/ClientBroker.ts +221 -0
  13. package/studio-plugin/src/modules/Communication.ts +399 -0
  14. package/studio-plugin/src/modules/Recording.ts +28 -0
  15. package/studio-plugin/src/modules/State.ts +94 -0
  16. package/studio-plugin/src/modules/UI.ts +725 -0
  17. package/studio-plugin/src/modules/Utils.ts +318 -0
  18. package/studio-plugin/src/modules/handlers/AssetHandlers.ts +241 -0
  19. package/studio-plugin/src/modules/handlers/BuildHandlers.ts +481 -0
  20. package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +128 -0
  21. package/studio-plugin/src/modules/handlers/InputHandlers.ts +102 -0
  22. package/studio-plugin/src/modules/handlers/InstanceHandlers.ts +380 -0
  23. package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +391 -0
  24. package/studio-plugin/src/modules/handlers/PropertyHandlers.ts +191 -0
  25. package/studio-plugin/src/modules/handlers/QueryHandlers.ts +827 -0
  26. package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +530 -0
  27. package/studio-plugin/src/modules/handlers/TestHandlers.ts +277 -0
  28. package/studio-plugin/src/server/index.server.ts +63 -0
  29. package/studio-plugin/src/types/index.d.ts +44 -0
  30. package/studio-plugin/tsconfig.json +20 -0
package/dist/index.js ADDED
@@ -0,0 +1,4483 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../core/dist/server.js
4
+ import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { CallToolRequestSchema as CallToolRequestSchema2, ErrorCode as ErrorCode2, ListToolsRequestSchema as ListToolsRequestSchema2, McpError as McpError2 } from "@modelcontextprotocol/sdk/types.js";
7
+
8
+ // ../core/dist/http-server.js
9
+ import express from "express";
10
+ import cors from "cors";
11
+ import http from "http";
12
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
13
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
14
+ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
15
+ var TOOL_HANDLERS = {
16
+ get_file_tree: (tools, body) => tools.getFileTree(body.path),
17
+ search_files: (tools, body) => tools.searchFiles(body.query, body.searchType),
18
+ get_place_info: (tools) => tools.getPlaceInfo(),
19
+ get_services: (tools, body) => tools.getServices(body.serviceName),
20
+ search_objects: (tools, body) => tools.searchObjects(body.query, body.searchType, body.propertyName),
21
+ get_instance_properties: (tools, body) => tools.getInstanceProperties(body.instancePath, body.excludeSource),
22
+ get_instance_children: (tools, body) => tools.getInstanceChildren(body.instancePath),
23
+ search_by_property: (tools, body) => tools.searchByProperty(body.propertyName, body.propertyValue),
24
+ get_class_info: (tools, body) => tools.getClassInfo(body.className),
25
+ get_project_structure: (tools, body) => tools.getProjectStructure(body.path, body.maxDepth, body.scriptsOnly),
26
+ set_property: (tools, body) => tools.setProperty(body.instancePath, body.propertyName, body.propertyValue),
27
+ set_properties: (tools, body) => tools.setProperties(body.instancePath, body.properties),
28
+ mass_set_property: (tools, body) => tools.massSetProperty(body.paths, body.propertyName, body.propertyValue),
29
+ mass_get_property: (tools, body) => tools.massGetProperty(body.paths, body.propertyName),
30
+ create_object: (tools, body) => tools.createObject(body.className, body.parent, body.name, body.properties),
31
+ mass_create_objects: (tools, body) => tools.massCreateObjects(body.objects),
32
+ delete_object: (tools, body) => tools.deleteObject(body.instancePath),
33
+ smart_duplicate: (tools, body) => tools.smartDuplicate(body.instancePath, body.count, body.options),
34
+ mass_duplicate: (tools, body) => tools.massDuplicate(body.duplications),
35
+ grep_scripts: (tools, body) => tools.grepScripts(body.pattern, {
36
+ caseSensitive: body.caseSensitive,
37
+ usePattern: body.usePattern,
38
+ contextLines: body.contextLines,
39
+ maxResults: body.maxResults,
40
+ maxResultsPerScript: body.maxResultsPerScript,
41
+ filesOnly: body.filesOnly,
42
+ path: body.path,
43
+ classFilter: body.classFilter
44
+ }),
45
+ get_script_source: (tools, body) => tools.getScriptSource(body.instancePath, body.startLine, body.endLine),
46
+ set_script_source: (tools, body) => tools.setScriptSource(body.instancePath, body.source),
47
+ edit_script_lines: (tools, body) => tools.editScriptLines(body.instancePath, body.old_string, body.new_string, body.startLine),
48
+ insert_script_lines: (tools, body) => tools.insertScriptLines(body.instancePath, body.afterLine, body.newContent),
49
+ delete_script_lines: (tools, body) => tools.deleteScriptLines(body.instancePath, body.startLine, body.endLine),
50
+ set_attribute: (tools, body) => tools.setAttribute(body.instancePath, body.attributeName, body.attributeValue, body.valueType),
51
+ get_attributes: (tools, body) => tools.getAttributes(body.instancePath),
52
+ delete_attribute: (tools, body) => tools.deleteAttribute(body.instancePath, body.attributeName),
53
+ get_tags: (tools, body) => tools.getTags(body.instancePath),
54
+ add_tag: (tools, body) => tools.addTag(body.instancePath, body.tagName),
55
+ remove_tag: (tools, body) => tools.removeTag(body.instancePath, body.tagName),
56
+ get_tagged: (tools, body) => tools.getTagged(body.tagName),
57
+ get_selection: (tools) => tools.getSelection(),
58
+ execute_luau: (tools, body) => tools.executeLuau(body.code, body.target),
59
+ start_playtest: (tools, body) => tools.startPlaytest(body.mode, body.numPlayers),
60
+ stop_playtest: (tools) => tools.stopPlaytest(),
61
+ get_playtest_output: (tools, body) => tools.getPlaytestOutput(body.target),
62
+ get_connected_instances: (tools) => tools.getConnectedInstances(),
63
+ export_build: (tools, body) => tools.exportBuild(body.instancePath, body.outputId, body.style),
64
+ create_build: (tools, body) => tools.createBuild(body.id, body.style, body.palette, body.parts, body.bounds),
65
+ generate_build: (tools, body) => tools.generateBuild(body.id, body.style, body.palette, body.code, body.seed),
66
+ import_build: (tools, body) => tools.importBuild(body.buildData, body.targetPath, body.position),
67
+ list_library: (tools, body) => tools.listLibrary(body.style),
68
+ search_materials: (tools, body) => tools.searchMaterials(body.query, body.maxResults),
69
+ get_build: (tools, body) => tools.getBuild(body.id),
70
+ import_scene: (tools, body) => tools.importScene(body.sceneData, body.targetPath),
71
+ undo: (tools) => tools.undo(),
72
+ redo: (tools) => tools.redo(),
73
+ search_assets: (tools, body) => tools.searchAssets(body.assetType, body.query, body.maxResults, body.sortBy, body.verifiedCreatorsOnly),
74
+ get_asset_details: (tools, body) => tools.getAssetDetails(body.assetId),
75
+ get_asset_thumbnail: (tools, body) => tools.getAssetThumbnail(body.assetId, body.size),
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),
78
+ upload_asset: (tools, body) => tools.uploadAsset(body.filePath, body.assetType, body.displayName, body.description, body.userId, body.groupId),
79
+ clone_object: (tools, body) => tools.cloneObject(body.instancePath, body.targetParentPath),
80
+ get_descendants: (tools, body) => tools.getDescendants(body.instancePath, body.maxDepth, body.classFilter),
81
+ compare_instances: (tools, body) => tools.compareInstances(body.instancePathA, body.instancePathB),
82
+ get_output_log: (tools, body) => tools.getOutputLog(body.maxEntries, body.messageType),
83
+ bulk_set_attributes: (tools, body) => tools.bulkSetAttributes(body.instancePath, body.attributes),
84
+ capture_screenshot: (tools) => tools.captureScreenshot(),
85
+ simulate_mouse_input: (tools, body) => tools.simulateMouseInput(body.action, body.x, body.y, body.button, body.scrollDirection, body.target),
86
+ simulate_keyboard_input: (tools, body) => tools.simulateKeyboardInput(body.keyCode, body.action, body.duration, body.target),
87
+ character_navigation: (tools, body) => tools.characterNavigation(body.position, body.instancePath, body.waitForCompletion, body.timeout, body.target),
88
+ find_and_replace_in_scripts: (tools, body) => tools.findAndReplaceInScripts(body.pattern, body.replacement, {
89
+ caseSensitive: body.caseSensitive,
90
+ usePattern: body.usePattern,
91
+ path: body.path,
92
+ classFilter: body.classFilter,
93
+ dryRun: body.dryRun,
94
+ maxReplacements: body.maxReplacements
95
+ })
96
+ };
97
+ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
98
+ const app = express();
99
+ let mcpServerActive = false;
100
+ let lastMCPActivity = 0;
101
+ let mcpServerStartTime = 0;
102
+ const proxyInstances = /* @__PURE__ */ new Set();
103
+ const setMCPServerActive = (active) => {
104
+ mcpServerActive = active;
105
+ if (active) {
106
+ mcpServerStartTime = Date.now();
107
+ lastMCPActivity = Date.now();
108
+ } else {
109
+ mcpServerStartTime = 0;
110
+ lastMCPActivity = 0;
111
+ }
112
+ };
113
+ const trackMCPActivity = () => {
114
+ if (mcpServerActive) {
115
+ lastMCPActivity = Date.now();
116
+ }
117
+ };
118
+ const isMCPServerActive = () => {
119
+ if (!mcpServerActive)
120
+ return false;
121
+ return Date.now() - lastMCPActivity < 3e4;
122
+ };
123
+ const isPluginConnected = () => {
124
+ return bridge.getInstances().length > 0;
125
+ };
126
+ app.use(cors());
127
+ app.use(express.json({ limit: "50mb" }));
128
+ app.use(express.urlencoded({ limit: "50mb", extended: true }));
129
+ app.get("/health", (req, res) => {
130
+ const instances = bridge.getInstances();
131
+ res.json({
132
+ status: "ok",
133
+ service: "robloxstudio-mcp",
134
+ version: serverConfig?.version,
135
+ pluginConnected: instances.length > 0,
136
+ instanceCount: instances.length,
137
+ instances: instances.map((i) => ({
138
+ instanceId: i.instanceId,
139
+ role: i.role,
140
+ lastActivity: i.lastActivity,
141
+ connectedAt: i.connectedAt
142
+ })),
143
+ mcpServerActive: isMCPServerActive(),
144
+ uptime: mcpServerActive ? Date.now() - mcpServerStartTime : 0,
145
+ pendingRequests: bridge.getPendingRequestCount(),
146
+ proxyInstanceCount: proxyInstances.size,
147
+ streamableHttp: !!serverConfig
148
+ });
149
+ });
150
+ app.post("/ready", (req, res) => {
151
+ const { instanceId, role } = req.body;
152
+ if (instanceId && role) {
153
+ const assignedRole = bridge.registerInstance(instanceId, role);
154
+ res.json({ success: true, assignedRole });
155
+ } else {
156
+ bridge.registerInstance("legacy", "edit");
157
+ res.json({ success: true, assignedRole: "edit" });
158
+ }
159
+ });
160
+ app.post("/disconnect", (req, res) => {
161
+ const { instanceId } = req.body;
162
+ if (instanceId) {
163
+ bridge.unregisterInstance(instanceId);
164
+ } else {
165
+ bridge.unregisterInstance("legacy");
166
+ bridge.clearAllPendingRequests();
167
+ }
168
+ res.json({ success: true });
169
+ });
170
+ app.get("/status", (req, res) => {
171
+ const instances = bridge.getInstances();
172
+ res.json({
173
+ pluginConnected: instances.length > 0,
174
+ instanceCount: instances.length,
175
+ instances: instances.map((i) => ({ instanceId: i.instanceId, role: i.role })),
176
+ mcpServerActive: isMCPServerActive(),
177
+ lastMCPActivity,
178
+ uptime: mcpServerActive ? Date.now() - mcpServerStartTime : 0
179
+ });
180
+ });
181
+ app.get("/instances", (req, res) => {
182
+ res.json({ instances: bridge.getInstances() });
183
+ });
184
+ app.get("/poll", (req, res) => {
185
+ const instanceId = req.query.instanceId;
186
+ if (instanceId) {
187
+ bridge.updateInstanceActivity(instanceId);
188
+ }
189
+ let callerRole = "edit";
190
+ if (instanceId) {
191
+ const inst = bridge.getInstances().find((i) => i.instanceId === instanceId);
192
+ if (inst) {
193
+ callerRole = inst.role;
194
+ }
195
+ }
196
+ if (!isMCPServerActive()) {
197
+ res.status(503).json({
198
+ error: "MCP server not connected",
199
+ pluginConnected: true,
200
+ mcpConnected: false,
201
+ request: null
202
+ });
203
+ return;
204
+ }
205
+ const pendingRequest = bridge.getPendingRequest(callerRole);
206
+ if (pendingRequest) {
207
+ res.json({
208
+ request: pendingRequest.request,
209
+ requestId: pendingRequest.requestId,
210
+ mcpConnected: true,
211
+ pluginConnected: true,
212
+ proxyInstanceCount: proxyInstances.size
213
+ });
214
+ } else {
215
+ res.json({
216
+ request: null,
217
+ mcpConnected: true,
218
+ pluginConnected: true,
219
+ proxyInstanceCount: proxyInstances.size
220
+ });
221
+ }
222
+ });
223
+ app.post("/response", (req, res) => {
224
+ const { requestId, response, error } = req.body;
225
+ if (error) {
226
+ bridge.rejectRequest(requestId, error);
227
+ } else {
228
+ bridge.resolveRequest(requestId, response);
229
+ }
230
+ res.json({ success: true });
231
+ });
232
+ app.post("/proxy", async (req, res) => {
233
+ const { endpoint, data, target, proxyInstanceId } = req.body;
234
+ if (!endpoint) {
235
+ res.status(400).json({ error: "endpoint is required" });
236
+ return;
237
+ }
238
+ if (proxyInstanceId) {
239
+ proxyInstances.add(proxyInstanceId);
240
+ }
241
+ try {
242
+ const response = await bridge.sendRequest(endpoint, data, target || "edit");
243
+ res.json({ response });
244
+ } catch (err) {
245
+ res.status(500).json({ error: err.message || "Proxy request failed" });
246
+ }
247
+ });
248
+ if (serverConfig) {
249
+ const filteredTools = serverConfig.tools.filter((t) => !allowedTools || allowedTools.has(t.name));
250
+ app.post("/mcp", async (req, res) => {
251
+ try {
252
+ trackMCPActivity();
253
+ const server2 = new Server({ name: serverConfig.name, version: serverConfig.version }, { capabilities: { tools: {} } });
254
+ server2.setRequestHandler(ListToolsRequestSchema, async () => ({
255
+ tools: filteredTools.map((t) => ({
256
+ name: t.name,
257
+ description: t.description,
258
+ inputSchema: t.inputSchema
259
+ }))
260
+ }));
261
+ server2.setRequestHandler(CallToolRequestSchema, async (request) => {
262
+ const { name, arguments: args } = request.params;
263
+ if (allowedTools && !allowedTools.has(name)) {
264
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
265
+ }
266
+ const handler = TOOL_HANDLERS[name];
267
+ if (!handler) {
268
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
269
+ }
270
+ try {
271
+ return await handler(tools, args || {});
272
+ } catch (error) {
273
+ if (error instanceof McpError)
274
+ throw error;
275
+ throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
276
+ }
277
+ });
278
+ const transport = new StreamableHTTPServerTransport({
279
+ sessionIdGenerator: void 0
280
+ });
281
+ await server2.connect(transport);
282
+ await transport.handleRequest(req, res, req.body);
283
+ res.on("close", () => {
284
+ transport.close();
285
+ server2.close();
286
+ });
287
+ } catch (error) {
288
+ if (!res.headersSent) {
289
+ res.status(500).json({
290
+ jsonrpc: "2.0",
291
+ error: { code: -32603, message: "Internal server error" },
292
+ id: null
293
+ });
294
+ }
295
+ }
296
+ });
297
+ app.get("/mcp", (req, res) => {
298
+ res.writeHead(405).end(JSON.stringify({
299
+ jsonrpc: "2.0",
300
+ error: { code: -32e3, message: "Method not allowed." },
301
+ id: null
302
+ }));
303
+ });
304
+ app.delete("/mcp", (req, res) => {
305
+ res.writeHead(405).end(JSON.stringify({
306
+ jsonrpc: "2.0",
307
+ error: { code: -32e3, message: "Method not allowed." },
308
+ id: null
309
+ }));
310
+ });
311
+ }
312
+ app.use("/mcp/*", (req, res, next) => {
313
+ trackMCPActivity();
314
+ next();
315
+ });
316
+ for (const [toolName, handler] of Object.entries(TOOL_HANDLERS)) {
317
+ if (allowedTools && !allowedTools.has(toolName))
318
+ continue;
319
+ app.post(`/mcp/${toolName}`, async (req, res) => {
320
+ try {
321
+ const result = await handler(tools, req.body);
322
+ res.json(result);
323
+ } catch (error) {
324
+ res.status(500).json({ error: error instanceof Error ? error.message : "Unknown error" });
325
+ }
326
+ });
327
+ }
328
+ app.isPluginConnected = isPluginConnected;
329
+ app.setMCPServerActive = setMCPServerActive;
330
+ app.isMCPServerActive = isMCPServerActive;
331
+ app.trackMCPActivity = trackMCPActivity;
332
+ return app;
333
+ }
334
+ function listenWithRetry(app, host, startPort, maxAttempts = 5) {
335
+ return new Promise(async (resolve2, reject) => {
336
+ for (let i = 0; i < maxAttempts; i++) {
337
+ const port = startPort + i;
338
+ try {
339
+ const server2 = await bindPort(app, host, port);
340
+ resolve2({ server: server2, port });
341
+ return;
342
+ } catch (err) {
343
+ if (err.code === "EADDRINUSE") {
344
+ console.error(`Port ${port} in use, trying next...`);
345
+ continue;
346
+ }
347
+ reject(err);
348
+ return;
349
+ }
350
+ }
351
+ reject(new Error(`All ports ${startPort}-${startPort + maxAttempts - 1} are in use. Stop some MCP server instances and retry.`));
352
+ });
353
+ }
354
+ function bindPort(app, host, port) {
355
+ return new Promise((resolve2, reject) => {
356
+ const server2 = http.createServer(app);
357
+ const onError = (err) => {
358
+ server2.removeListener("error", onError);
359
+ reject(err);
360
+ };
361
+ server2.once("error", onError);
362
+ server2.listen(port, host, () => {
363
+ server2.removeListener("error", onError);
364
+ resolve2(server2);
365
+ });
366
+ });
367
+ }
368
+
369
+ // ../core/dist/tools/studio-client.js
370
+ var StudioHttpClient = class {
371
+ bridge;
372
+ constructor(bridge) {
373
+ this.bridge = bridge;
374
+ }
375
+ async request(endpoint, data, target = "edit") {
376
+ try {
377
+ const response = await this.bridge.sendRequest(endpoint, data, target);
378
+ return response;
379
+ } catch (error) {
380
+ if (error instanceof Error && error.message === "Request timeout") {
381
+ throw new Error("Studio plugin connection timeout. Make sure the Roblox Studio plugin is running and activated.");
382
+ }
383
+ throw error;
384
+ }
385
+ }
386
+ };
387
+
388
+ // ../core/dist/tools/build-executor.js
389
+ import * as vm from "vm";
390
+ var DEFAULT_TIMEOUT = 1e4;
391
+ var DEFAULT_MAX_PARTS = 1e4;
392
+ var VALID_SHAPES = /* @__PURE__ */ new Set(["Block", "Wedge", "Cylinder", "Ball", "CornerWedge"]);
393
+ function createSeededRng(seed) {
394
+ let s = seed;
395
+ return () => {
396
+ s = s * 1664525 + 1013904223 & 4294967295;
397
+ return (s >>> 0) / 4294967296;
398
+ };
399
+ }
400
+ function computeBoundsFromParts(parts) {
401
+ let maxX = 0, maxY = 0, maxZ = 0;
402
+ for (const p of parts) {
403
+ const px = Math.abs(p[0]) + p[3] / 2;
404
+ const py = Math.abs(p[1]) + p[4] / 2;
405
+ const pz = Math.abs(p[2]) + p[5] / 2;
406
+ maxX = Math.max(maxX, px);
407
+ maxY = Math.max(maxY, py);
408
+ maxZ = Math.max(maxZ, pz);
409
+ }
410
+ return [
411
+ Math.round(maxX * 2 * 10) / 10,
412
+ Math.round(maxY * 2 * 10) / 10,
413
+ Math.round(maxZ * 2 * 10) / 10
414
+ ];
415
+ }
416
+ function runBuildExecutor(code, palette, seed, options) {
417
+ const timeout = options?.timeout ?? DEFAULT_TIMEOUT;
418
+ const maxParts = options?.maxParts ?? DEFAULT_MAX_PARTS;
419
+ const paletteKeys = new Set(Object.keys(palette));
420
+ const parts = [];
421
+ function checkLimit() {
422
+ if (parts.length >= maxParts) {
423
+ throw new Error(`Part limit exceeded: max ${maxParts} parts allowed`);
424
+ }
425
+ }
426
+ function validateKey(key, fnName) {
427
+ if (!paletteKeys.has(key)) {
428
+ throw new Error(`${fnName}: palette key "${key}" not found. Available keys: ${[...paletteKeys].join(", ")}`);
429
+ }
430
+ }
431
+ function validateNumber(val, name) {
432
+ if (typeof val !== "number" || !isFinite(val)) {
433
+ throw new Error(`${name} must be a finite number, got ${val}`);
434
+ }
435
+ }
436
+ function partFn(x, y, z, sx, sy, sz, key, shape, transparency) {
437
+ validateNumber(x, "part x");
438
+ validateNumber(y, "part y");
439
+ validateNumber(z, "part z");
440
+ validateNumber(sx, "part sx");
441
+ validateNumber(sy, "part sy");
442
+ validateNumber(sz, "part sz");
443
+ validateKey(key, "part");
444
+ if (shape !== void 0 && !VALID_SHAPES.has(shape)) {
445
+ throw new Error(`part: invalid shape "${shape}". Valid: ${[...VALID_SHAPES].join(", ")}`);
446
+ }
447
+ checkLimit();
448
+ const entry = [x, y, z, sx, sy, sz, 0, 0, 0, key];
449
+ if (shape !== void 0)
450
+ entry.push(shape);
451
+ if (transparency !== void 0)
452
+ entry.push(transparency);
453
+ parts.push(entry);
454
+ }
455
+ function rpartFn(x, y, z, sx, sy, sz, rx, ry, rz, key, shape, transparency) {
456
+ validateNumber(x, "rpart x");
457
+ validateNumber(y, "rpart y");
458
+ validateNumber(z, "rpart z");
459
+ validateNumber(sx, "rpart sx");
460
+ validateNumber(sy, "rpart sy");
461
+ validateNumber(sz, "rpart sz");
462
+ validateNumber(rx, "rpart rx");
463
+ validateNumber(ry, "rpart ry");
464
+ validateNumber(rz, "rpart rz");
465
+ validateKey(key, "rpart");
466
+ if (shape !== void 0 && !VALID_SHAPES.has(shape)) {
467
+ throw new Error(`rpart: invalid shape "${shape}". Valid: ${[...VALID_SHAPES].join(", ")}`);
468
+ }
469
+ checkLimit();
470
+ const entry = [x, y, z, sx, sy, sz, rx, ry, rz, key];
471
+ if (shape !== void 0)
472
+ entry.push(shape);
473
+ if (transparency !== void 0)
474
+ entry.push(transparency);
475
+ parts.push(entry);
476
+ }
477
+ function fillFn(x1, y1, z1, x2, y2, z2, key, unitSize) {
478
+ validateKey(key, "fill");
479
+ [x1, y1, z1, x2, y2, z2].forEach((v, i) => validateNumber(v, `fill arg${i}`));
480
+ if (!unitSize) {
481
+ const cx = (x1 + x2) / 2;
482
+ const cy = (y1 + y2) / 2;
483
+ const cz = (z1 + z2) / 2;
484
+ const sx = Math.abs(x2 - x1);
485
+ const sy = Math.abs(y2 - y1);
486
+ const sz = Math.abs(z2 - z1);
487
+ checkLimit();
488
+ parts.push([cx, cy, cz, sx, sy, sz, 0, 0, 0, key]);
489
+ } else {
490
+ const [ux, uy, uz] = unitSize;
491
+ validateNumber(ux, "fill unitSize[0]");
492
+ validateNumber(uy, "fill unitSize[1]");
493
+ validateNumber(uz, "fill unitSize[2]");
494
+ const minX = Math.min(x1, x2);
495
+ const minY = Math.min(y1, y2);
496
+ const minZ = Math.min(z1, z2);
497
+ const maxX = Math.max(x1, x2);
498
+ const maxY = Math.max(y1, y2);
499
+ const maxZ = Math.max(z1, z2);
500
+ for (let x = minX + ux / 2; x < maxX; x += ux) {
501
+ for (let y = minY + uy / 2; y < maxY; y += uy) {
502
+ for (let z = minZ + uz / 2; z < maxZ; z += uz) {
503
+ checkLimit();
504
+ parts.push([
505
+ Math.round(x * 1e3) / 1e3,
506
+ Math.round(y * 1e3) / 1e3,
507
+ Math.round(z * 1e3) / 1e3,
508
+ ux,
509
+ uy,
510
+ uz,
511
+ 0,
512
+ 0,
513
+ 0,
514
+ key
515
+ ]);
516
+ }
517
+ }
518
+ }
519
+ }
520
+ }
521
+ function beamFn(x1, y1, z1, x2, y2, z2, thickness, key) {
522
+ validateKey(key, "beam");
523
+ [x1, y1, z1, x2, y2, z2, thickness].forEach((v, i) => validateNumber(v, `beam arg${i}`));
524
+ const cx = (x1 + x2) / 2;
525
+ const cy = (y1 + y2) / 2;
526
+ const cz = (z1 + z2) / 2;
527
+ const dx = x2 - x1;
528
+ const dy = y2 - y1;
529
+ const dz = z2 - z1;
530
+ const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
531
+ const ry = Math.atan2(dx, dz) * (180 / Math.PI);
532
+ const horizontalDist = Math.sqrt(dx * dx + dz * dz);
533
+ const rx = -Math.atan2(dy, horizontalDist) * (180 / Math.PI);
534
+ checkLimit();
535
+ parts.push([
536
+ Math.round(cx * 1e3) / 1e3,
537
+ Math.round(cy * 1e3) / 1e3,
538
+ Math.round(cz * 1e3) / 1e3,
539
+ thickness,
540
+ thickness,
541
+ Math.round(length * 1e3) / 1e3,
542
+ Math.round(rx * 100) / 100,
543
+ Math.round(ry * 100) / 100,
544
+ 0,
545
+ key
546
+ ]);
547
+ }
548
+ function wallFn(x1, z1, x2, z2, height, thickness, key) {
549
+ validateKey(key, "wall");
550
+ [x1, z1, x2, z2, height, thickness].forEach((v, i) => validateNumber(v, `wall arg${i}`));
551
+ const cx = (x1 + x2) / 2;
552
+ const cz = (z1 + z2) / 2;
553
+ const cy = height / 2;
554
+ const dx = x2 - x1;
555
+ const dz = z2 - z1;
556
+ const wallLength = Math.sqrt(dx * dx + dz * dz);
557
+ const ry = Math.atan2(dx, dz) * (180 / Math.PI);
558
+ checkLimit();
559
+ parts.push([
560
+ Math.round(cx * 1e3) / 1e3,
561
+ Math.round(cy * 1e3) / 1e3,
562
+ Math.round(cz * 1e3) / 1e3,
563
+ thickness,
564
+ height,
565
+ Math.round(wallLength * 1e3) / 1e3,
566
+ 0,
567
+ Math.round(ry * 100) / 100,
568
+ 0,
569
+ key
570
+ ]);
571
+ }
572
+ function floorFn(x1, z1, x2, z2, y, thickness, key) {
573
+ validateKey(key, "floor");
574
+ [x1, z1, x2, z2, y, thickness].forEach((v, i) => validateNumber(v, `floor arg${i}`));
575
+ const cx = (x1 + x2) / 2;
576
+ const cz = (z1 + z2) / 2;
577
+ const sx = Math.abs(x2 - x1);
578
+ const sz = Math.abs(z2 - z1);
579
+ checkLimit();
580
+ parts.push([
581
+ Math.round(cx * 1e3) / 1e3,
582
+ y,
583
+ Math.round(cz * 1e3) / 1e3,
584
+ sx,
585
+ thickness,
586
+ sz,
587
+ 0,
588
+ 0,
589
+ 0,
590
+ key
591
+ ]);
592
+ }
593
+ function rowFn(x, y, z, count, spacingX, spacingZ, partFnCb) {
594
+ validateNumber(x, "row x");
595
+ validateNumber(y, "row y");
596
+ validateNumber(z, "row z");
597
+ validateNumber(count, "row count");
598
+ validateNumber(spacingX, "row spacingX");
599
+ validateNumber(spacingZ, "row spacingZ");
600
+ if (typeof partFnCb !== "function") {
601
+ throw new Error("row: partFn must be a function");
602
+ }
603
+ for (let i = 0; i < count; i++) {
604
+ partFnCb(i, x + i * spacingX, y, z + i * spacingZ);
605
+ }
606
+ }
607
+ function gridFn(x, y, z, countX, countZ, spacingX, spacingZ, partFnCb) {
608
+ validateNumber(x, "grid x");
609
+ validateNumber(y, "grid y");
610
+ validateNumber(z, "grid z");
611
+ validateNumber(countX, "grid countX");
612
+ validateNumber(countZ, "grid countZ");
613
+ validateNumber(spacingX, "grid spacingX");
614
+ validateNumber(spacingZ, "grid spacingZ");
615
+ if (typeof partFnCb !== "function") {
616
+ throw new Error("grid: partFn must be a function");
617
+ }
618
+ for (let ix = 0; ix < countX; ix++) {
619
+ for (let iz = 0; iz < countZ; iz++) {
620
+ partFnCb(ix, iz, x + ix * spacingX, y, z + iz * spacingZ);
621
+ }
622
+ }
623
+ }
624
+ function roomFn(x, y, z, w, h, d, wallKey, floorKey, ceilKey, wallThickness) {
625
+ const t = wallThickness ?? 1;
626
+ const fk = floorKey ?? wallKey;
627
+ const ck = ceilKey ?? wallKey;
628
+ floorFn(x - w / 2, z - d / 2, x + w / 2, z + d / 2, y, t, fk);
629
+ floorFn(x - w / 2, z - d / 2, x + w / 2, z + d / 2, y + h, t, ck);
630
+ wallFn(x - w / 2, z - d / 2, x - w / 2, z + d / 2, h, t, wallKey);
631
+ wallFn(x + w / 2, z - d / 2, x + w / 2, z + d / 2, h, t, wallKey);
632
+ wallFn(x - w / 2, z - d / 2, x + w / 2, z - d / 2, h, t, wallKey);
633
+ wallFn(x - w / 2, z + d / 2, x + w / 2, z + d / 2, h, t, wallKey);
634
+ }
635
+ function roofFn(x, y, z, w, d, style, key, overhang) {
636
+ const oh = overhang ?? 1;
637
+ validateKey(key, "roof");
638
+ if (style === "flat") {
639
+ floorFn(x - w / 2 - oh, z - d / 2 - oh, x + w / 2 + oh, z + d / 2 + oh, y, 1, key);
640
+ } else if (style === "gable") {
641
+ const peakH = w / 2 * 0.6;
642
+ const slopeW = Math.sqrt((w / 2 + oh) * (w / 2 + oh) + peakH * peakH);
643
+ const angle = Math.atan2(peakH, w / 2 + oh) * (180 / Math.PI);
644
+ rpartFn(x - (w / 4 + oh / 2) * 0.5, y + peakH / 2, z, slopeW, 0.5, d + oh * 2, -angle, 0, 0, key);
645
+ rpartFn(x + (w / 4 + oh / 2) * 0.5, y + peakH / 2, z, slopeW, 0.5, d + oh * 2, angle, 0, 0, key);
646
+ } else if (style === "hip") {
647
+ const peakH = w / 3;
648
+ floorFn(x - w / 4, z - d / 4, x + w / 4, z + d / 4, y + peakH, 0.5, key);
649
+ const slopeW = Math.sqrt((w / 2 + oh) * (w / 2 + oh) + peakH * peakH);
650
+ const angle = Math.atan2(peakH, w / 2 + oh) * (180 / Math.PI);
651
+ rpartFn(x - w / 4, y + peakH / 2, z, slopeW * 0.6, 0.5, d + oh, -angle, 0, 0, key);
652
+ rpartFn(x + w / 4, y + peakH / 2, z, slopeW * 0.6, 0.5, d + oh, angle, 0, 0, key);
653
+ }
654
+ }
655
+ function stairsFn(x1, y1, z1, x2, y2, z2, w, key) {
656
+ validateKey(key, "stairs");
657
+ const dx = x2 - x1;
658
+ const dy = y2 - y1;
659
+ const dz = z2 - z1;
660
+ const dist = Math.sqrt(dx * dx + dz * dz);
661
+ const stepCount = Math.max(2, Math.round(Math.abs(dy) / 0.5));
662
+ const stepH = dy / stepCount;
663
+ const stepDx = dx / stepCount;
664
+ const stepDz = dz / stepCount;
665
+ for (let i = 0; i < stepCount; i++) {
666
+ checkLimit();
667
+ const sx = x1 + stepDx * (i + 0.5);
668
+ const sy = y1 + stepH * (i + 0.5);
669
+ const sz = z1 + stepDz * (i + 0.5);
670
+ const stepDepth = dist / stepCount;
671
+ partFn(Math.round(sx * 100) / 100, Math.round(sy * 100) / 100, Math.round(sz * 100) / 100, w, Math.abs(stepH), stepDepth, key);
672
+ }
673
+ }
674
+ function archFn(x, y, z, w, h, thickness, key, segments) {
675
+ validateKey(key, "arch");
676
+ const segs = segments ?? 8;
677
+ const radius = w / 2;
678
+ const archH = h - radius;
679
+ if (archH > 0) {
680
+ partFn(x - w / 2, y + archH / 2, z, thickness, archH, thickness, key);
681
+ partFn(x + w / 2, y + archH / 2, z, thickness, archH, thickness, key);
682
+ }
683
+ for (let i = 0; i < segs; i++) {
684
+ const a1 = Math.PI / segs * i;
685
+ const a2 = Math.PI / segs * (i + 1);
686
+ const mx = x + radius * Math.cos((a1 + a2) / 2 + Math.PI / 2);
687
+ const my = y + archH + radius * Math.sin((a1 + a2) / 2 + Math.PI / 2) * (radius / (radius || 1));
688
+ checkLimit();
689
+ const segLen = 2 * radius * Math.sin((a2 - a1) / 2);
690
+ const angle = (a1 + a2) / 2 * (180 / Math.PI);
691
+ rpartFn(Math.round(mx * 100) / 100, Math.round(my * 100) / 100, z, segLen, thickness, thickness, 0, 0, Math.round(angle * 100) / 100, key);
692
+ }
693
+ }
694
+ function columnFn(x, y, z, height, radius, key, capKey) {
695
+ validateKey(key, "column");
696
+ rpartFn(x, y + height / 2, z, height, radius * 2, radius * 2, 0, 0, 90, key, "Cylinder");
697
+ const ck = capKey ?? key;
698
+ validateKey(ck, "column cap");
699
+ partFn(x, y + 0.25, z, radius * 2.5, 0.5, radius * 2.5, ck);
700
+ partFn(x, y + height - 0.25, z, radius * 2.5, 0.5, radius * 2.5, ck);
701
+ }
702
+ function pewFn(x, y, z, w, d, seatKey, legKey) {
703
+ validateKey(seatKey, "pew");
704
+ const lk = legKey ?? seatKey;
705
+ validateKey(lk, "pew legs");
706
+ partFn(x, y + 1.5, z, w, 0.3, d, seatKey);
707
+ partFn(x, y + 2.5, z - d / 2 + 0.15, w, 2, 0.3, seatKey);
708
+ partFn(x - w / 2 + 0.25, y + 0.75, z, 0.5, 1.5, d, lk);
709
+ partFn(x + w / 2 - 0.25, y + 0.75, z, 0.5, 1.5, d, lk);
710
+ }
711
+ function fenceFn(x1, z1, x2, z2, y, key, postSpacing) {
712
+ validateKey(key, "fence");
713
+ const spacing = postSpacing ?? 4;
714
+ const dx = x2 - x1;
715
+ const dz = z2 - z1;
716
+ const length = Math.sqrt(dx * dx + dz * dz);
717
+ const count = Math.max(2, Math.round(length / spacing) + 1);
718
+ for (let i = 0; i < count; i++) {
719
+ const t = i / (count - 1);
720
+ checkLimit();
721
+ partFn(x1 + dx * t, y + 1.5, z1 + dz * t, 0.5, 3, 0.5, key);
722
+ }
723
+ wallFn(x1, z1, x2, z2, 1, 0.3, key);
724
+ const cx = (x1 + x2) / 2;
725
+ const cz = (z1 + z2) / 2;
726
+ const ry = Math.atan2(dx, dz) * (180 / Math.PI);
727
+ checkLimit();
728
+ parts.push([
729
+ Math.round(cx * 1e3) / 1e3,
730
+ y + 2.5,
731
+ Math.round(cz * 1e3) / 1e3,
732
+ 0.3,
733
+ 0.3,
734
+ Math.round(length * 1e3) / 1e3,
735
+ 0,
736
+ Math.round(ry * 100) / 100,
737
+ 0,
738
+ key
739
+ ]);
740
+ }
741
+ const rng = createSeededRng(seed ?? 42);
742
+ const sandbox = {
743
+ part: partFn,
744
+ rpart: rpartFn,
745
+ fill: fillFn,
746
+ beam: beamFn,
747
+ wall: wallFn,
748
+ floor: floorFn,
749
+ row: rowFn,
750
+ grid: gridFn,
751
+ room: roomFn,
752
+ roof: roofFn,
753
+ stairs: stairsFn,
754
+ arch: archFn,
755
+ column: columnFn,
756
+ pew: pewFn,
757
+ fence: fenceFn,
758
+ Math,
759
+ GRID_SIZE: 1,
760
+ rng,
761
+ console: { log: () => {
762
+ }, warn: () => {
763
+ }, error: () => {
764
+ } }
765
+ };
766
+ const context = vm.createContext(sandbox, {
767
+ codeGeneration: { strings: false, wasm: false }
768
+ });
769
+ const script = new vm.Script(code, { filename: "build-generator.js" });
770
+ try {
771
+ script.runInContext(context, { timeout });
772
+ } catch (err) {
773
+ if (err.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
774
+ throw new Error(`Build code execution timed out after ${timeout}ms`);
775
+ }
776
+ throw new Error(`Build code execution error: ${err.message}`);
777
+ }
778
+ if (parts.length === 0) {
779
+ throw new Error("Build code produced no parts. Make sure to call part(), wall(), floor(), etc.");
780
+ }
781
+ const bounds = computeBoundsFromParts(parts);
782
+ return { parts, bounds, partCount: parts.length };
783
+ }
784
+
785
+ // ../core/dist/opencloud-client.js
786
+ var OpenCloudClient = class {
787
+ apiKey;
788
+ baseUrl;
789
+ timeout;
790
+ constructor(config = {}) {
791
+ this.apiKey = config.apiKey || process.env.ROBLOX_OPEN_CLOUD_API_KEY || "";
792
+ this.baseUrl = config.baseUrl || "https://apis.roblox.com";
793
+ this.timeout = config.timeout || 3e4;
794
+ }
795
+ hasApiKey() {
796
+ return !!this.apiKey;
797
+ }
798
+ async request(endpoint, options = {}) {
799
+ if (!this.apiKey) {
800
+ throw new Error("Open Cloud API key not configured. Set ROBLOX_OPEN_CLOUD_API_KEY environment variable.");
801
+ }
802
+ const { method = "GET", params, body } = options;
803
+ const url = new URL(`${this.baseUrl}${endpoint}`);
804
+ if (params) {
805
+ for (const [key, value] of Object.entries(params)) {
806
+ if (value !== void 0) {
807
+ url.searchParams.set(key, String(value));
808
+ }
809
+ }
810
+ }
811
+ const controller = new AbortController();
812
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
813
+ try {
814
+ const response = await fetch(url.toString(), {
815
+ method,
816
+ headers: {
817
+ "x-api-key": this.apiKey,
818
+ "Content-Type": "application/json"
819
+ },
820
+ body: body ? JSON.stringify(body) : void 0,
821
+ signal: controller.signal
822
+ });
823
+ clearTimeout(timeoutId);
824
+ if (!response.ok) {
825
+ const errorBody = await response.text();
826
+ let errorMessage;
827
+ try {
828
+ const errorJson = JSON.parse(errorBody);
829
+ errorMessage = errorJson.detail || errorJson.message || errorBody;
830
+ } catch {
831
+ errorMessage = errorBody;
832
+ }
833
+ if (response.status === 401) {
834
+ throw new Error("Invalid or expired API key");
835
+ } else if (response.status === 403) {
836
+ throw new Error(`API key lacks required permissions: ${errorMessage}`);
837
+ } else if (response.status === 429) {
838
+ throw new Error("Rate limit exceeded. Please try again later.");
839
+ } else {
840
+ throw new Error(`Open Cloud API error (${response.status}): ${errorMessage}`);
841
+ }
842
+ }
843
+ return await response.json();
844
+ } catch (error) {
845
+ clearTimeout(timeoutId);
846
+ if (error instanceof Error) {
847
+ if (error.name === "AbortError") {
848
+ throw new Error("Request timed out");
849
+ }
850
+ throw error;
851
+ }
852
+ throw new Error(`Unknown error: ${String(error)}`);
853
+ }
854
+ }
855
+ async searchAssets(params) {
856
+ return this.request("/toolbox-service/v2/assets:search", {
857
+ params: {
858
+ searchCategoryType: params.searchCategoryType,
859
+ query: params.query,
860
+ pageToken: params.pageToken,
861
+ pageNumber: params.pageNumber,
862
+ maxPageSize: params.maxPageSize || 25,
863
+ sortDirection: params.sortDirection,
864
+ sortCategory: params.sortCategory,
865
+ includeOnlyVerifiedCreators: params.includeOnlyVerifiedCreators,
866
+ userId: params.userId,
867
+ groupId: params.groupId
868
+ }
869
+ });
870
+ }
871
+ async getAssetDetails(assetId) {
872
+ return this.request(`/toolbox-service/v2/assets/${assetId}`);
873
+ }
874
+ async getAssetThumbnail(assetId, size = "420x420") {
875
+ const url = `https://thumbnails.roblox.com/v1/assets?assetIds=${assetId}&size=${size}&format=Png`;
876
+ try {
877
+ const response = await fetch(url);
878
+ if (!response.ok)
879
+ return null;
880
+ const data = await response.json();
881
+ const thumbnail = data.data[0];
882
+ if (!thumbnail || thumbnail.state !== "Completed" || !thumbnail.imageUrl) {
883
+ return null;
884
+ }
885
+ const imageResponse = await fetch(thumbnail.imageUrl);
886
+ if (!imageResponse.ok)
887
+ return null;
888
+ const arrayBuffer = await imageResponse.arrayBuffer();
889
+ const base64 = Buffer.from(arrayBuffer).toString("base64");
890
+ return { base64, mimeType: "image/png" };
891
+ } catch {
892
+ return null;
893
+ }
894
+ }
895
+ async getAssetThumbnails(assetIds, size = "420x420") {
896
+ const result = /* @__PURE__ */ new Map();
897
+ if (assetIds.length === 0)
898
+ return result;
899
+ const batches = [];
900
+ for (let i = 0; i < assetIds.length; i += 100) {
901
+ batches.push(assetIds.slice(i, i + 100));
902
+ }
903
+ for (const batch of batches) {
904
+ const url = `https://thumbnails.roblox.com/v1/assets?assetIds=${batch.join(",")}&size=${size}&format=Png`;
905
+ try {
906
+ const response = await fetch(url);
907
+ if (response.ok) {
908
+ const data = await response.json();
909
+ for (const thumbnail of data.data) {
910
+ if (thumbnail.state === "Completed" && thumbnail.imageUrl) {
911
+ result.set(thumbnail.targetId, thumbnail.imageUrl);
912
+ }
913
+ }
914
+ }
915
+ } catch {
916
+ }
917
+ }
918
+ return result;
919
+ }
920
+ async createAsset(uploadRequest, fileContent, fileName) {
921
+ const formData = new FormData();
922
+ formData.append("request", JSON.stringify(uploadRequest));
923
+ formData.append("fileContent", new Blob([new Uint8Array(fileContent)], { type: this.getMimeType(fileName) }), fileName);
924
+ const operation = await this.requestMultipart("/assets/v1/assets", formData);
925
+ if (operation.done)
926
+ return operation;
927
+ return this.pollOperation(operation.path);
928
+ }
929
+ getMimeType(fileName) {
930
+ const ext = fileName.split(".").pop()?.toLowerCase();
931
+ const mimeTypes = {
932
+ // Image (Decal)
933
+ png: "image/png",
934
+ jpg: "image/jpeg",
935
+ jpeg: "image/jpeg",
936
+ bmp: "image/bmp",
937
+ tga: "image/tga",
938
+ // Audio
939
+ mp3: "audio/mpeg",
940
+ ogg: "audio/ogg",
941
+ wav: "audio/wav",
942
+ flac: "audio/flac",
943
+ // Model
944
+ fbx: "model/fbx",
945
+ gltf: "model/gltf+json",
946
+ glb: "model/gltf-binary",
947
+ rbxm: "model/x-rbxm",
948
+ rbxmx: "model/x-rbxm",
949
+ // Video
950
+ mp4: "video/mp4",
951
+ mov: "video/mov"
952
+ };
953
+ if (!ext || !mimeTypes[ext]) {
954
+ throw new Error(`Unsupported file format: .${ext ?? "(none)"}. Supported: Image: png/jpg/bmp/tga, Audio: mp3/ogg/wav/flac, Model: fbx/gltf/glb/rbxm/rbxmx, Video: mp4/mov`);
955
+ }
956
+ return mimeTypes[ext];
957
+ }
958
+ async requestMultipart(endpoint, formData) {
959
+ if (!this.apiKey) {
960
+ throw new Error("Open Cloud API key not configured. Set ROBLOX_OPEN_CLOUD_API_KEY environment variable.");
961
+ }
962
+ const url = `${this.baseUrl}${endpoint}`;
963
+ const controller = new AbortController();
964
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
965
+ try {
966
+ const response = await fetch(url, {
967
+ method: "POST",
968
+ headers: { "x-api-key": this.apiKey },
969
+ body: formData,
970
+ signal: controller.signal
971
+ });
972
+ clearTimeout(timeoutId);
973
+ if (!response.ok) {
974
+ const errorBody = await response.text();
975
+ let errorMessage;
976
+ try {
977
+ const errorJson = JSON.parse(errorBody);
978
+ errorMessage = errorJson.detail || errorJson.message || errorBody;
979
+ } catch {
980
+ errorMessage = errorBody;
981
+ }
982
+ if (response.status === 401) {
983
+ throw new Error("Invalid or expired API key");
984
+ } else if (response.status === 403) {
985
+ throw new Error(`API key lacks required permissions: ${errorMessage}`);
986
+ } else if (response.status === 429) {
987
+ throw new Error("Rate limit exceeded. Please try again later.");
988
+ } else {
989
+ throw new Error(`Open Cloud API error (${response.status}): ${errorMessage}`);
990
+ }
991
+ }
992
+ return await response.json();
993
+ } catch (error) {
994
+ clearTimeout(timeoutId);
995
+ if (error instanceof Error) {
996
+ if (error.name === "AbortError") {
997
+ throw new Error("Request timed out");
998
+ }
999
+ throw error;
1000
+ }
1001
+ throw new Error(`Unknown error: ${String(error)}`);
1002
+ }
1003
+ }
1004
+ async pollOperation(operationPath, maxAttempts = 30, intervalMs = 2e3) {
1005
+ const operationId = operationPath.replace("operations/", "");
1006
+ for (let i = 0; i < maxAttempts; i++) {
1007
+ const result = await this.request(`/assets/v1/operations/${operationId}`);
1008
+ if (result.done)
1009
+ return result;
1010
+ if (result.error) {
1011
+ throw new Error(`Asset upload failed: ${result.error.message}`);
1012
+ }
1013
+ await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
1014
+ }
1015
+ throw new Error(`Asset upload timed out after ${maxAttempts * intervalMs / 1e3}s. Operation ID: ${operationId}`);
1016
+ }
1017
+ };
1018
+
1019
+ // ../core/dist/roblox-cookie-client.js
1020
+ var RobloxCookieClient = class {
1021
+ cookie;
1022
+ csrfToken = null;
1023
+ constructor(cookie) {
1024
+ this.cookie = cookie || process.env.ROBLOSECURITY || "";
1025
+ }
1026
+ hasCookie() {
1027
+ return !!this.cookie;
1028
+ }
1029
+ async fetchWithCsrf(url, options = {}) {
1030
+ const headers = {
1031
+ Cookie: `.ROBLOSECURITY=${this.cookie}`,
1032
+ ...options.headers || {}
1033
+ };
1034
+ if (this.csrfToken) {
1035
+ headers["X-CSRF-TOKEN"] = this.csrfToken;
1036
+ }
1037
+ const response = await fetch(url, { ...options, headers });
1038
+ if (response.status === 403) {
1039
+ const newToken = response.headers.get("x-csrf-token");
1040
+ if (newToken) {
1041
+ this.csrfToken = newToken;
1042
+ headers["X-CSRF-TOKEN"] = newToken;
1043
+ return fetch(url, { ...options, headers });
1044
+ }
1045
+ }
1046
+ return response;
1047
+ }
1048
+ async uploadDecal(fileContent, name, description) {
1049
+ if (!this.cookie) {
1050
+ throw new Error("ROBLOSECURITY cookie is not set.");
1051
+ }
1052
+ const encodedName = encodeURIComponent(name);
1053
+ const encodedDesc = encodeURIComponent(description);
1054
+ const url = `https://data.roblox.com/data/upload/json?assetTypeId=13&name=${encodedName}&description=${encodedDesc}`;
1055
+ const response = await this.fetchWithCsrf(url, {
1056
+ method: "POST",
1057
+ headers: {
1058
+ "Content-Type": "application/octet-stream",
1059
+ "User-Agent": "RobloxStudio/WinInet",
1060
+ Requester: "Client"
1061
+ },
1062
+ body: new Uint8Array(fileContent)
1063
+ });
1064
+ if (!response.ok) {
1065
+ const body = await response.text();
1066
+ throw new Error(`Decal upload failed (${response.status}): ${body}`);
1067
+ }
1068
+ const result = await response.json();
1069
+ if (!result.Success || !result.AssetId) {
1070
+ throw new Error(`Decal upload failed: ${result.Message || "Unknown error"}`);
1071
+ }
1072
+ return {
1073
+ assetId: result.AssetId,
1074
+ backingAssetId: result.BackingAssetId || 0
1075
+ };
1076
+ }
1077
+ async getAssetDetails(assetIds) {
1078
+ if (!this.cookie) {
1079
+ throw new Error("ROBLOSECURITY cookie is not set.");
1080
+ }
1081
+ const response = await this.fetchWithCsrf("https://itemconfiguration.roblox.com/v1/creations/get-asset-details", {
1082
+ method: "POST",
1083
+ headers: { "Content-Type": "application/json" },
1084
+ body: JSON.stringify({ assetIds })
1085
+ });
1086
+ if (!response.ok) {
1087
+ const body = await response.text();
1088
+ throw new Error(`Failed to get asset details (${response.status}): ${body}`);
1089
+ }
1090
+ return response.json();
1091
+ }
1092
+ };
1093
+
1094
+ // ../core/dist/png-encoder.js
1095
+ import { deflateSync } from "zlib";
1096
+ var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
1097
+ var CRC_TABLE = new Uint32Array(256);
1098
+ for (let n = 0; n < 256; n++) {
1099
+ let c = n;
1100
+ for (let k = 0; k < 8; k++)
1101
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
1102
+ CRC_TABLE[n] = c;
1103
+ }
1104
+ function crc32(buf) {
1105
+ let crc = 4294967295;
1106
+ for (let i = 0; i < buf.length; i++)
1107
+ crc = CRC_TABLE[(crc ^ buf[i]) & 255] ^ crc >>> 8;
1108
+ return (crc ^ 4294967295) >>> 0;
1109
+ }
1110
+ function writeChunk(type, data) {
1111
+ const typeBytes = Buffer.from(type, "ascii");
1112
+ const length = Buffer.alloc(4);
1113
+ length.writeUInt32BE(data.length);
1114
+ const crcInput = Buffer.concat([typeBytes, data]);
1115
+ const checksum = Buffer.alloc(4);
1116
+ checksum.writeUInt32BE(crc32(crcInput));
1117
+ return Buffer.concat([length, typeBytes, data, checksum]);
1118
+ }
1119
+ function rgbaToPng(rgba, width, height) {
1120
+ if (width <= 0 || height <= 0)
1121
+ throw new Error(`Invalid PNG dimensions: ${width}x${height}`);
1122
+ const expected = width * height * 4;
1123
+ if (rgba.length < expected)
1124
+ throw new Error(`Buffer too small: got ${rgba.length}, need ${expected}`);
1125
+ const stride = width * 4;
1126
+ const filtered = Buffer.alloc(height * (1 + stride));
1127
+ for (let y = 0; y < height; y++) {
1128
+ filtered[y * (1 + stride)] = 0;
1129
+ rgba.copy(filtered, y * (1 + stride) + 1, y * stride, (y + 1) * stride);
1130
+ }
1131
+ const ihdr = Buffer.alloc(13);
1132
+ ihdr.writeUInt32BE(width, 0);
1133
+ ihdr.writeUInt32BE(height, 4);
1134
+ ihdr[8] = 8;
1135
+ ihdr[9] = 6;
1136
+ ihdr[10] = 0;
1137
+ ihdr[11] = 0;
1138
+ ihdr[12] = 0;
1139
+ return Buffer.concat([
1140
+ PNG_SIGNATURE,
1141
+ writeChunk("IHDR", ihdr),
1142
+ writeChunk("IDAT", deflateSync(filtered)),
1143
+ writeChunk("IEND", Buffer.alloc(0))
1144
+ ]);
1145
+ }
1146
+
1147
+ // ../core/dist/tools/index.js
1148
+ import * as fs from "fs";
1149
+ import * as os from "os";
1150
+ import * as path from "path";
1151
+ function encodePngFromRgbaResponse(response) {
1152
+ if (!response.data || response.width === void 0 || response.height === void 0) {
1153
+ throw new Error("Render response missing data, width, or height");
1154
+ }
1155
+ const rgbaBuffer = Buffer.from(response.data, "base64");
1156
+ return rgbaToPng(rgbaBuffer, response.width, response.height);
1157
+ }
1158
+ var RobloxStudioTools = class _RobloxStudioTools {
1159
+ client;
1160
+ bridge;
1161
+ openCloudClient;
1162
+ cookieClient;
1163
+ constructor(bridge) {
1164
+ this.client = new StudioHttpClient(bridge);
1165
+ this.bridge = bridge;
1166
+ this.openCloudClient = new OpenCloudClient();
1167
+ this.cookieClient = new RobloxCookieClient();
1168
+ }
1169
+ async getFileTree(path2 = "") {
1170
+ const response = await this.client.request("/api/file-tree", { path: path2 });
1171
+ return {
1172
+ content: [
1173
+ {
1174
+ type: "text",
1175
+ text: JSON.stringify(response)
1176
+ }
1177
+ ]
1178
+ };
1179
+ }
1180
+ async searchFiles(query, searchType = "name") {
1181
+ const response = await this.client.request("/api/search-files", { query, searchType });
1182
+ return {
1183
+ content: [
1184
+ {
1185
+ type: "text",
1186
+ text: JSON.stringify(response)
1187
+ }
1188
+ ]
1189
+ };
1190
+ }
1191
+ async getPlaceInfo() {
1192
+ const response = await this.client.request("/api/place-info", {});
1193
+ return {
1194
+ content: [
1195
+ {
1196
+ type: "text",
1197
+ text: JSON.stringify(response)
1198
+ }
1199
+ ]
1200
+ };
1201
+ }
1202
+ async getServices(serviceName) {
1203
+ const response = await this.client.request("/api/services", { serviceName });
1204
+ return {
1205
+ content: [
1206
+ {
1207
+ type: "text",
1208
+ text: JSON.stringify(response)
1209
+ }
1210
+ ]
1211
+ };
1212
+ }
1213
+ async searchObjects(query, searchType = "name", propertyName) {
1214
+ const response = await this.client.request("/api/search-objects", {
1215
+ query,
1216
+ searchType,
1217
+ propertyName
1218
+ });
1219
+ return {
1220
+ content: [
1221
+ {
1222
+ type: "text",
1223
+ text: JSON.stringify(response)
1224
+ }
1225
+ ]
1226
+ };
1227
+ }
1228
+ async getInstanceProperties(instancePath, excludeSource) {
1229
+ if (!instancePath) {
1230
+ throw new Error("Instance path is required for get_instance_properties");
1231
+ }
1232
+ const response = await this.client.request("/api/instance-properties", { instancePath, excludeSource });
1233
+ return {
1234
+ content: [
1235
+ {
1236
+ type: "text",
1237
+ text: JSON.stringify(response)
1238
+ }
1239
+ ]
1240
+ };
1241
+ }
1242
+ async getInstanceChildren(instancePath) {
1243
+ if (!instancePath) {
1244
+ throw new Error("Instance path is required for get_instance_children");
1245
+ }
1246
+ const response = await this.client.request("/api/instance-children", { instancePath });
1247
+ return {
1248
+ content: [
1249
+ {
1250
+ type: "text",
1251
+ text: JSON.stringify(response)
1252
+ }
1253
+ ]
1254
+ };
1255
+ }
1256
+ async searchByProperty(propertyName, propertyValue) {
1257
+ if (!propertyName || !propertyValue) {
1258
+ throw new Error("Property name and value are required for search_by_property");
1259
+ }
1260
+ const response = await this.client.request("/api/search-by-property", {
1261
+ propertyName,
1262
+ propertyValue
1263
+ });
1264
+ return {
1265
+ content: [
1266
+ {
1267
+ type: "text",
1268
+ text: JSON.stringify(response)
1269
+ }
1270
+ ]
1271
+ };
1272
+ }
1273
+ async getClassInfo(className) {
1274
+ if (!className) {
1275
+ throw new Error("Class name is required for get_class_info");
1276
+ }
1277
+ const response = await this.client.request("/api/class-info", { className });
1278
+ return {
1279
+ content: [
1280
+ {
1281
+ type: "text",
1282
+ text: JSON.stringify(response)
1283
+ }
1284
+ ]
1285
+ };
1286
+ }
1287
+ async getProjectStructure(path2, maxDepth, scriptsOnly) {
1288
+ const response = await this.client.request("/api/project-structure", {
1289
+ path: path2,
1290
+ maxDepth,
1291
+ scriptsOnly
1292
+ });
1293
+ return {
1294
+ content: [
1295
+ {
1296
+ type: "text",
1297
+ text: JSON.stringify(response)
1298
+ }
1299
+ ]
1300
+ };
1301
+ }
1302
+ async setProperty(instancePath, propertyName, propertyValue) {
1303
+ if (!instancePath || !propertyName) {
1304
+ throw new Error("Instance path and property name are required for set_property");
1305
+ }
1306
+ const response = await this.client.request("/api/set-property", {
1307
+ instancePath,
1308
+ propertyName,
1309
+ propertyValue
1310
+ });
1311
+ return {
1312
+ content: [
1313
+ {
1314
+ type: "text",
1315
+ text: JSON.stringify(response)
1316
+ }
1317
+ ]
1318
+ };
1319
+ }
1320
+ async setProperties(instancePath, properties) {
1321
+ if (!instancePath || !properties) {
1322
+ throw new Error("instancePath and properties are required for set_properties");
1323
+ }
1324
+ const response = await this.client.request("/api/set-properties", { instancePath, properties });
1325
+ return { content: [{ type: "text", text: JSON.stringify(response) }] };
1326
+ }
1327
+ async massSetProperty(paths, propertyName, propertyValue) {
1328
+ if (!paths || paths.length === 0 || !propertyName) {
1329
+ throw new Error("Paths array and property name are required for mass_set_property");
1330
+ }
1331
+ const response = await this.client.request("/api/mass-set-property", {
1332
+ paths,
1333
+ propertyName,
1334
+ propertyValue
1335
+ });
1336
+ return {
1337
+ content: [
1338
+ {
1339
+ type: "text",
1340
+ text: JSON.stringify(response)
1341
+ }
1342
+ ]
1343
+ };
1344
+ }
1345
+ async massGetProperty(paths, propertyName) {
1346
+ if (!paths || paths.length === 0 || !propertyName) {
1347
+ throw new Error("Paths array and property name are required for mass_get_property");
1348
+ }
1349
+ const response = await this.client.request("/api/mass-get-property", {
1350
+ paths,
1351
+ propertyName
1352
+ });
1353
+ return {
1354
+ content: [
1355
+ {
1356
+ type: "text",
1357
+ text: JSON.stringify(response)
1358
+ }
1359
+ ]
1360
+ };
1361
+ }
1362
+ async createObject(className, parent, name, properties) {
1363
+ if (!className || !parent) {
1364
+ throw new Error("Class name and parent are required for create_object");
1365
+ }
1366
+ const response = await this.client.request("/api/create-object", {
1367
+ className,
1368
+ parent,
1369
+ name,
1370
+ properties
1371
+ });
1372
+ return {
1373
+ content: [
1374
+ {
1375
+ type: "text",
1376
+ text: JSON.stringify(response)
1377
+ }
1378
+ ]
1379
+ };
1380
+ }
1381
+ async massCreateObjects(objects) {
1382
+ if (!objects || objects.length === 0) {
1383
+ throw new Error("Objects array is required for mass_create_objects");
1384
+ }
1385
+ const response = await this.client.request("/api/mass-create-objects", { objects });
1386
+ return {
1387
+ content: [
1388
+ {
1389
+ type: "text",
1390
+ text: JSON.stringify(response)
1391
+ }
1392
+ ]
1393
+ };
1394
+ }
1395
+ async deleteObject(instancePath) {
1396
+ if (!instancePath) {
1397
+ throw new Error("Instance path is required for delete_object");
1398
+ }
1399
+ const response = await this.client.request("/api/delete-object", { instancePath });
1400
+ return {
1401
+ content: [
1402
+ {
1403
+ type: "text",
1404
+ text: JSON.stringify(response)
1405
+ }
1406
+ ]
1407
+ };
1408
+ }
1409
+ async smartDuplicate(instancePath, count, options) {
1410
+ if (!instancePath || count < 1) {
1411
+ throw new Error("Instance path and count > 0 are required for smart_duplicate");
1412
+ }
1413
+ const response = await this.client.request("/api/smart-duplicate", {
1414
+ instancePath,
1415
+ count,
1416
+ options
1417
+ });
1418
+ return {
1419
+ content: [
1420
+ {
1421
+ type: "text",
1422
+ text: JSON.stringify(response)
1423
+ }
1424
+ ]
1425
+ };
1426
+ }
1427
+ async massDuplicate(duplications) {
1428
+ if (!duplications || duplications.length === 0) {
1429
+ throw new Error("Duplications array is required for mass_duplicate");
1430
+ }
1431
+ const response = await this.client.request("/api/mass-duplicate", { duplications });
1432
+ return {
1433
+ content: [
1434
+ {
1435
+ type: "text",
1436
+ text: JSON.stringify(response)
1437
+ }
1438
+ ]
1439
+ };
1440
+ }
1441
+ async getScriptSource(instancePath, startLine, endLine) {
1442
+ if (!instancePath) {
1443
+ throw new Error("Instance path is required for get_script_source");
1444
+ }
1445
+ const response = await this.client.request("/api/get-script-source", { instancePath, startLine, endLine });
1446
+ if (response.error) {
1447
+ return { content: [{ type: "text", text: `Error: ${response.error}` }] };
1448
+ }
1449
+ const scriptTypeInfo = {
1450
+ "Script": "Server Script, runs on the server only",
1451
+ "LocalScript": "Local Script, runs on the client",
1452
+ "ModuleScript": "Module Script, shared library loaded via require()"
1453
+ };
1454
+ const serviceInfo = {
1455
+ "Workspace": "Workspace, 3D world replicated to all clients",
1456
+ "ServerScriptService": "ServerScriptService, server only",
1457
+ "ServerStorage": "ServerStorage, server only storage",
1458
+ "StarterGui": "StarterGui, UI templates copied to each player",
1459
+ "StarterPlayerScripts": "StarterPlayerScripts, client scripts",
1460
+ "StarterCharacterScripts": "StarterCharacterScripts, character scripts",
1461
+ "ReplicatedStorage": "ReplicatedStorage, shared server and client",
1462
+ "ReplicatedFirst": "ReplicatedFirst, first to load on client"
1463
+ };
1464
+ const pathStr = response.instancePath || instancePath;
1465
+ const pathSegments = pathStr.split(".");
1466
+ const topService = typeof response.topService === "string" && response.topService.length > 0 ? response.topService : pathSegments[0] === "game" ? pathSegments[1] ?? "game" : pathSegments[0];
1467
+ const typeNote = scriptTypeInfo[response.className] || response.className;
1468
+ const serviceNote = serviceInfo[topService] || topService;
1469
+ const headerLines = [
1470
+ `Path: ${pathStr}`,
1471
+ `Type: ${typeNote}`,
1472
+ `Location: ${serviceNote}`,
1473
+ `Lines: ${response.lineCount} total${response.isPartial ? ` (showing ${response.startLine}-${response.endLine})` : ""}`
1474
+ ];
1475
+ if (response.enabled === false) {
1476
+ headerLines.push(`Status: DISABLED`);
1477
+ }
1478
+ if (response.truncated) {
1479
+ headerLines.push(`Note: Truncated to first 1000 lines, use startLine/endLine to read more`);
1480
+ }
1481
+ const header = headerLines.join("\n");
1482
+ const code = response.numberedSource || response.source;
1483
+ return {
1484
+ content: [{
1485
+ type: "text",
1486
+ text: `${header}
1487
+
1488
+ ${code}`
1489
+ }]
1490
+ };
1491
+ }
1492
+ async setScriptSource(instancePath, source) {
1493
+ if (!instancePath || typeof source !== "string") {
1494
+ throw new Error("Instance path and source code string are required for set_script_source");
1495
+ }
1496
+ const response = await this.client.request("/api/set-script-source", { instancePath, source });
1497
+ return {
1498
+ content: [
1499
+ {
1500
+ type: "text",
1501
+ text: JSON.stringify(response)
1502
+ }
1503
+ ]
1504
+ };
1505
+ }
1506
+ async editScriptLines(instancePath, oldString, newString, startLine) {
1507
+ if (!instancePath || typeof oldString !== "string" || typeof newString !== "string") {
1508
+ throw new Error("Instance path, old_string, and new_string are required for edit_script_lines");
1509
+ }
1510
+ const payload = { instancePath, old_string: oldString, new_string: newString };
1511
+ if (startLine !== void 0)
1512
+ payload.startLine = startLine;
1513
+ const response = await this.client.request("/api/edit-script-lines", payload);
1514
+ return {
1515
+ content: [
1516
+ {
1517
+ type: "text",
1518
+ text: JSON.stringify(response)
1519
+ }
1520
+ ]
1521
+ };
1522
+ }
1523
+ async insertScriptLines(instancePath, afterLine, newContent) {
1524
+ if (!instancePath || typeof newContent !== "string") {
1525
+ throw new Error("Instance path and newContent are required for insert_script_lines");
1526
+ }
1527
+ const response = await this.client.request("/api/insert-script-lines", { instancePath, afterLine: afterLine || 0, newContent });
1528
+ return {
1529
+ content: [
1530
+ {
1531
+ type: "text",
1532
+ text: JSON.stringify(response)
1533
+ }
1534
+ ]
1535
+ };
1536
+ }
1537
+ async deleteScriptLines(instancePath, startLine, endLine) {
1538
+ if (!instancePath || !startLine || !endLine) {
1539
+ throw new Error("Instance path, startLine, and endLine are required for delete_script_lines");
1540
+ }
1541
+ const response = await this.client.request("/api/delete-script-lines", { instancePath, startLine, endLine });
1542
+ return {
1543
+ content: [
1544
+ {
1545
+ type: "text",
1546
+ text: JSON.stringify(response)
1547
+ }
1548
+ ]
1549
+ };
1550
+ }
1551
+ async grepScripts(pattern, options) {
1552
+ if (!pattern) {
1553
+ throw new Error("Pattern is required for grep_scripts");
1554
+ }
1555
+ const response = await this.client.request("/api/grep-scripts", {
1556
+ pattern,
1557
+ ...options
1558
+ });
1559
+ return {
1560
+ content: [
1561
+ {
1562
+ type: "text",
1563
+ text: JSON.stringify(response)
1564
+ }
1565
+ ]
1566
+ };
1567
+ }
1568
+ async setAttribute(instancePath, attributeName, attributeValue, valueType) {
1569
+ if (!instancePath || !attributeName) {
1570
+ throw new Error("Instance path and attribute name are required for set_attribute");
1571
+ }
1572
+ const response = await this.client.request("/api/set-attribute", { instancePath, attributeName, attributeValue, valueType });
1573
+ return {
1574
+ content: [
1575
+ {
1576
+ type: "text",
1577
+ text: JSON.stringify(response)
1578
+ }
1579
+ ]
1580
+ };
1581
+ }
1582
+ async getAttributes(instancePath) {
1583
+ if (!instancePath) {
1584
+ throw new Error("Instance path is required for get_attributes");
1585
+ }
1586
+ const response = await this.client.request("/api/get-attributes", { instancePath });
1587
+ return {
1588
+ content: [
1589
+ {
1590
+ type: "text",
1591
+ text: JSON.stringify(response)
1592
+ }
1593
+ ]
1594
+ };
1595
+ }
1596
+ async deleteAttribute(instancePath, attributeName) {
1597
+ if (!instancePath || !attributeName) {
1598
+ throw new Error("Instance path and attribute name are required for delete_attribute");
1599
+ }
1600
+ const response = await this.client.request("/api/delete-attribute", { instancePath, attributeName });
1601
+ return {
1602
+ content: [
1603
+ {
1604
+ type: "text",
1605
+ text: JSON.stringify(response)
1606
+ }
1607
+ ]
1608
+ };
1609
+ }
1610
+ async getTags(instancePath) {
1611
+ if (!instancePath) {
1612
+ throw new Error("Instance path is required for get_tags");
1613
+ }
1614
+ const response = await this.client.request("/api/get-tags", { instancePath });
1615
+ return {
1616
+ content: [
1617
+ {
1618
+ type: "text",
1619
+ text: JSON.stringify(response)
1620
+ }
1621
+ ]
1622
+ };
1623
+ }
1624
+ async addTag(instancePath, tagName) {
1625
+ if (!instancePath || !tagName) {
1626
+ throw new Error("Instance path and tag name are required for add_tag");
1627
+ }
1628
+ const response = await this.client.request("/api/add-tag", { instancePath, tagName });
1629
+ return {
1630
+ content: [
1631
+ {
1632
+ type: "text",
1633
+ text: JSON.stringify(response)
1634
+ }
1635
+ ]
1636
+ };
1637
+ }
1638
+ async removeTag(instancePath, tagName) {
1639
+ if (!instancePath || !tagName) {
1640
+ throw new Error("Instance path and tag name are required for remove_tag");
1641
+ }
1642
+ const response = await this.client.request("/api/remove-tag", { instancePath, tagName });
1643
+ return {
1644
+ content: [
1645
+ {
1646
+ type: "text",
1647
+ text: JSON.stringify(response)
1648
+ }
1649
+ ]
1650
+ };
1651
+ }
1652
+ async getTagged(tagName) {
1653
+ if (!tagName) {
1654
+ throw new Error("Tag name is required for get_tagged");
1655
+ }
1656
+ const response = await this.client.request("/api/get-tagged", { tagName });
1657
+ return {
1658
+ content: [
1659
+ {
1660
+ type: "text",
1661
+ text: JSON.stringify(response)
1662
+ }
1663
+ ]
1664
+ };
1665
+ }
1666
+ async getSelection() {
1667
+ const response = await this.client.request("/api/get-selection", {});
1668
+ return {
1669
+ content: [
1670
+ {
1671
+ type: "text",
1672
+ text: JSON.stringify(response)
1673
+ }
1674
+ ]
1675
+ };
1676
+ }
1677
+ async executeLuau(code, target) {
1678
+ if (!code) {
1679
+ throw new Error("Code is required for execute_luau");
1680
+ }
1681
+ const response = await this.client.request("/api/execute-luau", { code }, target || "edit");
1682
+ return {
1683
+ content: [
1684
+ {
1685
+ type: "text",
1686
+ text: JSON.stringify(response)
1687
+ }
1688
+ ]
1689
+ };
1690
+ }
1691
+ async startPlaytest(mode, numPlayers) {
1692
+ if (mode !== "play" && mode !== "run") {
1693
+ throw new Error('mode must be "play" or "run"');
1694
+ }
1695
+ const data = { mode };
1696
+ if (numPlayers !== void 0) {
1697
+ data.numPlayers = numPlayers;
1698
+ }
1699
+ const response = await this.client.request("/api/start-playtest", data);
1700
+ return {
1701
+ content: [
1702
+ {
1703
+ type: "text",
1704
+ text: JSON.stringify(response)
1705
+ }
1706
+ ]
1707
+ };
1708
+ }
1709
+ async stopPlaytest() {
1710
+ const response = await this.client.request("/api/stop-playtest", {});
1711
+ return {
1712
+ content: [
1713
+ {
1714
+ type: "text",
1715
+ text: JSON.stringify(response)
1716
+ }
1717
+ ]
1718
+ };
1719
+ }
1720
+ async getPlaytestOutput(target) {
1721
+ const response = await this.client.request("/api/get-playtest-output", {}, target || "edit");
1722
+ return {
1723
+ content: [
1724
+ {
1725
+ type: "text",
1726
+ text: JSON.stringify(response)
1727
+ }
1728
+ ]
1729
+ };
1730
+ }
1731
+ async getConnectedInstances() {
1732
+ const instances = this.bridge.getInstances();
1733
+ return {
1734
+ content: [
1735
+ {
1736
+ type: "text",
1737
+ text: JSON.stringify({ instances, count: instances.length })
1738
+ }
1739
+ ]
1740
+ };
1741
+ }
1742
+ async undo() {
1743
+ const response = await this.client.request("/api/undo", {});
1744
+ return {
1745
+ content: [
1746
+ {
1747
+ type: "text",
1748
+ text: JSON.stringify(response)
1749
+ }
1750
+ ]
1751
+ };
1752
+ }
1753
+ async redo() {
1754
+ const response = await this.client.request("/api/redo", {});
1755
+ return {
1756
+ content: [
1757
+ {
1758
+ type: "text",
1759
+ text: JSON.stringify(response)
1760
+ }
1761
+ ]
1762
+ };
1763
+ }
1764
+ static findProjectRoot(startDir) {
1765
+ let dir = path.resolve(startDir);
1766
+ while (true) {
1767
+ if (fs.existsSync(path.join(dir, ".git")) || fs.existsSync(path.join(dir, "package.json"))) {
1768
+ return dir;
1769
+ }
1770
+ const parent = path.dirname(dir);
1771
+ if (parent === dir)
1772
+ return null;
1773
+ dir = parent;
1774
+ }
1775
+ }
1776
+ static isDirectory(candidate) {
1777
+ if (!candidate)
1778
+ return false;
1779
+ try {
1780
+ return fs.statSync(candidate).isDirectory();
1781
+ } catch {
1782
+ return false;
1783
+ }
1784
+ }
1785
+ static ensureWritableDirectory(candidate, label) {
1786
+ const resolved = path.resolve(candidate);
1787
+ try {
1788
+ fs.mkdirSync(resolved, { recursive: true });
1789
+ } catch (error) {
1790
+ throw new Error(`Unable to create ${label} build-library directory at ${resolved}: ${error.message}`);
1791
+ }
1792
+ if (!_RobloxStudioTools.isDirectory(resolved)) {
1793
+ throw new Error(`${label} build-library path is not a directory: ${resolved}`);
1794
+ }
1795
+ try {
1796
+ fs.accessSync(resolved, fs.constants.W_OK);
1797
+ } catch (error) {
1798
+ throw new Error(`${label} build-library directory is not writable: ${resolved}. ${error.message}`);
1799
+ }
1800
+ return resolved;
1801
+ }
1802
+ static _cachedLibraryPath;
1803
+ static findLibraryPath() {
1804
+ if (_RobloxStudioTools._cachedLibraryPath)
1805
+ return _RobloxStudioTools._cachedLibraryPath;
1806
+ const overridePath = process.env.ROBLOXSTUDIO_MCP_BUILD_LIBRARY || process.env.BUILD_LIBRARY_PATH;
1807
+ const cwd = path.resolve(process.cwd());
1808
+ const projectRoot = _RobloxStudioTools.findProjectRoot(cwd);
1809
+ const homeLibraryPath = path.join(os.homedir(), ".robloxstudio-mcp", "build-library");
1810
+ const projectLibraryPath = projectRoot ? path.join(projectRoot, "build-library") : null;
1811
+ const cwdLibraryPath = path.join(cwd, "build-library");
1812
+ let result;
1813
+ if (overridePath) {
1814
+ result = _RobloxStudioTools.ensureWritableDirectory(overridePath, "override");
1815
+ } else {
1816
+ const existing = [projectLibraryPath, cwdLibraryPath].find((c) => c && _RobloxStudioTools.isDirectory(c) && (() => {
1817
+ try {
1818
+ fs.accessSync(c, fs.constants.W_OK);
1819
+ return true;
1820
+ } catch {
1821
+ return false;
1822
+ }
1823
+ })());
1824
+ if (existing) {
1825
+ result = path.resolve(existing);
1826
+ } else if (projectLibraryPath) {
1827
+ try {
1828
+ result = _RobloxStudioTools.ensureWritableDirectory(projectLibraryPath, "project-root");
1829
+ } catch (err) {
1830
+ console.error(`Warning: could not create build-library at project root (${projectLibraryPath}): ${err.message}. Falling back to home directory.`);
1831
+ result = _RobloxStudioTools.ensureWritableDirectory(homeLibraryPath, "home");
1832
+ }
1833
+ } else {
1834
+ result = _RobloxStudioTools.ensureWritableDirectory(homeLibraryPath, "home");
1835
+ }
1836
+ }
1837
+ _RobloxStudioTools._cachedLibraryPath = result;
1838
+ return result;
1839
+ }
1840
+ async exportBuild(instancePath, outputId, style = "misc") {
1841
+ if (!instancePath) {
1842
+ throw new Error("Instance path is required for export_build");
1843
+ }
1844
+ const response = await this.client.request("/api/export-build", {
1845
+ instancePath,
1846
+ outputId,
1847
+ style
1848
+ });
1849
+ if (response && response.success && response.buildData) {
1850
+ const buildData = response.buildData;
1851
+ const buildId = buildData.id || `${style}/exported`;
1852
+ const filePath = path.join(_RobloxStudioTools.findLibraryPath(), `${buildId}.json`);
1853
+ const dirPath = path.dirname(filePath);
1854
+ if (!fs.existsSync(dirPath)) {
1855
+ fs.mkdirSync(dirPath, { recursive: true });
1856
+ }
1857
+ fs.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
1858
+ response.savedTo = filePath;
1859
+ }
1860
+ return {
1861
+ content: [
1862
+ {
1863
+ type: "text",
1864
+ text: JSON.stringify(response)
1865
+ }
1866
+ ]
1867
+ };
1868
+ }
1869
+ normalizePalette(palette) {
1870
+ if (!palette || typeof palette !== "object" || Array.isArray(palette)) {
1871
+ throw new Error("palette must be an object mapping keys to [BrickColor, Material] tuples");
1872
+ }
1873
+ const normalized = {};
1874
+ for (const [key, value] of Object.entries(palette)) {
1875
+ if (!Array.isArray(value) || value.length < 2) {
1876
+ throw new Error(`Palette key "${key}" must map to [BrickColor, Material]`);
1877
+ }
1878
+ normalized[key] = [String(value[0]), String(value[1])];
1879
+ }
1880
+ if (Object.keys(normalized).length === 0) {
1881
+ throw new Error("palette must contain at least one key");
1882
+ }
1883
+ return normalized;
1884
+ }
1885
+ normalizeBuildParts(parts, paletteKeys) {
1886
+ if (!Array.isArray(parts) || parts.length === 0) {
1887
+ throw new Error("parts must be a non-empty array");
1888
+ }
1889
+ const ALLOWED_SHAPES = /* @__PURE__ */ new Set(["Block", "Wedge", "Cylinder", "Ball", "CornerWedge"]);
1890
+ const normalized = [];
1891
+ for (let i = 0; i < parts.length; i++) {
1892
+ const part = parts[i];
1893
+ if (Array.isArray(part)) {
1894
+ if (part.length < 10) {
1895
+ throw new Error(`Part ${i} must have at least 10 elements`);
1896
+ }
1897
+ const [px, py, pz, sx, sy, sz, rx, ry, rz, paletteKey, ...rest] = part;
1898
+ if (typeof paletteKey !== "string" || !paletteKeys.has(paletteKey)) {
1899
+ throw new Error(`Part ${i} references unknown palette key "${paletteKey}"`);
1900
+ }
1901
+ const tuple2 = [px, py, pz, sx, sy, sz, rx, ry, rz, paletteKey];
1902
+ if (rest[0] !== void 0) {
1903
+ if (!ALLOWED_SHAPES.has(rest[0]))
1904
+ throw new Error(`Part ${i} has invalid shape "${rest[0]}"`);
1905
+ tuple2.push(rest[0]);
1906
+ }
1907
+ if (rest[1] !== void 0) {
1908
+ if (!rest[0])
1909
+ tuple2.push("Block");
1910
+ tuple2.push(rest[1]);
1911
+ }
1912
+ normalized.push(tuple2);
1913
+ continue;
1914
+ }
1915
+ if (!part || typeof part !== "object") {
1916
+ throw new Error(`Part ${i} must be an array or object`);
1917
+ }
1918
+ const r = part;
1919
+ const position = r.position;
1920
+ const size = r.size;
1921
+ const rotation = r.rotation;
1922
+ const pk = r.paletteKey;
1923
+ if (!Array.isArray(position) || position.length !== 3)
1924
+ throw new Error(`Part ${i}: position must be [x,y,z]`);
1925
+ if (!Array.isArray(size) || size.length !== 3)
1926
+ throw new Error(`Part ${i}: size must be [x,y,z]`);
1927
+ if (!Array.isArray(rotation) || rotation.length !== 3)
1928
+ throw new Error(`Part ${i}: rotation must be [x,y,z]`);
1929
+ if (typeof pk !== "string" || !paletteKeys.has(pk))
1930
+ throw new Error(`Part ${i} references unknown palette key "${pk}"`);
1931
+ const tuple = [...position, ...size, ...rotation, pk];
1932
+ if (r.shape !== void 0) {
1933
+ if (!ALLOWED_SHAPES.has(r.shape))
1934
+ throw new Error(`Part ${i} has invalid shape "${r.shape}"`);
1935
+ tuple.push(r.shape);
1936
+ }
1937
+ if (r.transparency !== void 0) {
1938
+ if (!r.shape)
1939
+ tuple.push("Block");
1940
+ tuple.push(r.transparency);
1941
+ }
1942
+ normalized.push(tuple);
1943
+ }
1944
+ return normalized;
1945
+ }
1946
+ async createBuild(id, style, palette, parts, bounds) {
1947
+ if (!id) {
1948
+ throw new Error("id is required for create_build");
1949
+ }
1950
+ const normalizedPalette = this.normalizePalette(palette);
1951
+ const normalizedParts = this.normalizeBuildParts(parts, new Set(Object.keys(normalizedPalette)));
1952
+ const computedBounds = bounds || this.computeBounds(normalizedParts);
1953
+ const buildData = { id, style, bounds: computedBounds, palette: normalizedPalette, parts: normalizedParts };
1954
+ const filePath = path.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
1955
+ const dirPath = path.dirname(filePath);
1956
+ if (!fs.existsSync(dirPath)) {
1957
+ fs.mkdirSync(dirPath, { recursive: true });
1958
+ }
1959
+ fs.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
1960
+ return {
1961
+ content: [
1962
+ {
1963
+ type: "text",
1964
+ text: JSON.stringify({
1965
+ success: true,
1966
+ id,
1967
+ style,
1968
+ bounds: computedBounds,
1969
+ partCount: normalizedParts.length,
1970
+ paletteKeys: Object.keys(normalizedPalette),
1971
+ savedTo: filePath
1972
+ })
1973
+ }
1974
+ ]
1975
+ };
1976
+ }
1977
+ computeBounds(parts) {
1978
+ let maxX = 0, maxY = 0, maxZ = 0;
1979
+ for (const p of parts) {
1980
+ const px = Math.abs(p[0]) + p[3] / 2;
1981
+ const py = Math.abs(p[1]) + p[4] / 2;
1982
+ const pz = Math.abs(p[2]) + p[5] / 2;
1983
+ maxX = Math.max(maxX, px);
1984
+ maxY = Math.max(maxY, py);
1985
+ maxZ = Math.max(maxZ, pz);
1986
+ }
1987
+ return [
1988
+ Math.round(maxX * 2 * 10) / 10,
1989
+ Math.round(maxY * 2 * 10) / 10,
1990
+ Math.round(maxZ * 2 * 10) / 10
1991
+ ];
1992
+ }
1993
+ async generateBuild(id, style, palette, code, seed) {
1994
+ if (!id || !palette || !code) {
1995
+ throw new Error("id, palette, and code are required for generate_build");
1996
+ }
1997
+ for (const [key, value] of Object.entries(palette)) {
1998
+ if (!Array.isArray(value) || value.length < 2 || value.length > 3) {
1999
+ throw new Error(`Palette key "${key}" must map to [BrickColor, Material] or [BrickColor, Material, MaterialVariant]`);
2000
+ }
2001
+ }
2002
+ const result = runBuildExecutor(code, palette, seed);
2003
+ const buildData = {
2004
+ id,
2005
+ style,
2006
+ bounds: result.bounds,
2007
+ palette,
2008
+ parts: result.parts,
2009
+ generatorCode: code
2010
+ };
2011
+ if (seed !== void 0)
2012
+ buildData.generatorSeed = seed;
2013
+ const filePath = path.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
2014
+ const dirPath = path.dirname(filePath);
2015
+ if (!fs.existsSync(dirPath)) {
2016
+ fs.mkdirSync(dirPath, { recursive: true });
2017
+ }
2018
+ fs.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
2019
+ return {
2020
+ content: [
2021
+ {
2022
+ type: "text",
2023
+ text: JSON.stringify({
2024
+ success: true,
2025
+ id,
2026
+ style,
2027
+ bounds: result.bounds,
2028
+ partCount: result.partCount,
2029
+ paletteKeys: Object.keys(palette),
2030
+ savedTo: filePath
2031
+ })
2032
+ }
2033
+ ]
2034
+ };
2035
+ }
2036
+ async importBuild(buildData, targetPath, position) {
2037
+ if (!buildData || !targetPath) {
2038
+ throw new Error("buildData (or library ID string) and targetPath are required for import_build");
2039
+ }
2040
+ let resolved;
2041
+ if (typeof buildData === "string") {
2042
+ const filePath = path.join(_RobloxStudioTools.findLibraryPath(), `${buildData}.json`);
2043
+ if (!fs.existsSync(filePath)) {
2044
+ throw new Error(`Build not found in library: ${buildData}`);
2045
+ }
2046
+ resolved = JSON.parse(fs.readFileSync(filePath, "utf-8"));
2047
+ } else if (buildData.id && !buildData.parts) {
2048
+ const filePath = path.join(_RobloxStudioTools.findLibraryPath(), `${buildData.id}.json`);
2049
+ if (!fs.existsSync(filePath)) {
2050
+ throw new Error(`Build not found in library: ${buildData.id}`);
2051
+ }
2052
+ resolved = JSON.parse(fs.readFileSync(filePath, "utf-8"));
2053
+ } else {
2054
+ resolved = buildData;
2055
+ }
2056
+ const response = await this.client.request("/api/import-build", {
2057
+ buildData: resolved,
2058
+ targetPath,
2059
+ position
2060
+ });
2061
+ return {
2062
+ content: [
2063
+ {
2064
+ type: "text",
2065
+ text: JSON.stringify(response)
2066
+ }
2067
+ ]
2068
+ };
2069
+ }
2070
+ async listLibrary(style) {
2071
+ const libraryPath = _RobloxStudioTools.findLibraryPath();
2072
+ const styles = style ? [style] : ["medieval", "modern", "nature", "scifi", "misc"];
2073
+ const builds = [];
2074
+ for (const s of styles) {
2075
+ const dirPath = path.join(libraryPath, s);
2076
+ if (!fs.existsSync(dirPath))
2077
+ continue;
2078
+ const files = fs.readdirSync(dirPath).filter((f) => f.endsWith(".json"));
2079
+ for (const file of files) {
2080
+ try {
2081
+ const content = fs.readFileSync(path.join(dirPath, file), "utf-8");
2082
+ const data = JSON.parse(content);
2083
+ builds.push({
2084
+ id: data.id || `${s}/${file.replace(".json", "")}`,
2085
+ style: data.style || s,
2086
+ bounds: data.bounds || [0, 0, 0],
2087
+ partCount: Array.isArray(data.parts) ? data.parts.length : 0
2088
+ });
2089
+ } catch {
2090
+ }
2091
+ }
2092
+ }
2093
+ return {
2094
+ content: [
2095
+ {
2096
+ type: "text",
2097
+ text: JSON.stringify({ builds, total: builds.length })
2098
+ }
2099
+ ]
2100
+ };
2101
+ }
2102
+ async searchMaterials(query, maxResults) {
2103
+ const response = await this.client.request("/api/search-materials", {
2104
+ query: query ?? "",
2105
+ maxResults: maxResults ?? 50
2106
+ });
2107
+ return {
2108
+ content: [
2109
+ {
2110
+ type: "text",
2111
+ text: JSON.stringify(response)
2112
+ }
2113
+ ]
2114
+ };
2115
+ }
2116
+ async getBuild(id) {
2117
+ if (!id) {
2118
+ throw new Error("Build ID is required for get_build");
2119
+ }
2120
+ const filePath = path.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
2121
+ if (!fs.existsSync(filePath)) {
2122
+ throw new Error(`Build not found in library: ${id}`);
2123
+ }
2124
+ const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
2125
+ const result = {
2126
+ id: data.id,
2127
+ style: data.style,
2128
+ bounds: data.bounds,
2129
+ partCount: Array.isArray(data.parts) ? data.parts.length : 0,
2130
+ paletteKeys: data.palette ? Object.keys(data.palette) : [],
2131
+ palette: data.palette
2132
+ };
2133
+ if (data.generatorCode) {
2134
+ result.generatorCode = data.generatorCode;
2135
+ result.generatorSeed = data.generatorSeed;
2136
+ }
2137
+ return {
2138
+ content: [
2139
+ {
2140
+ type: "text",
2141
+ text: JSON.stringify(result)
2142
+ }
2143
+ ]
2144
+ };
2145
+ }
2146
+ async importScene(sceneData, targetPath = "game.Workspace") {
2147
+ if (!sceneData) {
2148
+ throw new Error("sceneData is required for import_scene");
2149
+ }
2150
+ const libraryPath = _RobloxStudioTools.findLibraryPath();
2151
+ const expandedBuilds = [];
2152
+ const modelMap = sceneData.models || {};
2153
+ const placements = sceneData.place || [];
2154
+ const isVec3Tuple = (value) => {
2155
+ return Array.isArray(value) && value.length === 3 && value.every((component) => typeof component === "number" && Number.isFinite(component));
2156
+ };
2157
+ for (const [placementIndex, placement] of placements.entries()) {
2158
+ let modelKey;
2159
+ let position;
2160
+ let rotation;
2161
+ let validatedKeyPath;
2162
+ if (Array.isArray(placement)) {
2163
+ if (placement.length < 2 || placement.length > 3) {
2164
+ throw new Error(`Invalid sceneData.place[${placementIndex}]: expected [modelKey, [x,y,z], [rotX?,rotY?,rotZ?]]`);
2165
+ }
2166
+ const [tupleModelKey, tuplePosition, tupleRotation] = placement;
2167
+ if (typeof tupleModelKey !== "string" || tupleModelKey.trim() === "") {
2168
+ throw new Error(`Invalid sceneData.place[${placementIndex}][0]: model key must be a non-empty string`);
2169
+ }
2170
+ modelKey = tupleModelKey.trim();
2171
+ validatedKeyPath = `sceneData.place[${placementIndex}][0]`;
2172
+ if (!isVec3Tuple(tuplePosition)) {
2173
+ throw new Error(`Invalid sceneData.place[${placementIndex}][1]: position must be a numeric [x,y,z] tuple`);
2174
+ }
2175
+ position = tuplePosition;
2176
+ if (tupleRotation !== void 0) {
2177
+ if (!isVec3Tuple(tupleRotation)) {
2178
+ throw new Error(`Invalid sceneData.place[${placementIndex}][2]: rotation must be a numeric [x,y,z] tuple when provided`);
2179
+ }
2180
+ rotation = tupleRotation;
2181
+ }
2182
+ } else if (placement && typeof placement === "object") {
2183
+ const placementRecord = placement;
2184
+ const objectModelKey = placementRecord.modelKey;
2185
+ const objectPosition = placementRecord.position;
2186
+ const objectRotation = placementRecord.rotation;
2187
+ if (typeof objectModelKey !== "string" || objectModelKey.trim() === "") {
2188
+ throw new Error(`Invalid sceneData.place[${placementIndex}].modelKey: model key must be a non-empty string`);
2189
+ }
2190
+ if (!isVec3Tuple(objectPosition)) {
2191
+ throw new Error(`Invalid sceneData.place[${placementIndex}].position: must be a numeric [x,y,z] tuple`);
2192
+ }
2193
+ if (objectRotation !== void 0 && !isVec3Tuple(objectRotation)) {
2194
+ throw new Error(`Invalid sceneData.place[${placementIndex}].rotation: must be a numeric [x,y,z] tuple when provided`);
2195
+ }
2196
+ modelKey = objectModelKey.trim();
2197
+ validatedKeyPath = `sceneData.place[${placementIndex}].modelKey`;
2198
+ position = objectPosition;
2199
+ rotation = objectRotation;
2200
+ } else {
2201
+ throw new Error(`Invalid sceneData.place[${placementIndex}]: expected an object placement or [modelKey, [x,y,z], [rotX?,rotY?,rotZ?]] tuple`);
2202
+ }
2203
+ const buildId = modelMap[modelKey];
2204
+ if (!buildId) {
2205
+ throw new Error(`Invalid ${validatedKeyPath}: model key "${modelKey}" is not defined in sceneData.models`);
2206
+ }
2207
+ const filePath = path.join(libraryPath, `${buildId}.json`);
2208
+ if (!fs.existsSync(filePath)) {
2209
+ throw new Error(`Build not found in library: ${buildId}`);
2210
+ }
2211
+ const content = fs.readFileSync(filePath, "utf-8");
2212
+ const buildData = JSON.parse(content);
2213
+ const buildName = buildId.split("/").pop() || buildId;
2214
+ expandedBuilds.push({
2215
+ buildData,
2216
+ position,
2217
+ rotation: rotation || [0, 0, 0],
2218
+ name: buildName
2219
+ });
2220
+ }
2221
+ const customs = sceneData.custom || [];
2222
+ for (const custom of customs) {
2223
+ expandedBuilds.push({
2224
+ buildData: {
2225
+ palette: custom.palette,
2226
+ parts: custom.parts
2227
+ },
2228
+ position: custom.o || [0, 0, 0],
2229
+ rotation: [0, 0, 0],
2230
+ name: custom.n || "Custom"
2231
+ });
2232
+ }
2233
+ if (expandedBuilds.length === 0) {
2234
+ throw new Error("No builds to import - check model references and library");
2235
+ }
2236
+ const response = await this.client.request("/api/import-scene", {
2237
+ expandedBuilds,
2238
+ targetPath
2239
+ });
2240
+ return {
2241
+ content: [
2242
+ {
2243
+ type: "text",
2244
+ text: JSON.stringify(response)
2245
+ }
2246
+ ]
2247
+ };
2248
+ }
2249
+ // === Asset Tools ===
2250
+ async searchAssets(assetType, query, maxResults, sortBy, verifiedCreatorsOnly) {
2251
+ if (!this.openCloudClient.hasApiKey()) {
2252
+ return {
2253
+ content: [{
2254
+ type: "text",
2255
+ text: JSON.stringify({ error: "ROBLOX_OPEN_CLOUD_API_KEY environment variable is not set. Set it to use Creator Store asset tools." })
2256
+ }]
2257
+ };
2258
+ }
2259
+ const response = await this.openCloudClient.searchAssets({
2260
+ searchCategoryType: assetType,
2261
+ query,
2262
+ maxPageSize: maxResults,
2263
+ sortCategory: sortBy,
2264
+ includeOnlyVerifiedCreators: verifiedCreatorsOnly
2265
+ });
2266
+ return {
2267
+ content: [{
2268
+ type: "text",
2269
+ text: JSON.stringify(response)
2270
+ }]
2271
+ };
2272
+ }
2273
+ async getAssetDetails(assetId) {
2274
+ if (!assetId) {
2275
+ throw new Error("Asset ID is required for get_asset_details");
2276
+ }
2277
+ if (this.cookieClient.hasCookie() && !this.openCloudClient.hasApiKey()) {
2278
+ const results = await this.cookieClient.getAssetDetails([assetId]);
2279
+ const asset = results[0];
2280
+ if (!asset) {
2281
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Asset not found or not owned by authenticated user" }) }] };
2282
+ }
2283
+ return { content: [{ type: "text", text: JSON.stringify(asset) }] };
2284
+ }
2285
+ if (!this.openCloudClient.hasApiKey()) {
2286
+ return {
2287
+ content: [{
2288
+ type: "text",
2289
+ text: JSON.stringify({ error: "No auth configured. Set ROBLOSECURITY or ROBLOX_OPEN_CLOUD_API_KEY env var." })
2290
+ }]
2291
+ };
2292
+ }
2293
+ const response = await this.openCloudClient.getAssetDetails(assetId);
2294
+ return {
2295
+ content: [{
2296
+ type: "text",
2297
+ text: JSON.stringify(response)
2298
+ }]
2299
+ };
2300
+ }
2301
+ async getAssetThumbnail(assetId, size) {
2302
+ if (!assetId) {
2303
+ throw new Error("Asset ID is required for get_asset_thumbnail");
2304
+ }
2305
+ if (!this.openCloudClient.hasApiKey()) {
2306
+ return {
2307
+ content: [{
2308
+ type: "text",
2309
+ text: JSON.stringify({ error: "ROBLOX_OPEN_CLOUD_API_KEY environment variable is not set. Set it to use Creator Store asset tools." })
2310
+ }]
2311
+ };
2312
+ }
2313
+ const result = await this.openCloudClient.getAssetThumbnail(assetId, size);
2314
+ if (!result) {
2315
+ return {
2316
+ content: [{
2317
+ type: "text",
2318
+ text: JSON.stringify({ error: "Thumbnail not available for this asset" })
2319
+ }]
2320
+ };
2321
+ }
2322
+ return {
2323
+ content: [{
2324
+ type: "image",
2325
+ data: result.base64,
2326
+ mimeType: result.mimeType
2327
+ }]
2328
+ };
2329
+ }
2330
+ async insertAsset(assetId, parentPath, position) {
2331
+ if (!assetId) {
2332
+ throw new Error("Asset ID is required for insert_asset");
2333
+ }
2334
+ const response = await this.client.request("/api/insert-asset", {
2335
+ assetId,
2336
+ parentPath: parentPath || "game.Workspace",
2337
+ position
2338
+ });
2339
+ return {
2340
+ content: [{
2341
+ type: "text",
2342
+ text: JSON.stringify(response)
2343
+ }]
2344
+ };
2345
+ }
2346
+ async previewAsset(assetId, includeProperties, maxDepth) {
2347
+ if (!assetId) {
2348
+ throw new Error("Asset ID is required for preview_asset");
2349
+ }
2350
+ const response = await this.client.request("/api/preview-asset", {
2351
+ assetId,
2352
+ includeProperties: includeProperties ?? true,
2353
+ maxDepth: maxDepth ?? 10
2354
+ });
2355
+ return {
2356
+ content: [{
2357
+ type: "text",
2358
+ text: JSON.stringify(response)
2359
+ }]
2360
+ };
2361
+ }
2362
+ // Decal asset IDs are the wrapper asset; ImageLabel.Image needs the underlying image
2363
+ // content ID. The only reliable cross-auth way to resolve this is InsertService:LoadAsset
2364
+ // via the connected Studio plugin - the unauthenticated economy endpoint returns 401.
2365
+ async resolveImageId(decalAssetId) {
2366
+ const code = `
2367
+ local InsertService = game:GetService("InsertService")
2368
+ local ok, result = pcall(function() return InsertService:LoadAsset(${decalAssetId}) end)
2369
+ if not ok then return nil end
2370
+ local decal = result:FindFirstChildWhichIsA("Decal", true)
2371
+ local id = decal and decal.Texture:match("(%d+)") or nil
2372
+ result:Destroy()
2373
+ return id
2374
+ `;
2375
+ try {
2376
+ const response = await this.client.request("/api/execute-luau", { code }, "edit");
2377
+ const returnValue = response?.returnValue;
2378
+ if (returnValue !== void 0 && returnValue !== null && /^\d+$/.test(String(returnValue))) {
2379
+ return String(returnValue);
2380
+ }
2381
+ } catch {
2382
+ }
2383
+ return null;
2384
+ }
2385
+ async uploadAsset(filePath, assetType, displayName, description, userId, groupId) {
2386
+ if (!fs.existsSync(filePath)) {
2387
+ throw new Error(`File not found: ${filePath}`);
2388
+ }
2389
+ const fileContent = fs.readFileSync(filePath);
2390
+ const fileName = path.basename(filePath);
2391
+ if (assetType === "Decal" && this.cookieClient.hasCookie()) {
2392
+ const result2 = await this.cookieClient.uploadDecal(fileContent, displayName, description || "");
2393
+ return {
2394
+ content: [{
2395
+ type: "text",
2396
+ text: JSON.stringify({
2397
+ done: true,
2398
+ response: {
2399
+ assetId: String(result2.assetId),
2400
+ displayName,
2401
+ assetType,
2402
+ decalId: String(result2.assetId),
2403
+ imageId: String(result2.backingAssetId)
2404
+ }
2405
+ })
2406
+ }]
2407
+ };
2408
+ }
2409
+ if (!this.openCloudClient.hasApiKey()) {
2410
+ const cookieHint = assetType === "Decal" ? " Alternatively, set ROBLOSECURITY to use cookie auth." : "";
2411
+ throw new Error(`No auth configured for ${assetType} upload. Set ROBLOX_OPEN_CLOUD_API_KEY (needs asset:write scope).${cookieHint}`);
2412
+ }
2413
+ const resolvedGroupId = groupId || process.env.ROBLOX_CREATOR_GROUP_ID;
2414
+ const resolvedUserId = userId || process.env.ROBLOX_CREATOR_USER_ID;
2415
+ if (!resolvedUserId && !resolvedGroupId) {
2416
+ throw new Error("Creator identity required for Open Cloud upload. Set ROBLOX_CREATOR_USER_ID or ROBLOX_CREATOR_GROUP_ID, or pass userId/groupId as parameters.");
2417
+ }
2418
+ const creator = {};
2419
+ if (resolvedGroupId) {
2420
+ creator.groupId = resolvedGroupId;
2421
+ } else {
2422
+ creator.userId = resolvedUserId;
2423
+ }
2424
+ const result = await this.openCloudClient.createAsset({
2425
+ assetType,
2426
+ displayName,
2427
+ description: description || "",
2428
+ creationContext: { creator }
2429
+ }, fileContent, fileName);
2430
+ if (assetType === "Decal") {
2431
+ const decalId = result.response?.assetId;
2432
+ const imageId = decalId ? await this.resolveImageId(decalId) : null;
2433
+ return {
2434
+ content: [{
2435
+ type: "text",
2436
+ text: JSON.stringify({
2437
+ ...result,
2438
+ decalId: decalId ?? null,
2439
+ imageId
2440
+ })
2441
+ }]
2442
+ };
2443
+ }
2444
+ return {
2445
+ content: [{
2446
+ type: "text",
2447
+ text: JSON.stringify(result)
2448
+ }]
2449
+ };
2450
+ }
2451
+ async simulateMouseInput(action, x, y, button, scrollDirection, target) {
2452
+ if (!action) {
2453
+ throw new Error("action is required for simulate_mouse_input");
2454
+ }
2455
+ const response = await this.client.request("/api/simulate-mouse-input", {
2456
+ action,
2457
+ x,
2458
+ y,
2459
+ button,
2460
+ scrollDirection
2461
+ }, target || "edit");
2462
+ return {
2463
+ content: [{
2464
+ type: "text",
2465
+ text: JSON.stringify(response)
2466
+ }]
2467
+ };
2468
+ }
2469
+ async simulateKeyboardInput(keyCode, action, duration, target) {
2470
+ if (!keyCode) {
2471
+ throw new Error("keyCode is required for simulate_keyboard_input");
2472
+ }
2473
+ const response = await this.client.request("/api/simulate-keyboard-input", {
2474
+ keyCode,
2475
+ action,
2476
+ duration
2477
+ }, target || "edit");
2478
+ return {
2479
+ content: [{
2480
+ type: "text",
2481
+ text: JSON.stringify(response)
2482
+ }]
2483
+ };
2484
+ }
2485
+ async characterNavigation(position, instancePath, waitForCompletion, timeout, target) {
2486
+ if (!position && !instancePath) {
2487
+ throw new Error("Either position or instancePath is required for character_navigation");
2488
+ }
2489
+ const response = await this.client.request("/api/character-navigation", {
2490
+ position,
2491
+ instancePath,
2492
+ waitForCompletion,
2493
+ timeout
2494
+ }, target || "edit");
2495
+ return {
2496
+ content: [{
2497
+ type: "text",
2498
+ text: JSON.stringify(response)
2499
+ }]
2500
+ };
2501
+ }
2502
+ async cloneObject(instancePath, targetParentPath) {
2503
+ if (!instancePath || !targetParentPath) {
2504
+ throw new Error("instancePath and targetParentPath are required for clone_object");
2505
+ }
2506
+ const response = await this.client.request("/api/clone-object", { instancePath, targetParentPath });
2507
+ return { content: [{ type: "text", text: JSON.stringify(response) }] };
2508
+ }
2509
+ async getDescendants(instancePath, maxDepth, classFilter) {
2510
+ if (!instancePath) {
2511
+ throw new Error("instancePath is required for get_descendants");
2512
+ }
2513
+ const response = await this.client.request("/api/get-descendants", { instancePath, maxDepth, classFilter });
2514
+ return { content: [{ type: "text", text: JSON.stringify(response) }] };
2515
+ }
2516
+ async compareInstances(instancePathA, instancePathB) {
2517
+ if (!instancePathA || !instancePathB) {
2518
+ throw new Error("instancePathA and instancePathB are required for compare_instances");
2519
+ }
2520
+ const response = await this.client.request("/api/compare-instances", { instancePathA, instancePathB });
2521
+ return { content: [{ type: "text", text: JSON.stringify(response) }] };
2522
+ }
2523
+ async getOutputLog(maxEntries, messageType) {
2524
+ const response = await this.client.request("/api/get-output-log", { maxEntries, messageType });
2525
+ return { content: [{ type: "text", text: JSON.stringify(response) }] };
2526
+ }
2527
+ async bulkSetAttributes(instancePath, attributes) {
2528
+ if (!instancePath || !attributes) {
2529
+ throw new Error("instancePath and attributes are required for bulk_set_attributes");
2530
+ }
2531
+ const response = await this.client.request("/api/bulk-set-attributes", { instancePath, attributes });
2532
+ return { content: [{ type: "text", text: JSON.stringify(response) }] };
2533
+ }
2534
+ async findAndReplaceInScripts(pattern, replacement, options) {
2535
+ if (!pattern) {
2536
+ throw new Error("pattern is required for find_and_replace_in_scripts");
2537
+ }
2538
+ if (replacement === void 0 || replacement === null) {
2539
+ throw new Error("replacement is required for find_and_replace_in_scripts");
2540
+ }
2541
+ const response = await this.client.request("/api/find-and-replace-in-scripts", {
2542
+ pattern,
2543
+ replacement,
2544
+ ...options
2545
+ });
2546
+ return {
2547
+ content: [{
2548
+ type: "text",
2549
+ text: JSON.stringify(response)
2550
+ }]
2551
+ };
2552
+ }
2553
+ async captureScreenshot() {
2554
+ const response = await this.client.request("/api/capture-screenshot", {});
2555
+ if (response.error) {
2556
+ return {
2557
+ content: [{
2558
+ type: "text",
2559
+ text: response.error
2560
+ }]
2561
+ };
2562
+ }
2563
+ const pngBuffer = encodePngFromRgbaResponse(response);
2564
+ return {
2565
+ content: [{
2566
+ type: "image",
2567
+ data: pngBuffer.toString("base64"),
2568
+ mimeType: "image/png"
2569
+ }]
2570
+ };
2571
+ }
2572
+ };
2573
+
2574
+ // ../core/dist/bridge-service.js
2575
+ import { v4 as uuidv4 } from "uuid";
2576
+ var STALE_INSTANCE_MS = 3e4;
2577
+ var BridgeService = class {
2578
+ pendingRequests = /* @__PURE__ */ new Map();
2579
+ instances = /* @__PURE__ */ new Map();
2580
+ nextClientIndex = 1;
2581
+ requestTimeout = 3e4;
2582
+ registerInstance(instanceId, role) {
2583
+ let assignedRole = role;
2584
+ if (role === "client") {
2585
+ assignedRole = `client-${this.nextClientIndex}`;
2586
+ this.nextClientIndex++;
2587
+ }
2588
+ this.instances.set(instanceId, {
2589
+ instanceId,
2590
+ role: assignedRole,
2591
+ lastActivity: Date.now(),
2592
+ connectedAt: Date.now()
2593
+ });
2594
+ return assignedRole;
2595
+ }
2596
+ unregisterInstance(instanceId) {
2597
+ this.instances.delete(instanceId);
2598
+ for (const [id, req] of this.pendingRequests.entries()) {
2599
+ const targetRole = req.target;
2600
+ const hasHandler = Array.from(this.instances.values()).some((i) => i.role === targetRole);
2601
+ if (!hasHandler) {
2602
+ clearTimeout(req.timeoutId);
2603
+ this.pendingRequests.delete(id);
2604
+ req.reject(new Error(`Target instance "${targetRole}" disconnected`));
2605
+ }
2606
+ }
2607
+ }
2608
+ getInstances() {
2609
+ return Array.from(this.instances.values());
2610
+ }
2611
+ getPendingRequestCount() {
2612
+ return this.pendingRequests.size;
2613
+ }
2614
+ updateInstanceActivity(instanceId) {
2615
+ const inst = this.instances.get(instanceId);
2616
+ if (inst) {
2617
+ inst.lastActivity = Date.now();
2618
+ }
2619
+ }
2620
+ cleanupStaleInstances() {
2621
+ const now = Date.now();
2622
+ for (const [id, inst] of this.instances.entries()) {
2623
+ if (now - inst.lastActivity > STALE_INSTANCE_MS) {
2624
+ this.unregisterInstance(id);
2625
+ }
2626
+ }
2627
+ }
2628
+ async sendRequest(endpoint, data, target = "edit") {
2629
+ const requestId = uuidv4();
2630
+ return new Promise((resolve2, reject) => {
2631
+ const timeoutId = setTimeout(() => {
2632
+ if (this.pendingRequests.has(requestId)) {
2633
+ this.pendingRequests.delete(requestId);
2634
+ reject(new Error("Request timeout"));
2635
+ }
2636
+ }, this.requestTimeout);
2637
+ const request = {
2638
+ id: requestId,
2639
+ endpoint,
2640
+ data,
2641
+ target,
2642
+ timestamp: Date.now(),
2643
+ resolve: resolve2,
2644
+ reject,
2645
+ timeoutId
2646
+ };
2647
+ this.pendingRequests.set(requestId, request);
2648
+ });
2649
+ }
2650
+ getPendingRequest(callerRole = "edit") {
2651
+ let oldestRequest = null;
2652
+ for (const request of this.pendingRequests.values()) {
2653
+ if (request.target !== callerRole)
2654
+ continue;
2655
+ if (!oldestRequest || request.timestamp < oldestRequest.timestamp) {
2656
+ oldestRequest = request;
2657
+ }
2658
+ }
2659
+ if (oldestRequest) {
2660
+ return {
2661
+ requestId: oldestRequest.id,
2662
+ request: {
2663
+ endpoint: oldestRequest.endpoint,
2664
+ data: oldestRequest.data
2665
+ }
2666
+ };
2667
+ }
2668
+ return null;
2669
+ }
2670
+ resolveRequest(requestId, response) {
2671
+ const request = this.pendingRequests.get(requestId);
2672
+ if (request) {
2673
+ clearTimeout(request.timeoutId);
2674
+ this.pendingRequests.delete(requestId);
2675
+ request.resolve(response);
2676
+ }
2677
+ }
2678
+ rejectRequest(requestId, error) {
2679
+ const request = this.pendingRequests.get(requestId);
2680
+ if (request) {
2681
+ clearTimeout(request.timeoutId);
2682
+ this.pendingRequests.delete(requestId);
2683
+ request.reject(error);
2684
+ }
2685
+ }
2686
+ cleanupOldRequests() {
2687
+ const now = Date.now();
2688
+ for (const [id, request] of this.pendingRequests.entries()) {
2689
+ if (now - request.timestamp > this.requestTimeout) {
2690
+ clearTimeout(request.timeoutId);
2691
+ this.pendingRequests.delete(id);
2692
+ request.reject(new Error("Request timeout"));
2693
+ }
2694
+ }
2695
+ }
2696
+ clearAllPendingRequests() {
2697
+ for (const [, request] of this.pendingRequests.entries()) {
2698
+ clearTimeout(request.timeoutId);
2699
+ request.reject(new Error("Connection closed"));
2700
+ }
2701
+ this.pendingRequests.clear();
2702
+ }
2703
+ };
2704
+
2705
+ // ../core/dist/proxy-bridge-service.js
2706
+ import { v4 as uuidv42 } from "uuid";
2707
+ var ProxyBridgeService = class extends BridgeService {
2708
+ primaryBaseUrl;
2709
+ proxyInstanceId;
2710
+ proxyRequestTimeout = 3e4;
2711
+ constructor(primaryBaseUrl) {
2712
+ super();
2713
+ this.primaryBaseUrl = primaryBaseUrl;
2714
+ this.proxyInstanceId = uuidv42();
2715
+ }
2716
+ async sendRequest(endpoint, data, target = "edit") {
2717
+ const controller = new AbortController();
2718
+ const timeoutId = setTimeout(() => controller.abort(), this.proxyRequestTimeout);
2719
+ try {
2720
+ const response = await fetch(`${this.primaryBaseUrl}/proxy`, {
2721
+ method: "POST",
2722
+ headers: { "Content-Type": "application/json" },
2723
+ body: JSON.stringify({ endpoint, data, target, proxyInstanceId: this.proxyInstanceId }),
2724
+ signal: controller.signal
2725
+ });
2726
+ clearTimeout(timeoutId);
2727
+ if (!response.ok) {
2728
+ const body = await response.text();
2729
+ throw new Error(`Proxy request failed (${response.status}): ${body}`);
2730
+ }
2731
+ const result = await response.json();
2732
+ if (result.error) {
2733
+ throw new Error(result.error);
2734
+ }
2735
+ return result.response;
2736
+ } catch (err) {
2737
+ clearTimeout(timeoutId);
2738
+ if (err.name === "AbortError") {
2739
+ throw new Error("Proxy request timeout");
2740
+ }
2741
+ throw err;
2742
+ }
2743
+ }
2744
+ cleanupOldRequests() {
2745
+ }
2746
+ clearAllPendingRequests() {
2747
+ }
2748
+ };
2749
+
2750
+ // ../core/dist/server.js
2751
+ var RobloxStudioMCPServer = class {
2752
+ server;
2753
+ tools;
2754
+ bridge;
2755
+ allowedToolNames;
2756
+ config;
2757
+ constructor(config) {
2758
+ this.config = config;
2759
+ this.allowedToolNames = new Set(config.tools.map((t) => t.name));
2760
+ this.server = new Server2({
2761
+ name: config.name,
2762
+ version: config.version
2763
+ }, {
2764
+ capabilities: {
2765
+ tools: {}
2766
+ }
2767
+ });
2768
+ this.bridge = new BridgeService();
2769
+ this.tools = new RobloxStudioTools(this.bridge);
2770
+ this.setupToolHandlers();
2771
+ }
2772
+ setupToolHandlers() {
2773
+ this.server.setRequestHandler(ListToolsRequestSchema2, async () => {
2774
+ return {
2775
+ tools: this.config.tools.map((t) => ({
2776
+ name: t.name,
2777
+ description: t.description,
2778
+ inputSchema: t.inputSchema
2779
+ }))
2780
+ };
2781
+ });
2782
+ this.server.setRequestHandler(CallToolRequestSchema2, async (request) => {
2783
+ const { name, arguments: args } = request.params;
2784
+ if (!this.allowedToolNames.has(name)) {
2785
+ throw new McpError2(ErrorCode2.MethodNotFound, `Unknown tool: ${name}`);
2786
+ }
2787
+ const handler = TOOL_HANDLERS[name];
2788
+ if (!handler) {
2789
+ throw new McpError2(ErrorCode2.MethodNotFound, `Unknown tool: ${name}`);
2790
+ }
2791
+ try {
2792
+ return await handler(this.tools, args ?? {});
2793
+ } catch (error) {
2794
+ if (error instanceof McpError2)
2795
+ throw error;
2796
+ throw new McpError2(ErrorCode2.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
2797
+ }
2798
+ });
2799
+ }
2800
+ async run() {
2801
+ const basePort = process.env.ROBLOX_STUDIO_PORT ? parseInt(process.env.ROBLOX_STUDIO_PORT) : 58741;
2802
+ const host = process.env.ROBLOX_STUDIO_HOST || "0.0.0.0";
2803
+ let bridgeMode = "primary";
2804
+ let httpHandle;
2805
+ let primaryApp;
2806
+ let boundPort = 0;
2807
+ let promotionInterval;
2808
+ try {
2809
+ primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config);
2810
+ const result = await listenWithRetry(primaryApp, host, basePort, 5);
2811
+ httpHandle = result.server;
2812
+ boundPort = result.port;
2813
+ console.error(`HTTP server listening on ${host}:${boundPort} for Studio plugin (primary mode)`);
2814
+ console.error(`Streamable HTTP MCP endpoint: http://localhost:${boundPort}/mcp`);
2815
+ } catch {
2816
+ bridgeMode = "proxy";
2817
+ primaryApp = void 0;
2818
+ const proxyBridge = new ProxyBridgeService(`http://localhost:${basePort}`);
2819
+ this.bridge = proxyBridge;
2820
+ this.tools = new RobloxStudioTools(this.bridge);
2821
+ console.error(`All ports ${basePort}-${basePort + 4} in use - entering proxy mode (forwarding to localhost:${basePort})`);
2822
+ const promotionIntervalMs = parseInt(process.env.ROBLOX_STUDIO_PROXY_PROMOTION_INTERVAL_MS || "5000");
2823
+ promotionInterval = setInterval(async () => {
2824
+ try {
2825
+ this.bridge = new BridgeService();
2826
+ this.tools = new RobloxStudioTools(this.bridge);
2827
+ primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config);
2828
+ const result = await listenWithRetry(primaryApp, host, basePort, 5);
2829
+ httpHandle = result.server;
2830
+ boundPort = result.port;
2831
+ bridgeMode = "primary";
2832
+ primaryApp.setMCPServerActive(true);
2833
+ console.error(`Promoted from proxy to primary on port ${boundPort}`);
2834
+ if (promotionInterval)
2835
+ clearInterval(promotionInterval);
2836
+ } catch {
2837
+ this.bridge = new ProxyBridgeService(`http://localhost:${basePort}`);
2838
+ this.tools = new RobloxStudioTools(this.bridge);
2839
+ primaryApp = void 0;
2840
+ }
2841
+ }, promotionIntervalMs);
2842
+ }
2843
+ const LEGACY_PORT = 3002;
2844
+ let legacyHandle;
2845
+ let legacyApp;
2846
+ if (boundPort !== LEGACY_PORT && bridgeMode === "primary") {
2847
+ legacyApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config);
2848
+ try {
2849
+ const result = await listenWithRetry(legacyApp, host, LEGACY_PORT, 1);
2850
+ legacyHandle = result.server;
2851
+ console.error(`Legacy HTTP server also listening on ${host}:${LEGACY_PORT} for old plugins`);
2852
+ legacyApp.setMCPServerActive(true);
2853
+ } catch {
2854
+ console.error(`Legacy port ${LEGACY_PORT} in use, skipping backward-compat listener`);
2855
+ }
2856
+ }
2857
+ const transport = new StdioServerTransport();
2858
+ await this.server.connect(transport);
2859
+ console.error(`${this.config.name} v${this.config.version} running on stdio`);
2860
+ if (primaryApp) {
2861
+ primaryApp.setMCPServerActive(true);
2862
+ }
2863
+ console.error(bridgeMode === "primary" ? "MCP server marked as active (primary mode)" : "MCP server active in proxy mode - forwarding requests to primary");
2864
+ console.error("Waiting for Studio plugin to connect...");
2865
+ const activityInterval = setInterval(() => {
2866
+ if (primaryApp)
2867
+ primaryApp.trackMCPActivity();
2868
+ if (legacyApp)
2869
+ legacyApp.trackMCPActivity();
2870
+ if (bridgeMode === "primary" && primaryApp) {
2871
+ const pluginConnected = primaryApp.isPluginConnected();
2872
+ const mcpActive = primaryApp.isMCPServerActive();
2873
+ if (pluginConnected && mcpActive) {
2874
+ } else if (pluginConnected && !mcpActive) {
2875
+ console.error("Studio plugin connected, but MCP server inactive");
2876
+ } else if (!pluginConnected && mcpActive) {
2877
+ console.error("MCP server active, waiting for Studio plugin...");
2878
+ } else {
2879
+ console.error("Waiting for connections...");
2880
+ }
2881
+ }
2882
+ }, 5e3);
2883
+ const cleanupInterval = setInterval(() => {
2884
+ this.bridge.cleanupOldRequests();
2885
+ this.bridge.cleanupStaleInstances();
2886
+ }, 5e3);
2887
+ const shutdown = async () => {
2888
+ console.error("Shutting down MCP server...");
2889
+ clearInterval(activityInterval);
2890
+ clearInterval(cleanupInterval);
2891
+ if (promotionInterval)
2892
+ clearInterval(promotionInterval);
2893
+ await this.server.close().catch(() => {
2894
+ });
2895
+ if (httpHandle)
2896
+ httpHandle.close();
2897
+ if (legacyHandle)
2898
+ legacyHandle.close();
2899
+ process.exit(0);
2900
+ };
2901
+ process.on("SIGTERM", shutdown);
2902
+ process.on("SIGINT", shutdown);
2903
+ process.on("SIGHUP", shutdown);
2904
+ process.stdin.on("end", shutdown);
2905
+ process.stdin.on("close", shutdown);
2906
+ }
2907
+ };
2908
+
2909
+ // ../core/dist/tools/definitions.js
2910
+ var TOOL_DEFINITIONS = [
2911
+ // === File & Instance Browsing ===
2912
+ {
2913
+ name: "get_file_tree",
2914
+ category: "read",
2915
+ description: "Get instance hierarchy tree from Studio",
2916
+ inputSchema: {
2917
+ type: "object",
2918
+ properties: {
2919
+ path: {
2920
+ type: "string",
2921
+ description: "Root path (default: game root)"
2922
+ }
2923
+ }
2924
+ }
2925
+ },
2926
+ {
2927
+ name: "search_files",
2928
+ category: "read",
2929
+ description: "Search instances by name, class, or script content",
2930
+ inputSchema: {
2931
+ type: "object",
2932
+ properties: {
2933
+ query: {
2934
+ type: "string",
2935
+ description: "Name, class, or code pattern"
2936
+ },
2937
+ searchType: {
2938
+ type: "string",
2939
+ enum: ["name", "type", "content"],
2940
+ description: "Search mode (default: name)"
2941
+ }
2942
+ },
2943
+ required: ["query"]
2944
+ }
2945
+ },
2946
+ // === Place & Service Info ===
2947
+ {
2948
+ name: "get_place_info",
2949
+ category: "read",
2950
+ description: "Get place ID, name, and game settings",
2951
+ inputSchema: {
2952
+ type: "object",
2953
+ properties: {}
2954
+ }
2955
+ },
2956
+ {
2957
+ name: "get_services",
2958
+ category: "read",
2959
+ description: "Get available services and their children",
2960
+ inputSchema: {
2961
+ type: "object",
2962
+ properties: {
2963
+ serviceName: {
2964
+ type: "string",
2965
+ description: "Specific service name"
2966
+ }
2967
+ }
2968
+ }
2969
+ },
2970
+ {
2971
+ name: "search_objects",
2972
+ category: "read",
2973
+ description: "Find instances by name, class, or properties",
2974
+ inputSchema: {
2975
+ type: "object",
2976
+ properties: {
2977
+ query: {
2978
+ type: "string",
2979
+ description: "Search query"
2980
+ },
2981
+ searchType: {
2982
+ type: "string",
2983
+ enum: ["name", "class", "property"],
2984
+ description: "Search mode (default: name)"
2985
+ },
2986
+ propertyName: {
2987
+ type: "string",
2988
+ description: 'Property name when searchType is "property"'
2989
+ }
2990
+ },
2991
+ required: ["query"]
2992
+ }
2993
+ },
2994
+ // === Instance Inspection ===
2995
+ {
2996
+ name: "get_instance_properties",
2997
+ category: "read",
2998
+ description: "Get all properties of an instance",
2999
+ inputSchema: {
3000
+ type: "object",
3001
+ properties: {
3002
+ instancePath: {
3003
+ type: "string",
3004
+ description: "Instance path (dot notation)"
3005
+ },
3006
+ excludeSource: {
3007
+ type: "boolean",
3008
+ description: "For scripts, return SourceLength/LineCount instead of full source (default: false)"
3009
+ }
3010
+ },
3011
+ required: ["instancePath"]
3012
+ }
3013
+ },
3014
+ {
3015
+ name: "get_instance_children",
3016
+ category: "read",
3017
+ description: "Get children and their class types",
3018
+ inputSchema: {
3019
+ type: "object",
3020
+ properties: {
3021
+ instancePath: {
3022
+ type: "string",
3023
+ description: "Instance path (dot notation)"
3024
+ }
3025
+ },
3026
+ required: ["instancePath"]
3027
+ }
3028
+ },
3029
+ {
3030
+ name: "search_by_property",
3031
+ category: "read",
3032
+ description: "Find objects with specific property values",
3033
+ inputSchema: {
3034
+ type: "object",
3035
+ properties: {
3036
+ propertyName: {
3037
+ type: "string",
3038
+ description: "Property name"
3039
+ },
3040
+ propertyValue: {
3041
+ type: "string",
3042
+ description: "Value to match"
3043
+ }
3044
+ },
3045
+ required: ["propertyName", "propertyValue"]
3046
+ }
3047
+ },
3048
+ {
3049
+ name: "get_class_info",
3050
+ category: "read",
3051
+ description: "Get properties/methods for a class",
3052
+ inputSchema: {
3053
+ type: "object",
3054
+ properties: {
3055
+ className: {
3056
+ type: "string",
3057
+ description: "Roblox class name"
3058
+ }
3059
+ },
3060
+ required: ["className"]
3061
+ }
3062
+ },
3063
+ // === Project Structure ===
3064
+ {
3065
+ name: "get_project_structure",
3066
+ category: "read",
3067
+ description: "Get full game hierarchy tree. Increase maxDepth (default 3) for deeper traversal.",
3068
+ inputSchema: {
3069
+ type: "object",
3070
+ properties: {
3071
+ path: {
3072
+ type: "string",
3073
+ description: "Root path (default: workspace root)"
3074
+ },
3075
+ maxDepth: {
3076
+ type: "number",
3077
+ description: "Max traversal depth (default: 3)"
3078
+ },
3079
+ scriptsOnly: {
3080
+ type: "boolean",
3081
+ description: "Show only scripts (default: false)"
3082
+ }
3083
+ }
3084
+ }
3085
+ },
3086
+ // === Property Write ===
3087
+ {
3088
+ name: "set_property",
3089
+ category: "write",
3090
+ description: "Set a property on an instance",
3091
+ inputSchema: {
3092
+ type: "object",
3093
+ properties: {
3094
+ instancePath: {
3095
+ type: "string",
3096
+ description: "Instance path (dot notation)"
3097
+ },
3098
+ propertyName: {
3099
+ type: "string",
3100
+ description: "Property name"
3101
+ },
3102
+ propertyValue: {
3103
+ description: "Value to set (string, number, boolean, or object for Vector3/Color3/UDim2)"
3104
+ }
3105
+ },
3106
+ required: ["instancePath", "propertyName", "propertyValue"]
3107
+ }
3108
+ },
3109
+ {
3110
+ name: "mass_set_property",
3111
+ category: "write",
3112
+ description: "Set a property on multiple instances",
3113
+ inputSchema: {
3114
+ type: "object",
3115
+ properties: {
3116
+ paths: {
3117
+ type: "array",
3118
+ items: { type: "string" },
3119
+ description: "Instance paths"
3120
+ },
3121
+ propertyName: {
3122
+ type: "string",
3123
+ description: "Property name"
3124
+ },
3125
+ propertyValue: {
3126
+ description: "Value to set (string, number, boolean, or object for Vector3/Color3/UDim2)"
3127
+ }
3128
+ },
3129
+ required: ["paths", "propertyName", "propertyValue"]
3130
+ }
3131
+ },
3132
+ {
3133
+ name: "mass_get_property",
3134
+ category: "read",
3135
+ description: "Get a property from multiple instances",
3136
+ inputSchema: {
3137
+ type: "object",
3138
+ properties: {
3139
+ paths: {
3140
+ type: "array",
3141
+ items: { type: "string" },
3142
+ description: "Instance paths"
3143
+ },
3144
+ propertyName: {
3145
+ type: "string",
3146
+ description: "Property name"
3147
+ }
3148
+ },
3149
+ required: ["paths", "propertyName"]
3150
+ }
3151
+ },
3152
+ {
3153
+ name: "set_properties",
3154
+ category: "write",
3155
+ description: "Set multiple properties on a single instance in one call.",
3156
+ inputSchema: {
3157
+ type: "object",
3158
+ properties: {
3159
+ instancePath: {
3160
+ type: "string",
3161
+ description: "Instance path"
3162
+ },
3163
+ properties: {
3164
+ type: "object",
3165
+ description: "Map of property name to value"
3166
+ }
3167
+ },
3168
+ required: ["instancePath", "properties"]
3169
+ }
3170
+ },
3171
+ // === Object Creation/Deletion ===
3172
+ {
3173
+ name: "create_object",
3174
+ category: "write",
3175
+ description: "Create a new instance. Optionally set properties on creation.",
3176
+ inputSchema: {
3177
+ type: "object",
3178
+ properties: {
3179
+ className: {
3180
+ type: "string",
3181
+ description: "Roblox class name"
3182
+ },
3183
+ parent: {
3184
+ type: "string",
3185
+ description: "Parent instance path"
3186
+ },
3187
+ name: {
3188
+ type: "string",
3189
+ description: "Optional name"
3190
+ },
3191
+ properties: {
3192
+ type: "object",
3193
+ description: "Properties to set on creation"
3194
+ }
3195
+ },
3196
+ required: ["className", "parent"]
3197
+ }
3198
+ },
3199
+ {
3200
+ name: "mass_create_objects",
3201
+ category: "write",
3202
+ description: "Create multiple instances. Each can have optional properties.",
3203
+ inputSchema: {
3204
+ type: "object",
3205
+ properties: {
3206
+ objects: {
3207
+ type: "array",
3208
+ items: {
3209
+ type: "object",
3210
+ properties: {
3211
+ className: {
3212
+ type: "string",
3213
+ description: "Roblox class name"
3214
+ },
3215
+ parent: {
3216
+ type: "string",
3217
+ description: "Parent instance path"
3218
+ },
3219
+ name: {
3220
+ type: "string",
3221
+ description: "Optional name"
3222
+ },
3223
+ properties: {
3224
+ type: "object",
3225
+ description: "Properties to set on creation"
3226
+ }
3227
+ },
3228
+ required: ["className", "parent"]
3229
+ },
3230
+ description: "Objects to create"
3231
+ }
3232
+ },
3233
+ required: ["objects"]
3234
+ }
3235
+ },
3236
+ {
3237
+ name: "delete_object",
3238
+ category: "write",
3239
+ description: "Delete an instance",
3240
+ inputSchema: {
3241
+ type: "object",
3242
+ properties: {
3243
+ instancePath: {
3244
+ type: "string",
3245
+ description: "Instance path (dot notation)"
3246
+ }
3247
+ },
3248
+ required: ["instancePath"]
3249
+ }
3250
+ },
3251
+ // === Duplication ===
3252
+ {
3253
+ name: "smart_duplicate",
3254
+ category: "write",
3255
+ description: "Duplicate with naming, positioning, and property variations",
3256
+ inputSchema: {
3257
+ type: "object",
3258
+ properties: {
3259
+ instancePath: {
3260
+ type: "string",
3261
+ description: "Instance path (dot notation)"
3262
+ },
3263
+ count: {
3264
+ type: "number",
3265
+ description: "Number of duplicates"
3266
+ },
3267
+ options: {
3268
+ type: "object",
3269
+ properties: {
3270
+ namePattern: {
3271
+ type: "string",
3272
+ description: "Name pattern ({n} placeholder)"
3273
+ },
3274
+ positionOffset: {
3275
+ type: "array",
3276
+ items: { type: "number" },
3277
+ description: "X, Y, Z offset per duplicate"
3278
+ },
3279
+ rotationOffset: {
3280
+ type: "array",
3281
+ items: { type: "number" },
3282
+ description: "X, Y, Z rotation offset"
3283
+ },
3284
+ scaleOffset: {
3285
+ type: "array",
3286
+ items: { type: "number" },
3287
+ description: "X, Y, Z scale multiplier"
3288
+ },
3289
+ propertyVariations: {
3290
+ type: "object",
3291
+ description: "Property name to array of values"
3292
+ },
3293
+ targetParents: {
3294
+ type: "array",
3295
+ items: { type: "string" },
3296
+ description: "Different parent per duplicate"
3297
+ }
3298
+ }
3299
+ }
3300
+ },
3301
+ required: ["instancePath", "count"]
3302
+ }
3303
+ },
3304
+ {
3305
+ name: "mass_duplicate",
3306
+ category: "write",
3307
+ description: "Batch smart_duplicate operations",
3308
+ inputSchema: {
3309
+ type: "object",
3310
+ properties: {
3311
+ duplications: {
3312
+ type: "array",
3313
+ items: {
3314
+ type: "object",
3315
+ properties: {
3316
+ instancePath: {
3317
+ type: "string",
3318
+ description: "Instance path (dot notation)"
3319
+ },
3320
+ count: {
3321
+ type: "number",
3322
+ description: "Number of duplicates"
3323
+ },
3324
+ options: {
3325
+ type: "object",
3326
+ properties: {
3327
+ namePattern: {
3328
+ type: "string",
3329
+ description: "Name pattern ({n} placeholder)"
3330
+ },
3331
+ positionOffset: {
3332
+ type: "array",
3333
+ items: { type: "number" },
3334
+ description: "X, Y, Z offset per duplicate"
3335
+ },
3336
+ rotationOffset: {
3337
+ type: "array",
3338
+ items: { type: "number" },
3339
+ description: "X, Y, Z rotation offset"
3340
+ },
3341
+ scaleOffset: {
3342
+ type: "array",
3343
+ items: { type: "number" },
3344
+ description: "X, Y, Z scale multiplier"
3345
+ },
3346
+ propertyVariations: {
3347
+ type: "object",
3348
+ description: "Property name to array of values"
3349
+ },
3350
+ targetParents: {
3351
+ type: "array",
3352
+ items: { type: "string" },
3353
+ description: "Different parent per duplicate"
3354
+ }
3355
+ }
3356
+ }
3357
+ },
3358
+ required: ["instancePath", "count"]
3359
+ },
3360
+ description: "Duplication operations"
3361
+ }
3362
+ },
3363
+ required: ["duplications"]
3364
+ }
3365
+ },
3366
+ // === Calculated/Relative Properties ===
3367
+ // === Script Read/Write ===
3368
+ {
3369
+ name: "get_script_source",
3370
+ category: "read",
3371
+ description: 'Get script source. Returns "source" and "numberedSource" (line-numbered). Use startLine/endLine for large scripts.',
3372
+ inputSchema: {
3373
+ type: "object",
3374
+ properties: {
3375
+ instancePath: {
3376
+ type: "string",
3377
+ description: "Script instance path"
3378
+ },
3379
+ startLine: {
3380
+ type: "number",
3381
+ description: "Start line (1-indexed)"
3382
+ },
3383
+ endLine: {
3384
+ type: "number",
3385
+ description: "End line (inclusive)"
3386
+ }
3387
+ },
3388
+ required: ["instancePath"]
3389
+ }
3390
+ },
3391
+ {
3392
+ name: "set_script_source",
3393
+ category: "write",
3394
+ description: "Replace entire script source. For partial edits use edit/insert/delete_script_lines.",
3395
+ inputSchema: {
3396
+ type: "object",
3397
+ properties: {
3398
+ instancePath: {
3399
+ type: "string",
3400
+ description: "Script instance path"
3401
+ },
3402
+ source: {
3403
+ type: "string",
3404
+ description: "New source code"
3405
+ }
3406
+ },
3407
+ required: ["instancePath", "source"]
3408
+ }
3409
+ },
3410
+ {
3411
+ name: "edit_script_lines",
3412
+ category: "write",
3413
+ description: "Replace exact text in a script. Without startLine, old_string must match exactly once in the script. Pass startLine (1-indexed, from get_script_source) to anchor the edit to a specific line when old_string is ambiguous (e.g. repeated closing braces).",
3414
+ inputSchema: {
3415
+ type: "object",
3416
+ properties: {
3417
+ instancePath: {
3418
+ type: "string",
3419
+ description: "Script instance path"
3420
+ },
3421
+ old_string: {
3422
+ type: "string",
3423
+ description: "Exact text to find and replace. Must be unique in the script unless startLine is provided."
3424
+ },
3425
+ new_string: {
3426
+ type: "string",
3427
+ description: "Replacement text"
3428
+ },
3429
+ startLine: {
3430
+ type: "number",
3431
+ description: "Optional 1-indexed line where old_string begins. When provided, skips uniqueness check and requires old_string to match starting at that exact line."
3432
+ }
3433
+ },
3434
+ required: ["instancePath", "old_string", "new_string"]
3435
+ }
3436
+ },
3437
+ {
3438
+ name: "insert_script_lines",
3439
+ category: "write",
3440
+ description: "Insert lines after a given line number (0 = beginning).",
3441
+ inputSchema: {
3442
+ type: "object",
3443
+ properties: {
3444
+ instancePath: {
3445
+ type: "string",
3446
+ description: "Script instance path"
3447
+ },
3448
+ afterLine: {
3449
+ type: "number",
3450
+ description: "Insert after this line (0 = beginning)"
3451
+ },
3452
+ newContent: {
3453
+ type: "string",
3454
+ description: "Content to insert"
3455
+ }
3456
+ },
3457
+ required: ["instancePath", "newContent"]
3458
+ }
3459
+ },
3460
+ {
3461
+ name: "delete_script_lines",
3462
+ category: "write",
3463
+ description: "Delete a range of lines. 1-indexed, inclusive.",
3464
+ inputSchema: {
3465
+ type: "object",
3466
+ properties: {
3467
+ instancePath: {
3468
+ type: "string",
3469
+ description: "Script instance path"
3470
+ },
3471
+ startLine: {
3472
+ type: "number",
3473
+ description: "Start line (1-indexed)"
3474
+ },
3475
+ endLine: {
3476
+ type: "number",
3477
+ description: "End line (inclusive)"
3478
+ }
3479
+ },
3480
+ required: ["instancePath", "startLine", "endLine"]
3481
+ }
3482
+ },
3483
+ // === Attributes ===
3484
+ {
3485
+ name: "set_attribute",
3486
+ category: "write",
3487
+ description: "Set an attribute. Supports primitives, Vector3, Color3, UDim2, BrickColor.",
3488
+ inputSchema: {
3489
+ type: "object",
3490
+ properties: {
3491
+ instancePath: {
3492
+ type: "string",
3493
+ description: "Instance path (dot notation)"
3494
+ },
3495
+ attributeName: {
3496
+ type: "string",
3497
+ description: "Attribute name"
3498
+ },
3499
+ attributeValue: {
3500
+ description: "Value (string, number, boolean, or object for Vector3/Color3/UDim2)"
3501
+ },
3502
+ valueType: {
3503
+ type: "string",
3504
+ description: "Type hint if needed"
3505
+ }
3506
+ },
3507
+ required: ["instancePath", "attributeName", "attributeValue"]
3508
+ }
3509
+ },
3510
+ {
3511
+ name: "get_attributes",
3512
+ category: "read",
3513
+ description: "Get all attributes on an instance",
3514
+ inputSchema: {
3515
+ type: "object",
3516
+ properties: {
3517
+ instancePath: {
3518
+ type: "string",
3519
+ description: "Instance path (dot notation)"
3520
+ }
3521
+ },
3522
+ required: ["instancePath"]
3523
+ }
3524
+ },
3525
+ {
3526
+ name: "delete_attribute",
3527
+ category: "write",
3528
+ description: "Delete an attribute",
3529
+ inputSchema: {
3530
+ type: "object",
3531
+ properties: {
3532
+ instancePath: {
3533
+ type: "string",
3534
+ description: "Instance path (dot notation)"
3535
+ },
3536
+ attributeName: {
3537
+ type: "string",
3538
+ description: "Attribute name"
3539
+ }
3540
+ },
3541
+ required: ["instancePath", "attributeName"]
3542
+ }
3543
+ },
3544
+ // === Tags ===
3545
+ {
3546
+ name: "get_tags",
3547
+ category: "read",
3548
+ description: "Get all tags on an instance",
3549
+ inputSchema: {
3550
+ type: "object",
3551
+ properties: {
3552
+ instancePath: {
3553
+ type: "string",
3554
+ description: "Instance path (dot notation)"
3555
+ }
3556
+ },
3557
+ required: ["instancePath"]
3558
+ }
3559
+ },
3560
+ {
3561
+ name: "add_tag",
3562
+ category: "write",
3563
+ description: "Add a tag",
3564
+ inputSchema: {
3565
+ type: "object",
3566
+ properties: {
3567
+ instancePath: {
3568
+ type: "string",
3569
+ description: "Instance path (dot notation)"
3570
+ },
3571
+ tagName: {
3572
+ type: "string",
3573
+ description: "Tag name"
3574
+ }
3575
+ },
3576
+ required: ["instancePath", "tagName"]
3577
+ }
3578
+ },
3579
+ {
3580
+ name: "remove_tag",
3581
+ category: "write",
3582
+ description: "Remove a tag",
3583
+ inputSchema: {
3584
+ type: "object",
3585
+ properties: {
3586
+ instancePath: {
3587
+ type: "string",
3588
+ description: "Instance path (dot notation)"
3589
+ },
3590
+ tagName: {
3591
+ type: "string",
3592
+ description: "Tag name"
3593
+ }
3594
+ },
3595
+ required: ["instancePath", "tagName"]
3596
+ }
3597
+ },
3598
+ {
3599
+ name: "get_tagged",
3600
+ category: "read",
3601
+ description: "Get all instances with a specific tag",
3602
+ inputSchema: {
3603
+ type: "object",
3604
+ properties: {
3605
+ tagName: {
3606
+ type: "string",
3607
+ description: "Tag name"
3608
+ }
3609
+ },
3610
+ required: ["tagName"]
3611
+ }
3612
+ },
3613
+ // === Selection ===
3614
+ {
3615
+ name: "get_selection",
3616
+ category: "read",
3617
+ description: "Get all currently selected objects",
3618
+ inputSchema: {
3619
+ type: "object",
3620
+ properties: {}
3621
+ }
3622
+ },
3623
+ // === Luau Execution ===
3624
+ {
3625
+ name: "execute_luau",
3626
+ category: "write",
3627
+ description: "Execute Luau code in plugin context. Use print()/warn() for output. Return value is captured.",
3628
+ inputSchema: {
3629
+ type: "object",
3630
+ properties: {
3631
+ code: {
3632
+ type: "string",
3633
+ description: "Luau code to execute"
3634
+ },
3635
+ target: {
3636
+ type: "string",
3637
+ description: 'Instance target: "edit" (default), "server", "client-1", "client-2", etc.'
3638
+ }
3639
+ },
3640
+ required: ["code"]
3641
+ }
3642
+ },
3643
+ // === Script Search ===
3644
+ {
3645
+ name: "grep_scripts",
3646
+ category: "read",
3647
+ description: "Ripgrep-inspired search across all script sources. Supports literal and Lua pattern matching, context lines, early termination, and results grouped by script with line/column numbers.",
3648
+ inputSchema: {
3649
+ type: "object",
3650
+ properties: {
3651
+ pattern: {
3652
+ type: "string",
3653
+ description: "Search pattern (literal string or Lua pattern)"
3654
+ },
3655
+ caseSensitive: {
3656
+ type: "boolean",
3657
+ description: "Case-sensitive search (default: false)"
3658
+ },
3659
+ usePattern: {
3660
+ type: "boolean",
3661
+ description: "Use Lua pattern matching instead of literal (default: false)"
3662
+ },
3663
+ contextLines: {
3664
+ type: "number",
3665
+ description: "Number of context lines before/after each match (default: 0)"
3666
+ },
3667
+ maxResults: {
3668
+ type: "number",
3669
+ description: "Max total matches before stopping (default: 100)"
3670
+ },
3671
+ maxResultsPerScript: {
3672
+ type: "number",
3673
+ description: "Max matches per script (like rg -m)"
3674
+ },
3675
+ filesOnly: {
3676
+ type: "boolean",
3677
+ description: "Only return matching script paths, not line details (default: false)"
3678
+ },
3679
+ path: {
3680
+ type: "string",
3681
+ description: 'Subtree to search (e.g. "game.ServerScriptService")'
3682
+ },
3683
+ classFilter: {
3684
+ type: "string",
3685
+ enum: ["Script", "LocalScript", "ModuleScript"],
3686
+ description: "Only search scripts of this class type"
3687
+ }
3688
+ },
3689
+ required: ["pattern"]
3690
+ }
3691
+ },
3692
+ // === Playtest ===
3693
+ {
3694
+ name: "start_playtest",
3695
+ category: "write",
3696
+ description: "Start playtest. Captures print/warn/error via LogService. Poll with get_playtest_output, end with stop_playtest. Use numPlayers for multi-client testing (server + N clients).",
3697
+ inputSchema: {
3698
+ type: "object",
3699
+ properties: {
3700
+ mode: {
3701
+ type: "string",
3702
+ enum: ["play", "run"],
3703
+ description: "Play mode"
3704
+ },
3705
+ numPlayers: {
3706
+ type: "number",
3707
+ description: "Number of client players (1-8). Triggers server + clients mode via TestService."
3708
+ }
3709
+ },
3710
+ required: ["mode"]
3711
+ }
3712
+ },
3713
+ {
3714
+ name: "stop_playtest",
3715
+ category: "write",
3716
+ description: "Stop playtest and return all captured output.",
3717
+ inputSchema: {
3718
+ type: "object",
3719
+ properties: {}
3720
+ }
3721
+ },
3722
+ {
3723
+ name: "get_playtest_output",
3724
+ category: "read",
3725
+ description: "Poll output buffer without stopping. Returns isRunning and captured messages.",
3726
+ inputSchema: {
3727
+ type: "object",
3728
+ properties: {
3729
+ target: {
3730
+ type: "string",
3731
+ description: 'Instance target: "edit" (default), "server", "client-1", "client-2", etc.'
3732
+ }
3733
+ }
3734
+ }
3735
+ },
3736
+ // === Multi-Instance ===
3737
+ {
3738
+ name: "get_connected_instances",
3739
+ category: "read",
3740
+ description: "List all connected plugin instances with their roles. Use during multi-client playtest to discover server and client instances for targeted commands.",
3741
+ inputSchema: {
3742
+ type: "object",
3743
+ properties: {}
3744
+ }
3745
+ },
3746
+ // === Undo/Redo ===
3747
+ {
3748
+ name: "undo",
3749
+ category: "write",
3750
+ description: "Undo the last change in Roblox Studio. Uses ChangeHistoryService to reverse the most recent operation.",
3751
+ inputSchema: {
3752
+ type: "object",
3753
+ properties: {}
3754
+ }
3755
+ },
3756
+ {
3757
+ name: "redo",
3758
+ category: "write",
3759
+ description: "Redo the last undone change in Roblox Studio. Uses ChangeHistoryService to reapply the most recently undone operation.",
3760
+ inputSchema: {
3761
+ type: "object",
3762
+ properties: {}
3763
+ }
3764
+ },
3765
+ // === Build Library ===
3766
+ {
3767
+ name: "export_build",
3768
+ category: "read",
3769
+ description: "Export a Model/Folder into a compact, token-efficient build JSON format and auto-save it to the local build library. The output contains a palette (unique BrickColor+Material combos mapped to short keys) and compact part arrays with positions normalized relative to the bounding box center. The file is saved to build-library/{style}/{id}.json automatically.",
3770
+ inputSchema: {
3771
+ type: "object",
3772
+ properties: {
3773
+ instancePath: {
3774
+ type: "string",
3775
+ description: "Path to the Model or Folder to export (dot notation)"
3776
+ },
3777
+ outputId: {
3778
+ type: "string",
3779
+ description: 'Build ID for the output (e.g. "medieval/cottage_01"). Defaults to style/instance_name.'
3780
+ },
3781
+ style: {
3782
+ type: "string",
3783
+ enum: ["medieval", "modern", "nature", "scifi", "misc"],
3784
+ description: "Style category for the build (default: misc)"
3785
+ }
3786
+ },
3787
+ required: ["instancePath"]
3788
+ }
3789
+ },
3790
+ {
3791
+ name: "create_build",
3792
+ category: "write",
3793
+ description: "Create a new build model from scratch and save it to the library. Define parts using compact arrays [posX, posY, posZ, sizeX, sizeY, sizeZ, rotX, rotY, rotZ, paletteKey, shape?, transparency?]. Palette maps short keys to [BrickColor, Material] pairs. The build is saved and can be referenced by import_build or import_scene.",
3794
+ inputSchema: {
3795
+ type: "object",
3796
+ properties: {
3797
+ id: {
3798
+ type: "string",
3799
+ description: 'Build ID including style prefix (e.g. "medieval/torch_01", "nature/bush_small")'
3800
+ },
3801
+ style: {
3802
+ type: "string",
3803
+ enum: ["medieval", "modern", "nature", "scifi", "misc"],
3804
+ description: "Style category"
3805
+ },
3806
+ palette: {
3807
+ type: "object",
3808
+ description: 'Map of short keys to [BrickColor, Material] or [BrickColor, Material, MaterialVariant] tuples. E.g. {"a": ["Dark stone grey", "Concrete"], "b": ["Brown", "Wood", "MyCustomWood"]}'
3809
+ },
3810
+ parts: {
3811
+ type: "array",
3812
+ description: "Array of parts. Object format: {position:[x,y,z], size:[x,y,z], rotation:[x,y,z], paletteKey, shape?, transparency?}. Tuple format [posX,posY,posZ,sizeX,sizeY,sizeZ,rotX,rotY,rotZ,paletteKey,shape?,transparency?] also accepted.",
3813
+ items: {
3814
+ anyOf: [
3815
+ {
3816
+ type: "object",
3817
+ additionalProperties: false,
3818
+ required: ["position", "size", "rotation", "paletteKey"],
3819
+ properties: {
3820
+ position: { type: "array", items: { type: "number" }, minItems: 3, maxItems: 3 },
3821
+ size: { type: "array", items: { type: "number" }, minItems: 3, maxItems: 3 },
3822
+ rotation: { type: "array", items: { type: "number" }, minItems: 3, maxItems: 3 },
3823
+ paletteKey: { type: "string", minLength: 1 },
3824
+ shape: { type: "string", enum: ["Block", "Wedge", "Cylinder", "Ball", "CornerWedge"] },
3825
+ transparency: { type: "number", minimum: 0, maximum: 1 }
3826
+ }
3827
+ },
3828
+ {
3829
+ type: "array",
3830
+ minItems: 10,
3831
+ items: { anyOf: [{ type: "number" }, { type: "string" }] }
3832
+ }
3833
+ ]
3834
+ }
3835
+ },
3836
+ bounds: {
3837
+ type: "array",
3838
+ items: { type: "number" },
3839
+ description: "Optional bounding box [X, Y, Z]. Auto-computed if omitted."
3840
+ }
3841
+ },
3842
+ required: ["id", "style", "palette", "parts"]
3843
+ }
3844
+ },
3845
+ {
3846
+ name: "generate_build",
3847
+ category: "write",
3848
+ description: `Procedurally generate a build via JS code. ALWAYS generate the entire scene in ONE call - never split into multiple small builds. PREFER high-level primitives over manual loops. No comments. No unnecessary variables. Maximize build detail per line.
3849
+
3850
+ EDITING: When modifying an existing build, call get_build first to retrieve the original code. Then make ONLY the targeted changes the user requested - do not rewrite unchanged code. Pass the modified code to generate_build.
3851
+
3852
+ HIGH-LEVEL (use these first - each replaces 5-20 lines):
3853
+ room(x,y,z, w,h,d, wallKey, floorKey?, ceilKey?, wallThickness?) - Complete enclosed room (floor+ceiling+4 walls)
3854
+ roof(x,y,z, w,d, style, key, overhang?) - style: "flat"|"gable"|"hip"
3855
+ stairs(x1,y1,z1, x2,y2,z2, width, key) - Auto-generates steps between two points
3856
+ column(x,y,z, height, radius, key, capKey?) - Cylinder with base+capital
3857
+ pew(x,y,z, w,d, seatKey, legKey?) - Bench with seat+backrest+legs
3858
+ arch(x,y,z, w,h, thickness, key, segments?) - Curved archway
3859
+ fence(x1,z1, x2,z2, y, key, postSpacing?) - Fence with posts+rails
3860
+
3861
+ BASIC:
3862
+ part(x,y,z, sx,sy,sz, key, shape?, transparency?)
3863
+ rpart(x,y,z, sx,sy,sz, rx,ry,rz, key, shape?, transparency?)
3864
+ wall(x1,z1, x2,z2, height, thickness, key) - vertical plane from (x1,z1) to (x2,z2)
3865
+ floor(x1,z1, x2,z2, y, thickness, key) - horizontal plane at height y, corners (x1,z1)-(x2,z2). NOT fill - only takes 2D corners+y, not 3D points
3866
+ fill(x1,y1,z1, x2,y2,z2, key, [ux,uy,uz]?) - 3D volume between two 3D points
3867
+ beam(x1,y1,z1, x2,y2,z2, thickness, key)
3868
+
3869
+ IMPORTANT: Palette keys must match exactly. Use only keys defined in your palette object, not color names.
3870
+ CUSTOM MATERIALS: Use search_materials to find MaterialVariant names, then reference them as the 3rd palette element: {"a": ["Color", "BaseMaterial", "VariantName"]}.
3871
+
3872
+ REPETITION:
3873
+ row(x,y,z, count, spacingX, spacingZ, fn(i,cx,cy,cz))
3874
+ grid(x,y,z, countX, countZ, spacingX, spacingZ, fn(ix,iz,cx,cy,cz))
3875
+
3876
+ Shapes: Block(default), Wedge, Cylinder, Ball, CornerWedge. Max 10000 parts. Math and rng() available.
3877
+ CYLINDER AXIS: Roblox cylinders extend along the X axis. For upright cylinders, use size (height, diameter, diameter) with rz=90. The column() primitive handles this automatically.
3878
+
3879
+ EXAMPLE - compact cabin (17 lines):
3880
+ room(0,0,0,8,4,6,"a","b","a")
3881
+ roof(0,4,0,8,6,"gable","c")
3882
+ wall(-4,0,-2,4,0,-2,4,1,"a")
3883
+ part(0,2,3,3,3,0.3,"a","Block",0.4)
3884
+ row(-2,0,-1,3,0,2,(i,cx,cy,cz)=>{pew(cx,0,cz,3,2,"d")})
3885
+ column(-3,0,-2,4,0.5,"a","b")
3886
+ column(3,0,-2,4,0.5,"a","b")
3887
+ part(0,2,0,2,1,1,"b")`,
3888
+ inputSchema: {
3889
+ type: "object",
3890
+ properties: {
3891
+ id: {
3892
+ type: "string",
3893
+ description: 'Build ID including style prefix (e.g. "medieval/church_01")'
3894
+ },
3895
+ style: {
3896
+ type: "string",
3897
+ enum: ["medieval", "modern", "nature", "scifi", "misc"],
3898
+ description: "Style category"
3899
+ },
3900
+ palette: {
3901
+ type: "object",
3902
+ description: 'Map of short keys to [BrickColor, Material] or [BrickColor, Material, MaterialVariant] tuples. E.g. {"a": ["Dark stone grey", "Cobblestone"], "b": ["Brown", "WoodPlanks", "MyCustomWood"]}. MaterialVariant is optional - use it to reference custom materials from MaterialService.'
3903
+ },
3904
+ code: {
3905
+ type: "string",
3906
+ description: "JavaScript code using the primitives above to generate parts procedurally"
3907
+ },
3908
+ seed: {
3909
+ type: "number",
3910
+ description: "Optional seed for deterministic rng() output (default: 42)"
3911
+ }
3912
+ },
3913
+ required: ["id", "style", "palette", "code"]
3914
+ }
3915
+ },
3916
+ {
3917
+ name: "import_build",
3918
+ category: "write",
3919
+ description: 'Import a build into Roblox Studio. Accepts either a full build data object OR a library ID string (e.g. "medieval/church_01") to load from the build library. When using generate_build or create_build, pass the build ID string instead of the full data.',
3920
+ inputSchema: {
3921
+ type: "object",
3922
+ properties: {
3923
+ buildData: {
3924
+ description: 'Either a build data object (with palette, parts, etc.) OR a library ID string (e.g. "medieval/church_01") to load from the build library'
3925
+ },
3926
+ targetPath: {
3927
+ type: "string",
3928
+ description: "Parent instance path where the model will be created"
3929
+ },
3930
+ position: {
3931
+ type: "array",
3932
+ items: { type: "number" },
3933
+ description: "World position offset [X, Y, Z]"
3934
+ }
3935
+ },
3936
+ required: ["buildData", "targetPath"]
3937
+ }
3938
+ },
3939
+ {
3940
+ name: "list_library",
3941
+ category: "read",
3942
+ description: "List available builds in the local build library. Returns build IDs, styles, bounds, and part counts. Optionally filter by style.",
3943
+ inputSchema: {
3944
+ type: "object",
3945
+ properties: {
3946
+ style: {
3947
+ type: "string",
3948
+ enum: ["medieval", "modern", "nature", "scifi", "misc"],
3949
+ description: "Filter by style category"
3950
+ }
3951
+ }
3952
+ }
3953
+ },
3954
+ {
3955
+ name: "search_materials",
3956
+ category: "read",
3957
+ description: "Search for MaterialVariant instances in MaterialService by name. Use this to find custom materials before using them in generate_build or create_build palettes. Returns material names and their base material types.",
3958
+ inputSchema: {
3959
+ type: "object",
3960
+ properties: {
3961
+ query: {
3962
+ type: "string",
3963
+ description: "Search query to match against material names (case-insensitive). Leave empty to list all."
3964
+ },
3965
+ maxResults: {
3966
+ type: "number",
3967
+ description: "Max results to return (default: 50)"
3968
+ }
3969
+ }
3970
+ }
3971
+ },
3972
+ {
3973
+ name: "get_build",
3974
+ category: "read",
3975
+ description: "Get a build from the library by ID. Returns metadata, palette, and generator code (if the build was created with generate_build). IMPORTANT: When the user asks to modify an existing build, ALWAYS call get_build first to retrieve the original code, then make targeted edits to only the relevant lines, and call generate_build with the modified code. Never rewrite the entire code from scratch - only change what the user asked to change.",
3976
+ inputSchema: {
3977
+ type: "object",
3978
+ properties: {
3979
+ id: {
3980
+ type: "string",
3981
+ description: 'Build ID (e.g. "medieval/church_01")'
3982
+ }
3983
+ },
3984
+ required: ["id"]
3985
+ }
3986
+ },
3987
+ {
3988
+ name: "import_scene",
3989
+ category: "write",
3990
+ description: "Import a full scene layout. Provide a scene with model references (resolved from library) and placement data. Each model is placed at the specified position/rotation. Can also include inline custom builds.",
3991
+ inputSchema: {
3992
+ type: "object",
3993
+ properties: {
3994
+ sceneData: {
3995
+ type: "object",
3996
+ description: "Scene layout object with: models (map of key to library build ID), place (array of [key, position, rotation?]), and optional custom (array of inline build objects with name, position, palette, parts)",
3997
+ properties: {
3998
+ models: {
3999
+ type: "object",
4000
+ description: 'Map of short keys to library build IDs (e.g. {"A": "medieval/cottage_01"})'
4001
+ },
4002
+ place: {
4003
+ type: "array",
4004
+ 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.",
4005
+ items: {
4006
+ anyOf: [
4007
+ {
4008
+ type: "object",
4009
+ additionalProperties: false,
4010
+ required: ["modelKey", "position"],
4011
+ properties: {
4012
+ modelKey: {
4013
+ type: "string"
4014
+ },
4015
+ position: {
4016
+ type: "array",
4017
+ items: { type: "number" }
4018
+ },
4019
+ rotation: {
4020
+ type: "array",
4021
+ items: { type: "number" }
4022
+ }
4023
+ }
4024
+ },
4025
+ {
4026
+ type: "array",
4027
+ items: {
4028
+ anyOf: [
4029
+ {
4030
+ type: "string"
4031
+ },
4032
+ {
4033
+ type: "array",
4034
+ items: { type: "number" }
4035
+ }
4036
+ ]
4037
+ }
4038
+ }
4039
+ ]
4040
+ }
4041
+ },
4042
+ custom: {
4043
+ type: "array",
4044
+ description: "Array of inline custom builds with {n: name, o: [x,y,z], palette: {...}, parts: [...]}",
4045
+ items: { type: "object" }
4046
+ }
4047
+ }
4048
+ },
4049
+ targetPath: {
4050
+ type: "string",
4051
+ description: "Parent instance path for the scene (default: game.Workspace)"
4052
+ }
4053
+ },
4054
+ required: ["sceneData"]
4055
+ }
4056
+ },
4057
+ // === Asset Tools ===
4058
+ {
4059
+ name: "search_assets",
4060
+ category: "read",
4061
+ description: "Search the Creator Store (Roblox marketplace) for assets by type and keywords. Requires ROBLOX_OPEN_CLOUD_API_KEY env var (no cookie auth for this endpoint).",
4062
+ inputSchema: {
4063
+ type: "object",
4064
+ properties: {
4065
+ assetType: {
4066
+ type: "string",
4067
+ enum: ["Audio", "Model", "Decal", "Plugin", "MeshPart", "Video", "FontFamily"],
4068
+ description: "Type of asset to search for"
4069
+ },
4070
+ query: {
4071
+ type: "string",
4072
+ description: "Search keywords"
4073
+ },
4074
+ maxResults: {
4075
+ type: "number",
4076
+ description: "Max results to return (default: 25)"
4077
+ },
4078
+ sortBy: {
4079
+ type: "string",
4080
+ enum: ["Relevance", "Trending", "Top", "AudioDuration", "CreateTime", "UpdatedTime", "Ratings"],
4081
+ description: "Sort order (default: Relevance)"
4082
+ },
4083
+ verifiedCreatorsOnly: {
4084
+ type: "boolean",
4085
+ description: "Only show assets from verified creators (default: false)"
4086
+ }
4087
+ },
4088
+ required: ["assetType"]
4089
+ }
4090
+ },
4091
+ {
4092
+ name: "get_asset_details",
4093
+ category: "read",
4094
+ description: "Get detailed marketplace metadata for a specific asset. Uses ROBLOX_OPEN_CLOUD_API_KEY or falls back to ROBLOSECURITY cookie (own assets only).",
4095
+ inputSchema: {
4096
+ type: "object",
4097
+ properties: {
4098
+ assetId: {
4099
+ type: "number",
4100
+ description: "The Roblox asset ID"
4101
+ }
4102
+ },
4103
+ required: ["assetId"]
4104
+ }
4105
+ },
4106
+ {
4107
+ name: "get_asset_thumbnail",
4108
+ category: "read",
4109
+ description: "Get the thumbnail image for an asset as base64 PNG, suitable for vision LLMs. Thumbnails API is public but asset validation uses ROBLOX_OPEN_CLOUD_API_KEY.",
4110
+ inputSchema: {
4111
+ type: "object",
4112
+ properties: {
4113
+ assetId: {
4114
+ type: "number",
4115
+ description: "The Roblox asset ID"
4116
+ },
4117
+ size: {
4118
+ type: "string",
4119
+ enum: ["150x150", "420x420", "768x432"],
4120
+ description: "Thumbnail size (default: 420x420)"
4121
+ }
4122
+ },
4123
+ required: ["assetId"]
4124
+ }
4125
+ },
4126
+ {
4127
+ name: "insert_asset",
4128
+ category: "write",
4129
+ description: "Insert a Roblox asset into Studio by loading it via AssetService and parenting it to a target location. Optionally set position.",
4130
+ inputSchema: {
4131
+ type: "object",
4132
+ properties: {
4133
+ assetId: {
4134
+ type: "number",
4135
+ description: "The Roblox asset ID to insert"
4136
+ },
4137
+ parentPath: {
4138
+ type: "string",
4139
+ description: "Parent instance path (default: game.Workspace)"
4140
+ },
4141
+ position: {
4142
+ type: "object",
4143
+ properties: {
4144
+ x: { type: "number" },
4145
+ y: { type: "number" },
4146
+ z: { type: "number" }
4147
+ },
4148
+ description: "Optional world position to place the asset"
4149
+ }
4150
+ },
4151
+ required: ["assetId"]
4152
+ }
4153
+ },
4154
+ {
4155
+ name: "preview_asset",
4156
+ category: "read",
4157
+ description: "Preview a Roblox asset without permanently inserting it. Loads the asset, builds a hierarchy tree with properties and summary stats, then destroys it. Useful for inspecting asset contents before insertion.",
4158
+ inputSchema: {
4159
+ type: "object",
4160
+ properties: {
4161
+ assetId: {
4162
+ type: "number",
4163
+ description: "The Roblox asset ID to preview"
4164
+ },
4165
+ includeProperties: {
4166
+ type: "boolean",
4167
+ description: "Include detailed properties for each instance (default: true)"
4168
+ },
4169
+ maxDepth: {
4170
+ type: "number",
4171
+ description: "Max hierarchy traversal depth (default: 10)"
4172
+ }
4173
+ },
4174
+ required: ["assetId"]
4175
+ }
4176
+ },
4177
+ {
4178
+ name: "upload_asset",
4179
+ category: "write",
4180
+ description: "Upload any supported asset type to Roblox: Audio (mp3/ogg/wav/flac), Decal (png/jpg/bmp/tga), Model (fbx/gltf/glb/rbxm/rbxmx), Animation (rbxm/rbxmx), or Video (mp4/mov). Decal supports ROBLOSECURITY cookie auth or ROBLOX_OPEN_CLOUD_API_KEY. All other types require Open Cloud API key with asset:write scope + creator ID. Audio: max 7 min, 100 uploads/month (ID-verified). Video: max 5 min, requires 13+ ID-verified.",
4181
+ inputSchema: {
4182
+ type: "object",
4183
+ properties: {
4184
+ filePath: {
4185
+ type: "string",
4186
+ description: "Absolute path to the file on disk"
4187
+ },
4188
+ assetType: {
4189
+ type: "string",
4190
+ enum: ["Audio", "Decal", "Model", "Animation", "Video"],
4191
+ description: "Type of asset to upload. Must match the file format."
4192
+ },
4193
+ displayName: {
4194
+ type: "string",
4195
+ description: "Display name for the asset (max 50 characters)"
4196
+ },
4197
+ description: {
4198
+ type: "string",
4199
+ description: "Description for the asset (default: empty string)"
4200
+ },
4201
+ userId: {
4202
+ type: "string",
4203
+ description: "Roblox user ID for the asset creator. Overrides ROBLOX_CREATOR_USER_ID env var."
4204
+ },
4205
+ groupId: {
4206
+ type: "string",
4207
+ description: "Roblox group ID for the asset creator. Overrides ROBLOX_CREATOR_GROUP_ID env var. Takes precedence over userId if both provided."
4208
+ }
4209
+ },
4210
+ required: ["filePath", "assetType", "displayName"]
4211
+ }
4212
+ },
4213
+ {
4214
+ name: "capture_screenshot",
4215
+ category: "read",
4216
+ 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.',
4217
+ inputSchema: {
4218
+ type: "object",
4219
+ properties: {}
4220
+ }
4221
+ },
4222
+ // === Input Simulation ===
4223
+ {
4224
+ name: "simulate_mouse_input",
4225
+ category: "write",
4226
+ description: "Simulate mouse input in the Roblox Studio viewport via VirtualInputManager. Use during playtest to click UI buttons, interact with objects, or navigate menus. Coordinates are viewport pixels (top-left is 0,0). Use capture_screenshot to identify UI element positions before clicking.",
4227
+ inputSchema: {
4228
+ type: "object",
4229
+ properties: {
4230
+ action: {
4231
+ type: "string",
4232
+ enum: ["click", "mouseDown", "mouseUp", "move", "scroll"],
4233
+ description: 'Mouse action to perform. "click" does mouseDown + short delay + mouseUp.'
4234
+ },
4235
+ x: {
4236
+ type: "number",
4237
+ description: "Viewport pixel X coordinate"
4238
+ },
4239
+ y: {
4240
+ type: "number",
4241
+ description: "Viewport pixel Y coordinate"
4242
+ },
4243
+ button: {
4244
+ type: "string",
4245
+ enum: ["Left", "Right", "Middle"],
4246
+ description: "Mouse button (default: Left)"
4247
+ },
4248
+ scrollDirection: {
4249
+ type: "string",
4250
+ enum: ["up", "down"],
4251
+ description: 'Scroll direction (only for "scroll" action)'
4252
+ },
4253
+ target: {
4254
+ type: "string",
4255
+ description: 'Instance target: "edit" (default), "server", "client-1", "client-2", etc.'
4256
+ }
4257
+ },
4258
+ required: ["action", "x", "y"]
4259
+ }
4260
+ },
4261
+ {
4262
+ name: "simulate_keyboard_input",
4263
+ category: "write",
4264
+ description: 'Simulate keyboard input via VirtualInputManager. Use during playtest for character movement (W/A/S/D), jumping (Space), interactions (E), or any key-driven action. For sustained movement, use "press" to hold and "release" to let go.',
4265
+ inputSchema: {
4266
+ type: "object",
4267
+ properties: {
4268
+ keyCode: {
4269
+ type: "string",
4270
+ description: 'Enum.KeyCode name: "W", "A", "S", "D", "Space", "E", "F", "LeftShift", "LeftControl", "Return", "Tab", "Escape", "One", "Two", etc.'
4271
+ },
4272
+ action: {
4273
+ type: "string",
4274
+ enum: ["press", "release", "tap"],
4275
+ description: '"tap" (default) = press + wait + release. "press" = key down only. "release" = key up only.'
4276
+ },
4277
+ duration: {
4278
+ type: "number",
4279
+ description: 'Hold duration in seconds for "tap" action (default: 0.1). Use longer values for sustained input like walking.'
4280
+ },
4281
+ target: {
4282
+ type: "string",
4283
+ description: 'Instance target: "edit" (default), "server", "client-1", "client-2", etc.'
4284
+ }
4285
+ },
4286
+ required: ["keyCode"]
4287
+ }
4288
+ },
4289
+ // === Character Navigation ===
4290
+ {
4291
+ name: "character_navigation",
4292
+ category: "write",
4293
+ description: 'Move the player character to a target position or instance during playtest. Uses PathfindingService for automatic navigation around obstacles, falling back to direct movement. Requires an active playtest in "play" mode. Does NOT simulate player input - moves the character directly.',
4294
+ inputSchema: {
4295
+ type: "object",
4296
+ properties: {
4297
+ position: {
4298
+ type: "array",
4299
+ items: { type: "number" },
4300
+ description: "Target world position [x, y, z]. Either this or instancePath is required."
4301
+ },
4302
+ instancePath: {
4303
+ type: "string",
4304
+ description: "Instance to navigate to (dot notation). The character walks to its Position. Either this or position is required."
4305
+ },
4306
+ waitForCompletion: {
4307
+ type: "boolean",
4308
+ description: "Wait for the character to arrive before returning (default: true)"
4309
+ },
4310
+ timeout: {
4311
+ type: "number",
4312
+ description: "Max seconds to wait for navigation to complete (default: 25)"
4313
+ },
4314
+ target: {
4315
+ type: "string",
4316
+ description: 'Instance target: "edit" (default), "server", "client-1", "client-2", etc.'
4317
+ }
4318
+ }
4319
+ }
4320
+ },
4321
+ // === Instance Operations ===
4322
+ {
4323
+ name: "clone_object",
4324
+ category: "write",
4325
+ description: "Clone an instance to a new parent location. Creates a deep copy of the instance and all its descendants.",
4326
+ inputSchema: {
4327
+ type: "object",
4328
+ properties: {
4329
+ instancePath: {
4330
+ type: "string",
4331
+ description: "Path of the instance to clone"
4332
+ },
4333
+ targetParentPath: {
4334
+ type: "string",
4335
+ description: "Path of the parent to place the clone under"
4336
+ }
4337
+ },
4338
+ required: ["instancePath", "targetParentPath"]
4339
+ }
4340
+ },
4341
+ // === Descendants & Comparison ===
4342
+ {
4343
+ name: "get_descendants",
4344
+ category: "read",
4345
+ description: "Get all descendants of an instance recursively with depth info. More efficient than repeated get_instance_children calls.",
4346
+ inputSchema: {
4347
+ type: "object",
4348
+ properties: {
4349
+ instancePath: {
4350
+ type: "string",
4351
+ description: "Root instance path"
4352
+ },
4353
+ maxDepth: {
4354
+ type: "number",
4355
+ description: "Maximum recursion depth (default: 10)"
4356
+ },
4357
+ classFilter: {
4358
+ type: "string",
4359
+ description: 'Only include instances of this class (uses IsA, so "BasePart" matches Part, MeshPart, etc.)'
4360
+ }
4361
+ },
4362
+ required: ["instancePath"]
4363
+ }
4364
+ },
4365
+ {
4366
+ name: "compare_instances",
4367
+ category: "read",
4368
+ description: "Diff two instances by comparing their properties. Useful for debugging why a duplicate behaves differently.",
4369
+ inputSchema: {
4370
+ type: "object",
4371
+ properties: {
4372
+ instancePathA: {
4373
+ type: "string",
4374
+ description: "First instance path"
4375
+ },
4376
+ instancePathB: {
4377
+ type: "string",
4378
+ description: "Second instance path"
4379
+ }
4380
+ },
4381
+ required: ["instancePathA", "instancePathB"]
4382
+ }
4383
+ },
4384
+ // === Output & Diagnostics ===
4385
+ {
4386
+ name: "get_output_log",
4387
+ category: "read",
4388
+ description: "Get the Studio output log history. Works in both edit and play mode.",
4389
+ inputSchema: {
4390
+ type: "object",
4391
+ properties: {
4392
+ maxEntries: {
4393
+ type: "number",
4394
+ description: "Maximum number of log entries to return (default: 100)"
4395
+ },
4396
+ messageType: {
4397
+ type: "string",
4398
+ description: 'Filter by message type (e.g. "Enum.MessageType.MessageOutput", "Enum.MessageType.MessageWarning", "Enum.MessageType.MessageError")'
4399
+ }
4400
+ }
4401
+ }
4402
+ },
4403
+ // === Bulk Attributes ===
4404
+ {
4405
+ name: "bulk_set_attributes",
4406
+ category: "write",
4407
+ description: "Set multiple attributes on an instance in a single call. More efficient than repeated set_attribute calls.",
4408
+ inputSchema: {
4409
+ type: "object",
4410
+ properties: {
4411
+ instancePath: {
4412
+ type: "string",
4413
+ description: "Instance path"
4414
+ },
4415
+ attributes: {
4416
+ type: "object",
4417
+ description: "Map of attribute names to values. Supports Vector3, Color3, UDim2 via _type convention."
4418
+ }
4419
+ },
4420
+ required: ["instancePath", "attributes"]
4421
+ }
4422
+ },
4423
+ // === Find and Replace ===
4424
+ {
4425
+ name: "find_and_replace_in_scripts",
4426
+ category: "write",
4427
+ description: "Find and replace text across all scripts in the game. Supports literal and Lua pattern matching. Use dryRun to preview changes before applying. Pairs with grep_scripts for search-only operations.",
4428
+ inputSchema: {
4429
+ type: "object",
4430
+ properties: {
4431
+ pattern: {
4432
+ type: "string",
4433
+ description: "Text or Lua pattern to find"
4434
+ },
4435
+ replacement: {
4436
+ type: "string",
4437
+ description: "Replacement text. When usePattern is true, supports Lua captures (%1, %2, etc.)."
4438
+ },
4439
+ caseSensitive: {
4440
+ type: "boolean",
4441
+ description: "Case-sensitive matching (default: false). Must be true when usePattern is true."
4442
+ },
4443
+ usePattern: {
4444
+ type: "boolean",
4445
+ description: "Use Lua pattern matching instead of literal (default: false). Requires caseSensitive: true."
4446
+ },
4447
+ path: {
4448
+ type: "string",
4449
+ description: 'Limit scope to a subtree (e.g. "game.ServerScriptService")'
4450
+ },
4451
+ classFilter: {
4452
+ type: "string",
4453
+ enum: ["Script", "LocalScript", "ModuleScript"],
4454
+ description: "Only search scripts of this class type"
4455
+ },
4456
+ dryRun: {
4457
+ type: "boolean",
4458
+ description: "Preview changes without applying them (default: false)"
4459
+ },
4460
+ maxReplacements: {
4461
+ type: "number",
4462
+ description: "Safety limit on total replacements (default: 1000)"
4463
+ }
4464
+ },
4465
+ required: ["pattern", "replacement"]
4466
+ }
4467
+ }
4468
+ ];
4469
+ var getReadOnlyTools = () => TOOL_DEFINITIONS.filter((t) => t.category === "read");
4470
+
4471
+ // src/index.ts
4472
+ import { createRequire } from "module";
4473
+ var require2 = createRequire(import.meta.url);
4474
+ var { version: VERSION } = require2("../package.json");
4475
+ var server = new RobloxStudioMCPServer({
4476
+ name: "robloxstudio-mcp-inspector",
4477
+ version: VERSION,
4478
+ tools: getReadOnlyTools()
4479
+ });
4480
+ server.run().catch((error) => {
4481
+ console.error("Server failed to start:", error);
4482
+ process.exit(1);
4483
+ });