@chrrxs/robloxstudio-mcp 3.0.0 → 3.0.1
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 +217 -51
- package/package.json +2 -2
- package/studio-plugin/MCPPlugin.rbxmx +332 -40
- package/studio-plugin/.Carbon.rbxm.lock +0 -0
- package/studio-plugin/Carbon.rbxm +0 -0
- package/studio-plugin/INSTALLATION.md +0 -170
- package/studio-plugin/MCPInspectorPlugin.rbxmx +0 -169759
- package/studio-plugin/default.project.json +0 -19
- package/studio-plugin/dev.project.json +0 -23
- package/studio-plugin/include/LibMP.lua +0 -156378
- package/studio-plugin/inspector-icon.png +0 -0
- package/studio-plugin/package-lock.json +0 -706
- package/studio-plugin/package.json +0 -19
- package/studio-plugin/plugin.json +0 -10
- package/studio-plugin/src/modules/AssetSanitizationPolicy.ts +0 -127
- package/studio-plugin/src/modules/ClientBroker.ts +0 -450
- package/studio-plugin/src/modules/Communication.ts +0 -601
- package/studio-plugin/src/modules/EvalBridges.ts +0 -255
- package/studio-plugin/src/modules/HttpDiagnostics.ts +0 -50
- package/studio-plugin/src/modules/LuauExec.ts +0 -403
- package/studio-plugin/src/modules/Recording.ts +0 -28
- package/studio-plugin/src/modules/RenderMonitor.ts +0 -60
- package/studio-plugin/src/modules/RuntimeLogBuffer.ts +0 -210
- package/studio-plugin/src/modules/ServerUrlSettings.ts +0 -117
- package/studio-plugin/src/modules/State.ts +0 -39
- package/studio-plugin/src/modules/StopPlayMonitor.ts +0 -267
- package/studio-plugin/src/modules/UI.ts +0 -597
- package/studio-plugin/src/modules/Utils.ts +0 -527
- package/studio-plugin/src/modules/handlers/AssetHandlers.ts +0 -391
- package/studio-plugin/src/modules/handlers/BreakpointHandlers.ts +0 -460
- package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +0 -170
- package/studio-plugin/src/modules/handlers/EvalRuntimeHandlers.ts +0 -149
- package/studio-plugin/src/modules/handlers/GenerateModelHandlers.ts +0 -168
- package/studio-plugin/src/modules/handlers/InputHandlers.ts +0 -163
- package/studio-plugin/src/modules/handlers/LogHandlers.ts +0 -14
- package/studio-plugin/src/modules/handlers/MemoryHandlers.ts +0 -44
- package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +0 -96
- package/studio-plugin/src/modules/handlers/MicroProfilerHandlers.ts +0 -1263
- package/studio-plugin/src/modules/handlers/PropertyHandlers.ts +0 -62
- package/studio-plugin/src/modules/handlers/QueryHandlers.ts +0 -716
- package/studio-plugin/src/modules/handlers/SceneAnalysisHandlers.ts +0 -216
- package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +0 -531
- package/studio-plugin/src/modules/handlers/ScriptProfilerHandlers.ts +0 -386
- package/studio-plugin/src/modules/handlers/SerializationHandlers.ts +0 -172
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +0 -350
- package/studio-plugin/src/server/index.server.ts +0 -135
- package/studio-plugin/src/types/index.d.ts +0 -57
- package/studio-plugin/tsconfig.json +0 -20
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
import { HttpService, Players, RunService } from "@rbxts/services";
|
|
2
|
-
import StopPlayMonitor from "../StopPlayMonitor";
|
|
3
|
-
|
|
4
|
-
interface StudioTestServiceMultiplayer extends StudioTestService {
|
|
5
|
-
ExecuteMultiplayerTestAsync(numPlayers: number, testArgs: unknown): unknown;
|
|
6
|
-
AddPlayers(numPlayers: number): void;
|
|
7
|
-
CanLeaveTest(): boolean;
|
|
8
|
-
LeaveTest(): void;
|
|
9
|
-
EditModeActive: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const StudioTestService = game.GetService("StudioTestService") as StudioTestServiceMultiplayer;
|
|
13
|
-
|
|
14
|
-
let testRunning = false;
|
|
15
|
-
|
|
16
|
-
type MultiplayerPhase = "idle" | "starting" | "running" | "completed" | "failed";
|
|
17
|
-
|
|
18
|
-
interface MultiplayerSessionState {
|
|
19
|
-
phase: MultiplayerPhase;
|
|
20
|
-
testId?: string;
|
|
21
|
-
numPlayers?: number;
|
|
22
|
-
testArgs?: unknown;
|
|
23
|
-
startedAt?: number;
|
|
24
|
-
completedAt?: number;
|
|
25
|
-
ok?: boolean;
|
|
26
|
-
result?: unknown;
|
|
27
|
-
error?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
let multiplayerState: MultiplayerSessionState = { phase: "idle" };
|
|
31
|
-
|
|
32
|
-
function detectPeerRole(): string {
|
|
33
|
-
if (!RunService.IsRunning()) return "edit";
|
|
34
|
-
if (RunService.IsServer()) return "server";
|
|
35
|
-
return "client";
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function getPlayersSnapshot() {
|
|
39
|
-
const players = Players.GetPlayers().map((player) => ({
|
|
40
|
-
name: player.Name,
|
|
41
|
-
userId: player.UserId,
|
|
42
|
-
displayName: player.DisplayName,
|
|
43
|
-
}));
|
|
44
|
-
players.sort((a, b) => a.name < b.name);
|
|
45
|
-
return players;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function cloneMultiplayerState(): MultiplayerSessionState {
|
|
49
|
-
return {
|
|
50
|
-
phase: multiplayerState.phase,
|
|
51
|
-
testId: multiplayerState.testId,
|
|
52
|
-
numPlayers: multiplayerState.numPlayers,
|
|
53
|
-
testArgs: multiplayerState.testArgs,
|
|
54
|
-
startedAt: multiplayerState.startedAt,
|
|
55
|
-
completedAt: multiplayerState.completedAt,
|
|
56
|
-
ok: multiplayerState.ok,
|
|
57
|
-
result: multiplayerState.result,
|
|
58
|
-
error: multiplayerState.error,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function normalizeNumPlayers(value: unknown): number | undefined {
|
|
63
|
-
if (!typeIs(value, "number")) return undefined;
|
|
64
|
-
const n = math.floor(value);
|
|
65
|
-
if (n !== value || n < 1 || n > 8) return undefined;
|
|
66
|
-
return n;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function startPlaytest(requestData: Record<string, unknown>) {
|
|
70
|
-
const mode = requestData.mode as string | undefined;
|
|
71
|
-
const numPlayers = requestData.numPlayers as number | undefined;
|
|
72
|
-
|
|
73
|
-
if (mode !== "play" && mode !== "run") {
|
|
74
|
-
return { error: 'mode must be "play" or "run"' };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (numPlayers !== undefined) {
|
|
78
|
-
return { error: "start_playtest is single-player only. Use multiplayer_test_start for multi-client StudioTestService sessions." };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Self-heal: if testRunning is stuck true but Studio reports no active
|
|
82
|
-
// playtest, the previous start_playtest's task.spawn was orphaned
|
|
83
|
-
// (plugin reload mid-test, Studio entered some inconsistent state, etc).
|
|
84
|
-
// Reset it so subsequent starts don't hit a false "already running".
|
|
85
|
-
if (testRunning && !RunService.IsRunning()) {
|
|
86
|
-
testRunning = false;
|
|
87
|
-
// Runtime eval bridges are created by the play server/client plugin
|
|
88
|
-
// peers and disappear with the play DataModels.
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (testRunning) {
|
|
92
|
-
return { error: "A test is already running" };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
testRunning = true;
|
|
96
|
-
|
|
97
|
-
task.spawn(() => {
|
|
98
|
-
const [ok, result] = pcall(() => {
|
|
99
|
-
if (mode === "play") {
|
|
100
|
-
return StudioTestService.ExecutePlayModeAsync({});
|
|
101
|
-
}
|
|
102
|
-
return StudioTestService.ExecuteRunModeAsync({});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
if (!ok) {
|
|
106
|
-
warn(`[robloxstudio-mcp] Playtest ended with error: ${result}`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
testRunning = false;
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
const response: Record<string, unknown> = {
|
|
113
|
-
success: true,
|
|
114
|
-
message: `Playtest started in ${mode} mode.`,
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
return response;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function stopPlaytest(_requestData: Record<string, unknown>) {
|
|
121
|
-
// Signal the play-server DM's StopPlayMonitor via plugin:SetSetting.
|
|
122
|
-
// The monitor acknowledges with the matching request id only after its
|
|
123
|
-
// StudioTestService:EndTest call returns from pcall.
|
|
124
|
-
const stopRequest = StopPlayMonitor.requestStop();
|
|
125
|
-
if (!stopRequest.ok || stopRequest.requestId === undefined) {
|
|
126
|
-
return { error: "Plugin not ready. Try again in a moment." };
|
|
127
|
-
}
|
|
128
|
-
const consumption = StopPlayMonitor.waitForConsumption(stopRequest.requestId);
|
|
129
|
-
if (!consumption.ok) {
|
|
130
|
-
// Two distinct failure modes collapse here, distinguished by whether
|
|
131
|
-
// THIS edit DM has a playtest tracked:
|
|
132
|
-
//
|
|
133
|
-
// - testRunning=false: no playtest was running from this edit DM
|
|
134
|
-
// (true negative). Return "no active playtest" — fine to retry only
|
|
135
|
-
// after actually starting a playtest.
|
|
136
|
-
// - testRunning=true: a playtest IS running but the cross-DM signal
|
|
137
|
-
// didn't propagate within the consumption timeout (false negative
|
|
138
|
-
// from the caller's perspective — playtest may actually have ended).
|
|
139
|
-
// Tell the caller it's a timing issue and they can retry.
|
|
140
|
-
//
|
|
141
|
-
// Either way clean up the pending request so a future playtest's monitor
|
|
142
|
-
// doesn't fire EndTest on startup against a stale signal.
|
|
143
|
-
StopPlayMonitor.clearPending(stopRequest.requestId);
|
|
144
|
-
if (testRunning) {
|
|
145
|
-
return {
|
|
146
|
-
error:
|
|
147
|
-
"Playtest stop signal failed or was not acknowledged. " +
|
|
148
|
-
"The playtest may have ended anyway; check get_connected_instances.",
|
|
149
|
-
detail: consumption.error,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
if (consumption.consumed) {
|
|
153
|
-
return { error: "Playtest stop request reached the play server, but EndTest failed.", detail: consumption.error };
|
|
154
|
-
}
|
|
155
|
-
return { error: "No active playtest to stop.", detail: consumption.error };
|
|
156
|
-
}
|
|
157
|
-
StopPlayMonitor.clearPending(stopRequest.requestId);
|
|
158
|
-
// Request was consumed (EndTest called). ExecutePlayModeAsync in our
|
|
159
|
-
// startPlaytest task.spawn is still unwinding though — testRunning stays
|
|
160
|
-
// true until that yield completes and the post-block runs. Wait so
|
|
161
|
-
// back-to-back stop -> start sequences don't race against the prior
|
|
162
|
-
// teardown and get "A test is already running". 10s covers play-DM
|
|
163
|
-
// teardown on heavier places; if it still hasn't cleared we return
|
|
164
|
-
// anyway so users aren't stuck — but note that in the response so the
|
|
165
|
-
// caller knows a subsequent start may need a moment.
|
|
166
|
-
const deadline = tick() + 10;
|
|
167
|
-
while (testRunning && tick() < deadline) {
|
|
168
|
-
task.wait(0.1);
|
|
169
|
-
}
|
|
170
|
-
if (testRunning) {
|
|
171
|
-
return {
|
|
172
|
-
success: true,
|
|
173
|
-
message: "Playtest stop signal sent; teardown still in progress.",
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
return { success: true, message: "Playtest stopped." };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function multiplayerTestStart(requestData: Record<string, unknown>) {
|
|
180
|
-
if (RunService.IsRunning()) {
|
|
181
|
-
return { error: "multiplayer_test_start must be called on the edit DataModel. Route with target=edit." };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const numPlayers = normalizeNumPlayers(requestData.numPlayers);
|
|
185
|
-
if (numPlayers === undefined) {
|
|
186
|
-
return { error: "numPlayers must be an integer from 1 to 8" };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (multiplayerState.phase === "starting" || multiplayerState.phase === "running") {
|
|
190
|
-
return {
|
|
191
|
-
error: "A multiplayer Studio test is already running",
|
|
192
|
-
state: cloneMultiplayerState(),
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const testArgs = requestData.testArgs !== undefined ? requestData.testArgs : {};
|
|
197
|
-
const testId = HttpService.GenerateGUID(false);
|
|
198
|
-
|
|
199
|
-
multiplayerState = {
|
|
200
|
-
phase: "starting",
|
|
201
|
-
testId,
|
|
202
|
-
numPlayers,
|
|
203
|
-
testArgs,
|
|
204
|
-
startedAt: tick(),
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
task.spawn(() => {
|
|
208
|
-
multiplayerState.phase = "running";
|
|
209
|
-
const [ok, result] = pcall(() => {
|
|
210
|
-
return StudioTestService.ExecuteMultiplayerTestAsync(numPlayers, testArgs);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
multiplayerState.completedAt = tick();
|
|
214
|
-
multiplayerState.ok = ok;
|
|
215
|
-
if (ok) {
|
|
216
|
-
multiplayerState.phase = "completed";
|
|
217
|
-
multiplayerState.result = result;
|
|
218
|
-
multiplayerState.error = undefined;
|
|
219
|
-
} else {
|
|
220
|
-
multiplayerState.phase = "failed";
|
|
221
|
-
multiplayerState.result = undefined;
|
|
222
|
-
multiplayerState.error = tostring(result);
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
const response: Record<string, unknown> = {
|
|
227
|
-
success: true,
|
|
228
|
-
message: `Multiplayer Studio test starting with ${numPlayers} player(s).`,
|
|
229
|
-
testId,
|
|
230
|
-
phase: multiplayerState.phase,
|
|
231
|
-
numPlayers,
|
|
232
|
-
testArgs,
|
|
233
|
-
};
|
|
234
|
-
return response;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function multiplayerTestState(_requestData: Record<string, unknown>) {
|
|
238
|
-
const peer = detectPeerRole();
|
|
239
|
-
const response: Record<string, unknown> = {
|
|
240
|
-
success: true,
|
|
241
|
-
peer,
|
|
242
|
-
isRunning: RunService.IsRunning(),
|
|
243
|
-
isRunMode: RunService.IsRunMode(),
|
|
244
|
-
editModeActive: StudioTestService.EditModeActive,
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
if (peer === "edit") {
|
|
248
|
-
response.session = cloneMultiplayerState();
|
|
249
|
-
return response;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const [argsOk, args] = pcall(() => StudioTestService.GetTestArgs());
|
|
253
|
-
response.testArgsOk = argsOk;
|
|
254
|
-
response.testArgs = argsOk ? args : undefined;
|
|
255
|
-
if (!argsOk) response.testArgsError = tostring(args);
|
|
256
|
-
|
|
257
|
-
const players = getPlayersSnapshot();
|
|
258
|
-
response.players = players;
|
|
259
|
-
response.playerCount = players.size();
|
|
260
|
-
|
|
261
|
-
if (peer === "client") {
|
|
262
|
-
response.localPlayer = Players.LocalPlayer ? Players.LocalPlayer.Name : undefined;
|
|
263
|
-
const [canLeaveOk, canLeave] = pcall(() => StudioTestService.CanLeaveTest());
|
|
264
|
-
response.canLeaveOk = canLeaveOk;
|
|
265
|
-
response.canLeave = canLeaveOk ? canLeave : false;
|
|
266
|
-
if (!canLeaveOk) response.canLeaveError = tostring(canLeave);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return response;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function multiplayerTestAddPlayers(requestData: Record<string, unknown>) {
|
|
273
|
-
if (!RunService.IsRunning() || !RunService.IsServer()) {
|
|
274
|
-
return { error: "multiplayer_test_add_players must be called on the running server peer. Route with target=server." };
|
|
275
|
-
}
|
|
276
|
-
const numPlayers = normalizeNumPlayers(requestData.numPlayers);
|
|
277
|
-
if (numPlayers === undefined) {
|
|
278
|
-
return { error: "numPlayers must be an integer from 1 to 8" };
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const before = Players.GetPlayers().size();
|
|
282
|
-
const [ok, result] = pcall(() => StudioTestService.AddPlayers(numPlayers));
|
|
283
|
-
if (!ok) {
|
|
284
|
-
return { error: tostring(result) };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const deadline = tick() + ((requestData.timeout as number | undefined) ?? 10);
|
|
288
|
-
while (Players.GetPlayers().size() < before + numPlayers && tick() < deadline) {
|
|
289
|
-
task.wait(0.1);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const players = getPlayersSnapshot();
|
|
293
|
-
return {
|
|
294
|
-
success: true,
|
|
295
|
-
message: `Requested ${numPlayers} additional player(s).`,
|
|
296
|
-
playerCount: players.size(),
|
|
297
|
-
players,
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function multiplayerTestLeaveClient(_requestData: Record<string, unknown>) {
|
|
302
|
-
if (!RunService.IsRunning() || RunService.IsServer()) {
|
|
303
|
-
return { error: "multiplayer_test_leave_client must be called on a running client peer. Route with target=client-N." };
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const [canLeaveOk, canLeave] = pcall(() => StudioTestService.CanLeaveTest());
|
|
307
|
-
if (!canLeaveOk) {
|
|
308
|
-
return { error: tostring(canLeave), canLeaveOk: false };
|
|
309
|
-
}
|
|
310
|
-
if (!canLeave) {
|
|
311
|
-
return { error: "This client cannot leave the current test session.", canLeaveOk: true, canLeave: false };
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const localPlayer = Players.LocalPlayer ? Players.LocalPlayer.Name : undefined;
|
|
315
|
-
task.defer(() => {
|
|
316
|
-
pcall(() => StudioTestService.LeaveTest());
|
|
317
|
-
});
|
|
318
|
-
return {
|
|
319
|
-
success: true,
|
|
320
|
-
message: "Client leave requested.",
|
|
321
|
-
localPlayer,
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function multiplayerTestEnd(requestData: Record<string, unknown>) {
|
|
326
|
-
if (!RunService.IsRunning() || !RunService.IsServer()) {
|
|
327
|
-
return { error: "multiplayer_test_end must be called on the running server peer. Route with target=server." };
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const value = requestData.value !== undefined ? requestData.value : "ended_by_mcp";
|
|
331
|
-
const [ok, result] = pcall(() => StudioTestService.EndTest(value));
|
|
332
|
-
if (!ok) {
|
|
333
|
-
return { error: tostring(result) };
|
|
334
|
-
}
|
|
335
|
-
return {
|
|
336
|
-
success: true,
|
|
337
|
-
message: "Multiplayer Studio test end requested.",
|
|
338
|
-
value,
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
export = {
|
|
343
|
-
startPlaytest,
|
|
344
|
-
stopPlaytest,
|
|
345
|
-
multiplayerTestStart,
|
|
346
|
-
multiplayerTestState,
|
|
347
|
-
multiplayerTestAddPlayers,
|
|
348
|
-
multiplayerTestLeaveClient,
|
|
349
|
-
multiplayerTestEnd,
|
|
350
|
-
};
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import State from "../modules/State";
|
|
2
|
-
import UI from "../modules/UI";
|
|
3
|
-
import Communication from "../modules/Communication";
|
|
4
|
-
import ClientBroker from "../modules/ClientBroker";
|
|
5
|
-
import ServerUrlSettings from "../modules/ServerUrlSettings";
|
|
6
|
-
import { cleanupLegacyEditBridges, ensureRuntimeBridgeInstalled } from "../modules/EvalBridges";
|
|
7
|
-
import RuntimeLogBuffer from "../modules/RuntimeLogBuffer";
|
|
8
|
-
import StopPlayMonitor from "../modules/StopPlayMonitor";
|
|
9
|
-
import BreakpointHandlers from "../modules/handlers/BreakpointHandlers";
|
|
10
|
-
import * as RenderMonitor from "../modules/RenderMonitor";
|
|
11
|
-
|
|
12
|
-
// Track render-loop liveness so input/screenshot tools can report "window
|
|
13
|
-
// minimized / not rendering" instead of silently no-op'ing. No-op in the
|
|
14
|
-
// server DM (RenderStepped can't connect there).
|
|
15
|
-
RenderMonitor.start();
|
|
16
|
-
|
|
17
|
-
// Attach the per-peer LogService.MessageOut listener as early as possible so
|
|
18
|
-
// boot-time prints from the user's place scripts are captured. Powers the
|
|
19
|
-
// get_runtime_logs MCP tool. Idempotent; safe to call before UI.init().
|
|
20
|
-
RuntimeLogBuffer.install();
|
|
21
|
-
|
|
22
|
-
// Share the plugin reference with the stop-play signaling module so both the
|
|
23
|
-
// edit DM (write the flag) and the play-server DM (read+act on the flag) can
|
|
24
|
-
// access plugin:SetSetting/GetSetting.
|
|
25
|
-
StopPlayMonitor.init(plugin);
|
|
26
|
-
BreakpointHandlers.init(plugin);
|
|
27
|
-
ServerUrlSettings.init(plugin);
|
|
28
|
-
|
|
29
|
-
function applyRememberedServerUrl(): void {
|
|
30
|
-
if (ClientBroker.forkRole() === "client") return;
|
|
31
|
-
|
|
32
|
-
const rememberedServerUrl = ServerUrlSettings.readServerUrl();
|
|
33
|
-
if (rememberedServerUrl === undefined) return;
|
|
34
|
-
|
|
35
|
-
const conn = State.getActiveConnection();
|
|
36
|
-
conn.serverUrl = rememberedServerUrl;
|
|
37
|
-
const port = ServerUrlSettings.extractPort(rememberedServerUrl);
|
|
38
|
-
if (port !== undefined) conn.port = port;
|
|
39
|
-
ClientBroker.setServerUrl(rememberedServerUrl);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
applyRememberedServerUrl();
|
|
43
|
-
|
|
44
|
-
UI.init(plugin);
|
|
45
|
-
const elements = UI.getElements();
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const ICON_DISCONNECTED = "rbxassetid://__BUTTON_ICON_DISCONNECTED__";
|
|
49
|
-
const ICON_CONNECTING = "rbxassetid://__BUTTON_ICON_CONNECTING__";
|
|
50
|
-
const ICON_CONNECTED = "rbxassetid://__BUTTON_ICON_CONNECTED__";
|
|
51
|
-
const TOOLBAR_REGISTRATION_DELAY_SECONDS = 1;
|
|
52
|
-
|
|
53
|
-
let toolbarButtonRegistered = false;
|
|
54
|
-
|
|
55
|
-
function registerToolbarButton() {
|
|
56
|
-
if (toolbarButtonRegistered) {
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
toolbarButtonRegistered = true;
|
|
60
|
-
|
|
61
|
-
const toolbar = plugin.CreateToolbar("__TOOLBAR_NAME__");
|
|
62
|
-
const button = toolbar.CreateButton("__BUTTON_TITLE__", "__BUTTON_TOOLTIP__", ICON_DISCONNECTED);
|
|
63
|
-
UI.setToolbarButton(button, { disconnected: ICON_DISCONNECTED, connecting: ICON_CONNECTING, connected: ICON_CONNECTED });
|
|
64
|
-
|
|
65
|
-
button.Click.Connect(() => {
|
|
66
|
-
elements.screenGui.Enabled = !elements.screenGui.Enabled;
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
elements.connectButton.Activated.Connect(() => {
|
|
72
|
-
const conn = State.getActiveConnection();
|
|
73
|
-
if (conn && conn.isActive) {
|
|
74
|
-
Communication.deactivatePlugin();
|
|
75
|
-
} else {
|
|
76
|
-
Communication.activatePlugin();
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
plugin.Unloading.Connect(() => {
|
|
82
|
-
Communication.deactivateAll();
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
UI.updateUIState();
|
|
87
|
-
Communication.checkForUpdates();
|
|
88
|
-
task.delay(TOOLBAR_REGISTRATION_DELAY_SECONDS, registerToolbarButton);
|
|
89
|
-
|
|
90
|
-
// Auto-activate per peer. The boshyxd plugin only registers with MCP when the
|
|
91
|
-
// user clicks Connect in its UI, but that UI is invisible in play DMs - so
|
|
92
|
-
// play peers' plugin instances load without ever registering. Run after a
|
|
93
|
-
// short delay so the UI/State have a chance to initialize first.
|
|
94
|
-
task.delay(2, () => {
|
|
95
|
-
const role = ClientBroker.forkRole();
|
|
96
|
-
if (role === "edit") {
|
|
97
|
-
cleanupLegacyEditBridges();
|
|
98
|
-
} else {
|
|
99
|
-
const result = ensureRuntimeBridgeInstalled();
|
|
100
|
-
if (!result.installed) {
|
|
101
|
-
warn(`[robloxstudio-mcp] Runtime eval bridge install failed: ${result.error}`);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
if (role === "edit" || role === "server") {
|
|
105
|
-
pcall(() => {
|
|
106
|
-
const conn = State.getActiveConnection();
|
|
107
|
-
if (!conn.isActive) {
|
|
108
|
-
if (role === "server") {
|
|
109
|
-
const inheritedServerUrl = ServerUrlSettings.readServerUrl() ?? ClientBroker.DEFAULT_MCP_URL;
|
|
110
|
-
conn.serverUrl = ServerUrlSettings.normalizeServerUrl(inheritedServerUrl);
|
|
111
|
-
elements.urlInput.Text = conn.serverUrl;
|
|
112
|
-
const port = ServerUrlSettings.extractPort(conn.serverUrl);
|
|
113
|
-
if (port !== undefined) conn.port = port;
|
|
114
|
-
ClientBroker.setServerUrl(conn.serverUrl);
|
|
115
|
-
}
|
|
116
|
-
// Defensive default: in invisible play-DM UIs, the input field
|
|
117
|
-
// may not be populated by the time we activate.
|
|
118
|
-
if (conn.serverUrl === undefined || conn.serverUrl === "") {
|
|
119
|
-
conn.serverUrl = ClientBroker.DEFAULT_MCP_URL;
|
|
120
|
-
elements.urlInput.Text = conn.serverUrl;
|
|
121
|
-
}
|
|
122
|
-
Communication.activatePlugin();
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
if (role === "server") {
|
|
127
|
-
ClientBroker.setupServerBroker();
|
|
128
|
-
// The play-server DM is the only one where StudioTestService:EndTest is
|
|
129
|
-
// legal, so the stop-play monitor lives here. It consumes tokenized
|
|
130
|
-
// stop requests from plugin settings and acknowledges EndTest results.
|
|
131
|
-
StopPlayMonitor.startMonitor();
|
|
132
|
-
} else if (role === "client") {
|
|
133
|
-
ClientBroker.setupClientBroker();
|
|
134
|
-
}
|
|
135
|
-
});
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/// <reference types="@rbxts/types/plugin" />
|
|
2
|
-
|
|
3
|
-
export interface Connection {
|
|
4
|
-
port: number;
|
|
5
|
-
serverUrl: string;
|
|
6
|
-
isActive: boolean;
|
|
7
|
-
pollInterval: number;
|
|
8
|
-
lastPoll: number;
|
|
9
|
-
consecutiveFailures: number;
|
|
10
|
-
maxFailuresBeforeError: number;
|
|
11
|
-
lastSuccessfulConnection: number;
|
|
12
|
-
currentRetryDelay: number;
|
|
13
|
-
maxRetryDelay: number;
|
|
14
|
-
retryBackoffMultiplier: number;
|
|
15
|
-
lastHttpOk: boolean;
|
|
16
|
-
lastMcpOk: boolean;
|
|
17
|
-
mcpWaitStartTime?: number;
|
|
18
|
-
isPolling: boolean;
|
|
19
|
-
heartbeatConnection?: RBXScriptConnection;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface RequestData {
|
|
23
|
-
[key: string]: unknown;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface RequestPayload {
|
|
27
|
-
endpoint: string;
|
|
28
|
-
data?: RequestData;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface PollResponse {
|
|
32
|
-
mcpConnected: boolean;
|
|
33
|
-
serverVersion?: string;
|
|
34
|
-
pluginVersion?: string;
|
|
35
|
-
pluginVariant?: string;
|
|
36
|
-
versionMismatch?: boolean;
|
|
37
|
-
request?: RequestPayload;
|
|
38
|
-
requestId?: string;
|
|
39
|
-
// Server signals knownInstance=false when its in-memory instances map
|
|
40
|
-
// doesn't contain our pluginSessionId (typically after an MCP process
|
|
41
|
-
// restart). The plugin re-issues /ready when it sees this.
|
|
42
|
-
knownInstance?: boolean;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface ReadyResponse {
|
|
46
|
-
success: boolean;
|
|
47
|
-
assignedRole?: string;
|
|
48
|
-
instanceId?: string;
|
|
49
|
-
serverVersion?: string;
|
|
50
|
-
versionMismatch?: boolean;
|
|
51
|
-
error?: string;
|
|
52
|
-
message?: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
declare global {
|
|
56
|
-
function loadstring(code: string): LuaTuple<[(() => unknown) | undefined, string?]>;
|
|
57
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"allowSyntheticDefaultImports": true,
|
|
4
|
-
"downlevelIteration": true,
|
|
5
|
-
"jsx": "react",
|
|
6
|
-
"jsxFactory": "React.createElement",
|
|
7
|
-
"jsxFragmentFactory": "React.Fragment",
|
|
8
|
-
"module": "commonjs",
|
|
9
|
-
"moduleDetection": "force",
|
|
10
|
-
"moduleResolution": "node",
|
|
11
|
-
"noLib": true,
|
|
12
|
-
"resolveJsonModule": true,
|
|
13
|
-
"strict": true,
|
|
14
|
-
"target": "ESNext",
|
|
15
|
-
"typeRoots": ["node_modules/@rbxts"],
|
|
16
|
-
"rootDir": "src",
|
|
17
|
-
"outDir": "out",
|
|
18
|
-
"declaration": false
|
|
19
|
-
}
|
|
20
|
-
}
|