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 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 pluginConnected && Date.now() - lastPluginActivity < 3e4;
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
- pluginConnected,
261
+ version: serverConfig?.version,
262
+ pluginConnected: instances.length > 0,
263
+ instanceCount: instances.length,
264
+ instances: instances.map((i) => ({
265
+ instanceId: i.instanceId,
266
+ role: i.role,
267
+ lastActivity: i.lastActivity,
268
+ connectedAt: i.connectedAt
269
+ })),
248
270
  mcpServerActive: isMCPServerActive(),
249
271
  uptime: mcpServerActive ? Date.now() - mcpServerStartTime : 0,
250
- proxyInstanceCount: proxyInstances.size
272
+ pendingRequests: bridge.getPendingRequestCount(),
273
+ proxyInstanceCount: proxyInstances.size,
274
+ streamableHttp: !!serverConfig
251
275
  });
252
276
  });
253
277
  app.post("/ready", (req, res) => {
254
- pluginConnected = true;
255
- lastPluginActivity = Date.now();
256
- res.json({ success: true });
278
+ const { instanceId, role } = req.body;
279
+ if (instanceId && role) {
280
+ const assignedRole = bridge.registerInstance(instanceId, role);
281
+ res.json({ success: true, assignedRole });
282
+ } else {
283
+ bridge.registerInstance("legacy", "edit");
284
+ res.json({ success: true, assignedRole: "edit" });
285
+ }
257
286
  });
258
287
  app.post("/disconnect", (req, res) => {
259
- pluginConnected = false;
260
- bridge.clearAllPendingRequests();
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: isPluginConnected(),
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
- if (!pluginConnected) {
273
- pluginConnected = true;
312
+ const instanceId = req.query.instanceId;
313
+ if (instanceId) {
314
+ bridge.updateInstanceActivity(instanceId);
315
+ }
316
+ let callerRole = "edit";
317
+ if (instanceId) {
318
+ const inst = bridge.getInstances().find((i) => i.instanceId === instanceId);
319
+ if (inst) {
320
+ callerRole = inst.role;
321
+ }
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 response = await this.client.request("/api/start-playtest", { mode });
1682
+ const data = { mode };
1683
+ if (numPlayers !== void 0) {
1684
+ data.numPlayers = numPlayers;
1685
+ }
1686
+ const response = await this.client.request("/api/start-playtest", data);
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
- async sendRequest(endpoint, data) {
2439
+ registerInstance(instanceId, role) {
2440
+ let assignedRole = role;
2441
+ if (role === "client") {
2442
+ assignedRole = `client-${this.nextClientIndex}`;
2443
+ this.nextClientIndex++;
2444
+ }
2445
+ this.instances.set(instanceId, {
2446
+ instanceId,
2447
+ role: assignedRole,
2448
+ lastActivity: Date.now(),
2449
+ connectedAt: Date.now()
2450
+ });
2451
+ return assignedRole;
2452
+ }
2453
+ unregisterInstance(instanceId) {
2454
+ this.instances.delete(instanceId);
2455
+ for (const [id, req] of this.pendingRequests.entries()) {
2456
+ const targetRole = req.target;
2457
+ const hasHandler = Array.from(this.instances.values()).some((i) => i.role === targetRole);
2458
+ if (!hasHandler) {
2459
+ clearTimeout(req.timeoutId);
2460
+ this.pendingRequests.delete(id);
2461
+ req.reject(new Error(`Target instance "${targetRole}" disconnected`));
2462
+ }
2463
+ }
2464
+ }
2465
+ getInstances() {
2466
+ return Array.from(this.instances.values());
2467
+ }
2468
+ getPendingRequestCount() {
2469
+ return this.pendingRequests.size;
2470
+ }
2471
+ updateInstanceActivity(instanceId) {
2472
+ const inst = this.instances.get(instanceId);
2473
+ if (inst) {
2474
+ inst.lastActivity = Date.now();
2475
+ }
2476
+ }
2477
+ cleanupStaleInstances() {
2478
+ const now = Date.now();
2479
+ for (const [id, inst] of this.instances.entries()) {
2480
+ if (now - inst.lastActivity > STALE_INSTANCE_MS) {
2481
+ this.unregisterInstance(id);
2482
+ }
2483
+ }
2484
+ }
2485
+ async sendRequest(endpoint, data, target = "edit") {
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 Server({
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(ListToolsRequestSchema, async () => {
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(CallToolRequestSchema, async (request) => {
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 McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
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 McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
2819
+ throw new McpError2(ErrorCode2.MethodNotFound, `Unknown tool: ${name}`);
2638
2820
  }
2639
2821
  } catch (error) {
2640
- if (error instanceof McpError)
2822
+ if (error instanceof McpError2)
2641
2823
  throw error;
2642
- throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
2824
+ throw new McpError2(ErrorCode2.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
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
  }