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 +951 -148
- package/package.json +2 -2
- package/studio-plugin/MCPPlugin.rbxmx +248 -13
- package/studio-plugin/bash.exe.stackdump +37 -0
- package/studio-plugin/src/modules/Communication.ts +29 -6
- package/studio-plugin/src/modules/State.ts +1 -0
- package/studio-plugin/src/modules/UI.ts +110 -24
- package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +44 -34
- package/studio-plugin/src/modules/handlers/RenderHandlers.ts +194 -0
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +10 -1
- package/studio-plugin/src/types/index.d.ts +5 -0
package/dist/index.js
CHANGED
|
@@ -99,15 +99,18 @@ var init_install_plugin = __esm({
|
|
|
99
99
|
}
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
272
|
+
pendingRequests: bridge.getPendingRequestCount(),
|
|
273
|
+
proxyInstanceCount: proxyInstances.size,
|
|
274
|
+
streamableHttp: !!serverConfig
|
|
221
275
|
});
|
|
222
276
|
});
|
|
223
277
|
app.post("/ready", (req, res) => {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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:
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
909
|
-
import { deflateSync
|
|
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)
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
2819
|
+
throw new McpError2(ErrorCode2.MethodNotFound, `Unknown tool: ${name}`);
|
|
2302
2820
|
}
|
|
2303
2821
|
} catch (error) {
|
|
2304
|
-
if (error instanceof
|
|
2822
|
+
if (error instanceof McpError2)
|
|
2305
2823
|
throw error;
|
|
2306
|
-
throw new
|
|
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
|
-
//
|
|
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
|
|
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 (
|
|
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 (
|
|
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];
|