robloxstudio-mcp 2.6.0-next.0 → 2.6.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -99,15 +99,18 @@ var init_install_plugin = __esm({
99
99
  }
100
100
  });
101
101
 
102
- // ../../node_modules/@robloxstudio-mcp/core/dist/server.js
103
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
102
+ // ../core/dist/server.js
103
+ import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
104
104
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
105
- import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
105
+ import { CallToolRequestSchema as CallToolRequestSchema2, ErrorCode as ErrorCode2, ListToolsRequestSchema as ListToolsRequestSchema2, McpError as McpError2 } from "@modelcontextprotocol/sdk/types.js";
106
106
 
107
- // ../../node_modules/@robloxstudio-mcp/core/dist/http-server.js
107
+ // ../core/dist/http-server.js
108
108
  import express from "express";
109
109
  import cors from "cors";
110
110
  import http from "http";
111
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
112
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
113
+ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
111
114
  var TOOL_HANDLERS = {
112
115
  get_file_tree: (tools, body) => tools.getFileTree(body.path),
113
116
  search_files: (tools, body) => tools.searchFiles(body.query, body.searchType),
@@ -155,10 +158,11 @@ var TOOL_HANDLERS = {
155
158
  remove_tag: (tools, body) => tools.removeTag(body.instancePath, body.tagName),
156
159
  get_tagged: (tools, body) => tools.getTagged(body.tagName),
157
160
  get_selection: (tools) => tools.getSelection(),
158
- execute_luau: (tools, body) => tools.executeLuau(body.code),
159
- start_playtest: (tools, body) => tools.startPlaytest(body.mode),
161
+ execute_luau: (tools, body) => tools.executeLuau(body.code, body.target),
162
+ start_playtest: (tools, body) => tools.startPlaytest(body.mode, body.numPlayers),
160
163
  stop_playtest: (tools) => tools.stopPlaytest(),
161
- get_playtest_output: (tools) => tools.getPlaytestOutput(),
164
+ get_playtest_output: (tools, body) => tools.getPlaytestOutput(body.target),
165
+ get_connected_instances: (tools) => tools.getConnectedInstances(),
162
166
  export_build: (tools, body) => tools.exportBuild(body.instancePath, body.outputId, body.style),
163
167
  create_build: (tools, body) => tools.createBuild(body.id, body.style, body.palette, body.parts, body.bounds),
164
168
  generate_build: (tools, body) => tools.generateBuild(body.id, body.style, body.palette, body.code, body.seed),
@@ -174,15 +178,54 @@ var TOOL_HANDLERS = {
174
178
  get_asset_thumbnail: (tools, body) => tools.getAssetThumbnail(body.assetId, body.size),
175
179
  insert_asset: (tools, body) => tools.insertAsset(body.assetId, body.parentPath, body.position),
176
180
  preview_asset: (tools, body) => tools.previewAsset(body.assetId, body.includeProperties, body.maxDepth),
177
- capture_screenshot: (tools) => tools.captureScreenshot()
181
+ render_object_screenshot: (tools, body) => tools.renderObjectScreenshot(body.instancePath, {
182
+ cameraPreset: body.cameraPreset,
183
+ padding: body.padding,
184
+ backdropColor: body.backdropColor,
185
+ savePath: body.savePath,
186
+ outputDir: body.outputDir,
187
+ fileName: body.fileName,
188
+ returnImage: body.returnImage
189
+ }),
190
+ render_model_screenshot: (tools, body) => tools.renderModelScreenshot(body.instancePath, {
191
+ cameraPreset: body.cameraPreset,
192
+ padding: body.padding,
193
+ backdropColor: body.backdropColor,
194
+ savePath: body.savePath,
195
+ outputDir: body.outputDir,
196
+ fileName: body.fileName,
197
+ returnImage: body.returnImage
198
+ }),
199
+ batch_render_objects: (tools, body) => tools.batchRenderObjects(body.parentPath, body.outputDir, {
200
+ recursive: body.recursive,
201
+ cameraPreset: body.cameraPreset,
202
+ padding: body.padding,
203
+ backdropColor: body.backdropColor
204
+ }),
205
+ batch_render_models: (tools, body) => tools.batchRenderModels(body.parentPath, body.outputDir, {
206
+ recursive: body.recursive,
207
+ cameraPreset: body.cameraPreset,
208
+ padding: body.padding,
209
+ backdropColor: body.backdropColor
210
+ }),
211
+ capture_screenshot: (tools) => tools.captureScreenshot(),
212
+ simulate_mouse_input: (tools, body) => tools.simulateMouseInput(body.action, body.x, body.y, body.button, body.scrollDirection, body.target),
213
+ simulate_keyboard_input: (tools, body) => tools.simulateKeyboardInput(body.keyCode, body.action, body.duration, body.target),
214
+ character_navigation: (tools, body) => tools.characterNavigation(body.position, body.instancePath, body.waitForCompletion, body.timeout, body.target),
215
+ find_and_replace_in_scripts: (tools, body) => tools.findAndReplaceInScripts(body.pattern, body.replacement, {
216
+ caseSensitive: body.caseSensitive,
217
+ usePattern: body.usePattern,
218
+ path: body.path,
219
+ classFilter: body.classFilter,
220
+ dryRun: body.dryRun,
221
+ maxReplacements: body.maxReplacements
222
+ })
178
223
  };
179
- function createHttpServer(tools, bridge, allowedTools) {
224
+ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
180
225
  const app = express();
181
- let pluginConnected = false;
182
226
  let mcpServerActive = false;
183
227
  let lastMCPActivity = 0;
184
228
  let mcpServerStartTime = 0;
185
- let lastPluginActivity = 0;
186
229
  const proxyInstances = /* @__PURE__ */ new Set();
187
230
  const setMCPServerActive = (active) => {
188
231
  mcpServerActive = active;
@@ -205,44 +248,78 @@ function createHttpServer(tools, bridge, allowedTools) {
205
248
  return Date.now() - lastMCPActivity < 3e4;
206
249
  };
207
250
  const isPluginConnected = () => {
208
- return pluginConnected && Date.now() - lastPluginActivity < 3e4;
251
+ return bridge.getInstances().length > 0;
209
252
  };
210
253
  app.use(cors());
211
254
  app.use(express.json({ limit: "50mb" }));
212
255
  app.use(express.urlencoded({ limit: "50mb", extended: true }));
213
256
  app.get("/health", (req, res) => {
257
+ const instances = bridge.getInstances();
214
258
  res.json({
215
259
  status: "ok",
216
260
  service: "robloxstudio-mcp",
217
- pluginConnected,
261
+ version: serverConfig?.version,
262
+ pluginConnected: instances.length > 0,
263
+ instanceCount: instances.length,
264
+ instances: instances.map((i) => ({
265
+ instanceId: i.instanceId,
266
+ role: i.role,
267
+ lastActivity: i.lastActivity,
268
+ connectedAt: i.connectedAt
269
+ })),
218
270
  mcpServerActive: isMCPServerActive(),
219
271
  uptime: mcpServerActive ? Date.now() - mcpServerStartTime : 0,
220
- proxyInstanceCount: proxyInstances.size
272
+ pendingRequests: bridge.getPendingRequestCount(),
273
+ proxyInstanceCount: proxyInstances.size,
274
+ streamableHttp: !!serverConfig
221
275
  });
222
276
  });
223
277
  app.post("/ready", (req, res) => {
224
- pluginConnected = true;
225
- lastPluginActivity = Date.now();
226
- res.json({ success: true });
278
+ const { instanceId, role } = req.body;
279
+ if (instanceId && role) {
280
+ const assignedRole = bridge.registerInstance(instanceId, role);
281
+ res.json({ success: true, assignedRole });
282
+ } else {
283
+ bridge.registerInstance("legacy", "edit");
284
+ res.json({ success: true, assignedRole: "edit" });
285
+ }
227
286
  });
228
287
  app.post("/disconnect", (req, res) => {
229
- pluginConnected = false;
230
- bridge.clearAllPendingRequests();
288
+ const { instanceId } = req.body;
289
+ if (instanceId) {
290
+ bridge.unregisterInstance(instanceId);
291
+ } else {
292
+ bridge.unregisterInstance("legacy");
293
+ bridge.clearAllPendingRequests();
294
+ }
231
295
  res.json({ success: true });
232
296
  });
233
297
  app.get("/status", (req, res) => {
298
+ const instances = bridge.getInstances();
234
299
  res.json({
235
- pluginConnected: isPluginConnected(),
300
+ pluginConnected: instances.length > 0,
301
+ instanceCount: instances.length,
302
+ instances: instances.map((i) => ({ instanceId: i.instanceId, role: i.role })),
236
303
  mcpServerActive: isMCPServerActive(),
237
304
  lastMCPActivity,
238
305
  uptime: mcpServerActive ? Date.now() - mcpServerStartTime : 0
239
306
  });
240
307
  });
308
+ app.get("/instances", (req, res) => {
309
+ res.json({ instances: bridge.getInstances() });
310
+ });
241
311
  app.get("/poll", (req, res) => {
242
- if (!pluginConnected) {
243
- pluginConnected = true;
312
+ const instanceId = req.query.instanceId;
313
+ if (instanceId) {
314
+ bridge.updateInstanceActivity(instanceId);
315
+ }
316
+ let callerRole = "edit";
317
+ if (instanceId) {
318
+ const inst = bridge.getInstances().find((i) => i.instanceId === instanceId);
319
+ if (inst) {
320
+ callerRole = inst.role;
321
+ }
244
322
  }
245
- lastPluginActivity = Date.now();
246
323
  if (!isMCPServerActive()) {
247
324
  res.status(503).json({
248
325
  error: "MCP server not connected",
@@ -252,7 +329,7 @@ function createHttpServer(tools, bridge, allowedTools) {
252
329
  });
253
330
  return;
254
331
  }
255
- const pendingRequest = bridge.getPendingRequest();
332
+ const pendingRequest = bridge.getPendingRequest(callerRole);
256
333
  if (pendingRequest) {
257
334
  res.json({
258
335
  request: pendingRequest.request,
@@ -280,7 +357,7 @@ function createHttpServer(tools, bridge, allowedTools) {
280
357
  res.json({ success: true });
281
358
  });
282
359
  app.post("/proxy", async (req, res) => {
283
- const { endpoint, data, proxyInstanceId } = req.body;
360
+ const { endpoint, data, target, proxyInstanceId } = req.body;
284
361
  if (!endpoint) {
285
362
  res.status(400).json({ error: "endpoint is required" });
286
363
  return;
@@ -289,12 +366,76 @@ function createHttpServer(tools, bridge, allowedTools) {
289
366
  proxyInstances.add(proxyInstanceId);
290
367
  }
291
368
  try {
292
- const response = await bridge.sendRequest(endpoint, data);
369
+ const response = await bridge.sendRequest(endpoint, data, target || "edit");
293
370
  res.json({ response });
294
371
  } catch (err) {
295
372
  res.status(500).json({ error: err.message || "Proxy request failed" });
296
373
  }
297
374
  });
375
+ if (serverConfig) {
376
+ const filteredTools = serverConfig.tools.filter((t) => !allowedTools || allowedTools.has(t.name));
377
+ app.post("/mcp", async (req, res) => {
378
+ try {
379
+ trackMCPActivity();
380
+ const server = new Server({ name: serverConfig.name, version: serverConfig.version }, { capabilities: { tools: {} } });
381
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
382
+ tools: filteredTools.map((t) => ({
383
+ name: t.name,
384
+ description: t.description,
385
+ inputSchema: t.inputSchema
386
+ }))
387
+ }));
388
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
389
+ const { name, arguments: args } = request.params;
390
+ if (allowedTools && !allowedTools.has(name)) {
391
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
392
+ }
393
+ const handler = TOOL_HANDLERS[name];
394
+ if (!handler) {
395
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
396
+ }
397
+ try {
398
+ return await handler(tools, args || {});
399
+ } catch (error) {
400
+ if (error instanceof McpError)
401
+ throw error;
402
+ throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
403
+ }
404
+ });
405
+ const transport = new StreamableHTTPServerTransport({
406
+ sessionIdGenerator: void 0
407
+ });
408
+ await server.connect(transport);
409
+ await transport.handleRequest(req, res, req.body);
410
+ res.on("close", () => {
411
+ transport.close();
412
+ server.close();
413
+ });
414
+ } catch (error) {
415
+ if (!res.headersSent) {
416
+ res.status(500).json({
417
+ jsonrpc: "2.0",
418
+ error: { code: -32603, message: "Internal server error" },
419
+ id: null
420
+ });
421
+ }
422
+ }
423
+ });
424
+ app.get("/mcp", (req, res) => {
425
+ res.writeHead(405).end(JSON.stringify({
426
+ jsonrpc: "2.0",
427
+ error: { code: -32e3, message: "Method not allowed." },
428
+ id: null
429
+ }));
430
+ });
431
+ app.delete("/mcp", (req, res) => {
432
+ res.writeHead(405).end(JSON.stringify({
433
+ jsonrpc: "2.0",
434
+ error: { code: -32e3, message: "Method not allowed." },
435
+ id: null
436
+ }));
437
+ });
438
+ }
298
439
  app.use("/mcp/*", (req, res, next) => {
299
440
  trackMCPActivity();
300
441
  next();
@@ -352,15 +493,15 @@ function bindPort(app, host, port) {
352
493
  });
353
494
  }
354
495
 
355
- // ../../node_modules/@robloxstudio-mcp/core/dist/tools/studio-client.js
496
+ // ../core/dist/tools/studio-client.js
356
497
  var StudioHttpClient = class {
357
498
  bridge;
358
499
  constructor(bridge) {
359
500
  this.bridge = bridge;
360
501
  }
361
- async request(endpoint, data) {
502
+ async request(endpoint, data, target = "edit") {
362
503
  try {
363
- const response = await this.bridge.sendRequest(endpoint, data);
504
+ const response = await this.bridge.sendRequest(endpoint, data, target);
364
505
  return response;
365
506
  } catch (error) {
366
507
  if (error instanceof Error && error.message === "Request timeout") {
@@ -371,7 +512,7 @@ var StudioHttpClient = class {
371
512
  }
372
513
  };
373
514
 
374
- // ../../node_modules/@robloxstudio-mcp/core/dist/tools/build-executor.js
515
+ // ../core/dist/tools/build-executor.js
375
516
  import * as vm from "vm";
376
517
  var DEFAULT_TIMEOUT = 1e4;
377
518
  var DEFAULT_MAX_PARTS = 1e4;
@@ -768,7 +909,7 @@ function runBuildExecutor(code, palette, seed, options) {
768
909
  return { parts, bounds, partCount: parts.length };
769
910
  }
770
911
 
771
- // ../../node_modules/@robloxstudio-mcp/core/dist/opencloud-client.js
912
+ // ../core/dist/opencloud-client.js
772
913
  var OpenCloudClient = class {
773
914
  apiKey;
774
915
  baseUrl;
@@ -905,19 +1046,37 @@ var OpenCloudClient = class {
905
1046
  }
906
1047
  };
907
1048
 
908
- // ../../node_modules/@robloxstudio-mcp/core/dist/png-encoder.js
909
- import { deflateSync, crc32 } from "zlib";
1049
+ // ../core/dist/png-encoder.js
1050
+ import { deflateSync } from "zlib";
910
1051
  var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
1052
+ var CRC_TABLE = new Uint32Array(256);
1053
+ for (let n = 0; n < 256; n++) {
1054
+ let c = n;
1055
+ for (let k = 0; k < 8; k++)
1056
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
1057
+ CRC_TABLE[n] = c;
1058
+ }
1059
+ function crc32(buf) {
1060
+ let crc = 4294967295;
1061
+ for (let i = 0; i < buf.length; i++)
1062
+ crc = CRC_TABLE[(crc ^ buf[i]) & 255] ^ crc >>> 8;
1063
+ return (crc ^ 4294967295) >>> 0;
1064
+ }
911
1065
  function writeChunk(type, data) {
912
1066
  const typeBytes = Buffer.from(type, "ascii");
913
1067
  const length = Buffer.alloc(4);
914
1068
  length.writeUInt32BE(data.length);
915
1069
  const crcInput = Buffer.concat([typeBytes, data]);
916
1070
  const checksum = Buffer.alloc(4);
917
- checksum.writeUInt32BE(crc32(crcInput) >>> 0);
1071
+ checksum.writeUInt32BE(crc32(crcInput));
918
1072
  return Buffer.concat([length, typeBytes, data, checksum]);
919
1073
  }
920
1074
  function rgbaToPng(rgba, width, height) {
1075
+ if (width <= 0 || height <= 0)
1076
+ throw new Error(`Invalid PNG dimensions: ${width}x${height}`);
1077
+ const expected = width * height * 4;
1078
+ if (rgba.length < expected)
1079
+ throw new Error(`Buffer too small: got ${rgba.length}, need ${expected}`);
921
1080
  const stride = width * 4;
922
1081
  const filtered = Buffer.alloc(height * (1 + stride));
923
1082
  for (let y = 0; y < height; y++) {
@@ -940,14 +1099,50 @@ function rgbaToPng(rgba, width, height) {
940
1099
  ]);
941
1100
  }
942
1101
 
943
- // ../../node_modules/@robloxstudio-mcp/core/dist/tools/index.js
1102
+ // ../core/dist/tools/index.js
944
1103
  import * as fs from "fs";
945
1104
  import * as path from "path";
1105
+ function encodePngFromRgbaResponse(response) {
1106
+ if (!response.data || response.width === void 0 || response.height === void 0) {
1107
+ throw new Error("Render response missing data, width, or height");
1108
+ }
1109
+ const rgbaBuffer = Buffer.from(response.data, "base64");
1110
+ return rgbaToPng(rgbaBuffer, response.width, response.height);
1111
+ }
1112
+ function ensureParentDirectory(filePath) {
1113
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1114
+ }
1115
+ function writePng(filePath, pngBuffer) {
1116
+ ensureParentDirectory(filePath);
1117
+ fs.writeFileSync(filePath, pngBuffer);
1118
+ }
1119
+ function sanitizeFileSegment(name) {
1120
+ const sanitized = name.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_").replace(/\s+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
1121
+ return sanitized || "model";
1122
+ }
1123
+ function ensurePngExtension(fileName) {
1124
+ return path.extname(fileName) ? fileName : `${fileName}.png`;
1125
+ }
1126
+ function getDefaultObjectName(instancePath, instanceName) {
1127
+ return instanceName || instancePath.split(".").pop() || "object";
1128
+ }
1129
+ function resolveSingleRenderSavePath(instancePath, instanceName, options) {
1130
+ if (options?.savePath) {
1131
+ return options.savePath;
1132
+ }
1133
+ if (!options?.outputDir) {
1134
+ return void 0;
1135
+ }
1136
+ const baseName = sanitizeFileSegment(options.fileName || getDefaultObjectName(instancePath, instanceName));
1137
+ return path.join(options.outputDir, ensurePngExtension(baseName));
1138
+ }
946
1139
  var RobloxStudioTools = class _RobloxStudioTools {
947
1140
  client;
1141
+ bridge;
948
1142
  openCloudClient;
949
1143
  constructor(bridge) {
950
1144
  this.client = new StudioHttpClient(bridge);
1145
+ this.bridge = bridge;
951
1146
  this.openCloudClient = new OpenCloudClient();
952
1147
  }
953
1148
  async getFileTree(path2 = "") {
@@ -1466,11 +1661,11 @@ var RobloxStudioTools = class _RobloxStudioTools {
1466
1661
  ]
1467
1662
  };
1468
1663
  }
1469
- async executeLuau(code) {
1664
+ async executeLuau(code, target) {
1470
1665
  if (!code) {
1471
1666
  throw new Error("Code is required for execute_luau");
1472
1667
  }
1473
- const response = await this.client.request("/api/execute-luau", { code });
1668
+ const response = await this.client.request("/api/execute-luau", { code }, target || "edit");
1474
1669
  return {
1475
1670
  content: [
1476
1671
  {
@@ -1480,11 +1675,15 @@ var RobloxStudioTools = class _RobloxStudioTools {
1480
1675
  ]
1481
1676
  };
1482
1677
  }
1483
- async startPlaytest(mode) {
1678
+ async startPlaytest(mode, numPlayers) {
1484
1679
  if (mode !== "play" && mode !== "run") {
1485
1680
  throw new Error('mode must be "play" or "run"');
1486
1681
  }
1487
- const response = await this.client.request("/api/start-playtest", { mode });
1682
+ const data = { mode };
1683
+ if (numPlayers !== void 0) {
1684
+ data.numPlayers = numPlayers;
1685
+ }
1686
+ const response = await this.client.request("/api/start-playtest", data);
1488
1687
  return {
1489
1688
  content: [
1490
1689
  {
@@ -1505,8 +1704,8 @@ var RobloxStudioTools = class _RobloxStudioTools {
1505
1704
  ]
1506
1705
  };
1507
1706
  }
1508
- async getPlaytestOutput() {
1509
- const response = await this.client.request("/api/get-playtest-output", {});
1707
+ async getPlaytestOutput(target) {
1708
+ const response = await this.client.request("/api/get-playtest-output", {}, target || "edit");
1510
1709
  return {
1511
1710
  content: [
1512
1711
  {
@@ -1516,6 +1715,17 @@ var RobloxStudioTools = class _RobloxStudioTools {
1516
1715
  ]
1517
1716
  };
1518
1717
  }
1718
+ async getConnectedInstances() {
1719
+ const instances = this.bridge.getInstances();
1720
+ return {
1721
+ content: [
1722
+ {
1723
+ type: "text",
1724
+ text: JSON.stringify({ instances, count: instances.length })
1725
+ }
1726
+ ]
1727
+ };
1728
+ }
1519
1729
  async undo() {
1520
1730
  const response = await this.client.request("/api/undo", {});
1521
1731
  return {
@@ -1991,6 +2201,212 @@ var RobloxStudioTools = class _RobloxStudioTools {
1991
2201
  }]
1992
2202
  };
1993
2203
  }
2204
+ async requestRenderObjectScreenshot(instancePath, options) {
2205
+ if (!instancePath) {
2206
+ throw new Error("instancePath is required for render_object_screenshot");
2207
+ }
2208
+ const response = await this.client.request("/api/render-model-screenshot", {
2209
+ instancePath,
2210
+ cameraPreset: options?.cameraPreset,
2211
+ padding: options?.padding,
2212
+ backdropColor: options?.backdropColor
2213
+ });
2214
+ if (response.error) {
2215
+ throw new Error(response.error);
2216
+ }
2217
+ return response;
2218
+ }
2219
+ async renderObjectScreenshot(instancePath, options) {
2220
+ const response = await this.requestRenderObjectScreenshot(instancePath, options);
2221
+ const pngBuffer = encodePngFromRgbaResponse(response);
2222
+ const savePath = resolveSingleRenderSavePath(response.instancePath ?? instancePath, response.instanceName, options);
2223
+ const returnImage = options?.returnImage ?? true;
2224
+ if (savePath) {
2225
+ writePng(savePath, pngBuffer);
2226
+ }
2227
+ const content = [];
2228
+ if (returnImage) {
2229
+ content.push({
2230
+ type: "image",
2231
+ data: pngBuffer.toString("base64"),
2232
+ mimeType: "image/png"
2233
+ });
2234
+ }
2235
+ content.push({
2236
+ type: "text",
2237
+ text: JSON.stringify({
2238
+ success: true,
2239
+ instancePath: response.instancePath ?? instancePath,
2240
+ instanceName: response.instanceName,
2241
+ cameraPreset: response.cameraPreset ?? options?.cameraPreset ?? "isometric",
2242
+ width: response.width,
2243
+ height: response.height,
2244
+ savedPath: savePath
2245
+ })
2246
+ });
2247
+ return { content };
2248
+ }
2249
+ async renderModelScreenshot(instancePath, options) {
2250
+ return this.renderObjectScreenshot(instancePath, options);
2251
+ }
2252
+ async collectRenderablePaths(parentPath, recursive) {
2253
+ const response = await this.client.request("/api/instance-children", {
2254
+ instancePath: parentPath
2255
+ });
2256
+ if (response.error) {
2257
+ throw new Error(response.error);
2258
+ }
2259
+ const children = response.children ?? [];
2260
+ const renderablePaths = children.filter((child) => child.className === "Model" || child.className === "Part" || child.className === "MeshPart" || child.className === "WedgePart" || child.className === "CornerWedgePart" || child.className === "TrussPart" || child.className === "SpawnLocation" || child.className === "Seat" || child.className === "VehicleSeat" || child.className === "UnionOperation").map((child) => child.path);
2261
+ if (!recursive) {
2262
+ return renderablePaths;
2263
+ }
2264
+ const nestedTargets = children.filter((child) => child.hasChildren).map((child) => child.path);
2265
+ for (const nestedPath of nestedTargets) {
2266
+ renderablePaths.push(...await this.collectRenderablePaths(nestedPath, true));
2267
+ }
2268
+ return renderablePaths;
2269
+ }
2270
+ async batchRenderObjects(parentPath, outputDir, options) {
2271
+ if (!parentPath || !outputDir) {
2272
+ throw new Error("parentPath and outputDir are required for batch_render_objects");
2273
+ }
2274
+ const recursive = options?.recursive ?? false;
2275
+ const renderablePaths = await this.collectRenderablePaths(parentPath, recursive);
2276
+ const usedFilePaths = /* @__PURE__ */ new Set();
2277
+ const results = [];
2278
+ fs.mkdirSync(outputDir, { recursive: true });
2279
+ for (const objectPath of renderablePaths) {
2280
+ const objectName = objectPath.split(".").pop() || "object";
2281
+ const fileBaseName = sanitizeFileSegment(objectName);
2282
+ let filePath = path.join(outputDir, `${fileBaseName}.png`);
2283
+ let suffix = 1;
2284
+ while (usedFilePaths.has(filePath)) {
2285
+ filePath = path.join(outputDir, `${fileBaseName}_${suffix}.png`);
2286
+ suffix += 1;
2287
+ }
2288
+ usedFilePaths.add(filePath);
2289
+ try {
2290
+ const response = await this.requestRenderObjectScreenshot(objectPath, options);
2291
+ const pngBuffer = encodePngFromRgbaResponse(response);
2292
+ writePng(filePath, pngBuffer);
2293
+ results.push({
2294
+ success: true,
2295
+ instancePath: response.instancePath ?? objectPath,
2296
+ instanceName: response.instanceName ?? objectName,
2297
+ cameraPreset: response.cameraPreset ?? options?.cameraPreset ?? "isometric",
2298
+ width: response.width,
2299
+ height: response.height,
2300
+ filePath
2301
+ });
2302
+ } catch (error) {
2303
+ results.push({
2304
+ success: false,
2305
+ instancePath: objectPath,
2306
+ instanceName: objectName,
2307
+ filePath,
2308
+ error: error instanceof Error ? error.message : String(error)
2309
+ });
2310
+ }
2311
+ }
2312
+ const manifestPath = path.join(outputDir, "render-manifest.json");
2313
+ fs.writeFileSync(manifestPath, JSON.stringify({
2314
+ parentPath,
2315
+ outputDir,
2316
+ recursive,
2317
+ cameraPreset: options?.cameraPreset ?? "isometric",
2318
+ totalObjects: renderablePaths.length,
2319
+ renderedAt: (/* @__PURE__ */ new Date()).toISOString(),
2320
+ results
2321
+ }, null, 2));
2322
+ return {
2323
+ content: [{
2324
+ type: "text",
2325
+ text: JSON.stringify({
2326
+ success: true,
2327
+ parentPath,
2328
+ outputDir,
2329
+ recursive,
2330
+ totalObjects: renderablePaths.length,
2331
+ manifestPath,
2332
+ results
2333
+ })
2334
+ }]
2335
+ };
2336
+ }
2337
+ async batchRenderModels(parentPath, outputDir, options) {
2338
+ return this.batchRenderObjects(parentPath, outputDir, options);
2339
+ }
2340
+ async simulateMouseInput(action, x, y, button, scrollDirection, target) {
2341
+ if (!action) {
2342
+ throw new Error("action is required for simulate_mouse_input");
2343
+ }
2344
+ const response = await this.client.request("/api/simulate-mouse-input", {
2345
+ action,
2346
+ x,
2347
+ y,
2348
+ button,
2349
+ scrollDirection
2350
+ }, target || "edit");
2351
+ return {
2352
+ content: [{
2353
+ type: "text",
2354
+ text: JSON.stringify(response)
2355
+ }]
2356
+ };
2357
+ }
2358
+ async simulateKeyboardInput(keyCode, action, duration, target) {
2359
+ if (!keyCode) {
2360
+ throw new Error("keyCode is required for simulate_keyboard_input");
2361
+ }
2362
+ const response = await this.client.request("/api/simulate-keyboard-input", {
2363
+ keyCode,
2364
+ action,
2365
+ duration
2366
+ }, target || "edit");
2367
+ return {
2368
+ content: [{
2369
+ type: "text",
2370
+ text: JSON.stringify(response)
2371
+ }]
2372
+ };
2373
+ }
2374
+ async characterNavigation(position, instancePath, waitForCompletion, timeout, target) {
2375
+ if (!position && !instancePath) {
2376
+ throw new Error("Either position or instancePath is required for character_navigation");
2377
+ }
2378
+ const response = await this.client.request("/api/character-navigation", {
2379
+ position,
2380
+ instancePath,
2381
+ waitForCompletion,
2382
+ timeout
2383
+ }, target || "edit");
2384
+ return {
2385
+ content: [{
2386
+ type: "text",
2387
+ text: JSON.stringify(response)
2388
+ }]
2389
+ };
2390
+ }
2391
+ async findAndReplaceInScripts(pattern, replacement, options) {
2392
+ if (!pattern) {
2393
+ throw new Error("pattern is required for find_and_replace_in_scripts");
2394
+ }
2395
+ if (replacement === void 0 || replacement === null) {
2396
+ throw new Error("replacement is required for find_and_replace_in_scripts");
2397
+ }
2398
+ const response = await this.client.request("/api/find-and-replace-in-scripts", {
2399
+ pattern,
2400
+ replacement,
2401
+ ...options
2402
+ });
2403
+ return {
2404
+ content: [{
2405
+ type: "text",
2406
+ text: JSON.stringify(response)
2407
+ }]
2408
+ };
2409
+ }
1994
2410
  async captureScreenshot() {
1995
2411
  const response = await this.client.request("/api/capture-screenshot", {});
1996
2412
  if (response.error) {
@@ -2001,8 +2417,7 @@ var RobloxStudioTools = class _RobloxStudioTools {
2001
2417
  }]
2002
2418
  };
2003
2419
  }
2004
- const rgbaBuffer = Buffer.from(response.data, "base64");
2005
- const pngBuffer = rgbaToPng(rgbaBuffer, response.width, response.height);
2420
+ const pngBuffer = encodePngFromRgbaResponse(response);
2006
2421
  return {
2007
2422
  content: [{
2008
2423
  type: "image",
@@ -2013,12 +2428,61 @@ var RobloxStudioTools = class _RobloxStudioTools {
2013
2428
  }
2014
2429
  };
2015
2430
 
2016
- // ../../node_modules/@robloxstudio-mcp/core/dist/bridge-service.js
2431
+ // ../core/dist/bridge-service.js
2017
2432
  import { v4 as uuidv4 } from "uuid";
2433
+ var STALE_INSTANCE_MS = 3e4;
2018
2434
  var BridgeService = class {
2019
2435
  pendingRequests = /* @__PURE__ */ new Map();
2436
+ instances = /* @__PURE__ */ new Map();
2437
+ nextClientIndex = 1;
2020
2438
  requestTimeout = 3e4;
2021
- async sendRequest(endpoint, data) {
2439
+ registerInstance(instanceId, role) {
2440
+ let assignedRole = role;
2441
+ if (role === "client") {
2442
+ assignedRole = `client-${this.nextClientIndex}`;
2443
+ this.nextClientIndex++;
2444
+ }
2445
+ this.instances.set(instanceId, {
2446
+ instanceId,
2447
+ role: assignedRole,
2448
+ lastActivity: Date.now(),
2449
+ connectedAt: Date.now()
2450
+ });
2451
+ return assignedRole;
2452
+ }
2453
+ unregisterInstance(instanceId) {
2454
+ this.instances.delete(instanceId);
2455
+ for (const [id, req] of this.pendingRequests.entries()) {
2456
+ const targetRole = req.target;
2457
+ const hasHandler = Array.from(this.instances.values()).some((i) => i.role === targetRole);
2458
+ if (!hasHandler) {
2459
+ clearTimeout(req.timeoutId);
2460
+ this.pendingRequests.delete(id);
2461
+ req.reject(new Error(`Target instance "${targetRole}" disconnected`));
2462
+ }
2463
+ }
2464
+ }
2465
+ getInstances() {
2466
+ return Array.from(this.instances.values());
2467
+ }
2468
+ getPendingRequestCount() {
2469
+ return this.pendingRequests.size;
2470
+ }
2471
+ updateInstanceActivity(instanceId) {
2472
+ const inst = this.instances.get(instanceId);
2473
+ if (inst) {
2474
+ inst.lastActivity = Date.now();
2475
+ }
2476
+ }
2477
+ cleanupStaleInstances() {
2478
+ const now = Date.now();
2479
+ for (const [id, inst] of this.instances.entries()) {
2480
+ if (now - inst.lastActivity > STALE_INSTANCE_MS) {
2481
+ this.unregisterInstance(id);
2482
+ }
2483
+ }
2484
+ }
2485
+ async sendRequest(endpoint, data, target = "edit") {
2022
2486
  const requestId = uuidv4();
2023
2487
  return new Promise((resolve, reject) => {
2024
2488
  const timeoutId = setTimeout(() => {
@@ -2031,6 +2495,7 @@ var BridgeService = class {
2031
2495
  id: requestId,
2032
2496
  endpoint,
2033
2497
  data,
2498
+ target,
2034
2499
  timestamp: Date.now(),
2035
2500
  resolve,
2036
2501
  reject,
@@ -2039,9 +2504,11 @@ var BridgeService = class {
2039
2504
  this.pendingRequests.set(requestId, request);
2040
2505
  });
2041
2506
  }
2042
- getPendingRequest() {
2507
+ getPendingRequest(callerRole = "edit") {
2043
2508
  let oldestRequest = null;
2044
2509
  for (const request of this.pendingRequests.values()) {
2510
+ if (request.target !== callerRole)
2511
+ continue;
2045
2512
  if (!oldestRequest || request.timestamp < oldestRequest.timestamp) {
2046
2513
  oldestRequest = request;
2047
2514
  }
@@ -2092,7 +2559,7 @@ var BridgeService = class {
2092
2559
  }
2093
2560
  };
2094
2561
 
2095
- // ../../node_modules/@robloxstudio-mcp/core/dist/proxy-bridge-service.js
2562
+ // ../core/dist/proxy-bridge-service.js
2096
2563
  import { v4 as uuidv42 } from "uuid";
2097
2564
  var ProxyBridgeService = class extends BridgeService {
2098
2565
  primaryBaseUrl;
@@ -2103,14 +2570,14 @@ var ProxyBridgeService = class extends BridgeService {
2103
2570
  this.primaryBaseUrl = primaryBaseUrl;
2104
2571
  this.proxyInstanceId = uuidv42();
2105
2572
  }
2106
- async sendRequest(endpoint, data) {
2573
+ async sendRequest(endpoint, data, target = "edit") {
2107
2574
  const controller = new AbortController();
2108
2575
  const timeoutId = setTimeout(() => controller.abort(), this.proxyRequestTimeout);
2109
2576
  try {
2110
2577
  const response = await fetch(`${this.primaryBaseUrl}/proxy`, {
2111
2578
  method: "POST",
2112
2579
  headers: { "Content-Type": "application/json" },
2113
- body: JSON.stringify({ endpoint, data, proxyInstanceId: this.proxyInstanceId }),
2580
+ body: JSON.stringify({ endpoint, data, target, proxyInstanceId: this.proxyInstanceId }),
2114
2581
  signal: controller.signal
2115
2582
  });
2116
2583
  clearTimeout(timeoutId);
@@ -2137,7 +2604,7 @@ var ProxyBridgeService = class extends BridgeService {
2137
2604
  }
2138
2605
  };
2139
2606
 
2140
- // ../../node_modules/@robloxstudio-mcp/core/dist/server.js
2607
+ // ../core/dist/server.js
2141
2608
  var RobloxStudioMCPServer = class {
2142
2609
  server;
2143
2610
  tools;
@@ -2147,7 +2614,7 @@ var RobloxStudioMCPServer = class {
2147
2614
  constructor(config) {
2148
2615
  this.config = config;
2149
2616
  this.allowedToolNames = new Set(config.tools.map((t) => t.name));
2150
- this.server = new Server({
2617
+ this.server = new Server2({
2151
2618
  name: config.name,
2152
2619
  version: config.version
2153
2620
  }, {
@@ -2160,7 +2627,7 @@ var RobloxStudioMCPServer = class {
2160
2627
  this.setupToolHandlers();
2161
2628
  }
2162
2629
  setupToolHandlers() {
2163
- this.server.setRequestHandler(ListToolsRequestSchema, async () => {
2630
+ this.server.setRequestHandler(ListToolsRequestSchema2, async () => {
2164
2631
  return {
2165
2632
  tools: this.config.tools.map((t) => ({
2166
2633
  name: t.name,
@@ -2169,10 +2636,10 @@ var RobloxStudioMCPServer = class {
2169
2636
  }))
2170
2637
  };
2171
2638
  });
2172
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
2639
+ this.server.setRequestHandler(CallToolRequestSchema2, async (request) => {
2173
2640
  const { name, arguments: args } = request.params;
2174
2641
  if (!this.allowedToolNames.has(name)) {
2175
- throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
2642
+ throw new McpError2(ErrorCode2.MethodNotFound, `Unknown tool: ${name}`);
2176
2643
  }
2177
2644
  try {
2178
2645
  switch (name) {
@@ -2258,13 +2725,15 @@ var RobloxStudioMCPServer = class {
2258
2725
  case "get_selection":
2259
2726
  return await this.tools.getSelection();
2260
2727
  case "execute_luau":
2261
- return await this.tools.executeLuau(args?.code);
2728
+ return await this.tools.executeLuau(args?.code, args?.target);
2262
2729
  case "start_playtest":
2263
- return await this.tools.startPlaytest(args?.mode);
2730
+ return await this.tools.startPlaytest(args?.mode, args?.numPlayers);
2264
2731
  case "stop_playtest":
2265
2732
  return await this.tools.stopPlaytest();
2266
2733
  case "get_playtest_output":
2267
- return await this.tools.getPlaytestOutput();
2734
+ return await this.tools.getPlaytestOutput(args?.target);
2735
+ case "get_connected_instances":
2736
+ return await this.tools.getConnectedInstances();
2268
2737
  case "export_build":
2269
2738
  return await this.tools.exportBuild(args?.instancePath, args?.outputId, args?.style);
2270
2739
  case "create_build":
@@ -2295,15 +2764,64 @@ var RobloxStudioMCPServer = class {
2295
2764
  return await this.tools.insertAsset(args?.assetId, args?.parentPath, args?.position);
2296
2765
  case "preview_asset":
2297
2766
  return await this.tools.previewAsset(args?.assetId, args?.includeProperties, args?.maxDepth);
2767
+ case "render_object_screenshot":
2768
+ return await this.tools.renderObjectScreenshot(args?.instancePath, {
2769
+ cameraPreset: args?.cameraPreset,
2770
+ padding: args?.padding,
2771
+ backdropColor: args?.backdropColor,
2772
+ savePath: args?.savePath,
2773
+ outputDir: args?.outputDir,
2774
+ fileName: args?.fileName,
2775
+ returnImage: args?.returnImage
2776
+ });
2777
+ case "render_model_screenshot":
2778
+ return await this.tools.renderModelScreenshot(args?.instancePath, {
2779
+ cameraPreset: args?.cameraPreset,
2780
+ padding: args?.padding,
2781
+ backdropColor: args?.backdropColor,
2782
+ savePath: args?.savePath,
2783
+ outputDir: args?.outputDir,
2784
+ fileName: args?.fileName,
2785
+ returnImage: args?.returnImage
2786
+ });
2787
+ case "batch_render_objects":
2788
+ return await this.tools.batchRenderObjects(args?.parentPath, args?.outputDir, {
2789
+ recursive: args?.recursive,
2790
+ cameraPreset: args?.cameraPreset,
2791
+ padding: args?.padding,
2792
+ backdropColor: args?.backdropColor
2793
+ });
2794
+ case "batch_render_models":
2795
+ return await this.tools.batchRenderModels(args?.parentPath, args?.outputDir, {
2796
+ recursive: args?.recursive,
2797
+ cameraPreset: args?.cameraPreset,
2798
+ padding: args?.padding,
2799
+ backdropColor: args?.backdropColor
2800
+ });
2298
2801
  case "capture_screenshot":
2299
2802
  return await this.tools.captureScreenshot();
2803
+ case "simulate_mouse_input":
2804
+ return await this.tools.simulateMouseInput(args?.action, args?.x, args?.y, args?.button, args?.scrollDirection, args?.target);
2805
+ case "simulate_keyboard_input":
2806
+ return await this.tools.simulateKeyboardInput(args?.keyCode, args?.action, args?.duration, args?.target);
2807
+ case "character_navigation":
2808
+ return await this.tools.characterNavigation(args?.position, args?.instancePath, args?.waitForCompletion, args?.timeout, args?.target);
2809
+ case "find_and_replace_in_scripts":
2810
+ return await this.tools.findAndReplaceInScripts(args?.pattern, args?.replacement, {
2811
+ caseSensitive: args?.caseSensitive,
2812
+ usePattern: args?.usePattern,
2813
+ path: args?.path,
2814
+ classFilter: args?.classFilter,
2815
+ dryRun: args?.dryRun,
2816
+ maxReplacements: args?.maxReplacements
2817
+ });
2300
2818
  default:
2301
- throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
2819
+ throw new McpError2(ErrorCode2.MethodNotFound, `Unknown tool: ${name}`);
2302
2820
  }
2303
2821
  } catch (error) {
2304
- if (error instanceof McpError)
2822
+ if (error instanceof McpError2)
2305
2823
  throw error;
2306
- throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
2824
+ throw new McpError2(ErrorCode2.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
2307
2825
  }
2308
2826
  });
2309
2827
  }
@@ -2316,11 +2834,12 @@ var RobloxStudioMCPServer = class {
2316
2834
  let boundPort = 0;
2317
2835
  let promotionInterval;
2318
2836
  try {
2319
- primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames);
2837
+ primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config);
2320
2838
  const result = await listenWithRetry(primaryApp, host, basePort, 5);
2321
2839
  httpHandle = result.server;
2322
2840
  boundPort = result.port;
2323
2841
  console.error(`HTTP server listening on ${host}:${boundPort} for Studio plugin (primary mode)`);
2842
+ console.error(`Streamable HTTP MCP endpoint: http://localhost:${boundPort}/mcp`);
2324
2843
  } catch {
2325
2844
  bridgeMode = "proxy";
2326
2845
  primaryApp = void 0;
@@ -2333,7 +2852,7 @@ var RobloxStudioMCPServer = class {
2333
2852
  try {
2334
2853
  this.bridge = new BridgeService();
2335
2854
  this.tools = new RobloxStudioTools(this.bridge);
2336
- primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames);
2855
+ primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config);
2337
2856
  const result = await listenWithRetry(primaryApp, host, basePort, 5);
2338
2857
  httpHandle = result.server;
2339
2858
  boundPort = result.port;
@@ -2353,7 +2872,7 @@ var RobloxStudioMCPServer = class {
2353
2872
  let legacyHandle;
2354
2873
  let legacyApp;
2355
2874
  if (boundPort !== LEGACY_PORT && bridgeMode === "primary") {
2356
- legacyApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames);
2875
+ legacyApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config);
2357
2876
  try {
2358
2877
  const result = await listenWithRetry(legacyApp, host, LEGACY_PORT, 1);
2359
2878
  legacyHandle = result.server;
@@ -2391,13 +2910,16 @@ var RobloxStudioMCPServer = class {
2391
2910
  }, 5e3);
2392
2911
  const cleanupInterval = setInterval(() => {
2393
2912
  this.bridge.cleanupOldRequests();
2913
+ this.bridge.cleanupStaleInstances();
2394
2914
  }, 5e3);
2395
- const shutdown = () => {
2915
+ const shutdown = async () => {
2396
2916
  console.error("Shutting down MCP server...");
2397
2917
  clearInterval(activityInterval);
2398
2918
  clearInterval(cleanupInterval);
2399
2919
  if (promotionInterval)
2400
2920
  clearInterval(promotionInterval);
2921
+ await this.server.close().catch(() => {
2922
+ });
2401
2923
  if (httpHandle)
2402
2924
  httpHandle.close();
2403
2925
  if (legacyHandle)
@@ -2412,7 +2934,7 @@ var RobloxStudioMCPServer = class {
2412
2934
  }
2413
2935
  };
2414
2936
 
2415
- // ../../node_modules/@robloxstudio-mcp/core/dist/tools/definitions.js
2937
+ // ../core/dist/tools/definitions.js
2416
2938
  var TOOL_DEFINITIONS = [
2417
2939
  // === File & Instance Browsing ===
2418
2940
  {
@@ -2424,8 +2946,7 @@ var TOOL_DEFINITIONS = [
2424
2946
  properties: {
2425
2947
  path: {
2426
2948
  type: "string",
2427
- description: "Root path (default: game root)",
2428
- default: ""
2949
+ description: "Root path (default: game root)"
2429
2950
  }
2430
2951
  }
2431
2952
  }
@@ -2444,8 +2965,7 @@ var TOOL_DEFINITIONS = [
2444
2965
  searchType: {
2445
2966
  type: "string",
2446
2967
  enum: ["name", "type", "content"],
2447
- description: "Search mode",
2448
- default: "name"
2968
+ description: "Search mode (default: name)"
2449
2969
  }
2450
2970
  },
2451
2971
  required: ["query"]
@@ -2489,8 +3009,7 @@ var TOOL_DEFINITIONS = [
2489
3009
  searchType: {
2490
3010
  type: "string",
2491
3011
  enum: ["name", "class", "property"],
2492
- description: "Search mode",
2493
- default: "name"
3012
+ description: "Search mode (default: name)"
2494
3013
  },
2495
3014
  propertyName: {
2496
3015
  type: "string",
@@ -2514,8 +3033,7 @@ var TOOL_DEFINITIONS = [
2514
3033
  },
2515
3034
  excludeSource: {
2516
3035
  type: "boolean",
2517
- description: "For scripts, return SourceLength/LineCount instead of full source (default: false)",
2518
- default: false
3036
+ description: "For scripts, return SourceLength/LineCount instead of full source (default: false)"
2519
3037
  }
2520
3038
  },
2521
3039
  required: ["instancePath"]
@@ -2580,18 +3098,15 @@ var TOOL_DEFINITIONS = [
2580
3098
  properties: {
2581
3099
  path: {
2582
3100
  type: "string",
2583
- description: "Root path (default: workspace root)",
2584
- default: ""
3101
+ description: "Root path (default: workspace root)"
2585
3102
  },
2586
3103
  maxDepth: {
2587
3104
  type: "number",
2588
- description: "Max traversal depth (default: 3)",
2589
- default: 3
3105
+ description: "Max traversal depth (default: 3)"
2590
3106
  },
2591
3107
  scriptsOnly: {
2592
3108
  type: "boolean",
2593
- description: "Show only scripts",
2594
- default: false
3109
+ description: "Show only scripts (default: false)"
2595
3110
  }
2596
3111
  }
2597
3112
  }
@@ -2613,7 +3128,7 @@ var TOOL_DEFINITIONS = [
2613
3128
  description: "Property name"
2614
3129
  },
2615
3130
  propertyValue: {
2616
- description: "Value to set"
3131
+ description: "Value to set (string, number, boolean, or object for Vector3/Color3/UDim2)"
2617
3132
  }
2618
3133
  },
2619
3134
  required: ["instancePath", "propertyName", "propertyValue"]
@@ -2636,7 +3151,7 @@ var TOOL_DEFINITIONS = [
2636
3151
  description: "Property name"
2637
3152
  },
2638
3153
  propertyValue: {
2639
- description: "Value to set"
3154
+ description: "Value to set (string, number, boolean, or object for Vector3/Color3/UDim2)"
2640
3155
  }
2641
3156
  },
2642
3157
  required: ["paths", "propertyName", "propertyValue"]
@@ -2768,22 +3283,16 @@ var TOOL_DEFINITIONS = [
2768
3283
  positionOffset: {
2769
3284
  type: "array",
2770
3285
  items: { type: "number" },
2771
- minItems: 3,
2772
- maxItems: 3,
2773
3286
  description: "X, Y, Z offset per duplicate"
2774
3287
  },
2775
3288
  rotationOffset: {
2776
3289
  type: "array",
2777
3290
  items: { type: "number" },
2778
- minItems: 3,
2779
- maxItems: 3,
2780
3291
  description: "X, Y, Z rotation offset"
2781
3292
  },
2782
3293
  scaleOffset: {
2783
3294
  type: "array",
2784
3295
  items: { type: "number" },
2785
- minItems: 3,
2786
- maxItems: 3,
2787
3296
  description: "X, Y, Z scale multiplier"
2788
3297
  },
2789
3298
  propertyVariations: {
@@ -2831,22 +3340,16 @@ var TOOL_DEFINITIONS = [
2831
3340
  positionOffset: {
2832
3341
  type: "array",
2833
3342
  items: { type: "number" },
2834
- minItems: 3,
2835
- maxItems: 3,
2836
3343
  description: "X, Y, Z offset per duplicate"
2837
3344
  },
2838
3345
  rotationOffset: {
2839
3346
  type: "array",
2840
3347
  items: { type: "number" },
2841
- minItems: 3,
2842
- maxItems: 3,
2843
3348
  description: "X, Y, Z rotation offset"
2844
3349
  },
2845
3350
  scaleOffset: {
2846
3351
  type: "array",
2847
3352
  items: { type: "number" },
2848
- minItems: 3,
2849
- maxItems: 3,
2850
3353
  description: "X, Y, Z scale multiplier"
2851
3354
  },
2852
3355
  propertyVariations: {
@@ -2920,7 +3423,7 @@ var TOOL_DEFINITIONS = [
2920
3423
  description: "Operation"
2921
3424
  },
2922
3425
  value: {
2923
- description: "Operand value"
3426
+ description: "Operand value (number or object for Vector3/UDim2 components)"
2924
3427
  },
2925
3428
  component: {
2926
3429
  type: "string",
@@ -3014,8 +3517,7 @@ var TOOL_DEFINITIONS = [
3014
3517
  },
3015
3518
  afterLine: {
3016
3519
  type: "number",
3017
- description: "Insert after this line (0 = beginning)",
3018
- default: 0
3520
+ description: "Insert after this line (0 = beginning)"
3019
3521
  },
3020
3522
  newContent: {
3021
3523
  type: "string",
@@ -3084,7 +3586,7 @@ var TOOL_DEFINITIONS = [
3084
3586
  description: "Attribute name"
3085
3587
  },
3086
3588
  attributeValue: {
3087
- description: "Value. Objects for Vector3/Color3/UDim2."
3589
+ description: "Value (string, number, boolean, or object for Vector3/Color3/UDim2)"
3088
3590
  },
3089
3591
  valueType: {
3090
3592
  type: "string",
@@ -3218,6 +3720,10 @@ var TOOL_DEFINITIONS = [
3218
3720
  code: {
3219
3721
  type: "string",
3220
3722
  description: "Luau code to execute"
3723
+ },
3724
+ target: {
3725
+ type: "string",
3726
+ description: 'Instance target: "edit" (default), "server", "client-1", "client-2", etc.'
3221
3727
  }
3222
3728
  },
3223
3729
  required: ["code"]
@@ -3237,23 +3743,19 @@ var TOOL_DEFINITIONS = [
3237
3743
  },
3238
3744
  caseSensitive: {
3239
3745
  type: "boolean",
3240
- description: "Case-sensitive search (default: false)",
3241
- default: false
3746
+ description: "Case-sensitive search (default: false)"
3242
3747
  },
3243
3748
  usePattern: {
3244
3749
  type: "boolean",
3245
- description: "Use Lua pattern matching instead of literal (default: false)",
3246
- default: false
3750
+ description: "Use Lua pattern matching instead of literal (default: false)"
3247
3751
  },
3248
3752
  contextLines: {
3249
3753
  type: "number",
3250
- description: "Number of context lines before/after each match (like rg -C)",
3251
- default: 0
3754
+ description: "Number of context lines before/after each match (default: 0)"
3252
3755
  },
3253
3756
  maxResults: {
3254
3757
  type: "number",
3255
- description: "Max total matches before stopping (default: 100)",
3256
- default: 100
3758
+ description: "Max total matches before stopping (default: 100)"
3257
3759
  },
3258
3760
  maxResultsPerScript: {
3259
3761
  type: "number",
@@ -3261,8 +3763,7 @@ var TOOL_DEFINITIONS = [
3261
3763
  },
3262
3764
  filesOnly: {
3263
3765
  type: "boolean",
3264
- description: "Only return matching script paths, not line details (like rg -l)",
3265
- default: false
3766
+ description: "Only return matching script paths, not line details (default: false)"
3266
3767
  },
3267
3768
  path: {
3268
3769
  type: "string",
@@ -3281,7 +3782,7 @@ var TOOL_DEFINITIONS = [
3281
3782
  {
3282
3783
  name: "start_playtest",
3283
3784
  category: "read",
3284
- description: "Start playtest. Captures print/warn/error via LogService. Poll with get_playtest_output, end with stop_playtest.",
3785
+ 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).",
3285
3786
  inputSchema: {
3286
3787
  type: "object",
3287
3788
  properties: {
@@ -3289,6 +3790,10 @@ var TOOL_DEFINITIONS = [
3289
3790
  type: "string",
3290
3791
  enum: ["play", "run"],
3291
3792
  description: "Play mode"
3793
+ },
3794
+ numPlayers: {
3795
+ type: "number",
3796
+ description: "Number of client players (1-8). Triggers server + clients mode via TestService."
3292
3797
  }
3293
3798
  },
3294
3799
  required: ["mode"]
@@ -3307,6 +3812,21 @@ var TOOL_DEFINITIONS = [
3307
3812
  name: "get_playtest_output",
3308
3813
  category: "read",
3309
3814
  description: "Poll output buffer without stopping. Returns isRunning and captured messages.",
3815
+ inputSchema: {
3816
+ type: "object",
3817
+ properties: {
3818
+ target: {
3819
+ type: "string",
3820
+ description: 'Instance target: "edit" (default), "server", "client-1", "client-2", etc.'
3821
+ }
3822
+ }
3823
+ }
3824
+ },
3825
+ // === Multi-Instance ===
3826
+ {
3827
+ name: "get_connected_instances",
3828
+ category: "read",
3829
+ description: "List all connected plugin instances with their roles. Use during multi-client playtest to discover server and client instances for targeted commands.",
3310
3830
  inputSchema: {
3311
3831
  type: "object",
3312
3832
  properties: {}
@@ -3350,8 +3870,7 @@ var TOOL_DEFINITIONS = [
3350
3870
  style: {
3351
3871
  type: "string",
3352
3872
  enum: ["medieval", "modern", "nature", "scifi", "misc"],
3353
- description: "Style category for the build",
3354
- default: "misc"
3873
+ description: "Style category for the build (default: misc)"
3355
3874
  }
3356
3875
  },
3357
3876
  required: ["instancePath"]
@@ -3382,7 +3901,6 @@ var TOOL_DEFINITIONS = [
3382
3901
  description: "Array of part arrays. Each: [posX, posY, posZ, sizeX, sizeY, sizeZ, rotX, rotY, rotZ, paletteKey, shape?, transparency?]. Shapes: Block (default), Wedge, Cylinder, Ball, CornerWedge.",
3383
3902
  items: {
3384
3903
  type: "array",
3385
- minItems: 10,
3386
3904
  items: {
3387
3905
  anyOf: [{ type: "number" }, { type: "string" }]
3388
3906
  }
@@ -3391,8 +3909,6 @@ var TOOL_DEFINITIONS = [
3391
3909
  bounds: {
3392
3910
  type: "array",
3393
3911
  items: { type: "number" },
3394
- minItems: 3,
3395
- maxItems: 3,
3396
3912
  description: "Optional bounding box [X, Y, Z]. Auto-computed if omitted."
3397
3913
  }
3398
3914
  },
@@ -3487,8 +4003,6 @@ part(0,2,0,2,1,1,"b")`,
3487
4003
  position: {
3488
4004
  type: "array",
3489
4005
  items: { type: "number" },
3490
- minItems: 3,
3491
- maxItems: 3,
3492
4006
  description: "World position offset [X, Y, Z]"
3493
4007
  }
3494
4008
  },
@@ -3523,8 +4037,7 @@ part(0,2,0,2,1,1,"b")`,
3523
4037
  },
3524
4038
  maxResults: {
3525
4039
  type: "number",
3526
- description: "Max results to return (default: 50)",
3527
- default: 50
4040
+ description: "Max results to return (default: 50)"
3528
4041
  }
3529
4042
  }
3530
4043
  }
@@ -3570,38 +4083,28 @@ part(0,2,0,2,1,1,"b")`,
3570
4083
  required: ["modelKey", "position"],
3571
4084
  properties: {
3572
4085
  modelKey: {
3573
- type: "string",
3574
- minLength: 1
4086
+ type: "string"
3575
4087
  },
3576
4088
  position: {
3577
4089
  type: "array",
3578
- items: { type: "number" },
3579
- minItems: 3,
3580
- maxItems: 3
4090
+ items: { type: "number" }
3581
4091
  },
3582
4092
  rotation: {
3583
4093
  type: "array",
3584
- items: { type: "number" },
3585
- minItems: 3,
3586
- maxItems: 3
4094
+ items: { type: "number" }
3587
4095
  }
3588
4096
  }
3589
4097
  },
3590
4098
  {
3591
4099
  type: "array",
3592
- minItems: 2,
3593
- maxItems: 3,
3594
4100
  items: {
3595
4101
  anyOf: [
3596
4102
  {
3597
- type: "string",
3598
- minLength: 1
4103
+ type: "string"
3599
4104
  },
3600
4105
  {
3601
4106
  type: "array",
3602
- items: { type: "number" },
3603
- minItems: 3,
3604
- maxItems: 3
4107
+ items: { type: "number" }
3605
4108
  }
3606
4109
  ]
3607
4110
  }
@@ -3618,8 +4121,7 @@ part(0,2,0,2,1,1,"b")`,
3618
4121
  },
3619
4122
  targetPath: {
3620
4123
  type: "string",
3621
- description: "Parent instance path for the scene (default: game.Workspace)",
3622
- default: "game.Workspace"
4124
+ description: "Parent instance path for the scene (default: game.Workspace)"
3623
4125
  }
3624
4126
  },
3625
4127
  required: ["sceneData"]
@@ -3644,19 +4146,16 @@ part(0,2,0,2,1,1,"b")`,
3644
4146
  },
3645
4147
  maxResults: {
3646
4148
  type: "number",
3647
- description: "Max results to return (default: 25)",
3648
- default: 25
4149
+ description: "Max results to return (default: 25)"
3649
4150
  },
3650
4151
  sortBy: {
3651
4152
  type: "string",
3652
4153
  enum: ["Relevance", "Trending", "Top", "AudioDuration", "CreateTime", "UpdatedTime", "Ratings"],
3653
- description: "Sort order (default: Relevance)",
3654
- default: "Relevance"
4154
+ description: "Sort order (default: Relevance)"
3655
4155
  },
3656
4156
  verifiedCreatorsOnly: {
3657
4157
  type: "boolean",
3658
- description: "Only show assets from verified creators",
3659
- default: false
4158
+ description: "Only show assets from verified creators (default: false)"
3660
4159
  }
3661
4160
  },
3662
4161
  required: ["assetType"]
@@ -3691,8 +4190,7 @@ part(0,2,0,2,1,1,"b")`,
3691
4190
  size: {
3692
4191
  type: "string",
3693
4192
  enum: ["150x150", "420x420", "768x432"],
3694
- description: "Thumbnail size (default: 420x420)",
3695
- default: "420x420"
4193
+ description: "Thumbnail size (default: 420x420)"
3696
4194
  }
3697
4195
  },
3698
4196
  required: ["assetId"]
@@ -3711,8 +4209,7 @@ part(0,2,0,2,1,1,"b")`,
3711
4209
  },
3712
4210
  parentPath: {
3713
4211
  type: "string",
3714
- description: "Parent instance path (default: game.Workspace)",
3715
- default: "game.Workspace"
4212
+ description: "Parent instance path (default: game.Workspace)"
3716
4213
  },
3717
4214
  position: {
3718
4215
  type: "object",
@@ -3740,18 +4237,180 @@ part(0,2,0,2,1,1,"b")`,
3740
4237
  },
3741
4238
  includeProperties: {
3742
4239
  type: "boolean",
3743
- description: "Include detailed properties for each instance (default: true)",
3744
- default: true
4240
+ description: "Include detailed properties for each instance (default: true)"
3745
4241
  },
3746
4242
  maxDepth: {
3747
4243
  type: "number",
3748
- description: "Max hierarchy traversal depth (default: 10)",
3749
- default: 10
4244
+ description: "Max hierarchy traversal depth (default: 10)"
3750
4245
  }
3751
4246
  },
3752
4247
  required: ["assetId"]
3753
4248
  }
3754
4249
  },
4250
+ {
4251
+ name: "render_object_screenshot",
4252
+ category: "write",
4253
+ description: "Stage a renderable Studio object by instance path, frame it with a temporary camera, capture a screenshot, and optionally save the PNG to disk. Supports Models and BaseParts such as MeshPart.",
4254
+ inputSchema: {
4255
+ type: "object",
4256
+ properties: {
4257
+ instancePath: {
4258
+ type: "string",
4259
+ description: "Target Studio instance path. Must resolve to a Model or BasePart such as MeshPart."
4260
+ },
4261
+ cameraPreset: {
4262
+ type: "string",
4263
+ enum: ["front", "isometric", "top", "icon"],
4264
+ description: "Camera angle preset (default: isometric)"
4265
+ },
4266
+ padding: {
4267
+ type: "number",
4268
+ description: "Framing multiplier applied to the object bounds (default: 1.35)"
4269
+ },
4270
+ backdropColor: {
4271
+ type: "array",
4272
+ items: { type: "number" },
4273
+ description: "Solid RGB backdrop color as [r, g, b] (default: [0,255,0])"
4274
+ },
4275
+ savePath: {
4276
+ type: "string",
4277
+ description: "Optional filesystem path where the PNG should be written"
4278
+ },
4279
+ outputDir: {
4280
+ type: "string",
4281
+ description: "Optional filesystem directory where the PNG should be written with an auto-generated filename"
4282
+ },
4283
+ fileName: {
4284
+ type: "string",
4285
+ description: "Optional file name to use when outputDir is provided (default: derived from the object name)"
4286
+ },
4287
+ returnImage: {
4288
+ type: "boolean",
4289
+ description: "Return the PNG image content in the MCP response (default: true)"
4290
+ }
4291
+ },
4292
+ required: ["instancePath"]
4293
+ }
4294
+ },
4295
+ {
4296
+ name: "render_model_screenshot",
4297
+ category: "write",
4298
+ description: "Deprecated alias for render_object_screenshot. Stages a Model or BasePart from Studio by instance path, frames it with a temporary camera, captures a screenshot, and optionally saves the PNG to disk.",
4299
+ inputSchema: {
4300
+ type: "object",
4301
+ properties: {
4302
+ instancePath: {
4303
+ type: "string",
4304
+ description: "Target Studio instance path. Must resolve to a Model or BasePart such as MeshPart."
4305
+ },
4306
+ cameraPreset: {
4307
+ type: "string",
4308
+ enum: ["front", "isometric", "top", "icon"],
4309
+ description: "Camera angle preset (default: isometric)"
4310
+ },
4311
+ padding: {
4312
+ type: "number",
4313
+ description: "Framing multiplier applied to the model bounds (default: 1.35)"
4314
+ },
4315
+ backdropColor: {
4316
+ type: "array",
4317
+ items: { type: "number" },
4318
+ description: "Solid RGB backdrop color as [r, g, b] (default: [0,255,0])"
4319
+ },
4320
+ savePath: {
4321
+ type: "string",
4322
+ description: "Optional filesystem path where the PNG should be written"
4323
+ },
4324
+ outputDir: {
4325
+ type: "string",
4326
+ description: "Optional filesystem directory where the PNG should be written with an auto-generated filename"
4327
+ },
4328
+ fileName: {
4329
+ type: "string",
4330
+ description: "Optional file name to use when outputDir is provided (default: derived from the object name)"
4331
+ },
4332
+ returnImage: {
4333
+ type: "boolean",
4334
+ description: "Return the PNG image content in the MCP response (default: true)"
4335
+ }
4336
+ },
4337
+ required: ["instancePath"]
4338
+ }
4339
+ },
4340
+ {
4341
+ name: "batch_render_objects",
4342
+ category: "write",
4343
+ description: "Render all direct or descendant renderable objects under a Studio path and save PNGs plus a manifest to an output directory. Supports Models and BaseParts such as MeshPart.",
4344
+ inputSchema: {
4345
+ type: "object",
4346
+ properties: {
4347
+ parentPath: {
4348
+ type: "string",
4349
+ description: "Studio path whose child or descendant Models/BaseParts should be rendered"
4350
+ },
4351
+ outputDir: {
4352
+ type: "string",
4353
+ description: "Filesystem directory for generated PNGs and manifest JSON"
4354
+ },
4355
+ recursive: {
4356
+ type: "boolean",
4357
+ description: "Recursively traverse nested folders/models (default: false)"
4358
+ },
4359
+ cameraPreset: {
4360
+ type: "string",
4361
+ enum: ["front", "isometric", "top", "icon"],
4362
+ description: "Camera angle preset used for each render (default: isometric)"
4363
+ },
4364
+ padding: {
4365
+ type: "number",
4366
+ description: "Framing multiplier applied to each object bounds (default: 1.35)"
4367
+ },
4368
+ backdropColor: {
4369
+ type: "array",
4370
+ items: { type: "number" },
4371
+ description: "Solid RGB backdrop color as [r, g, b] (default: [0,255,0])"
4372
+ }
4373
+ },
4374
+ required: ["parentPath", "outputDir"]
4375
+ }
4376
+ },
4377
+ {
4378
+ name: "batch_render_models",
4379
+ category: "write",
4380
+ description: "Deprecated alias for batch_render_objects. Renders child Models/BaseParts under a Studio path and saves PNGs plus a manifest to an output directory.",
4381
+ inputSchema: {
4382
+ type: "object",
4383
+ properties: {
4384
+ parentPath: {
4385
+ type: "string",
4386
+ description: "Studio path whose child or descendant Models/BaseParts should be rendered"
4387
+ },
4388
+ outputDir: {
4389
+ type: "string",
4390
+ description: "Filesystem directory for generated PNGs and manifest JSON"
4391
+ },
4392
+ recursive: {
4393
+ type: "boolean",
4394
+ description: "Recursively traverse nested folders/models (default: false)"
4395
+ },
4396
+ cameraPreset: {
4397
+ type: "string",
4398
+ enum: ["front", "isometric", "top", "icon"],
4399
+ description: "Camera angle preset used for each render (default: isometric)"
4400
+ },
4401
+ padding: {
4402
+ type: "number",
4403
+ description: "Framing multiplier applied to each model bounds (default: 1.35)"
4404
+ },
4405
+ backdropColor: {
4406
+ type: "array",
4407
+ items: { type: "number" },
4408
+ description: "Solid RGB backdrop color as [r, g, b] (default: [0,255,0])"
4409
+ }
4410
+ },
4411
+ required: ["parentPath", "outputDir"]
4412
+ }
4413
+ },
3755
4414
  {
3756
4415
  name: "capture_screenshot",
3757
4416
  category: "read",
@@ -3760,6 +4419,150 @@ part(0,2,0,2,1,1,"b")`,
3760
4419
  type: "object",
3761
4420
  properties: {}
3762
4421
  }
4422
+ },
4423
+ // === Input Simulation ===
4424
+ {
4425
+ name: "simulate_mouse_input",
4426
+ category: "write",
4427
+ 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.",
4428
+ inputSchema: {
4429
+ type: "object",
4430
+ properties: {
4431
+ action: {
4432
+ type: "string",
4433
+ enum: ["click", "mouseDown", "mouseUp", "move", "scroll"],
4434
+ description: 'Mouse action to perform. "click" does mouseDown + short delay + mouseUp.'
4435
+ },
4436
+ x: {
4437
+ type: "number",
4438
+ description: "Viewport pixel X coordinate"
4439
+ },
4440
+ y: {
4441
+ type: "number",
4442
+ description: "Viewport pixel Y coordinate"
4443
+ },
4444
+ button: {
4445
+ type: "string",
4446
+ enum: ["Left", "Right", "Middle"],
4447
+ description: "Mouse button (default: Left)"
4448
+ },
4449
+ scrollDirection: {
4450
+ type: "string",
4451
+ enum: ["up", "down"],
4452
+ description: 'Scroll direction (only for "scroll" action)'
4453
+ },
4454
+ target: {
4455
+ type: "string",
4456
+ description: 'Instance target: "edit" (default), "server", "client-1", "client-2", etc.'
4457
+ }
4458
+ },
4459
+ required: ["action", "x", "y"]
4460
+ }
4461
+ },
4462
+ {
4463
+ name: "simulate_keyboard_input",
4464
+ category: "write",
4465
+ 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.',
4466
+ inputSchema: {
4467
+ type: "object",
4468
+ properties: {
4469
+ keyCode: {
4470
+ type: "string",
4471
+ description: 'Enum.KeyCode name: "W", "A", "S", "D", "Space", "E", "F", "LeftShift", "LeftControl", "Return", "Tab", "Escape", "One", "Two", etc.'
4472
+ },
4473
+ action: {
4474
+ type: "string",
4475
+ enum: ["press", "release", "tap"],
4476
+ description: '"tap" (default) = press + wait + release. "press" = key down only. "release" = key up only.'
4477
+ },
4478
+ duration: {
4479
+ type: "number",
4480
+ description: 'Hold duration in seconds for "tap" action (default: 0.1). Use longer values for sustained input like walking.'
4481
+ },
4482
+ target: {
4483
+ type: "string",
4484
+ description: 'Instance target: "edit" (default), "server", "client-1", "client-2", etc.'
4485
+ }
4486
+ },
4487
+ required: ["keyCode"]
4488
+ }
4489
+ },
4490
+ // === Character Navigation ===
4491
+ {
4492
+ name: "character_navigation",
4493
+ category: "write",
4494
+ 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 \u2014 moves the character directly.',
4495
+ inputSchema: {
4496
+ type: "object",
4497
+ properties: {
4498
+ position: {
4499
+ type: "array",
4500
+ items: { type: "number" },
4501
+ description: "Target world position [x, y, z]. Either this or instancePath is required."
4502
+ },
4503
+ instancePath: {
4504
+ type: "string",
4505
+ description: "Instance to navigate to (dot notation). The character walks to its Position. Either this or position is required."
4506
+ },
4507
+ waitForCompletion: {
4508
+ type: "boolean",
4509
+ description: "Wait for the character to arrive before returning (default: true)"
4510
+ },
4511
+ timeout: {
4512
+ type: "number",
4513
+ description: "Max seconds to wait for navigation to complete (default: 25)"
4514
+ },
4515
+ target: {
4516
+ type: "string",
4517
+ description: 'Instance target: "edit" (default), "server", "client-1", "client-2", etc.'
4518
+ }
4519
+ }
4520
+ }
4521
+ },
4522
+ // === Find and Replace ===
4523
+ {
4524
+ name: "find_and_replace_in_scripts",
4525
+ category: "write",
4526
+ 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.",
4527
+ inputSchema: {
4528
+ type: "object",
4529
+ properties: {
4530
+ pattern: {
4531
+ type: "string",
4532
+ description: "Text or Lua pattern to find"
4533
+ },
4534
+ replacement: {
4535
+ type: "string",
4536
+ description: "Replacement text. When usePattern is true, supports Lua captures (%1, %2, etc.)."
4537
+ },
4538
+ caseSensitive: {
4539
+ type: "boolean",
4540
+ description: "Case-sensitive matching (default: false). Must be true when usePattern is true."
4541
+ },
4542
+ usePattern: {
4543
+ type: "boolean",
4544
+ description: "Use Lua pattern matching instead of literal (default: false). Requires caseSensitive: true."
4545
+ },
4546
+ path: {
4547
+ type: "string",
4548
+ description: 'Limit scope to a subtree (e.g. "game.ServerScriptService")'
4549
+ },
4550
+ classFilter: {
4551
+ type: "string",
4552
+ enum: ["Script", "LocalScript", "ModuleScript"],
4553
+ description: "Only search scripts of this class type"
4554
+ },
4555
+ dryRun: {
4556
+ type: "boolean",
4557
+ description: "Preview changes without applying them (default: false)"
4558
+ },
4559
+ maxReplacements: {
4560
+ type: "number",
4561
+ description: "Safety limit on total replacements (default: 1000)"
4562
+ }
4563
+ },
4564
+ required: ["pattern", "replacement"]
4565
+ }
3763
4566
  }
3764
4567
  ];
3765
4568
  var getAllTools = () => [...TOOL_DEFINITIONS];