@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.
@@ -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