@chrrxs/robloxstudio-mcp 2.9.1 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +106 -3
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +451 -125
- package/studio-plugin/MCPPlugin.rbxmx +451 -125
- package/studio-plugin/src/modules/ClientBroker.ts +125 -48
- package/studio-plugin/src/modules/Communication.ts +59 -26
- package/studio-plugin/src/modules/RuntimeLogBuffer.ts +138 -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/server/index.server.ts +5 -0
- package/studio-plugin/src/types/index.d.ts +4 -0
|
@@ -10,6 +10,11 @@ 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()
|
|
15
20
|
local toolbar = plugin:CreateToolbar("MCP Inspector")
|
|
@@ -72,6 +77,7 @@ local HttpService = _services.HttpService
|
|
|
72
77
|
local Players = _services.Players
|
|
73
78
|
local ReplicatedStorage = _services.ReplicatedStorage
|
|
74
79
|
local RunService = _services.RunService
|
|
80
|
+
local RuntimeLogBuffer = TS.import(script, script.Parent, "RuntimeLogBuffer")
|
|
75
81
|
-- The client peer cannot reach the MCP HTTP server - Roblox forbids
|
|
76
82
|
-- HttpService:RequestAsync from the client DM even under PluginSecurity, and
|
|
77
83
|
-- HttpEnabled reads as false there regardless of identity. So the server peer
|
|
@@ -87,6 +93,37 @@ local RunService = _services.RunService
|
|
|
87
93
|
-- edit DM untouched.
|
|
88
94
|
local MCP_URL = "http://localhost:58741"
|
|
89
95
|
local BROKER_NAME = "__MCPClientBroker"
|
|
96
|
+
-- Endpoints the server-peer broker is allowed to forward to the client peer.
|
|
97
|
+
-- Each requires the client peer's plugin VM (because the buffer / require
|
|
98
|
+
-- cache / etc. lives there) so the server peer alone can't satisfy them.
|
|
99
|
+
local CLIENT_BROKER_ALLOWED_ENDPOINTS = {
|
|
100
|
+
["/api/execute-luau"] = true,
|
|
101
|
+
["/api/get-runtime-logs"] = true,
|
|
102
|
+
}
|
|
103
|
+
-- Throttle re-ready calls per proxyId so a brief window of unknownInstance
|
|
104
|
+
-- polls doesn't cause a re-register stampede.
|
|
105
|
+
local lastReadyByProxy = {}
|
|
106
|
+
local postJson
|
|
107
|
+
local function reRegisterProxy(proxyId, role)
|
|
108
|
+
local now = tick()
|
|
109
|
+
local _proxyId = proxyId
|
|
110
|
+
local _condition = lastReadyByProxy[_proxyId]
|
|
111
|
+
if _condition == nil then
|
|
112
|
+
_condition = 0
|
|
113
|
+
end
|
|
114
|
+
local last = _condition
|
|
115
|
+
if now - last < 2 then
|
|
116
|
+
return nil
|
|
117
|
+
end
|
|
118
|
+
local _proxyId_1 = proxyId
|
|
119
|
+
lastReadyByProxy[_proxyId_1] = now
|
|
120
|
+
pcall(function()
|
|
121
|
+
return postJson("/ready", {
|
|
122
|
+
instanceId = proxyId,
|
|
123
|
+
role = role,
|
|
124
|
+
})
|
|
125
|
+
end)
|
|
126
|
+
end
|
|
90
127
|
local function forkRole()
|
|
91
128
|
if not RunService:IsRunning() then
|
|
92
129
|
return "edit"
|
|
@@ -96,7 +133,7 @@ local function forkRole()
|
|
|
96
133
|
end
|
|
97
134
|
return "client"
|
|
98
135
|
end
|
|
99
|
-
|
|
136
|
+
function postJson(endpoint, body)
|
|
100
137
|
return pcall(function()
|
|
101
138
|
return HttpService:RequestAsync({
|
|
102
139
|
Url = `{MCP_URL}{endpoint}`,
|
|
@@ -108,6 +145,56 @@ local function postJson(endpoint, body)
|
|
|
108
145
|
})
|
|
109
146
|
end)
|
|
110
147
|
end
|
|
148
|
+
local function handleExecuteLuau(data)
|
|
149
|
+
local code = data and (data.code)
|
|
150
|
+
if type(code) == "string" == false or code == "" then
|
|
151
|
+
return {
|
|
152
|
+
success = false,
|
|
153
|
+
error = "code is required",
|
|
154
|
+
}
|
|
155
|
+
end
|
|
156
|
+
local m = Instance.new("ModuleScript")
|
|
157
|
+
m.Name = "__MCPClientEval"
|
|
158
|
+
local okSet, setErr = pcall(function()
|
|
159
|
+
m.Source = code
|
|
160
|
+
end)
|
|
161
|
+
if not okSet then
|
|
162
|
+
m:Destroy()
|
|
163
|
+
return {
|
|
164
|
+
success = false,
|
|
165
|
+
error = `Source set failed: {tostring(setErr)}`,
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
m.Parent = game.Workspace
|
|
169
|
+
local okReq, result = pcall(function()
|
|
170
|
+
return require(m)
|
|
171
|
+
end)
|
|
172
|
+
m:Destroy()
|
|
173
|
+
if okReq then
|
|
174
|
+
return {
|
|
175
|
+
success = true,
|
|
176
|
+
returnValue = if result ~= nil then tostring(result) else nil,
|
|
177
|
+
message = "Code executed successfully",
|
|
178
|
+
}
|
|
179
|
+
end
|
|
180
|
+
return {
|
|
181
|
+
success = false,
|
|
182
|
+
error = tostring(result),
|
|
183
|
+
}
|
|
184
|
+
end
|
|
185
|
+
local function handleGetRuntimeLogs(data)
|
|
186
|
+
local d = data or {}
|
|
187
|
+
local since = d.since
|
|
188
|
+
local tail = d.tail
|
|
189
|
+
local filter = d.filter
|
|
190
|
+
-- "client" is the generic peer tag; MCP-side aggregator overrides with
|
|
191
|
+
-- the specific role (e.g. "client-1") on target=all fan-out.
|
|
192
|
+
return RuntimeLogBuffer.query({
|
|
193
|
+
since = since,
|
|
194
|
+
tail = tail,
|
|
195
|
+
filter = filter,
|
|
196
|
+
}, "client")
|
|
197
|
+
end
|
|
111
198
|
local function setupClientBroker()
|
|
112
199
|
local rf = ReplicatedStorage:WaitForChild(BROKER_NAME, 10)
|
|
113
200
|
if not rf or not rf:IsA("RemoteFunction") then
|
|
@@ -115,44 +202,20 @@ local function setupClientBroker()
|
|
|
115
202
|
return nil
|
|
116
203
|
end
|
|
117
204
|
rf.OnClientInvoke = function(payload)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
205
|
+
-- Two payload shapes in the wild:
|
|
206
|
+
-- - {endpoint, data} from v2.10+ server-peer broker (this is the new
|
|
207
|
+
-- discriminated form that lets us dispatch on endpoint)
|
|
208
|
+
-- - {code} from pre-v2.10 server-peer broker (raw execute-luau payload)
|
|
209
|
+
-- The shapes coexist gracefully because we fall back to execute-luau
|
|
210
|
+
-- when endpoint is missing.
|
|
211
|
+
if payload and payload.endpoint == "/api/get-runtime-logs" then
|
|
212
|
+
return handleGetRuntimeLogs(payload.data)
|
|
124
213
|
end
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
214
|
+
if payload and payload.endpoint == "/api/execute-luau" then
|
|
215
|
+
return handleExecuteLuau(payload.data)
|
|
137
216
|
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
|
|
150
|
-
end
|
|
151
|
-
local _arg0 = {
|
|
152
|
-
success = false,
|
|
153
|
-
error = tostring(result),
|
|
154
|
-
}
|
|
155
|
-
return _arg0
|
|
217
|
+
-- Legacy: raw execute-luau payload at the top level.
|
|
218
|
+
return handleExecuteLuau(payload)
|
|
156
219
|
end
|
|
157
220
|
end
|
|
158
221
|
local proxyByPlayer = {}
|
|
@@ -179,33 +242,47 @@ local function pollProxy(proxyId, player, rf)
|
|
|
179
242
|
local okJson, body = pcall(function()
|
|
180
243
|
return HttpService:JSONDecode(res.Body)
|
|
181
244
|
end)
|
|
182
|
-
if okJson and body
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
245
|
+
if okJson and body then
|
|
246
|
+
-- Server lost our proxy registration (process restart, etc.) -
|
|
247
|
+
-- re-register so the next poll cycle starts routing again.
|
|
248
|
+
if body.knownInstance == false then
|
|
249
|
+
reRegisterProxy(proxyId, "client")
|
|
250
|
+
end
|
|
251
|
+
if body.request and body.requestId ~= nil then
|
|
252
|
+
local request = body.request
|
|
253
|
+
local response
|
|
254
|
+
local _endpoint = request.endpoint
|
|
255
|
+
if CLIENT_BROKER_ALLOWED_ENDPOINTS[_endpoint] ~= nil then
|
|
256
|
+
-- Forward as a discriminated envelope so the client-side
|
|
257
|
+
-- OnClientInvoke knows which endpoint it's serving.
|
|
258
|
+
local envelope = {
|
|
259
|
+
endpoint = request.endpoint,
|
|
260
|
+
data = request.data,
|
|
193
261
|
}
|
|
262
|
+
local okInvoke, invokeRes = pcall(function()
|
|
263
|
+
return rf:InvokeClient(player, envelope)
|
|
264
|
+
end)
|
|
265
|
+
if okInvoke then
|
|
266
|
+
response = if invokeRes ~= nil then invokeRes else {
|
|
267
|
+
success = false,
|
|
268
|
+
error = "nil response",
|
|
269
|
+
}
|
|
270
|
+
else
|
|
271
|
+
response = {
|
|
272
|
+
success = false,
|
|
273
|
+
error = `InvokeClient failed: {tostring(invokeRes)}`,
|
|
274
|
+
}
|
|
275
|
+
end
|
|
194
276
|
else
|
|
195
277
|
response = {
|
|
196
|
-
|
|
197
|
-
error = `InvokeClient failed: {tostring(invokeRes)}`,
|
|
278
|
+
error = `Client-proxy does not forward {tostring(request.endpoint)}. ` .. `Allowed: /api/execute-luau, /api/get-runtime-logs.`,
|
|
198
279
|
}
|
|
199
280
|
end
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
281
|
+
postJson("/response", {
|
|
282
|
+
requestId = body.requestId,
|
|
283
|
+
response = response,
|
|
284
|
+
})
|
|
204
285
|
end
|
|
205
|
-
postJson("/response", {
|
|
206
|
-
requestId = body.requestId,
|
|
207
|
-
response = response,
|
|
208
|
-
})
|
|
209
286
|
end
|
|
210
287
|
end
|
|
211
288
|
task.wait(0.5)
|
|
@@ -264,22 +341,28 @@ local function startEditProxyLoop()
|
|
|
264
341
|
local okJson, body = pcall(function()
|
|
265
342
|
return HttpService:JSONDecode(pollRes.Body)
|
|
266
343
|
end)
|
|
267
|
-
if okJson and body
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
344
|
+
if okJson and body then
|
|
345
|
+
-- Re-register if the server lost our edit-proxy registration.
|
|
346
|
+
if body.knownInstance == false then
|
|
347
|
+
reRegisterProxy(proxyId, "edit-proxy")
|
|
348
|
+
end
|
|
349
|
+
if body.request and body.request.endpoint == "/api/stop-playtest" and body.requestId ~= nil then
|
|
350
|
+
local sts = game:GetService("StudioTestService")
|
|
351
|
+
local endOk, endErr = pcall(function()
|
|
352
|
+
return sts:EndTest("stopped_by_mcp")
|
|
353
|
+
end)
|
|
354
|
+
local response = if endOk then {
|
|
355
|
+
success = true,
|
|
356
|
+
message = "Playtest stopped via edit-proxy/EndTest",
|
|
357
|
+
} else {
|
|
358
|
+
success = false,
|
|
359
|
+
error = `EndTest failed: {tostring(endErr)}`,
|
|
360
|
+
}
|
|
361
|
+
postJson("/response", {
|
|
362
|
+
requestId = body.requestId,
|
|
363
|
+
response = response,
|
|
364
|
+
})
|
|
365
|
+
end
|
|
283
366
|
end
|
|
284
367
|
end
|
|
285
368
|
task.wait(0.15)
|
|
@@ -351,6 +434,7 @@ local BuildHandlers = TS.import(script, script.Parent, "handlers", "BuildHandler
|
|
|
351
434
|
local AssetHandlers = TS.import(script, script.Parent, "handlers", "AssetHandlers")
|
|
352
435
|
local CaptureHandlers = TS.import(script, script.Parent, "handlers", "CaptureHandlers")
|
|
353
436
|
local InputHandlers = TS.import(script, script.Parent, "handlers", "InputHandlers")
|
|
437
|
+
local LogHandlers = TS.import(script, script.Parent, "handlers", "LogHandlers")
|
|
354
438
|
local instanceId = HttpService:GenerateGUID(false)
|
|
355
439
|
local assignedRole
|
|
356
440
|
local function detectRole()
|
|
@@ -419,6 +503,7 @@ local routeMap = {
|
|
|
419
503
|
["/api/simulate-mouse-input"] = InputHandlers.simulateMouseInput,
|
|
420
504
|
["/api/simulate-keyboard-input"] = InputHandlers.simulateKeyboardInput,
|
|
421
505
|
["/api/find-and-replace-in-scripts"] = ScriptHandlers.findAndReplaceInScripts,
|
|
506
|
+
["/api/get-runtime-logs"] = LogHandlers.getRuntimeLogs,
|
|
422
507
|
}
|
|
423
508
|
local function processRequest(request)
|
|
424
509
|
local endpoint = request.endpoint
|
|
@@ -460,6 +545,43 @@ local function getConnectionStatus(connIndex)
|
|
|
460
545
|
end
|
|
461
546
|
return "connecting"
|
|
462
547
|
end
|
|
548
|
+
-- Throttle for re-issuing /ready after the server reports knownInstance=false.
|
|
549
|
+
-- Without this, every poll during the brief window where the server has just
|
|
550
|
+
-- restarted but hasn't seen our re-ready yet would fire a duplicate /ready.
|
|
551
|
+
local lastReadyPostAt = 0
|
|
552
|
+
local function sendReady(conn)
|
|
553
|
+
local now = tick()
|
|
554
|
+
if now - lastReadyPostAt < 2 then
|
|
555
|
+
return nil
|
|
556
|
+
end
|
|
557
|
+
lastReadyPostAt = now
|
|
558
|
+
task.spawn(function()
|
|
559
|
+
local readyOk, readyResult = pcall(function()
|
|
560
|
+
return HttpService:RequestAsync({
|
|
561
|
+
Url = `{conn.serverUrl}/ready`,
|
|
562
|
+
Method = "POST",
|
|
563
|
+
Headers = {
|
|
564
|
+
["Content-Type"] = "application/json",
|
|
565
|
+
},
|
|
566
|
+
Body = HttpService:JSONEncode({
|
|
567
|
+
instanceId = instanceId,
|
|
568
|
+
role = detectRole(),
|
|
569
|
+
pluginReady = true,
|
|
570
|
+
timestamp = tick(),
|
|
571
|
+
}),
|
|
572
|
+
})
|
|
573
|
+
end)
|
|
574
|
+
if readyOk and readyResult.Success then
|
|
575
|
+
local parseOk, readyData = pcall(function()
|
|
576
|
+
return HttpService:JSONDecode(readyResult.Body)
|
|
577
|
+
end)
|
|
578
|
+
local _value = parseOk and readyData.assignedRole
|
|
579
|
+
if _value ~= "" and _value then
|
|
580
|
+
assignedRole = readyData.assignedRole
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
end)
|
|
584
|
+
end
|
|
463
585
|
local function pollForRequests(connIndex)
|
|
464
586
|
local conn = State.getConnection(connIndex)
|
|
465
587
|
if not conn or not conn.isActive then
|
|
@@ -489,6 +611,14 @@ local function pollForRequests(connIndex)
|
|
|
489
611
|
local mcpConnected = data.mcpConnected == true
|
|
490
612
|
conn.lastHttpOk = true
|
|
491
613
|
conn.lastMcpOk = mcpConnected
|
|
614
|
+
-- Server tells us when its in-memory instances map doesn't have us
|
|
615
|
+
-- (e.g. after an MCP process restart). Re-issue /ready immediately so
|
|
616
|
+
-- target=server/client-N start routing again. The throttle inside
|
|
617
|
+
-- sendReady() prevents duplicate registrations while the server
|
|
618
|
+
-- catches up.
|
|
619
|
+
if data.knownInstance == false then
|
|
620
|
+
sendReady(conn)
|
|
621
|
+
end
|
|
492
622
|
if connIndex == State.getActiveTabIndex() then
|
|
493
623
|
local el = ui
|
|
494
624
|
el.step1Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
|
|
@@ -644,42 +774,19 @@ local function activatePlugin(connIndex)
|
|
|
644
774
|
UI.updateUIState()
|
|
645
775
|
end
|
|
646
776
|
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
|
|
777
|
+
if not conn.heartbeatConnection then
|
|
778
|
+
conn.heartbeatConnection = RunService.Heartbeat:Connect(function()
|
|
779
|
+
local now = tick()
|
|
780
|
+
local currentInterval = if conn.consecutiveFailures > 5 then conn.currentRetryDelay else conn.pollInterval
|
|
781
|
+
if now - conn.lastPoll > currentInterval then
|
|
782
|
+
conn.lastPoll = now
|
|
783
|
+
pollForRequests(idx)
|
|
680
784
|
end
|
|
681
|
-
end
|
|
682
|
-
end
|
|
785
|
+
end)
|
|
786
|
+
end
|
|
787
|
+
-- Initial /ready; pollForRequests will also re-fire ready if the server
|
|
788
|
+
-- later reports knownInstance=false (process restart, etc).
|
|
789
|
+
sendReady(conn)
|
|
683
790
|
end
|
|
684
791
|
local function deactivatePlugin(connIndex)
|
|
685
792
|
local _condition = connIndex
|
|
@@ -2681,6 +2788,33 @@ return {
|
|
|
2681
2788
|
</Properties>
|
|
2682
2789
|
</Item>
|
|
2683
2790
|
<Item class="ModuleScript" referent="11">
|
|
2791
|
+
<Properties>
|
|
2792
|
+
<string name="Name">LogHandlers</string>
|
|
2793
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
2794
|
+
local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
|
|
2795
|
+
local RuntimeLogBuffer = TS.import(script, script.Parent.Parent, "RuntimeLogBuffer")
|
|
2796
|
+
local function getRuntimeLogs(requestData)
|
|
2797
|
+
local since = requestData.since
|
|
2798
|
+
local tail = requestData.tail
|
|
2799
|
+
local filter = requestData.filter
|
|
2800
|
+
-- Plugin-side peer tag is generic ("edit"|"server"|"client"). The MCP-side
|
|
2801
|
+
-- aggregator overrides it with the specific instance role (e.g. "client-1")
|
|
2802
|
+
-- during fan-out for target=all, so this value is only authoritative for
|
|
2803
|
+
-- the single-peer query path.
|
|
2804
|
+
local peer = RuntimeLogBuffer.detectPeer()
|
|
2805
|
+
return RuntimeLogBuffer.query({
|
|
2806
|
+
since = since,
|
|
2807
|
+
tail = tail,
|
|
2808
|
+
filter = filter,
|
|
2809
|
+
}, peer)
|
|
2810
|
+
end
|
|
2811
|
+
return {
|
|
2812
|
+
getRuntimeLogs = getRuntimeLogs,
|
|
2813
|
+
}
|
|
2814
|
+
]]></string>
|
|
2815
|
+
</Properties>
|
|
2816
|
+
</Item>
|
|
2817
|
+
<Item class="ModuleScript" referent="12">
|
|
2684
2818
|
<Properties>
|
|
2685
2819
|
<string name="Name">MetadataHandlers</string>
|
|
2686
2820
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -3152,8 +3286,19 @@ local function executeLuau(requestData)
|
|
|
3152
3286
|
local runViaModuleScript = function()
|
|
3153
3287
|
local m = Instance.new("ModuleScript")
|
|
3154
3288
|
m.Name = "__MCPExecLuauPayload"
|
|
3289
|
+
-- Wrap user code in an IIFE so require() always gets exactly one
|
|
3290
|
+
-- return value. Without this, code like `print("x")` errors with
|
|
3291
|
+
-- "Module code did not return exactly one value" because top-level
|
|
3292
|
+
-- ModuleScripts must return exactly one value.
|
|
3293
|
+
--
|
|
3294
|
+
-- The DOUBLE parens around the call are load-bearing: in Luau,
|
|
3295
|
+
-- `return f()` propagates whatever multi-value tuple f returns,
|
|
3296
|
+
-- including zero values. Outer parens adjust the call to exactly
|
|
3297
|
+
-- one value (the first, or nil). So `return ((f)())` always
|
|
3298
|
+
-- returns exactly one value, regardless of what f does.
|
|
3299
|
+
local wrapped = `return ((function()\n{code}\nend)())`
|
|
3155
3300
|
local okSet, setErr = pcall(function()
|
|
3156
|
-
m.Source =
|
|
3301
|
+
m.Source = wrapped
|
|
3157
3302
|
end)
|
|
3158
3303
|
if not okSet then
|
|
3159
3304
|
m:Destroy()
|
|
@@ -3306,7 +3451,7 @@ return {
|
|
|
3306
3451
|
]]></string>
|
|
3307
3452
|
</Properties>
|
|
3308
3453
|
</Item>
|
|
3309
|
-
<Item class="ModuleScript" referent="
|
|
3454
|
+
<Item class="ModuleScript" referent="13">
|
|
3310
3455
|
<Properties>
|
|
3311
3456
|
<string name="Name">PropertyHandlers</string>
|
|
3312
3457
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -3558,7 +3703,7 @@ return {
|
|
|
3558
3703
|
]]></string>
|
|
3559
3704
|
</Properties>
|
|
3560
3705
|
</Item>
|
|
3561
|
-
<Item class="ModuleScript" referent="
|
|
3706
|
+
<Item class="ModuleScript" referent="14">
|
|
3562
3707
|
<Properties>
|
|
3563
3708
|
<string name="Name">QueryHandlers</string>
|
|
3564
3709
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -4585,7 +4730,7 @@ return {
|
|
|
4585
4730
|
]]></string>
|
|
4586
4731
|
</Properties>
|
|
4587
4732
|
</Item>
|
|
4588
|
-
<Item class="ModuleScript" referent="
|
|
4733
|
+
<Item class="ModuleScript" referent="15">
|
|
4589
4734
|
<Properties>
|
|
4590
4735
|
<string name="Name">ScriptHandlers</string>
|
|
4591
4736
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5281,7 +5426,7 @@ return {
|
|
|
5281
5426
|
]]></string>
|
|
5282
5427
|
</Properties>
|
|
5283
5428
|
</Item>
|
|
5284
|
-
<Item class="ModuleScript" referent="
|
|
5429
|
+
<Item class="ModuleScript" referent="16">
|
|
5285
5430
|
<Properties>
|
|
5286
5431
|
<string name="Name">TestHandlers</string>
|
|
5287
5432
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5600,7 +5745,7 @@ return {
|
|
|
5600
5745
|
</Properties>
|
|
5601
5746
|
</Item>
|
|
5602
5747
|
</Item>
|
|
5603
|
-
<Item class="ModuleScript" referent="
|
|
5748
|
+
<Item class="ModuleScript" referent="17">
|
|
5604
5749
|
<Properties>
|
|
5605
5750
|
<string name="Name">Recording</string>
|
|
5606
5751
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -5630,11 +5775,192 @@ return {
|
|
|
5630
5775
|
]]></string>
|
|
5631
5776
|
</Properties>
|
|
5632
5777
|
</Item>
|
|
5633
|
-
<Item class="ModuleScript" referent="
|
|
5778
|
+
<Item class="ModuleScript" referent="18">
|
|
5779
|
+
<Properties>
|
|
5780
|
+
<string name="Name">RuntimeLogBuffer</string>
|
|
5781
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
5782
|
+
local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
5783
|
+
-- Per-peer in-memory ring buffer for LogService.MessageOut events.
|
|
5784
|
+
-- Powers the get_runtime_logs MCP tool. Replaces the out-of-tree LogBuffer
|
|
5785
|
+
-- primitives + StringValue approach from chrrxs/roblox-mcp-primitives.
|
|
5786
|
+
--
|
|
5787
|
+
-- Each peer's plugin attaches a MessageOut listener at plugin load (edit DM,
|
|
5788
|
+
-- play-server DM, play-client DM all run their own copy of this module).
|
|
5789
|
+
-- Captured entries live in plugin module-state; nothing is parented to the
|
|
5790
|
+
-- DataModel. The buffer is bounded by a message-byte budget; oldest entries
|
|
5791
|
+
-- drop when over budget.
|
|
5792
|
+
--
|
|
5793
|
+
-- Peer-tag caveat: returned entries reflect which peer's plugin CAPTURED the
|
|
5794
|
+
-- entry, NOT which peer's script originated the print. LogService reflects
|
|
5795
|
+
-- prints across peers in Studio Play (a server print ends up in both the
|
|
5796
|
+
-- server and client LogService:GetLogHistory()) and origin is empirically
|
|
5797
|
+
-- undetectable from inside MessageOut. The MCP-side aggregator handles
|
|
5798
|
+
-- cross-peer dedup via a 2s timestamp window.
|
|
5799
|
+
local _services = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services")
|
|
5800
|
+
local LogService = _services.LogService
|
|
5801
|
+
local RunService = _services.RunService
|
|
5802
|
+
local MAX_BYTES = 64 * 1024
|
|
5803
|
+
local HARD_ENTRY_CAP = 50_000
|
|
5804
|
+
local entries = {}
|
|
5805
|
+
local totalBytes = 0
|
|
5806
|
+
local totalDropped = 0
|
|
5807
|
+
local nextSeq = 1
|
|
5808
|
+
local installed = false
|
|
5809
|
+
local function levelTag(t)
|
|
5810
|
+
if t == Enum.MessageType.MessageWarning then
|
|
5811
|
+
return "WARN"
|
|
5812
|
+
end
|
|
5813
|
+
if t == Enum.MessageType.MessageError then
|
|
5814
|
+
return "ERR"
|
|
5815
|
+
end
|
|
5816
|
+
if t == Enum.MessageType.MessageInfo then
|
|
5817
|
+
return "INFO"
|
|
5818
|
+
end
|
|
5819
|
+
return "OUT"
|
|
5820
|
+
end
|
|
5821
|
+
local function nowSec()
|
|
5822
|
+
return DateTime.now().UnixTimestampMillis / 1000
|
|
5823
|
+
end
|
|
5824
|
+
local function dropOldestUntilFits(incomingBytes)
|
|
5825
|
+
while #entries > 0 and (totalBytes + incomingBytes > MAX_BYTES or #entries >= HARD_ENTRY_CAP) do
|
|
5826
|
+
local dropped = table.remove(entries, 1)
|
|
5827
|
+
totalBytes -= #dropped.message
|
|
5828
|
+
totalDropped += 1
|
|
5829
|
+
end
|
|
5830
|
+
end
|
|
5831
|
+
local function install()
|
|
5832
|
+
if installed then
|
|
5833
|
+
return nil
|
|
5834
|
+
end
|
|
5835
|
+
if not RunService:IsStudio() then
|
|
5836
|
+
return nil
|
|
5837
|
+
end
|
|
5838
|
+
installed = true
|
|
5839
|
+
LogService.MessageOut:Connect(function(msg, t)
|
|
5840
|
+
local bytes = #msg
|
|
5841
|
+
dropOldestUntilFits(bytes)
|
|
5842
|
+
local _arg0 = {
|
|
5843
|
+
seq = nextSeq,
|
|
5844
|
+
ts = nowSec(),
|
|
5845
|
+
level = levelTag(t),
|
|
5846
|
+
message = msg,
|
|
5847
|
+
}
|
|
5848
|
+
table.insert(entries, _arg0)
|
|
5849
|
+
nextSeq += 1
|
|
5850
|
+
totalBytes += bytes
|
|
5851
|
+
end)
|
|
5852
|
+
end
|
|
5853
|
+
local function detectPeer()
|
|
5854
|
+
if not RunService:IsRunning() then
|
|
5855
|
+
return "edit"
|
|
5856
|
+
end
|
|
5857
|
+
if RunService:IsServer() then
|
|
5858
|
+
return "server"
|
|
5859
|
+
end
|
|
5860
|
+
return "client"
|
|
5861
|
+
end
|
|
5862
|
+
local function query(opts, peer)
|
|
5863
|
+
local _result
|
|
5864
|
+
if opts.since ~= nil then
|
|
5865
|
+
-- ▼ ReadonlyArray.filter ▼
|
|
5866
|
+
local _newValue = {}
|
|
5867
|
+
local _callback = function(e)
|
|
5868
|
+
return e.seq > (opts.since)
|
|
5869
|
+
end
|
|
5870
|
+
local _length = 0
|
|
5871
|
+
for _k, _v in entries do
|
|
5872
|
+
if _callback(_v, _k - 1, entries) == true then
|
|
5873
|
+
_length += 1
|
|
5874
|
+
_newValue[_length] = _v
|
|
5875
|
+
end
|
|
5876
|
+
end
|
|
5877
|
+
-- ▲ ReadonlyArray.filter ▲
|
|
5878
|
+
_result = _newValue
|
|
5879
|
+
else
|
|
5880
|
+
local _array = {}
|
|
5881
|
+
local _length = #_array
|
|
5882
|
+
table.move(entries, 1, #entries, _length + 1, _array)
|
|
5883
|
+
_result = _array
|
|
5884
|
+
end
|
|
5885
|
+
local result = _result
|
|
5886
|
+
if opts.filter ~= nil then
|
|
5887
|
+
-- Plain substring search (4th arg = true). Pattern matching here was
|
|
5888
|
+
-- surprising in practice - Lua magic chars in messages would silently
|
|
5889
|
+
-- not match (e.g. filter="MARK-EDIT" against "MARK-EDIT-001" fails
|
|
5890
|
+
-- because '-' means "0+" in Lua patterns). Substring search matches
|
|
5891
|
+
-- most users' mental model of "filter messages containing this text".
|
|
5892
|
+
local needle = opts.filter
|
|
5893
|
+
-- ▼ ReadonlyArray.filter ▼
|
|
5894
|
+
local _newValue = {}
|
|
5895
|
+
local _callback = function(e)
|
|
5896
|
+
local start = string.find(e.message, needle, 1, true)
|
|
5897
|
+
return start ~= nil
|
|
5898
|
+
end
|
|
5899
|
+
local _length = 0
|
|
5900
|
+
for _k, _v in result do
|
|
5901
|
+
if _callback(_v, _k - 1, result) == true then
|
|
5902
|
+
_length += 1
|
|
5903
|
+
_newValue[_length] = _v
|
|
5904
|
+
end
|
|
5905
|
+
end
|
|
5906
|
+
-- ▲ ReadonlyArray.filter ▲
|
|
5907
|
+
result = _newValue
|
|
5908
|
+
end
|
|
5909
|
+
if opts.tail ~= nil and #result > opts.tail then
|
|
5910
|
+
-- roblox-ts arrays don't expose .slice; manual tail copy.
|
|
5911
|
+
local tailed = {}
|
|
5912
|
+
local start = #result - opts.tail
|
|
5913
|
+
do
|
|
5914
|
+
local i = start
|
|
5915
|
+
local _shouldIncrement = false
|
|
5916
|
+
while true do
|
|
5917
|
+
if _shouldIncrement then
|
|
5918
|
+
i += 1
|
|
5919
|
+
else
|
|
5920
|
+
_shouldIncrement = true
|
|
5921
|
+
end
|
|
5922
|
+
if not (i < #result) then
|
|
5923
|
+
break
|
|
5924
|
+
end
|
|
5925
|
+
local _arg0 = result[i + 1]
|
|
5926
|
+
table.insert(tailed, _arg0)
|
|
5927
|
+
end
|
|
5928
|
+
end
|
|
5929
|
+
result = tailed
|
|
5930
|
+
end
|
|
5931
|
+
local last = if #entries > 0 then entries[#entries] else nil
|
|
5932
|
+
local _object = {
|
|
5933
|
+
peer = peer,
|
|
5934
|
+
entries = result,
|
|
5935
|
+
totalDropped = totalDropped,
|
|
5936
|
+
}
|
|
5937
|
+
local _left = "nextSince"
|
|
5938
|
+
local _result_1
|
|
5939
|
+
if last then
|
|
5940
|
+
_result_1 = last.seq
|
|
5941
|
+
else
|
|
5942
|
+
local _condition = opts.since
|
|
5943
|
+
if _condition == nil then
|
|
5944
|
+
_condition = 0
|
|
5945
|
+
end
|
|
5946
|
+
_result_1 = _condition
|
|
5947
|
+
end
|
|
5948
|
+
_object[_left] = _result_1
|
|
5949
|
+
return _object
|
|
5950
|
+
end
|
|
5951
|
+
return {
|
|
5952
|
+
install = install,
|
|
5953
|
+
detectPeer = detectPeer,
|
|
5954
|
+
query = query,
|
|
5955
|
+
}
|
|
5956
|
+
]]></string>
|
|
5957
|
+
</Properties>
|
|
5958
|
+
</Item>
|
|
5959
|
+
<Item class="ModuleScript" referent="19">
|
|
5634
5960
|
<Properties>
|
|
5635
5961
|
<string name="Name">State</string>
|
|
5636
5962
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
5637
|
-
local CURRENT_VERSION = "2.
|
|
5963
|
+
local CURRENT_VERSION = "2.10.0"
|
|
5638
5964
|
local MAX_CONNECTIONS = 5
|
|
5639
5965
|
local BASE_PORT = 58741
|
|
5640
5966
|
local activeTabIndex = 0
|
|
@@ -5726,7 +6052,7 @@ return {
|
|
|
5726
6052
|
]]></string>
|
|
5727
6053
|
</Properties>
|
|
5728
6054
|
</Item>
|
|
5729
|
-
<Item class="ModuleScript" referent="
|
|
6055
|
+
<Item class="ModuleScript" referent="20">
|
|
5730
6056
|
<Properties>
|
|
5731
6057
|
<string name="Name">UI</string>
|
|
5732
6058
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -6446,7 +6772,7 @@ return {
|
|
|
6446
6772
|
]]></string>
|
|
6447
6773
|
</Properties>
|
|
6448
6774
|
</Item>
|
|
6449
|
-
<Item class="ModuleScript" referent="
|
|
6775
|
+
<Item class="ModuleScript" referent="21">
|
|
6450
6776
|
<Properties>
|
|
6451
6777
|
<string name="Name">Utils</string>
|
|
6452
6778
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -6976,11 +7302,11 @@ return {
|
|
|
6976
7302
|
</Properties>
|
|
6977
7303
|
</Item>
|
|
6978
7304
|
</Item>
|
|
6979
|
-
<Item class="Folder" referent="
|
|
7305
|
+
<Item class="Folder" referent="25">
|
|
6980
7306
|
<Properties>
|
|
6981
7307
|
<string name="Name">include</string>
|
|
6982
7308
|
</Properties>
|
|
6983
|
-
<Item class="ModuleScript" referent="
|
|
7309
|
+
<Item class="ModuleScript" referent="22">
|
|
6984
7310
|
<Properties>
|
|
6985
7311
|
<string name="Name">Promise</string>
|
|
6986
7312
|
<string name="Source"><![CDATA[--[[
|
|
@@ -9054,7 +9380,7 @@ return Promise
|
|
|
9054
9380
|
]]></string>
|
|
9055
9381
|
</Properties>
|
|
9056
9382
|
</Item>
|
|
9057
|
-
<Item class="ModuleScript" referent="
|
|
9383
|
+
<Item class="ModuleScript" referent="23">
|
|
9058
9384
|
<Properties>
|
|
9059
9385
|
<string name="Name">RuntimeLib</string>
|
|
9060
9386
|
<string name="Source"><![CDATA[local Promise = require(script.Parent.Promise)
|
|
@@ -9321,15 +9647,15 @@ return TS
|
|
|
9321
9647
|
</Properties>
|
|
9322
9648
|
</Item>
|
|
9323
9649
|
</Item>
|
|
9324
|
-
<Item class="Folder" referent="
|
|
9650
|
+
<Item class="Folder" referent="26">
|
|
9325
9651
|
<Properties>
|
|
9326
9652
|
<string name="Name">node_modules</string>
|
|
9327
9653
|
</Properties>
|
|
9328
|
-
<Item class="Folder" referent="
|
|
9654
|
+
<Item class="Folder" referent="27">
|
|
9329
9655
|
<Properties>
|
|
9330
9656
|
<string name="Name">@rbxts</string>
|
|
9331
9657
|
</Properties>
|
|
9332
|
-
<Item class="ModuleScript" referent="
|
|
9658
|
+
<Item class="ModuleScript" referent="24">
|
|
9333
9659
|
<Properties>
|
|
9334
9660
|
<string name="Name">services</string>
|
|
9335
9661
|
<string name="Source"><![CDATA[return setmetatable({}, {
|