@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.
- package/dist/index.js +1292 -281
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +494 -203
- package/studio-plugin/MCPPlugin.rbxmx +494 -203
- package/studio-plugin/src/modules/ClientBroker.ts +7 -2
- package/studio-plugin/src/modules/Communication.ts +6 -12
- package/studio-plugin/src/modules/EvalBridges.ts +96 -68
- package/studio-plugin/src/modules/LuauExec.ts +134 -36
- package/studio-plugin/src/modules/RuntimeLogBuffer.ts +9 -9
- package/studio-plugin/src/modules/handlers/EvalRuntimeHandlers.ts +149 -0
- package/studio-plugin/src/modules/handlers/LogHandlers.ts +5 -6
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +2 -33
- package/studio-plugin/src/server/index.server.ts +27 -8
|
@@ -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
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
//
|
|
204
|
-
//
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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();
|