@princeofscale/bloxforge-inspector 3.0.0-rc.1 → 3.0.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 +1455 -378
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +380 -84
- package/studio-plugin/MCPPlugin.rbxmx +380 -84
- package/studio-plugin/src/modules/ClientBroker.ts +67 -14
- package/studio-plugin/src/modules/Communication.ts +151 -22
- package/studio-plugin/src/modules/RuntimeLogBuffer.ts +31 -2
- package/studio-plugin/src/modules/State.ts +1 -1
- package/studio-plugin/src/modules/handlers/JobHandlers.ts +2 -1
- package/studio-plugin/src/types/index.d.ts +7 -0
|
@@ -240,11 +240,41 @@ local CLIENT_BROKER_ALLOWED_ENDPOINTS = {
|
|
|
240
240
|
-- Throttle re-ready calls per proxyId so a brief window of unknownInstance
|
|
241
241
|
-- polls doesn't cause a re-register stampede.
|
|
242
242
|
local lastReadyByProxy = {}
|
|
243
|
-
local
|
|
244
|
-
local
|
|
243
|
+
local completedProxyRequests = {}
|
|
244
|
+
local completedProxyRequestOrder = {}
|
|
245
|
+
local COMPLETED_PROXY_REQUEST_LIMIT = 500
|
|
246
|
+
local function rememberCompletedProxyRequest(requestId, response)
|
|
247
|
+
local _requestId = requestId
|
|
248
|
+
local _arg1 = {
|
|
249
|
+
response = response,
|
|
250
|
+
}
|
|
251
|
+
completedProxyRequests[_requestId] = _arg1
|
|
252
|
+
local _requestId_1 = requestId
|
|
253
|
+
table.insert(completedProxyRequestOrder, _requestId_1)
|
|
254
|
+
if #completedProxyRequestOrder > COMPLETED_PROXY_REQUEST_LIMIT then
|
|
255
|
+
local _arg0 = table.remove(completedProxyRequestOrder, 1)
|
|
256
|
+
completedProxyRequests[_arg0] = nil
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
local postJson, formatPostJsonFailure
|
|
260
|
+
local function sendProxyResponse(requestId, response, proxy, fence)
|
|
261
|
+
local _object = {
|
|
262
|
+
requestId = requestId,
|
|
263
|
+
pluginSessionId = proxy.pluginSessionId,
|
|
264
|
+
response = response,
|
|
265
|
+
}
|
|
266
|
+
for _k, _v in fence do
|
|
267
|
+
_object[_k] = _v
|
|
268
|
+
end
|
|
269
|
+
local ok, result = postJson("/response", _object, proxy.sessionToken)
|
|
270
|
+
if not ok or not result or not result.Success then
|
|
271
|
+
warn(`[BloxForge] proxy response delivery failed: {formatPostJsonFailure("/response", ok, result)}`)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
local function reRegisterProxy(proxy, role)
|
|
275
|
+
local proxyId = proxy.pluginSessionId
|
|
245
276
|
local now = tick()
|
|
246
|
-
local
|
|
247
|
-
local _condition = lastReadyByProxy[_proxyId]
|
|
277
|
+
local _condition = lastReadyByProxy[proxyId]
|
|
248
278
|
if _condition == nil then
|
|
249
279
|
_condition = 0
|
|
250
280
|
end
|
|
@@ -252,10 +282,9 @@ local function reRegisterProxy(proxyId, role)
|
|
|
252
282
|
if now - last < 2 then
|
|
253
283
|
return nil
|
|
254
284
|
end
|
|
255
|
-
|
|
256
|
-
lastReadyByProxy[_proxyId_1] = now
|
|
285
|
+
lastReadyByProxy[proxyId] = now
|
|
257
286
|
pcall(function()
|
|
258
|
-
|
|
287
|
+
local ok, result = postJson("/ready", {
|
|
259
288
|
pluginSessionId = proxyId,
|
|
260
289
|
instanceId = computeInstanceId(),
|
|
261
290
|
role = role,
|
|
@@ -267,6 +296,12 @@ local function reRegisterProxy(proxyId, role)
|
|
|
267
296
|
pluginVariant = State.PLUGIN_VARIANT,
|
|
268
297
|
protocolVersion = State.PROTOCOL_VERSION,
|
|
269
298
|
})
|
|
299
|
+
if ok and result and result.Success then
|
|
300
|
+
local body = HttpService:JSONDecode(result.Body)
|
|
301
|
+
if body.sessionToken ~= nil then
|
|
302
|
+
proxy.sessionToken = body.sessionToken
|
|
303
|
+
end
|
|
304
|
+
end
|
|
270
305
|
end)
|
|
271
306
|
end
|
|
272
307
|
local function forkRole()
|
|
@@ -278,19 +313,23 @@ local function forkRole()
|
|
|
278
313
|
end
|
|
279
314
|
return "client"
|
|
280
315
|
end
|
|
281
|
-
function postJson(endpoint, body)
|
|
316
|
+
function postJson(endpoint, body, sessionToken)
|
|
317
|
+
local headers = {
|
|
318
|
+
["Content-Type"] = "application/json",
|
|
319
|
+
}
|
|
320
|
+
if sessionToken ~= nil and sessionToken ~= "" then
|
|
321
|
+
headers.Authorization = `Bearer {sessionToken}`
|
|
322
|
+
end
|
|
282
323
|
return pcall(function()
|
|
283
324
|
return HttpService:RequestAsync({
|
|
284
325
|
Url = `{mcpUrl}{endpoint}`,
|
|
285
326
|
Method = "POST",
|
|
286
|
-
Headers =
|
|
287
|
-
["Content-Type"] = "application/json",
|
|
288
|
-
},
|
|
327
|
+
Headers = headers,
|
|
289
328
|
Body = HttpService:JSONEncode(body),
|
|
290
329
|
})
|
|
291
330
|
end)
|
|
292
331
|
end
|
|
293
|
-
|
|
332
|
+
function formatPostJsonFailure(endpoint, ok, res)
|
|
294
333
|
return HttpDiagnostics.formatRequestFailure(`{mcpUrl}{endpoint}`, ok, res)
|
|
295
334
|
end
|
|
296
335
|
local function setServerUrl(serverUrl)
|
|
@@ -471,7 +510,7 @@ local function unregisterProxy(player, entry)
|
|
|
471
510
|
proxyRegisterFailuresByPlayer[_player_1] = nil
|
|
472
511
|
postJson("/disconnect", {
|
|
473
512
|
pluginSessionId = proxy.pluginSessionId,
|
|
474
|
-
})
|
|
513
|
+
}, proxy.sessionToken)
|
|
475
514
|
end
|
|
476
515
|
local function disconnectAllProxies()
|
|
477
516
|
for player, entry in proxyByPlayer do
|
|
@@ -480,7 +519,8 @@ local function disconnectAllProxies()
|
|
|
480
519
|
table.clear(proxyByPlayer)
|
|
481
520
|
table.clear(proxyRegisterFailuresByPlayer)
|
|
482
521
|
end
|
|
483
|
-
local function pollProxy(
|
|
522
|
+
local function pollProxy(proxy, player, rf)
|
|
523
|
+
local proxyId = proxy.pluginSessionId
|
|
484
524
|
while true do
|
|
485
525
|
local _condition = player.Parent ~= nil
|
|
486
526
|
if _condition then
|
|
@@ -498,11 +538,17 @@ local function pollProxy(proxyId, player, rf)
|
|
|
498
538
|
return HttpService:RequestAsync({
|
|
499
539
|
Url = `{mcpUrl}/poll?pluginSessionId={proxyId}`,
|
|
500
540
|
Method = "GET",
|
|
501
|
-
Headers = {
|
|
541
|
+
Headers = if proxy.sessionToken ~= "" then {
|
|
542
|
+
["Content-Type"] = "application/json",
|
|
543
|
+
Authorization = `Bearer {proxy.sessionToken}`,
|
|
544
|
+
} else {
|
|
502
545
|
["Content-Type"] = "application/json",
|
|
503
546
|
},
|
|
504
547
|
})
|
|
505
548
|
end)
|
|
549
|
+
if ok and res and res.StatusCode == 401 then
|
|
550
|
+
reRegisterProxy(proxy, "client")
|
|
551
|
+
end
|
|
506
552
|
if ok and res and (res.Success or res.StatusCode == 503) then
|
|
507
553
|
local okJson, body = pcall(function()
|
|
508
554
|
return HttpService:JSONDecode(res.Body)
|
|
@@ -511,9 +557,41 @@ local function pollProxy(proxyId, player, rf)
|
|
|
511
557
|
-- Server lost our proxy registration (process restart, etc.) -
|
|
512
558
|
-- re-register so the next poll cycle starts routing again.
|
|
513
559
|
if body.knownInstance == false then
|
|
514
|
-
reRegisterProxy(
|
|
560
|
+
reRegisterProxy(proxy, "client")
|
|
515
561
|
end
|
|
516
562
|
if body.request and body.requestId ~= nil then
|
|
563
|
+
local _value = body.serverEpoch
|
|
564
|
+
local _condition_1 = not (_value ~= "" and _value)
|
|
565
|
+
if not _condition_1 then
|
|
566
|
+
_condition_1 = body.deliveryAttempt == nil
|
|
567
|
+
if not _condition_1 then
|
|
568
|
+
local _value_1 = body.leaseToken
|
|
569
|
+
_condition_1 = not (_value_1 ~= "" and _value_1)
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
if _condition_1 then
|
|
573
|
+
continue
|
|
574
|
+
end
|
|
575
|
+
local fence = {
|
|
576
|
+
serverEpoch = body.serverEpoch,
|
|
577
|
+
deliveryAttempt = body.deliveryAttempt,
|
|
578
|
+
leaseToken = body.leaseToken,
|
|
579
|
+
}
|
|
580
|
+
local _requestId = body.requestId
|
|
581
|
+
local cached = completedProxyRequests[_requestId]
|
|
582
|
+
if cached ~= nil then
|
|
583
|
+
sendProxyResponse(body.requestId, cached.response, proxy, fence)
|
|
584
|
+
task.wait(0.5)
|
|
585
|
+
continue
|
|
586
|
+
end
|
|
587
|
+
local _object = {
|
|
588
|
+
requestId = body.requestId,
|
|
589
|
+
pluginSessionId = proxyId,
|
|
590
|
+
}
|
|
591
|
+
for _k, _v in fence do
|
|
592
|
+
_object[_k] = _v
|
|
593
|
+
end
|
|
594
|
+
postJson("/ack", _object, proxy.sessionToken)
|
|
517
595
|
local request = body.request
|
|
518
596
|
local response
|
|
519
597
|
local _endpoint = request.endpoint
|
|
@@ -547,10 +625,8 @@ local function pollProxy(proxyId, player, rf)
|
|
|
547
625
|
error = `Client-proxy does not forward {tostring(request.endpoint)}. ` .. `Allowed: {table.concat(allowed, ", ")}.`,
|
|
548
626
|
}
|
|
549
627
|
end
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
response = response,
|
|
553
|
-
})
|
|
628
|
+
rememberCompletedProxyRequest(body.requestId, response)
|
|
629
|
+
sendProxyResponse(body.requestId, response, proxy, fence)
|
|
554
630
|
end
|
|
555
631
|
end
|
|
556
632
|
end
|
|
@@ -587,19 +663,26 @@ local function registerProxy(player, rf)
|
|
|
587
663
|
_condition = "client"
|
|
588
664
|
end
|
|
589
665
|
local assigned = _condition
|
|
590
|
-
local
|
|
591
|
-
local _arg1 = {
|
|
666
|
+
local _object = {
|
|
592
667
|
pluginSessionId = proxyId,
|
|
593
668
|
role = assigned,
|
|
594
669
|
}
|
|
595
|
-
|
|
670
|
+
local _left = "sessionToken"
|
|
671
|
+
local _condition_1 = body.sessionToken
|
|
672
|
+
if _condition_1 == nil then
|
|
673
|
+
_condition_1 = ""
|
|
674
|
+
end
|
|
675
|
+
_object[_left] = _condition_1
|
|
676
|
+
local entry = _object
|
|
677
|
+
local _player_1 = player
|
|
678
|
+
proxyByPlayer[_player_1] = entry
|
|
596
679
|
local _player_2 = player
|
|
597
680
|
if proxyRegisterFailuresByPlayer[_player_2] ~= nil then
|
|
598
681
|
local _player_3 = player
|
|
599
682
|
proxyRegisterFailuresByPlayer[_player_3] = nil
|
|
600
683
|
print(`[BloxForge] proxy registered for {player.Name} as {assigned} via {mcpUrl}`)
|
|
601
684
|
end
|
|
602
|
-
task.spawn(pollProxy,
|
|
685
|
+
task.spawn(pollProxy, entry, player, rf)
|
|
603
686
|
end
|
|
604
687
|
-- (Removed: startEditProxyLoop. The play-server DM no longer registers an
|
|
605
688
|
-- "edit-proxy" peer with the MCP server. stop_playtest now uses a cross-DM
|
|
@@ -755,6 +838,20 @@ local function detectRole()
|
|
|
755
838
|
return "client"
|
|
756
839
|
end
|
|
757
840
|
local initialRole = detectRole()
|
|
841
|
+
local activeRequests = {}
|
|
842
|
+
local completedRequests = {}
|
|
843
|
+
local completedRequestOrder = {}
|
|
844
|
+
local COMPLETED_REQUEST_LIMIT = 500
|
|
845
|
+
local syncThreadRequests = {}
|
|
846
|
+
local cancelledSyncRequests = {}
|
|
847
|
+
local function isCancelledForThread(co)
|
|
848
|
+
local _co = co
|
|
849
|
+
local reqId = syncThreadRequests[_co]
|
|
850
|
+
if reqId == nil then
|
|
851
|
+
return false
|
|
852
|
+
end
|
|
853
|
+
return cancelledSyncRequests[reqId] ~= nil
|
|
854
|
+
end
|
|
758
855
|
local routeMap = {
|
|
759
856
|
["/api/file-tree"] = QueryHandlers.getFileTree,
|
|
760
857
|
["/api/search-files"] = QueryHandlers.searchFiles,
|
|
@@ -843,19 +940,119 @@ local function processRequest(request)
|
|
|
843
940
|
}
|
|
844
941
|
end
|
|
845
942
|
end
|
|
846
|
-
local function sendResponse(conn, requestId, responseData)
|
|
847
|
-
|
|
848
|
-
|
|
943
|
+
local function sendResponse(conn, requestId, responseData, serverEpoch, deliveryAttempt, leaseToken)
|
|
944
|
+
local body = {
|
|
945
|
+
requestId = requestId,
|
|
946
|
+
response = responseData,
|
|
947
|
+
}
|
|
948
|
+
if serverEpoch ~= nil then
|
|
949
|
+
body.serverEpoch = serverEpoch
|
|
950
|
+
end
|
|
951
|
+
if deliveryAttempt ~= nil then
|
|
952
|
+
body.deliveryAttempt = deliveryAttempt
|
|
953
|
+
end
|
|
954
|
+
if leaseToken ~= nil then
|
|
955
|
+
body.leaseToken = leaseToken
|
|
956
|
+
end
|
|
957
|
+
local ok, result = pcall(function()
|
|
958
|
+
local _object = {
|
|
849
959
|
Url = `{conn.serverUrl}/response`,
|
|
850
960
|
Method = "POST",
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
961
|
+
}
|
|
962
|
+
local _left = "Headers"
|
|
963
|
+
local _object_1 = {
|
|
964
|
+
["Content-Type"] = "application/json",
|
|
965
|
+
}
|
|
966
|
+
local _value = conn.sessionToken
|
|
967
|
+
for _k, _v in (if _value ~= "" and _value then {
|
|
968
|
+
Authorization = `Bearer {conn.sessionToken}`,
|
|
969
|
+
} else {}) do
|
|
970
|
+
_object_1[_k] = _v
|
|
971
|
+
end
|
|
972
|
+
_object[_left] = _object_1
|
|
973
|
+
_object.Body = HttpService:JSONEncode(body)
|
|
974
|
+
return HttpService:RequestAsync(_object)
|
|
975
|
+
end)
|
|
976
|
+
if not ok then
|
|
977
|
+
warn(`[BloxForge] Plugin response serialization failed for {requestId}: {tostring(result)}`)
|
|
978
|
+
elseif not result.Success then
|
|
979
|
+
warn(`[BloxForge] Failed to deliver response for {requestId}: HTTP {result.StatusCode}`)
|
|
980
|
+
end
|
|
981
|
+
end
|
|
982
|
+
local function handleRequestOnce(conn, requestId, request, serverEpoch, deliveryAttempt, leaseToken)
|
|
983
|
+
-- If we've already completed this request, just re-deliver the cached response
|
|
984
|
+
local _requestId = requestId
|
|
985
|
+
local completed = completedRequests[_requestId]
|
|
986
|
+
if completed then
|
|
987
|
+
sendResponse(conn, requestId, completed.response, serverEpoch, deliveryAttempt, leaseToken)
|
|
988
|
+
return nil
|
|
989
|
+
end
|
|
990
|
+
-- If it's actively being processed, do nothing. The running thread will deliver
|
|
991
|
+
-- the response when it finishes.
|
|
992
|
+
local _requestId_1 = requestId
|
|
993
|
+
if activeRequests[_requestId_1] ~= nil then
|
|
994
|
+
return nil
|
|
995
|
+
end
|
|
996
|
+
-- Acknowledge the fenced request so the server knows we've started work.
|
|
997
|
+
-- This prevents the lease from expiring while we're executing long-running operations.
|
|
998
|
+
if serverEpoch ~= nil and deliveryAttempt ~= nil and leaseToken ~= nil then
|
|
999
|
+
task.spawn(function()
|
|
1000
|
+
pcall(function()
|
|
1001
|
+
local _object = {
|
|
1002
|
+
Url = `{conn.serverUrl}/ack`,
|
|
1003
|
+
Method = "POST",
|
|
1004
|
+
}
|
|
1005
|
+
local _left = "Headers"
|
|
1006
|
+
local _object_1 = {
|
|
1007
|
+
["Content-Type"] = "application/json",
|
|
1008
|
+
}
|
|
1009
|
+
local _value = conn.sessionToken
|
|
1010
|
+
for _k, _v in (if _value ~= "" and _value then {
|
|
1011
|
+
Authorization = `Bearer {conn.sessionToken}`,
|
|
1012
|
+
} else {}) do
|
|
1013
|
+
_object_1[_k] = _v
|
|
1014
|
+
end
|
|
1015
|
+
_object[_left] = _object_1
|
|
1016
|
+
_object.Body = HttpService:JSONEncode({
|
|
1017
|
+
pluginSessionId = pluginSessionId,
|
|
1018
|
+
requestId = requestId,
|
|
1019
|
+
serverEpoch = serverEpoch,
|
|
1020
|
+
deliveryAttempt = deliveryAttempt,
|
|
1021
|
+
leaseToken = leaseToken,
|
|
1022
|
+
})
|
|
1023
|
+
HttpService:RequestAsync(_object)
|
|
1024
|
+
end)
|
|
1025
|
+
end)
|
|
1026
|
+
end
|
|
1027
|
+
local _requestId_2 = requestId
|
|
1028
|
+
activeRequests[_requestId_2] = true
|
|
1029
|
+
task.spawn(function()
|
|
1030
|
+
local co = coroutine.running()
|
|
1031
|
+
local _requestId_3 = requestId
|
|
1032
|
+
syncThreadRequests[co] = _requestId_3
|
|
1033
|
+
local ok, result = pcall(function()
|
|
1034
|
+
return processRequest(request)
|
|
1035
|
+
end)
|
|
1036
|
+
syncThreadRequests[co] = nil
|
|
1037
|
+
local response = if ok then result else {
|
|
1038
|
+
error = tostring(result),
|
|
1039
|
+
}
|
|
1040
|
+
local _requestId_4 = requestId
|
|
1041
|
+
activeRequests[_requestId_4] = nil
|
|
1042
|
+
if #completedRequestOrder >= COMPLETED_REQUEST_LIMIT then
|
|
1043
|
+
local oldest = table.remove(completedRequestOrder, 1)
|
|
1044
|
+
if oldest ~= nil then
|
|
1045
|
+
completedRequests[oldest] = nil
|
|
1046
|
+
end
|
|
1047
|
+
end
|
|
1048
|
+
local _requestId_5 = requestId
|
|
1049
|
+
local _arg1 = {
|
|
1050
|
+
response = response,
|
|
1051
|
+
}
|
|
1052
|
+
completedRequests[_requestId_5] = _arg1
|
|
1053
|
+
local _requestId_6 = requestId
|
|
1054
|
+
table.insert(completedRequestOrder, _requestId_6)
|
|
1055
|
+
sendResponse(conn, requestId, response, serverEpoch, deliveryAttempt, leaseToken)
|
|
859
1056
|
end)
|
|
860
1057
|
end
|
|
861
1058
|
local function getConnectionStatus(connIndex)
|
|
@@ -971,8 +1168,12 @@ function sendReady(conn)
|
|
|
971
1168
|
local parseOk, readyData = pcall(function()
|
|
972
1169
|
return HttpService:JSONDecode(readyResult.Body)
|
|
973
1170
|
end)
|
|
974
|
-
local _value = parseOk and readyData.
|
|
1171
|
+
local _value = parseOk and readyData.sessionToken
|
|
975
1172
|
if _value ~= "" and _value then
|
|
1173
|
+
conn.sessionToken = readyData.sessionToken
|
|
1174
|
+
end
|
|
1175
|
+
local _value_1 = parseOk and readyData.assignedRole
|
|
1176
|
+
if _value_1 ~= "" and _value_1 then
|
|
976
1177
|
assignedRole = readyData.assignedRole
|
|
977
1178
|
end
|
|
978
1179
|
local _condition = parseOk
|
|
@@ -984,12 +1185,51 @@ function sendReady(conn)
|
|
|
984
1185
|
end
|
|
985
1186
|
end
|
|
986
1187
|
lastReadyInstanceId = if _condition then readyData.instanceId else instanceId
|
|
1188
|
+
local _condition_1 = parseOk
|
|
1189
|
+
if _condition_1 then
|
|
1190
|
+
_condition_1 = readyData.serverEpoch
|
|
1191
|
+
if _condition_1 ~= "" and _condition_1 then
|
|
1192
|
+
-- ▼ ReadonlyMap.size ▼
|
|
1193
|
+
local _size = 0
|
|
1194
|
+
for _ in completedRequests do
|
|
1195
|
+
_size += 1
|
|
1196
|
+
end
|
|
1197
|
+
-- ▲ ReadonlyMap.size ▲
|
|
1198
|
+
_condition_1 = _size > 0
|
|
1199
|
+
end
|
|
1200
|
+
end
|
|
1201
|
+
if _condition_1 ~= "" and _condition_1 then
|
|
1202
|
+
local receipts = {}
|
|
1203
|
+
for id, _ in completedRequests do
|
|
1204
|
+
local _arg0 = {
|
|
1205
|
+
requestId = id,
|
|
1206
|
+
completedAt = tick() * 1000,
|
|
1207
|
+
}
|
|
1208
|
+
table.insert(receipts, _arg0)
|
|
1209
|
+
end
|
|
1210
|
+
task.spawn(function()
|
|
1211
|
+
pcall(function()
|
|
1212
|
+
return HttpService:RequestAsync({
|
|
1213
|
+
Url = `{conn.serverUrl}/reconcile`,
|
|
1214
|
+
Method = "POST",
|
|
1215
|
+
Headers = {
|
|
1216
|
+
["Content-Type"] = "application/json",
|
|
1217
|
+
},
|
|
1218
|
+
Body = HttpService:JSONEncode({
|
|
1219
|
+
pluginSessionId = pluginSessionId,
|
|
1220
|
+
serverEpoch = readyData.serverEpoch,
|
|
1221
|
+
receipts = receipts,
|
|
1222
|
+
}),
|
|
1223
|
+
})
|
|
1224
|
+
end)
|
|
1225
|
+
end)
|
|
1226
|
+
end
|
|
987
1227
|
ServerUrlSettings.rememberServerUrl(conn.serverUrl)
|
|
988
|
-
local
|
|
989
|
-
if
|
|
990
|
-
|
|
1228
|
+
local _condition_2 = assignedRole
|
|
1229
|
+
if _condition_2 == nil then
|
|
1230
|
+
_condition_2 = detectRole()
|
|
991
1231
|
end
|
|
992
|
-
local connectedRole =
|
|
1232
|
+
local connectedRole = _condition_2
|
|
993
1233
|
if readyFailureLogKeys[readyLogKey] ~= nil then
|
|
994
1234
|
readyFailureLogKeys[readyLogKey] = nil
|
|
995
1235
|
print(`[BloxForge] /ready connected for {instanceId}/{connectedRole} via {conn.serverUrl}`)
|
|
@@ -1000,30 +1240,53 @@ local function streamUrl(serverUrl)
|
|
|
1000
1240
|
local websocketUrl = string.gsub(serverUrl, "^http", "ws")
|
|
1001
1241
|
return `{websocketUrl}/stream?pluginSessionId={pluginSessionId}`
|
|
1002
1242
|
end
|
|
1003
|
-
local function sendStreamResponse(conn, requestId, response)
|
|
1243
|
+
local function sendStreamResponse(conn, requestId, response, serverEpoch, deliveryAttempt, leaseToken)
|
|
1004
1244
|
if not conn.streamOpen or not conn.streamClient then
|
|
1005
1245
|
return nil
|
|
1006
1246
|
end
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1247
|
+
local body = {
|
|
1248
|
+
type = "response",
|
|
1249
|
+
requestId = requestId,
|
|
1250
|
+
response = response,
|
|
1251
|
+
}
|
|
1252
|
+
if serverEpoch ~= nil then
|
|
1253
|
+
body.serverEpoch = serverEpoch
|
|
1254
|
+
end
|
|
1255
|
+
if deliveryAttempt ~= nil then
|
|
1256
|
+
body.deliveryAttempt = deliveryAttempt
|
|
1257
|
+
end
|
|
1258
|
+
if leaseToken ~= nil then
|
|
1259
|
+
body.leaseToken = leaseToken
|
|
1260
|
+
end
|
|
1261
|
+
local ok, result = pcall(function()
|
|
1262
|
+
return conn.streamClient:Send(HttpService:JSONEncode(body))
|
|
1013
1263
|
end)
|
|
1264
|
+
if not ok then
|
|
1265
|
+
warn(`[BloxForge] Failed to send stream response for {requestId}: {tostring(result)}`)
|
|
1266
|
+
end
|
|
1014
1267
|
end
|
|
1015
1268
|
local function startRequestStream(conn)
|
|
1016
1269
|
if not conn.isActive or conn.streamClient then
|
|
1017
1270
|
return nil
|
|
1018
1271
|
end
|
|
1019
1272
|
local ok, stream = pcall(function()
|
|
1020
|
-
|
|
1273
|
+
local _exp = Enum.WebStreamClientType.WebSocket
|
|
1274
|
+
local _object = {
|
|
1021
1275
|
Url = streamUrl(conn.serverUrl),
|
|
1022
1276
|
Method = "GET",
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1277
|
+
}
|
|
1278
|
+
local _left = "Headers"
|
|
1279
|
+
local _object_1 = {
|
|
1280
|
+
["Content-Type"] = "application/json",
|
|
1281
|
+
}
|
|
1282
|
+
local _value = conn.sessionToken
|
|
1283
|
+
for _k, _v in (if _value ~= "" and _value then {
|
|
1284
|
+
Authorization = `Bearer {conn.sessionToken}`,
|
|
1285
|
+
} else {}) do
|
|
1286
|
+
_object_1[_k] = _v
|
|
1287
|
+
end
|
|
1288
|
+
_object[_left] = _object_1
|
|
1289
|
+
return HttpService:CreateWebStreamClient(_exp, _object)
|
|
1027
1290
|
end)
|
|
1028
1291
|
if not ok or not stream then
|
|
1029
1292
|
return nil
|
|
@@ -1055,14 +1318,7 @@ local function startRequestStream(conn)
|
|
|
1055
1318
|
if _condition then
|
|
1056
1319
|
return nil
|
|
1057
1320
|
end
|
|
1058
|
-
|
|
1059
|
-
local handled, response = pcall(function()
|
|
1060
|
-
return processRequest(frame.request)
|
|
1061
|
-
end)
|
|
1062
|
-
sendStreamResponse(conn, frame.requestId, if handled then response else {
|
|
1063
|
-
error = tostring(response),
|
|
1064
|
-
})
|
|
1065
|
-
end)
|
|
1321
|
+
handleRequestOnce(conn, frame.requestId, frame.request, frame.serverEpoch, frame.deliveryAttempt, frame.leaseToken)
|
|
1066
1322
|
end)
|
|
1067
1323
|
local close = function()
|
|
1068
1324
|
if conn.streamClient ~= stream then
|
|
@@ -1085,13 +1341,22 @@ local function pollForRequests(connIndex)
|
|
|
1085
1341
|
end
|
|
1086
1342
|
conn.isPolling = true
|
|
1087
1343
|
local success, result = pcall(function()
|
|
1088
|
-
|
|
1344
|
+
local _object = {
|
|
1089
1345
|
Url = `{conn.serverUrl}/poll?pluginSessionId={pluginSessionId}`,
|
|
1090
1346
|
Method = "GET",
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1347
|
+
}
|
|
1348
|
+
local _left = "Headers"
|
|
1349
|
+
local _object_1 = {
|
|
1350
|
+
["Content-Type"] = "application/json",
|
|
1351
|
+
}
|
|
1352
|
+
local _value = conn.sessionToken
|
|
1353
|
+
for _k, _v in (if _value ~= "" and _value then {
|
|
1354
|
+
Authorization = `Bearer {conn.sessionToken}`,
|
|
1355
|
+
} else {}) do
|
|
1356
|
+
_object_1[_k] = _v
|
|
1357
|
+
end
|
|
1358
|
+
_object[_left] = _object_1
|
|
1359
|
+
return HttpService:RequestAsync(_object)
|
|
1095
1360
|
end)
|
|
1096
1361
|
conn.isPolling = false
|
|
1097
1362
|
local ui = UI.getElements()
|
|
@@ -1226,19 +1491,13 @@ local function pollForRequests(connIndex)
|
|
|
1226
1491
|
UI.startPulseAnimation()
|
|
1227
1492
|
end
|
|
1228
1493
|
end
|
|
1494
|
+
if data.cancellations then
|
|
1495
|
+
for _, c in data.cancellations do
|
|
1496
|
+
cancelledSyncRequests[c] = true
|
|
1497
|
+
end
|
|
1498
|
+
end
|
|
1229
1499
|
if data.request and mcpConnected then
|
|
1230
|
-
|
|
1231
|
-
local ok, response = pcall(function()
|
|
1232
|
-
return processRequest(data.request)
|
|
1233
|
-
end)
|
|
1234
|
-
if ok then
|
|
1235
|
-
sendResponse(conn, data.requestId, response)
|
|
1236
|
-
else
|
|
1237
|
-
sendResponse(conn, data.requestId, {
|
|
1238
|
-
error = tostring(response),
|
|
1239
|
-
})
|
|
1240
|
-
end
|
|
1241
|
-
end)
|
|
1500
|
+
handleRequestOnce(conn, data.requestId, data.request, data.serverEpoch, data.deliveryAttempt, data.leaseToken)
|
|
1242
1501
|
end
|
|
1243
1502
|
elseif conn.isActive then
|
|
1244
1503
|
conn.consecutiveFailures += 1
|
|
@@ -1489,6 +1748,7 @@ local function checkForUpdates()
|
|
|
1489
1748
|
end
|
|
1490
1749
|
return {
|
|
1491
1750
|
getConnectionStatus = getConnectionStatus,
|
|
1751
|
+
isCancelledForThread = isCancelledForThread,
|
|
1492
1752
|
activatePlugin = activatePlugin,
|
|
1493
1753
|
deactivatePlugin = deactivatePlugin,
|
|
1494
1754
|
deactivateAll = deactivateAll,
|
|
@@ -1617,9 +1877,9 @@ local function computeBridgeStamp()
|
|
|
1617
1877
|
for i = 1, #combined do
|
|
1618
1878
|
h = (h * 33 + (string.byte(combined, i))) % 2147483647
|
|
1619
1879
|
end
|
|
1620
|
-
-- "3.0.0
|
|
1880
|
+
-- "3.0.0" is replaced with the package version at package time
|
|
1621
1881
|
-- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
|
|
1622
|
-
return `{tostring(h)}-3.0.0
|
|
1882
|
+
return `{tostring(h)}-3.0.0`
|
|
1623
1883
|
end
|
|
1624
1884
|
local BRIDGE_STAMP = computeBridgeStamp()
|
|
1625
1885
|
local function setSource(scriptInst, source)
|
|
@@ -4300,6 +4560,7 @@ local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
|
|
|
4300
4560
|
-- discard its result on completion.
|
|
4301
4561
|
local LuauExec = TS.import(script, script.Parent.Parent, "LuauExec")
|
|
4302
4562
|
local JobRegistry = TS.import(script, script.Parent.Parent, "JobRegistry")
|
|
4563
|
+
local Communication = TS.import(script, script.Parent.Parent, "Communication")
|
|
4303
4564
|
-- Install a sanctioned progress/cancel API once. Server-generated long-running
|
|
4304
4565
|
-- Luau (terrain/template/batch builders) can call _G.__mcp.progress(done,total,msg)
|
|
4305
4566
|
-- and _G.__mcp.checkCancelled() to report progress and bail early. We do NOT
|
|
@@ -4313,7 +4574,7 @@ local function installMcpGlobal()
|
|
|
4313
4574
|
JobRegistry.reportProgress(coroutine.running(), done, total, message, stage)
|
|
4314
4575
|
end,
|
|
4315
4576
|
checkCancelled = function()
|
|
4316
|
-
return JobRegistry.isCancelledForThread(coroutine.running())
|
|
4577
|
+
return JobRegistry.isCancelledForThread(coroutine.running()) or Communication.isCancelledForThread(coroutine.running())
|
|
4317
4578
|
end,
|
|
4318
4579
|
}
|
|
4319
4580
|
end
|
|
@@ -10993,6 +11254,36 @@ end
|
|
|
10993
11254
|
local function nowSec()
|
|
10994
11255
|
return DateTime.now().UnixTimestampMillis / 1000
|
|
10995
11256
|
end
|
|
11257
|
+
-- Studio can expose binary-bearing Output messages that JSONEncode rejects.
|
|
11258
|
+
-- Preserve valid UTF-8 and make only malformed bytes visible and JSON-safe.
|
|
11259
|
+
local function escapeInvalidUtf8(message)
|
|
11260
|
+
local valid = utf8.len(message)
|
|
11261
|
+
if type(valid) == "number" then
|
|
11262
|
+
return message
|
|
11263
|
+
end
|
|
11264
|
+
local parts = {}
|
|
11265
|
+
local cursor = 1
|
|
11266
|
+
while cursor <= #message do
|
|
11267
|
+
local suffixValid, invalidPosition = utf8.len(message, cursor)
|
|
11268
|
+
if type(suffixValid) == "number" then
|
|
11269
|
+
local _arg0 = string.sub(message, cursor)
|
|
11270
|
+
table.insert(parts, _arg0)
|
|
11271
|
+
break
|
|
11272
|
+
end
|
|
11273
|
+
if not (type(invalidPosition) == "number") then
|
|
11274
|
+
break
|
|
11275
|
+
end
|
|
11276
|
+
if invalidPosition > cursor then
|
|
11277
|
+
local _arg0 = string.sub(message, cursor, invalidPosition - 1)
|
|
11278
|
+
table.insert(parts, _arg0)
|
|
11279
|
+
end
|
|
11280
|
+
local invalidByte = string.byte(message, invalidPosition)
|
|
11281
|
+
local _arg0 = string.format("\\x%02X", invalidByte)
|
|
11282
|
+
table.insert(parts, _arg0)
|
|
11283
|
+
cursor = invalidPosition + 1
|
|
11284
|
+
end
|
|
11285
|
+
return table.concat(parts, "")
|
|
11286
|
+
end
|
|
10996
11287
|
local function dropOldestUntilFits(incomingBytes)
|
|
10997
11288
|
while #entries > 0 and (totalBytes + incomingBytes > MAX_BYTES or #entries >= HARD_ENTRY_CAP) do
|
|
10998
11289
|
local dropped = table.remove(entries, 1)
|
|
@@ -11001,13 +11292,18 @@ local function dropOldestUntilFits(incomingBytes)
|
|
|
11001
11292
|
end
|
|
11002
11293
|
end
|
|
11003
11294
|
local function pushEntry(message, level, ts)
|
|
11004
|
-
local
|
|
11295
|
+
local safeMessage = escapeInvalidUtf8(message)
|
|
11296
|
+
local bytes = #safeMessage
|
|
11297
|
+
if bytes > MAX_BYTES then
|
|
11298
|
+
totalDropped += 1
|
|
11299
|
+
return nil
|
|
11300
|
+
end
|
|
11005
11301
|
dropOldestUntilFits(bytes)
|
|
11006
11302
|
local _arg0 = {
|
|
11007
11303
|
seq = nextSeq,
|
|
11008
11304
|
ts = ts,
|
|
11009
11305
|
level = level,
|
|
11010
|
-
message =
|
|
11306
|
+
message = safeMessage,
|
|
11011
11307
|
}
|
|
11012
11308
|
table.insert(entries, _arg0)
|
|
11013
11309
|
nextSeq += 1
|
|
@@ -11299,9 +11595,9 @@ return {
|
|
|
11299
11595
|
<Properties>
|
|
11300
11596
|
<string name="Name">State</string>
|
|
11301
11597
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
11302
|
-
local CURRENT_VERSION = "3.0.0
|
|
11598
|
+
local CURRENT_VERSION = "3.0.0"
|
|
11303
11599
|
local PLUGIN_VARIANT = "main"
|
|
11304
|
-
local PROTOCOL_VERSION =
|
|
11600
|
+
local PROTOCOL_VERSION = 3
|
|
11305
11601
|
local MAX_CONNECTIONS = 5
|
|
11306
11602
|
local BASE_PORT = 58741
|
|
11307
11603
|
local activeTabIndex = 0
|