@chrrxs/robloxstudio-mcp-inspector 3.1.3 → 3.1.4

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
@@ -1945,6 +1945,15 @@ import { toNodeHandler } from "@modelcontextprotocol/node";
1945
1945
 
1946
1946
  // ../core/dist/bridge-service.js
1947
1947
  import { createHash, randomUUID } from "crypto";
1948
+ var MultiplayerGroupInUseError = class extends Error {
1949
+ groupId;
1950
+ code = "multiplayer_group_in_use";
1951
+ constructor(groupId) {
1952
+ super(`Multiplayer Group "${groupId}" still has connected runtime peers and cannot be removed.`);
1953
+ this.groupId = groupId;
1954
+ this.name = "MultiplayerGroupInUseError";
1955
+ }
1956
+ };
1948
1957
  function isExecutionOutcome(value) {
1949
1958
  return value === "success" || value === "error" || value === "not_executed" || value === "unknown";
1950
1959
  }
@@ -2233,6 +2242,11 @@ var BridgeService = class {
2233
2242
  const group = this.multiplayerGroupsById.get(groupId);
2234
2243
  if (!group)
2235
2244
  return void 0;
2245
+ for (const peer of this.peersById.values()) {
2246
+ if (peer.multiplayerGroupId === groupId && isRuntimeRole(peer.role)) {
2247
+ throw new MultiplayerGroupInUseError(groupId);
2248
+ }
2249
+ }
2236
2250
  this.multiplayerGroupsById.delete(groupId);
2237
2251
  for (const peer of this.peersById.values()) {
2238
2252
  if (peer.multiplayerGroupId === groupId)
@@ -5838,20 +5852,20 @@ var StudioInstanceManager = class {
5838
5852
  };
5839
5853
 
5840
5854
  // ../core/dist/mcp-runtime.js
5841
- var INTERNAL_RESULT_KEYS = /* @__PURE__ */ new Set([
5842
- "bundleModifiedAt",
5843
- "bundlePath",
5844
- "bundleSha256",
5845
- "connectedAt",
5846
- "debug",
5847
- "diagnostics",
5848
- "internal",
5849
- "lastActivity",
5850
- "transportPeerId",
5851
- "pluginVariant",
5852
- "pluginVersion",
5853
- "serverVersion"
5854
- ]);
5855
+ var INTERNAL_ENVELOPE_KEYS = {
5856
+ bundleModifiedAt: true,
5857
+ bundlePath: true,
5858
+ bundleSha256: true,
5859
+ connectedAt: true,
5860
+ debug: true,
5861
+ diagnostics: true,
5862
+ internal: true,
5863
+ lastActivity: true,
5864
+ transportPeerId: true,
5865
+ pluginVariant: true,
5866
+ pluginVersion: true,
5867
+ serverVersion: true
5868
+ };
5855
5869
  var TEXT_RESULT_TOOLS = /* @__PURE__ */ new Set(["get_roblox_docs"]);
5856
5870
  var SIDE_EFFECTING_READ_TOOLS = /* @__PURE__ */ new Set([
5857
5871
  "capture_micro_profiler",
@@ -5900,18 +5914,15 @@ function compactPublicValue(value) {
5900
5914
  return value.map(compactPublicValue);
5901
5915
  if (!value || typeof value !== "object")
5902
5916
  return value;
5903
- const compact = {};
5904
- for (const [key, child] of Object.entries(value)) {
5905
- if (child === void 0 || INTERNAL_RESULT_KEYS.has(key))
5906
- continue;
5907
- compact[key] = compactPublicValue(child);
5908
- }
5909
- return compact;
5917
+ return Object.fromEntries(Object.entries(value).filter(([, child]) => child !== void 0).map(([key, child]) => [key, compactPublicValue(child)]));
5918
+ }
5919
+ function compactPublicEnvelope(value) {
5920
+ return Object.fromEntries(Object.entries(value).filter(([key, child]) => child !== void 0 && !Object.hasOwn(INTERNAL_ENVELOPE_KEYS, key)).map(([key, child]) => [key, compactPublicValue(child)]));
5910
5921
  }
5911
5922
  function asStructuredObject(value) {
5912
5923
  if (!value || typeof value !== "object" || Array.isArray(value))
5913
5924
  return void 0;
5914
- return compactPublicValue(value);
5925
+ return compactPublicEnvelope(value);
5915
5926
  }
5916
5927
  function parseJsonObject(text) {
5917
5928
  try {
@@ -5936,6 +5947,7 @@ function normalizeToolResult(raw, era) {
5936
5947
  ...result.isError ? { isError: true } : {}
5937
5948
  };
5938
5949
  }
5950
+ const isError = result.isError || structured.success === false || typeof structured.error === "string" && structured.error.length > 0;
5939
5951
  const content = era === "modern" ? originalContent.filter((_, index) => index !== jsonTextIndex) : [
5940
5952
  { type: "text", text: JSON.stringify(structured) },
5941
5953
  ...originalContent.filter((_, index) => index !== jsonTextIndex)
@@ -5943,7 +5955,7 @@ function normalizeToolResult(raw, era) {
5943
5955
  return {
5944
5956
  content,
5945
5957
  structuredContent: structured,
5946
- ...result.isError ? { isError: true } : {}
5958
+ ...isError ? { isError: true } : {}
5947
5959
  };
5948
5960
  }
5949
5961
  function publicRoutingError(error) {
@@ -5966,8 +5978,11 @@ function publicRoutingError(error) {
5966
5978
  };
5967
5979
  }
5968
5980
  function publicToolErrorBody(name, error) {
5981
+ if (error instanceof MultiplayerGroupInUseError) {
5982
+ return { error: error.code, message: error.message, multiplayer_group_id: error.groupId };
5983
+ }
5969
5984
  if (error instanceof StudioLaunchPreDispatchError) {
5970
- return compactPublicValue(error.toResponseBody());
5985
+ return compactPublicEnvelope(error.toResponseBody());
5971
5986
  }
5972
5987
  if (error instanceof RoutingFailure)
5973
5988
  return publicRoutingError(error);
@@ -6441,6 +6456,47 @@ var WebSocketStudioTransport = class {
6441
6456
  };
6442
6457
 
6443
6458
  // ../core/dist/http-server.js
6459
+ var TOOL_PROXY_ENDPOINTS = {
6460
+ get_place_info: ["/api/place-info"],
6461
+ search_objects: ["/api/search-objects"],
6462
+ get_instance_properties: ["/api/instance-properties"],
6463
+ get_project_structure: ["/api/project-structure"],
6464
+ set_properties: ["/api/set-properties"],
6465
+ get_script_source: ["/api/get-script-source"],
6466
+ set_script_source: ["/api/set-script-source"],
6467
+ edit_script_lines: ["/api/edit-script-lines"],
6468
+ insert_script_lines: ["/api/insert-script-lines"],
6469
+ delete_script_lines: ["/api/delete-script-lines"],
6470
+ get_attributes: ["/api/get-attributes"],
6471
+ selection: ["/api/get-selection", "/api/set-selection", "/api/focus-viewport"],
6472
+ execute_luau: ["/api/execute-luau"],
6473
+ eval_server_runtime: ["/api/eval-runtime"],
6474
+ eval_client_runtime: ["/api/eval-runtime"],
6475
+ grep_scripts: ["/api/grep-scripts"],
6476
+ solo_playtest: ["/api/start-playtest", "/api/stop-playtest", "/api/multiplayer-test-state"],
6477
+ multiplayer_playtest: [
6478
+ "/api/multiplayer-test-start",
6479
+ "/api/multiplayer-test-add-players",
6480
+ "/api/multiplayer-test-leave-client",
6481
+ "/api/multiplayer-test-end",
6482
+ "/api/multiplayer-test-state"
6483
+ ],
6484
+ get_runtime_logs: ["/api/get-runtime-logs"],
6485
+ capture_script_profiler: ["/api/capture-script-profiler"],
6486
+ capture_micro_profiler: ["/api/capture-micro-profiler"],
6487
+ breakpoints: ["/api/breakpoints"],
6488
+ insert_asset: ["/api/insert-asset"],
6489
+ generate_model: ["/api/generate-model"],
6490
+ preview_asset: ["/api/preview-asset"],
6491
+ capture_screenshot: ["/api/capture-studio", "/api/capture-screenshot", "/api/capture-begin", "/api/capture-read"],
6492
+ simulate_mouse_input: ["/api/simulate-mouse-input"],
6493
+ simulate_keyboard_input: ["/api/simulate-keyboard-input"],
6494
+ get_memory_breakdown: ["/api/get-memory-breakdown"],
6495
+ get_scene_analysis: ["/api/get-scene-analysis"],
6496
+ export_rbxm: ["/api/export-rbxm"],
6497
+ import_rbxm: ["/api/import-rbxm"],
6498
+ find_and_replace_in_scripts: ["/api/find-and-replace-in-scripts"]
6499
+ };
6444
6500
  function toPassivePeer(peer) {
6445
6501
  return {
6446
6502
  instanceId: peer.instanceId,
@@ -6627,6 +6683,7 @@ ${body}`);
6627
6683
  }
6628
6684
  function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
6629
6685
  const app = express();
6686
+ const allowedProxyEndpoints = allowedTools ? new Set(Object.entries(TOOL_PROXY_ENDPOINTS).filter(([toolName]) => allowedTools.has(toolName)).flatMap(([, endpoints]) => endpoints)) : void 0;
6630
6687
  const studioLifecycleCallable = !allowedTools || allowedTools.has("manage_instance");
6631
6688
  const studioLifecycleCapabilities = studioLifecycleCallable ? tools.getStudioLifecycleCapabilities() : void 0;
6632
6689
  let mcpServerActive = false;
@@ -7034,14 +7091,27 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
7034
7091
  const group = await bridge.createMultiplayerGroupEverywhere(groupId, controllerInstanceId);
7035
7092
  res.json({ success: true, group });
7036
7093
  });
7037
- app.post("/remove-multiplayer-group", async (req, res) => {
7094
+ app.post("/remove-multiplayer-group", async (req, res, next) => {
7038
7095
  const { groupId } = req.body;
7039
7096
  if (typeof groupId !== "string" || groupId.length === 0) {
7040
7097
  res.status(400).json({ error: "groupId is required" });
7041
7098
  return;
7042
7099
  }
7043
- const removed = await bridge.removeMultiplayerGroupEverywhere(groupId);
7044
- res.json({ success: true, removed });
7100
+ try {
7101
+ const removed = await bridge.removeMultiplayerGroupEverywhere(groupId);
7102
+ res.json({ success: true, removed });
7103
+ } catch (error) {
7104
+ if (error instanceof MultiplayerGroupInUseError) {
7105
+ res.status(409).json({
7106
+ success: false,
7107
+ error: error.code,
7108
+ message: error.message,
7109
+ groupId: error.groupId
7110
+ });
7111
+ return;
7112
+ }
7113
+ next(error);
7114
+ }
7045
7115
  });
7046
7116
  app.get("/status", (req, res) => {
7047
7117
  const peers = bridge.getPublicPeers().map(toPassivePeer);
@@ -7088,6 +7158,10 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
7088
7158
  res.status(400).json({ error: "endpoint and targetPeerId are required" });
7089
7159
  return;
7090
7160
  }
7161
+ if (allowedProxyEndpoints && !allowedProxyEndpoints.has(endpoint)) {
7162
+ res.status(403).json({ error: "forbidden_endpoint" });
7163
+ return;
7164
+ }
7091
7165
  if (timeoutMs !== void 0 && (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 3e5)) {
7092
7166
  res.status(400).json({ error: "timeoutMs must be an integer between 1 and 300000" });
7093
7167
  return;
@@ -10886,7 +10960,10 @@ var RobloxStudioTools = class {
10886
10960
  _requestPeer(endpoint, data, targetPeerId, timeoutMs, signal, operationId) {
10887
10961
  return this.client.request(endpoint, data, targetPeerId, timeoutMs, signal, operationId);
10888
10962
  }
10889
- _request(endpoint, data, instanceId, role, timeoutMs, signal) {
10963
+ async _request(endpoint, data, instanceId, role, timeoutMs, signal) {
10964
+ const refresh = this.bridge.refreshTopologyForRouting(signal);
10965
+ if (refresh)
10966
+ await refresh;
10890
10967
  const peer = this._peerForRoleInScope(instanceId, role);
10891
10968
  if (!peer) {
10892
10969
  throw new RoutingFailure({
@@ -10899,6 +10976,9 @@ var RobloxStudioTools = class {
10899
10976
  }
10900
10977
  // Resolve an optional Studio process plus role to one exact Peer and dispatch.
10901
10978
  async _callSingle(endpoint, data, target, instance_id, timeoutMs, signal, operationId) {
10979
+ const refresh = this.bridge.refreshTopologyForRouting(signal);
10980
+ if (refresh)
10981
+ await refresh;
10902
10982
  const resolved = this.bridge.resolveTarget({ instance_id, target });
10903
10983
  if (!resolved.ok)
10904
10984
  throw new RoutingFailure(resolved.error);
@@ -10930,6 +11010,7 @@ var RobloxStudioTools = class {
10930
11010
  clientRole: client?.role
10931
11011
  };
10932
11012
  }
11013
+ // Resolve synchronously after the caller has refreshed and captured its topology.
10933
11014
  _resolveInstanceIdOnly(instance_id) {
10934
11015
  if (instance_id !== void 0) {
10935
11016
  const resolvedInstanceId = this.bridge.resolveConnectedInstanceId(instance_id);
@@ -11143,6 +11224,9 @@ var RobloxStudioTools = class {
11143
11224
  async _waitForRuntimeRoles(instanceId, opts, timeoutSec = 30) {
11144
11225
  const deadline = Date.now() + timeoutSec * 1e3;
11145
11226
  while (Date.now() < deadline) {
11227
+ const refresh = this.bridge.refreshTopologyForRouting();
11228
+ if (refresh)
11229
+ await refresh;
11146
11230
  const roles = this._rolesForScope(instanceId);
11147
11231
  const clientRoles = this._clientRolesForScope(instanceId);
11148
11232
  const hasServer = !opts.server || roles.includes("server");
@@ -11164,6 +11248,9 @@ var RobloxStudioTools = class {
11164
11248
  const deadline = Date.now() + timeoutSec * 1e3;
11165
11249
  let exactSince;
11166
11250
  while (Date.now() < deadline) {
11251
+ const refresh = this.bridge.refreshTopologyForRouting();
11252
+ if (refresh)
11253
+ await refresh;
11167
11254
  const roles2 = this._rolesForScope(instanceId);
11168
11255
  const clientCount2 = this._clientRolesForScope(instanceId).length;
11169
11256
  if (clientCount2 > expectedClientCount) {
@@ -11186,6 +11273,9 @@ var RobloxStudioTools = class {
11186
11273
  async _waitForRuntimeRolesFresh(instanceId, connectedAfter, requiredRoles, timeoutSec = 60) {
11187
11274
  const deadline = Date.now() + timeoutSec * 1e3;
11188
11275
  while (Date.now() < deadline) {
11276
+ const refresh = this.bridge.refreshTopologyForRouting();
11277
+ if (refresh)
11278
+ await refresh;
11189
11279
  const peers = this.bridge.getPeersInScope(instanceId);
11190
11280
  const roles = peers.map((peer) => peer.role);
11191
11281
  const freshRoles = new Set(peers.filter((peer) => peer.connectedAt >= connectedAfter).map((peer) => peer.role));
@@ -11483,6 +11573,9 @@ var RobloxStudioTools = class {
11483
11573
  if (angleY !== void 0 && (angleY < -89 || angleY > 89)) {
11484
11574
  throw new Error("selection angleY must be between -89 and 89");
11485
11575
  }
11576
+ const refresh = this.bridge.refreshTopologyForRouting();
11577
+ if (refresh)
11578
+ await refresh;
11486
11579
  const { instanceId, clientRole } = this._resolveRuntime(instance_id);
11487
11580
  const response = await this._callSingle("/api/focus-viewport", {
11488
11581
  path: instancePath,
@@ -11547,6 +11640,9 @@ var RobloxStudioTools = class {
11547
11640
  }
11548
11641
  async setNetworkProfile(profile, target, overrides, instance_id) {
11549
11642
  const values = normalizeNetworkProfile(profile, overrides);
11643
+ const refresh = this.bridge.refreshTopologyForRouting();
11644
+ if (refresh)
11645
+ await refresh;
11550
11646
  const instanceId = this._resolveInstanceIdOnly(instance_id);
11551
11647
  const clientRoles = this._clientRolesForScope(instanceId);
11552
11648
  const selectedTarget = target ?? "client-1";
@@ -11613,6 +11709,9 @@ var RobloxStudioTools = class {
11613
11709
  const selectedInclude = this._normalizeSimulationInclude(include);
11614
11710
  const includeNetwork = selectedInclude === "network" || selectedInclude === "both";
11615
11711
  const includeDeviceSimulator = selectedInclude === "deviceSimulator" || selectedInclude === "both";
11712
+ const refresh = this.bridge.refreshTopologyForRouting();
11713
+ if (refresh)
11714
+ await refresh;
11616
11715
  const resolved = this._resolveSimulationTargets(target, instance_id, "get_simulation_state");
11617
11716
  const roleEntries = await Promise.all(resolved.roles.map(async (role) => {
11618
11717
  const state = {};
@@ -11659,6 +11758,9 @@ var RobloxStudioTools = class {
11659
11758
  if (!resetNetwork && !resetDeviceSimulator) {
11660
11759
  throw new Error("reset_simulation_state requires network=true and/or deviceSimulator=true; both default to true");
11661
11760
  }
11761
+ const refresh = this.bridge.refreshTopologyForRouting();
11762
+ if (refresh)
11763
+ await refresh;
11662
11764
  const resolved = this._resolveSimulationTargets(target, instance_id, "reset_simulation_state");
11663
11765
  const roleEntries = await Promise.all(resolved.roles.map(async (role) => {
11664
11766
  const result = {};
@@ -11717,6 +11819,9 @@ var RobloxStudioTools = class {
11717
11819
  if (deviceId !== void 0 && (typeof deviceId !== "string" || deviceId.trim() === "")) {
11718
11820
  throw new Error("deviceId must be a non-empty string when provided");
11719
11821
  }
11822
+ const refresh = this.bridge.refreshTopologyForRouting();
11823
+ if (refresh)
11824
+ await refresh;
11720
11825
  const resolved = this._resolveDeviceSimulatorSingleTarget(target, instance_id, "get_device_simulator_state");
11721
11826
  const state = await this._executeDeviceSimulatorOperation(resolved.instanceId, resolved.role, "get", {
11722
11827
  includeDeviceList: includeDeviceList !== false,
@@ -11741,6 +11846,9 @@ var RobloxStudioTools = class {
11741
11846
  if (stopSimulation !== true && !hasDeviceSimulatorSettings(settings)) {
11742
11847
  throw new Error("set_device_simulator requires stopSimulation=true or at least one simulator setting");
11743
11848
  }
11849
+ const refresh = this.bridge.refreshTopologyForRouting();
11850
+ if (refresh)
11851
+ await refresh;
11744
11852
  const resolved = this._resolveDeviceSimulatorSetTargets(target, instance_id);
11745
11853
  const responses = await Promise.allSettled(resolved.roles.map(async (role) => {
11746
11854
  const result = await this._executeDeviceSimulatorOperation(resolved.instanceId, role, "set", stopSimulation === true ? { stopSimulation: true } : { settings });
@@ -11799,6 +11907,9 @@ var RobloxStudioTools = class {
11799
11907
  label: raw.label
11800
11908
  };
11801
11909
  });
11910
+ const refresh = this.bridge.refreshTopologyForRouting();
11911
+ if (refresh)
11912
+ await refresh;
11802
11913
  const resolved = this._resolveDeviceSimulatorSingleTarget(target, instance_id, "capture_device_matrix");
11803
11914
  if (resolved.role.startsWith("client-") && await this._isMultiplayerTestRunning(resolved.instanceId)) {
11804
11915
  throw new Error("capture_device_matrix does not support StudioTestService multiplayer client targets because Roblox scopes temporary screenshot textures per client process");
@@ -12150,6 +12261,9 @@ var RobloxStudioTools = class {
12150
12261
  if (outputPath) {
12151
12262
  data.__mcp_include_raw_json = true;
12152
12263
  }
12264
+ const refresh = this.bridge.refreshTopologyForRouting();
12265
+ if (refresh)
12266
+ await refresh;
12153
12267
  const resolved = this.bridge.resolveTarget({ instance_id, target: targetRole });
12154
12268
  if (!resolved.ok)
12155
12269
  throw new RoutingFailure(resolved.error);
@@ -12210,6 +12324,9 @@ var RobloxStudioTools = class {
12210
12324
  if (summaryOutputPath || baselinePath || baseline || includeComparisonIndex) {
12211
12325
  data.__mcp_include_comparison_index = true;
12212
12326
  }
12327
+ const refresh = this.bridge.refreshTopologyForRouting();
12328
+ if (refresh)
12329
+ await refresh;
12213
12330
  const resolved = this.bridge.resolveTarget({ instance_id, target: targetRole });
12214
12331
  if (!resolved.ok)
12215
12332
  throw new RoutingFailure(resolved.error);
@@ -12264,6 +12381,9 @@ var RobloxStudioTools = class {
12264
12381
  const data = { ...request, action };
12265
12382
  delete data.target;
12266
12383
  delete data.instance_id;
12384
+ const refresh = this.bridge.refreshTopologyForRouting();
12385
+ if (refresh)
12386
+ await refresh;
12267
12387
  const resolved = this.bridge.resolveTarget({ instance_id, target: targetRole });
12268
12388
  if (!resolved.ok)
12269
12389
  throw new RoutingFailure(resolved.error);
@@ -12614,6 +12734,9 @@ var RobloxStudioTools = class {
12614
12734
  throw new Error("solo_playtest requires action=start|stop|status");
12615
12735
  }
12616
12736
  if (action === "status") {
12737
+ const refresh = this.bridge.refreshTopologyForRouting();
12738
+ if (refresh)
12739
+ await refresh;
12617
12740
  const instanceId = this._resolveInstanceIdOnly(instance_id);
12618
12741
  const { roles, runtimeRoles } = this._briefRoles(instanceId);
12619
12742
  return this._textResult({
@@ -12674,6 +12797,9 @@ var RobloxStudioTools = class {
12674
12797
  }
12675
12798
  const data = { mode };
12676
12799
  const startedAt = Date.now();
12800
+ const refresh = this.bridge.refreshTopologyForRouting();
12801
+ if (refresh)
12802
+ await refresh;
12677
12803
  const resolved = this.bridge.resolveTarget({ instance_id, target: void 0 });
12678
12804
  if (!resolved.ok)
12679
12805
  throw new RoutingFailure(resolved.error);
@@ -12724,6 +12850,9 @@ var RobloxStudioTools = class {
12724
12850
  };
12725
12851
  }
12726
12852
  async stopPlaytest(instance_id, timeout = 15) {
12853
+ const refresh = this.bridge.refreshTopologyForRouting();
12854
+ if (refresh)
12855
+ await refresh;
12727
12856
  const { instanceId } = this._resolveSingleTarget("edit", instance_id);
12728
12857
  let response;
12729
12858
  let stopRequestError;
@@ -12774,6 +12903,9 @@ var RobloxStudioTools = class {
12774
12903
  };
12775
12904
  }
12776
12905
  async _buildMultiplayerState(instanceId) {
12906
+ const refresh = this.bridge.refreshTopologyForRouting();
12907
+ if (refresh)
12908
+ await refresh;
12777
12909
  const peers = this.bridge.getPublicPeers().filter((peer) => this.bridge.getInstanceIdsInScope(instanceId).includes(peer.instanceId)).sort((a, b) => a.role.localeCompare(b.role));
12778
12910
  const multiplayerGroup = this.bridge.getMultiplayerGroups().find((group) => group.instanceIds.includes(instanceId));
12779
12911
  const body = {
@@ -12819,6 +12951,9 @@ var RobloxStudioTools = class {
12819
12951
  async _waitForMultiplayerEditDone(instanceId, timeoutSec = 30) {
12820
12952
  const deadline = Date.now() + timeoutSec * 1e3;
12821
12953
  while (Date.now() < deadline) {
12954
+ const refresh = this.bridge.refreshTopologyForRouting();
12955
+ if (refresh)
12956
+ await refresh;
12822
12957
  if (!this._rolesForScope(instanceId).includes("edit"))
12823
12958
  return false;
12824
12959
  try {
@@ -12833,6 +12968,9 @@ var RobloxStudioTools = class {
12833
12968
  return false;
12834
12969
  }
12835
12970
  async _isMultiplayerTestRunning(instanceId) {
12971
+ const refresh = this.bridge.refreshTopologyForRouting();
12972
+ if (refresh)
12973
+ await refresh;
12836
12974
  return this.bridge.getMultiplayerGroups().some((group) => group.instanceIds.includes(instanceId));
12837
12975
  }
12838
12976
  async _waitForMultiplayerStart(instanceId, clientCount, timeoutSec = 30, connectedAfter) {
@@ -12873,6 +13011,9 @@ var RobloxStudioTools = class {
12873
13011
  throw new Error("multiplayer_playtest requires action=start|status|add_players|leave_client|end");
12874
13012
  }
12875
13013
  const briefState = async (instanceId) => {
13014
+ const refresh = this.bridge.refreshTopologyForRouting();
13015
+ if (refresh)
13016
+ await refresh;
12876
13017
  const state = await this._buildMultiplayerState(this._resolveInstanceIdOnly(instanceId));
12877
13018
  const roles = Array.isArray(state.peers) ? state.peers.flatMap((peer) => peer !== null && typeof peer === "object" && "role" in peer && typeof peer.role === "string" ? [peer.role] : []) : [];
12878
13019
  return {
@@ -12970,6 +13111,9 @@ var RobloxStudioTools = class {
12970
13111
  if (!Number.isInteger(numPlayers) || numPlayers < 1 || numPlayers > 8) {
12971
13112
  throw new Error("numPlayers must be an integer from 1 to 8");
12972
13113
  }
13114
+ const refresh = this.bridge.refreshTopologyForRouting();
13115
+ if (refresh)
13116
+ await refresh;
12973
13117
  const editTarget = this._resolveSingleTarget("edit", instance_id);
12974
13118
  const existingRuntime = this._runtimeTargetsForScope(editTarget.instanceId);
12975
13119
  if (existingRuntime.length > 0) {
@@ -13026,6 +13170,9 @@ var RobloxStudioTools = class {
13026
13170
  };
13027
13171
  }
13028
13172
  async multiplayerTestState(instance_id) {
13173
+ const refresh = this.bridge.refreshTopologyForRouting();
13174
+ if (refresh)
13175
+ await refresh;
13029
13176
  const instanceId = this._resolveInstanceIdOnly(instance_id);
13030
13177
  const state = await this._buildMultiplayerState(instanceId);
13031
13178
  return { content: [{ type: "text", text: JSON.stringify(state) }] };
@@ -13034,6 +13181,9 @@ var RobloxStudioTools = class {
13034
13181
  if (!Number.isInteger(numPlayers) || numPlayers < 1 || numPlayers > 8) {
13035
13182
  throw new Error("numPlayers must be an integer from 1 to 8");
13036
13183
  }
13184
+ const refresh = this.bridge.refreshTopologyForRouting();
13185
+ if (refresh)
13186
+ await refresh;
13037
13187
  const serverTarget = this._resolveSingleTarget("server", instance_id);
13038
13188
  const group = this.bridge.getMultiplayerGroups().find((candidate) => candidate.instanceIds.includes(serverTarget.instanceId));
13039
13189
  const scopeInstanceId = group?.controllerInstanceId ?? serverTarget.instanceId;
@@ -13062,6 +13212,9 @@ var RobloxStudioTools = class {
13062
13212
  if (!/^client-\d+$/.test(target)) {
13063
13213
  throw new Error(`multiplayer_test_leave_client requires target=client-N (got: ${target})`);
13064
13214
  }
13215
+ const refresh = this.bridge.refreshTopologyForRouting();
13216
+ if (refresh)
13217
+ await refresh;
13065
13218
  const clientTarget = this._resolveSingleTarget(target, instance_id);
13066
13219
  const group = this.bridge.getMultiplayerGroups().find((candidate) => candidate.instanceIds.includes(clientTarget.instanceId));
13067
13220
  const scopeInstanceId = group?.controllerInstanceId ?? clientTarget.instanceId;
@@ -13085,6 +13238,9 @@ var RobloxStudioTools = class {
13085
13238
  };
13086
13239
  }
13087
13240
  async multiplayerTestEnd(value, timeout, instance_id) {
13241
+ const refresh = this.bridge.refreshTopologyForRouting();
13242
+ if (refresh)
13243
+ await refresh;
13088
13244
  let serverTarget;
13089
13245
  try {
13090
13246
  serverTarget = this._resolveSingleTarget("server", instance_id);
@@ -13133,6 +13289,9 @@ var RobloxStudioTools = class {
13133
13289
  return result;
13134
13290
  }
13135
13291
  async getConnectedInstances() {
13292
+ const refresh = this.bridge.refreshTopologyForRouting();
13293
+ if (refresh)
13294
+ await refresh;
13136
13295
  return this._textResult({
13137
13296
  instances: this.bridge.getConnectedInstances(),
13138
13297
  multiplayerGroups: this.bridge.getConnectedMultiplayerGroups()
@@ -13674,6 +13833,9 @@ var RobloxStudioTools = class {
13674
13833
  if (!action) {
13675
13834
  throw new Error("action is required for simulate_mouse_input");
13676
13835
  }
13836
+ const refresh = this.bridge.refreshTopologyForRouting();
13837
+ if (refresh)
13838
+ await refresh;
13677
13839
  const { instanceId, clientRole } = this._resolveRuntime(instance_id);
13678
13840
  const response = await this._callSingle("/api/simulate-mouse-input", {
13679
13841
  action,
@@ -13692,6 +13854,9 @@ var RobloxStudioTools = class {
13692
13854
  if (!keyCode && text === void 0) {
13693
13855
  throw new Error("keyCode or text is required for simulate_keyboard_input");
13694
13856
  }
13857
+ const refresh = this.bridge.refreshTopologyForRouting();
13858
+ if (refresh)
13859
+ await refresh;
13695
13860
  const { instanceId, clientRole } = this._resolveRuntime(instance_id);
13696
13861
  const response = await this._callSingle("/api/simulate-keyboard-input", {
13697
13862
  keyCode,
@@ -13730,6 +13895,9 @@ var RobloxStudioTools = class {
13730
13895
  const data = {};
13731
13896
  if (tags !== void 0)
13732
13897
  data.tags = tags;
13898
+ const refresh = this.bridge.refreshTopologyForRouting();
13899
+ if (refresh)
13900
+ await refresh;
13733
13901
  const resolved = this.bridge.resolveTarget({ instance_id, target: tgt });
13734
13902
  if (!resolved.ok)
13735
13903
  throw new RoutingFailure(resolved.error);
@@ -13763,6 +13931,9 @@ var RobloxStudioTools = class {
13763
13931
  data.topN = topN;
13764
13932
  if (raw !== void 0)
13765
13933
  data.raw = raw;
13934
+ const refresh = this.bridge.refreshTopologyForRouting();
13935
+ if (refresh)
13936
+ await refresh;
13766
13937
  const resolved = this.bridge.resolveTarget({ instance_id, target: tgt });
13767
13938
  if (!resolved.ok)
13768
13939
  throw new RoutingFailure(resolved.error);
@@ -13895,18 +14066,23 @@ var RobloxStudioTools = class {
13895
14066
  return { content: [{ type: "text", text: JSON.stringify(response) }] };
13896
14067
  }
13897
14068
  async _captureViewportImage(instanceId, targetRole, format, quality, maxBytes = MAX_INLINE_IMAGE_BYTES) {
13898
- let response;
13899
- if (targetRole.startsWith("client-")) {
13900
- const begin = await this._callSingle("/api/capture-begin", {}, targetRole, instanceId);
13901
- if (begin.error) {
13902
- return { success: false, error: begin.error };
13903
- }
13904
- if (!begin.contentId) {
13905
- return { success: false, error: "Screenshot capture failed: no content id returned from client." };
14069
+ const fmt = format === "png" ? "png" : "jpeg";
14070
+ const q = quality === void 0 ? 92 : Math.max(1, Math.min(100, Math.floor(quality)));
14071
+ let response = await this._callSingle("/api/capture-studio", { encoding: fmt === "png" ? "png" : "rgba8" }, targetRole, instanceId);
14072
+ const studioFastPathMissing = response.unavailable !== void 0 || (response.error?.includes("/api/capture-studio") ?? false);
14073
+ if (studioFastPathMissing) {
14074
+ if (targetRole.startsWith("client-")) {
14075
+ const begin = await this._callSingle("/api/capture-begin", {}, targetRole, instanceId);
14076
+ if (begin.error) {
14077
+ return { success: false, error: begin.error };
14078
+ }
14079
+ if (!begin.contentId) {
14080
+ return { success: false, error: "Screenshot capture failed: no content id returned from client." };
14081
+ }
14082
+ response = await this._callSingle("/api/capture-read", { contentId: begin.contentId }, "edit", instanceId);
14083
+ } else {
14084
+ response = await this._callSingle("/api/capture-screenshot", {}, "edit", instanceId);
13906
14085
  }
13907
- response = await this._callSingle("/api/capture-read", { contentId: begin.contentId }, "edit", instanceId);
13908
- } else {
13909
- response = await this._callSingle("/api/capture-screenshot", {}, "edit", instanceId);
13910
14086
  }
13911
14087
  if (response.error) {
13912
14088
  let text = response.error;
@@ -13920,11 +14096,19 @@ var RobloxStudioTools = class {
13920
14096
  if (w === void 0 || h === void 0) {
13921
14097
  return { success: false, error: "Screenshot response missing dimensions." };
13922
14098
  }
13923
- const fmt = format === "png" ? "png" : "jpeg";
13924
- const q = quality === void 0 ? 92 : Math.max(1, Math.min(100, Math.floor(quality)));
13925
- const encoded = encodeImageFromRgbaResponse(response, fmt, q);
13926
- let { buffer } = encoded;
13927
- const { mimeType } = encoded;
14099
+ let buffer;
14100
+ let mimeType;
14101
+ if (response.encoding === "png") {
14102
+ if (!response.data) {
14103
+ return { success: false, error: "Screenshot response missing PNG data." };
14104
+ }
14105
+ buffer = Buffer.from(response.data, "base64");
14106
+ mimeType = "image/png";
14107
+ } else {
14108
+ const encoded = encodeImageFromRgbaResponse(response, fmt, q);
14109
+ buffer = encoded.buffer;
14110
+ mimeType = encoded.mimeType;
14111
+ }
13928
14112
  let usedQ = q;
13929
14113
  let note = "";
13930
14114
  if (buffer.length > maxBytes) {
@@ -13963,6 +14147,9 @@ var RobloxStudioTools = class {
13963
14147
  };
13964
14148
  }
13965
14149
  async captureScreenshot(instance_id, format, quality) {
14150
+ const refresh = this.bridge.refreshTopologyForRouting();
14151
+ if (refresh)
14152
+ await refresh;
13966
14153
  const { instanceId, clientRole } = this._resolveRuntime(instance_id);
13967
14154
  const capture = await this._captureViewportImage(instanceId, clientRole ?? "edit", format, quality);
13968
14155
  if (!capture.success) {
@@ -14160,6 +14347,13 @@ var ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
14160
14347
  headers: this.authHeaders({ "Content-Type": "application/json" }),
14161
14348
  body: JSON.stringify({ groupId })
14162
14349
  });
14350
+ if (response.status === 409) {
14351
+ const refusal = await response.json().catch(() => void 0);
14352
+ if (refusal && typeof refusal === "object" && !Array.isArray(refusal) && "success" in refusal && refusal.success === false && "error" in refusal && refusal.error === "multiplayer_group_in_use" && "groupId" in refusal && refusal.groupId === groupId) {
14353
+ throw new MultiplayerGroupInUseError(groupId);
14354
+ }
14355
+ throw new Error("Proxy Multiplayer Group removal returned an invalid refusal.");
14356
+ }
14163
14357
  if (!response.ok) {
14164
14358
  const body = await response.text().catch(() => "");
14165
14359
  throw new Error(`Proxy Multiplayer Group removal failed (${response.status}): ${body || response.statusText}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrrxs/robloxstudio-mcp-inspector",
3
- "version": "3.1.3",
3
+ "version": "3.1.4",
4
4
  "description": "DataModel read-only MCP server for inspecting and debugging Roblox Studio from AI coding tools",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",