@princeofscale/bloxforge-inspector 2.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/assets/Baseplate.rbxl +0 -0
  2. package/dist/index.js +19281 -0
  3. package/package.json +62 -0
  4. package/studio-plugin/INSTALLATION.md +161 -0
  5. package/studio-plugin/MCPInspectorPlugin.rbxmx +171699 -0
  6. package/studio-plugin/MCPPlugin.rbxmx +171699 -0
  7. package/studio-plugin/default.project.json +19 -0
  8. package/studio-plugin/dev.project.json +23 -0
  9. package/studio-plugin/include/LibMP.lua +156378 -0
  10. package/studio-plugin/inspector-icon.png +0 -0
  11. package/studio-plugin/package-lock.json +692 -0
  12. package/studio-plugin/package.json +19 -0
  13. package/studio-plugin/plugin.json +10 -0
  14. package/studio-plugin/src/modules/ClientBroker.ts +447 -0
  15. package/studio-plugin/src/modules/Communication.ts +697 -0
  16. package/studio-plugin/src/modules/EvalBridges.ts +255 -0
  17. package/studio-plugin/src/modules/HttpDiagnostics.ts +50 -0
  18. package/studio-plugin/src/modules/JobRegistry.ts +77 -0
  19. package/studio-plugin/src/modules/LuauExec.ts +465 -0
  20. package/studio-plugin/src/modules/Recording.ts +28 -0
  21. package/studio-plugin/src/modules/RenderMonitor.ts +60 -0
  22. package/studio-plugin/src/modules/RuntimeLogBuffer.ts +152 -0
  23. package/studio-plugin/src/modules/ServerUrlSettings.ts +114 -0
  24. package/studio-plugin/src/modules/State.ts +101 -0
  25. package/studio-plugin/src/modules/StopPlayMonitor.ts +276 -0
  26. package/studio-plugin/src/modules/UI.ts +790 -0
  27. package/studio-plugin/src/modules/Utils.ts +318 -0
  28. package/studio-plugin/src/modules/handlers/AssetHandlers.ts +241 -0
  29. package/studio-plugin/src/modules/handlers/BreakpointHandlers.ts +460 -0
  30. package/studio-plugin/src/modules/handlers/BuildHandlers.ts +481 -0
  31. package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +203 -0
  32. package/studio-plugin/src/modules/handlers/EvalRuntimeHandlers.ts +149 -0
  33. package/studio-plugin/src/modules/handlers/InputHandlers.ts +160 -0
  34. package/studio-plugin/src/modules/handlers/InstanceHandlers.ts +380 -0
  35. package/studio-plugin/src/modules/handlers/JobHandlers.ts +111 -0
  36. package/studio-plugin/src/modules/handlers/LogHandlers.ts +14 -0
  37. package/studio-plugin/src/modules/handlers/MemoryHandlers.ts +44 -0
  38. package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +354 -0
  39. package/studio-plugin/src/modules/handlers/MicroProfilerHandlers.ts +1263 -0
  40. package/studio-plugin/src/modules/handlers/PropertyHandlers.ts +191 -0
  41. package/studio-plugin/src/modules/handlers/QueryHandlers.ts +809 -0
  42. package/studio-plugin/src/modules/handlers/SceneAnalysisHandlers.ts +216 -0
  43. package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +530 -0
  44. package/studio-plugin/src/modules/handlers/ScriptProfilerHandlers.ts +386 -0
  45. package/studio-plugin/src/modules/handlers/SerializationHandlers.ts +172 -0
  46. package/studio-plugin/src/modules/handlers/TestHandlers.ts +535 -0
  47. package/studio-plugin/src/server/index.server.ts +134 -0
  48. package/studio-plugin/src/types/index.d.ts +83 -0
  49. package/studio-plugin/tsconfig.json +21 -0
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "studio-plugin",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "build": "rbxtsc",
7
+ "watch": "rbxtsc -w",
8
+ "dev": "rbxtsc -w --project tsconfig.json"
9
+ },
10
+ "devDependencies": {
11
+ "@rbxts/compiler-types": "^3.0.0-types.0",
12
+ "@rbxts/types": "^1.0.906",
13
+ "roblox-ts": "^3.0.0",
14
+ "typescript": "5.5.3"
15
+ },
16
+ "dependencies": {
17
+ "@rbxts/services": "^1.6.0"
18
+ }
19
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "Name": "BloxForge Plugin",
3
+ "Description": "Provides AI tool access to Roblox Studio data through MCP protocol",
4
+ "Author": "BloxForge",
5
+ "Version": "2.19.3",
6
+ "Id": "bloxforge-plugin",
7
+ "Permissions": [
8
+ "HTTP"
9
+ ]
10
+ }
@@ -0,0 +1,447 @@
1
+ import { HttpService, Players, ReplicatedStorage, RunService, ServerStorage } from "@rbxts/services";
2
+ import RuntimeLogBuffer from "./RuntimeLogBuffer";
3
+ import MemoryHandlers from "./handlers/MemoryHandlers";
4
+ import SceneAnalysisHandlers from "./handlers/SceneAnalysisHandlers";
5
+ import CaptureHandlers from "./handlers/CaptureHandlers";
6
+ import InputHandlers from "./handlers/InputHandlers";
7
+ import EvalRuntimeHandlers from "./handlers/EvalRuntimeHandlers";
8
+ import BreakpointHandlers from "./handlers/BreakpointHandlers";
9
+ import ScriptProfilerHandlers from "./handlers/ScriptProfilerHandlers";
10
+ import LuauExec from "./LuauExec";
11
+ import State from "./State";
12
+ import HttpDiagnostics from "./HttpDiagnostics";
13
+
14
+ interface StudioTestServiceMultiplayer extends StudioTestService {
15
+ CanLeaveTest(): boolean;
16
+ LeaveTest(): void;
17
+ EditModeActive: boolean;
18
+ }
19
+
20
+ const StudioTestService = game.GetService("StudioTestService") as StudioTestServiceMultiplayer;
21
+
22
+ // Mirror of Communication.computeInstanceId() — duplicated here because the
23
+ // client broker runs in the play-server DM where it can't easily import from
24
+ // the edit-side module, and the place identifier must match what the edit-DM
25
+ // plugin reports. Both use the same algorithm against the shared DataModel.
26
+ function computeInstanceId(): string {
27
+ if (game.PlaceId !== 0) {
28
+ return `place:${tostring(game.PlaceId)}`;
29
+ }
30
+ const existing = ServerStorage.GetAttribute("__MCPPlaceId");
31
+ if (typeIs(existing, "string") && existing !== "") {
32
+ return `anon:${existing as string}`;
33
+ }
34
+ const fresh = HttpService.GenerateGUID(false);
35
+ pcall(() => ServerStorage.SetAttribute("__MCPPlaceId", fresh));
36
+ return `anon:${fresh}`;
37
+ }
38
+
39
+ let cachedPlaceName: string | undefined;
40
+ function resolvePlaceName(): string {
41
+ if (cachedPlaceName !== undefined) return cachedPlaceName;
42
+ if (game.PlaceId === 0) {
43
+ cachedPlaceName = game.Name;
44
+ return cachedPlaceName;
45
+ }
46
+ const MarketplaceService = game.GetService("MarketplaceService");
47
+ const [ok, info] = pcall(() => MarketplaceService.GetProductInfo(game.PlaceId));
48
+ if (ok && info !== undefined) {
49
+ const name = (info as { Name?: string }).Name;
50
+ if (typeIs(name, "string") && name !== "") {
51
+ cachedPlaceName = name;
52
+ return cachedPlaceName;
53
+ }
54
+ }
55
+ return game.Name;
56
+ }
57
+
58
+ // The client peer cannot reach the MCP HTTP server - Roblox forbids
59
+ // HttpService:RequestAsync from the client DM even under PluginSecurity, and
60
+ // HttpEnabled reads as false there regardless of identity. So the server peer
61
+ // brokers execute_luau requests to the client via a RemoteFunction it places
62
+ // in ReplicatedStorage; each player gets a proxy "client" registration on the
63
+ // MCP side, polled and dispatched by the server peer.
64
+ //
65
+ // (Previously the server peer also registered an "edit-proxy" role to
66
+ // intercept /api/stop-playtest and call StudioTestService:EndTest. That hack
67
+ // is gone: stop now uses StopPlayMonitor with plugin:SetSetting cross-DM
68
+ // signaling, which works regardless of MCP server state.)
69
+
70
+ const DEFAULT_MCP_URL = "http://localhost:58741";
71
+ let mcpUrl = DEFAULT_MCP_URL;
72
+ const BROKER_NAME = "__MCPClientBroker";
73
+ const BROKER_OWNER_ATTRIBUTE = "__MCPBrokerOwner";
74
+
75
+ interface ProxyEntry {
76
+ pluginSessionId: string;
77
+ role: string;
78
+ }
79
+
80
+ interface BrokerEnvelope {
81
+ endpoint?: string;
82
+ data?: Record<string, unknown>;
83
+ // Backward-compat: older server-broker code (pre-v2.10) sent the raw
84
+ // {code} payload directly. If we see code at the top level and no
85
+ // endpoint, treat it as execute-luau.
86
+ code?: string;
87
+ }
88
+
89
+
90
+ // Endpoints the server-peer broker is allowed to forward to the client peer.
91
+ // Each requires the client peer's plugin VM (because the buffer / require
92
+ // cache / etc. lives there) so the server peer alone can't satisfy them.
93
+ const CLIENT_BROKER_ALLOWED_ENDPOINTS = new Set<string>([
94
+ "/api/execute-luau",
95
+ "/api/eval-runtime",
96
+ "/api/get-runtime-logs",
97
+ "/api/breakpoints",
98
+ "/api/capture-script-profiler",
99
+ "/api/get-memory-breakdown",
100
+ "/api/get-scene-analysis",
101
+ "/api/multiplayer-test-state",
102
+ "/api/multiplayer-test-leave-client",
103
+ // Screenshot capture must run in the client peer (CaptureService captures
104
+ // the play viewport there); the edit DM reads the temp id back separately.
105
+ "/api/capture-begin",
106
+ // Virtual input (CreateVirtualInput) drives the running client's input
107
+ // pipeline, so it must execute in the client peer's VM.
108
+ "/api/simulate-mouse-input",
109
+ "/api/simulate-keyboard-input",
110
+ ]);
111
+
112
+ interface ReadyResponseBody {
113
+ assignedRole?: string;
114
+ }
115
+
116
+ interface PollResponseBody {
117
+ requestId?: string;
118
+ request?: {
119
+ endpoint: string;
120
+ data?: Record<string, unknown>;
121
+ };
122
+ // Server signals knownInstance=false when our proxy isn't in its
123
+ // in-memory instances map (typically after an MCP process restart).
124
+ // Triggers a re-register POST to /ready.
125
+ knownInstance?: boolean;
126
+ }
127
+
128
+ // Throttle re-ready calls per proxyId so a brief window of unknownInstance
129
+ // polls doesn't cause a re-register stampede.
130
+ const lastReadyByProxy = new Map<string, number>();
131
+
132
+ function reRegisterProxy(proxyId: string, role: string): void {
133
+ const now = tick();
134
+ const last = lastReadyByProxy.get(proxyId) ?? 0;
135
+ if (now - last < 2) return;
136
+ lastReadyByProxy.set(proxyId, now);
137
+ pcall(() =>
138
+ postJson("/ready", {
139
+ pluginSessionId: proxyId,
140
+ instanceId: computeInstanceId(),
141
+ role,
142
+ placeId: game.PlaceId,
143
+ placeName: resolvePlaceName(),
144
+ dataModelName: game.Name,
145
+ isRunning: RunService.IsRunning(),
146
+ pluginVersion: State.CURRENT_VERSION,
147
+ pluginVariant: State.PLUGIN_VARIANT,
148
+ protocolVersion: State.PROTOCOL_VERSION,
149
+ }),
150
+ );
151
+ }
152
+
153
+ function forkRole(): "edit" | "server" | "client" {
154
+ if (!RunService.IsRunning()) return "edit";
155
+ if (RunService.IsServer()) return "server";
156
+ return "client";
157
+ }
158
+
159
+ function postJson(endpoint: string, body: Record<string, unknown>) {
160
+ return pcall(() =>
161
+ HttpService.RequestAsync({
162
+ Url: `${mcpUrl}${endpoint}`,
163
+ Method: "POST",
164
+ Headers: { "Content-Type": "application/json" },
165
+ Body: HttpService.JSONEncode(body),
166
+ }),
167
+ );
168
+ }
169
+
170
+ function formatPostJsonFailure(endpoint: string, ok: boolean, res: unknown): string {
171
+ return HttpDiagnostics.formatRequestFailure(`${mcpUrl}${endpoint}`, ok, res);
172
+ }
173
+
174
+ function setServerUrl(serverUrl: string | undefined): void {
175
+ if (serverUrl !== undefined && serverUrl !== "") {
176
+ mcpUrl = serverUrl;
177
+ }
178
+ }
179
+
180
+ function getServerUrl(): string {
181
+ return mcpUrl;
182
+ }
183
+
184
+ function handleExecuteLuau(data: Record<string, unknown> | undefined) {
185
+ const code = data && (data.code as string | undefined);
186
+ if (typeIs(code, "string") === false || code === "") {
187
+ return { success: false, error: "code is required" };
188
+ }
189
+ // Shared with edit/server (MetadataHandlers.executeLuau). Adds the IIFE
190
+ // wrapper (so `print("hi")` with no return doesn't fail the
191
+ // ModuleScript's "must return one value" rule) and JSON-encodes table
192
+ // returns instead of yielding "table: 0xaddr".
193
+ return LuauExec.execute(code as string);
194
+ }
195
+
196
+ function handleGetRuntimeLogs(data: Record<string, unknown> | undefined): unknown {
197
+ const d = data ?? {};
198
+ const since = d.since as number | undefined;
199
+ const tail = d.tail as number | undefined;
200
+ const filter = d.filter as string | undefined;
201
+ // "client" is the generic capture tag; MCP-side aggregation overrides it
202
+ // with the specific role (e.g. "client-1") for capturedBy.
203
+ return RuntimeLogBuffer.query({ since, tail, filter }, "client");
204
+ }
205
+
206
+ function handleMultiplayerTestState(): unknown {
207
+ const [argsOk, args] = pcall(() => StudioTestService.GetTestArgs());
208
+ const [canLeaveOk, canLeave] = pcall(() => StudioTestService.CanLeaveTest());
209
+ const players = Players.GetPlayers().map((player) => ({
210
+ name: player.Name,
211
+ userId: player.UserId,
212
+ displayName: player.DisplayName,
213
+ }));
214
+ players.sort((a, b) => a.name < b.name);
215
+ return {
216
+ success: true,
217
+ peer: "client",
218
+ isRunning: RunService.IsRunning(),
219
+ isRunMode: RunService.IsRunMode(),
220
+ editModeActive: StudioTestService.EditModeActive,
221
+ testArgsOk: argsOk,
222
+ testArgs: argsOk ? args : undefined,
223
+ testArgsError: argsOk ? undefined : tostring(args),
224
+ players,
225
+ playerCount: players.size(),
226
+ localPlayer: Players.LocalPlayer ? Players.LocalPlayer.Name : undefined,
227
+ canLeaveOk,
228
+ canLeave: canLeaveOk ? canLeave : false,
229
+ canLeaveError: canLeaveOk ? undefined : tostring(canLeave),
230
+ };
231
+ }
232
+
233
+ function handleMultiplayerTestLeaveClient(): unknown {
234
+ const [canLeaveOk, canLeave] = pcall(() => StudioTestService.CanLeaveTest());
235
+ if (!canLeaveOk) {
236
+ return { error: tostring(canLeave), canLeaveOk: false };
237
+ }
238
+ if (!canLeave) {
239
+ return { error: "This client cannot leave the current test session.", canLeaveOk: true, canLeave: false };
240
+ }
241
+ const localPlayer = Players.LocalPlayer ? Players.LocalPlayer.Name : undefined;
242
+ task.defer(() => {
243
+ pcall(() => StudioTestService.LeaveTest());
244
+ });
245
+ return {
246
+ success: true,
247
+ message: "Client leave requested.",
248
+ localPlayer,
249
+ };
250
+ }
251
+
252
+ function setupClientBroker() {
253
+ const rf = ReplicatedStorage.WaitForChild(BROKER_NAME, 10);
254
+ if (!rf || !rf.IsA("RemoteFunction")) {
255
+ warn(`[BloxForge] client: ${BROKER_NAME} not found`);
256
+ return;
257
+ }
258
+ rf.OnClientInvoke = (payload: BrokerEnvelope | undefined) => {
259
+ // Two payload shapes in the wild:
260
+ // - {endpoint, data} from v2.10+ server-peer broker (this is the new
261
+ // discriminated form that lets us dispatch on endpoint)
262
+ // - {code} from pre-v2.10 server-peer broker (raw execute-luau payload)
263
+ // The shapes coexist gracefully because we fall back to execute-luau
264
+ // when endpoint is missing.
265
+ if (payload && payload.endpoint === "/api/get-runtime-logs") {
266
+ return handleGetRuntimeLogs(payload.data);
267
+ }
268
+ if (payload && payload.endpoint === "/api/breakpoints") {
269
+ return BreakpointHandlers.breakpoints(payload.data ?? {});
270
+ }
271
+ if (payload && payload.endpoint === "/api/capture-script-profiler") {
272
+ return ScriptProfilerHandlers.captureScriptProfiler(payload.data ?? {});
273
+ }
274
+ if (payload && payload.endpoint === "/api/get-memory-breakdown") {
275
+ return MemoryHandlers.getMemoryBreakdown(payload.data ?? {});
276
+ }
277
+ if (payload && payload.endpoint === "/api/get-scene-analysis") {
278
+ return SceneAnalysisHandlers.getSceneAnalysis(payload.data ?? {});
279
+ }
280
+ if (payload && payload.endpoint === "/api/multiplayer-test-state") {
281
+ return handleMultiplayerTestState();
282
+ }
283
+ if (payload && payload.endpoint === "/api/multiplayer-test-leave-client") {
284
+ return handleMultiplayerTestLeaveClient();
285
+ }
286
+ if (payload && payload.endpoint === "/api/capture-begin") {
287
+ return CaptureHandlers.captureBegin();
288
+ }
289
+ if (payload && payload.endpoint === "/api/simulate-mouse-input") {
290
+ return InputHandlers.simulateMouseInput(payload.data ?? {});
291
+ }
292
+ if (payload && payload.endpoint === "/api/simulate-keyboard-input") {
293
+ return InputHandlers.simulateKeyboardInput(payload.data ?? {});
294
+ }
295
+ if (payload && payload.endpoint === "/api/execute-luau") {
296
+ return handleExecuteLuau(payload.data);
297
+ }
298
+ if (payload && payload.endpoint === "/api/eval-runtime") {
299
+ return EvalRuntimeHandlers.evalRuntime(payload.data ?? {});
300
+ }
301
+ // Legacy: raw execute-luau payload at the top level.
302
+ return handleExecuteLuau(payload as Record<string, unknown> | undefined);
303
+ };
304
+ }
305
+
306
+ const proxyByPlayer = new Map<Player, ProxyEntry>();
307
+ const proxyRegisterFailuresByPlayer = new Set<Player>();
308
+ let serverBrokerStarted = false;
309
+
310
+ function unregisterProxy(player: Player, entry?: ProxyEntry): void {
311
+ const proxy = entry ?? proxyByPlayer.get(player);
312
+ if (!proxy) return;
313
+ proxyByPlayer.delete(player);
314
+ proxyRegisterFailuresByPlayer.delete(player);
315
+ postJson("/disconnect", { pluginSessionId: proxy.pluginSessionId });
316
+ }
317
+
318
+ function disconnectAllProxies(): void {
319
+ for (const [player, entry] of proxyByPlayer) {
320
+ unregisterProxy(player, entry);
321
+ }
322
+ proxyByPlayer.clear();
323
+ proxyRegisterFailuresByPlayer.clear();
324
+ }
325
+
326
+ function pollProxy(proxyId: string, player: Player, rf: RemoteFunction) {
327
+ while (player.Parent !== undefined && proxyByPlayer.has(player)) {
328
+ if (!RunService.IsRunning()) {
329
+ unregisterProxy(player);
330
+ break;
331
+ }
332
+ const [ok, res] = pcall(() =>
333
+ HttpService.RequestAsync({
334
+ Url: `${mcpUrl}/poll?pluginSessionId=${proxyId}`,
335
+ Method: "GET",
336
+ Headers: { "Content-Type": "application/json" },
337
+ }),
338
+ );
339
+ if (ok && res && (res.Success || res.StatusCode === 503)) {
340
+ const [okJson, body] = pcall(() => HttpService.JSONDecode(res.Body) as PollResponseBody);
341
+ if (okJson && body) {
342
+ // Server lost our proxy registration (process restart, etc.) -
343
+ // re-register so the next poll cycle starts routing again.
344
+ if (body.knownInstance === false) {
345
+ reRegisterProxy(proxyId, "client");
346
+ }
347
+ if (body.request && body.requestId !== undefined) {
348
+ const request = body.request;
349
+ let response: unknown;
350
+ if (CLIENT_BROKER_ALLOWED_ENDPOINTS.has(request.endpoint)) {
351
+ // Forward as a discriminated envelope so the client-side
352
+ // OnClientInvoke knows which endpoint it's serving.
353
+ const envelope = { endpoint: request.endpoint, data: request.data };
354
+ const [okInvoke, invokeRes] = pcall(() => rf.InvokeClient(player, envelope));
355
+ if (okInvoke) {
356
+ response = invokeRes !== undefined ? invokeRes : { success: false, error: "nil response" };
357
+ } else {
358
+ response = { success: false, error: `InvokeClient failed: ${tostring(invokeRes)}` };
359
+ }
360
+ } else {
361
+ const allowed: string[] = [];
362
+ for (const ep of CLIENT_BROKER_ALLOWED_ENDPOINTS) allowed.push(ep);
363
+ response = {
364
+ error:
365
+ `Client-proxy does not forward ${tostring(request.endpoint)}. ` +
366
+ `Allowed: ${allowed.join(", ")}.`,
367
+ };
368
+ }
369
+ postJson("/response", { requestId: body.requestId, response });
370
+ }
371
+ }
372
+ }
373
+ task.wait(0.5);
374
+ }
375
+ }
376
+
377
+ function registerProxy(player: Player, rf: RemoteFunction) {
378
+ if (proxyByPlayer.has(player)) return;
379
+ const proxyId = HttpService.GenerateGUID(false);
380
+ const [ok, res] = postJson("/ready", {
381
+ pluginSessionId: proxyId,
382
+ instanceId: computeInstanceId(),
383
+ role: "client",
384
+ placeId: game.PlaceId,
385
+ placeName: resolvePlaceName(),
386
+ dataModelName: game.Name,
387
+ isRunning: RunService.IsRunning(),
388
+ pluginVersion: State.CURRENT_VERSION,
389
+ pluginVariant: State.PLUGIN_VARIANT,
390
+ protocolVersion: State.PROTOCOL_VERSION,
391
+ });
392
+ if (!ok || !res || !res.Success) {
393
+ proxyRegisterFailuresByPlayer.add(player);
394
+ warn(`[BloxForge] proxy register failed for ${player.Name}: ${formatPostJsonFailure("/ready", ok, res)}`);
395
+ return;
396
+ }
397
+ const body = HttpService.JSONDecode(res.Body) as ReadyResponseBody;
398
+ const assigned = body.assignedRole ?? "client";
399
+ proxyByPlayer.set(player, { pluginSessionId: proxyId, role: assigned });
400
+ if (proxyRegisterFailuresByPlayer.has(player)) {
401
+ proxyRegisterFailuresByPlayer.delete(player);
402
+ print(`[BloxForge] proxy registered for ${player.Name} as ${assigned} via ${mcpUrl}`);
403
+ }
404
+ task.spawn(pollProxy, proxyId, player, rf);
405
+ }
406
+
407
+ // (Removed: startEditProxyLoop. The play-server DM no longer registers an
408
+ // "edit-proxy" peer with the MCP server. stop_playtest now uses a cross-DM
409
+ // plugin:SetSetting request consumed by StopPlayMonitor in the play-server DM,
410
+ // which doesn't depend on MCP server state or peer registration at all.)
411
+
412
+ function setupServerBroker() {
413
+ if (serverBrokerStarted) return;
414
+ let rf = ReplicatedStorage.FindFirstChild(BROKER_NAME) as RemoteFunction | undefined;
415
+ if (!rf) {
416
+ rf = new Instance("RemoteFunction");
417
+ rf.Name = BROKER_NAME;
418
+ rf.Parent = ReplicatedStorage;
419
+ }
420
+ if (rf.GetAttribute(BROKER_OWNER_ATTRIBUTE) !== undefined) {
421
+ return;
422
+ }
423
+ rf.SetAttribute(BROKER_OWNER_ATTRIBUTE, HttpService.GenerateGUID(false));
424
+ serverBrokerStarted = true;
425
+ const broker = rf;
426
+ Players.PlayerAdded.Connect((p) => registerProxy(p, broker));
427
+ for (const p of Players.GetPlayers()) {
428
+ task.spawn(registerProxy, p, broker);
429
+ }
430
+ Players.PlayerRemoving.Connect((p) => {
431
+ unregisterProxy(p);
432
+ });
433
+ game.BindToClose(() => {
434
+ disconnectAllProxies();
435
+ });
436
+ }
437
+
438
+ export = {
439
+ MCP_URL: DEFAULT_MCP_URL,
440
+ DEFAULT_MCP_URL,
441
+ getServerUrl,
442
+ setServerUrl,
443
+ disconnectAllProxies,
444
+ forkRole,
445
+ setupClientBroker,
446
+ setupServerBroker,
447
+ };