@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 +238 -44
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +524 -46
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
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
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
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
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
|
|
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
|
-
...
|
|
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
|
|
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
|
-
|
|
7044
|
-
|
|
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
|
-
|
|
13899
|
-
|
|
13900
|
-
|
|
13901
|
-
|
|
13902
|
-
|
|
13903
|
-
|
|
13904
|
-
|
|
13905
|
-
|
|
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
|
-
|
|
13924
|
-
|
|
13925
|
-
|
|
13926
|
-
|
|
13927
|
-
|
|
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