@chrrxs/robloxstudio-mcp 2.13.0 → 2.15.0
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 +546 -29
- package/package.json +2 -2
- package/studio-plugin/INSTALLATION.md +1 -0
- package/studio-plugin/MCPInspectorPlugin.rbxmx +1573 -330
- package/studio-plugin/MCPPlugin.rbxmx +633 -27
- package/studio-plugin/src/modules/ClientBroker.ts +77 -2
- package/studio-plugin/src/modules/Communication.ts +7 -0
- package/studio-plugin/src/modules/handlers/SceneAnalysisHandlers.ts +216 -0
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +246 -12
package/dist/index.js
CHANGED
|
@@ -43,18 +43,25 @@ var init_bridge_service = __esm({
|
|
|
43
43
|
requestTimeout = 3e4;
|
|
44
44
|
registerInstance(input) {
|
|
45
45
|
const { pluginSessionId, instanceId, role } = input;
|
|
46
|
+
const prior = this.instances.get(pluginSessionId);
|
|
46
47
|
let assignedRole = role;
|
|
47
48
|
if (role === "client") {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
if (prior && prior.instanceId === instanceId && prior.role.match(/^client-\d+$/)) {
|
|
50
|
+
assignedRole = prior.role;
|
|
51
|
+
} else {
|
|
52
|
+
const used = /* @__PURE__ */ new Set();
|
|
53
|
+
for (const inst of this.instances.values()) {
|
|
54
|
+
if (inst.instanceId !== instanceId || inst.pluginSessionId === pluginSessionId)
|
|
55
|
+
continue;
|
|
56
|
+
const match = inst.role.match(/^client-(\d+)$/);
|
|
57
|
+
if (match)
|
|
58
|
+
used.add(Number(match[1]));
|
|
59
|
+
}
|
|
60
|
+
let idx = 1;
|
|
61
|
+
while (used.has(idx))
|
|
62
|
+
idx++;
|
|
63
|
+
assignedRole = `client-${idx}`;
|
|
53
64
|
}
|
|
54
|
-
let idx = 1;
|
|
55
|
-
while (used.has(idx))
|
|
56
|
-
idx++;
|
|
57
|
-
assignedRole = `client-${idx}`;
|
|
58
65
|
}
|
|
59
66
|
const existing = Array.from(this.instances.values()).find((i) => i.instanceId === instanceId && i.role === assignedRole && i.pluginSessionId !== pluginSessionId);
|
|
60
67
|
if (existing) {
|
|
@@ -76,7 +83,7 @@ var init_bridge_service = __esm({
|
|
|
76
83
|
dataModelName: input.dataModelName ?? "",
|
|
77
84
|
isRunning: input.isRunning ?? false,
|
|
78
85
|
lastActivity: Date.now(),
|
|
79
|
-
connectedAt: Date.now()
|
|
86
|
+
connectedAt: prior?.connectedAt ?? Date.now()
|
|
80
87
|
});
|
|
81
88
|
return { ok: true, assignedRole, instanceId };
|
|
82
89
|
}
|
|
@@ -214,7 +221,7 @@ var init_bridge_service = __esm({
|
|
|
214
221
|
}
|
|
215
222
|
if (distinctInstanceIds.size > 1) {
|
|
216
223
|
const errorCode = role ? "ambiguous_target" : "multiple_instances_connected";
|
|
217
|
-
const msg = role ? `target=${role} is ambiguous
|
|
224
|
+
const msg = role ? `target=${role} is ambiguous because multiple Studio places are connected. Pass instance_id to choose a place.` : "Multiple Studio places are connected. Pass instance_id to disambiguate.";
|
|
218
225
|
return { ok: false, error: { code: errorCode, message: msg, data: errorData } };
|
|
219
226
|
}
|
|
220
227
|
const onlyInstanceId = instances[0].instanceId;
|
|
@@ -236,6 +243,7 @@ var init_bridge_service = __esm({
|
|
|
236
243
|
targetInstanceId,
|
|
237
244
|
targetRole,
|
|
238
245
|
timestamp: Date.now(),
|
|
246
|
+
inFlight: false,
|
|
239
247
|
resolve: resolve2,
|
|
240
248
|
reject,
|
|
241
249
|
timeoutId
|
|
@@ -250,11 +258,14 @@ var init_bridge_service = __esm({
|
|
|
250
258
|
continue;
|
|
251
259
|
if (request.targetRole !== callerRole)
|
|
252
260
|
continue;
|
|
261
|
+
if (request.inFlight)
|
|
262
|
+
continue;
|
|
253
263
|
if (!oldestRequest || request.timestamp < oldestRequest.timestamp) {
|
|
254
264
|
oldestRequest = request;
|
|
255
265
|
}
|
|
256
266
|
}
|
|
257
267
|
if (oldestRequest) {
|
|
268
|
+
oldestRequest.inFlight = true;
|
|
258
269
|
return {
|
|
259
270
|
requestId: oldestRequest.id,
|
|
260
271
|
request: {
|
|
@@ -677,6 +688,11 @@ var init_http_server = __esm({
|
|
|
677
688
|
start_playtest: (tools, body) => tools.startPlaytest(body.mode, body.numPlayers, body.instance_id),
|
|
678
689
|
stop_playtest: (tools, body) => tools.stopPlaytest(body.instance_id),
|
|
679
690
|
get_playtest_output: (tools, body) => tools.getPlaytestOutput(body.target, body.instance_id),
|
|
691
|
+
multiplayer_test_start: (tools, body) => tools.multiplayerTestStart(body.numPlayers, body.testArgs, body.timeout, body.instance_id),
|
|
692
|
+
multiplayer_test_state: (tools, body) => tools.multiplayerTestState(body.instance_id),
|
|
693
|
+
multiplayer_test_add_players: (tools, body) => tools.multiplayerTestAddPlayers(body.numPlayers, body.timeout, body.instance_id),
|
|
694
|
+
multiplayer_test_leave_client: (tools, body) => tools.multiplayerTestLeaveClient(body.target, body.timeout, body.instance_id),
|
|
695
|
+
multiplayer_test_end: (tools, body) => tools.multiplayerTestEnd(body.value, body.timeout, body.instance_id),
|
|
680
696
|
get_runtime_logs: (tools, body) => tools.getRuntimeLogs(body.target, body.since, body.tail, body.filter, body.instance_id),
|
|
681
697
|
get_connected_instances: (tools) => tools.getConnectedInstances(),
|
|
682
698
|
export_build: (tools, body) => tools.exportBuild(body.instancePath, body.outputId, body.style, body.instance_id),
|
|
@@ -705,6 +721,7 @@ var init_http_server = __esm({
|
|
|
705
721
|
simulate_keyboard_input: (tools, body) => tools.simulateKeyboardInput(body.keyCode, body.action, body.duration, body.text, body.target, body.instance_id),
|
|
706
722
|
character_navigation: (tools, body) => tools.characterNavigation(body.position, body.instancePath, body.waitForCompletion, body.timeout, body.target, body.instance_id),
|
|
707
723
|
get_memory_breakdown: (tools, body) => tools.getMemoryBreakdown(body.target, body.tags, body.instance_id),
|
|
724
|
+
get_scene_analysis: (tools, body) => tools.getSceneAnalysis(body.mode, body.target, body.topN, body.raw, body.instance_id),
|
|
708
725
|
export_rbxm: (tools, body) => tools.exportRbxm(body.instance_paths, body.output_path, body.target, body.instance_id),
|
|
709
726
|
import_rbxm: (tools, body) => tools.importRbxm(body.source, body.parent_path, body.target, body.instance_id),
|
|
710
727
|
find_and_replace_in_scripts: (tools, body) => tools.findAndReplaceInScripts(body.pattern, body.replacement, {
|
|
@@ -2714,6 +2731,9 @@ function parseBridgeResponse(response) {
|
|
|
2714
2731
|
}
|
|
2715
2732
|
return JSON.stringify(response);
|
|
2716
2733
|
}
|
|
2734
|
+
function sleep(ms) {
|
|
2735
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2736
|
+
}
|
|
2717
2737
|
var SERVER_LOCAL_NAME, CLIENT_LOCAL_NAME, EVAL_WRAPPER_LINE_OFFSET, RobloxStudioTools;
|
|
2718
2738
|
var init_tools = __esm({
|
|
2719
2739
|
"../core/dist/tools/index.js"() {
|
|
@@ -2774,6 +2794,110 @@ var init_tools = __esm({
|
|
|
2774
2794
|
const clientRoles = roles.filter((role) => role.startsWith("client")).sort();
|
|
2775
2795
|
return { instanceId: resolvedId, clientRole: clientRoles[0] };
|
|
2776
2796
|
}
|
|
2797
|
+
_resolveInstanceIdOnly(instance_id) {
|
|
2798
|
+
const instances = this.bridge.getInstances();
|
|
2799
|
+
const publicList = this.bridge.getPublicInstances();
|
|
2800
|
+
const errorData = { instances: publicList, count: publicList.length };
|
|
2801
|
+
if (instance_id !== void 0) {
|
|
2802
|
+
if (!instances.some((i) => i.instanceId === instance_id)) {
|
|
2803
|
+
throw new RoutingFailure({
|
|
2804
|
+
code: "unrecognized_instance_id",
|
|
2805
|
+
message: `instance_id "${instance_id}" is not connected. Pass one from data.instances.`,
|
|
2806
|
+
data: errorData
|
|
2807
|
+
});
|
|
2808
|
+
}
|
|
2809
|
+
return instance_id;
|
|
2810
|
+
}
|
|
2811
|
+
const distinct = Array.from(new Set(instances.map((i) => i.instanceId)));
|
|
2812
|
+
if (distinct.length === 0) {
|
|
2813
|
+
throw new RoutingFailure({
|
|
2814
|
+
code: "unrecognized_instance_id",
|
|
2815
|
+
message: "No Studio plugin is connected.",
|
|
2816
|
+
data: errorData
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
if (distinct.length > 1) {
|
|
2820
|
+
throw new RoutingFailure({
|
|
2821
|
+
code: "multiple_instances_connected",
|
|
2822
|
+
message: "Multiple Studio places are connected. Pass instance_id to disambiguate.",
|
|
2823
|
+
data: errorData
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
return distinct[0];
|
|
2827
|
+
}
|
|
2828
|
+
_resolveSingleTarget(target, instance_id) {
|
|
2829
|
+
const resolved = this.bridge.resolveTarget({ instance_id, target });
|
|
2830
|
+
if (!resolved.ok)
|
|
2831
|
+
throw new RoutingFailure(resolved.error);
|
|
2832
|
+
if (resolved.mode !== "single") {
|
|
2833
|
+
throw new RoutingFailure({
|
|
2834
|
+
code: "target_role_not_present_on_instance",
|
|
2835
|
+
message: "Pick a specific target role for this tool.",
|
|
2836
|
+
data: {
|
|
2837
|
+
instances: this.bridge.getPublicInstances(),
|
|
2838
|
+
count: this.bridge.getInstances().length
|
|
2839
|
+
}
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
2842
|
+
return { instanceId: resolved.targetInstanceId, role: resolved.targetRole };
|
|
2843
|
+
}
|
|
2844
|
+
_rolesForInstance(instanceId) {
|
|
2845
|
+
return this.bridge.getInstances().filter((i) => i.instanceId === instanceId).map((i) => i.role);
|
|
2846
|
+
}
|
|
2847
|
+
_clientRolesForInstance(instanceId) {
|
|
2848
|
+
return this._rolesForInstance(instanceId).filter((role) => /^client-\d+$/.test(role)).sort((a, b) => Number(a.slice("client-".length)) - Number(b.slice("client-".length)));
|
|
2849
|
+
}
|
|
2850
|
+
async _waitForRuntimeRoles(instanceId, opts, timeoutSec = 30) {
|
|
2851
|
+
const deadline = Date.now() + timeoutSec * 1e3;
|
|
2852
|
+
while (Date.now() < deadline) {
|
|
2853
|
+
const roles = this._rolesForInstance(instanceId);
|
|
2854
|
+
const hasServer = !opts.server || roles.includes("server");
|
|
2855
|
+
const hasClients = opts.clientCount === void 0 || this._clientRolesForInstance(instanceId).length >= opts.clientCount;
|
|
2856
|
+
const absent = opts.absentRole === void 0 || !roles.includes(opts.absentRole);
|
|
2857
|
+
const runtimeAbsent = !opts.noRuntime || !roles.some((role) => role === "server" || /^client-\d+$/.test(role));
|
|
2858
|
+
if (hasServer && hasClients && absent && runtimeAbsent) {
|
|
2859
|
+
return { ok: true, roles, timedOut: false };
|
|
2860
|
+
}
|
|
2861
|
+
await sleep(250);
|
|
2862
|
+
}
|
|
2863
|
+
return { ok: false, roles: this._rolesForInstance(instanceId), timedOut: true };
|
|
2864
|
+
}
|
|
2865
|
+
async _waitForExactClientCount(instanceId, expectedClientCount, timeoutSec = 30, stableMs = 3e3) {
|
|
2866
|
+
const deadline = Date.now() + timeoutSec * 1e3;
|
|
2867
|
+
let exactSince;
|
|
2868
|
+
while (Date.now() < deadline) {
|
|
2869
|
+
const roles2 = this._rolesForInstance(instanceId);
|
|
2870
|
+
const clientCount2 = this._clientRolesForInstance(instanceId).length;
|
|
2871
|
+
if (clientCount2 > expectedClientCount) {
|
|
2872
|
+
return { ok: false, roles: roles2, timedOut: false, extraClients: true, clientCount: clientCount2 };
|
|
2873
|
+
}
|
|
2874
|
+
if (roles2.includes("server") && clientCount2 === expectedClientCount) {
|
|
2875
|
+
exactSince ??= Date.now();
|
|
2876
|
+
if (Date.now() - exactSince >= stableMs) {
|
|
2877
|
+
return { ok: true, roles: roles2, timedOut: false, extraClients: false, clientCount: clientCount2 };
|
|
2878
|
+
}
|
|
2879
|
+
} else {
|
|
2880
|
+
exactSince = void 0;
|
|
2881
|
+
}
|
|
2882
|
+
await sleep(250);
|
|
2883
|
+
}
|
|
2884
|
+
const roles = this._rolesForInstance(instanceId);
|
|
2885
|
+
const clientCount = this._clientRolesForInstance(instanceId).length;
|
|
2886
|
+
return { ok: false, roles, timedOut: true, extraClients: clientCount > expectedClientCount, clientCount };
|
|
2887
|
+
}
|
|
2888
|
+
async _waitForRuntimeRolesFresh(instanceId, connectedAfter, requiredRoles, timeoutSec = 60) {
|
|
2889
|
+
const deadline = Date.now() + timeoutSec * 1e3;
|
|
2890
|
+
while (Date.now() < deadline) {
|
|
2891
|
+
const instances = this.bridge.getInstances().filter((i) => i.instanceId === instanceId);
|
|
2892
|
+
const roles = instances.map((i) => i.role);
|
|
2893
|
+
const freshRoles = new Set(instances.filter((i) => i.connectedAt >= connectedAfter).map((i) => i.role));
|
|
2894
|
+
if (requiredRoles.every((role) => freshRoles.has(role))) {
|
|
2895
|
+
return { ok: true, roles, timedOut: false };
|
|
2896
|
+
}
|
|
2897
|
+
await sleep(250);
|
|
2898
|
+
}
|
|
2899
|
+
return { ok: false, roles: this._rolesForInstance(instanceId), timedOut: true };
|
|
2900
|
+
}
|
|
2777
2901
|
async getFileTree(path2 = "", instance_id) {
|
|
2778
2902
|
const response = await this._callSingle("/api/file-tree", { path: path2 }, void 0, instance_id);
|
|
2779
2903
|
return {
|
|
@@ -3418,16 +3542,41 @@ ${code}`
|
|
|
3418
3542
|
if (mode !== "play" && mode !== "run") {
|
|
3419
3543
|
throw new Error('mode must be "play" or "run"');
|
|
3420
3544
|
}
|
|
3421
|
-
const data = { mode };
|
|
3422
3545
|
if (numPlayers !== void 0) {
|
|
3423
|
-
|
|
3546
|
+
throw new Error("start_playtest is single-player only. Use multiplayer_test_start for multi-client StudioTestService sessions.");
|
|
3547
|
+
}
|
|
3548
|
+
const data = { mode };
|
|
3549
|
+
const startedAt = Date.now();
|
|
3550
|
+
const resolved = this.bridge.resolveTarget({ instance_id, target: void 0 });
|
|
3551
|
+
if (!resolved.ok)
|
|
3552
|
+
throw new RoutingFailure(resolved.error);
|
|
3553
|
+
if (resolved.mode !== "single") {
|
|
3554
|
+
throw new RoutingFailure({
|
|
3555
|
+
code: "target_role_not_present_on_instance",
|
|
3556
|
+
message: "This tool does not support target=all. Pick a specific role or omit target.",
|
|
3557
|
+
data: {
|
|
3558
|
+
instances: this.bridge.getPublicInstances(),
|
|
3559
|
+
count: this.bridge.getInstances().length
|
|
3560
|
+
}
|
|
3561
|
+
});
|
|
3424
3562
|
}
|
|
3425
|
-
const response = await this.
|
|
3563
|
+
const response = await this.client.request("/api/start-playtest", data, resolved.targetInstanceId, resolved.targetRole);
|
|
3564
|
+
let wait;
|
|
3565
|
+
if (response?.success === true) {
|
|
3566
|
+
const requiredRoles = mode === "play" ? ["server", "client-1"] : ["server"];
|
|
3567
|
+
wait = await this._waitForRuntimeRolesFresh(resolved.targetInstanceId, startedAt, requiredRoles);
|
|
3568
|
+
}
|
|
3569
|
+
const body = wait ? {
|
|
3570
|
+
...response,
|
|
3571
|
+
runtimeReady: wait.ok,
|
|
3572
|
+
timedOut: wait.timedOut,
|
|
3573
|
+
roles: wait.roles
|
|
3574
|
+
} : response;
|
|
3426
3575
|
return {
|
|
3427
3576
|
content: [
|
|
3428
3577
|
{
|
|
3429
3578
|
type: "text",
|
|
3430
|
-
text: JSON.stringify(
|
|
3579
|
+
text: JSON.stringify(body)
|
|
3431
3580
|
}
|
|
3432
3581
|
]
|
|
3433
3582
|
};
|
|
@@ -3449,6 +3598,196 @@ ${code}`
|
|
|
3449
3598
|
]
|
|
3450
3599
|
};
|
|
3451
3600
|
}
|
|
3601
|
+
async _buildMultiplayerState(instanceId) {
|
|
3602
|
+
const peers = this.bridge.getPublicInstances().filter((i) => i.instanceId === instanceId).sort((a, b) => a.role.localeCompare(b.role));
|
|
3603
|
+
const body = {
|
|
3604
|
+
instanceId,
|
|
3605
|
+
peers,
|
|
3606
|
+
peerCount: peers.length
|
|
3607
|
+
};
|
|
3608
|
+
const edit = peers.find((p) => p.role === "edit");
|
|
3609
|
+
const server = peers.find((p) => p.role === "server");
|
|
3610
|
+
let editState;
|
|
3611
|
+
let serverState;
|
|
3612
|
+
if (edit) {
|
|
3613
|
+
try {
|
|
3614
|
+
editState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "edit");
|
|
3615
|
+
body.edit = editState;
|
|
3616
|
+
} catch (err) {
|
|
3617
|
+
body.edit = { error: err instanceof Error ? err.message : String(err) };
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
if (server) {
|
|
3621
|
+
try {
|
|
3622
|
+
serverState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "server");
|
|
3623
|
+
body.server = serverState;
|
|
3624
|
+
} catch (err) {
|
|
3625
|
+
body.server = { error: err instanceof Error ? err.message : String(err) };
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
const session = editState?.session;
|
|
3629
|
+
const rawPhase = typeof session?.phase === "string" ? session.phase : void 0;
|
|
3630
|
+
const hasRuntime = peers.some((p) => p.role === "server" || p.role.startsWith("client-"));
|
|
3631
|
+
body.phase = rawPhase === "starting" && hasRuntime ? "running" : rawPhase ?? (hasRuntime ? "running" : "idle");
|
|
3632
|
+
body.testId = session?.testId;
|
|
3633
|
+
body.numPlayers = session?.numPlayers;
|
|
3634
|
+
body.testArgs = session?.testArgs ?? serverState?.testArgs;
|
|
3635
|
+
body.result = session?.result;
|
|
3636
|
+
body.error = session?.error;
|
|
3637
|
+
body.players = serverState?.players ?? [];
|
|
3638
|
+
body.playerCount = serverState?.playerCount ?? 0;
|
|
3639
|
+
body.clientRoles = this._clientRolesForInstance(instanceId);
|
|
3640
|
+
return body;
|
|
3641
|
+
}
|
|
3642
|
+
async _waitForMultiplayerEditDone(instanceId, timeoutSec = 30) {
|
|
3643
|
+
const deadline = Date.now() + timeoutSec * 1e3;
|
|
3644
|
+
while (Date.now() < deadline) {
|
|
3645
|
+
if (!this._rolesForInstance(instanceId).includes("edit"))
|
|
3646
|
+
return false;
|
|
3647
|
+
try {
|
|
3648
|
+
const editState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "edit");
|
|
3649
|
+
const phase = editState?.session?.phase;
|
|
3650
|
+
if (phase === "completed" || phase === "failed")
|
|
3651
|
+
return true;
|
|
3652
|
+
} catch {
|
|
3653
|
+
}
|
|
3654
|
+
await sleep(250);
|
|
3655
|
+
}
|
|
3656
|
+
return false;
|
|
3657
|
+
}
|
|
3658
|
+
async _isMultiplayerTestRunning(instanceId) {
|
|
3659
|
+
if (!this._rolesForInstance(instanceId).includes("edit"))
|
|
3660
|
+
return false;
|
|
3661
|
+
try {
|
|
3662
|
+
const editState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "edit");
|
|
3663
|
+
const phase = editState?.session?.phase;
|
|
3664
|
+
return phase === "starting" || phase === "running";
|
|
3665
|
+
} catch {
|
|
3666
|
+
return false;
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
async _waitForMultiplayerStart(instanceId, clientCount, timeoutSec = 30) {
|
|
3670
|
+
const deadline = Date.now() + timeoutSec * 1e3;
|
|
3671
|
+
while (Date.now() < deadline) {
|
|
3672
|
+
const exact = await this._waitForExactClientCount(instanceId, clientCount, 0.25, 0);
|
|
3673
|
+
if (exact.ok || exact.extraClients) {
|
|
3674
|
+
return { ok: exact.ok, roles: exact.roles, timedOut: false, error: exact.extraClients ? `Expected ${clientCount} client(s), but Studio registered ${exact.clientCount}.` : void 0 };
|
|
3675
|
+
}
|
|
3676
|
+
try {
|
|
3677
|
+
const editState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "edit");
|
|
3678
|
+
const session = editState?.session;
|
|
3679
|
+
if (session?.phase === "failed" || session?.phase === "completed") {
|
|
3680
|
+
return { ok: false, roles: this._rolesForInstance(instanceId), timedOut: false, phase: session.phase, error: session.error };
|
|
3681
|
+
}
|
|
3682
|
+
} catch {
|
|
3683
|
+
}
|
|
3684
|
+
await sleep(250);
|
|
3685
|
+
}
|
|
3686
|
+
return { ok: false, roles: this._rolesForInstance(instanceId), timedOut: true };
|
|
3687
|
+
}
|
|
3688
|
+
async multiplayerTestStart(numPlayers, testArgs, timeout, instance_id) {
|
|
3689
|
+
if (!Number.isInteger(numPlayers) || numPlayers < 1 || numPlayers > 8) {
|
|
3690
|
+
throw new Error("numPlayers must be an integer from 1 to 8");
|
|
3691
|
+
}
|
|
3692
|
+
const editTarget = this._resolveSingleTarget("edit", instance_id);
|
|
3693
|
+
const response = await this.client.request("/api/multiplayer-test-start", { numPlayers, testArgs: testArgs ?? {} }, editTarget.instanceId, editTarget.role);
|
|
3694
|
+
if (response?.error) {
|
|
3695
|
+
return { content: [{ type: "text", text: JSON.stringify(response) }] };
|
|
3696
|
+
}
|
|
3697
|
+
const wait = await this._waitForMultiplayerStart(editTarget.instanceId, numPlayers, timeout ?? 30);
|
|
3698
|
+
const state = await this._buildMultiplayerState(editTarget.instanceId);
|
|
3699
|
+
return {
|
|
3700
|
+
content: [{
|
|
3701
|
+
type: "text",
|
|
3702
|
+
text: JSON.stringify({
|
|
3703
|
+
...response,
|
|
3704
|
+
ready: wait.ok,
|
|
3705
|
+
timedOut: wait.timedOut,
|
|
3706
|
+
wait,
|
|
3707
|
+
roles: wait.roles,
|
|
3708
|
+
state
|
|
3709
|
+
})
|
|
3710
|
+
}]
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3713
|
+
async multiplayerTestState(instance_id) {
|
|
3714
|
+
const instanceId = this._resolveInstanceIdOnly(instance_id);
|
|
3715
|
+
const state = await this._buildMultiplayerState(instanceId);
|
|
3716
|
+
return { content: [{ type: "text", text: JSON.stringify(state) }] };
|
|
3717
|
+
}
|
|
3718
|
+
async multiplayerTestAddPlayers(numPlayers, timeout, instance_id) {
|
|
3719
|
+
if (!Number.isInteger(numPlayers) || numPlayers < 1 || numPlayers > 8) {
|
|
3720
|
+
throw new Error("numPlayers must be an integer from 1 to 8");
|
|
3721
|
+
}
|
|
3722
|
+
const serverTarget = this._resolveSingleTarget("server", instance_id);
|
|
3723
|
+
const before = this._clientRolesForInstance(serverTarget.instanceId).length;
|
|
3724
|
+
const response = await this.client.request("/api/multiplayer-test-add-players", { numPlayers, timeout: timeout ?? 10 }, serverTarget.instanceId, serverTarget.role);
|
|
3725
|
+
if (response?.error) {
|
|
3726
|
+
return { content: [{ type: "text", text: JSON.stringify(response) }] };
|
|
3727
|
+
}
|
|
3728
|
+
const wait = await this._waitForExactClientCount(serverTarget.instanceId, before + numPlayers, timeout ?? 30);
|
|
3729
|
+
const state = await this._buildMultiplayerState(serverTarget.instanceId);
|
|
3730
|
+
return {
|
|
3731
|
+
content: [{
|
|
3732
|
+
type: "text",
|
|
3733
|
+
text: JSON.stringify({
|
|
3734
|
+
...response,
|
|
3735
|
+
ready: wait.ok,
|
|
3736
|
+
timedOut: wait.timedOut,
|
|
3737
|
+
wait,
|
|
3738
|
+
roles: wait.roles,
|
|
3739
|
+
state
|
|
3740
|
+
})
|
|
3741
|
+
}]
|
|
3742
|
+
};
|
|
3743
|
+
}
|
|
3744
|
+
async multiplayerTestLeaveClient(target = "client-1", timeout, instance_id) {
|
|
3745
|
+
if (!/^client-\d+$/.test(target)) {
|
|
3746
|
+
throw new Error(`multiplayer_test_leave_client requires target=client-N (got: ${target})`);
|
|
3747
|
+
}
|
|
3748
|
+
const clientTarget = this._resolveSingleTarget(target, instance_id);
|
|
3749
|
+
const response = await this.client.request("/api/multiplayer-test-leave-client", {}, clientTarget.instanceId, clientTarget.role);
|
|
3750
|
+
if (response?.error) {
|
|
3751
|
+
return { content: [{ type: "text", text: JSON.stringify(response) }] };
|
|
3752
|
+
}
|
|
3753
|
+
const wait = await this._waitForRuntimeRoles(clientTarget.instanceId, { absentRole: clientTarget.role }, timeout ?? 30);
|
|
3754
|
+
const state = await this._buildMultiplayerState(clientTarget.instanceId);
|
|
3755
|
+
return {
|
|
3756
|
+
content: [{
|
|
3757
|
+
type: "text",
|
|
3758
|
+
text: JSON.stringify({
|
|
3759
|
+
...response,
|
|
3760
|
+
left: wait.ok,
|
|
3761
|
+
timedOut: wait.timedOut,
|
|
3762
|
+
roles: wait.roles,
|
|
3763
|
+
state
|
|
3764
|
+
})
|
|
3765
|
+
}]
|
|
3766
|
+
};
|
|
3767
|
+
}
|
|
3768
|
+
async multiplayerTestEnd(value, timeout, instance_id) {
|
|
3769
|
+
const serverTarget = this._resolveSingleTarget("server", instance_id);
|
|
3770
|
+
const response = await this.client.request("/api/multiplayer-test-end", { value: value ?? "ended_by_mcp" }, serverTarget.instanceId, serverTarget.role);
|
|
3771
|
+
if (response?.error) {
|
|
3772
|
+
return { content: [{ type: "text", text: JSON.stringify(response) }] };
|
|
3773
|
+
}
|
|
3774
|
+
const editDone = await this._waitForMultiplayerEditDone(serverTarget.instanceId, timeout ?? 30);
|
|
3775
|
+
const wait = await this._waitForRuntimeRoles(serverTarget.instanceId, { noRuntime: true }, timeout ?? 30);
|
|
3776
|
+
const state = await this._buildMultiplayerState(serverTarget.instanceId);
|
|
3777
|
+
return {
|
|
3778
|
+
content: [{
|
|
3779
|
+
type: "text",
|
|
3780
|
+
text: JSON.stringify({
|
|
3781
|
+
...response,
|
|
3782
|
+
ended: wait.ok,
|
|
3783
|
+
editDone,
|
|
3784
|
+
timedOut: wait.timedOut,
|
|
3785
|
+
roles: wait.roles,
|
|
3786
|
+
state
|
|
3787
|
+
})
|
|
3788
|
+
}]
|
|
3789
|
+
};
|
|
3790
|
+
}
|
|
3452
3791
|
async getConnectedInstances() {
|
|
3453
3792
|
const instances = this.bridge.getPublicInstances();
|
|
3454
3793
|
return {
|
|
@@ -3484,15 +3823,15 @@ ${code}`
|
|
|
3484
3823
|
}
|
|
3485
3824
|
static findProjectRoot(startDir) {
|
|
3486
3825
|
let dir = path.resolve(startDir);
|
|
3487
|
-
|
|
3826
|
+
let previous = "";
|
|
3827
|
+
while (dir !== previous) {
|
|
3488
3828
|
if (fs.existsSync(path.join(dir, ".git")) || fs.existsSync(path.join(dir, "package.json"))) {
|
|
3489
3829
|
return dir;
|
|
3490
3830
|
}
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
return null;
|
|
3494
|
-
dir = parent;
|
|
3831
|
+
previous = dir;
|
|
3832
|
+
dir = path.dirname(dir);
|
|
3495
3833
|
}
|
|
3834
|
+
return null;
|
|
3496
3835
|
}
|
|
3497
3836
|
static isDirectory(candidate) {
|
|
3498
3837
|
if (!candidate)
|
|
@@ -4302,6 +4641,39 @@ ${code}`
|
|
|
4302
4641
|
}
|
|
4303
4642
|
return { content: [{ type: "text", text: JSON.stringify(body) }] };
|
|
4304
4643
|
}
|
|
4644
|
+
async getSceneAnalysis(mode, target, topN, raw, instance_id) {
|
|
4645
|
+
const tgt = target ?? "all";
|
|
4646
|
+
const data = {};
|
|
4647
|
+
if (mode !== void 0)
|
|
4648
|
+
data.mode = mode;
|
|
4649
|
+
if (topN !== void 0)
|
|
4650
|
+
data.topN = topN;
|
|
4651
|
+
if (raw !== void 0)
|
|
4652
|
+
data.raw = raw;
|
|
4653
|
+
const resolved = this.bridge.resolveTarget({ instance_id, target: tgt });
|
|
4654
|
+
if (!resolved.ok)
|
|
4655
|
+
throw new RoutingFailure(resolved.error);
|
|
4656
|
+
if (resolved.mode === "single") {
|
|
4657
|
+
const response = await this.client.request("/api/get-scene-analysis", data, resolved.targetInstanceId, resolved.targetRole);
|
|
4658
|
+
return { content: [{ type: "text", text: JSON.stringify(response) }] };
|
|
4659
|
+
}
|
|
4660
|
+
const targets = resolved.targets.filter((t) => t.targetRole !== "edit-proxy");
|
|
4661
|
+
const responses = await Promise.allSettled(targets.map(async (t) => ({
|
|
4662
|
+
peer: t.targetRole,
|
|
4663
|
+
result: await this.client.request("/api/get-scene-analysis", data, t.targetInstanceId, t.targetRole)
|
|
4664
|
+
})));
|
|
4665
|
+
const body = {};
|
|
4666
|
+
for (let i = 0; i < responses.length; i++) {
|
|
4667
|
+
const r = responses[i];
|
|
4668
|
+
const peer = targets[i].targetRole;
|
|
4669
|
+
if (r.status === "fulfilled") {
|
|
4670
|
+
body[peer] = r.value.result;
|
|
4671
|
+
} else {
|
|
4672
|
+
body[peer] = { error: "disconnected" };
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
return { content: [{ type: "text", text: JSON.stringify(body) }] };
|
|
4676
|
+
}
|
|
4305
4677
|
async exportRbxm(instancePaths, outputPath, target, instance_id) {
|
|
4306
4678
|
if (!Array.isArray(instancePaths) || instancePaths.length === 0) {
|
|
4307
4679
|
throw new Error("instance_paths must be a non-empty array for export_rbxm");
|
|
@@ -4425,10 +4797,14 @@ ${code}`
|
|
|
4425
4797
|
response = await this._callSingle("/api/capture-screenshot", {}, "edit", instanceId);
|
|
4426
4798
|
}
|
|
4427
4799
|
if (response.error) {
|
|
4800
|
+
let text = response.error;
|
|
4801
|
+
if (clientRole && response.error.includes("Failed to load texture, unexpected format") && await this._isMultiplayerTestRunning(instanceId)) {
|
|
4802
|
+
text = `Screenshot capture reached the multiplayer client, but Roblox returned a temporary screenshot texture that the edit peer cannot read in StudioTestService multiplayer sessions. Regular start_playtest capture works because the temporary rbxtemp:// handle is readable from the edit process; multiplayer client handles appear to be scoped to the client process. Raw error: ${response.error}`;
|
|
4803
|
+
}
|
|
4428
4804
|
return {
|
|
4429
4805
|
content: [{
|
|
4430
4806
|
type: "text",
|
|
4431
|
-
text
|
|
4807
|
+
text
|
|
4432
4808
|
}]
|
|
4433
4809
|
};
|
|
4434
4810
|
}
|
|
@@ -4440,7 +4816,9 @@ ${code}`
|
|
|
4440
4816
|
const fmt = format === "png" ? "png" : "jpeg";
|
|
4441
4817
|
const q = quality === void 0 ? 92 : Math.max(1, Math.min(100, Math.floor(quality)));
|
|
4442
4818
|
const MAX_IMAGE_BYTES = 6e6;
|
|
4443
|
-
|
|
4819
|
+
const encoded = encodeImageFromRgbaResponse(response, fmt, q);
|
|
4820
|
+
let { buffer } = encoded;
|
|
4821
|
+
const { mimeType } = encoded;
|
|
4444
4822
|
let usedQ = q;
|
|
4445
4823
|
let note = "";
|
|
4446
4824
|
if (buffer.length > MAX_IMAGE_BYTES) {
|
|
@@ -5607,7 +5985,7 @@ var init_definitions = __esm({
|
|
|
5607
5985
|
{
|
|
5608
5986
|
name: "execute_luau",
|
|
5609
5987
|
category: "write",
|
|
5610
|
-
description:
|
|
5988
|
+
description: 'Execute Luau code in plugin context. target="server" and target="client-N" run against live runtime DataModels with PluginSecurity permissions; use eval_*_runtime instead when you need the game Script/LocalScript VM require cache. Use print()/warn() for output. Return value is captured.',
|
|
5611
5989
|
inputSchema: {
|
|
5612
5990
|
type: "object",
|
|
5613
5991
|
properties: {
|
|
@@ -5630,7 +6008,7 @@ var init_definitions = __esm({
|
|
|
5630
6008
|
{
|
|
5631
6009
|
name: "eval_server_runtime",
|
|
5632
6010
|
category: "write",
|
|
5633
|
-
description: "Execute Luau on the server peer in the running game's Script VM (shares require cache with user game scripts
|
|
6011
|
+
description: "Execute Luau on the server peer in the running game's Script VM (shares require cache with user game scripts, unlike execute_luau target=server which runs in plugin context). Requires a running playtest; the bridge is installed automatically (including for playtests started manually via the Studio Play button).",
|
|
5634
6012
|
inputSchema: {
|
|
5635
6013
|
type: "object",
|
|
5636
6014
|
properties: {
|
|
@@ -5649,7 +6027,7 @@ var init_definitions = __esm({
|
|
|
5649
6027
|
{
|
|
5650
6028
|
name: "eval_client_runtime",
|
|
5651
6029
|
category: "write",
|
|
5652
|
-
description: "Execute Luau on a client peer in the running game's LocalScript VM (shares require cache with user game scripts
|
|
6030
|
+
description: "Execute Luau on a client peer in the running game's LocalScript VM (shares require cache with user game scripts, unlike execute_luau target=client-N which runs in plugin context). Requires a running playtest; the bridge is installed automatically (including for playtests started manually via the Studio Play button).",
|
|
5653
6031
|
inputSchema: {
|
|
5654
6032
|
type: "object",
|
|
5655
6033
|
properties: {
|
|
@@ -5726,7 +6104,7 @@ var init_definitions = __esm({
|
|
|
5726
6104
|
{
|
|
5727
6105
|
name: "start_playtest",
|
|
5728
6106
|
category: "write",
|
|
5729
|
-
description: "Start playtest. Captures print/warn/error via LogService. Poll with get_playtest_output, end with stop_playtest.
|
|
6107
|
+
description: "Start a simple single-player Studio playtest in play or run mode, waiting until a runtime peer registers with MCP. Captures print/warn/error via LogService. Poll with get_playtest_output, end with stop_playtest. For multi-client testing use multiplayer_test_start instead.",
|
|
5730
6108
|
inputSchema: {
|
|
5731
6109
|
type: "object",
|
|
5732
6110
|
properties: {
|
|
@@ -5737,7 +6115,7 @@ var init_definitions = __esm({
|
|
|
5737
6115
|
},
|
|
5738
6116
|
numPlayers: {
|
|
5739
6117
|
type: "number",
|
|
5740
|
-
description: "
|
|
6118
|
+
description: "Deprecated and rejected. Use multiplayer_test_start for multi-client testing."
|
|
5741
6119
|
},
|
|
5742
6120
|
instance_id: {
|
|
5743
6121
|
type: "string",
|
|
@@ -5779,6 +6157,112 @@ var init_definitions = __esm({
|
|
|
5779
6157
|
}
|
|
5780
6158
|
}
|
|
5781
6159
|
},
|
|
6160
|
+
{
|
|
6161
|
+
name: "multiplayer_test_start",
|
|
6162
|
+
category: "write",
|
|
6163
|
+
description: "Start a StudioTestService multiplayer test and wait for the server plus requested client peers to connect. Use this for multi-client runtime testing.",
|
|
6164
|
+
inputSchema: {
|
|
6165
|
+
type: "object",
|
|
6166
|
+
properties: {
|
|
6167
|
+
numPlayers: {
|
|
6168
|
+
type: "number",
|
|
6169
|
+
description: "Number of client players to start (1-8)."
|
|
6170
|
+
},
|
|
6171
|
+
testArgs: {
|
|
6172
|
+
description: "JSON-compatible table passed to StudioTestService:GetTestArgs() on server and clients."
|
|
6173
|
+
},
|
|
6174
|
+
timeout: {
|
|
6175
|
+
type: "number",
|
|
6176
|
+
description: "Max seconds to wait for server + clients to register (default 30)."
|
|
6177
|
+
},
|
|
6178
|
+
instance_id: {
|
|
6179
|
+
type: "string",
|
|
6180
|
+
description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
|
|
6181
|
+
}
|
|
6182
|
+
},
|
|
6183
|
+
required: ["numPlayers"]
|
|
6184
|
+
}
|
|
6185
|
+
},
|
|
6186
|
+
{
|
|
6187
|
+
name: "multiplayer_test_state",
|
|
6188
|
+
category: "read",
|
|
6189
|
+
description: "Get the active multiplayer StudioTestService state for a place: phase, peers, players, original testArgs, result/error, and connected client roles.",
|
|
6190
|
+
inputSchema: {
|
|
6191
|
+
type: "object",
|
|
6192
|
+
properties: {
|
|
6193
|
+
instance_id: {
|
|
6194
|
+
type: "string",
|
|
6195
|
+
description: "Which connected Studio place to inspect. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
|
|
6196
|
+
}
|
|
6197
|
+
}
|
|
6198
|
+
}
|
|
6199
|
+
},
|
|
6200
|
+
{
|
|
6201
|
+
name: "multiplayer_test_add_players",
|
|
6202
|
+
category: "write",
|
|
6203
|
+
description: "Add client players to a running StudioTestService multiplayer test and wait for the new clients to connect.",
|
|
6204
|
+
inputSchema: {
|
|
6205
|
+
type: "object",
|
|
6206
|
+
properties: {
|
|
6207
|
+
numPlayers: {
|
|
6208
|
+
type: "number",
|
|
6209
|
+
description: "Number of additional client players to add (1-8)."
|
|
6210
|
+
},
|
|
6211
|
+
timeout: {
|
|
6212
|
+
type: "number",
|
|
6213
|
+
description: "Max seconds to wait for new clients to register (default 30)."
|
|
6214
|
+
},
|
|
6215
|
+
instance_id: {
|
|
6216
|
+
type: "string",
|
|
6217
|
+
description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
|
|
6218
|
+
}
|
|
6219
|
+
},
|
|
6220
|
+
required: ["numPlayers"]
|
|
6221
|
+
}
|
|
6222
|
+
},
|
|
6223
|
+
{
|
|
6224
|
+
name: "multiplayer_test_leave_client",
|
|
6225
|
+
category: "write",
|
|
6226
|
+
description: "Disconnect a specific client from a running StudioTestService multiplayer test, then wait for that client peer to leave.",
|
|
6227
|
+
inputSchema: {
|
|
6228
|
+
type: "object",
|
|
6229
|
+
properties: {
|
|
6230
|
+
target: {
|
|
6231
|
+
type: "string",
|
|
6232
|
+
description: 'Client target to leave: "client-1" (default), "client-2", etc.'
|
|
6233
|
+
},
|
|
6234
|
+
timeout: {
|
|
6235
|
+
type: "number",
|
|
6236
|
+
description: "Max seconds to wait for the client peer to disconnect (default 30)."
|
|
6237
|
+
},
|
|
6238
|
+
instance_id: {
|
|
6239
|
+
type: "string",
|
|
6240
|
+
description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
|
|
6241
|
+
}
|
|
6242
|
+
}
|
|
6243
|
+
}
|
|
6244
|
+
},
|
|
6245
|
+
{
|
|
6246
|
+
name: "multiplayer_test_end",
|
|
6247
|
+
category: "write",
|
|
6248
|
+
description: "End a running StudioTestService multiplayer test with an optional return value, then wait for all runtime peers to disconnect.",
|
|
6249
|
+
inputSchema: {
|
|
6250
|
+
type: "object",
|
|
6251
|
+
properties: {
|
|
6252
|
+
value: {
|
|
6253
|
+
description: "JSON-compatible value returned to the edit-side ExecuteMultiplayerTestAsync call."
|
|
6254
|
+
},
|
|
6255
|
+
timeout: {
|
|
6256
|
+
type: "number",
|
|
6257
|
+
description: "Max seconds to wait for runtime peers to disconnect (default 30)."
|
|
6258
|
+
},
|
|
6259
|
+
instance_id: {
|
|
6260
|
+
type: "string",
|
|
6261
|
+
description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
|
|
6262
|
+
}
|
|
6263
|
+
}
|
|
6264
|
+
}
|
|
6265
|
+
},
|
|
5782
6266
|
{
|
|
5783
6267
|
name: "get_runtime_logs",
|
|
5784
6268
|
category: "read",
|
|
@@ -6323,7 +6807,7 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
6323
6807
|
{
|
|
6324
6808
|
name: "capture_screenshot",
|
|
6325
6809
|
category: "read",
|
|
6326
|
-
description: 'Capture the Roblox Studio viewport at native resolution and return it as an image, plus a text line stating the exact pixel dimensions. Works in Edit mode and
|
|
6810
|
+
description: 'Capture the Roblox Studio viewport at native resolution and return it as an image, plus a text line stating the exact pixel dimensions. Works in Edit mode and regular playtests (auto-detects a running client and captures the live play viewport). StudioTestService multiplayer client screenshots are currently blocked by Roblox temporary-texture process scoping; the tool returns a clear error in that case. The returned image is never downscaled, so its pixel grid is exactly the coordinate space simulate_mouse_input uses \u2014 read click positions straight off this image. For reading fine text/UI, use format="png" (lossless) or a higher quality; enlarging the Studio window raises resolution. Requires EditableImage API enabled (Game Settings > Security > "Allow Mesh / Image APIs") and the window to be visible.',
|
|
6327
6811
|
inputSchema: {
|
|
6328
6812
|
type: "object",
|
|
6329
6813
|
properties: {
|
|
@@ -6598,6 +7082,39 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
6598
7082
|
}
|
|
6599
7083
|
}
|
|
6600
7084
|
},
|
|
7085
|
+
{
|
|
7086
|
+
name: "get_scene_analysis",
|
|
7087
|
+
category: "read",
|
|
7088
|
+
description: 'Read Roblox SceneAnalysisService data for attribution-focused performance analysis. Complements get_memory_breakdown: returns compact top-N entries for instance composition, script memory, unparented instances, triangle composition, animation memory, and audio memory. Requires the Studio Scene Analysis beta feature; if disabled, returns scene_analysis_not_enabled with betaFeatureRequired=true. target="all" (default) returns per-peer data; single-peer targets return that peer directly. raw=true includes the full nested Scene Analysis tree.',
|
|
7089
|
+
inputSchema: {
|
|
7090
|
+
type: "object",
|
|
7091
|
+
properties: {
|
|
7092
|
+
mode: {
|
|
7093
|
+
type: "string",
|
|
7094
|
+
enum: ["all", "instance_composition", "script_memory", "unparented_instances", "triangle_composition", "animation_memory", "audio_memory"],
|
|
7095
|
+
description: 'Scene analysis mode to read. Defaults to "all".'
|
|
7096
|
+
},
|
|
7097
|
+
target: {
|
|
7098
|
+
type: "string",
|
|
7099
|
+
description: 'Peer to read from: "edit", "server", "client-N", or "all" (default).'
|
|
7100
|
+
},
|
|
7101
|
+
topN: {
|
|
7102
|
+
type: "number",
|
|
7103
|
+
minimum: 1,
|
|
7104
|
+
maximum: 100,
|
|
7105
|
+
description: "Number of flattened top entries to include per mode. Defaults to 10; plugin clamps to 1-100."
|
|
7106
|
+
},
|
|
7107
|
+
raw: {
|
|
7108
|
+
type: "boolean",
|
|
7109
|
+
description: "Include the full nested SceneAnalysisService tree in each mode result. Defaults to false."
|
|
7110
|
+
},
|
|
7111
|
+
instance_id: {
|
|
7112
|
+
type: "string",
|
|
7113
|
+
description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
|
|
7114
|
+
}
|
|
7115
|
+
}
|
|
7116
|
+
}
|
|
7117
|
+
},
|
|
6601
7118
|
// === SerializationService round-trip ===
|
|
6602
7119
|
{
|
|
6603
7120
|
name: "export_rbxm",
|
|
@@ -6781,7 +7298,7 @@ function handleVariantConflict({ pluginsFolder, otherAssetName, replace }) {
|
|
|
6781
7298
|
}
|
|
6782
7299
|
console.warn(`
|
|
6783
7300
|
[install-plugin] WARNING: ${otherAssetName} is already present in ${pluginsFolder}.
|
|
6784
|
-
|
|
7301
|
+
Only one MCP plugin variant should be present. If both variants are in the Studio Plugins folder, Studio loads both and runtime routing can become unpredictable.
|
|
6785
7302
|
Re-run with --replace-variant to remove ${otherAssetName}, or delete it manually.
|
|
6786
7303
|
`);
|
|
6787
7304
|
}
|