@chrrxs/robloxstudio-mcp 2.15.2 → 2.16.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 +1288 -60
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +616 -236
- package/studio-plugin/MCPPlugin.rbxmx +616 -236
- package/studio-plugin/src/modules/ClientBroker.ts +32 -6
- package/studio-plugin/src/modules/Communication.ts +44 -34
- package/studio-plugin/src/modules/EvalBridges.ts +91 -64
- package/studio-plugin/src/modules/HttpDiagnostics.ts +50 -0
- package/studio-plugin/src/modules/ServerUrlSettings.ts +48 -0
- package/studio-plugin/src/modules/StopPlayMonitor.ts +152 -35
- package/studio-plugin/src/modules/handlers/EvalRuntimeHandlers.ts +34 -6
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +20 -46
- package/studio-plugin/src/server/index.server.ts +41 -11
|
@@ -10,6 +10,10 @@ local State = TS.import(script, script, "modules", "State")
|
|
|
10
10
|
local UI = TS.import(script, script, "modules", "UI")
|
|
11
11
|
local Communication = TS.import(script, script, "modules", "Communication")
|
|
12
12
|
local ClientBroker = TS.import(script, script, "modules", "ClientBroker")
|
|
13
|
+
local ServerUrlSettings = TS.import(script, script, "modules", "ServerUrlSettings")
|
|
14
|
+
local _EvalBridges = TS.import(script, script, "modules", "EvalBridges")
|
|
15
|
+
local cleanupLegacyEditBridges = _EvalBridges.cleanupLegacyEditBridges
|
|
16
|
+
local ensureRuntimeBridgeInstalled = _EvalBridges.ensureRuntimeBridgeInstalled
|
|
13
17
|
local RuntimeLogBuffer = TS.import(script, script, "modules", "RuntimeLogBuffer")
|
|
14
18
|
local StopPlayMonitor = TS.import(script, script, "modules", "StopPlayMonitor")
|
|
15
19
|
local RenderMonitor = TS.import(script, script, "modules", "RenderMonitor")
|
|
@@ -25,18 +29,30 @@ RuntimeLogBuffer.install()
|
|
|
25
29
|
-- edit DM (write the flag) and the play-server DM (read+act on the flag) can
|
|
26
30
|
-- access plugin:SetSetting/GetSetting.
|
|
27
31
|
StopPlayMonitor.init(plugin)
|
|
32
|
+
ServerUrlSettings.init(plugin)
|
|
28
33
|
UI.init(plugin)
|
|
29
34
|
local elements = UI.getElements()
|
|
30
35
|
local ICON_DISCONNECTED = "rbxassetid://75876056391496"
|
|
31
36
|
local ICON_CONNECTING = "rbxassetid://71302583919560"
|
|
32
37
|
local ICON_CONNECTED = "rbxassetid://130958234173611"
|
|
33
|
-
local
|
|
34
|
-
local
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
local TOOLBAR_REGISTRATION_DELAY_SECONDS = 1
|
|
39
|
+
local toolbarButtonRegistered = false
|
|
40
|
+
local function registerToolbarButton()
|
|
41
|
+
if toolbarButtonRegistered then
|
|
42
|
+
return nil
|
|
43
|
+
end
|
|
44
|
+
toolbarButtonRegistered = true
|
|
45
|
+
local toolbar = plugin:CreateToolbar("MCP Integration")
|
|
46
|
+
local button = toolbar:CreateButton("MCP Server", "Connect to MCP Server for AI Integration", ICON_DISCONNECTED)
|
|
47
|
+
UI.setToolbarButton(button, {
|
|
48
|
+
disconnected = ICON_DISCONNECTED,
|
|
49
|
+
connecting = ICON_CONNECTING,
|
|
50
|
+
connected = ICON_CONNECTED,
|
|
51
|
+
})
|
|
52
|
+
button.Click:Connect(function()
|
|
53
|
+
elements.screenGui.Enabled = not elements.screenGui.Enabled
|
|
54
|
+
end)
|
|
55
|
+
end
|
|
40
56
|
elements.connectButton.Activated:Connect(function()
|
|
41
57
|
local conn = State.getActiveConnection()
|
|
42
58
|
if conn and conn.isActive then
|
|
@@ -45,29 +61,54 @@ elements.connectButton.Activated:Connect(function()
|
|
|
45
61
|
Communication.activatePlugin(State.getActiveTabIndex())
|
|
46
62
|
end
|
|
47
63
|
end)
|
|
48
|
-
button.Click:Connect(function()
|
|
49
|
-
elements.screenGui.Enabled = not elements.screenGui.Enabled
|
|
50
|
-
end)
|
|
51
64
|
plugin.Unloading:Connect(function()
|
|
52
65
|
Communication.deactivateAll()
|
|
53
66
|
end)
|
|
54
67
|
UI.updateUIState()
|
|
55
68
|
Communication.checkForUpdates()
|
|
69
|
+
task.delay(TOOLBAR_REGISTRATION_DELAY_SECONDS, registerToolbarButton)
|
|
56
70
|
-- Auto-activate per peer. The boshyxd plugin only registers with MCP when the
|
|
57
71
|
-- user clicks Connect in its UI, but that UI is invisible in play DMs - so
|
|
58
72
|
-- play peers' plugin instances load without ever registering. Run after a
|
|
59
73
|
-- short delay so the UI/State have a chance to initialize first.
|
|
60
74
|
task.delay(2, function()
|
|
61
75
|
local role = ClientBroker.forkRole()
|
|
76
|
+
if role == "edit" then
|
|
77
|
+
cleanupLegacyEditBridges()
|
|
78
|
+
else
|
|
79
|
+
local result = ensureRuntimeBridgeInstalled()
|
|
80
|
+
if not result.installed then
|
|
81
|
+
warn(`[robloxstudio-mcp] Runtime eval bridge install failed: {result.error}`)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
62
84
|
if role == "edit" or role == "server" then
|
|
63
85
|
pcall(function()
|
|
64
86
|
local idx = State.getActiveTabIndex()
|
|
65
87
|
local conn = State.getConnection(idx)
|
|
66
88
|
if conn and not conn.isActive then
|
|
89
|
+
if role == "server" then
|
|
90
|
+
local _condition = ServerUrlSettings.readServerUrl()
|
|
91
|
+
if _condition == nil then
|
|
92
|
+
_condition = ClientBroker.DEFAULT_MCP_URL
|
|
93
|
+
end
|
|
94
|
+
local inheritedServerUrl = _condition
|
|
95
|
+
conn.serverUrl = inheritedServerUrl
|
|
96
|
+
elements.urlInput.Text = inheritedServerUrl
|
|
97
|
+
local portStr = string.match(conn.serverUrl, ":(%d+)$")
|
|
98
|
+
if portStr ~= 0 and portStr == portStr and portStr ~= "" and portStr then
|
|
99
|
+
local _condition_1 = tonumber(portStr)
|
|
100
|
+
if _condition_1 == nil then
|
|
101
|
+
_condition_1 = conn.port
|
|
102
|
+
end
|
|
103
|
+
conn.port = _condition_1
|
|
104
|
+
end
|
|
105
|
+
ClientBroker.setServerUrl(inheritedServerUrl)
|
|
106
|
+
end
|
|
67
107
|
-- Defensive default: in invisible play-DM UIs, the input field
|
|
68
108
|
-- may not be populated by the time we activate.
|
|
69
109
|
if conn.serverUrl == nil or conn.serverUrl == "" then
|
|
70
|
-
conn.serverUrl = ClientBroker.
|
|
110
|
+
conn.serverUrl = ClientBroker.DEFAULT_MCP_URL
|
|
111
|
+
elements.urlInput.Text = conn.serverUrl
|
|
71
112
|
end
|
|
72
113
|
Communication.activatePlugin(idx)
|
|
73
114
|
end
|
|
@@ -76,8 +117,8 @@ task.delay(2, function()
|
|
|
76
117
|
if role == "server" then
|
|
77
118
|
ClientBroker.setupServerBroker()
|
|
78
119
|
-- The play-server DM is the only one where StudioTestService:EndTest is
|
|
79
|
-
-- legal, so the stop-play monitor lives here.
|
|
80
|
-
--
|
|
120
|
+
-- legal, so the stop-play monitor lives here. It consumes tokenized
|
|
121
|
+
-- stop requests from plugin settings and acknowledges EndTest results.
|
|
81
122
|
StopPlayMonitor.startMonitor()
|
|
82
123
|
elseif role == "client" then
|
|
83
124
|
ClientBroker.setupClientBroker()
|
|
@@ -108,6 +149,7 @@ local InputHandlers = TS.import(script, script.Parent, "handlers", "InputHandler
|
|
|
108
149
|
local EvalRuntimeHandlers = TS.import(script, script.Parent, "handlers", "EvalRuntimeHandlers")
|
|
109
150
|
local LuauExec = TS.import(script, script.Parent, "LuauExec")
|
|
110
151
|
local State = TS.import(script, script.Parent, "State")
|
|
152
|
+
local HttpDiagnostics = TS.import(script, script.Parent, "HttpDiagnostics")
|
|
111
153
|
local StudioTestService = game:GetService("StudioTestService")
|
|
112
154
|
-- Mirror of Communication.computeInstanceId() — duplicated here because the
|
|
113
155
|
-- client broker runs in the play-server DM where it can't easily import from
|
|
@@ -160,7 +202,8 @@ end
|
|
|
160
202
|
-- intercept /api/stop-playtest and call StudioTestService:EndTest. That hack
|
|
161
203
|
-- is gone: stop now uses StopPlayMonitor with plugin:SetSetting cross-DM
|
|
162
204
|
-- signaling, which works regardless of MCP server state.)
|
|
163
|
-
local
|
|
205
|
+
local DEFAULT_MCP_URL = "http://localhost:58741"
|
|
206
|
+
local mcpUrl = DEFAULT_MCP_URL
|
|
164
207
|
local BROKER_NAME = "__MCPClientBroker"
|
|
165
208
|
local BROKER_OWNER_ATTRIBUTE = "__MCPBrokerOwner"
|
|
166
209
|
-- Endpoints the server-peer broker is allowed to forward to the client peer.
|
|
@@ -221,7 +264,7 @@ end
|
|
|
221
264
|
function postJson(endpoint, body)
|
|
222
265
|
return pcall(function()
|
|
223
266
|
return HttpService:RequestAsync({
|
|
224
|
-
Url = `{
|
|
267
|
+
Url = `{mcpUrl}{endpoint}`,
|
|
225
268
|
Method = "POST",
|
|
226
269
|
Headers = {
|
|
227
270
|
["Content-Type"] = "application/json",
|
|
@@ -230,6 +273,17 @@ function postJson(endpoint, body)
|
|
|
230
273
|
})
|
|
231
274
|
end)
|
|
232
275
|
end
|
|
276
|
+
local function formatPostJsonFailure(endpoint, ok, res)
|
|
277
|
+
return HttpDiagnostics.formatRequestFailure(`{mcpUrl}{endpoint}`, ok, res)
|
|
278
|
+
end
|
|
279
|
+
local function setServerUrl(serverUrl)
|
|
280
|
+
if serverUrl ~= nil and serverUrl ~= "" then
|
|
281
|
+
mcpUrl = serverUrl
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
local function getServerUrl()
|
|
285
|
+
return mcpUrl
|
|
286
|
+
end
|
|
233
287
|
local function handleExecuteLuau(data)
|
|
234
288
|
local code = data and (data.code)
|
|
235
289
|
if type(code) == "string" == false or code == "" then
|
|
@@ -376,6 +430,7 @@ local function setupClientBroker()
|
|
|
376
430
|
end
|
|
377
431
|
end
|
|
378
432
|
local proxyByPlayer = {}
|
|
433
|
+
local proxyRegisterFailuresByPlayer = {}
|
|
379
434
|
local serverBrokerStarted = false
|
|
380
435
|
local function pollProxy(proxyId, player, rf)
|
|
381
436
|
while true do
|
|
@@ -389,7 +444,7 @@ local function pollProxy(proxyId, player, rf)
|
|
|
389
444
|
end
|
|
390
445
|
local ok, res = pcall(function()
|
|
391
446
|
return HttpService:RequestAsync({
|
|
392
|
-
Url = `{
|
|
447
|
+
Url = `{mcpUrl}/poll?pluginSessionId={proxyId}`,
|
|
393
448
|
Method = "GET",
|
|
394
449
|
Headers = {
|
|
395
450
|
["Content-Type"] = "application/json",
|
|
@@ -468,7 +523,9 @@ local function registerProxy(player, rf)
|
|
|
468
523
|
pluginVariant = State.PLUGIN_VARIANT,
|
|
469
524
|
})
|
|
470
525
|
if not ok or not res or not res.Success then
|
|
471
|
-
|
|
526
|
+
local _player_1 = player
|
|
527
|
+
proxyRegisterFailuresByPlayer[_player_1] = true
|
|
528
|
+
warn(`[robloxstudio-mcp] proxy register failed for {player.Name}: {formatPostJsonFailure("/ready", ok, res)}`)
|
|
472
529
|
return nil
|
|
473
530
|
end
|
|
474
531
|
local body = HttpService:JSONDecode(res.Body)
|
|
@@ -483,11 +540,17 @@ local function registerProxy(player, rf)
|
|
|
483
540
|
role = assigned,
|
|
484
541
|
}
|
|
485
542
|
proxyByPlayer[_player_1] = _arg1
|
|
543
|
+
local _player_2 = player
|
|
544
|
+
if proxyRegisterFailuresByPlayer[_player_2] ~= nil then
|
|
545
|
+
local _player_3 = player
|
|
546
|
+
proxyRegisterFailuresByPlayer[_player_3] = nil
|
|
547
|
+
print(`[robloxstudio-mcp] proxy registered for {player.Name} as {assigned} via {mcpUrl}`)
|
|
548
|
+
end
|
|
486
549
|
task.spawn(pollProxy, proxyId, player, rf)
|
|
487
550
|
end
|
|
488
551
|
-- (Removed: startEditProxyLoop. The play-server DM no longer registers an
|
|
489
552
|
-- "edit-proxy" peer with the MCP server. stop_playtest now uses a cross-DM
|
|
490
|
-
-- plugin:SetSetting
|
|
553
|
+
-- plugin:SetSetting request consumed by StopPlayMonitor in the play-server DM,
|
|
491
554
|
-- which doesn't depend on MCP server state or peer registration at all.)
|
|
492
555
|
local function setupServerBroker()
|
|
493
556
|
if serverBrokerStarted then
|
|
@@ -517,6 +580,8 @@ local function setupServerBroker()
|
|
|
517
580
|
if entry then
|
|
518
581
|
local _p_1 = p
|
|
519
582
|
proxyByPlayer[_p_1] = nil
|
|
583
|
+
local _p_2 = p
|
|
584
|
+
proxyRegisterFailuresByPlayer[_p_2] = nil
|
|
520
585
|
postJson("/disconnect", {
|
|
521
586
|
pluginSessionId = entry.pluginSessionId,
|
|
522
587
|
})
|
|
@@ -532,7 +597,10 @@ local function setupServerBroker()
|
|
|
532
597
|
end)
|
|
533
598
|
end
|
|
534
599
|
return {
|
|
535
|
-
MCP_URL =
|
|
600
|
+
MCP_URL = DEFAULT_MCP_URL,
|
|
601
|
+
DEFAULT_MCP_URL = DEFAULT_MCP_URL,
|
|
602
|
+
getServerUrl = getServerUrl,
|
|
603
|
+
setServerUrl = setServerUrl,
|
|
536
604
|
forkRole = forkRole,
|
|
537
605
|
setupClientBroker = setupClientBroker,
|
|
538
606
|
setupServerBroker = setupServerBroker,
|
|
@@ -552,7 +620,7 @@ local ServerStorage = _services.ServerStorage
|
|
|
552
620
|
local State = TS.import(script, script.Parent, "State")
|
|
553
621
|
local Utils = TS.import(script, script.Parent, "Utils")
|
|
554
622
|
local UI = TS.import(script, script.Parent, "UI")
|
|
555
|
-
local
|
|
623
|
+
local cleanupLegacyEditBridges = TS.import(script, script.Parent, "EvalBridges").cleanupLegacyEditBridges
|
|
556
624
|
local QueryHandlers = TS.import(script, script.Parent, "handlers", "QueryHandlers")
|
|
557
625
|
local PropertyHandlers = TS.import(script, script.Parent, "handlers", "PropertyHandlers")
|
|
558
626
|
local InstanceHandlers = TS.import(script, script.Parent, "handlers", "InstanceHandlers")
|
|
@@ -568,6 +636,8 @@ local SerializationHandlers = TS.import(script, script.Parent, "handlers", "Seri
|
|
|
568
636
|
local MemoryHandlers = TS.import(script, script.Parent, "handlers", "MemoryHandlers")
|
|
569
637
|
local SceneAnalysisHandlers = TS.import(script, script.Parent, "handlers", "SceneAnalysisHandlers")
|
|
570
638
|
local EvalRuntimeHandlers = TS.import(script, script.Parent, "handlers", "EvalRuntimeHandlers")
|
|
639
|
+
local ServerUrlSettings = TS.import(script, script.Parent, "ServerUrlSettings")
|
|
640
|
+
local HttpDiagnostics = TS.import(script, script.Parent, "HttpDiagnostics")
|
|
571
641
|
-- Per-plugin-load random GUID. Used as the /poll URL param so the server
|
|
572
642
|
-- can tell our polls apart from any other plugin's polls. Not user-facing —
|
|
573
643
|
-- MCP tools and the LLM operate on instanceId (the place identifier).
|
|
@@ -597,6 +667,7 @@ local assignedRole
|
|
|
597
667
|
local duplicateInstanceRole = false
|
|
598
668
|
local hasVersionMismatch = false
|
|
599
669
|
local lastVersionMismatchWarningKey
|
|
670
|
+
local readyFailureLogKeys = {}
|
|
600
671
|
-- Cache the published place name from MarketplaceService:GetProductInfo so
|
|
601
672
|
-- /ready can carry a friendly identifier (e.g. "Natural Disasters") distinct
|
|
602
673
|
-- from game.Name (the DataModel name, often "Place1" in edit). We only fetch
|
|
@@ -804,31 +875,49 @@ function sendReady(conn)
|
|
|
804
875
|
}),
|
|
805
876
|
})
|
|
806
877
|
end)
|
|
878
|
+
local readyUrl = `{conn.serverUrl}/ready`
|
|
879
|
+
local readyRole = detectRole()
|
|
880
|
+
local readyLogKey = `{conn.serverUrl}|{instanceId}|{readyRole}`
|
|
807
881
|
if not readyOk then
|
|
882
|
+
readyFailureLogKeys[readyLogKey] = true
|
|
883
|
+
warn(`[robloxstudio-mcp] /ready failed for {instanceId}/{readyRole}: {HttpDiagnostics.formatRequestFailure(readyUrl, readyOk, readyResult)}`)
|
|
808
884
|
return nil
|
|
809
885
|
end
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
ui
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
886
|
+
if not readyResult.Success then
|
|
887
|
+
local reason = HttpDiagnostics.formatRequestFailure(readyUrl, true, readyResult)
|
|
888
|
+
readyFailureLogKeys[readyLogKey] = true
|
|
889
|
+
-- 409 = duplicate_instance_role. Surface in UI and stop polling.
|
|
890
|
+
if readyResult.StatusCode == 409 then
|
|
891
|
+
duplicateInstanceRole = true
|
|
892
|
+
conn.isActive = false
|
|
893
|
+
local ui = UI.getElements()
|
|
894
|
+
if State.getActiveTabIndex() == 0 then
|
|
895
|
+
ui.statusLabel.Text = "Duplicate instance"
|
|
896
|
+
ui.statusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
|
|
897
|
+
ui.detailStatusLabel.Text = reason
|
|
898
|
+
ui.detailStatusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
|
|
899
|
+
end
|
|
900
|
+
warn(`[robloxstudio-mcp] /ready rejected for {instanceId}/{readyRole}: {reason}`)
|
|
901
|
+
return nil
|
|
902
|
+
end
|
|
903
|
+
warn(`[robloxstudio-mcp] /ready rejected for {instanceId}/{readyRole}: {reason}`)
|
|
822
904
|
return nil
|
|
823
905
|
end
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
906
|
+
local parseOk, readyData = pcall(function()
|
|
907
|
+
return HttpService:JSONDecode(readyResult.Body)
|
|
908
|
+
end)
|
|
909
|
+
local _value = parseOk and readyData.assignedRole
|
|
910
|
+
if _value ~= "" and _value then
|
|
911
|
+
assignedRole = readyData.assignedRole
|
|
912
|
+
end
|
|
913
|
+
local _condition = assignedRole
|
|
914
|
+
if _condition == nil then
|
|
915
|
+
_condition = detectRole()
|
|
916
|
+
end
|
|
917
|
+
local connectedRole = _condition
|
|
918
|
+
if readyFailureLogKeys[readyLogKey] ~= nil then
|
|
919
|
+
readyFailureLogKeys[readyLogKey] = nil
|
|
920
|
+
print(`[robloxstudio-mcp] /ready connected for {instanceId}/{connectedRole} via {conn.serverUrl}`)
|
|
832
921
|
end
|
|
833
922
|
end)
|
|
834
923
|
end
|
|
@@ -874,7 +963,7 @@ local function pollForRequests(connIndex)
|
|
|
874
963
|
local warningKey = `{State.CURRENT_VERSION}:{serverVersion}`
|
|
875
964
|
if lastVersionMismatchWarningKey ~= warningKey then
|
|
876
965
|
lastVersionMismatchWarningKey = warningKey
|
|
877
|
-
warn(`[
|
|
966
|
+
warn(`[robloxstudio-mcp] Version mismatch: Studio plugin v{State.CURRENT_VERSION} / MCP v{serverVersion}. Run npx -y @chrrxs/robloxstudio-mcp@latest --auto-install-plugin and restart Studio.`)
|
|
878
967
|
end
|
|
879
968
|
UI.showBanner("version-mismatch", `Plugin v{State.CURRENT_VERSION} / MCP v{serverVersion} mismatch`)
|
|
880
969
|
elseif hasVersionMismatch then
|
|
@@ -1042,6 +1131,7 @@ local function activatePlugin(connIndex)
|
|
|
1042
1131
|
UI.updateTabLabel(idx)
|
|
1043
1132
|
UI.updateUIState()
|
|
1044
1133
|
end
|
|
1134
|
+
ServerUrlSettings.rememberServerUrl(conn.serverUrl)
|
|
1045
1135
|
UI.updateTabDot(idx)
|
|
1046
1136
|
if not conn.heartbeatConnection then
|
|
1047
1137
|
conn.heartbeatConnection = RunService.Heartbeat:Connect(function()
|
|
@@ -1056,18 +1146,10 @@ local function activatePlugin(connIndex)
|
|
|
1056
1146
|
-- Initial /ready; pollForRequests will also re-fire ready if the server
|
|
1057
1147
|
-- later reports knownInstance=false (process restart, etc).
|
|
1058
1148
|
sendReady(conn)
|
|
1059
|
-
--
|
|
1060
|
-
--
|
|
1061
|
-
-- clones them into the play DMs and eval_*_runtime works with no setup
|
|
1062
|
-
-- roundtrip. Only the edit DM installs; play DMs already have the cloned
|
|
1063
|
-
-- copies. Idempotent, so reconnects don't re-dirty the place.
|
|
1149
|
+
-- Remove legacy edit-mode eval bridge scripts from older plugin builds.
|
|
1150
|
+
-- Current bridges are created only in running play DataModels.
|
|
1064
1151
|
if not RunService:IsRunning() then
|
|
1065
|
-
task.spawn(
|
|
1066
|
-
local result = ensureBridgesInstalled()
|
|
1067
|
-
if not result.installed then
|
|
1068
|
-
warn(`[MCPPlugin] Eval bridge install failed: {result.error}`)
|
|
1069
|
-
end
|
|
1070
|
-
end)
|
|
1152
|
+
task.spawn(cleanupLegacyEditBridges)
|
|
1071
1153
|
end
|
|
1072
1154
|
-- Watch for game.Name updates so a stale "Place1" captured at first
|
|
1073
1155
|
-- /ready gets refreshed once Studio settles on the real DM name.
|
|
@@ -1188,29 +1270,15 @@ local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
|
1188
1270
|
-- ServerScriptService.LoadStringEnabled, so eval_server_runtime works even
|
|
1189
1271
|
-- when LoadStringEnabled=false (the default in fresh places).
|
|
1190
1272
|
--
|
|
1191
|
-
-- Lifecycle:
|
|
1192
|
-
--
|
|
1193
|
-
--
|
|
1194
|
-
--
|
|
1195
|
-
--
|
|
1196
|
-
-- DM after a playtest ends (rather than cleaning up) so that a playtest the
|
|
1197
|
-
-- dev starts MANUALLY via the Studio Play button — not the MCP start_playtest
|
|
1198
|
-
-- tool — also gets the bridges cloned in. This is intentionally a little
|
|
1199
|
-
-- intrusive (two helper scripts visible in Explorer) in exchange for a
|
|
1200
|
-
-- zero-roundtrip eval_*_runtime experience for devs working 1:1 with an agent.
|
|
1201
|
-
--
|
|
1202
|
-
-- Archivable handling: ExecutePlayModeAsync's deep-clone SKIPS instances
|
|
1203
|
-
-- with Archivable=false (verified empirically in v2.9.0 testing - bridges
|
|
1204
|
-
-- never reached the play DMs because we'd set them to false). We now keep
|
|
1205
|
-
-- Archivable=true so the clone works, and rely on cleanupBridges() to
|
|
1206
|
-
-- remove the scripts from the edit DM when the test ends. The only failure
|
|
1207
|
-
-- mode is the user saving DURING an active playtest, which would persist
|
|
1208
|
-
-- the bridges to the .rbxl - that's a no-op next session because
|
|
1209
|
-
-- installBridges() always calls cleanupBridges() first to clear stale
|
|
1210
|
-
-- instances. The RemoteFunction/BindableFunction that the bridge scripts
|
|
1211
|
-
-- CREATE at runtime stay Archivable=false (they're runtime-only and should
|
|
1212
|
-
-- never appear in a save).
|
|
1273
|
+
-- Lifecycle: bridge scripts are created only in running play DataModels.
|
|
1274
|
+
-- The server plugin peer creates the Script in runtime ServerScriptService;
|
|
1275
|
+
-- each client plugin peer creates its LocalScript in that client's
|
|
1276
|
+
-- PlayerScripts. Nothing is installed into the edit DataModel anymore.
|
|
1277
|
+
-- Runtime-created scripts disappear naturally when the playtest stops.
|
|
1213
1278
|
local _services = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services")
|
|
1279
|
+
local Players = _services.Players
|
|
1280
|
+
local ReplicatedStorage = _services.ReplicatedStorage
|
|
1281
|
+
local RunService = _services.RunService
|
|
1214
1282
|
local ServerScriptService = _services.ServerScriptService
|
|
1215
1283
|
local StarterPlayer = _services.StarterPlayer
|
|
1216
1284
|
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
@@ -1283,12 +1351,10 @@ bf.OnInvoke = function(payload)\
|
|
|
1283
1351
|
end\
|
|
1284
1352
|
`
|
|
1285
1353
|
-- Stamp written onto each installed bridge Script so we can tell whether the
|
|
1286
|
-
-- bridge currently in the DM was produced by THIS plugin build.
|
|
1287
|
-
-- hash of the actual bridge source plus the plugin version, so ANY
|
|
1288
|
-
-- the source (or a version bump) yields a new stamp
|
|
1289
|
-
--
|
|
1290
|
-
-- keeping a stale bridge that happens to still be present (e.g. one saved into
|
|
1291
|
-
-- the .rbxl from an older build).
|
|
1354
|
+
-- runtime bridge currently in the play DM was produced by THIS plugin build.
|
|
1355
|
+
-- It's a djb2 hash of the actual bridge source plus the plugin version, so ANY
|
|
1356
|
+
-- change to the source (or a version bump) yields a new stamp and triggers a
|
|
1357
|
+
-- runtime refresh instead of keeping a stale bridge.
|
|
1292
1358
|
local STAMP_ATTR = "__MCPBridgeStamp"
|
|
1293
1359
|
local function computeBridgeStamp()
|
|
1294
1360
|
local combined = `{SERVER_BRIDGE_SOURCE}|{CLIENT_BRIDGE_SOURCE}`
|
|
@@ -1296,9 +1362,9 @@ local function computeBridgeStamp()
|
|
|
1296
1362
|
for i = 1, #combined do
|
|
1297
1363
|
h = (h * 33 + (string.byte(combined, i))) % 2147483647
|
|
1298
1364
|
end
|
|
1299
|
-
-- "2.
|
|
1365
|
+
-- "2.16.1" is replaced with the package version at package time
|
|
1300
1366
|
-- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
|
|
1301
|
-
return `{tostring(h)}-2.
|
|
1367
|
+
return `{tostring(h)}-2.16.1`
|
|
1302
1368
|
end
|
|
1303
1369
|
local BRIDGE_STAMP = computeBridgeStamp()
|
|
1304
1370
|
local function setSource(scriptInst, source)
|
|
@@ -1314,15 +1380,26 @@ local function setSource(scriptInst, source)
|
|
|
1314
1380
|
scriptInst.Source = source
|
|
1315
1381
|
end
|
|
1316
1382
|
end
|
|
1317
|
-
local function
|
|
1383
|
+
local function findLegacyEditBridges()
|
|
1318
1384
|
local sps = getStarterPlayerScripts()
|
|
1319
1385
|
return {
|
|
1320
1386
|
server = ServerScriptService:FindFirstChild(SERVER_SCRIPT_NAME),
|
|
1321
1387
|
client = if sps then sps:FindFirstChild(CLIENT_SCRIPT_NAME) else nil,
|
|
1322
1388
|
}
|
|
1323
1389
|
end
|
|
1324
|
-
local function
|
|
1325
|
-
local
|
|
1390
|
+
local function destroyIfPresent(parent, name)
|
|
1391
|
+
local existing = parent:FindFirstChild(name)
|
|
1392
|
+
if existing then
|
|
1393
|
+
pcall(function()
|
|
1394
|
+
return existing:Destroy()
|
|
1395
|
+
end)
|
|
1396
|
+
end
|
|
1397
|
+
end
|
|
1398
|
+
local function cleanupLegacyEditBridges()
|
|
1399
|
+
if RunService:IsRunning() then
|
|
1400
|
+
return nil
|
|
1401
|
+
end
|
|
1402
|
+
local _binding = findLegacyEditBridges()
|
|
1326
1403
|
local server = _binding.server
|
|
1327
1404
|
local client = _binding.client
|
|
1328
1405
|
if server then
|
|
@@ -1336,54 +1413,79 @@ local function cleanupBridges()
|
|
|
1336
1413
|
end)
|
|
1337
1414
|
end
|
|
1338
1415
|
end
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
local
|
|
1345
|
-
local
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
-- plugin, or one persisted in the saved place), so force a refresh.
|
|
1353
|
-
local sStamp = server:GetAttribute(STAMP_ATTR)
|
|
1354
|
-
local cStamp = client:GetAttribute(STAMP_ATTR)
|
|
1355
|
-
if sStamp == BRIDGE_STAMP and cStamp == BRIDGE_STAMP then
|
|
1356
|
-
return {
|
|
1357
|
-
installed = true,
|
|
1358
|
-
}
|
|
1359
|
-
end
|
|
1416
|
+
local function serverRuntimeBridgeReady()
|
|
1417
|
+
local scriptInst = ServerScriptService:FindFirstChild(SERVER_SCRIPT_NAME)
|
|
1418
|
+
local bindable = ServerScriptService:FindFirstChild(BRIDGE_NAMES.serverLocal)
|
|
1419
|
+
return scriptInst ~= nil and scriptInst:GetAttribute(STAMP_ATTR) == BRIDGE_STAMP and bindable ~= nil and bindable:IsA("BindableFunction")
|
|
1420
|
+
end
|
|
1421
|
+
local function getPlayerScripts()
|
|
1422
|
+
local localPlayer = Players.LocalPlayer
|
|
1423
|
+
if not localPlayer then
|
|
1424
|
+
return nil
|
|
1425
|
+
end
|
|
1426
|
+
local playerScripts = localPlayer:FindFirstChild("PlayerScripts")
|
|
1427
|
+
if not playerScripts then
|
|
1428
|
+
playerScripts = localPlayer:WaitForChild("PlayerScripts", 5)
|
|
1360
1429
|
end
|
|
1361
|
-
return
|
|
1430
|
+
return playerScripts
|
|
1362
1431
|
end
|
|
1363
|
-
function
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1432
|
+
local function clientRuntimeBridgeReady()
|
|
1433
|
+
local playerScripts = getPlayerScripts()
|
|
1434
|
+
if not playerScripts then
|
|
1435
|
+
return false
|
|
1436
|
+
end
|
|
1437
|
+
local scriptInst = playerScripts:FindFirstChild(CLIENT_SCRIPT_NAME)
|
|
1438
|
+
local bindable = ReplicatedStorage:FindFirstChild(BRIDGE_NAMES.clientLocal)
|
|
1439
|
+
return scriptInst ~= nil and scriptInst:GetAttribute(STAMP_ATTR) == BRIDGE_STAMP and bindable ~= nil and bindable:IsA("BindableFunction")
|
|
1440
|
+
end
|
|
1441
|
+
local function installServerRuntimeBridge()
|
|
1442
|
+
if serverRuntimeBridgeReady() then
|
|
1443
|
+
return {
|
|
1444
|
+
installed = true,
|
|
1445
|
+
}
|
|
1446
|
+
end
|
|
1369
1447
|
local ok, err = pcall(function()
|
|
1448
|
+
destroyIfPresent(ServerScriptService, SERVER_SCRIPT_NAME)
|
|
1449
|
+
destroyIfPresent(ServerScriptService, BRIDGE_NAMES.serverLocal)
|
|
1370
1450
|
local serverScript = Instance.new("Script")
|
|
1371
1451
|
serverScript.Name = SERVER_SCRIPT_NAME
|
|
1372
|
-
|
|
1373
|
-
-- script. cleanupBridges() removes it from the edit DM when the
|
|
1374
|
-
-- playtest ends.
|
|
1452
|
+
serverScript.Archivable = false
|
|
1375
1453
|
setSource(serverScript, SERVER_BRIDGE_SOURCE)
|
|
1376
1454
|
serverScript:SetAttribute(STAMP_ATTR, BRIDGE_STAMP)
|
|
1377
1455
|
serverScript.Parent = ServerScriptService
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1456
|
+
end)
|
|
1457
|
+
if not ok then
|
|
1458
|
+
return {
|
|
1459
|
+
installed = false,
|
|
1460
|
+
error = tostring(err),
|
|
1461
|
+
}
|
|
1462
|
+
end
|
|
1463
|
+
return {
|
|
1464
|
+
installed = true,
|
|
1465
|
+
}
|
|
1466
|
+
end
|
|
1467
|
+
local function installClientRuntimeBridge()
|
|
1468
|
+
if clientRuntimeBridgeReady() then
|
|
1469
|
+
return {
|
|
1470
|
+
installed = true,
|
|
1471
|
+
}
|
|
1472
|
+
end
|
|
1473
|
+
local playerScripts = getPlayerScripts()
|
|
1474
|
+
if not playerScripts then
|
|
1475
|
+
return {
|
|
1476
|
+
installed = false,
|
|
1477
|
+
error = "Players.LocalPlayer.PlayerScripts not found - cannot install client eval bridge",
|
|
1478
|
+
}
|
|
1479
|
+
end
|
|
1480
|
+
local ok, err = pcall(function()
|
|
1481
|
+
destroyIfPresent(playerScripts, CLIENT_SCRIPT_NAME)
|
|
1482
|
+
destroyIfPresent(ReplicatedStorage, BRIDGE_NAMES.clientLocal)
|
|
1382
1483
|
local clientScript = Instance.new("LocalScript")
|
|
1383
1484
|
clientScript.Name = CLIENT_SCRIPT_NAME
|
|
1485
|
+
clientScript.Archivable = false
|
|
1384
1486
|
setSource(clientScript, CLIENT_BRIDGE_SOURCE)
|
|
1385
1487
|
clientScript:SetAttribute(STAMP_ATTR, BRIDGE_STAMP)
|
|
1386
|
-
clientScript.Parent =
|
|
1488
|
+
clientScript.Parent = playerScripts
|
|
1387
1489
|
end)
|
|
1388
1490
|
if not ok then
|
|
1389
1491
|
return {
|
|
@@ -1395,10 +1497,21 @@ function installBridges()
|
|
|
1395
1497
|
installed = true,
|
|
1396
1498
|
}
|
|
1397
1499
|
end
|
|
1500
|
+
local function ensureRuntimeBridgeInstalled()
|
|
1501
|
+
if not RunService:IsRunning() then
|
|
1502
|
+
return {
|
|
1503
|
+
installed = false,
|
|
1504
|
+
error = "Eval bridges are installed only in running play DataModels",
|
|
1505
|
+
}
|
|
1506
|
+
end
|
|
1507
|
+
if RunService:IsServer() then
|
|
1508
|
+
return installServerRuntimeBridge()
|
|
1509
|
+
end
|
|
1510
|
+
return installClientRuntimeBridge()
|
|
1511
|
+
end
|
|
1398
1512
|
return {
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
installBridges = installBridges,
|
|
1513
|
+
cleanupLegacyEditBridges = cleanupLegacyEditBridges,
|
|
1514
|
+
ensureRuntimeBridgeInstalled = ensureRuntimeBridgeInstalled,
|
|
1402
1515
|
BRIDGE_NAMES = BRIDGE_NAMES,
|
|
1403
1516
|
}
|
|
1404
1517
|
]]></string>
|
|
@@ -2484,9 +2597,27 @@ local LogService = _services.LogService
|
|
|
2484
2597
|
local ReplicatedStorage = _services.ReplicatedStorage
|
|
2485
2598
|
local RunService = _services.RunService
|
|
2486
2599
|
local ServerScriptService = _services.ServerScriptService
|
|
2487
|
-
local
|
|
2600
|
+
local _EvalBridges = TS.import(script, script.Parent.Parent, "EvalBridges")
|
|
2601
|
+
local BRIDGE_NAMES = _EvalBridges.BRIDGE_NAMES
|
|
2602
|
+
local ensureRuntimeBridgeInstalled = _EvalBridges.ensureRuntimeBridgeInstalled
|
|
2488
2603
|
local LuauExec = TS.import(script, script.Parent.Parent, "LuauExec")
|
|
2489
2604
|
local PAYLOAD_INSTANCE_NAME = "__MCPEvalPayload"
|
|
2605
|
+
local function findBridge(config)
|
|
2606
|
+
local bridge = config.service:FindFirstChild(config.bridgeName)
|
|
2607
|
+
return if bridge and bridge:IsA("BindableFunction") then bridge else nil
|
|
2608
|
+
end
|
|
2609
|
+
local function waitForBridge(config, timeoutSec)
|
|
2610
|
+
if timeoutSec == nil then
|
|
2611
|
+
timeoutSec = 2
|
|
2612
|
+
end
|
|
2613
|
+
local deadline = tick() + timeoutSec
|
|
2614
|
+
local bridge = findBridge(config)
|
|
2615
|
+
while not bridge and tick() < deadline do
|
|
2616
|
+
task.wait(0.05)
|
|
2617
|
+
bridge = findBridge(config)
|
|
2618
|
+
end
|
|
2619
|
+
return bridge
|
|
2620
|
+
end
|
|
2490
2621
|
local function getBridgeConfig()
|
|
2491
2622
|
if not RunService:IsRunning() then
|
|
2492
2623
|
return {
|
|
@@ -2497,13 +2628,13 @@ local function getBridgeConfig()
|
|
|
2497
2628
|
return {
|
|
2498
2629
|
service = ServerScriptService,
|
|
2499
2630
|
bridgeName = BRIDGE_NAMES.serverLocal,
|
|
2500
|
-
missingError = "ServerEvalBridge not found. The bridge runs inside the play DM, so a playtest must be running. The bridge installs automatically
|
|
2631
|
+
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.",
|
|
2501
2632
|
}
|
|
2502
2633
|
end
|
|
2503
2634
|
return {
|
|
2504
2635
|
service = ReplicatedStorage,
|
|
2505
2636
|
bridgeName = BRIDGE_NAMES.clientLocal,
|
|
2506
|
-
missingError = "ClientEvalBridge not found. The bridge runs inside the play DM, so a playtest must be running. The bridge installs automatically
|
|
2637
|
+
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.",
|
|
2507
2638
|
}
|
|
2508
2639
|
end
|
|
2509
2640
|
local function evalRuntime(requestData)
|
|
@@ -2520,11 +2651,21 @@ local function evalRuntime(requestData)
|
|
|
2520
2651
|
error = config.error,
|
|
2521
2652
|
}
|
|
2522
2653
|
end
|
|
2523
|
-
local bridge =
|
|
2524
|
-
if not bridge
|
|
2654
|
+
local bridge = findBridge(config)
|
|
2655
|
+
if not bridge then
|
|
2656
|
+
local install = ensureRuntimeBridgeInstalled()
|
|
2657
|
+
if not install.installed then
|
|
2658
|
+
return {
|
|
2659
|
+
bridge = "missing",
|
|
2660
|
+
error = `{config.missingError} Runtime bridge install failed: {install.error}`,
|
|
2661
|
+
}
|
|
2662
|
+
end
|
|
2663
|
+
bridge = waitForBridge(config)
|
|
2664
|
+
end
|
|
2665
|
+
if not bridge then
|
|
2525
2666
|
return {
|
|
2526
2667
|
bridge = "missing",
|
|
2527
|
-
error = config.missingError
|
|
2668
|
+
error = `{config.missingError} Runtime bridge was installed but did not become ready.`,
|
|
2528
2669
|
}
|
|
2529
2670
|
end
|
|
2530
2671
|
local m = Instance.new("ModuleScript")
|
|
@@ -6375,9 +6516,6 @@ local HttpService = _services.HttpService
|
|
|
6375
6516
|
local LogService = _services.LogService
|
|
6376
6517
|
local Players = _services.Players
|
|
6377
6518
|
local RunService = _services.RunService
|
|
6378
|
-
local _EvalBridges = TS.import(script, script.Parent.Parent, "EvalBridges")
|
|
6379
|
-
local installBridges = _EvalBridges.installBridges
|
|
6380
|
-
local ensureBridgesInstalled = _EvalBridges.ensureBridgesInstalled
|
|
6381
6519
|
local StopPlayMonitor = TS.import(script, script.Parent.Parent, "StopPlayMonitor")
|
|
6382
6520
|
local StudioTestService = game:GetService("StudioTestService")
|
|
6383
6521
|
local ServerScriptService = game:GetService("ServerScriptService")
|
|
@@ -6564,9 +6702,8 @@ local function startPlaytest(requestData)
|
|
|
6564
6702
|
logConnection = nil
|
|
6565
6703
|
end
|
|
6566
6704
|
cleanupStopListener()
|
|
6567
|
-
--
|
|
6568
|
-
--
|
|
6569
|
-
-- EvalBridges.ts lifecycle comment.
|
|
6705
|
+
-- Runtime eval bridges are created by the play server/client plugin
|
|
6706
|
+
-- peers and disappear with the play DataModels.
|
|
6570
6707
|
end
|
|
6571
6708
|
if testRunning then
|
|
6572
6709
|
return {
|
|
@@ -6607,15 +6744,7 @@ local function startPlaytest(requestData)
|
|
|
6607
6744
|
return injectStopListener()
|
|
6608
6745
|
end)
|
|
6609
6746
|
if not injected then
|
|
6610
|
-
warn(`[
|
|
6611
|
-
end
|
|
6612
|
-
-- Force-refresh the game-VM eval bridges (ServerEvalBridge + ClientEvalBridge)
|
|
6613
|
-
-- right before cloning so the play DMs get the current source. They also
|
|
6614
|
-
-- live permanently in the edit DM (installed on connect) so manually-started
|
|
6615
|
-
-- playtests get them too; here we just ensure they're fresh.
|
|
6616
|
-
local bridgeInstall = installBridges()
|
|
6617
|
-
if not bridgeInstall.installed then
|
|
6618
|
-
warn(`[MCP] Eval bridge install failed: {bridgeInstall.error}`)
|
|
6747
|
+
warn(`[robloxstudio-mcp] Failed to inject stop listener: {injErr}`)
|
|
6619
6748
|
end
|
|
6620
6749
|
task.spawn(function()
|
|
6621
6750
|
local ok, result = pcall(function()
|
|
@@ -6635,35 +6764,25 @@ local function startPlaytest(requestData)
|
|
|
6635
6764
|
end
|
|
6636
6765
|
testRunning = false
|
|
6637
6766
|
cleanupStopListener()
|
|
6638
|
-
-- Eval bridges persist in the edit DM (see EvalBridges.ts) — do not
|
|
6639
|
-
-- clean up here, so the next manual playtest still gets them.
|
|
6640
|
-
ensureBridgesInstalled()
|
|
6641
6767
|
end)
|
|
6642
6768
|
local response = {
|
|
6643
6769
|
success = true,
|
|
6644
6770
|
message = `Playtest started in {mode} mode.`,
|
|
6645
6771
|
}
|
|
6646
|
-
-- Only mention eval bridges when they failed — when they're fine, the
|
|
6647
|
-
-- detail is noise. eval_server_runtime / eval_client_runtime will surface
|
|
6648
|
-
-- their own clear errors if the caller tries to use them after a failed
|
|
6649
|
-
-- install.
|
|
6650
|
-
if not bridgeInstall.installed then
|
|
6651
|
-
response.evalBridgesError = bridgeInstall.error
|
|
6652
|
-
end
|
|
6653
6772
|
return response
|
|
6654
6773
|
end
|
|
6655
6774
|
local function stopPlaytest(_requestData)
|
|
6656
|
-
-- Signal the play-server DM's StopPlayMonitor via plugin:SetSetting
|
|
6657
|
-
--
|
|
6658
|
-
--
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
if not StopPlayMonitor.requestStop() then
|
|
6775
|
+
-- Signal the play-server DM's StopPlayMonitor via plugin:SetSetting.
|
|
6776
|
+
-- The monitor acknowledges with the matching request id only after its
|
|
6777
|
+
-- StudioTestService:EndTest call returns from pcall.
|
|
6778
|
+
local stopRequest = StopPlayMonitor.requestStop()
|
|
6779
|
+
if not stopRequest.ok or stopRequest.requestId == nil then
|
|
6662
6780
|
return {
|
|
6663
6781
|
error = "Plugin not ready. Try again in a moment.",
|
|
6664
6782
|
}
|
|
6665
6783
|
end
|
|
6666
|
-
|
|
6784
|
+
local consumption = StopPlayMonitor.waitForConsumption(stopRequest.requestId)
|
|
6785
|
+
if not consumption.ok then
|
|
6667
6786
|
-- Two distinct failure modes collapse here, distinguished by whether
|
|
6668
6787
|
-- THIS edit DM has a playtest tracked:
|
|
6669
6788
|
--
|
|
@@ -6675,19 +6794,28 @@ local function stopPlaytest(_requestData)
|
|
|
6675
6794
|
-- from the caller's perspective — playtest may actually have ended).
|
|
6676
6795
|
-- Tell the caller it's a timing issue and they can retry.
|
|
6677
6796
|
--
|
|
6678
|
-
-- Either way clean up the pending
|
|
6797
|
+
-- Either way clean up the pending request so a future playtest's monitor
|
|
6679
6798
|
-- doesn't fire EndTest on startup against a stale signal.
|
|
6680
|
-
StopPlayMonitor.clearPending()
|
|
6799
|
+
StopPlayMonitor.clearPending(stopRequest.requestId)
|
|
6681
6800
|
if testRunning then
|
|
6682
6801
|
return {
|
|
6683
|
-
error = "Playtest stop signal
|
|
6802
|
+
error = "Playtest stop signal failed or was not acknowledged. " .. "The playtest may have ended anyway; check get_connected_instances.",
|
|
6803
|
+
detail = consumption.error,
|
|
6804
|
+
}
|
|
6805
|
+
end
|
|
6806
|
+
if consumption.consumed then
|
|
6807
|
+
return {
|
|
6808
|
+
error = "Playtest stop request reached the play server, but EndTest failed.",
|
|
6809
|
+
detail = consumption.error,
|
|
6684
6810
|
}
|
|
6685
6811
|
end
|
|
6686
6812
|
return {
|
|
6687
6813
|
error = "No active playtest to stop.",
|
|
6814
|
+
detail = consumption.error,
|
|
6688
6815
|
}
|
|
6689
6816
|
end
|
|
6690
|
-
|
|
6817
|
+
StopPlayMonitor.clearPending(stopRequest.requestId)
|
|
6818
|
+
-- Request was consumed (EndTest called). ExecutePlayModeAsync in our
|
|
6691
6819
|
-- startPlaytest task.spawn is still unwinding though — testRunning stays
|
|
6692
6820
|
-- true until that yield completes and the post-block runs. Wait so
|
|
6693
6821
|
-- back-to-back stop -> start sequences don't race against the prior
|
|
@@ -6744,10 +6872,6 @@ local function multiplayerTestStart(requestData)
|
|
|
6744
6872
|
end
|
|
6745
6873
|
local testArgs = if requestData.testArgs ~= nil then requestData.testArgs else {}
|
|
6746
6874
|
local testId = HttpService:GenerateGUID(false)
|
|
6747
|
-
local bridgeInstall = installBridges()
|
|
6748
|
-
if not bridgeInstall.installed then
|
|
6749
|
-
warn(`[MCP] Eval bridge install failed: {bridgeInstall.error}`)
|
|
6750
|
-
end
|
|
6751
6875
|
multiplayerState = {
|
|
6752
6876
|
phase = "starting",
|
|
6753
6877
|
testId = testId,
|
|
@@ -6771,7 +6895,6 @@ local function multiplayerTestStart(requestData)
|
|
|
6771
6895
|
multiplayerState.result = nil
|
|
6772
6896
|
multiplayerState.error = tostring(result)
|
|
6773
6897
|
end
|
|
6774
|
-
ensureBridgesInstalled()
|
|
6775
6898
|
end)
|
|
6776
6899
|
local response = {
|
|
6777
6900
|
success = true,
|
|
@@ -6781,9 +6904,6 @@ local function multiplayerTestStart(requestData)
|
|
|
6781
6904
|
numPlayers = numPlayers,
|
|
6782
6905
|
testArgs = testArgs,
|
|
6783
6906
|
}
|
|
6784
|
-
if not bridgeInstall.installed then
|
|
6785
|
-
response.evalBridgesError = bridgeInstall.error
|
|
6786
|
-
end
|
|
6787
6907
|
return response
|
|
6788
6908
|
end
|
|
6789
6909
|
local function multiplayerTestState(_requestData)
|
|
@@ -6999,6 +7119,87 @@ return {
|
|
|
6999
7119
|
</Item>
|
|
7000
7120
|
</Item>
|
|
7001
7121
|
<Item class="ModuleScript" referent="21">
|
|
7122
|
+
<Properties>
|
|
7123
|
+
<string name="Name">HttpDiagnostics</string>
|
|
7124
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
7125
|
+
local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
7126
|
+
local HttpService = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services").HttpService
|
|
7127
|
+
local function encodeForLog(value)
|
|
7128
|
+
local ok, encoded = pcall(function()
|
|
7129
|
+
return HttpService:JSONEncode(value)
|
|
7130
|
+
end)
|
|
7131
|
+
return if ok then encoded else tostring(value)
|
|
7132
|
+
end
|
|
7133
|
+
local function formatBody(body)
|
|
7134
|
+
if body == "" then
|
|
7135
|
+
return ""
|
|
7136
|
+
end
|
|
7137
|
+
local ok, decoded = pcall(function()
|
|
7138
|
+
return HttpService:JSONDecode(body)
|
|
7139
|
+
end)
|
|
7140
|
+
if ok and type(decoded) == "table" then
|
|
7141
|
+
local data = decoded
|
|
7142
|
+
local parts = {}
|
|
7143
|
+
local _error = data.error
|
|
7144
|
+
local _condition = type(_error) == "string"
|
|
7145
|
+
if _condition then
|
|
7146
|
+
_condition = data.error ~= ""
|
|
7147
|
+
end
|
|
7148
|
+
if _condition then
|
|
7149
|
+
local _arg0 = `error={data.error}`
|
|
7150
|
+
table.insert(parts, _arg0)
|
|
7151
|
+
end
|
|
7152
|
+
local _message = data.message
|
|
7153
|
+
local _condition_1 = type(_message) == "string"
|
|
7154
|
+
if _condition_1 then
|
|
7155
|
+
_condition_1 = data.message ~= ""
|
|
7156
|
+
end
|
|
7157
|
+
if _condition_1 then
|
|
7158
|
+
local _arg0 = `message={data.message}`
|
|
7159
|
+
table.insert(parts, _arg0)
|
|
7160
|
+
end
|
|
7161
|
+
if data.missingFields ~= nil then
|
|
7162
|
+
local _arg0 = `missingFields={encodeForLog(data.missingFields)}`
|
|
7163
|
+
table.insert(parts, _arg0)
|
|
7164
|
+
end
|
|
7165
|
+
if data.request ~= nil then
|
|
7166
|
+
local _arg0 = `request={encodeForLog(data.request)}`
|
|
7167
|
+
table.insert(parts, _arg0)
|
|
7168
|
+
end
|
|
7169
|
+
if data.existing ~= nil then
|
|
7170
|
+
local _arg0 = `existing={encodeForLog(data.existing)}`
|
|
7171
|
+
table.insert(parts, _arg0)
|
|
7172
|
+
end
|
|
7173
|
+
if data.details ~= nil then
|
|
7174
|
+
local _arg0 = `details={encodeForLog(data.details)}`
|
|
7175
|
+
table.insert(parts, _arg0)
|
|
7176
|
+
end
|
|
7177
|
+
if #parts > 0 then
|
|
7178
|
+
return table.concat(parts, " ")
|
|
7179
|
+
end
|
|
7180
|
+
end
|
|
7181
|
+
return `body={body}`
|
|
7182
|
+
end
|
|
7183
|
+
local function formatRequestFailure(url, ok, res)
|
|
7184
|
+
if not ok then
|
|
7185
|
+
return `RequestAsync threw for {url}: {tostring(res)}`
|
|
7186
|
+
end
|
|
7187
|
+
if res == nil then
|
|
7188
|
+
return `RequestAsync returned no response for {url}`
|
|
7189
|
+
end
|
|
7190
|
+
local response = res
|
|
7191
|
+
local statusMessage = if response.StatusMessage ~= "" then ` {response.StatusMessage}` else ""
|
|
7192
|
+
local body = formatBody(response.Body)
|
|
7193
|
+
local suffix = if body ~= "" then `: {body}` else ""
|
|
7194
|
+
return `HTTP {response.StatusCode}{statusMessage} from {url}{suffix}`
|
|
7195
|
+
end
|
|
7196
|
+
return {
|
|
7197
|
+
formatRequestFailure = formatRequestFailure,
|
|
7198
|
+
}
|
|
7199
|
+
]]></string>
|
|
7200
|
+
</Properties>
|
|
7201
|
+
</Item>
|
|
7202
|
+
<Item class="ModuleScript" referent="22">
|
|
7002
7203
|
<Properties>
|
|
7003
7204
|
<string name="Name">LuauExec</string>
|
|
7004
7205
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -7432,7 +7633,7 @@ return {
|
|
|
7432
7633
|
]]></string>
|
|
7433
7634
|
</Properties>
|
|
7434
7635
|
</Item>
|
|
7435
|
-
<Item class="ModuleScript" referent="
|
|
7636
|
+
<Item class="ModuleScript" referent="23">
|
|
7436
7637
|
<Properties>
|
|
7437
7638
|
<string name="Name">Recording</string>
|
|
7438
7639
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -7462,7 +7663,7 @@ return {
|
|
|
7462
7663
|
]]></string>
|
|
7463
7664
|
</Properties>
|
|
7464
7665
|
</Item>
|
|
7465
|
-
<Item class="ModuleScript" referent="
|
|
7666
|
+
<Item class="ModuleScript" referent="24">
|
|
7466
7667
|
<Properties>
|
|
7467
7668
|
<string name="Name">RenderMonitor</string>
|
|
7468
7669
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -7530,7 +7731,7 @@ return {
|
|
|
7530
7731
|
]]></string>
|
|
7531
7732
|
</Properties>
|
|
7532
7733
|
</Item>
|
|
7533
|
-
<Item class="ModuleScript" referent="
|
|
7734
|
+
<Item class="ModuleScript" referent="25">
|
|
7534
7735
|
<Properties>
|
|
7535
7736
|
<string name="Name">RuntimeLogBuffer</string>
|
|
7536
7737
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -7711,11 +7912,71 @@ return {
|
|
|
7711
7912
|
]]></string>
|
|
7712
7913
|
</Properties>
|
|
7713
7914
|
</Item>
|
|
7714
|
-
<Item class="ModuleScript" referent="
|
|
7915
|
+
<Item class="ModuleScript" referent="26">
|
|
7916
|
+
<Properties>
|
|
7917
|
+
<string name="Name">ServerUrlSettings</string>
|
|
7918
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
7919
|
+
local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
7920
|
+
local _services = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services")
|
|
7921
|
+
local HttpService = _services.HttpService
|
|
7922
|
+
local ServerStorage = _services.ServerStorage
|
|
7923
|
+
local SETTING_KEY_PREFIX = "MCP_SERVER_URL_"
|
|
7924
|
+
local pluginRef
|
|
7925
|
+
local function init(p)
|
|
7926
|
+
pluginRef = p
|
|
7927
|
+
end
|
|
7928
|
+
local function computeInstanceId()
|
|
7929
|
+
if game.PlaceId ~= 0 then
|
|
7930
|
+
return `place:{tostring(game.PlaceId)}`
|
|
7931
|
+
end
|
|
7932
|
+
local existing = ServerStorage:GetAttribute("__MCPPlaceId")
|
|
7933
|
+
if type(existing) == "string" and existing ~= "" then
|
|
7934
|
+
return `anon:{existing}`
|
|
7935
|
+
end
|
|
7936
|
+
local fresh = HttpService:GenerateGUID(false)
|
|
7937
|
+
pcall(function()
|
|
7938
|
+
return ServerStorage:SetAttribute("__MCPPlaceId", fresh)
|
|
7939
|
+
end)
|
|
7940
|
+
return `anon:{fresh}`
|
|
7941
|
+
end
|
|
7942
|
+
local function settingKey(instanceId)
|
|
7943
|
+
return SETTING_KEY_PREFIX .. instanceId
|
|
7944
|
+
end
|
|
7945
|
+
local function rememberServerUrl(serverUrl)
|
|
7946
|
+
if not pluginRef or serverUrl == "" then
|
|
7947
|
+
return nil
|
|
7948
|
+
end
|
|
7949
|
+
local key = settingKey(computeInstanceId())
|
|
7950
|
+
pcall(function()
|
|
7951
|
+
return pluginRef:SetSetting(key, serverUrl)
|
|
7952
|
+
end)
|
|
7953
|
+
end
|
|
7954
|
+
local function readServerUrl()
|
|
7955
|
+
if not pluginRef then
|
|
7956
|
+
return nil
|
|
7957
|
+
end
|
|
7958
|
+
local key = settingKey(computeInstanceId())
|
|
7959
|
+
local ok, value = pcall(function()
|
|
7960
|
+
return pluginRef:GetSetting(key)
|
|
7961
|
+
end)
|
|
7962
|
+
if ok and type(value) == "string" and value ~= "" then
|
|
7963
|
+
return value
|
|
7964
|
+
end
|
|
7965
|
+
return nil
|
|
7966
|
+
end
|
|
7967
|
+
return {
|
|
7968
|
+
init = init,
|
|
7969
|
+
rememberServerUrl = rememberServerUrl,
|
|
7970
|
+
readServerUrl = readServerUrl,
|
|
7971
|
+
}
|
|
7972
|
+
]]></string>
|
|
7973
|
+
</Properties>
|
|
7974
|
+
</Item>
|
|
7975
|
+
<Item class="ModuleScript" referent="27">
|
|
7715
7976
|
<Properties>
|
|
7716
7977
|
<string name="Name">State</string>
|
|
7717
7978
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
7718
|
-
local CURRENT_VERSION = "2.
|
|
7979
|
+
local CURRENT_VERSION = "2.16.1"
|
|
7719
7980
|
local PLUGIN_VARIANT = "main"
|
|
7720
7981
|
local MAX_CONNECTIONS = 5
|
|
7721
7982
|
local BASE_PORT = 58741
|
|
@@ -7809,7 +8070,7 @@ return {
|
|
|
7809
8070
|
]]></string>
|
|
7810
8071
|
</Properties>
|
|
7811
8072
|
</Item>
|
|
7812
|
-
<Item class="ModuleScript" referent="
|
|
8073
|
+
<Item class="ModuleScript" referent="28">
|
|
7813
8074
|
<Properties>
|
|
7814
8075
|
<string name="Name">StopPlayMonitor</string>
|
|
7815
8076
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -7821,16 +8082,16 @@ local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
|
7821
8082
|
-- `plugin:SetSetting` / `plugin:GetSetting` is a per-plugin persistent store
|
|
7822
8083
|
-- shared across every DataModel the plugin runs in (edit DMs, play-server
|
|
7823
8084
|
-- DMs, play-client DMs). For each connected place we use a dedicated key
|
|
7824
|
-
-- "MCP_STOP_PLAY_<instanceId>" as a
|
|
8085
|
+
-- "MCP_STOP_PLAY_<instanceId>" as a tiny request/result mailbox:
|
|
7825
8086
|
--
|
|
7826
|
-
-- * The edit DM's
|
|
8087
|
+
-- * The edit DM's handler writes a tokenized stop request into its own key
|
|
7827
8088
|
-- (computed from its placeId / ServerStorage anon UUID).
|
|
7828
8089
|
-- * Each play-server DM's monitor loop polls the key matching its own
|
|
7829
|
-
-- instanceId at
|
|
7830
|
-
--
|
|
7831
|
-
-- touch this key.
|
|
7832
|
-
-- * The edit DM waits up to ~8s for its
|
|
7833
|
-
--
|
|
8090
|
+
-- instanceId at 1Hz. On a fresh token, it calls StudioTestService:EndTest
|
|
8091
|
+
-- and writes a matching result token. Play-server DMs for other places
|
|
8092
|
+
-- never touch this key.
|
|
8093
|
+
-- * The edit DM waits up to ~8s for its result token, confirming a matching
|
|
8094
|
+
-- play-server actually consumed the request.
|
|
7834
8095
|
--
|
|
7835
8096
|
-- Earlier versions used a single shared boolean flag, which let any
|
|
7836
8097
|
-- play-server DM in the same Studio process consume any place's stop
|
|
@@ -7838,20 +8099,24 @@ local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
|
7838
8099
|
-- below is the fix.
|
|
7839
8100
|
local _services = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services")
|
|
7840
8101
|
local HttpService = _services.HttpService
|
|
8102
|
+
local RunService = _services.RunService
|
|
7841
8103
|
local ServerStorage = _services.ServerStorage
|
|
7842
8104
|
local StudioTestService = game:GetService("StudioTestService")
|
|
7843
8105
|
local SETTING_KEY_PREFIX = "MCP_STOP_PLAY_"
|
|
7844
|
-
--
|
|
7845
|
-
--
|
|
7846
|
-
--
|
|
7847
|
-
local POLL_INTERVAL_SEC =
|
|
8106
|
+
-- Keep this conservative. plugin:GetSetting is backed by Studio's plugin
|
|
8107
|
+
-- settings store, and this monitor runs during every play session, including
|
|
8108
|
+
-- manually-started Play. The official reference implementation polls at 1s.
|
|
8109
|
+
local POLL_INTERVAL_SEC = 1
|
|
7848
8110
|
-- Total time we wait for the matching play-server DM to consume the
|
|
7849
8111
|
-- signal. Must cover: monitor detection (<= POLL_INTERVAL_SEC) +
|
|
7850
8112
|
-- StudioTestService:EndTest teardown (several seconds on heavier places).
|
|
7851
|
-
-- 8s is
|
|
8113
|
+
-- 8s is intentionally shorter than the MCP request timeout but long enough
|
|
8114
|
+
-- for the 1s monitor cadence plus ordinary Studio teardown latency.
|
|
7852
8115
|
local WAIT_FOR_CONSUMPTION_TIMEOUT_SEC = 8.0
|
|
7853
8116
|
local WAIT_POLL_SEC = 0.1
|
|
8117
|
+
local REQUEST_TTL_SEC = 12.0
|
|
7854
8118
|
local pluginRef
|
|
8119
|
+
local endTestIssued = false
|
|
7855
8120
|
local function init(p)
|
|
7856
8121
|
pluginRef = p
|
|
7857
8122
|
end
|
|
@@ -7877,32 +8142,125 @@ end
|
|
|
7877
8142
|
local function settingKey(instanceId)
|
|
7878
8143
|
return SETTING_KEY_PREFIX .. instanceId
|
|
7879
8144
|
end
|
|
8145
|
+
local function readSetting(key)
|
|
8146
|
+
if not pluginRef then
|
|
8147
|
+
return nil
|
|
8148
|
+
end
|
|
8149
|
+
local ok, value = pcall(function()
|
|
8150
|
+
return pluginRef:GetSetting(key)
|
|
8151
|
+
end)
|
|
8152
|
+
return if ok then value else nil
|
|
8153
|
+
end
|
|
8154
|
+
local function writeSetting(key, value)
|
|
8155
|
+
if not pluginRef then
|
|
8156
|
+
return false
|
|
8157
|
+
end
|
|
8158
|
+
local ok = pcall(function()
|
|
8159
|
+
return pluginRef:SetSetting(key, value)
|
|
8160
|
+
end)
|
|
8161
|
+
return ok
|
|
8162
|
+
end
|
|
8163
|
+
local function decodePayload(value)
|
|
8164
|
+
local decoded = value
|
|
8165
|
+
local _value = value
|
|
8166
|
+
if type(_value) == "string" then
|
|
8167
|
+
local ok, result = pcall(function()
|
|
8168
|
+
return HttpService:JSONDecode(value)
|
|
8169
|
+
end)
|
|
8170
|
+
if not ok then
|
|
8171
|
+
return nil
|
|
8172
|
+
end
|
|
8173
|
+
decoded = result
|
|
8174
|
+
end
|
|
8175
|
+
local _decoded = decoded
|
|
8176
|
+
if not (type(_decoded) == "table") then
|
|
8177
|
+
return nil
|
|
8178
|
+
end
|
|
8179
|
+
local payload = decoded
|
|
8180
|
+
local _kind = payload.kind
|
|
8181
|
+
local _condition = not (type(_kind) == "string")
|
|
8182
|
+
if not _condition then
|
|
8183
|
+
local _id = payload.id
|
|
8184
|
+
_condition = not (type(_id) == "string")
|
|
8185
|
+
end
|
|
8186
|
+
if _condition then
|
|
8187
|
+
return nil
|
|
8188
|
+
end
|
|
8189
|
+
return payload
|
|
8190
|
+
end
|
|
8191
|
+
local function writePayload(key, payload)
|
|
8192
|
+
local encodedOk, encoded = pcall(function()
|
|
8193
|
+
return HttpService:JSONEncode(payload)
|
|
8194
|
+
end)
|
|
8195
|
+
if not encodedOk or not (type(encoded) == "string") then
|
|
8196
|
+
return false
|
|
8197
|
+
end
|
|
8198
|
+
return writeSetting(key, encoded)
|
|
8199
|
+
end
|
|
8200
|
+
local function writeResult(key, request, ok, errText)
|
|
8201
|
+
writePayload(key, {
|
|
8202
|
+
kind = "result",
|
|
8203
|
+
id = request.id,
|
|
8204
|
+
requestedAt = request.requestedAt,
|
|
8205
|
+
consumedAt = tick(),
|
|
8206
|
+
ok = ok,
|
|
8207
|
+
error = errText,
|
|
8208
|
+
})
|
|
8209
|
+
end
|
|
8210
|
+
local function handleStopRequest(key, request)
|
|
8211
|
+
local _condition = request.kind ~= "request"
|
|
8212
|
+
if not _condition then
|
|
8213
|
+
local _id = request.id
|
|
8214
|
+
_condition = not (type(_id) == "string")
|
|
8215
|
+
end
|
|
8216
|
+
if _condition then
|
|
8217
|
+
return nil
|
|
8218
|
+
end
|
|
8219
|
+
local _requestedAt = request.requestedAt
|
|
8220
|
+
if not (type(_requestedAt) == "number") then
|
|
8221
|
+
writeSetting(key, false)
|
|
8222
|
+
return nil
|
|
8223
|
+
end
|
|
8224
|
+
local age = tick() - request.requestedAt
|
|
8225
|
+
if age < -5 or age > REQUEST_TTL_SEC then
|
|
8226
|
+
writeSetting(key, false)
|
|
8227
|
+
return nil
|
|
8228
|
+
end
|
|
8229
|
+
if endTestIssued then
|
|
8230
|
+
writeResult(key, request, true)
|
|
8231
|
+
return nil
|
|
8232
|
+
end
|
|
8233
|
+
if not RunService:IsRunning() or not RunService:IsServer() then
|
|
8234
|
+
writeResult(key, request, false, "StopPlayMonitor is not running in the server DataModel.")
|
|
8235
|
+
return nil
|
|
8236
|
+
end
|
|
8237
|
+
endTestIssued = true
|
|
8238
|
+
local endOk, endErr = pcall(function()
|
|
8239
|
+
return StudioTestService:EndTest("stopped_by_mcp")
|
|
8240
|
+
end)
|
|
8241
|
+
writeResult(key, request, endOk, if endOk then nil else tostring(endErr))
|
|
8242
|
+
if not endOk then
|
|
8243
|
+
endTestIssued = false
|
|
8244
|
+
end
|
|
8245
|
+
end
|
|
7880
8246
|
local function startMonitor()
|
|
7881
8247
|
if not pluginRef then
|
|
7882
|
-
warn("[
|
|
8248
|
+
warn("[robloxstudio-mcp] StopPlayMonitor.startMonitor called before init; skipping")
|
|
7883
8249
|
return nil
|
|
7884
8250
|
end
|
|
7885
8251
|
local myKey = settingKey(computeInstanceId())
|
|
7886
|
-
-- Clear any stale value left from a prior session. If a real stop
|
|
7887
|
-
-- request is in-flight when this runs, the requesting edit DM will
|
|
7888
|
-
-- write again within its consumption-confirmation window.
|
|
7889
|
-
pcall(function()
|
|
7890
|
-
return pluginRef:SetSetting(myKey, false)
|
|
7891
|
-
end)
|
|
7892
8252
|
task.spawn(function()
|
|
7893
8253
|
while true do
|
|
7894
|
-
local
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
return StudioTestService:EndTest("stopped_by_mcp")
|
|
7905
|
-
end)
|
|
8254
|
+
local value = readSetting(myKey)
|
|
8255
|
+
if value == true then
|
|
8256
|
+
-- Legacy boolean requests are ambiguous and may be stale from
|
|
8257
|
+
-- a prior crashed session. New stop requests use token payloads.
|
|
8258
|
+
writeSetting(myKey, false)
|
|
8259
|
+
else
|
|
8260
|
+
local payload = decodePayload(value)
|
|
8261
|
+
if payload then
|
|
8262
|
+
handleStopRequest(myKey, payload)
|
|
8263
|
+
end
|
|
7906
8264
|
end
|
|
7907
8265
|
task.wait(POLL_INTERVAL_SEC)
|
|
7908
8266
|
end
|
|
@@ -7910,39 +8268,61 @@ local function startMonitor()
|
|
|
7910
8268
|
end
|
|
7911
8269
|
local function requestStop()
|
|
7912
8270
|
if not pluginRef then
|
|
7913
|
-
return
|
|
8271
|
+
return {
|
|
8272
|
+
ok = false,
|
|
8273
|
+
}
|
|
7914
8274
|
end
|
|
7915
8275
|
local myKey = settingKey(computeInstanceId())
|
|
7916
|
-
local
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
8276
|
+
local requestId = HttpService:GenerateGUID(false)
|
|
8277
|
+
local ok = writePayload(myKey, {
|
|
8278
|
+
kind = "request",
|
|
8279
|
+
id = requestId,
|
|
8280
|
+
requestedAt = tick(),
|
|
8281
|
+
})
|
|
8282
|
+
return {
|
|
8283
|
+
ok = ok,
|
|
8284
|
+
requestId = if ok then requestId else nil,
|
|
8285
|
+
}
|
|
7920
8286
|
end
|
|
7921
|
-
local function waitForConsumption()
|
|
8287
|
+
local function waitForConsumption(requestId)
|
|
7922
8288
|
if not pluginRef then
|
|
7923
|
-
return
|
|
8289
|
+
return {
|
|
8290
|
+
ok = false,
|
|
8291
|
+
consumed = false,
|
|
8292
|
+
error = "Plugin reference is not initialized.",
|
|
8293
|
+
}
|
|
7924
8294
|
end
|
|
7925
8295
|
local myKey = settingKey(computeInstanceId())
|
|
7926
8296
|
local start = tick()
|
|
7927
8297
|
while tick() - start < WAIT_FOR_CONSUMPTION_TIMEOUT_SEC do
|
|
7928
|
-
local
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
|
|
8298
|
+
local payload = decodePayload(readSetting(myKey))
|
|
8299
|
+
if payload and payload.kind == "result" and payload.id == requestId then
|
|
8300
|
+
return {
|
|
8301
|
+
ok = payload.ok == true,
|
|
8302
|
+
consumed = true,
|
|
8303
|
+
error = payload.error,
|
|
8304
|
+
}
|
|
7933
8305
|
end
|
|
7934
8306
|
task.wait(WAIT_POLL_SEC)
|
|
7935
8307
|
end
|
|
7936
|
-
return
|
|
8308
|
+
return {
|
|
8309
|
+
ok = false,
|
|
8310
|
+
consumed = false,
|
|
8311
|
+
error = "Timed out waiting for the play-server DataModel to acknowledge stop_playtest.",
|
|
8312
|
+
}
|
|
7937
8313
|
end
|
|
7938
|
-
local function clearPending()
|
|
8314
|
+
local function clearPending(requestId)
|
|
7939
8315
|
if not pluginRef then
|
|
7940
8316
|
return nil
|
|
7941
8317
|
end
|
|
7942
8318
|
local myKey = settingKey(computeInstanceId())
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
8319
|
+
if requestId ~= nil then
|
|
8320
|
+
local payload = decodePayload(readSetting(myKey))
|
|
8321
|
+
if payload and payload.id ~= requestId then
|
|
8322
|
+
return nil
|
|
8323
|
+
end
|
|
8324
|
+
end
|
|
8325
|
+
writeSetting(myKey, false)
|
|
7946
8326
|
end
|
|
7947
8327
|
return {
|
|
7948
8328
|
init = init,
|
|
@@ -7954,7 +8334,7 @@ return {
|
|
|
7954
8334
|
]]></string>
|
|
7955
8335
|
</Properties>
|
|
7956
8336
|
</Item>
|
|
7957
|
-
<Item class="ModuleScript" referent="
|
|
8337
|
+
<Item class="ModuleScript" referent="29">
|
|
7958
8338
|
<Properties>
|
|
7959
8339
|
<string name="Name">UI</string>
|
|
7960
8340
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -8725,7 +9105,7 @@ return {
|
|
|
8725
9105
|
]]></string>
|
|
8726
9106
|
</Properties>
|
|
8727
9107
|
</Item>
|
|
8728
|
-
<Item class="ModuleScript" referent="
|
|
9108
|
+
<Item class="ModuleScript" referent="30">
|
|
8729
9109
|
<Properties>
|
|
8730
9110
|
<string name="Name">Utils</string>
|
|
8731
9111
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -9255,11 +9635,11 @@ return {
|
|
|
9255
9635
|
</Properties>
|
|
9256
9636
|
</Item>
|
|
9257
9637
|
</Item>
|
|
9258
|
-
<Item class="Folder" referent="
|
|
9638
|
+
<Item class="Folder" referent="34">
|
|
9259
9639
|
<Properties>
|
|
9260
9640
|
<string name="Name">include</string>
|
|
9261
9641
|
</Properties>
|
|
9262
|
-
<Item class="ModuleScript" referent="
|
|
9642
|
+
<Item class="ModuleScript" referent="31">
|
|
9263
9643
|
<Properties>
|
|
9264
9644
|
<string name="Name">Promise</string>
|
|
9265
9645
|
<string name="Source"><![CDATA[--[[
|
|
@@ -11333,7 +11713,7 @@ return Promise
|
|
|
11333
11713
|
]]></string>
|
|
11334
11714
|
</Properties>
|
|
11335
11715
|
</Item>
|
|
11336
|
-
<Item class="ModuleScript" referent="
|
|
11716
|
+
<Item class="ModuleScript" referent="32">
|
|
11337
11717
|
<Properties>
|
|
11338
11718
|
<string name="Name">RuntimeLib</string>
|
|
11339
11719
|
<string name="Source"><![CDATA[local Promise = require(script.Parent.Promise)
|
|
@@ -11600,15 +11980,15 @@ return TS
|
|
|
11600
11980
|
</Properties>
|
|
11601
11981
|
</Item>
|
|
11602
11982
|
</Item>
|
|
11603
|
-
<Item class="Folder" referent="
|
|
11983
|
+
<Item class="Folder" referent="35">
|
|
11604
11984
|
<Properties>
|
|
11605
11985
|
<string name="Name">node_modules</string>
|
|
11606
11986
|
</Properties>
|
|
11607
|
-
<Item class="Folder" referent="
|
|
11987
|
+
<Item class="Folder" referent="36">
|
|
11608
11988
|
<Properties>
|
|
11609
11989
|
<string name="Name">@rbxts</string>
|
|
11610
11990
|
</Properties>
|
|
11611
|
-
<Item class="ModuleScript" referent="
|
|
11991
|
+
<Item class="ModuleScript" referent="33">
|
|
11612
11992
|
<Properties>
|
|
11613
11993
|
<string name="Name">services</string>
|
|
11614
11994
|
<string name="Source"><![CDATA[return setmetatable({}, {
|