@chrrxs/robloxstudio-mcp 2.9.1 → 2.10.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 +151 -48
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +526 -190
- package/studio-plugin/MCPPlugin.rbxmx +526 -190
- package/studio-plugin/src/modules/ClientBroker.ts +125 -48
- package/studio-plugin/src/modules/Communication.ts +60 -27
- package/studio-plugin/src/modules/EvalBridges.ts +16 -53
- package/studio-plugin/src/modules/RuntimeLogBuffer.ts +138 -0
- package/studio-plugin/src/modules/UI.ts +36 -0
- package/studio-plugin/src/modules/handlers/LogHandlers.ts +15 -0
- package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +12 -1
- package/studio-plugin/src/modules/handlers/QueryHandlers.ts +16 -1
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +1 -13
- package/studio-plugin/src/server/index.server.ts +11 -1
- package/studio-plugin/src/types/index.d.ts +4 -0
|
@@ -10,10 +10,23 @@ 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 RuntimeLogBuffer = TS.import(script, script, "modules", "RuntimeLogBuffer")
|
|
14
|
+
-- Attach the per-peer LogService.MessageOut listener as early as possible so
|
|
15
|
+
-- boot-time prints from the user's place scripts are captured. Powers the
|
|
16
|
+
-- get_runtime_logs MCP tool. Idempotent; safe to call before UI.init().
|
|
17
|
+
RuntimeLogBuffer.install()
|
|
13
18
|
UI.init(plugin)
|
|
14
19
|
local elements = UI.getElements()
|
|
20
|
+
local ICON_DISCONNECTED = "rbxassetid://75876056391496"
|
|
21
|
+
local ICON_CONNECTING = "rbxassetid://71302583919560"
|
|
22
|
+
local ICON_CONNECTED = "rbxassetid://130958234173611"
|
|
15
23
|
local toolbar = plugin:CreateToolbar("MCP Integration")
|
|
16
|
-
local button = toolbar:CreateButton("MCP Server", "Connect to MCP Server for AI Integration",
|
|
24
|
+
local button = toolbar:CreateButton("MCP Server", "Connect to MCP Server for AI Integration", ICON_DISCONNECTED)
|
|
25
|
+
UI.setToolbarButton(button, {
|
|
26
|
+
disconnected = ICON_DISCONNECTED,
|
|
27
|
+
connecting = ICON_CONNECTING,
|
|
28
|
+
connected = ICON_CONNECTED,
|
|
29
|
+
})
|
|
17
30
|
elements.connectButton.Activated:Connect(function()
|
|
18
31
|
local conn = State.getActiveConnection()
|
|
19
32
|
if conn and conn.isActive then
|
|
@@ -72,6 +85,7 @@ local HttpService = _services.HttpService
|
|
|
72
85
|
local Players = _services.Players
|
|
73
86
|
local ReplicatedStorage = _services.ReplicatedStorage
|
|
74
87
|
local RunService = _services.RunService
|
|
88
|
+
local RuntimeLogBuffer = TS.import(script, script.Parent, "RuntimeLogBuffer")
|
|
75
89
|
-- The client peer cannot reach the MCP HTTP server - Roblox forbids
|
|
76
90
|
-- HttpService:RequestAsync from the client DM even under PluginSecurity, and
|
|
77
91
|
-- HttpEnabled reads as false there regardless of identity. So the server peer
|
|
@@ -87,6 +101,37 @@ local RunService = _services.RunService
|
|
|
87
101
|
-- edit DM untouched.
|
|
88
102
|
local MCP_URL = "http://localhost:58741"
|
|
89
103
|
local BROKER_NAME = "__MCPClientBroker"
|
|
104
|
+
-- Endpoints the server-peer broker is allowed to forward to the client peer.
|
|
105
|
+
-- Each requires the client peer's plugin VM (because the buffer / require
|
|
106
|
+
-- cache / etc. lives there) so the server peer alone can't satisfy them.
|
|
107
|
+
local CLIENT_BROKER_ALLOWED_ENDPOINTS = {
|
|
108
|
+
["/api/execute-luau"] = true,
|
|
109
|
+
["/api/get-runtime-logs"] = true,
|
|
110
|
+
}
|
|
111
|
+
-- Throttle re-ready calls per proxyId so a brief window of unknownInstance
|
|
112
|
+
-- polls doesn't cause a re-register stampede.
|
|
113
|
+
local lastReadyByProxy = {}
|
|
114
|
+
local postJson
|
|
115
|
+
local function reRegisterProxy(proxyId, role)
|
|
116
|
+
local now = tick()
|
|
117
|
+
local _proxyId = proxyId
|
|
118
|
+
local _condition = lastReadyByProxy[_proxyId]
|
|
119
|
+
if _condition == nil then
|
|
120
|
+
_condition = 0
|
|
121
|
+
end
|
|
122
|
+
local last = _condition
|
|
123
|
+
if now - last < 2 then
|
|
124
|
+
return nil
|
|
125
|
+
end
|
|
126
|
+
local _proxyId_1 = proxyId
|
|
127
|
+
lastReadyByProxy[_proxyId_1] = now
|
|
128
|
+
pcall(function()
|
|
129
|
+
return postJson("/ready", {
|
|
130
|
+
instanceId = proxyId,
|
|
131
|
+
role = role,
|
|
132
|
+
})
|
|
133
|
+
end)
|
|
134
|
+
end
|
|
90
135
|
local function forkRole()
|
|
91
136
|
if not RunService:IsRunning() then
|
|
92
137
|
return "edit"
|
|
@@ -96,7 +141,7 @@ local function forkRole()
|
|
|
96
141
|
end
|
|
97
142
|
return "client"
|
|
98
143
|
end
|
|
99
|
-
|
|
144
|
+
function postJson(endpoint, body)
|
|
100
145
|
return pcall(function()
|
|
101
146
|
return HttpService:RequestAsync({
|
|
102
147
|
Url = `{MCP_URL}{endpoint}`,
|
|
@@ -108,6 +153,56 @@ local function postJson(endpoint, body)
|
|
|
108
153
|
})
|
|
109
154
|
end)
|
|
110
155
|
end
|
|
156
|
+
local function handleExecuteLuau(data)
|
|
157
|
+
local code = data and (data.code)
|
|
158
|
+
if type(code) == "string" == false or code == "" then
|
|
159
|
+
return {
|
|
160
|
+
success = false,
|
|
161
|
+
error = "code is required",
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
local m = Instance.new("ModuleScript")
|
|
165
|
+
m.Name = "__MCPClientEval"
|
|
166
|
+
local okSet, setErr = pcall(function()
|
|
167
|
+
m.Source = code
|
|
168
|
+
end)
|
|
169
|
+
if not okSet then
|
|
170
|
+
m:Destroy()
|
|
171
|
+
return {
|
|
172
|
+
success = false,
|
|
173
|
+
error = `Source set failed: {tostring(setErr)}`,
|
|
174
|
+
}
|
|
175
|
+
end
|
|
176
|
+
m.Parent = game.Workspace
|
|
177
|
+
local okReq, result = pcall(function()
|
|
178
|
+
return require(m)
|
|
179
|
+
end)
|
|
180
|
+
m:Destroy()
|
|
181
|
+
if okReq then
|
|
182
|
+
return {
|
|
183
|
+
success = true,
|
|
184
|
+
returnValue = if result ~= nil then tostring(result) else nil,
|
|
185
|
+
message = "Code executed successfully",
|
|
186
|
+
}
|
|
187
|
+
end
|
|
188
|
+
return {
|
|
189
|
+
success = false,
|
|
190
|
+
error = tostring(result),
|
|
191
|
+
}
|
|
192
|
+
end
|
|
193
|
+
local function handleGetRuntimeLogs(data)
|
|
194
|
+
local d = data or {}
|
|
195
|
+
local since = d.since
|
|
196
|
+
local tail = d.tail
|
|
197
|
+
local filter = d.filter
|
|
198
|
+
-- "client" is the generic peer tag; MCP-side aggregator overrides with
|
|
199
|
+
-- the specific role (e.g. "client-1") on target=all fan-out.
|
|
200
|
+
return RuntimeLogBuffer.query({
|
|
201
|
+
since = since,
|
|
202
|
+
tail = tail,
|
|
203
|
+
filter = filter,
|
|
204
|
+
}, "client")
|
|
205
|
+
end
|
|
111
206
|
local function setupClientBroker()
|
|
112
207
|
local rf = ReplicatedStorage:WaitForChild(BROKER_NAME, 10)
|
|
113
208
|
if not rf or not rf:IsA("RemoteFunction") then
|
|
@@ -115,44 +210,20 @@ local function setupClientBroker()
|
|
|
115
210
|
return nil
|
|
116
211
|
end
|
|
117
212
|
rf.OnClientInvoke = function(payload)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
m.Name = "__MCPClientEval"
|
|
127
|
-
local okSet, setErr = pcall(function()
|
|
128
|
-
m.Source = code
|
|
129
|
-
end)
|
|
130
|
-
if not okSet then
|
|
131
|
-
m:Destroy()
|
|
132
|
-
local _arg0 = {
|
|
133
|
-
success = false,
|
|
134
|
-
error = `Source set failed: {tostring(setErr)}`,
|
|
135
|
-
}
|
|
136
|
-
return _arg0
|
|
213
|
+
-- Two payload shapes in the wild:
|
|
214
|
+
-- - {endpoint, data} from v2.10+ server-peer broker (this is the new
|
|
215
|
+
-- discriminated form that lets us dispatch on endpoint)
|
|
216
|
+
-- - {code} from pre-v2.10 server-peer broker (raw execute-luau payload)
|
|
217
|
+
-- The shapes coexist gracefully because we fall back to execute-luau
|
|
218
|
+
-- when endpoint is missing.
|
|
219
|
+
if payload and payload.endpoint == "/api/get-runtime-logs" then
|
|
220
|
+
return handleGetRuntimeLogs(payload.data)
|
|
137
221
|
end
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return require(m)
|
|
141
|
-
end)
|
|
142
|
-
m:Destroy()
|
|
143
|
-
if okReq then
|
|
144
|
-
local _arg0 = {
|
|
145
|
-
success = true,
|
|
146
|
-
returnValue = if result ~= nil then tostring(result) else nil,
|
|
147
|
-
message = "Code executed successfully",
|
|
148
|
-
}
|
|
149
|
-
return _arg0
|
|
222
|
+
if payload and payload.endpoint == "/api/execute-luau" then
|
|
223
|
+
return handleExecuteLuau(payload.data)
|
|
150
224
|
end
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
error = tostring(result),
|
|
154
|
-
}
|
|
155
|
-
return _arg0
|
|
225
|
+
-- Legacy: raw execute-luau payload at the top level.
|
|
226
|
+
return handleExecuteLuau(payload)
|
|
156
227
|
end
|
|
157
228
|
end
|
|
158
229
|
local proxyByPlayer = {}
|
|
@@ -179,33 +250,47 @@ local function pollProxy(proxyId, player, rf)
|
|
|
179
250
|
local okJson, body = pcall(function()
|
|
180
251
|
return HttpService:JSONDecode(res.Body)
|
|
181
252
|
end)
|
|
182
|
-
if okJson and body
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
253
|
+
if okJson and body then
|
|
254
|
+
-- Server lost our proxy registration (process restart, etc.) -
|
|
255
|
+
-- re-register so the next poll cycle starts routing again.
|
|
256
|
+
if body.knownInstance == false then
|
|
257
|
+
reRegisterProxy(proxyId, "client")
|
|
258
|
+
end
|
|
259
|
+
if body.request and body.requestId ~= nil then
|
|
260
|
+
local request = body.request
|
|
261
|
+
local response
|
|
262
|
+
local _endpoint = request.endpoint
|
|
263
|
+
if CLIENT_BROKER_ALLOWED_ENDPOINTS[_endpoint] ~= nil then
|
|
264
|
+
-- Forward as a discriminated envelope so the client-side
|
|
265
|
+
-- OnClientInvoke knows which endpoint it's serving.
|
|
266
|
+
local envelope = {
|
|
267
|
+
endpoint = request.endpoint,
|
|
268
|
+
data = request.data,
|
|
193
269
|
}
|
|
270
|
+
local okInvoke, invokeRes = pcall(function()
|
|
271
|
+
return rf:InvokeClient(player, envelope)
|
|
272
|
+
end)
|
|
273
|
+
if okInvoke then
|
|
274
|
+
response = if invokeRes ~= nil then invokeRes else {
|
|
275
|
+
success = false,
|
|
276
|
+
error = "nil response",
|
|
277
|
+
}
|
|
278
|
+
else
|
|
279
|
+
response = {
|
|
280
|
+
success = false,
|
|
281
|
+
error = `InvokeClient failed: {tostring(invokeRes)}`,
|
|
282
|
+
}
|
|
283
|
+
end
|
|
194
284
|
else
|
|
195
285
|
response = {
|
|
196
|
-
|
|
197
|
-
error = `InvokeClient failed: {tostring(invokeRes)}`,
|
|
286
|
+
error = `Client-proxy does not forward {tostring(request.endpoint)}. ` .. `Allowed: /api/execute-luau, /api/get-runtime-logs.`,
|
|
198
287
|
}
|
|
199
288
|
end
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
289
|
+
postJson("/response", {
|
|
290
|
+
requestId = body.requestId,
|
|
291
|
+
response = response,
|
|
292
|
+
})
|
|
204
293
|
end
|
|
205
|
-
postJson("/response", {
|
|
206
|
-
requestId = body.requestId,
|
|
207
|
-
response = response,
|
|
208
|
-
})
|
|
209
294
|
end
|
|
210
295
|
end
|
|
211
296
|
task.wait(0.5)
|
|
@@ -264,22 +349,28 @@ local function startEditProxyLoop()
|
|
|
264
349
|
local okJson, body = pcall(function()
|
|
265
350
|
return HttpService:JSONDecode(pollRes.Body)
|
|
266
351
|
end)
|
|
267
|
-
if okJson and body
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
352
|
+
if okJson and body then
|
|
353
|
+
-- Re-register if the server lost our edit-proxy registration.
|
|
354
|
+
if body.knownInstance == false then
|
|
355
|
+
reRegisterProxy(proxyId, "edit-proxy")
|
|
356
|
+
end
|
|
357
|
+
if body.request and body.request.endpoint == "/api/stop-playtest" and body.requestId ~= nil then
|
|
358
|
+
local sts = game:GetService("StudioTestService")
|
|
359
|
+
local endOk, endErr = pcall(function()
|
|
360
|
+
return sts:EndTest("stopped_by_mcp")
|
|
361
|
+
end)
|
|
362
|
+
local response = if endOk then {
|
|
363
|
+
success = true,
|
|
364
|
+
message = "Playtest stopped via edit-proxy/EndTest",
|
|
365
|
+
} else {
|
|
366
|
+
success = false,
|
|
367
|
+
error = `EndTest failed: {tostring(endErr)}`,
|
|
368
|
+
}
|
|
369
|
+
postJson("/response", {
|
|
370
|
+
requestId = body.requestId,
|
|
371
|
+
response = response,
|
|
372
|
+
})
|
|
373
|
+
end
|
|
283
374
|
end
|
|
284
375
|
end
|
|
285
376
|
task.wait(0.15)
|
|
@@ -351,6 +442,7 @@ local BuildHandlers = TS.import(script, script.Parent, "handlers", "BuildHandler
|
|
|
351
442
|
local AssetHandlers = TS.import(script, script.Parent, "handlers", "AssetHandlers")
|
|
352
443
|
local CaptureHandlers = TS.import(script, script.Parent, "handlers", "CaptureHandlers")
|
|
353
444
|
local InputHandlers = TS.import(script, script.Parent, "handlers", "InputHandlers")
|
|
445
|
+
local LogHandlers = TS.import(script, script.Parent, "handlers", "LogHandlers")
|
|
354
446
|
local instanceId = HttpService:GenerateGUID(false)
|
|
355
447
|
local assignedRole
|
|
356
448
|
local function detectRole()
|
|
@@ -419,6 +511,7 @@ local routeMap = {
|
|
|
419
511
|
["/api/simulate-mouse-input"] = InputHandlers.simulateMouseInput,
|
|
420
512
|
["/api/simulate-keyboard-input"] = InputHandlers.simulateKeyboardInput,
|
|
421
513
|
["/api/find-and-replace-in-scripts"] = ScriptHandlers.findAndReplaceInScripts,
|
|
514
|
+
["/api/get-runtime-logs"] = LogHandlers.getRuntimeLogs,
|
|
422
515
|
}
|
|
423
516
|
local function processRequest(request)
|
|
424
517
|
local endpoint = request.endpoint
|
|
@@ -460,6 +553,43 @@ local function getConnectionStatus(connIndex)
|
|
|
460
553
|
end
|
|
461
554
|
return "connecting"
|
|
462
555
|
end
|
|
556
|
+
-- Throttle for re-issuing /ready after the server reports knownInstance=false.
|
|
557
|
+
-- Without this, every poll during the brief window where the server has just
|
|
558
|
+
-- restarted but hasn't seen our re-ready yet would fire a duplicate /ready.
|
|
559
|
+
local lastReadyPostAt = 0
|
|
560
|
+
local function sendReady(conn)
|
|
561
|
+
local now = tick()
|
|
562
|
+
if now - lastReadyPostAt < 2 then
|
|
563
|
+
return nil
|
|
564
|
+
end
|
|
565
|
+
lastReadyPostAt = now
|
|
566
|
+
task.spawn(function()
|
|
567
|
+
local readyOk, readyResult = pcall(function()
|
|
568
|
+
return HttpService:RequestAsync({
|
|
569
|
+
Url = `{conn.serverUrl}/ready`,
|
|
570
|
+
Method = "POST",
|
|
571
|
+
Headers = {
|
|
572
|
+
["Content-Type"] = "application/json",
|
|
573
|
+
},
|
|
574
|
+
Body = HttpService:JSONEncode({
|
|
575
|
+
instanceId = instanceId,
|
|
576
|
+
role = detectRole(),
|
|
577
|
+
pluginReady = true,
|
|
578
|
+
timestamp = tick(),
|
|
579
|
+
}),
|
|
580
|
+
})
|
|
581
|
+
end)
|
|
582
|
+
if readyOk and readyResult.Success then
|
|
583
|
+
local parseOk, readyData = pcall(function()
|
|
584
|
+
return HttpService:JSONDecode(readyResult.Body)
|
|
585
|
+
end)
|
|
586
|
+
local _value = parseOk and readyData.assignedRole
|
|
587
|
+
if _value ~= "" and _value then
|
|
588
|
+
assignedRole = readyData.assignedRole
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
end)
|
|
592
|
+
end
|
|
463
593
|
local function pollForRequests(connIndex)
|
|
464
594
|
local conn = State.getConnection(connIndex)
|
|
465
595
|
if not conn or not conn.isActive then
|
|
@@ -481,6 +611,9 @@ local function pollForRequests(connIndex)
|
|
|
481
611
|
conn.isPolling = false
|
|
482
612
|
local ui = UI.getElements()
|
|
483
613
|
UI.updateTabDot(connIndex)
|
|
614
|
+
if connIndex == State.getActiveTabIndex() then
|
|
615
|
+
UI.updateToolbarIcon()
|
|
616
|
+
end
|
|
484
617
|
if success and (result.Success or result.StatusCode == 503) then
|
|
485
618
|
conn.consecutiveFailures = 0
|
|
486
619
|
conn.currentRetryDelay = 0.5
|
|
@@ -489,6 +622,14 @@ local function pollForRequests(connIndex)
|
|
|
489
622
|
local mcpConnected = data.mcpConnected == true
|
|
490
623
|
conn.lastHttpOk = true
|
|
491
624
|
conn.lastMcpOk = mcpConnected
|
|
625
|
+
-- Server tells us when its in-memory instances map doesn't have us
|
|
626
|
+
-- (e.g. after an MCP process restart). Re-issue /ready immediately so
|
|
627
|
+
-- target=server/client-N start routing again. The throttle inside
|
|
628
|
+
-- sendReady() prevents duplicate registrations while the server
|
|
629
|
+
-- catches up.
|
|
630
|
+
if data.knownInstance == false then
|
|
631
|
+
sendReady(conn)
|
|
632
|
+
end
|
|
492
633
|
if connIndex == State.getActiveTabIndex() then
|
|
493
634
|
local el = ui
|
|
494
635
|
el.step1Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
|
|
@@ -629,7 +770,6 @@ local function activatePlugin(connIndex)
|
|
|
629
770
|
conn.isActive = true
|
|
630
771
|
conn.consecutiveFailures = 0
|
|
631
772
|
conn.currentRetryDelay = 0.5
|
|
632
|
-
ui.screenGui.Enabled = true
|
|
633
773
|
if idx == State.getActiveTabIndex() then
|
|
634
774
|
conn.serverUrl = ui.urlInput.Text
|
|
635
775
|
local portStr = string.match(conn.serverUrl, ":(%d+)$")
|
|
@@ -644,42 +784,19 @@ local function activatePlugin(connIndex)
|
|
|
644
784
|
UI.updateUIState()
|
|
645
785
|
end
|
|
646
786
|
UI.updateTabDot(idx)
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
pollForRequests(idx)
|
|
655
|
-
end
|
|
656
|
-
end)
|
|
657
|
-
end
|
|
658
|
-
local readyOk, readyResult = pcall(function()
|
|
659
|
-
return HttpService:RequestAsync({
|
|
660
|
-
Url = `{conn.serverUrl}/ready`,
|
|
661
|
-
Method = "POST",
|
|
662
|
-
Headers = {
|
|
663
|
-
["Content-Type"] = "application/json",
|
|
664
|
-
},
|
|
665
|
-
Body = HttpService:JSONEncode({
|
|
666
|
-
instanceId = instanceId,
|
|
667
|
-
role = detectRole(),
|
|
668
|
-
pluginReady = true,
|
|
669
|
-
timestamp = tick(),
|
|
670
|
-
}),
|
|
671
|
-
})
|
|
672
|
-
end)
|
|
673
|
-
if readyOk and readyResult.Success then
|
|
674
|
-
local parseOk, readyData = pcall(function()
|
|
675
|
-
return HttpService:JSONDecode(readyResult.Body)
|
|
676
|
-
end)
|
|
677
|
-
local _value = parseOk and readyData.assignedRole
|
|
678
|
-
if _value ~= "" and _value then
|
|
679
|
-
assignedRole = readyData.assignedRole
|
|
787
|
+
if not conn.heartbeatConnection then
|
|
788
|
+
conn.heartbeatConnection = RunService.Heartbeat:Connect(function()
|
|
789
|
+
local now = tick()
|
|
790
|
+
local currentInterval = if conn.consecutiveFailures > 5 then conn.currentRetryDelay else conn.pollInterval
|
|
791
|
+
if now - conn.lastPoll > currentInterval then
|
|
792
|
+
conn.lastPoll = now
|
|
793
|
+
pollForRequests(idx)
|
|
680
794
|
end
|
|
681
|
-
end
|
|
682
|
-
end
|
|
795
|
+
end)
|
|
796
|
+
end
|
|
797
|
+
-- Initial /ready; pollForRequests will also re-fire ready if the server
|
|
798
|
+
-- later reports knownInstance=false (process restart, etc).
|
|
799
|
+
sendReady(conn)
|
|
683
800
|
end
|
|
684
801
|
local function deactivatePlugin(connIndex)
|
|
685
802
|
local _condition = connIndex
|
|
@@ -782,15 +899,22 @@ local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
|
782
899
|
-- `require(SomeModule)` returns a fresh copy, not the one the running game
|
|
783
900
|
-- scripts hold. So runtime-mutated module state is invisible to probes.
|
|
784
901
|
--
|
|
785
|
-
-- These bridges fix that by living inside the user's game scripts
|
|
786
|
-
--
|
|
787
|
-
--
|
|
788
|
-
-- (
|
|
902
|
+
-- These bridges fix that by living inside the user's game scripts. Both
|
|
903
|
+
-- peers use the same symmetric shape:
|
|
904
|
+
-- - Server: a Script in ServerScriptService that creates a BindableFunction.
|
|
905
|
+
-- Plugin (server peer) invokes it with a fresh ModuleScript payload;
|
|
906
|
+
-- require() runs inside the Script VM so it shares the running server's
|
|
907
|
+
-- require cache.
|
|
789
908
|
-- - Client: a LocalScript in StarterPlayer.StarterPlayerScripts that
|
|
790
909
|
-- creates a BindableFunction. Plugin invokes it with a fresh ModuleScript
|
|
791
910
|
-- payload; require() runs inside the LocalScript VM so it shares the
|
|
792
911
|
-- game's require cache.
|
|
793
912
|
--
|
|
913
|
+
-- Why ModuleScript+require on both sides (no loadstring): require'd modules
|
|
914
|
+
-- run with the security level they were created at and don't need
|
|
915
|
+
-- ServerScriptService.LoadStringEnabled, so eval_server_runtime works even
|
|
916
|
+
-- when LoadStringEnabled=false (the default in fresh places).
|
|
917
|
+
--
|
|
794
918
|
-- Lifecycle: TestHandlers.startPlaytest inserts both scripts into the EDIT
|
|
795
919
|
-- DM right before ExecutePlayModeAsync. ExecutePlayModeAsync clones the
|
|
796
920
|
-- DataModel into the play DMs, so the scripts come along and run there.
|
|
@@ -821,7 +945,6 @@ local CLIENT_SCRIPT_NAME = "__MCP_ClientEvalBridge"
|
|
|
821
945
|
local BRIDGE_NAMES = {
|
|
822
946
|
serverScript = SERVER_SCRIPT_NAME,
|
|
823
947
|
clientScript = CLIENT_SCRIPT_NAME,
|
|
824
|
-
serverRemote = "__MCP_ServerEvalRemote",
|
|
825
948
|
serverLocal = "__MCP_ServerEvalLocal",
|
|
826
949
|
clientLocal = "__MCP_ClientEvalBridge",
|
|
827
950
|
}
|
|
@@ -832,7 +955,6 @@ local SERVER_BRIDGE_SOURCE = `\
|
|
|
832
955
|
-- stop_playtest. Provides shared-require-cache eval on the server peer for\
|
|
833
956
|
-- the eval_server_runtime MCP tool.\
|
|
834
957
|
\
|
|
835
|
-
local ReplicatedStorage = game:GetService("ReplicatedStorage")\
|
|
836
958
|
local ServerScriptService = game:GetService("ServerScriptService")\
|
|
837
959
|
local RunService = game:GetService("RunService")\
|
|
838
960
|
\
|
|
@@ -840,49 +962,18 @@ if not RunService:IsStudio() then\
|
|
|
840
962
|
return\
|
|
841
963
|
end\
|
|
842
964
|
\
|
|
843
|
-
local function evalCode(source)\
|
|
844
|
-
if type(source) ~= "string" then\
|
|
845
|
-
return false, "source must be a string"\
|
|
846
|
-
end\
|
|
847
|
-
local fn, compileErr = loadstring(source, "MCPServerEval")\
|
|
848
|
-
if not fn then\
|
|
849
|
-
local errStr = tostring(compileErr or "loadstring returned nil")\
|
|
850
|
-
-- Roblox returns nil from loadstring when LoadStringEnabled=false.\
|
|
851
|
-
-- Surface a clear, actionable error.\
|
|
852
|
-
if string.find(errStr, "not enabled", 1, true)\
|
|
853
|
-
or string.find(errStr, "disabled", 1, true)\
|
|
854
|
-
or errStr == "loadstring returned nil"\
|
|
855
|
-
then\
|
|
856
|
-
return false,\
|
|
857
|
-
"ServerScriptService.LoadStringEnabled is false. eval_server_runtime requires it. "\
|
|
858
|
-
.. "Enable it in Studio (ServerScriptService > Properties > LoadStringEnabled = true) "\
|
|
859
|
-
.. "and restart the playtest."\
|
|
860
|
-
end\
|
|
861
|
-
return false, errStr\
|
|
862
|
-
end\
|
|
863
|
-
return pcall(fn)\
|
|
864
|
-
end\
|
|
865
|
-
\
|
|
866
|
-
-- Defensive cleanup of stale instances from a prior session.\
|
|
867
|
-
local prevRf = ReplicatedStorage:FindFirstChild("{BRIDGE_NAMES.serverRemote}")\
|
|
868
|
-
if prevRf then prevRf:Destroy() end\
|
|
869
965
|
local prevBf = ServerScriptService:FindFirstChild("{BRIDGE_NAMES.serverLocal}")\
|
|
870
966
|
if prevBf then prevBf:Destroy() end\
|
|
871
967
|
\
|
|
872
|
-
local rf = Instance.new("RemoteFunction")\
|
|
873
|
-
rf.Name = "{BRIDGE_NAMES.serverRemote}"\
|
|
874
|
-
rf.Archivable = false\
|
|
875
|
-
rf.Parent = ReplicatedStorage\
|
|
876
|
-
rf.OnServerInvoke = function(_player, source)\
|
|
877
|
-
return evalCode(source)\
|
|
878
|
-
end\
|
|
879
|
-
\
|
|
880
968
|
local bf = Instance.new("BindableFunction")\
|
|
881
969
|
bf.Name = "{BRIDGE_NAMES.serverLocal}"\
|
|
882
970
|
bf.Archivable = false\
|
|
883
971
|
bf.Parent = ServerScriptService\
|
|
884
|
-
bf.OnInvoke = function(
|
|
885
|
-
|
|
972
|
+
bf.OnInvoke = function(payload)\
|
|
973
|
+
if typeof(payload) ~= "Instance" or not payload:IsA("ModuleScript") then\
|
|
974
|
+
return false, "payload must be a ModuleScript instance"\
|
|
975
|
+
end\
|
|
976
|
+
return pcall(require, payload)\
|
|
886
977
|
end\
|
|
887
978
|
`
|
|
888
979
|
local CLIENT_BRIDGE_SOURCE = `\
|
|
@@ -979,20 +1070,9 @@ local function installBridges()
|
|
|
979
1070
|
installed = true,
|
|
980
1071
|
}
|
|
981
1072
|
end
|
|
982
|
-
-- Heuristic check so start_playtest can surface a warning when
|
|
983
|
-
-- LoadStringEnabled is false (eval_server_runtime won't work in that mode).
|
|
984
|
-
-- We can't import the runtime LoadStringEnabled value cleanly without
|
|
985
|
-
-- pulling in the type — read defensively.
|
|
986
|
-
local function loadStringEnabled()
|
|
987
|
-
local ok, value = pcall(function()
|
|
988
|
-
return ServerScriptService.LoadStringEnabled
|
|
989
|
-
end)
|
|
990
|
-
return ok and value == true
|
|
991
|
-
end
|
|
992
1073
|
return {
|
|
993
1074
|
cleanupBridges = cleanupBridges,
|
|
994
1075
|
installBridges = installBridges,
|
|
995
|
-
loadStringEnabled = loadStringEnabled,
|
|
996
1076
|
BRIDGE_NAMES = BRIDGE_NAMES,
|
|
997
1077
|
}
|
|
998
1078
|
]]></string>
|
|
@@ -2681,6 +2761,33 @@ return {
|
|
|
2681
2761
|
</Properties>
|
|
2682
2762
|
</Item>
|
|
2683
2763
|
<Item class="ModuleScript" referent="11">
|
|
2764
|
+
<Properties>
|
|
2765
|
+
<string name="Name">LogHandlers</string>
|
|
2766
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
2767
|
+
local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
|
|
2768
|
+
local RuntimeLogBuffer = TS.import(script, script.Parent.Parent, "RuntimeLogBuffer")
|
|
2769
|
+
local function getRuntimeLogs(requestData)
|
|
2770
|
+
local since = requestData.since
|
|
2771
|
+
local tail = requestData.tail
|
|
2772
|
+
local filter = requestData.filter
|
|
2773
|
+
-- Plugin-side peer tag is generic ("edit"|"server"|"client"). The MCP-side
|
|
2774
|
+
-- aggregator overrides it with the specific instance role (e.g. "client-1")
|
|
2775
|
+
-- during fan-out for target=all, so this value is only authoritative for
|
|
2776
|
+
-- the single-peer query path.
|
|
2777
|
+
local peer = RuntimeLogBuffer.detectPeer()
|
|
2778
|
+
return RuntimeLogBuffer.query({
|
|
2779
|
+
since = since,
|
|
2780
|
+
tail = tail,
|
|
2781
|
+
filter = filter,
|
|
2782
|
+
}, peer)
|
|
2783
|
+
end
|
|
2784
|
+
return {
|
|
2785
|
+
getRuntimeLogs = getRuntimeLogs,
|
|
2786
|
+
}
|
|
2787
|
+
]]></string>
|
|
2788
|
+
</Properties>
|
|
2789
|
+
</Item>
|
|
2790
|
+
<Item class="ModuleScript" referent="12">
|
|
2684
2791
|
<Properties>
|
|
2685
2792
|
<string name="Name">MetadataHandlers</string>
|
|
2686
2793
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -3152,8 +3259,19 @@ local function executeLuau(requestData)
|
|
|
3152
3259
|
local runViaModuleScript = function()
|
|
3153
3260
|
local m = Instance.new("ModuleScript")
|
|
3154
3261
|
m.Name = "__MCPExecLuauPayload"
|
|
3262
|
+
-- Wrap user code in an IIFE so require() always gets exactly one
|
|
3263
|
+
-- return value. Without this, code like `print("x")` errors with
|
|
3264
|
+
-- "Module code did not return exactly one value" because top-level
|
|
3265
|
+
-- ModuleScripts must return exactly one value.
|
|
3266
|
+
--
|
|
3267
|
+
-- The DOUBLE parens around the call are load-bearing: in Luau,
|
|
3268
|
+
-- `return f()` propagates whatever multi-value tuple f returns,
|
|
3269
|
+
-- including zero values. Outer parens adjust the call to exactly
|
|
3270
|
+
-- one value (the first, or nil). So `return ((f)())` always
|
|
3271
|
+
-- returns exactly one value, regardless of what f does.
|
|
3272
|
+
local wrapped = `return ((function()\n{code}\nend)())`
|
|
3155
3273
|
local okSet, setErr = pcall(function()
|
|
3156
|
-
m.Source =
|
|
3274
|
+
m.Source = wrapped
|
|
3157
3275
|
end)
|
|
3158
3276
|
if not okSet then
|
|
3159
3277
|
m:Destroy()
|
|
@@ -3306,7 +3424,7 @@ return {
|
|
|
3306
3424
|
]]></string>
|
|
3307
3425
|
</Properties>
|
|
3308
3426
|
</Item>
|
|
3309
|
-
<Item class="ModuleScript" referent="
|
|
3427
|
+
<Item class="ModuleScript" referent="13">
|
|
3310
3428
|
<Properties>
|
|
3311
3429
|
<string name="Name">PropertyHandlers</string>
|
|
3312
3430
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -3558,7 +3676,7 @@ return {
|
|
|
3558
3676
|
]]></string>
|
|
3559
3677
|
</Properties>
|
|
3560
3678
|
</Item>
|
|
3561
|
-
<Item class="ModuleScript" referent="
|
|
3679
|
+
<Item class="ModuleScript" referent="14">
|
|
3562
3680
|
<Properties>
|
|
3563
3681
|
<string name="Name">QueryHandlers</string>
|
|
3564
3682
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -3666,8 +3784,23 @@ local function searchFiles(requestData)
|
|
|
3666
3784
|
}
|
|
3667
3785
|
end
|
|
3668
3786
|
local function getPlaceInfo(_requestData)
|
|
3787
|
+
local dataModelName = game.Name
|
|
3788
|
+
local placeName = dataModelName
|
|
3789
|
+
if game.PlaceId > 0 then
|
|
3790
|
+
local MarketplaceService = game:GetService("MarketplaceService")
|
|
3791
|
+
local ok, info = pcall(function()
|
|
3792
|
+
return MarketplaceService:GetProductInfo(game.PlaceId)
|
|
3793
|
+
end)
|
|
3794
|
+
if ok and info ~= nil then
|
|
3795
|
+
local name = info.Name
|
|
3796
|
+
if type(name) == "string" and name ~= "" then
|
|
3797
|
+
placeName = name
|
|
3798
|
+
end
|
|
3799
|
+
end
|
|
3800
|
+
end
|
|
3669
3801
|
return {
|
|
3670
|
-
placeName =
|
|
3802
|
+
placeName = placeName,
|
|
3803
|
+
dataModelName = dataModelName,
|
|
3671
3804
|
placeId = game.PlaceId,
|
|
3672
3805
|
gameId = game.GameId,
|
|
3673
3806
|
jobId = game.JobId,
|
|
@@ -4585,7 +4718,7 @@ return {
|
|
|
4585
4718
|
]]></string>
|
|
4586
4719
|
</Properties>
|
|
4587
4720
|
</Item>
|
|
4588
|
-
<Item class="ModuleScript" referent="
|
|
4721
|
+
<Item class="ModuleScript" referent="15">
|
|
4589
4722
|
<Properties>
|
|
4590
4723
|
<string name="Name">ScriptHandlers</string>
|
|
4591
4724
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5281,7 +5414,7 @@ return {
|
|
|
5281
5414
|
]]></string>
|
|
5282
5415
|
</Properties>
|
|
5283
5416
|
</Item>
|
|
5284
|
-
<Item class="ModuleScript" referent="
|
|
5417
|
+
<Item class="ModuleScript" referent="16">
|
|
5285
5418
|
<Properties>
|
|
5286
5419
|
<string name="Name">TestHandlers</string>
|
|
5287
5420
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5292,7 +5425,6 @@ local LogService = _services.LogService
|
|
|
5292
5425
|
local _EvalBridges = TS.import(script, script.Parent.Parent, "EvalBridges")
|
|
5293
5426
|
local installBridges = _EvalBridges.installBridges
|
|
5294
5427
|
local cleanupBridges = _EvalBridges.cleanupBridges
|
|
5295
|
-
local loadStringEnabled = _EvalBridges.loadStringEnabled
|
|
5296
5428
|
local StudioTestService = game:GetService("StudioTestService")
|
|
5297
5429
|
local ServerScriptService = game:GetService("ServerScriptService")
|
|
5298
5430
|
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
@@ -5453,7 +5585,6 @@ local function startPlaytest(requestData)
|
|
|
5453
5585
|
-- so eval_server_runtime / eval_client_runtime work without manual setup.
|
|
5454
5586
|
-- Bridges are cleaned up from the edit DM after the play DMs tear down.
|
|
5455
5587
|
local bridgeInstall = installBridges()
|
|
5456
|
-
local hasLoadString = loadStringEnabled()
|
|
5457
5588
|
if not bridgeInstall.installed then
|
|
5458
5589
|
warn(`[MCP] Eval bridge install failed: {bridgeInstall.error}`)
|
|
5459
5590
|
end
|
|
@@ -5487,13 +5618,6 @@ local function startPlaytest(requestData)
|
|
|
5487
5618
|
message = msg,
|
|
5488
5619
|
evalBridges = if bridgeInstall.installed then "installed" else `failed: {bridgeInstall.error}`,
|
|
5489
5620
|
}
|
|
5490
|
-
-- Surface loadstring availability up-front so callers know whether
|
|
5491
|
-
-- eval_server_runtime will work before they try it. eval_client_runtime
|
|
5492
|
-
-- doesn't need loadstring (it uses ModuleScript+require), so this only
|
|
5493
|
-
-- affects the server bridge.
|
|
5494
|
-
if not hasLoadString then
|
|
5495
|
-
response.serverEvalNote = "ServerScriptService.LoadStringEnabled is false. eval_server_runtime will not work " .. "until you enable it (ServerScriptService > Properties > LoadStringEnabled = true) " .. "and restart the playtest. eval_client_runtime is unaffected."
|
|
5496
|
-
end
|
|
5497
5621
|
return response
|
|
5498
5622
|
end
|
|
5499
5623
|
local function stopPlaytest(_requestData)
|
|
@@ -5600,7 +5724,7 @@ return {
|
|
|
5600
5724
|
</Properties>
|
|
5601
5725
|
</Item>
|
|
5602
5726
|
</Item>
|
|
5603
|
-
<Item class="ModuleScript" referent="
|
|
5727
|
+
<Item class="ModuleScript" referent="17">
|
|
5604
5728
|
<Properties>
|
|
5605
5729
|
<string name="Name">Recording</string>
|
|
5606
5730
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5630,11 +5754,192 @@ return {
|
|
|
5630
5754
|
]]></string>
|
|
5631
5755
|
</Properties>
|
|
5632
5756
|
</Item>
|
|
5633
|
-
<Item class="ModuleScript" referent="
|
|
5757
|
+
<Item class="ModuleScript" referent="18">
|
|
5758
|
+
<Properties>
|
|
5759
|
+
<string name="Name">RuntimeLogBuffer</string>
|
|
5760
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
5761
|
+
local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
5762
|
+
-- Per-peer in-memory ring buffer for LogService.MessageOut events.
|
|
5763
|
+
-- Powers the get_runtime_logs MCP tool. Replaces the out-of-tree LogBuffer
|
|
5764
|
+
-- primitives + StringValue approach from chrrxs/roblox-mcp-primitives.
|
|
5765
|
+
--
|
|
5766
|
+
-- Each peer's plugin attaches a MessageOut listener at plugin load (edit DM,
|
|
5767
|
+
-- play-server DM, play-client DM all run their own copy of this module).
|
|
5768
|
+
-- Captured entries live in plugin module-state; nothing is parented to the
|
|
5769
|
+
-- DataModel. The buffer is bounded by a message-byte budget; oldest entries
|
|
5770
|
+
-- drop when over budget.
|
|
5771
|
+
--
|
|
5772
|
+
-- Peer-tag caveat: returned entries reflect which peer's plugin CAPTURED the
|
|
5773
|
+
-- entry, NOT which peer's script originated the print. LogService reflects
|
|
5774
|
+
-- prints across peers in Studio Play (a server print ends up in both the
|
|
5775
|
+
-- server and client LogService:GetLogHistory()) and origin is empirically
|
|
5776
|
+
-- undetectable from inside MessageOut. The MCP-side aggregator handles
|
|
5777
|
+
-- cross-peer dedup via a 2s timestamp window.
|
|
5778
|
+
local _services = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services")
|
|
5779
|
+
local LogService = _services.LogService
|
|
5780
|
+
local RunService = _services.RunService
|
|
5781
|
+
local MAX_BYTES = 64 * 1024
|
|
5782
|
+
local HARD_ENTRY_CAP = 50_000
|
|
5783
|
+
local entries = {}
|
|
5784
|
+
local totalBytes = 0
|
|
5785
|
+
local totalDropped = 0
|
|
5786
|
+
local nextSeq = 1
|
|
5787
|
+
local installed = false
|
|
5788
|
+
local function levelTag(t)
|
|
5789
|
+
if t == Enum.MessageType.MessageWarning then
|
|
5790
|
+
return "WARN"
|
|
5791
|
+
end
|
|
5792
|
+
if t == Enum.MessageType.MessageError then
|
|
5793
|
+
return "ERR"
|
|
5794
|
+
end
|
|
5795
|
+
if t == Enum.MessageType.MessageInfo then
|
|
5796
|
+
return "INFO"
|
|
5797
|
+
end
|
|
5798
|
+
return "OUT"
|
|
5799
|
+
end
|
|
5800
|
+
local function nowSec()
|
|
5801
|
+
return DateTime.now().UnixTimestampMillis / 1000
|
|
5802
|
+
end
|
|
5803
|
+
local function dropOldestUntilFits(incomingBytes)
|
|
5804
|
+
while #entries > 0 and (totalBytes + incomingBytes > MAX_BYTES or #entries >= HARD_ENTRY_CAP) do
|
|
5805
|
+
local dropped = table.remove(entries, 1)
|
|
5806
|
+
totalBytes -= #dropped.message
|
|
5807
|
+
totalDropped += 1
|
|
5808
|
+
end
|
|
5809
|
+
end
|
|
5810
|
+
local function install()
|
|
5811
|
+
if installed then
|
|
5812
|
+
return nil
|
|
5813
|
+
end
|
|
5814
|
+
if not RunService:IsStudio() then
|
|
5815
|
+
return nil
|
|
5816
|
+
end
|
|
5817
|
+
installed = true
|
|
5818
|
+
LogService.MessageOut:Connect(function(msg, t)
|
|
5819
|
+
local bytes = #msg
|
|
5820
|
+
dropOldestUntilFits(bytes)
|
|
5821
|
+
local _arg0 = {
|
|
5822
|
+
seq = nextSeq,
|
|
5823
|
+
ts = nowSec(),
|
|
5824
|
+
level = levelTag(t),
|
|
5825
|
+
message = msg,
|
|
5826
|
+
}
|
|
5827
|
+
table.insert(entries, _arg0)
|
|
5828
|
+
nextSeq += 1
|
|
5829
|
+
totalBytes += bytes
|
|
5830
|
+
end)
|
|
5831
|
+
end
|
|
5832
|
+
local function detectPeer()
|
|
5833
|
+
if not RunService:IsRunning() then
|
|
5834
|
+
return "edit"
|
|
5835
|
+
end
|
|
5836
|
+
if RunService:IsServer() then
|
|
5837
|
+
return "server"
|
|
5838
|
+
end
|
|
5839
|
+
return "client"
|
|
5840
|
+
end
|
|
5841
|
+
local function query(opts, peer)
|
|
5842
|
+
local _result
|
|
5843
|
+
if opts.since ~= nil then
|
|
5844
|
+
-- ▼ ReadonlyArray.filter ▼
|
|
5845
|
+
local _newValue = {}
|
|
5846
|
+
local _callback = function(e)
|
|
5847
|
+
return e.seq > (opts.since)
|
|
5848
|
+
end
|
|
5849
|
+
local _length = 0
|
|
5850
|
+
for _k, _v in entries do
|
|
5851
|
+
if _callback(_v, _k - 1, entries) == true then
|
|
5852
|
+
_length += 1
|
|
5853
|
+
_newValue[_length] = _v
|
|
5854
|
+
end
|
|
5855
|
+
end
|
|
5856
|
+
-- ▲ ReadonlyArray.filter ▲
|
|
5857
|
+
_result = _newValue
|
|
5858
|
+
else
|
|
5859
|
+
local _array = {}
|
|
5860
|
+
local _length = #_array
|
|
5861
|
+
table.move(entries, 1, #entries, _length + 1, _array)
|
|
5862
|
+
_result = _array
|
|
5863
|
+
end
|
|
5864
|
+
local result = _result
|
|
5865
|
+
if opts.filter ~= nil then
|
|
5866
|
+
-- Plain substring search (4th arg = true). Pattern matching here was
|
|
5867
|
+
-- surprising in practice - Lua magic chars in messages would silently
|
|
5868
|
+
-- not match (e.g. filter="MARK-EDIT" against "MARK-EDIT-001" fails
|
|
5869
|
+
-- because '-' means "0+" in Lua patterns). Substring search matches
|
|
5870
|
+
-- most users' mental model of "filter messages containing this text".
|
|
5871
|
+
local needle = opts.filter
|
|
5872
|
+
-- ▼ ReadonlyArray.filter ▼
|
|
5873
|
+
local _newValue = {}
|
|
5874
|
+
local _callback = function(e)
|
|
5875
|
+
local start = string.find(e.message, needle, 1, true)
|
|
5876
|
+
return start ~= nil
|
|
5877
|
+
end
|
|
5878
|
+
local _length = 0
|
|
5879
|
+
for _k, _v in result do
|
|
5880
|
+
if _callback(_v, _k - 1, result) == true then
|
|
5881
|
+
_length += 1
|
|
5882
|
+
_newValue[_length] = _v
|
|
5883
|
+
end
|
|
5884
|
+
end
|
|
5885
|
+
-- ▲ ReadonlyArray.filter ▲
|
|
5886
|
+
result = _newValue
|
|
5887
|
+
end
|
|
5888
|
+
if opts.tail ~= nil and #result > opts.tail then
|
|
5889
|
+
-- roblox-ts arrays don't expose .slice; manual tail copy.
|
|
5890
|
+
local tailed = {}
|
|
5891
|
+
local start = #result - opts.tail
|
|
5892
|
+
do
|
|
5893
|
+
local i = start
|
|
5894
|
+
local _shouldIncrement = false
|
|
5895
|
+
while true do
|
|
5896
|
+
if _shouldIncrement then
|
|
5897
|
+
i += 1
|
|
5898
|
+
else
|
|
5899
|
+
_shouldIncrement = true
|
|
5900
|
+
end
|
|
5901
|
+
if not (i < #result) then
|
|
5902
|
+
break
|
|
5903
|
+
end
|
|
5904
|
+
local _arg0 = result[i + 1]
|
|
5905
|
+
table.insert(tailed, _arg0)
|
|
5906
|
+
end
|
|
5907
|
+
end
|
|
5908
|
+
result = tailed
|
|
5909
|
+
end
|
|
5910
|
+
local last = if #entries > 0 then entries[#entries] else nil
|
|
5911
|
+
local _object = {
|
|
5912
|
+
peer = peer,
|
|
5913
|
+
entries = result,
|
|
5914
|
+
totalDropped = totalDropped,
|
|
5915
|
+
}
|
|
5916
|
+
local _left = "nextSince"
|
|
5917
|
+
local _result_1
|
|
5918
|
+
if last then
|
|
5919
|
+
_result_1 = last.seq
|
|
5920
|
+
else
|
|
5921
|
+
local _condition = opts.since
|
|
5922
|
+
if _condition == nil then
|
|
5923
|
+
_condition = 0
|
|
5924
|
+
end
|
|
5925
|
+
_result_1 = _condition
|
|
5926
|
+
end
|
|
5927
|
+
_object[_left] = _result_1
|
|
5928
|
+
return _object
|
|
5929
|
+
end
|
|
5930
|
+
return {
|
|
5931
|
+
install = install,
|
|
5932
|
+
detectPeer = detectPeer,
|
|
5933
|
+
query = query,
|
|
5934
|
+
}
|
|
5935
|
+
]]></string>
|
|
5936
|
+
</Properties>
|
|
5937
|
+
</Item>
|
|
5938
|
+
<Item class="ModuleScript" referent="19">
|
|
5634
5939
|
<Properties>
|
|
5635
5940
|
<string name="Name">State</string>
|
|
5636
5941
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
5637
|
-
local CURRENT_VERSION = "2.
|
|
5942
|
+
local CURRENT_VERSION = "2.10.1"
|
|
5638
5943
|
local MAX_CONNECTIONS = 5
|
|
5639
5944
|
local BASE_PORT = 58741
|
|
5640
5945
|
local activeTabIndex = 0
|
|
@@ -5726,7 +6031,7 @@ return {
|
|
|
5726
6031
|
]]></string>
|
|
5727
6032
|
</Properties>
|
|
5728
6033
|
</Item>
|
|
5729
|
-
<Item class="ModuleScript" referent="
|
|
6034
|
+
<Item class="ModuleScript" referent="20">
|
|
5730
6035
|
<Properties>
|
|
5731
6036
|
<string name="Name">UI</string>
|
|
5732
6037
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5736,6 +6041,34 @@ local State = TS.import(script, script.Parent, "State")
|
|
|
5736
6041
|
local elements = nil
|
|
5737
6042
|
local pulseAnimation
|
|
5738
6043
|
local buttonHover = false
|
|
6044
|
+
local toolbarButton
|
|
6045
|
+
local toolbarIcons
|
|
6046
|
+
local lastToolbarIcon
|
|
6047
|
+
local updateToolbarIcon
|
|
6048
|
+
local function setToolbarButton(btn, icons)
|
|
6049
|
+
toolbarButton = btn
|
|
6050
|
+
toolbarIcons = icons
|
|
6051
|
+
lastToolbarIcon = nil
|
|
6052
|
+
updateToolbarIcon()
|
|
6053
|
+
end
|
|
6054
|
+
function updateToolbarIcon()
|
|
6055
|
+
if not toolbarButton or not toolbarIcons then
|
|
6056
|
+
return nil
|
|
6057
|
+
end
|
|
6058
|
+
local conn = State.getActiveConnection()
|
|
6059
|
+
local nextIcon
|
|
6060
|
+
if not conn or not conn.isActive then
|
|
6061
|
+
nextIcon = toolbarIcons.disconnected
|
|
6062
|
+
elseif conn.lastHttpOk and conn.lastMcpOk then
|
|
6063
|
+
nextIcon = toolbarIcons.connected
|
|
6064
|
+
else
|
|
6065
|
+
nextIcon = toolbarIcons.connecting
|
|
6066
|
+
end
|
|
6067
|
+
if nextIcon ~= lastToolbarIcon then
|
|
6068
|
+
toolbarButton.Icon = nextIcon
|
|
6069
|
+
lastToolbarIcon = nextIcon
|
|
6070
|
+
end
|
|
6071
|
+
end
|
|
5739
6072
|
local tabButtons = {}
|
|
5740
6073
|
local TWEEN_QUICK = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
|
|
5741
6074
|
local function tweenProp(instance, props)
|
|
@@ -6314,6 +6647,7 @@ local function init(pluginRef)
|
|
|
6314
6647
|
refreshTabBar()
|
|
6315
6648
|
end
|
|
6316
6649
|
function updateUIState()
|
|
6650
|
+
updateToolbarIcon()
|
|
6317
6651
|
local conn = State.getActiveConnection()
|
|
6318
6652
|
if not conn then
|
|
6319
6653
|
return nil
|
|
@@ -6439,6 +6773,8 @@ return {
|
|
|
6439
6773
|
updateTabLabel = updateTabLabel,
|
|
6440
6774
|
stopPulseAnimation = stopPulseAnimation,
|
|
6441
6775
|
startPulseAnimation = startPulseAnimation,
|
|
6776
|
+
setToolbarButton = setToolbarButton,
|
|
6777
|
+
updateToolbarIcon = updateToolbarIcon,
|
|
6442
6778
|
getElements = function()
|
|
6443
6779
|
return elements
|
|
6444
6780
|
end,
|
|
@@ -6446,7 +6782,7 @@ return {
|
|
|
6446
6782
|
]]></string>
|
|
6447
6783
|
</Properties>
|
|
6448
6784
|
</Item>
|
|
6449
|
-
<Item class="ModuleScript" referent="
|
|
6785
|
+
<Item class="ModuleScript" referent="21">
|
|
6450
6786
|
<Properties>
|
|
6451
6787
|
<string name="Name">Utils</string>
|
|
6452
6788
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -6976,11 +7312,11 @@ return {
|
|
|
6976
7312
|
</Properties>
|
|
6977
7313
|
</Item>
|
|
6978
7314
|
</Item>
|
|
6979
|
-
<Item class="Folder" referent="
|
|
7315
|
+
<Item class="Folder" referent="25">
|
|
6980
7316
|
<Properties>
|
|
6981
7317
|
<string name="Name">include</string>
|
|
6982
7318
|
</Properties>
|
|
6983
|
-
<Item class="ModuleScript" referent="
|
|
7319
|
+
<Item class="ModuleScript" referent="22">
|
|
6984
7320
|
<Properties>
|
|
6985
7321
|
<string name="Name">Promise</string>
|
|
6986
7322
|
<string name="Source"><![CDATA[--[[
|
|
@@ -9054,7 +9390,7 @@ return Promise
|
|
|
9054
9390
|
]]></string>
|
|
9055
9391
|
</Properties>
|
|
9056
9392
|
</Item>
|
|
9057
|
-
<Item class="ModuleScript" referent="
|
|
9393
|
+
<Item class="ModuleScript" referent="23">
|
|
9058
9394
|
<Properties>
|
|
9059
9395
|
<string name="Name">RuntimeLib</string>
|
|
9060
9396
|
<string name="Source"><![CDATA[local Promise = require(script.Parent.Promise)
|
|
@@ -9321,15 +9657,15 @@ return TS
|
|
|
9321
9657
|
</Properties>
|
|
9322
9658
|
</Item>
|
|
9323
9659
|
</Item>
|
|
9324
|
-
<Item class="Folder" referent="
|
|
9660
|
+
<Item class="Folder" referent="26">
|
|
9325
9661
|
<Properties>
|
|
9326
9662
|
<string name="Name">node_modules</string>
|
|
9327
9663
|
</Properties>
|
|
9328
|
-
<Item class="Folder" referent="
|
|
9664
|
+
<Item class="Folder" referent="27">
|
|
9329
9665
|
<Properties>
|
|
9330
9666
|
<string name="Name">@rbxts</string>
|
|
9331
9667
|
</Properties>
|
|
9332
|
-
<Item class="ModuleScript" referent="
|
|
9668
|
+
<Item class="ModuleScript" referent="24">
|
|
9333
9669
|
<Properties>
|
|
9334
9670
|
<string name="Name">services</string>
|
|
9335
9671
|
<string name="Source"><![CDATA[return setmetatable({}, {
|