@el4cteo/rbx-studio-mcp 0.7.2 → 0.7.6
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/bridge/console.js +28 -0
- package/dist/bridge/console.js.map +1 -1
- package/dist/bridge/remote.js +9 -3
- package/dist/bridge/remote.js.map +1 -1
- package/dist/bridge/rpc.js +2 -1
- package/dist/bridge/rpc.js.map +1 -1
- package/dist/lib/timeout.js +13 -0
- package/dist/lib/timeout.js.map +1 -0
- package/dist/tools/debug.js +15 -4
- package/dist/tools/debug.js.map +1 -1
- package/dist/tools/exec.js +13 -6
- package/dist/tools/exec.js.map +1 -1
- package/dist/tools/input.js +8 -1
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/playtest.js +11 -1
- package/dist/tools/playtest.js.map +1 -1
- package/package.json +75 -75
- package/plugin/src/ClientRelay.luau +88 -0
- package/plugin/src/Commands.luau +26 -0
- package/plugin/src/Config.luau +65 -65
- package/plugin/src/ExecRuntime.luau +161 -0
- package/plugin/src/Phrase.luau +3 -0
- package/plugin/src/RemoteTrace.luau +143 -0
- package/plugin/src/handlers/Debug.luau +574 -504
- package/plugin/src/handlers/Exec.luau +36 -161
- package/plugin/src/handlers/Input.luau +54 -75
- package/plugin/src/handlers/Playtest.luau +245 -205
- package/plugin/src/init.server.luau +946 -939
- package/scripts/test-bridge.mjs +37 -0
- package/scripts/test-console.mjs +25 -0
- package/scripts/test-failover.mjs +69 -0
- package/scripts/test-live.mjs +283 -115
- package/scripts/test-plugin.mjs +103 -85
- package/scripts/test-tools.mjs +162 -39
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
-- Passive, bounded RemoteEvent observations shared by the server and client.
|
|
3
|
+
local Paths = require(script.Parent.Paths)
|
|
4
|
+
local Trace = {}
|
|
5
|
+
local MAX_EVENTS = 1000 -- per direction; disconnect at the cap, never keep raw payloads
|
|
6
|
+
local MAX_REMOTES = 128
|
|
7
|
+
local MAX_ROWS = 20
|
|
8
|
+
local MAX_VISITS = 10000
|
|
9
|
+
|
|
10
|
+
local function brief(value: string, limit: number): string
|
|
11
|
+
local clipped = string.sub(value, 1, limit)
|
|
12
|
+
-- ASCII keeps invalid UTF-8, controls and JSON escaping from expanding output.
|
|
13
|
+
clipped = string.gsub(clipped, "[^ -~]", "?")
|
|
14
|
+
return clipped .. (if #value > limit then "..." else "")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
local function pathOf(remote: Instance): string
|
|
18
|
+
local path = Paths.of(remote)
|
|
19
|
+
if #path <= 256 then return path end
|
|
20
|
+
-- Keep Unicode instance names valid at the byte boundary.
|
|
21
|
+
local ok, boundary = pcall(utf8.offset, path, 0, 257)
|
|
22
|
+
return string.sub(path, 1, if ok and boundary then boundary - 1 else 256) .. "..."
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
local function shape(value: any, depth: number): string
|
|
26
|
+
local kind = typeof(value)
|
|
27
|
+
if kind == "string" then return "string:" .. string.format("%q", brief(value, 32)) end
|
|
28
|
+
if kind == "number" or kind == "boolean" or kind == "nil" then return kind .. ":" .. tostring(value) end
|
|
29
|
+
if kind == "Instance" then return "Instance<" .. value.ClassName .. ">" end
|
|
30
|
+
if kind ~= "table" then return kind end
|
|
31
|
+
if depth >= 2 then return "table{...}" end
|
|
32
|
+
local parts = {}
|
|
33
|
+
local key, item = next(value)
|
|
34
|
+
for _ = 1, 4 do
|
|
35
|
+
if key == nil then break end
|
|
36
|
+
local label = if typeof(key) == "string" then brief(key, 20) else typeof(key)
|
|
37
|
+
table.insert(parts, label .. "=" .. shape(item, depth + 1))
|
|
38
|
+
key, item = next(value, key)
|
|
39
|
+
end
|
|
40
|
+
if key ~= nil then table.insert(parts, "...") end
|
|
41
|
+
return brief("table{" .. table.concat(parts, ",") .. "}", 160)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
function Trace.arguments(...: any): string
|
|
45
|
+
local parts = {}
|
|
46
|
+
local count = select("#", ...)
|
|
47
|
+
for index = 1, math.min(count, 6) do
|
|
48
|
+
table.insert(parts, shape(select(index, ...), 0))
|
|
49
|
+
end
|
|
50
|
+
if count > 6 then table.insert(parts, "...") end
|
|
51
|
+
return brief("(" .. table.concat(parts, ", ") .. ")", 192)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
-- Returns an idempotent stop function; its deadline also cleans up if the caller fails.
|
|
55
|
+
function Trace.start(root: Instance, player: Player, client: boolean, seconds: number, excluded: Instance?): () -> { [string]: any }
|
|
56
|
+
local connections: { RBXScriptConnection } = {}
|
|
57
|
+
local watched: { [Instance]: boolean } = {}
|
|
58
|
+
local rows: { any } = {}
|
|
59
|
+
local byRemote: { [Instance]: any } = {}
|
|
60
|
+
local events, remotes, visits = 0, 0, 0
|
|
61
|
+
local omitted, scanLimited, eventLimited = false, false, false
|
|
62
|
+
local skipped = 0
|
|
63
|
+
local active = true
|
|
64
|
+
local started = os.clock()
|
|
65
|
+
local elapsed = 0
|
|
66
|
+
local timer: thread? = nil
|
|
67
|
+
local function stop(): { [string]: any }
|
|
68
|
+
if active then
|
|
69
|
+
active = false
|
|
70
|
+
elapsed = math.max(os.clock() - started, 0.001)
|
|
71
|
+
for _, connection in connections do connection:Disconnect() end
|
|
72
|
+
if timer then task.cancel(timer); timer = nil end
|
|
73
|
+
for _, row in rows do row.callsPerSecond = math.round(row.count / elapsed * 100) / 100 end
|
|
74
|
+
end
|
|
75
|
+
return { items = rows, events = events, seconds = elapsed, watched = remotes,
|
|
76
|
+
eventLimitReached = eventLimited, scanLimitReached = scanLimited, rowsOmitted = omitted, skipped = skipped }
|
|
77
|
+
end
|
|
78
|
+
local function record(remote: Instance, ...: any)
|
|
79
|
+
if not active or (remote ~= root and not remote:IsDescendantOf(root)) then return end
|
|
80
|
+
events += 1
|
|
81
|
+
local row = byRemote[remote]
|
|
82
|
+
if row == nil and #rows < MAX_ROWS then
|
|
83
|
+
row = { path = pathOf(remote), direction = if client then "server -> client" else "client -> server",
|
|
84
|
+
player = brief(player.Name, 64), count = 0, samples = {} }
|
|
85
|
+
byRemote[remote] = row
|
|
86
|
+
table.insert(rows, row)
|
|
87
|
+
end
|
|
88
|
+
if row then
|
|
89
|
+
row.count += 1
|
|
90
|
+
if #row.samples < 2 then
|
|
91
|
+
local sample = Trace.arguments(...)
|
|
92
|
+
if row.samples[1] ~= sample then table.insert(row.samples, sample) end
|
|
93
|
+
end
|
|
94
|
+
else omitted = true end
|
|
95
|
+
if events >= MAX_EVENTS then eventLimited = true; stop() end
|
|
96
|
+
end
|
|
97
|
+
local function attach(remote: Instance)
|
|
98
|
+
if not active or watched[remote] or not remote:IsA("RemoteEvent") then return end
|
|
99
|
+
if excluded and (remote == excluded or remote:IsDescendantOf(excluded)) then return end
|
|
100
|
+
-- Ignore all temporary bridge traffic, including overlapping MCP calls.
|
|
101
|
+
local ancestor: Instance? = remote.Parent
|
|
102
|
+
while ancestor and ancestor ~= game do
|
|
103
|
+
if ancestor:GetAttribute("MCPClientRelay") == true then return end
|
|
104
|
+
ancestor = ancestor.Parent
|
|
105
|
+
end
|
|
106
|
+
if remotes >= MAX_REMOTES then scanLimited = true; return end
|
|
107
|
+
watched[remote] = true
|
|
108
|
+
remotes += 1
|
|
109
|
+
if client then
|
|
110
|
+
table.insert(connections, (remote :: RemoteEvent).OnClientEvent:Connect(function(...) record(remote, ...) end))
|
|
111
|
+
else
|
|
112
|
+
table.insert(connections, (remote :: RemoteEvent).OnServerEvent:Connect(function(from, ...)
|
|
113
|
+
if from == player then record(remote, ...) end
|
|
114
|
+
end))
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
local ok, failure = pcall(function()
|
|
118
|
+
-- Subscribe first, then scan so remotes created during discovery are not missed.
|
|
119
|
+
table.insert(connections, root.DescendantAdded:Connect(function(object)
|
|
120
|
+
if visits >= MAX_VISITS then scanLimited = true; return end
|
|
121
|
+
visits += 1
|
|
122
|
+
local attached = pcall(attach, object)
|
|
123
|
+
if not attached then skipped += 1 end
|
|
124
|
+
end))
|
|
125
|
+
local pending = { root }
|
|
126
|
+
while #pending > 0 and visits < MAX_VISITS do
|
|
127
|
+
local object = table.remove(pending) :: Instance
|
|
128
|
+
visits += 1
|
|
129
|
+
attach(object)
|
|
130
|
+
local readable, children = pcall(object.GetChildren, object)
|
|
131
|
+
if not readable then skipped += 1; continue end
|
|
132
|
+
for _, child in children do
|
|
133
|
+
if visits + #pending >= MAX_VISITS then scanLimited = true; break end
|
|
134
|
+
table.insert(pending, child)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
if #pending > 0 then scanLimited = true end
|
|
138
|
+
timer = task.delay(seconds, function() timer = nil; stop() end)
|
|
139
|
+
end)
|
|
140
|
+
if not ok then stop(); error(failure, 0) end
|
|
141
|
+
return stop
|
|
142
|
+
end
|
|
143
|
+
return Trace
|