robloxstudio-mcp 2.6.0-next.1 → 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 +281 -60
- package/package.json +48 -48
- package/studio-plugin/bash.exe.stackdump +37 -0
- package/studio-plugin/src/modules/Communication.ts +38 -17
- 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 +53 -48
- package/studio-plugin/src/modules/handlers/RenderHandlers.ts +194 -250
- 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
|
@@ -100,14 +100,17 @@ var init_install_plugin = __esm({
|
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
// ../core/dist/server.js
|
|
103
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.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),
|
|
@@ -204,15 +208,24 @@ var TOOL_HANDLERS = {
|
|
|
204
208
|
padding: body.padding,
|
|
205
209
|
backdropColor: body.backdropColor
|
|
206
210
|
}),
|
|
207
|
-
capture_screenshot: (tools) => tools.captureScreenshot()
|
|
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
|
+
})
|
|
208
223
|
};
|
|
209
|
-
function createHttpServer(tools, bridge, allowedTools) {
|
|
224
|
+
function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
210
225
|
const app = express();
|
|
211
|
-
let pluginConnected = false;
|
|
212
226
|
let mcpServerActive = false;
|
|
213
227
|
let lastMCPActivity = 0;
|
|
214
228
|
let mcpServerStartTime = 0;
|
|
215
|
-
let lastPluginActivity = 0;
|
|
216
229
|
const proxyInstances = /* @__PURE__ */ new Set();
|
|
217
230
|
const setMCPServerActive = (active) => {
|
|
218
231
|
mcpServerActive = active;
|
|
@@ -235,44 +248,78 @@ function createHttpServer(tools, bridge, allowedTools) {
|
|
|
235
248
|
return Date.now() - lastMCPActivity < 3e4;
|
|
236
249
|
};
|
|
237
250
|
const isPluginConnected = () => {
|
|
238
|
-
return
|
|
251
|
+
return bridge.getInstances().length > 0;
|
|
239
252
|
};
|
|
240
253
|
app.use(cors());
|
|
241
254
|
app.use(express.json({ limit: "50mb" }));
|
|
242
255
|
app.use(express.urlencoded({ limit: "50mb", extended: true }));
|
|
243
256
|
app.get("/health", (req, res) => {
|
|
257
|
+
const instances = bridge.getInstances();
|
|
244
258
|
res.json({
|
|
245
259
|
status: "ok",
|
|
246
260
|
service: "robloxstudio-mcp",
|
|
247
|
-
|
|
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
|
+
})),
|
|
248
270
|
mcpServerActive: isMCPServerActive(),
|
|
249
271
|
uptime: mcpServerActive ? Date.now() - mcpServerStartTime : 0,
|
|
250
|
-
|
|
272
|
+
pendingRequests: bridge.getPendingRequestCount(),
|
|
273
|
+
proxyInstanceCount: proxyInstances.size,
|
|
274
|
+
streamableHttp: !!serverConfig
|
|
251
275
|
});
|
|
252
276
|
});
|
|
253
277
|
app.post("/ready", (req, res) => {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
+
}
|
|
257
286
|
});
|
|
258
287
|
app.post("/disconnect", (req, res) => {
|
|
259
|
-
|
|
260
|
-
|
|
288
|
+
const { instanceId } = req.body;
|
|
289
|
+
if (instanceId) {
|
|
290
|
+
bridge.unregisterInstance(instanceId);
|
|
291
|
+
} else {
|
|
292
|
+
bridge.unregisterInstance("legacy");
|
|
293
|
+
bridge.clearAllPendingRequests();
|
|
294
|
+
}
|
|
261
295
|
res.json({ success: true });
|
|
262
296
|
});
|
|
263
297
|
app.get("/status", (req, res) => {
|
|
298
|
+
const instances = bridge.getInstances();
|
|
264
299
|
res.json({
|
|
265
|
-
pluginConnected:
|
|
300
|
+
pluginConnected: instances.length > 0,
|
|
301
|
+
instanceCount: instances.length,
|
|
302
|
+
instances: instances.map((i) => ({ instanceId: i.instanceId, role: i.role })),
|
|
266
303
|
mcpServerActive: isMCPServerActive(),
|
|
267
304
|
lastMCPActivity,
|
|
268
305
|
uptime: mcpServerActive ? Date.now() - mcpServerStartTime : 0
|
|
269
306
|
});
|
|
270
307
|
});
|
|
308
|
+
app.get("/instances", (req, res) => {
|
|
309
|
+
res.json({ instances: bridge.getInstances() });
|
|
310
|
+
});
|
|
271
311
|
app.get("/poll", (req, res) => {
|
|
272
|
-
|
|
273
|
-
|
|
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
|
+
}
|
|
274
322
|
}
|
|
275
|
-
lastPluginActivity = Date.now();
|
|
276
323
|
if (!isMCPServerActive()) {
|
|
277
324
|
res.status(503).json({
|
|
278
325
|
error: "MCP server not connected",
|
|
@@ -282,7 +329,7 @@ function createHttpServer(tools, bridge, allowedTools) {
|
|
|
282
329
|
});
|
|
283
330
|
return;
|
|
284
331
|
}
|
|
285
|
-
const pendingRequest = bridge.getPendingRequest();
|
|
332
|
+
const pendingRequest = bridge.getPendingRequest(callerRole);
|
|
286
333
|
if (pendingRequest) {
|
|
287
334
|
res.json({
|
|
288
335
|
request: pendingRequest.request,
|
|
@@ -310,7 +357,7 @@ function createHttpServer(tools, bridge, allowedTools) {
|
|
|
310
357
|
res.json({ success: true });
|
|
311
358
|
});
|
|
312
359
|
app.post("/proxy", async (req, res) => {
|
|
313
|
-
const { endpoint, data, proxyInstanceId } = req.body;
|
|
360
|
+
const { endpoint, data, target, proxyInstanceId } = req.body;
|
|
314
361
|
if (!endpoint) {
|
|
315
362
|
res.status(400).json({ error: "endpoint is required" });
|
|
316
363
|
return;
|
|
@@ -319,12 +366,76 @@ function createHttpServer(tools, bridge, allowedTools) {
|
|
|
319
366
|
proxyInstances.add(proxyInstanceId);
|
|
320
367
|
}
|
|
321
368
|
try {
|
|
322
|
-
const response = await bridge.sendRequest(endpoint, data);
|
|
369
|
+
const response = await bridge.sendRequest(endpoint, data, target || "edit");
|
|
323
370
|
res.json({ response });
|
|
324
371
|
} catch (err) {
|
|
325
372
|
res.status(500).json({ error: err.message || "Proxy request failed" });
|
|
326
373
|
}
|
|
327
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
|
+
}
|
|
328
439
|
app.use("/mcp/*", (req, res, next) => {
|
|
329
440
|
trackMCPActivity();
|
|
330
441
|
next();
|
|
@@ -388,9 +499,9 @@ var StudioHttpClient = class {
|
|
|
388
499
|
constructor(bridge) {
|
|
389
500
|
this.bridge = bridge;
|
|
390
501
|
}
|
|
391
|
-
async request(endpoint, data) {
|
|
502
|
+
async request(endpoint, data, target = "edit") {
|
|
392
503
|
try {
|
|
393
|
-
const response = await this.bridge.sendRequest(endpoint, data);
|
|
504
|
+
const response = await this.bridge.sendRequest(endpoint, data, target);
|
|
394
505
|
return response;
|
|
395
506
|
} catch (error) {
|
|
396
507
|
if (error instanceof Error && error.message === "Request timeout") {
|
|
@@ -1027,9 +1138,11 @@ function resolveSingleRenderSavePath(instancePath, instanceName, options) {
|
|
|
1027
1138
|
}
|
|
1028
1139
|
var RobloxStudioTools = class _RobloxStudioTools {
|
|
1029
1140
|
client;
|
|
1141
|
+
bridge;
|
|
1030
1142
|
openCloudClient;
|
|
1031
1143
|
constructor(bridge) {
|
|
1032
1144
|
this.client = new StudioHttpClient(bridge);
|
|
1145
|
+
this.bridge = bridge;
|
|
1033
1146
|
this.openCloudClient = new OpenCloudClient();
|
|
1034
1147
|
}
|
|
1035
1148
|
async getFileTree(path2 = "") {
|
|
@@ -1548,11 +1661,11 @@ var RobloxStudioTools = class _RobloxStudioTools {
|
|
|
1548
1661
|
]
|
|
1549
1662
|
};
|
|
1550
1663
|
}
|
|
1551
|
-
async executeLuau(code) {
|
|
1664
|
+
async executeLuau(code, target) {
|
|
1552
1665
|
if (!code) {
|
|
1553
1666
|
throw new Error("Code is required for execute_luau");
|
|
1554
1667
|
}
|
|
1555
|
-
const response = await this.client.request("/api/execute-luau", { code });
|
|
1668
|
+
const response = await this.client.request("/api/execute-luau", { code }, target || "edit");
|
|
1556
1669
|
return {
|
|
1557
1670
|
content: [
|
|
1558
1671
|
{
|
|
@@ -1562,11 +1675,15 @@ var RobloxStudioTools = class _RobloxStudioTools {
|
|
|
1562
1675
|
]
|
|
1563
1676
|
};
|
|
1564
1677
|
}
|
|
1565
|
-
async startPlaytest(mode) {
|
|
1678
|
+
async startPlaytest(mode, numPlayers) {
|
|
1566
1679
|
if (mode !== "play" && mode !== "run") {
|
|
1567
1680
|
throw new Error('mode must be "play" or "run"');
|
|
1568
1681
|
}
|
|
1569
|
-
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);
|
|
1570
1687
|
return {
|
|
1571
1688
|
content: [
|
|
1572
1689
|
{
|
|
@@ -1587,8 +1704,8 @@ var RobloxStudioTools = class _RobloxStudioTools {
|
|
|
1587
1704
|
]
|
|
1588
1705
|
};
|
|
1589
1706
|
}
|
|
1590
|
-
async getPlaytestOutput() {
|
|
1591
|
-
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");
|
|
1592
1709
|
return {
|
|
1593
1710
|
content: [
|
|
1594
1711
|
{
|
|
@@ -1598,6 +1715,17 @@ var RobloxStudioTools = class _RobloxStudioTools {
|
|
|
1598
1715
|
]
|
|
1599
1716
|
};
|
|
1600
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
|
+
}
|
|
1601
1729
|
async undo() {
|
|
1602
1730
|
const response = await this.client.request("/api/undo", {});
|
|
1603
1731
|
return {
|
|
@@ -2209,7 +2337,7 @@ var RobloxStudioTools = class _RobloxStudioTools {
|
|
|
2209
2337
|
async batchRenderModels(parentPath, outputDir, options) {
|
|
2210
2338
|
return this.batchRenderObjects(parentPath, outputDir, options);
|
|
2211
2339
|
}
|
|
2212
|
-
async simulateMouseInput(action, x, y, button, scrollDirection) {
|
|
2340
|
+
async simulateMouseInput(action, x, y, button, scrollDirection, target) {
|
|
2213
2341
|
if (!action) {
|
|
2214
2342
|
throw new Error("action is required for simulate_mouse_input");
|
|
2215
2343
|
}
|
|
@@ -2219,7 +2347,7 @@ var RobloxStudioTools = class _RobloxStudioTools {
|
|
|
2219
2347
|
y,
|
|
2220
2348
|
button,
|
|
2221
2349
|
scrollDirection
|
|
2222
|
-
});
|
|
2350
|
+
}, target || "edit");
|
|
2223
2351
|
return {
|
|
2224
2352
|
content: [{
|
|
2225
2353
|
type: "text",
|
|
@@ -2227,7 +2355,7 @@ var RobloxStudioTools = class _RobloxStudioTools {
|
|
|
2227
2355
|
}]
|
|
2228
2356
|
};
|
|
2229
2357
|
}
|
|
2230
|
-
async simulateKeyboardInput(keyCode, action, duration) {
|
|
2358
|
+
async simulateKeyboardInput(keyCode, action, duration, target) {
|
|
2231
2359
|
if (!keyCode) {
|
|
2232
2360
|
throw new Error("keyCode is required for simulate_keyboard_input");
|
|
2233
2361
|
}
|
|
@@ -2235,7 +2363,7 @@ var RobloxStudioTools = class _RobloxStudioTools {
|
|
|
2235
2363
|
keyCode,
|
|
2236
2364
|
action,
|
|
2237
2365
|
duration
|
|
2238
|
-
});
|
|
2366
|
+
}, target || "edit");
|
|
2239
2367
|
return {
|
|
2240
2368
|
content: [{
|
|
2241
2369
|
type: "text",
|
|
@@ -2243,7 +2371,7 @@ var RobloxStudioTools = class _RobloxStudioTools {
|
|
|
2243
2371
|
}]
|
|
2244
2372
|
};
|
|
2245
2373
|
}
|
|
2246
|
-
async characterNavigation(position, instancePath, waitForCompletion, timeout) {
|
|
2374
|
+
async characterNavigation(position, instancePath, waitForCompletion, timeout, target) {
|
|
2247
2375
|
if (!position && !instancePath) {
|
|
2248
2376
|
throw new Error("Either position or instancePath is required for character_navigation");
|
|
2249
2377
|
}
|
|
@@ -2252,7 +2380,7 @@ var RobloxStudioTools = class _RobloxStudioTools {
|
|
|
2252
2380
|
instancePath,
|
|
2253
2381
|
waitForCompletion,
|
|
2254
2382
|
timeout
|
|
2255
|
-
});
|
|
2383
|
+
}, target || "edit");
|
|
2256
2384
|
return {
|
|
2257
2385
|
content: [{
|
|
2258
2386
|
type: "text",
|
|
@@ -2302,10 +2430,59 @@ var RobloxStudioTools = class _RobloxStudioTools {
|
|
|
2302
2430
|
|
|
2303
2431
|
// ../core/dist/bridge-service.js
|
|
2304
2432
|
import { v4 as uuidv4 } from "uuid";
|
|
2433
|
+
var STALE_INSTANCE_MS = 3e4;
|
|
2305
2434
|
var BridgeService = class {
|
|
2306
2435
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
2436
|
+
instances = /* @__PURE__ */ new Map();
|
|
2437
|
+
nextClientIndex = 1;
|
|
2307
2438
|
requestTimeout = 3e4;
|
|
2308
|
-
|
|
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") {
|
|
2309
2486
|
const requestId = uuidv4();
|
|
2310
2487
|
return new Promise((resolve, reject) => {
|
|
2311
2488
|
const timeoutId = setTimeout(() => {
|
|
@@ -2318,6 +2495,7 @@ var BridgeService = class {
|
|
|
2318
2495
|
id: requestId,
|
|
2319
2496
|
endpoint,
|
|
2320
2497
|
data,
|
|
2498
|
+
target,
|
|
2321
2499
|
timestamp: Date.now(),
|
|
2322
2500
|
resolve,
|
|
2323
2501
|
reject,
|
|
@@ -2326,9 +2504,11 @@ var BridgeService = class {
|
|
|
2326
2504
|
this.pendingRequests.set(requestId, request);
|
|
2327
2505
|
});
|
|
2328
2506
|
}
|
|
2329
|
-
getPendingRequest() {
|
|
2507
|
+
getPendingRequest(callerRole = "edit") {
|
|
2330
2508
|
let oldestRequest = null;
|
|
2331
2509
|
for (const request of this.pendingRequests.values()) {
|
|
2510
|
+
if (request.target !== callerRole)
|
|
2511
|
+
continue;
|
|
2332
2512
|
if (!oldestRequest || request.timestamp < oldestRequest.timestamp) {
|
|
2333
2513
|
oldestRequest = request;
|
|
2334
2514
|
}
|
|
@@ -2390,14 +2570,14 @@ var ProxyBridgeService = class extends BridgeService {
|
|
|
2390
2570
|
this.primaryBaseUrl = primaryBaseUrl;
|
|
2391
2571
|
this.proxyInstanceId = uuidv42();
|
|
2392
2572
|
}
|
|
2393
|
-
async sendRequest(endpoint, data) {
|
|
2573
|
+
async sendRequest(endpoint, data, target = "edit") {
|
|
2394
2574
|
const controller = new AbortController();
|
|
2395
2575
|
const timeoutId = setTimeout(() => controller.abort(), this.proxyRequestTimeout);
|
|
2396
2576
|
try {
|
|
2397
2577
|
const response = await fetch(`${this.primaryBaseUrl}/proxy`, {
|
|
2398
2578
|
method: "POST",
|
|
2399
2579
|
headers: { "Content-Type": "application/json" },
|
|
2400
|
-
body: JSON.stringify({ endpoint, data, proxyInstanceId: this.proxyInstanceId }),
|
|
2580
|
+
body: JSON.stringify({ endpoint, data, target, proxyInstanceId: this.proxyInstanceId }),
|
|
2401
2581
|
signal: controller.signal
|
|
2402
2582
|
});
|
|
2403
2583
|
clearTimeout(timeoutId);
|
|
@@ -2434,7 +2614,7 @@ var RobloxStudioMCPServer = class {
|
|
|
2434
2614
|
constructor(config) {
|
|
2435
2615
|
this.config = config;
|
|
2436
2616
|
this.allowedToolNames = new Set(config.tools.map((t) => t.name));
|
|
2437
|
-
this.server = new
|
|
2617
|
+
this.server = new Server2({
|
|
2438
2618
|
name: config.name,
|
|
2439
2619
|
version: config.version
|
|
2440
2620
|
}, {
|
|
@@ -2447,7 +2627,7 @@ var RobloxStudioMCPServer = class {
|
|
|
2447
2627
|
this.setupToolHandlers();
|
|
2448
2628
|
}
|
|
2449
2629
|
setupToolHandlers() {
|
|
2450
|
-
this.server.setRequestHandler(
|
|
2630
|
+
this.server.setRequestHandler(ListToolsRequestSchema2, async () => {
|
|
2451
2631
|
return {
|
|
2452
2632
|
tools: this.config.tools.map((t) => ({
|
|
2453
2633
|
name: t.name,
|
|
@@ -2456,10 +2636,10 @@ var RobloxStudioMCPServer = class {
|
|
|
2456
2636
|
}))
|
|
2457
2637
|
};
|
|
2458
2638
|
});
|
|
2459
|
-
this.server.setRequestHandler(
|
|
2639
|
+
this.server.setRequestHandler(CallToolRequestSchema2, async (request) => {
|
|
2460
2640
|
const { name, arguments: args } = request.params;
|
|
2461
2641
|
if (!this.allowedToolNames.has(name)) {
|
|
2462
|
-
throw new
|
|
2642
|
+
throw new McpError2(ErrorCode2.MethodNotFound, `Unknown tool: ${name}`);
|
|
2463
2643
|
}
|
|
2464
2644
|
try {
|
|
2465
2645
|
switch (name) {
|
|
@@ -2545,13 +2725,15 @@ var RobloxStudioMCPServer = class {
|
|
|
2545
2725
|
case "get_selection":
|
|
2546
2726
|
return await this.tools.getSelection();
|
|
2547
2727
|
case "execute_luau":
|
|
2548
|
-
return await this.tools.executeLuau(args?.code);
|
|
2728
|
+
return await this.tools.executeLuau(args?.code, args?.target);
|
|
2549
2729
|
case "start_playtest":
|
|
2550
|
-
return await this.tools.startPlaytest(args?.mode);
|
|
2730
|
+
return await this.tools.startPlaytest(args?.mode, args?.numPlayers);
|
|
2551
2731
|
case "stop_playtest":
|
|
2552
2732
|
return await this.tools.stopPlaytest();
|
|
2553
2733
|
case "get_playtest_output":
|
|
2554
|
-
return await this.tools.getPlaytestOutput();
|
|
2734
|
+
return await this.tools.getPlaytestOutput(args?.target);
|
|
2735
|
+
case "get_connected_instances":
|
|
2736
|
+
return await this.tools.getConnectedInstances();
|
|
2555
2737
|
case "export_build":
|
|
2556
2738
|
return await this.tools.exportBuild(args?.instancePath, args?.outputId, args?.style);
|
|
2557
2739
|
case "create_build":
|
|
@@ -2619,11 +2801,11 @@ var RobloxStudioMCPServer = class {
|
|
|
2619
2801
|
case "capture_screenshot":
|
|
2620
2802
|
return await this.tools.captureScreenshot();
|
|
2621
2803
|
case "simulate_mouse_input":
|
|
2622
|
-
return await this.tools.simulateMouseInput(args?.action, args?.x, args?.y, args?.button, args?.scrollDirection);
|
|
2804
|
+
return await this.tools.simulateMouseInput(args?.action, args?.x, args?.y, args?.button, args?.scrollDirection, args?.target);
|
|
2623
2805
|
case "simulate_keyboard_input":
|
|
2624
|
-
return await this.tools.simulateKeyboardInput(args?.keyCode, args?.action, args?.duration);
|
|
2806
|
+
return await this.tools.simulateKeyboardInput(args?.keyCode, args?.action, args?.duration, args?.target);
|
|
2625
2807
|
case "character_navigation":
|
|
2626
|
-
return await this.tools.characterNavigation(args?.position, args?.instancePath, args?.waitForCompletion, args?.timeout);
|
|
2808
|
+
return await this.tools.characterNavigation(args?.position, args?.instancePath, args?.waitForCompletion, args?.timeout, args?.target);
|
|
2627
2809
|
case "find_and_replace_in_scripts":
|
|
2628
2810
|
return await this.tools.findAndReplaceInScripts(args?.pattern, args?.replacement, {
|
|
2629
2811
|
caseSensitive: args?.caseSensitive,
|
|
@@ -2634,12 +2816,12 @@ var RobloxStudioMCPServer = class {
|
|
|
2634
2816
|
maxReplacements: args?.maxReplacements
|
|
2635
2817
|
});
|
|
2636
2818
|
default:
|
|
2637
|
-
throw new
|
|
2819
|
+
throw new McpError2(ErrorCode2.MethodNotFound, `Unknown tool: ${name}`);
|
|
2638
2820
|
}
|
|
2639
2821
|
} catch (error) {
|
|
2640
|
-
if (error instanceof
|
|
2822
|
+
if (error instanceof McpError2)
|
|
2641
2823
|
throw error;
|
|
2642
|
-
throw new
|
|
2824
|
+
throw new McpError2(ErrorCode2.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2643
2825
|
}
|
|
2644
2826
|
});
|
|
2645
2827
|
}
|
|
@@ -2652,11 +2834,12 @@ var RobloxStudioMCPServer = class {
|
|
|
2652
2834
|
let boundPort = 0;
|
|
2653
2835
|
let promotionInterval;
|
|
2654
2836
|
try {
|
|
2655
|
-
primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames);
|
|
2837
|
+
primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config);
|
|
2656
2838
|
const result = await listenWithRetry(primaryApp, host, basePort, 5);
|
|
2657
2839
|
httpHandle = result.server;
|
|
2658
2840
|
boundPort = result.port;
|
|
2659
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`);
|
|
2660
2843
|
} catch {
|
|
2661
2844
|
bridgeMode = "proxy";
|
|
2662
2845
|
primaryApp = void 0;
|
|
@@ -2669,7 +2852,7 @@ var RobloxStudioMCPServer = class {
|
|
|
2669
2852
|
try {
|
|
2670
2853
|
this.bridge = new BridgeService();
|
|
2671
2854
|
this.tools = new RobloxStudioTools(this.bridge);
|
|
2672
|
-
primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames);
|
|
2855
|
+
primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config);
|
|
2673
2856
|
const result = await listenWithRetry(primaryApp, host, basePort, 5);
|
|
2674
2857
|
httpHandle = result.server;
|
|
2675
2858
|
boundPort = result.port;
|
|
@@ -2689,7 +2872,7 @@ var RobloxStudioMCPServer = class {
|
|
|
2689
2872
|
let legacyHandle;
|
|
2690
2873
|
let legacyApp;
|
|
2691
2874
|
if (boundPort !== LEGACY_PORT && bridgeMode === "primary") {
|
|
2692
|
-
legacyApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames);
|
|
2875
|
+
legacyApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config);
|
|
2693
2876
|
try {
|
|
2694
2877
|
const result = await listenWithRetry(legacyApp, host, LEGACY_PORT, 1);
|
|
2695
2878
|
legacyHandle = result.server;
|
|
@@ -2727,13 +2910,16 @@ var RobloxStudioMCPServer = class {
|
|
|
2727
2910
|
}, 5e3);
|
|
2728
2911
|
const cleanupInterval = setInterval(() => {
|
|
2729
2912
|
this.bridge.cleanupOldRequests();
|
|
2913
|
+
this.bridge.cleanupStaleInstances();
|
|
2730
2914
|
}, 5e3);
|
|
2731
|
-
const shutdown = () => {
|
|
2915
|
+
const shutdown = async () => {
|
|
2732
2916
|
console.error("Shutting down MCP server...");
|
|
2733
2917
|
clearInterval(activityInterval);
|
|
2734
2918
|
clearInterval(cleanupInterval);
|
|
2735
2919
|
if (promotionInterval)
|
|
2736
2920
|
clearInterval(promotionInterval);
|
|
2921
|
+
await this.server.close().catch(() => {
|
|
2922
|
+
});
|
|
2737
2923
|
if (httpHandle)
|
|
2738
2924
|
httpHandle.close();
|
|
2739
2925
|
if (legacyHandle)
|
|
@@ -3534,6 +3720,10 @@ var TOOL_DEFINITIONS = [
|
|
|
3534
3720
|
code: {
|
|
3535
3721
|
type: "string",
|
|
3536
3722
|
description: "Luau code to execute"
|
|
3723
|
+
},
|
|
3724
|
+
target: {
|
|
3725
|
+
type: "string",
|
|
3726
|
+
description: 'Instance target: "edit" (default), "server", "client-1", "client-2", etc.'
|
|
3537
3727
|
}
|
|
3538
3728
|
},
|
|
3539
3729
|
required: ["code"]
|
|
@@ -3592,7 +3782,7 @@ var TOOL_DEFINITIONS = [
|
|
|
3592
3782
|
{
|
|
3593
3783
|
name: "start_playtest",
|
|
3594
3784
|
category: "read",
|
|
3595
|
-
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).",
|
|
3596
3786
|
inputSchema: {
|
|
3597
3787
|
type: "object",
|
|
3598
3788
|
properties: {
|
|
@@ -3600,6 +3790,10 @@ var TOOL_DEFINITIONS = [
|
|
|
3600
3790
|
type: "string",
|
|
3601
3791
|
enum: ["play", "run"],
|
|
3602
3792
|
description: "Play mode"
|
|
3793
|
+
},
|
|
3794
|
+
numPlayers: {
|
|
3795
|
+
type: "number",
|
|
3796
|
+
description: "Number of client players (1-8). Triggers server + clients mode via TestService."
|
|
3603
3797
|
}
|
|
3604
3798
|
},
|
|
3605
3799
|
required: ["mode"]
|
|
@@ -3618,6 +3812,21 @@ var TOOL_DEFINITIONS = [
|
|
|
3618
3812
|
name: "get_playtest_output",
|
|
3619
3813
|
category: "read",
|
|
3620
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.",
|
|
3621
3830
|
inputSchema: {
|
|
3622
3831
|
type: "object",
|
|
3623
3832
|
properties: {}
|
|
@@ -4241,6 +4450,10 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
4241
4450
|
type: "string",
|
|
4242
4451
|
enum: ["up", "down"],
|
|
4243
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.'
|
|
4244
4457
|
}
|
|
4245
4458
|
},
|
|
4246
4459
|
required: ["action", "x", "y"]
|
|
@@ -4265,6 +4478,10 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
4265
4478
|
duration: {
|
|
4266
4479
|
type: "number",
|
|
4267
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.'
|
|
4268
4485
|
}
|
|
4269
4486
|
},
|
|
4270
4487
|
required: ["keyCode"]
|
|
@@ -4294,6 +4511,10 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
4294
4511
|
timeout: {
|
|
4295
4512
|
type: "number",
|
|
4296
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.'
|
|
4297
4518
|
}
|
|
4298
4519
|
}
|
|
4299
4520
|
}
|