@chrrxs/robloxstudio-mcp 2.15.1 → 2.16.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.
@@ -0,0 +1,149 @@
1
+ import { LogService, ReplicatedStorage, RunService, ServerScriptService } from "@rbxts/services";
2
+ import { BRIDGE_NAMES, ensureRuntimeBridgeInstalled } from "../EvalBridges";
3
+ import LuauExec from "../LuauExec";
4
+
5
+ const PAYLOAD_INSTANCE_NAME = "__MCPEvalPayload";
6
+
7
+ interface BridgeInvokeResult {
8
+ ok?: boolean;
9
+ value?: unknown;
10
+ }
11
+
12
+ interface WrapperResult {
13
+ ok?: boolean;
14
+ value?: unknown;
15
+ output?: unknown;
16
+ }
17
+
18
+ function findBridge(config: { service: Instance; bridgeName: string }): BindableFunction | undefined {
19
+ const bridge = config.service.FindFirstChild(config.bridgeName);
20
+ return bridge && bridge.IsA("BindableFunction") ? bridge : undefined;
21
+ }
22
+
23
+ function waitForBridge(config: { service: Instance; bridgeName: string }, timeoutSec = 2): BindableFunction | undefined {
24
+ const deadline = tick() + timeoutSec;
25
+ let bridge = findBridge(config);
26
+ while (!bridge && tick() < deadline) {
27
+ task.wait(0.05);
28
+ bridge = findBridge(config);
29
+ }
30
+ return bridge;
31
+ }
32
+
33
+ function getBridgeConfig() {
34
+ if (!RunService.IsRunning()) {
35
+ return {
36
+ error: "eval_*_runtime requires a running playtest.",
37
+ };
38
+ }
39
+ if (RunService.IsServer()) {
40
+ return {
41
+ service: ServerScriptService,
42
+ bridgeName: BRIDGE_NAMES.serverLocal,
43
+ missingError: "ServerEvalBridge not found. The bridge runs inside the play DM, so a playtest must be running. The bridge installs automatically in the runtime server peer, including for manually-started playtests.",
44
+ };
45
+ }
46
+ return {
47
+ service: ReplicatedStorage,
48
+ bridgeName: BRIDGE_NAMES.clientLocal,
49
+ missingError: "ClientEvalBridge not found. The bridge runs inside the play DM, so a playtest must be running. The bridge installs automatically in the runtime client peer, including for manually-started playtests.",
50
+ };
51
+ }
52
+
53
+ function evalRuntime(requestData: Record<string, unknown>) {
54
+ const code = requestData.code as string;
55
+ if (!code || code === "") return { error: "Code is required" };
56
+
57
+ const config = getBridgeConfig();
58
+ if (config.error !== undefined) {
59
+ return { bridge: "missing", error: config.error };
60
+ }
61
+
62
+ let bridge = findBridge(config);
63
+ if (!bridge) {
64
+ const install = ensureRuntimeBridgeInstalled();
65
+ if (!install.installed) {
66
+ return {
67
+ bridge: "missing",
68
+ error: `${config.missingError} Runtime bridge install failed: ${install.error}`,
69
+ };
70
+ }
71
+ bridge = waitForBridge(config);
72
+ }
73
+ if (!bridge) {
74
+ return {
75
+ bridge: "missing",
76
+ error: `${config.missingError} Runtime bridge was installed but did not become ready.`,
77
+ };
78
+ }
79
+
80
+ const m = new Instance("ModuleScript");
81
+ m.Name = PAYLOAD_INSTANCE_NAME;
82
+ const userLines = LuauExec.countLines(code);
83
+ const wrapped = LuauExec.buildWrapper(code, PAYLOAD_INSTANCE_NAME);
84
+
85
+ const [okSet, setErr] = pcall(() => {
86
+ (m as unknown as { Source: string }).Source = wrapped;
87
+ });
88
+ if (!okSet) {
89
+ m.Destroy();
90
+ return {
91
+ bridge: "ok",
92
+ ok: false,
93
+ error: `ModuleScript Source set failed: ${tostring(setErr)}`,
94
+ };
95
+ }
96
+
97
+ m.Parent = game.GetService("Workspace");
98
+ const historyStart = LogService.GetLogHistory().size();
99
+ const [invokeOk, invokeResult] = pcall(() => bridge.Invoke(m) as BridgeInvokeResult);
100
+ m.Destroy();
101
+
102
+ if (!invokeOk) {
103
+ return {
104
+ bridge: "ok",
105
+ ok: false,
106
+ error: tostring(invokeResult),
107
+ };
108
+ }
109
+
110
+ if (!typeIs(invokeResult, "table")) {
111
+ return {
112
+ bridge: "ok",
113
+ ok: false,
114
+ error: `Eval bridge returned invalid result: ${tostring(invokeResult)}`,
115
+ };
116
+ }
117
+
118
+ const bridgeResult = invokeResult as BridgeInvokeResult;
119
+ if (bridgeResult.ok !== true) {
120
+ return {
121
+ bridge: "ok",
122
+ ok: false,
123
+ error: LuauExec.recoverPayloadRequireError(bridgeResult.value, userLines, PAYLOAD_INSTANCE_NAME, historyStart),
124
+ };
125
+ }
126
+
127
+ const inner = bridgeResult.value;
128
+ if (!typeIs(inner, "table")) {
129
+ return {
130
+ bridge: "ok",
131
+ ok: true,
132
+ result: inner === undefined ? undefined : LuauExec.formatReturnValue(inner),
133
+ };
134
+ }
135
+
136
+ const r = inner as WrapperResult;
137
+ const ok = r.ok === true;
138
+ return {
139
+ bridge: "ok",
140
+ ok,
141
+ result: ok && r.value !== undefined ? LuauExec.formatReturnValue(r.value) : undefined,
142
+ error: !ok ? tostring(r.value) : undefined,
143
+ output: r.output ?? [],
144
+ };
145
+ }
146
+
147
+ export = {
148
+ evalRuntime,
149
+ };
@@ -4,12 +4,11 @@ function getRuntimeLogs(requestData: Record<string, unknown>): unknown {
4
4
  const since = requestData.since as number | undefined;
5
5
  const tail = requestData.tail as number | undefined;
6
6
  const filter = requestData.filter as string | undefined;
7
- // Plugin-side peer tag is generic ("edit"|"server"|"client"). The MCP-side
8
- // aggregator overrides it with the specific instance role (e.g. "client-1")
9
- // during fan-out for target=all, so this value is only authoritative for
10
- // the single-peer query path.
11
- const peer = RuntimeLogBuffer.detectPeer();
12
- return RuntimeLogBuffer.query({ since, tail, filter }, peer);
7
+ // This is the buffer that captured the LogService event, not necessarily
8
+ // the script-origin peer. Ordinary playtests share/reflect logs across
9
+ // edit/server/client LogService buffers.
10
+ const capturedBy = RuntimeLogBuffer.detectPeer();
11
+ return RuntimeLogBuffer.query({ since, tail, filter }, capturedBy);
13
12
  }
14
13
 
15
14
  export = { getRuntimeLogs };
@@ -1,5 +1,4 @@
1
1
  import { HttpService, LogService, Players, RunService } from "@rbxts/services";
2
- import { installBridges, ensureBridgesInstalled } from "../EvalBridges";
3
2
  import StopPlayMonitor from "../StopPlayMonitor";
4
3
 
5
4
  interface StudioTestServiceMultiplayer extends StudioTestService {
@@ -200,9 +199,8 @@ function startPlaytest(requestData: Record<string, unknown>) {
200
199
  logConnection = undefined;
201
200
  }
202
201
  cleanupStopListener();
203
- // Note: eval bridges are intentionally NOT cleaned up they live
204
- // permanently in the edit DM so manual playtests also get them. See
205
- // EvalBridges.ts lifecycle comment.
202
+ // Runtime eval bridges are created by the play server/client plugin
203
+ // peers and disappear with the play DataModels.
206
204
  }
207
205
 
208
206
  if (testRunning) {
@@ -236,15 +234,6 @@ function startPlaytest(requestData: Record<string, unknown>) {
236
234
  warn(`[MCP] Failed to inject stop listener: ${injErr}`);
237
235
  }
238
236
 
239
- // Force-refresh the game-VM eval bridges (ServerEvalBridge + ClientEvalBridge)
240
- // right before cloning so the play DMs get the current source. They also
241
- // live permanently in the edit DM (installed on connect) so manually-started
242
- // playtests get them too; here we just ensure they're fresh.
243
- const bridgeInstall = installBridges();
244
- if (!bridgeInstall.installed) {
245
- warn(`[MCP] Eval bridge install failed: ${bridgeInstall.error}`);
246
- }
247
-
248
237
  task.spawn(() => {
249
238
  const [ok, result] = pcall(() => {
250
239
  if (mode === "play") {
@@ -266,22 +255,12 @@ function startPlaytest(requestData: Record<string, unknown>) {
266
255
  testRunning = false;
267
256
 
268
257
  cleanupStopListener();
269
- // Eval bridges persist in the edit DM (see EvalBridges.ts) — do not
270
- // clean up here, so the next manual playtest still gets them.
271
- ensureBridgesInstalled();
272
258
  });
273
259
 
274
260
  const response: Record<string, unknown> = {
275
261
  success: true,
276
262
  message: `Playtest started in ${mode} mode.`,
277
263
  };
278
- // Only mention eval bridges when they failed — when they're fine, the
279
- // detail is noise. eval_server_runtime / eval_client_runtime will surface
280
- // their own clear errors if the caller tries to use them after a failed
281
- // install.
282
- if (!bridgeInstall.installed) {
283
- response.evalBridgesError = bridgeInstall.error;
284
- }
285
264
 
286
265
  return response;
287
266
  }
@@ -370,11 +349,6 @@ function multiplayerTestStart(requestData: Record<string, unknown>) {
370
349
  const testArgs = requestData.testArgs !== undefined ? requestData.testArgs : {};
371
350
  const testId = HttpService.GenerateGUID(false);
372
351
 
373
- const bridgeInstall = installBridges();
374
- if (!bridgeInstall.installed) {
375
- warn(`[MCP] Eval bridge install failed: ${bridgeInstall.error}`);
376
- }
377
-
378
352
  multiplayerState = {
379
353
  phase: "starting",
380
354
  testId,
@@ -400,8 +374,6 @@ function multiplayerTestStart(requestData: Record<string, unknown>) {
400
374
  multiplayerState.result = undefined;
401
375
  multiplayerState.error = tostring(result);
402
376
  }
403
-
404
- ensureBridgesInstalled();
405
377
  });
406
378
 
407
379
  const response: Record<string, unknown> = {
@@ -412,9 +384,6 @@ function multiplayerTestStart(requestData: Record<string, unknown>) {
412
384
  numPlayers,
413
385
  testArgs,
414
386
  };
415
- if (!bridgeInstall.installed) {
416
- response.evalBridgesError = bridgeInstall.error;
417
- }
418
387
  return response;
419
388
  }
420
389
 
@@ -2,6 +2,7 @@ import State from "../modules/State";
2
2
  import UI from "../modules/UI";
3
3
  import Communication from "../modules/Communication";
4
4
  import ClientBroker from "../modules/ClientBroker";
5
+ import { cleanupLegacyEditBridges, ensureRuntimeBridgeInstalled } from "../modules/EvalBridges";
5
6
  import RuntimeLogBuffer from "../modules/RuntimeLogBuffer";
6
7
  import StopPlayMonitor from "../modules/StopPlayMonitor";
7
8
  import * as RenderMonitor from "../modules/RenderMonitor";
@@ -28,10 +29,24 @@ const elements = UI.getElements();
28
29
  const ICON_DISCONNECTED = "rbxassetid://__BUTTON_ICON_DISCONNECTED__";
29
30
  const ICON_CONNECTING = "rbxassetid://__BUTTON_ICON_CONNECTING__";
30
31
  const ICON_CONNECTED = "rbxassetid://__BUTTON_ICON_CONNECTED__";
32
+ const TOOLBAR_REGISTRATION_DELAY_SECONDS = 1;
31
33
 
32
- const toolbar = plugin.CreateToolbar("__TOOLBAR_NAME__");
33
- const button = toolbar.CreateButton("__BUTTON_TITLE__", "__BUTTON_TOOLTIP__", ICON_DISCONNECTED);
34
- UI.setToolbarButton(button, { disconnected: ICON_DISCONNECTED, connecting: ICON_CONNECTING, connected: ICON_CONNECTED });
34
+ let toolbarButtonRegistered = false;
35
+
36
+ function registerToolbarButton() {
37
+ if (toolbarButtonRegistered) {
38
+ return;
39
+ }
40
+ toolbarButtonRegistered = true;
41
+
42
+ const toolbar = plugin.CreateToolbar("__TOOLBAR_NAME__");
43
+ const button = toolbar.CreateButton("__BUTTON_TITLE__", "__BUTTON_TOOLTIP__", ICON_DISCONNECTED);
44
+ UI.setToolbarButton(button, { disconnected: ICON_DISCONNECTED, connecting: ICON_CONNECTING, connected: ICON_CONNECTED });
45
+
46
+ button.Click.Connect(() => {
47
+ elements.screenGui.Enabled = !elements.screenGui.Enabled;
48
+ });
49
+ }
35
50
 
36
51
 
37
52
  elements.connectButton.Activated.Connect(() => {
@@ -44,11 +59,6 @@ elements.connectButton.Activated.Connect(() => {
44
59
  });
45
60
 
46
61
 
47
- button.Click.Connect(() => {
48
- elements.screenGui.Enabled = !elements.screenGui.Enabled;
49
- });
50
-
51
-
52
62
  plugin.Unloading.Connect(() => {
53
63
  Communication.deactivateAll();
54
64
  });
@@ -56,6 +66,7 @@ plugin.Unloading.Connect(() => {
56
66
 
57
67
  UI.updateUIState();
58
68
  Communication.checkForUpdates();
69
+ task.delay(TOOLBAR_REGISTRATION_DELAY_SECONDS, registerToolbarButton);
59
70
 
60
71
  // Auto-activate per peer. The boshyxd plugin only registers with MCP when the
61
72
  // user clicks Connect in its UI, but that UI is invisible in play DMs - so
@@ -63,6 +74,14 @@ Communication.checkForUpdates();
63
74
  // short delay so the UI/State have a chance to initialize first.
64
75
  task.delay(2, () => {
65
76
  const role = ClientBroker.forkRole();
77
+ if (role === "edit") {
78
+ cleanupLegacyEditBridges();
79
+ } else {
80
+ const result = ensureRuntimeBridgeInstalled();
81
+ if (!result.installed) {
82
+ warn(`[MCPPlugin] Runtime eval bridge install failed: ${result.error}`);
83
+ }
84
+ }
66
85
  if (role === "edit" || role === "server") {
67
86
  pcall(() => {
68
87
  const idx = State.getActiveTabIndex();