@chrrxs/robloxstudio-mcp 3.0.1 → 3.0.2
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 +9353 -9165
- package/package.json +1 -1
- package/studio-plugin/MCPPlugin.rbxmx +71 -60
package/package.json
CHANGED
|
@@ -616,15 +616,33 @@ local function pollProxy(proxyId, player, rf)
|
|
|
616
616
|
unregisterProxy(player)
|
|
617
617
|
break
|
|
618
618
|
end
|
|
619
|
+
local nextPollDelay = 0.5
|
|
619
620
|
local ok, res = pcall(function()
|
|
620
|
-
|
|
621
|
-
Url = `{mcpUrl}/poll?pluginSessionId={proxyId}`,
|
|
621
|
+
local requestOptions = {
|
|
622
|
+
Url = `{mcpUrl}/poll?pluginSessionId={proxyId}&pollMode=long`,
|
|
622
623
|
Method = "GET",
|
|
623
624
|
Headers = {
|
|
624
625
|
["Content-Type"] = "application/json",
|
|
625
626
|
},
|
|
626
|
-
|
|
627
|
+
Timeout = State.POLL_REQUEST_TIMEOUT_SECONDS,
|
|
628
|
+
}
|
|
629
|
+
return HttpService:RequestAsync(requestOptions)
|
|
627
630
|
end)
|
|
631
|
+
-- RequestAsync may yield for the full long-poll window. Never apply a
|
|
632
|
+
-- stale response to a player whose proxy was removed or replaced.
|
|
633
|
+
local _player = player
|
|
634
|
+
local currentProxy = proxyByPlayer[_player]
|
|
635
|
+
local _condition_1 = player.Parent == nil
|
|
636
|
+
if not _condition_1 then
|
|
637
|
+
local _result = currentProxy
|
|
638
|
+
if _result ~= nil then
|
|
639
|
+
_result = _result.pluginSessionId
|
|
640
|
+
end
|
|
641
|
+
_condition_1 = _result ~= proxyId
|
|
642
|
+
end
|
|
643
|
+
if _condition_1 then
|
|
644
|
+
break
|
|
645
|
+
end
|
|
628
646
|
if ok and res and (res.Success or res.StatusCode == 503) then
|
|
629
647
|
local okJson, body = pcall(function()
|
|
630
648
|
return HttpService:JSONDecode(res.Body)
|
|
@@ -674,9 +692,17 @@ local function pollProxy(proxyId, player, rf)
|
|
|
674
692
|
response = response,
|
|
675
693
|
})
|
|
676
694
|
end
|
|
695
|
+
if res.Success and body.pollMode == "long" and body.knownInstance ~= false and body.request == nil then
|
|
696
|
+
nextPollDelay = 0
|
|
697
|
+
end
|
|
677
698
|
end
|
|
678
699
|
end
|
|
679
|
-
|
|
700
|
+
if nextPollDelay > 0 then
|
|
701
|
+
task.wait(nextPollDelay)
|
|
702
|
+
else
|
|
703
|
+
-- Yield one scheduler turn before replacing a completed long poll.
|
|
704
|
+
task.wait()
|
|
705
|
+
end
|
|
680
706
|
end
|
|
681
707
|
end
|
|
682
708
|
local function registerProxy(player, rf)
|
|
@@ -831,6 +857,7 @@ local lastVersionMismatchWarningKey
|
|
|
831
857
|
local lastReadyInstanceId
|
|
832
858
|
local readyFailureLogKeys = {}
|
|
833
859
|
local retryingDuplicateReady = false
|
|
860
|
+
local pollEpoch = 0
|
|
834
861
|
-- Cache the published place name from MarketplaceService:GetProductInfo so
|
|
835
862
|
-- /ready can carry a friendly identifier (e.g. "Natural Disasters") distinct
|
|
836
863
|
-- from game.Name (the DataModel name, often "Place1" in edit). We only fetch
|
|
@@ -1115,16 +1142,24 @@ local function pollForRequests()
|
|
|
1115
1142
|
if conn.isPolling then
|
|
1116
1143
|
return nil
|
|
1117
1144
|
end
|
|
1145
|
+
local epoch = pollEpoch
|
|
1118
1146
|
conn.isPolling = true
|
|
1119
1147
|
local success, result = pcall(function()
|
|
1120
|
-
|
|
1121
|
-
Url = `{conn.serverUrl}/poll?pluginSessionId={pluginSessionId}`,
|
|
1148
|
+
local requestOptions = {
|
|
1149
|
+
Url = `{conn.serverUrl}/poll?pluginSessionId={pluginSessionId}&pollMode=long`,
|
|
1122
1150
|
Method = "GET",
|
|
1123
1151
|
Headers = {
|
|
1124
1152
|
["Content-Type"] = "application/json",
|
|
1125
1153
|
},
|
|
1126
|
-
|
|
1154
|
+
Timeout = State.POLL_REQUEST_TIMEOUT_SECONDS,
|
|
1155
|
+
}
|
|
1156
|
+
return HttpService:RequestAsync(requestOptions)
|
|
1127
1157
|
end)
|
|
1158
|
+
-- A held request can finish after deactivate/reactivate. Only the current
|
|
1159
|
+
-- lifecycle epoch may mutate connection state or clear a newer poll's lock.
|
|
1160
|
+
if epoch ~= pollEpoch or not conn.isActive then
|
|
1161
|
+
return nil
|
|
1162
|
+
end
|
|
1128
1163
|
conn.isPolling = false
|
|
1129
1164
|
local ui = UI.getElements()
|
|
1130
1165
|
UI.updateToolbarIcon()
|
|
@@ -1133,6 +1168,12 @@ local function pollForRequests()
|
|
|
1133
1168
|
conn.currentRetryDelay = 0.5
|
|
1134
1169
|
conn.lastSuccessfulConnection = tick()
|
|
1135
1170
|
local data = HttpService:JSONDecode(result.Body)
|
|
1171
|
+
conn.lastPoll = tick()
|
|
1172
|
+
if result.Success and data.pollMode == "long" and data.knownInstance ~= false and data.request == nil then
|
|
1173
|
+
-- The server held this request until work or timeout, so immediately
|
|
1174
|
+
-- replace it instead of reintroducing the legacy 500 ms idle gap.
|
|
1175
|
+
conn.lastPoll = 0
|
|
1176
|
+
end
|
|
1136
1177
|
local mcpConnected = data.mcpConnected == true
|
|
1137
1178
|
conn.lastHttpOk = true
|
|
1138
1179
|
conn.lastMcpOk = mcpConnected
|
|
@@ -1223,6 +1264,7 @@ local function pollForRequests()
|
|
|
1223
1264
|
end)
|
|
1224
1265
|
end
|
|
1225
1266
|
elseif conn.isActive then
|
|
1267
|
+
conn.lastPoll = tick()
|
|
1226
1268
|
conn.consecutiveFailures += 1
|
|
1227
1269
|
if conn.consecutiveFailures > 1 then
|
|
1228
1270
|
conn.currentRetryDelay = math.min(conn.currentRetryDelay * conn.retryBackoffMultiplier, conn.maxRetryDelay)
|
|
@@ -1287,6 +1329,9 @@ local deactivatePlugin
|
|
|
1287
1329
|
local function activatePlugin()
|
|
1288
1330
|
local conn = State.getActiveConnection()
|
|
1289
1331
|
local ui = UI.getElements()
|
|
1332
|
+
pollEpoch += 1
|
|
1333
|
+
conn.isPolling = false
|
|
1334
|
+
conn.lastPoll = 0
|
|
1290
1335
|
conn.isActive = true
|
|
1291
1336
|
conn.consecutiveFailures = 0
|
|
1292
1337
|
conn.currentRetryDelay = 0.5
|
|
@@ -1335,6 +1380,8 @@ local function activatePlugin()
|
|
|
1335
1380
|
end
|
|
1336
1381
|
function deactivatePlugin()
|
|
1337
1382
|
local conn = State.getActiveConnection()
|
|
1383
|
+
pollEpoch += 1
|
|
1384
|
+
conn.isPolling = false
|
|
1338
1385
|
conn.isActive = false
|
|
1339
1386
|
conn.lastMcpOk = false
|
|
1340
1387
|
UI.updateUIState()
|
|
@@ -1528,9 +1575,9 @@ local function computeBridgeStamp()
|
|
|
1528
1575
|
for i = 1, #combined do
|
|
1529
1576
|
h = (h * 33 + (string.byte(combined, i))) % 2147483647
|
|
1530
1577
|
end
|
|
1531
|
-
-- "3.0.
|
|
1578
|
+
-- "3.0.2" is replaced with the package version at package time
|
|
1532
1579
|
-- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
|
|
1533
|
-
return `{tostring(h)}-3.0.
|
|
1580
|
+
return `{tostring(h)}-3.0.2`
|
|
1534
1581
|
end
|
|
1535
1582
|
local BRIDGE_STAMP = computeBridgeStamp()
|
|
1536
1583
|
local function setSource(scriptInst, source)
|
|
@@ -3600,11 +3647,14 @@ local function getAttributes(requestData)
|
|
|
3600
3647
|
}
|
|
3601
3648
|
count += 1
|
|
3602
3649
|
end
|
|
3603
|
-
|
|
3650
|
+
local response = {
|
|
3604
3651
|
instancePath = instancePath,
|
|
3605
|
-
attributes = serializedAttributes,
|
|
3606
3652
|
count = count,
|
|
3607
3653
|
}
|
|
3654
|
+
if count > 0 then
|
|
3655
|
+
response.attributes = serializedAttributes
|
|
3656
|
+
end
|
|
3657
|
+
return response
|
|
3608
3658
|
end)
|
|
3609
3659
|
if success then
|
|
3610
3660
|
return result
|
|
@@ -6152,7 +6202,6 @@ local function getProjectStructure(requestData)
|
|
|
6152
6202
|
name = instance.Name,
|
|
6153
6203
|
className = instance.ClassName,
|
|
6154
6204
|
path = getInstancePath(instance),
|
|
6155
|
-
children = {},
|
|
6156
6205
|
}
|
|
6157
6206
|
if instance:IsA("LuaSourceContainer") then
|
|
6158
6207
|
node.hasSource = true
|
|
@@ -6192,7 +6241,7 @@ local function getProjectStructure(requestData)
|
|
|
6192
6241
|
-- ▲ ReadonlyArray.filter ▲
|
|
6193
6242
|
children = _newValue
|
|
6194
6243
|
end
|
|
6195
|
-
local nodeChildren =
|
|
6244
|
+
local nodeChildren = {}
|
|
6196
6245
|
local childCount = #children
|
|
6197
6246
|
if childCount > 20 and depth < maxDepth then
|
|
6198
6247
|
local classGroups = {}
|
|
@@ -6267,6 +6316,9 @@ local function getProjectStructure(requestData)
|
|
|
6267
6316
|
table.insert(nodeChildren, _arg0)
|
|
6268
6317
|
end
|
|
6269
6318
|
end
|
|
6319
|
+
if #nodeChildren > 0 then
|
|
6320
|
+
node.children = nodeChildren
|
|
6321
|
+
end
|
|
6270
6322
|
return node
|
|
6271
6323
|
end
|
|
6272
6324
|
local result = getStructure(startInstance, 0)
|
|
@@ -6819,16 +6871,6 @@ local finishRecording = _binding_1.finishRecording
|
|
|
6819
6871
|
local SOURCE_TRUNCATE_CHAR_BUDGET = 25000
|
|
6820
6872
|
local SOURCE_TRUNCATE_LINE_BUDGET = 400
|
|
6821
6873
|
local SOURCE_TRUNCATE_TO_LINES = 300
|
|
6822
|
-
local function normalizeEscapes(s)
|
|
6823
|
-
local result = s
|
|
6824
|
-
result = (string.gsub(result, "\\\\", "\x01"))
|
|
6825
|
-
result = (string.gsub(result, "\\n", "\n"))
|
|
6826
|
-
result = (string.gsub(result, "\\t", "\t"))
|
|
6827
|
-
result = (string.gsub(result, "\\r", "\r"))
|
|
6828
|
-
result = (string.gsub(result, '\\"', '"'))
|
|
6829
|
-
result = (string.gsub(result, "\x01", "\\"))
|
|
6830
|
-
return result
|
|
6831
|
-
end
|
|
6832
6874
|
local function getTopServiceName(instance)
|
|
6833
6875
|
local topServiceInst = instance
|
|
6834
6876
|
while topServiceInst.Parent and topServiceInst.Parent ~= game do
|
|
@@ -6972,7 +7014,8 @@ local function setScriptSource(requestData)
|
|
|
6972
7014
|
error = `Instance is not a script-like object: {instance.ClassName}`,
|
|
6973
7015
|
}
|
|
6974
7016
|
end
|
|
6975
|
-
|
|
7017
|
+
-- Communication has already JSON-decoded the poll payload; source text is exact at this boundary.
|
|
7018
|
+
local sourceToSet = newSource
|
|
6976
7019
|
local recordingId = beginRecording(`Set script source: {instance.Name}`)
|
|
6977
7020
|
local readSuccess, readResult = pcall(function()
|
|
6978
7021
|
return #readScriptSource(instance)
|
|
@@ -6996,40 +7039,9 @@ local function setScriptSource(requestData)
|
|
|
6996
7039
|
message = `Script source updated successfully ({if applyResult.method == "UpdateSourceAsync" then "editor-safe" else "direct assignment"})`,
|
|
6997
7040
|
}
|
|
6998
7041
|
end
|
|
6999
|
-
local replaceSuccess, replaceResult = pcall(function()
|
|
7000
|
-
local parent = instance.Parent
|
|
7001
|
-
local name = instance.Name
|
|
7002
|
-
local className = instance.ClassName
|
|
7003
|
-
local wasBaseScript = instance:IsA("BaseScript")
|
|
7004
|
-
local enabled = if wasBaseScript then instance.Enabled else nil
|
|
7005
|
-
local newScript = Instance.new(className)
|
|
7006
|
-
newScript.Name = name
|
|
7007
|
-
-- @rbxts/types does not expose PluginSecurity Source writes.
|
|
7008
|
-
local writableNewScript = newScript
|
|
7009
|
-
writableNewScript.Source = sourceToSet
|
|
7010
|
-
if readScriptSource(newScript) ~= sourceToSet then
|
|
7011
|
-
error("Replacement script source did not match the requested source")
|
|
7012
|
-
end
|
|
7013
|
-
if wasBaseScript and enabled ~= nil then
|
|
7014
|
-
local newBaseScript = newScript
|
|
7015
|
-
newBaseScript.Enabled = enabled
|
|
7016
|
-
end
|
|
7017
|
-
newScript.Parent = parent
|
|
7018
|
-
instance:Destroy()
|
|
7019
|
-
return {
|
|
7020
|
-
success = true,
|
|
7021
|
-
instancePath = getInstancePath(newScript),
|
|
7022
|
-
method = "replace",
|
|
7023
|
-
message = "Script replaced successfully with new source",
|
|
7024
|
-
}
|
|
7025
|
-
end)
|
|
7026
|
-
if replaceSuccess then
|
|
7027
|
-
finishRecording(recordingId, true)
|
|
7028
|
-
return replaceResult
|
|
7029
|
-
end
|
|
7030
7042
|
finishRecording(recordingId, false)
|
|
7031
7043
|
return {
|
|
7032
|
-
error = `Failed to set script source
|
|
7044
|
+
error = `Failed to set script source: {applyResult.error}`,
|
|
7033
7045
|
}
|
|
7034
7046
|
end
|
|
7035
7047
|
local function editScriptLines(requestData)
|
|
@@ -7042,8 +7054,6 @@ local function editScriptLines(requestData)
|
|
|
7042
7054
|
error = "Instance path, old_string, and new_string are required",
|
|
7043
7055
|
}
|
|
7044
7056
|
end
|
|
7045
|
-
oldString = normalizeEscapes(oldString)
|
|
7046
|
-
newString = normalizeEscapes(newString)
|
|
7047
7057
|
local instance = getInstanceByPath(instancePath)
|
|
7048
7058
|
if not instance then
|
|
7049
7059
|
return {
|
|
@@ -7140,7 +7150,6 @@ local function insertScriptLines(requestData)
|
|
|
7140
7150
|
error = "Instance path and newContent are required",
|
|
7141
7151
|
}
|
|
7142
7152
|
end
|
|
7143
|
-
newContent = normalizeEscapes(newContent)
|
|
7144
7153
|
local instance = getInstanceByPath(instancePath)
|
|
7145
7154
|
if not instance then
|
|
7146
7155
|
return {
|
|
@@ -9564,9 +9573,10 @@ return {
|
|
|
9564
9573
|
<Properties>
|
|
9565
9574
|
<string name="Name">State</string>
|
|
9566
9575
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
9567
|
-
local CURRENT_VERSION = "3.0.
|
|
9576
|
+
local CURRENT_VERSION = "3.0.2"
|
|
9568
9577
|
local PLUGIN_VARIANT = "main"
|
|
9569
9578
|
local BASE_PORT = 58741
|
|
9579
|
+
local POLL_REQUEST_TIMEOUT_SECONDS = 20
|
|
9570
9580
|
local function createConnection(port)
|
|
9571
9581
|
return {
|
|
9572
9582
|
port = port,
|
|
@@ -9595,6 +9605,7 @@ return {
|
|
|
9595
9605
|
CURRENT_VERSION = CURRENT_VERSION,
|
|
9596
9606
|
PLUGIN_VARIANT = PLUGIN_VARIANT,
|
|
9597
9607
|
BASE_PORT = BASE_PORT,
|
|
9608
|
+
POLL_REQUEST_TIMEOUT_SECONDS = POLL_REQUEST_TIMEOUT_SECONDS,
|
|
9598
9609
|
getActiveConnection = getActiveConnection,
|
|
9599
9610
|
}
|
|
9600
9611
|
]]></string>
|