rbxstudio-mcp 1.9.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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +319 -0
  3. package/dist/__tests__/bridge-service.test.d.ts +2 -0
  4. package/dist/__tests__/bridge-service.test.d.ts.map +1 -0
  5. package/dist/__tests__/bridge-service.test.js +109 -0
  6. package/dist/__tests__/bridge-service.test.js.map +1 -0
  7. package/dist/__tests__/http-server.test.d.ts +2 -0
  8. package/dist/__tests__/http-server.test.d.ts.map +1 -0
  9. package/dist/__tests__/http-server.test.js +193 -0
  10. package/dist/__tests__/http-server.test.js.map +1 -0
  11. package/dist/__tests__/integration.test.d.ts +2 -0
  12. package/dist/__tests__/integration.test.d.ts.map +1 -0
  13. package/dist/__tests__/integration.test.js +182 -0
  14. package/dist/__tests__/integration.test.js.map +1 -0
  15. package/dist/__tests__/smoke.test.d.ts +2 -0
  16. package/dist/__tests__/smoke.test.d.ts.map +1 -0
  17. package/dist/__tests__/smoke.test.js +63 -0
  18. package/dist/__tests__/smoke.test.js.map +1 -0
  19. package/dist/bridge-service.d.ts +17 -0
  20. package/dist/bridge-service.d.ts.map +1 -0
  21. package/dist/bridge-service.js +77 -0
  22. package/dist/bridge-service.js.map +1 -0
  23. package/dist/http-server.d.ts +4 -0
  24. package/dist/http-server.d.ts.map +1 -0
  25. package/dist/http-server.js +290 -0
  26. package/dist/http-server.js.map +1 -0
  27. package/dist/index.d.ts +18 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +1102 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/tools/index.d.ts +273 -0
  32. package/dist/tools/index.d.ts.map +1 -0
  33. package/dist/tools/index.js +628 -0
  34. package/dist/tools/index.js.map +1 -0
  35. package/dist/tools/studio-client.d.ts +7 -0
  36. package/dist/tools/studio-client.d.ts.map +1 -0
  37. package/dist/tools/studio-client.js +19 -0
  38. package/dist/tools/studio-client.js.map +1 -0
  39. package/package.json +69 -0
  40. package/studio-plugin/INSTALLATION.md +150 -0
  41. package/studio-plugin/MCPPlugin.rbxmx +3253 -0
  42. package/studio-plugin/plugin.json +10 -0
  43. package/studio-plugin/plugin.luau +3584 -0
@@ -0,0 +1,3253 @@
1
+ <roblox xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.roblox.com/roblox.xsd" version="4">
2
+ <External>null</External>
3
+ <External>nil</External>
4
+ <Item class="Script" referent="RBX91DA8FC31CC84F2298F74E8208924E33">
5
+ <Properties>
6
+ <ProtectedString name="Source"><![CDATA[local HttpService = game:GetService("HttpService")
7
+ local StudioService = game:GetService("StudioService")
8
+ local Selection = game:GetService("Selection")
9
+ local RunService = game:GetService("RunService")
10
+ local ChangeHistoryService = game:GetService("ChangeHistoryService")
11
+ local ScriptEditorService = game:GetService("ScriptEditorService")
12
+ local CollectionService = game:GetService("CollectionService")
13
+
14
+ local toolbar = plugin:CreateToolbar("MCP Integration")
15
+ local button =
16
+ toolbar:CreateButton("MCP Server", "Connect to MCP Server for AI Integration", "rbxassetid://10734944444")
17
+
18
+ local pluginState = {
19
+ serverUrl = "http://localhost:3002",
20
+ mcpServerUrl = "http://localhost:3001",
21
+ isActive = false,
22
+ pollInterval = 0.5,
23
+ lastPoll = 0,
24
+ consecutiveFailures = 0,
25
+ maxFailuresBeforeError = 50,
26
+ lastSuccessfulConnection = 0,
27
+ currentRetryDelay = 0.5,
28
+ maxRetryDelay = 5,
29
+ retryBackoffMultiplier = 1.2,
30
+ -- UI step tracking
31
+ lastHttpOk = false,
32
+ mcpWaitStartTime = nil,
33
+ }
34
+
35
+ local screenGui = plugin:CreateDockWidgetPluginGuiAsync(
36
+ "MCPServerInterface",
37
+ DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, false, 400, 500, 350, 450)
38
+ )
39
+ screenGui.Title = "MCP Server v1.8.0"
40
+
41
+ local mainFrame = Instance.new("Frame")
42
+ mainFrame.Size = UDim2.new(1, 0, 1, 0)
43
+ mainFrame.BackgroundColor3 = Color3.fromRGB(17, 24, 39)
44
+ mainFrame.BorderSizePixel = 0
45
+ mainFrame.Parent = screenGui
46
+
47
+ local mainCorner = Instance.new("UICorner")
48
+ mainCorner.CornerRadius = UDim.new(0, 8)
49
+ mainCorner.Parent = mainFrame
50
+
51
+ local headerFrame = Instance.new("Frame")
52
+ headerFrame.Size = UDim2.new(1, 0, 0, 60)
53
+ headerFrame.Position = UDim2.new(0, 0, 0, 0)
54
+ headerFrame.BackgroundColor3 = Color3.fromRGB(59, 130, 246)
55
+ headerFrame.BorderSizePixel = 0
56
+ headerFrame.Parent = mainFrame
57
+
58
+ local headerCorner = Instance.new("UICorner")
59
+ headerCorner.CornerRadius = UDim.new(0, 8)
60
+ headerCorner.Parent = headerFrame
61
+
62
+ local headerGradient = Instance.new("UIGradient")
63
+ headerGradient.Color = ColorSequence.new{
64
+ ColorSequenceKeypoint.new(0, Color3.fromRGB(59, 130, 246)),
65
+ ColorSequenceKeypoint.new(1, Color3.fromRGB(147, 51, 234))
66
+ }
67
+ headerGradient.Rotation = 45
68
+ headerGradient.Parent = headerFrame
69
+
70
+ local titleContainer = Instance.new("Frame")
71
+ titleContainer.Size = UDim2.new(1, -70, 1, 0)
72
+ titleContainer.Position = UDim2.new(0, 15, 0, 0)
73
+ titleContainer.BackgroundTransparency = 1
74
+ titleContainer.Parent = headerFrame
75
+
76
+ local titleLabel = Instance.new("TextLabel")
77
+ titleLabel.Size = UDim2.new(1, 0, 0, 28)
78
+ titleLabel.Position = UDim2.new(0, 0, 0, 8)
79
+ titleLabel.BackgroundTransparency = 1
80
+ titleLabel.Text = "MCP Server"
81
+ titleLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
82
+ titleLabel.TextScaled = false
83
+ titleLabel.TextSize = 18
84
+ titleLabel.Font = Enum.Font.Jura
85
+ titleLabel.TextXAlignment = Enum.TextXAlignment.Left
86
+ titleLabel.Parent = titleContainer
87
+
88
+ local versionLabel = Instance.new("TextLabel")
89
+ versionLabel.Size = UDim2.new(1, 0, 0, 16)
90
+ versionLabel.Position = UDim2.new(0, 0, 0, 32)
91
+ versionLabel.BackgroundTransparency = 1
92
+ versionLabel.Text = "AI Integration • v1.7.2"
93
+ versionLabel.TextColor3 = Color3.fromRGB(191, 219, 254)
94
+ versionLabel.TextScaled = false
95
+ versionLabel.TextSize = 12
96
+ versionLabel.Font = Enum.Font.Jura
97
+ versionLabel.TextXAlignment = Enum.TextXAlignment.Left
98
+ versionLabel.Parent = titleContainer
99
+
100
+ local statusContainer = Instance.new("Frame")
101
+ statusContainer.Size = UDim2.new(0, 50, 0, 40)
102
+ statusContainer.Position = UDim2.new(1, -60, 0, 10)
103
+ statusContainer.BackgroundTransparency = 1
104
+ statusContainer.Parent = headerFrame
105
+
106
+ local statusIndicator = Instance.new("Frame")
107
+ statusIndicator.Size = UDim2.new(0, 16, 0, 16)
108
+ statusIndicator.Position = UDim2.new(0.5, -8, 0, 5)
109
+ statusIndicator.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
110
+ statusIndicator.BorderSizePixel = 0
111
+ statusIndicator.Parent = statusContainer
112
+
113
+ local statusCorner = Instance.new("UICorner")
114
+ statusCorner.CornerRadius = UDim.new(1, 0)
115
+ statusCorner.Parent = statusIndicator
116
+
117
+ local statusPulse = Instance.new("Frame")
118
+ statusPulse.Size = UDim2.new(0, 16, 0, 16)
119
+ statusPulse.Position = UDim2.new(0, 0, 0, 0)
120
+ statusPulse.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
121
+ statusPulse.BackgroundTransparency = 0.7
122
+ statusPulse.BorderSizePixel = 0
123
+ statusPulse.Parent = statusIndicator
124
+
125
+ local pulseCorner = Instance.new("UICorner")
126
+ pulseCorner.CornerRadius = UDim.new(1, 0)
127
+ pulseCorner.Parent = statusPulse
128
+
129
+ local statusText = Instance.new("TextLabel")
130
+ statusText.Size = UDim2.new(0, 50, 0, 12)
131
+ statusText.Position = UDim2.new(0, 0, 0, 24)
132
+ statusText.BackgroundTransparency = 1
133
+ statusText.Text = "OFFLINE"
134
+ statusText.TextColor3 = Color3.fromRGB(255, 255, 255)
135
+ statusText.TextScaled = false
136
+ statusText.TextSize = 8
137
+ statusText.Font = Enum.Font.Jura
138
+ statusText.TextXAlignment = Enum.TextXAlignment.Center
139
+ statusText.Parent = statusContainer
140
+
141
+ local contentFrame = Instance.new("ScrollingFrame")
142
+ contentFrame.Size = UDim2.new(1, -20, 1, -80)
143
+ contentFrame.Position = UDim2.new(0, 10, 0, 70)
144
+ contentFrame.BackgroundTransparency = 1
145
+ contentFrame.BorderSizePixel = 0
146
+ contentFrame.ScrollBarThickness = 6
147
+ contentFrame.ScrollBarImageColor3 = Color3.fromRGB(99, 102, 241)
148
+ contentFrame.CanvasSize = UDim2.new(0, 0, 0, 243)
149
+ contentFrame.AutomaticCanvasSize = Enum.AutomaticSize.None
150
+ contentFrame.Parent = mainFrame
151
+
152
+ local contentLayout = Instance.new("UIListLayout")
153
+ contentLayout.Padding = UDim.new(0, 12)
154
+ contentLayout.SortOrder = Enum.SortOrder.LayoutOrder
155
+ contentLayout.Parent = contentFrame
156
+
157
+ local connectionSection = Instance.new("Frame")
158
+ connectionSection.Size = UDim2.new(1, 0, 0, 110)
159
+ connectionSection.BackgroundColor3 = Color3.fromRGB(31, 41, 55)
160
+ connectionSection.BorderSizePixel = 0
161
+ connectionSection.LayoutOrder = 1
162
+ connectionSection.Parent = contentFrame
163
+
164
+ local connectionCorner = Instance.new("UICorner")
165
+ connectionCorner.CornerRadius = UDim.new(0, 8)
166
+ connectionCorner.Parent = connectionSection
167
+
168
+ local connectionPadding = Instance.new("UIPadding")
169
+ connectionPadding.PaddingLeft = UDim.new(0, 15)
170
+ connectionPadding.PaddingRight = UDim.new(0, 15)
171
+ connectionPadding.PaddingTop = UDim.new(0, 15)
172
+ connectionPadding.PaddingBottom = UDim.new(0, 15)
173
+ connectionPadding.Parent = connectionSection
174
+
175
+ local connectionTitle = Instance.new("TextLabel")
176
+ connectionTitle.Size = UDim2.new(1, 0, 0, 20)
177
+ connectionTitle.Position = UDim2.new(0, 0, 0, 0)
178
+ connectionTitle.BackgroundTransparency = 1
179
+ connectionTitle.Text = "Connection Settings"
180
+ connectionTitle.TextColor3 = Color3.fromRGB(255, 255, 255)
181
+ connectionTitle.TextScaled = false
182
+ connectionTitle.TextSize = 14
183
+ connectionTitle.Font = Enum.Font.Jura
184
+ connectionTitle.TextXAlignment = Enum.TextXAlignment.Left
185
+ connectionTitle.Parent = connectionSection
186
+
187
+ local urlLabel = Instance.new("TextLabel")
188
+ urlLabel.Size = UDim2.new(1, 0, 0, 16)
189
+ urlLabel.Position = UDim2.new(0, 0, 0, 30)
190
+ urlLabel.BackgroundTransparency = 1
191
+ urlLabel.Text = "Server URL"
192
+ urlLabel.TextColor3 = Color3.fromRGB(156, 163, 175)
193
+ urlLabel.TextScaled = false
194
+ urlLabel.TextSize = 12
195
+ urlLabel.Font = Enum.Font.Jura
196
+ urlLabel.TextXAlignment = Enum.TextXAlignment.Left
197
+ urlLabel.Parent = connectionSection
198
+
199
+ local urlInput = Instance.new("TextBox")
200
+ urlInput.Size = UDim2.new(1, 0, 0, 32)
201
+ urlInput.Position = UDim2.new(0, 0, 0, 50)
202
+ urlInput.BackgroundColor3 = Color3.fromRGB(55, 65, 81)
203
+ urlInput.BorderSizePixel = 1
204
+ urlInput.BorderColor3 = Color3.fromRGB(99, 102, 241)
205
+ urlInput.Text = "http://localhost:3002"
206
+ urlInput.TextColor3 = Color3.fromRGB(255, 255, 255)
207
+ urlInput.TextScaled = false
208
+ urlInput.TextSize = 12
209
+ urlInput.Font = Enum.Font.Jura
210
+ urlInput.ClearTextOnFocus = false
211
+ urlInput.PlaceholderText = "Enter server URL..."
212
+ urlInput.PlaceholderColor3 = Color3.fromRGB(107, 114, 128)
213
+ urlInput.Parent = connectionSection
214
+
215
+ local urlCorner = Instance.new("UICorner")
216
+ urlCorner.CornerRadius = UDim.new(0, 6)
217
+ urlCorner.Parent = urlInput
218
+
219
+ local urlPadding = Instance.new("UIPadding")
220
+ urlPadding.PaddingLeft = UDim.new(0, 12)
221
+ urlPadding.PaddingRight = UDim.new(0, 12)
222
+ urlPadding.Parent = urlInput
223
+
224
+ local statusSection = Instance.new("Frame")
225
+ statusSection.Size = UDim2.new(1, 0, 0, 170)
226
+ statusSection.BackgroundColor3 = Color3.fromRGB(31, 41, 55)
227
+ statusSection.BorderSizePixel = 0
228
+ statusSection.LayoutOrder = 2
229
+ statusSection.Parent = contentFrame
230
+
231
+ local statusSectionCorner = Instance.new("UICorner")
232
+ statusSectionCorner.CornerRadius = UDim.new(0, 8)
233
+ statusSectionCorner.Parent = statusSection
234
+
235
+ local statusSectionPadding = Instance.new("UIPadding")
236
+ statusSectionPadding.PaddingLeft = UDim.new(0, 15)
237
+ statusSectionPadding.PaddingRight = UDim.new(0, 15)
238
+ statusSectionPadding.PaddingTop = UDim.new(0, 15)
239
+ statusSectionPadding.PaddingBottom = UDim.new(0, 15)
240
+ statusSectionPadding.Parent = statusSection
241
+
242
+ local statusTitle = Instance.new("TextLabel")
243
+ statusTitle.Size = UDim2.new(1, 0, 0, 20)
244
+ statusTitle.Position = UDim2.new(0, 0, 0, 0)
245
+ statusTitle.BackgroundTransparency = 1
246
+ statusTitle.Text = "Connection Status"
247
+ statusTitle.TextColor3 = Color3.fromRGB(255, 255, 255)
248
+ statusTitle.TextScaled = false
249
+ statusTitle.TextSize = 14
250
+ statusTitle.Font = Enum.Font.Jura
251
+ statusTitle.TextXAlignment = Enum.TextXAlignment.Left
252
+ statusTitle.Parent = statusSection
253
+
254
+ local statusLabel = Instance.new("TextLabel")
255
+ statusLabel.Size = UDim2.new(1, 0, 0, 20)
256
+ statusLabel.Position = UDim2.new(0, 0, 0, 30)
257
+ statusLabel.BackgroundTransparency = 1
258
+ statusLabel.Text = "Disconnected"
259
+ statusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
260
+ statusLabel.TextScaled = false
261
+ statusLabel.TextSize = 13
262
+ statusLabel.Font = Enum.Font.Jura
263
+ statusLabel.TextXAlignment = Enum.TextXAlignment.Left
264
+ statusLabel.TextWrapped = true
265
+ statusLabel.Parent = statusSection
266
+
267
+ local detailStatusLabel = Instance.new("TextLabel")
268
+ detailStatusLabel.Size = UDim2.new(1, 0, 0, 12)
269
+ detailStatusLabel.Position = UDim2.new(0, 0, 0, 50)
270
+ detailStatusLabel.BackgroundTransparency = 1
271
+ detailStatusLabel.Text = "HTTP: X MCP: X"
272
+ detailStatusLabel.TextColor3 = Color3.fromRGB(156, 163, 175)
273
+ detailStatusLabel.TextScaled = false
274
+ detailStatusLabel.TextSize = 10
275
+ detailStatusLabel.Font = Enum.Font.Jura
276
+ detailStatusLabel.TextXAlignment = Enum.TextXAlignment.Left
277
+ detailStatusLabel.TextWrapped = true
278
+ detailStatusLabel.Parent = statusSection
279
+
280
+ -- Step-by-step status rows
281
+ local stepsFrame = Instance.new("Frame")
282
+ stepsFrame.Size = UDim2.new(1, 0, 0, 60)
283
+ stepsFrame.Position = UDim2.new(0, 0, 0, 68)
284
+ stepsFrame.BackgroundTransparency = 1
285
+ stepsFrame.Parent = statusSection
286
+
287
+ local stepsLayout = Instance.new("UIListLayout")
288
+ stepsLayout.Padding = UDim.new(0, 6)
289
+ stepsLayout.FillDirection = Enum.FillDirection.Vertical
290
+ stepsLayout.SortOrder = Enum.SortOrder.LayoutOrder
291
+ stepsLayout.Parent = stepsFrame
292
+
293
+ local function createStepRow(text)
294
+ local row = Instance.new("Frame")
295
+ row.Size = UDim2.new(1, 0, 0, 16)
296
+ row.BackgroundTransparency = 1
297
+
298
+ local dot = Instance.new("Frame")
299
+ dot.Size = UDim2.new(0, 10, 0, 10)
300
+ dot.Position = UDim2.new(0, 0, 0, 3)
301
+ dot.BackgroundColor3 = Color3.fromRGB(156, 163, 175)
302
+ dot.BorderSizePixel = 0
303
+ dot.Parent = row
304
+
305
+ local dotCorner = Instance.new("UICorner")
306
+ dotCorner.CornerRadius = UDim.new(1, 0)
307
+ dotCorner.Parent = dot
308
+
309
+ local label = Instance.new("TextLabel")
310
+ label.Size = UDim2.new(1, -18, 1, 0)
311
+ label.Position = UDim2.new(0, 18, 0, 0)
312
+ label.BackgroundTransparency = 1
313
+ label.Text = text
314
+ label.TextColor3 = Color3.fromRGB(209, 213, 219)
315
+ label.TextScaled = false
316
+ label.TextSize = 11
317
+ label.Font = Enum.Font.Jura
318
+ label.TextXAlignment = Enum.TextXAlignment.Left
319
+ label.Parent = row
320
+
321
+ row.Parent = stepsFrame
322
+ return row, dot, label
323
+ end
324
+
325
+ local step1Row, step1Dot, step1Label = createStepRow("1. HTTP server reachable")
326
+ local step2Row, step2Dot, step2Label = createStepRow("2. MCP bridge connected")
327
+ local step3Row, step3Dot, step3Label = createStepRow("3. Ready for commands")
328
+
329
+ -- Troubleshooting tip for common stuck state
330
+ local troubleshootLabel = Instance.new("TextLabel")
331
+ troubleshootLabel.Size = UDim2.new(1, 0, 0, 40)
332
+ troubleshootLabel.Position = UDim2.new(0, 0, 0, 130)
333
+ troubleshootLabel.BackgroundTransparency = 1
334
+ troubleshootLabel.TextWrapped = true
335
+ troubleshootLabel.Visible = false
336
+ troubleshootLabel.Text = "HTTP is OK but MCP isn't responding. Close all node.exe in Task Manager and restart the server."
337
+ troubleshootLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
338
+ troubleshootLabel.TextScaled = false
339
+ troubleshootLabel.TextSize = 11
340
+ troubleshootLabel.Font = Enum.Font.Jura
341
+ troubleshootLabel.TextXAlignment = Enum.TextXAlignment.Left
342
+ troubleshootLabel.Parent = statusSection
343
+
344
+ local connectButton = Instance.new("TextButton")
345
+ connectButton.Size = UDim2.new(1, 0, 0, 48)
346
+ connectButton.BackgroundColor3 = Color3.fromRGB(16, 185, 129)
347
+ connectButton.BorderSizePixel = 0
348
+ connectButton.Text = "Connect"
349
+ connectButton.TextColor3 = Color3.fromRGB(255, 255, 255)
350
+ connectButton.TextScaled = false
351
+ connectButton.TextSize = 16
352
+ connectButton.Font = Enum.Font.Jura
353
+ connectButton.LayoutOrder = 3
354
+ connectButton.Parent = contentFrame
355
+
356
+ local connectCorner = Instance.new("UICorner")
357
+ connectCorner.CornerRadius = UDim.new(0, 12)
358
+ connectCorner.Parent = connectButton
359
+
360
+
361
+
362
+ local TweenService = game:GetService("TweenService")
363
+ local buttonHover = false
364
+
365
+ connectButton.MouseEnter:Connect(function()
366
+ buttonHover = true
367
+ connectButton.BackgroundColor3 = not pluginState.isActive and Color3.fromRGB(5, 150, 105) or Color3.fromRGB(220, 38, 38)
368
+ end)
369
+
370
+ connectButton.MouseLeave:Connect(function()
371
+ buttonHover = false
372
+ connectButton.BackgroundColor3 = not pluginState.isActive and Color3.fromRGB(16, 185, 129) or Color3.fromRGB(239, 68, 68)
373
+ end)
374
+
375
+ local pulseAnimation = nil
376
+
377
+ local function createPulseAnimation()
378
+ if pulseAnimation then
379
+ pcall(function()
380
+ pulseAnimation:Cancel()
381
+ end)
382
+ pulseAnimation = nil
383
+ end
384
+
385
+ pcall(function()
386
+ pulseAnimation = TweenService:Create(statusPulse, TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut, -1, true), {
387
+ Size = UDim2.new(0, 24, 0, 24),
388
+ Position = UDim2.new(0, -4, 0, -4),
389
+ BackgroundTransparency = 1
390
+ })
391
+ end)
392
+
393
+ return pulseAnimation
394
+ end
395
+
396
+ local function stopPulseAnimation()
397
+ statusPulse.Size = UDim2.new(0, 16, 0, 16)
398
+ statusPulse.Position = UDim2.new(0, 0, 0, 0)
399
+ statusPulse.BackgroundTransparency = 0.7
400
+ end
401
+
402
+ local function startPulseAnimation()
403
+ statusPulse.Size = UDim2.new(0, 16, 0, 16)
404
+ statusPulse.Position = UDim2.new(0, 0, 0, 0)
405
+ statusPulse.BackgroundTransparency = 0.7
406
+ end
407
+
408
+ local function safeCall(func, ...)
409
+ local success, result = pcall(func, ...)
410
+ if success then
411
+ return result
412
+ else
413
+ warn("MCP Plugin Error: " .. tostring(result))
414
+ return nil
415
+ end
416
+ end
417
+
418
+ local function getInstancePath(instance)
419
+ if not instance or instance == game then
420
+ return "game"
421
+ end
422
+
423
+ local path = {}
424
+ local current = instance
425
+
426
+ while current and current ~= game do
427
+ table.insert(path, 1, current.Name)
428
+ current = current.Parent
429
+ end
430
+
431
+ return "game." .. table.concat(path, ".")
432
+ end
433
+
434
+ -- Helper to normalize line endings and split without inventing an extra trailing line
435
+ local function splitLines(source)
436
+ local normalized = (source or ""):gsub("\r\n", "\n"):gsub("\r", "\n")
437
+ local endsWithNewline = normalized:sub(-1) == "\n"
438
+
439
+ local lines = {}
440
+ local start = 1
441
+
442
+ while true do
443
+ local newlinePos = string.find(normalized, "\n", start, true)
444
+ if newlinePos then
445
+ table.insert(lines, string.sub(normalized, start, newlinePos - 1))
446
+ start = newlinePos + 1
447
+ else
448
+ local remainder = string.sub(normalized, start)
449
+ if remainder ~= "" or not endsWithNewline then
450
+ table.insert(lines, remainder)
451
+ end
452
+ break
453
+ end
454
+ end
455
+
456
+ if #lines == 0 then
457
+ table.insert(lines, "")
458
+ end
459
+
460
+ return lines, endsWithNewline
461
+ end
462
+
463
+ local function joinLines(lines, hadTrailingNewline)
464
+ local source = table.concat(lines, "\n")
465
+ if hadTrailingNewline and source:sub(-1) ~= "\n" then
466
+ source ..= "\n"
467
+ end
468
+ return source
469
+ end
470
+
471
+ -- Helper to convert property values from JSON to Roblox types
472
+ local function convertPropertyValue(instance, propertyName, propertyValue)
473
+ -- Handle nil
474
+ if propertyValue == nil then
475
+ return nil
476
+ end
477
+
478
+ -- Handle arrays (likely Vector3, Color3, UDim2)
479
+ if type(propertyValue) == "table" and #propertyValue > 0 then
480
+ -- Check if it's a Vector3-like property
481
+ if #propertyValue == 3 then
482
+ local prop = propertyName:lower()
483
+ if prop == "position" or prop == "size" or prop == "orientation" or prop == "velocity" or prop == "angularvelocity" then
484
+ return Vector3.new(propertyValue[1] or 0, propertyValue[2] or 0, propertyValue[3] or 0)
485
+ elseif prop == "color" or prop == "color3" then
486
+ return Color3.new(propertyValue[1] or 0, propertyValue[2] or 0, propertyValue[3] or 0)
487
+ else
488
+ -- Try to infer from current property type
489
+ local success, currentVal = pcall(function() return instance[propertyName] end)
490
+ if success then
491
+ if typeof(currentVal) == "Vector3" then
492
+ return Vector3.new(propertyValue[1] or 0, propertyValue[2] or 0, propertyValue[3] or 0)
493
+ elseif typeof(currentVal) == "Color3" then
494
+ return Color3.new(propertyValue[1] or 0, propertyValue[2] or 0, propertyValue[3] or 0)
495
+ end
496
+ end
497
+ end
498
+ elseif #propertyValue == 2 then
499
+ -- Possibly Vector2
500
+ local success, currentVal = pcall(function() return instance[propertyName] end)
501
+ if success and typeof(currentVal) == "Vector2" then
502
+ return Vector2.new(propertyValue[1] or 0, propertyValue[2] or 0)
503
+ end
504
+ end
505
+ end
506
+
507
+ -- Handle object with X, Y, Z keys (Vector3)
508
+ if type(propertyValue) == "table" and (propertyValue.X or propertyValue.Y or propertyValue.Z) then
509
+ return Vector3.new(propertyValue.X or 0, propertyValue.Y or 0, propertyValue.Z or 0)
510
+ end
511
+
512
+ -- Handle object with R, G, B keys (Color3)
513
+ if type(propertyValue) == "table" and (propertyValue.R or propertyValue.G or propertyValue.B) then
514
+ return Color3.new(propertyValue.R or 0, propertyValue.G or 0, propertyValue.B or 0)
515
+ end
516
+
517
+ -- Handle Enum values (strings like "Ball", "Cylinder", etc.)
518
+ if type(propertyValue) == "string" then
519
+ local success, currentVal = pcall(function() return instance[propertyName] end)
520
+ if success and typeof(currentVal) == "EnumItem" then
521
+ local enumType = tostring(currentVal.EnumType)
522
+ local enumSuccess, enumVal = pcall(function()
523
+ return Enum[enumType][propertyValue]
524
+ end)
525
+ if enumSuccess and enumVal then
526
+ return enumVal
527
+ end
528
+ end
529
+ -- Handle BrickColor
530
+ if propertyName == "BrickColor" then
531
+ return BrickColor.new(propertyValue)
532
+ end
533
+ end
534
+
535
+ -- Handle boolean strings
536
+ if type(propertyValue) == "string" then
537
+ if propertyValue == "true" then return true end
538
+ if propertyValue == "false" then return false end
539
+ end
540
+
541
+ -- Return as-is for primitives (number, boolean, string)
542
+ return propertyValue
543
+ end
544
+
545
+ local processRequest
546
+ local sendResponse
547
+ local handlers = {}
548
+
549
+ local function pollForRequests()
550
+ if not pluginState.isActive then
551
+ return
552
+ end
553
+
554
+ local success, result = pcall(function()
555
+ return HttpService:RequestAsync({
556
+ Url = pluginState.serverUrl .. "/poll",
557
+ Method = "GET",
558
+ Headers = {
559
+ ["Content-Type"] = "application/json",
560
+ },
561
+ })
562
+ end)
563
+
564
+ if success and (result.Success or result.StatusCode == 503) then
565
+ pluginState.consecutiveFailures = 0
566
+ pluginState.currentRetryDelay = 0.5
567
+ pluginState.lastSuccessfulConnection = tick()
568
+
569
+ local data = HttpService:JSONDecode(result.Body)
570
+ local mcpConnected = data.mcpConnected == true
571
+ -- Step indicators: HTTP request succeeded
572
+ pluginState.lastHttpOk = true
573
+ step1Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
574
+ step1Label.Text = "1. HTTP server reachable (OK)"
575
+
576
+ if mcpConnected and not statusLabel.Text:find("Connected") then
577
+ statusLabel.Text = "Connected"
578
+ statusLabel.TextColor3 = Color3.fromRGB(34, 197, 94)
579
+ statusIndicator.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
580
+ statusPulse.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
581
+ statusText.Text = "ONLINE"
582
+ detailStatusLabel.Text = "HTTP: OK MCP: OK"
583
+ detailStatusLabel.TextColor3 = Color3.fromRGB(34, 197, 94)
584
+ -- Steps 2/3 OK
585
+ step2Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
586
+ step2Label.Text = "2. MCP bridge connected (OK)"
587
+ step3Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
588
+ step3Label.Text = "3. Ready for commands (OK)"
589
+ pluginState.mcpWaitStartTime = nil
590
+ troubleshootLabel.Visible = false
591
+ stopPulseAnimation()
592
+ elseif not mcpConnected then
593
+ statusLabel.Text = "Waiting for MCP server"
594
+ statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
595
+ statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
596
+ statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
597
+ statusText.Text = "WAITING"
598
+ detailStatusLabel.Text = "HTTP: OK MCP: ..."
599
+ detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
600
+ -- Step 2/3 pending
601
+ step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
602
+ step2Label.Text = "2. MCP bridge connected (waiting...)"
603
+ step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
604
+ step3Label.Text = "3. Ready for commands (waiting...)"
605
+ -- Track stuck state where HTTP is OK but MCP isn't
606
+ if not pluginState.mcpWaitStartTime then
607
+ pluginState.mcpWaitStartTime = tick()
608
+ end
609
+ local elapsed = tick() - (pluginState.mcpWaitStartTime or tick())
610
+ troubleshootLabel.Visible = elapsed > 8
611
+ startPulseAnimation()
612
+ end
613
+
614
+ if data.request and mcpConnected then
615
+ local response = processRequest(data.request)
616
+ sendResponse(data.requestId, response)
617
+ end
618
+ elseif pluginState.isActive then
619
+ pluginState.consecutiveFailures = pluginState.consecutiveFailures + 1
620
+
621
+ if pluginState.consecutiveFailures > 1 then
622
+ pluginState.currentRetryDelay =
623
+ math.min(pluginState.currentRetryDelay * pluginState.retryBackoffMultiplier, pluginState.maxRetryDelay)
624
+ end
625
+
626
+ if pluginState.consecutiveFailures >= pluginState.maxFailuresBeforeError then
627
+ statusLabel.Text = "Server unavailable"
628
+ statusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
629
+ statusIndicator.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
630
+ statusPulse.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
631
+ statusText.Text = "ERROR"
632
+ detailStatusLabel.Text = "HTTP: X MCP: X"
633
+ detailStatusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
634
+ -- Steps show error
635
+ step1Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
636
+ step1Label.Text = "1. HTTP server reachable (error)"
637
+ step2Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
638
+ step2Label.Text = "2. MCP bridge connected (error)"
639
+ step3Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
640
+ step3Label.Text = "3. Ready for commands (error)"
641
+ pluginState.mcpWaitStartTime = nil
642
+ troubleshootLabel.Visible = false
643
+ stopPulseAnimation()
644
+ elseif pluginState.consecutiveFailures > 5 then
645
+ local waitTime = math.ceil(pluginState.currentRetryDelay)
646
+ statusLabel.Text = "Retrying (" .. waitTime .. "s)"
647
+ statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
648
+ statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
649
+ statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
650
+ statusText.Text = "RETRY"
651
+ detailStatusLabel.Text = "HTTP: ... MCP: ..."
652
+ detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
653
+ -- Steps show retrying
654
+ step1Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
655
+ step1Label.Text = "1. HTTP server reachable (retrying...)"
656
+ step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
657
+ step2Label.Text = "2. MCP bridge connected (retrying...)"
658
+ step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
659
+ step3Label.Text = "3. Ready for commands (retrying...)"
660
+ pluginState.mcpWaitStartTime = nil
661
+ troubleshootLabel.Visible = false
662
+ startPulseAnimation()
663
+ elseif pluginState.consecutiveFailures > 1 then
664
+ statusLabel.Text = "Connecting (attempt " .. pluginState.consecutiveFailures .. ")"
665
+ statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
666
+ statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
667
+ statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
668
+ statusText.Text = "CONNECTING"
669
+ detailStatusLabel.Text = "HTTP: ... MCP: ..."
670
+ detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
671
+ -- Steps show connecting
672
+ step1Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
673
+ step1Label.Text = "1. HTTP server reachable (connecting...)"
674
+ step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
675
+ step2Label.Text = "2. MCP bridge connected (connecting...)"
676
+ step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
677
+ step3Label.Text = "3. Ready for commands (connecting...)"
678
+ pluginState.mcpWaitStartTime = nil
679
+ troubleshootLabel.Visible = false
680
+ startPulseAnimation()
681
+ end
682
+ end
683
+ end
684
+
685
+ sendResponse = function(requestId, responseData)
686
+ pcall(function()
687
+ HttpService:RequestAsync({
688
+ Url = pluginState.serverUrl .. "/response",
689
+ Method = "POST",
690
+ Headers = {
691
+ ["Content-Type"] = "application/json",
692
+ },
693
+ Body = HttpService:JSONEncode({
694
+ requestId = requestId,
695
+ response = responseData,
696
+ }),
697
+ })
698
+ end)
699
+ end
700
+
701
+ processRequest = function(request)
702
+ local endpoint = request.endpoint
703
+ local data = request.data or {}
704
+
705
+ if endpoint == "/api/file-tree" then
706
+ return handlers.getFileTree(data)
707
+ elseif endpoint == "/api/search-files" then
708
+ return handlers.searchFiles(data)
709
+ elseif endpoint == "/api/place-info" then
710
+ return handlers.getPlaceInfo(data)
711
+ elseif endpoint == "/api/services" then
712
+ return handlers.getServices(data)
713
+ elseif endpoint == "/api/search-objects" then
714
+ return handlers.searchObjects(data)
715
+ elseif endpoint == "/api/instance-properties" then
716
+ return handlers.getInstanceProperties(data)
717
+ elseif endpoint == "/api/instance-children" then
718
+ return handlers.getInstanceChildren(data)
719
+ elseif endpoint == "/api/search-by-property" then
720
+ return handlers.searchByProperty(data)
721
+ elseif endpoint == "/api/class-info" then
722
+ return handlers.getClassInfo(data)
723
+ elseif endpoint == "/api/project-structure" then
724
+ return handlers.getProjectStructure(data)
725
+ elseif endpoint == "/api/set-property" then
726
+ return handlers.setProperty(data)
727
+ elseif endpoint == "/api/mass-set-property" then
728
+ return handlers.massSetProperty(data)
729
+ elseif endpoint == "/api/mass-get-property" then
730
+ return handlers.massGetProperty(data)
731
+ elseif endpoint == "/api/create-object" then
732
+ return handlers.createObject(data)
733
+ elseif endpoint == "/api/mass-create-objects" then
734
+ return handlers.massCreateObjects(data)
735
+ elseif endpoint == "/api/mass-create-objects-with-properties" then
736
+ return handlers.massCreateObjectsWithProperties(data)
737
+ elseif endpoint == "/api/delete-object" then
738
+ return handlers.deleteObject(data)
739
+ elseif endpoint == "/api/smart-duplicate" then
740
+ return handlers.smartDuplicate(data)
741
+ elseif endpoint == "/api/mass-duplicate" then
742
+ return handlers.massDuplicate(data)
743
+ elseif endpoint == "/api/set-calculated-property" then
744
+ return handlers.setCalculatedProperty(data)
745
+ elseif endpoint == "/api/set-relative-property" then
746
+ return handlers.setRelativeProperty(data)
747
+ elseif endpoint == "/api/get-script-source" then
748
+ return handlers.getScriptSource(data)
749
+ elseif endpoint == "/api/set-script-source" then
750
+ return handlers.setScriptSource(data)
751
+ -- Partial script editing endpoints
752
+ elseif endpoint == "/api/edit-script-lines" then
753
+ return handlers.editScriptLines(data)
754
+ elseif endpoint == "/api/insert-script-lines" then
755
+ return handlers.insertScriptLines(data)
756
+ elseif endpoint == "/api/delete-script-lines" then
757
+ return handlers.deleteScriptLines(data)
758
+ -- Attribute endpoints
759
+ elseif endpoint == "/api/get-attribute" then
760
+ return handlers.getAttribute(data)
761
+ elseif endpoint == "/api/set-attribute" then
762
+ return handlers.setAttribute(data)
763
+ elseif endpoint == "/api/get-attributes" then
764
+ return handlers.getAttributes(data)
765
+ elseif endpoint == "/api/delete-attribute" then
766
+ return handlers.deleteAttribute(data)
767
+ -- Tag endpoints
768
+ elseif endpoint == "/api/get-tags" then
769
+ return handlers.getTags(data)
770
+ elseif endpoint == "/api/add-tag" then
771
+ return handlers.addTag(data)
772
+ elseif endpoint == "/api/remove-tag" then
773
+ return handlers.removeTag(data)
774
+ elseif endpoint == "/api/get-tagged" then
775
+ return handlers.getTagged(data)
776
+ -- Selection endpoints
777
+ elseif endpoint == "/api/get-selection" then
778
+ return handlers.getSelection(data)
779
+ else
780
+ return { error = "Unknown endpoint: " .. tostring(endpoint) }
781
+ end
782
+ end
783
+
784
+ local function getInstanceByPath(path)
785
+ if path == "game" or path == "" then
786
+ return game
787
+ end
788
+
789
+ path = path:gsub("^game%.", "")
790
+
791
+ local parts = {}
792
+ for part in path:gmatch("[^%.]+") do
793
+ table.insert(parts, part)
794
+ end
795
+
796
+ local current = game
797
+ for _, part in ipairs(parts) do
798
+ current = current:FindFirstChild(part)
799
+ if not current then
800
+ return nil
801
+ end
802
+ end
803
+
804
+ return current
805
+ end
806
+
807
+ handlers.getFileTree = function(requestData)
808
+ local path = requestData.path or ""
809
+ local startInstance = getInstanceByPath(path)
810
+
811
+ if not startInstance then
812
+ return { error = "Path not found: " .. path }
813
+ end
814
+
815
+ local function buildTree(instance, depth)
816
+ if depth > 10 then
817
+ return { name = instance.Name, className = instance.ClassName, children = {} }
818
+ end
819
+
820
+ local node = {
821
+ name = instance.Name,
822
+ className = instance.ClassName,
823
+ path = getInstancePath(instance),
824
+ children = {},
825
+ }
826
+
827
+ if instance:IsA("LuaSourceContainer") then
828
+ node.hasSource = true
829
+ node.scriptType = instance.ClassName
830
+ end
831
+
832
+ for _, child in ipairs(instance:GetChildren()) do
833
+ table.insert(node.children, buildTree(child, depth + 1))
834
+ end
835
+
836
+ return node
837
+ end
838
+
839
+ return {
840
+ tree = buildTree(startInstance, 0),
841
+ timestamp = tick(),
842
+ }
843
+ end
844
+
845
+ handlers.searchFiles = function(requestData)
846
+ local query = requestData.query
847
+ local searchType = requestData.searchType or "name"
848
+
849
+ if not query then
850
+ return { error = "Query is required" }
851
+ end
852
+
853
+ local results = {}
854
+
855
+ local function searchRecursive(instance)
856
+ local match = false
857
+
858
+ if searchType == "name" then
859
+ match = instance.Name:lower():find(query:lower()) ~= nil
860
+ elseif searchType == "type" then
861
+ match = instance.ClassName:lower():find(query:lower()) ~= nil
862
+ elseif searchType == "content" and instance:IsA("LuaSourceContainer") then
863
+ match = instance.Source:lower():find(query:lower()) ~= nil
864
+ end
865
+
866
+ if match then
867
+ table.insert(results, {
868
+ name = instance.Name,
869
+ className = instance.ClassName,
870
+ path = getInstancePath(instance),
871
+ hasSource = instance:IsA("LuaSourceContainer"),
872
+ })
873
+ end
874
+
875
+ for _, child in ipairs(instance:GetChildren()) do
876
+ searchRecursive(child)
877
+ end
878
+ end
879
+
880
+ searchRecursive(game)
881
+
882
+ return {
883
+ results = results,
884
+ query = query,
885
+ searchType = searchType,
886
+ count = #results,
887
+ }
888
+ end
889
+
890
+ handlers.getPlaceInfo = function(requestData)
891
+ return {
892
+ placeName = game.Name,
893
+ placeId = game.PlaceId,
894
+ gameId = game.GameId,
895
+ jobId = game.JobId,
896
+ workspace = {
897
+ name = workspace.Name,
898
+ className = workspace.ClassName,
899
+ },
900
+ }
901
+ end
902
+
903
+ handlers.getServices = function(requestData)
904
+ local serviceName = requestData.serviceName
905
+
906
+ if serviceName then
907
+ local service = safeCall(game.GetService, game, serviceName)
908
+ if service then
909
+ return {
910
+ service = {
911
+ name = service.Name,
912
+ className = service.ClassName,
913
+ path = getInstancePath(service),
914
+ childCount = #service:GetChildren(),
915
+ },
916
+ }
917
+ else
918
+ return { error = "Service not found: " .. serviceName }
919
+ end
920
+ else
921
+ local services = {}
922
+ local commonServices = {
923
+ "Workspace",
924
+ "Players",
925
+ "StarterGui",
926
+ "StarterPack",
927
+ "StarterPlayer",
928
+ "ReplicatedStorage",
929
+ "ServerStorage",
930
+ "ServerScriptService",
931
+ "HttpService",
932
+ "TeleportService",
933
+ "DataStoreService",
934
+ }
935
+
936
+ for _, serviceName in ipairs(commonServices) do
937
+ local service = safeCall(game.GetService, game, serviceName)
938
+ if service then
939
+ table.insert(services, {
940
+ name = service.Name,
941
+ className = service.ClassName,
942
+ path = getInstancePath(service),
943
+ childCount = #service:GetChildren(),
944
+ })
945
+ end
946
+ end
947
+
948
+ return { services = services }
949
+ end
950
+ end
951
+
952
+ handlers.searchObjects = function(requestData)
953
+ local query = requestData.query
954
+ local searchType = requestData.searchType or "name"
955
+ local propertyName = requestData.propertyName
956
+
957
+ if not query then
958
+ return { error = "Query is required" }
959
+ end
960
+
961
+ local results = {}
962
+
963
+ local function searchRecursive(instance)
964
+ local match = false
965
+
966
+ if searchType == "name" then
967
+ match = instance.Name:lower():find(query:lower()) ~= nil
968
+ elseif searchType == "class" then
969
+ match = instance.ClassName:lower():find(query:lower()) ~= nil
970
+ elseif searchType == "property" and propertyName then
971
+ local success, value = pcall(function()
972
+ return tostring(instance[propertyName])
973
+ end)
974
+ if success then
975
+ match = value:lower():find(query:lower()) ~= nil
976
+ end
977
+ end
978
+
979
+ if match then
980
+ table.insert(results, {
981
+ name = instance.Name,
982
+ className = instance.ClassName,
983
+ path = getInstancePath(instance),
984
+ })
985
+ end
986
+
987
+ for _, child in ipairs(instance:GetChildren()) do
988
+ searchRecursive(child)
989
+ end
990
+ end
991
+
992
+ searchRecursive(game)
993
+
994
+ return {
995
+ results = results,
996
+ query = query,
997
+ searchType = searchType,
998
+ count = #results,
999
+ }
1000
+ end
1001
+
1002
+ handlers.getInstanceProperties = function(requestData)
1003
+ local instancePath = requestData.instancePath
1004
+ if not instancePath then
1005
+ return { error = "Instance path is required" }
1006
+ end
1007
+
1008
+ local instance = getInstanceByPath(instancePath)
1009
+ if not instance then
1010
+ return { error = "Instance not found: " .. instancePath }
1011
+ end
1012
+
1013
+ local properties = {}
1014
+ local success, result = pcall(function()
1015
+ local classInfo = {}
1016
+
1017
+ local basicProps = { "Name", "ClassName", "Parent" }
1018
+ for _, prop in ipairs(basicProps) do
1019
+ local propSuccess, propValue = pcall(function()
1020
+ local val = instance[prop]
1021
+ if prop == "Parent" and val then
1022
+ return getInstancePath(val)
1023
+ elseif val == nil then
1024
+ return "nil"
1025
+ else
1026
+ return tostring(val)
1027
+ end
1028
+ end)
1029
+ if propSuccess then
1030
+ properties[prop] = propValue
1031
+ end
1032
+ end
1033
+
1034
+ local commonProps = {
1035
+ "Size",
1036
+ "Position",
1037
+ "Rotation",
1038
+ "CFrame",
1039
+ "Anchored",
1040
+ "CanCollide",
1041
+ "Transparency",
1042
+ "BrickColor",
1043
+ "Material",
1044
+ "Color",
1045
+ "Text",
1046
+ "TextColor3",
1047
+ "BackgroundColor3",
1048
+ "Image",
1049
+ "ImageColor3",
1050
+ "Visible",
1051
+ "Active",
1052
+ "ZIndex",
1053
+ "BorderSizePixel",
1054
+ "BackgroundTransparency",
1055
+ "ImageTransparency",
1056
+ "TextTransparency",
1057
+ "Value",
1058
+ "Enabled",
1059
+ "Brightness",
1060
+ "Range",
1061
+ "Shadows",
1062
+ "Face",
1063
+ "SurfaceType",
1064
+ }
1065
+
1066
+ for _, prop in ipairs(commonProps) do
1067
+ local propSuccess, propValue = pcall(function()
1068
+ return tostring(instance[prop])
1069
+ end)
1070
+ if propSuccess then
1071
+ properties[prop] = propValue
1072
+ end
1073
+ end
1074
+
1075
+ if instance:IsA("LuaSourceContainer") then
1076
+ properties.Source = instance.Source
1077
+ if instance:IsA("BaseScript") then
1078
+ properties.Enabled = tostring(instance.Enabled)
1079
+ end
1080
+ end
1081
+
1082
+ -- Only Parts have a Shape property; MeshParts do not.
1083
+ if instance:IsA("Part") then
1084
+ properties.Shape = tostring(instance.Shape)
1085
+ end
1086
+
1087
+ -- TopSurface and BottomSurface exist on all BaseParts
1088
+ if instance:IsA("BasePart") then
1089
+ properties.TopSurface = tostring(instance.TopSurface)
1090
+ properties.BottomSurface = tostring(instance.BottomSurface)
1091
+ end
1092
+
1093
+ properties.ChildCount = tostring(#instance:GetChildren())
1094
+
1095
+ return properties
1096
+ end)
1097
+
1098
+ if success then
1099
+ return {
1100
+ instancePath = instancePath,
1101
+ className = instance.ClassName,
1102
+ properties = properties,
1103
+ }
1104
+ else
1105
+ return { error = "Failed to get properties: " .. tostring(result) }
1106
+ end
1107
+ end
1108
+
1109
+ handlers.getInstanceChildren = function(requestData)
1110
+ local instancePath = requestData.instancePath
1111
+ if not instancePath then
1112
+ return { error = "Instance path is required" }
1113
+ end
1114
+
1115
+ local instance = getInstanceByPath(instancePath)
1116
+ if not instance then
1117
+ return { error = "Instance not found: " .. instancePath }
1118
+ end
1119
+
1120
+ local children = {}
1121
+ for _, child in ipairs(instance:GetChildren()) do
1122
+ table.insert(children, {
1123
+ name = child.Name,
1124
+ className = child.ClassName,
1125
+ path = getInstancePath(child),
1126
+ hasChildren = #child:GetChildren() > 0,
1127
+ hasSource = child:IsA("LuaSourceContainer"),
1128
+ })
1129
+ end
1130
+
1131
+ return {
1132
+ instancePath = instancePath,
1133
+ children = children,
1134
+ count = #children,
1135
+ }
1136
+ end
1137
+
1138
+ handlers.searchByProperty = function(requestData)
1139
+ local propertyName = requestData.propertyName
1140
+ local propertyValue = requestData.propertyValue
1141
+
1142
+ if not propertyName or not propertyValue then
1143
+ return { error = "Property name and value are required" }
1144
+ end
1145
+
1146
+ local results = {}
1147
+
1148
+ local function searchRecursive(instance)
1149
+ local success, value = pcall(function()
1150
+ return tostring(instance[propertyName])
1151
+ end)
1152
+
1153
+ if success and value:lower():find(propertyValue:lower()) then
1154
+ table.insert(results, {
1155
+ name = instance.Name,
1156
+ className = instance.ClassName,
1157
+ path = getInstancePath(instance),
1158
+ propertyValue = value,
1159
+ })
1160
+ end
1161
+
1162
+ for _, child in ipairs(instance:GetChildren()) do
1163
+ searchRecursive(child)
1164
+ end
1165
+ end
1166
+
1167
+ searchRecursive(game)
1168
+
1169
+ return {
1170
+ propertyName = propertyName,
1171
+ propertyValue = propertyValue,
1172
+ results = results,
1173
+ count = #results,
1174
+ }
1175
+ end
1176
+
1177
+ handlers.getClassInfo = function(requestData)
1178
+ local className = requestData.className
1179
+ if not className then
1180
+ return { error = "Class name is required" }
1181
+ end
1182
+
1183
+ local success, tempInstance = pcall(function()
1184
+ return Instance.new(className)
1185
+ end)
1186
+
1187
+ if not success then
1188
+ return { error = "Invalid class name: " .. className }
1189
+ end
1190
+
1191
+ local classInfo = {
1192
+ className = className,
1193
+ properties = {},
1194
+ methods = {},
1195
+ events = {},
1196
+ }
1197
+
1198
+ local commonProps = {
1199
+ "Name",
1200
+ "ClassName",
1201
+ "Parent",
1202
+ "Size",
1203
+ "Position",
1204
+ "Rotation",
1205
+ "CFrame",
1206
+ "Anchored",
1207
+ "CanCollide",
1208
+ "Transparency",
1209
+ "BrickColor",
1210
+ "Material",
1211
+ "Color",
1212
+ "Text",
1213
+ "TextColor3",
1214
+ "BackgroundColor3",
1215
+ "Image",
1216
+ "ImageColor3",
1217
+ "Visible",
1218
+ "Active",
1219
+ "ZIndex",
1220
+ "BorderSizePixel",
1221
+ "BackgroundTransparency",
1222
+ "ImageTransparency",
1223
+ "TextTransparency",
1224
+ "Value",
1225
+ "Enabled",
1226
+ "Brightness",
1227
+ "Range",
1228
+ "Shadows",
1229
+ }
1230
+
1231
+ for _, prop in ipairs(commonProps) do
1232
+ local propSuccess, _ = pcall(function()
1233
+ return tempInstance[prop]
1234
+ end)
1235
+ if propSuccess then
1236
+ table.insert(classInfo.properties, prop)
1237
+ end
1238
+ end
1239
+
1240
+ local commonMethods = {
1241
+ "Destroy",
1242
+ "Clone",
1243
+ "FindFirstChild",
1244
+ "FindFirstChildOfClass",
1245
+ "GetChildren",
1246
+ "IsA",
1247
+ "IsAncestorOf",
1248
+ "IsDescendantOf",
1249
+ "WaitForChild",
1250
+ }
1251
+
1252
+ for _, method in ipairs(commonMethods) do
1253
+ local methodSuccess, _ = pcall(function()
1254
+ return tempInstance[method]
1255
+ end)
1256
+ if methodSuccess then
1257
+ table.insert(classInfo.methods, method)
1258
+ end
1259
+ end
1260
+
1261
+ tempInstance:Destroy()
1262
+
1263
+ return classInfo
1264
+ end
1265
+
1266
+ handlers.getProjectStructure = function(requestData)
1267
+ local startPath = requestData.path or ""
1268
+ local maxDepth = requestData.maxDepth or 3
1269
+ local showScriptsOnly = requestData.scriptsOnly or false
1270
+
1271
+ local startInstance
1272
+ if startPath == "" or startPath == "game" then
1273
+ local services = {}
1274
+ local mainServices = {
1275
+ "Workspace",
1276
+ "ServerScriptService",
1277
+ "ServerStorage",
1278
+ "ReplicatedStorage",
1279
+ "StarterGui",
1280
+ "StarterPack",
1281
+ "StarterPlayer",
1282
+ "Players",
1283
+ }
1284
+
1285
+ for _, serviceName in ipairs(mainServices) do
1286
+ local service = safeCall(game.GetService, game, serviceName)
1287
+ if service then
1288
+ local serviceInfo = {
1289
+ name = service.Name,
1290
+ className = service.ClassName,
1291
+ path = getInstancePath(service),
1292
+ childCount = #service:GetChildren(),
1293
+ hasChildren = #service:GetChildren() > 0,
1294
+ }
1295
+ table.insert(services, serviceInfo)
1296
+ end
1297
+ end
1298
+
1299
+ return {
1300
+ type = "service_overview",
1301
+ services = services,
1302
+ timestamp = tick(),
1303
+ note = "Use path parameter to explore specific locations (e.g., 'game.ServerScriptService')",
1304
+ }
1305
+ else
1306
+ startInstance = getInstanceByPath(startPath)
1307
+ if not startInstance then
1308
+ return { error = "Path not found: " .. startPath }
1309
+ end
1310
+ end
1311
+
1312
+ local function getStructure(instance, depth, currentPath)
1313
+ if depth > maxDepth then
1314
+ return {
1315
+ name = instance.Name,
1316
+ className = instance.ClassName,
1317
+ path = getInstancePath(instance),
1318
+ childCount = #instance:GetChildren(),
1319
+ hasMore = true,
1320
+ note = "Max depth reached - use this path to explore further",
1321
+ }
1322
+ end
1323
+
1324
+ local node = {
1325
+ name = instance.Name,
1326
+ className = instance.ClassName,
1327
+ path = getInstancePath(instance),
1328
+ children = {},
1329
+ }
1330
+
1331
+ if instance:IsA("LuaSourceContainer") then
1332
+ node.hasSource = true
1333
+ node.scriptType = instance.ClassName
1334
+ if instance:IsA("BaseScript") then
1335
+ node.enabled = instance.Enabled
1336
+ end
1337
+ end
1338
+
1339
+ if instance:IsA("GuiObject") then
1340
+ node.visible = instance.Visible
1341
+ if instance:IsA("Frame") or instance:IsA("ScreenGui") then
1342
+ node.guiType = "container"
1343
+ elseif instance:IsA("TextLabel") or instance:IsA("TextButton") then
1344
+ node.guiType = "text"
1345
+ if instance.Text and instance.Text ~= "" then
1346
+ node.text = instance.Text
1347
+ end
1348
+ elseif instance:IsA("ImageLabel") or instance:IsA("ImageButton") then
1349
+ node.guiType = "image"
1350
+ end
1351
+ end
1352
+
1353
+ local children = instance:GetChildren()
1354
+ if showScriptsOnly then
1355
+ local scriptChildren = {}
1356
+ for _, child in ipairs(children) do
1357
+ if child:IsA("BaseScript") or child:IsA("Folder") or child:IsA("ModuleScript") then
1358
+ table.insert(scriptChildren, child)
1359
+ end
1360
+ end
1361
+ children = scriptChildren
1362
+ end
1363
+
1364
+ local childCount = #children
1365
+ if childCount > 20 and depth < maxDepth then
1366
+ local classGroups = {}
1367
+ for _, child in ipairs(children) do
1368
+ local className = child.ClassName
1369
+ if not classGroups[className] then
1370
+ classGroups[className] = {}
1371
+ end
1372
+ table.insert(classGroups[className], child)
1373
+ end
1374
+
1375
+ node.childSummary = {}
1376
+ for className, classChildren in pairs(classGroups) do
1377
+ table.insert(node.childSummary, {
1378
+ className = className,
1379
+ count = #classChildren,
1380
+ examples = {
1381
+ classChildren[1] and classChildren[1].Name,
1382
+ classChildren[2] and classChildren[2].Name,
1383
+ },
1384
+ })
1385
+ end
1386
+
1387
+ for className, classChildren in pairs(classGroups) do
1388
+ for i = 1, math.min(3, #classChildren) do
1389
+ table.insert(node.children, getStructure(classChildren[i], depth + 1, currentPath))
1390
+ end
1391
+ if #classChildren > 3 then
1392
+ table.insert(node.children, {
1393
+ name = "... " .. (#classChildren - 3) .. " more " .. className .. " objects",
1394
+ className = "MoreIndicator",
1395
+ path = getInstancePath(instance) .. " [" .. className .. " children]",
1396
+ note = "Use specific path to explore these objects",
1397
+ })
1398
+ end
1399
+ end
1400
+ else
1401
+ for _, child in ipairs(children) do
1402
+ table.insert(node.children, getStructure(child, depth + 1, currentPath))
1403
+ end
1404
+ end
1405
+
1406
+ return node
1407
+ end
1408
+
1409
+ local result = getStructure(startInstance, 0, startPath)
1410
+ result.requestedPath = startPath
1411
+ result.maxDepth = maxDepth
1412
+ result.scriptsOnly = showScriptsOnly
1413
+ result.timestamp = tick()
1414
+
1415
+ return result
1416
+ end
1417
+
1418
+ handlers.setProperty = function(requestData)
1419
+ local instancePath = requestData.instancePath
1420
+ local propertyName = requestData.propertyName
1421
+ local propertyValue = requestData.propertyValue
1422
+
1423
+ if not instancePath or not propertyName then
1424
+ return { error = "Instance path and property name are required" }
1425
+ end
1426
+
1427
+ local instance = getInstanceByPath(instancePath)
1428
+ if not instance then
1429
+ return { error = "Instance not found: " .. instancePath }
1430
+ end
1431
+
1432
+ local success, result = pcall(function()
1433
+ -- Handle instance reference properties (Parent, PrimaryPart, etc.)
1434
+ if propertyName == "Parent" or propertyName == "PrimaryPart" then
1435
+ if type(propertyValue) == "string" then
1436
+ local refInstance = getInstanceByPath(propertyValue)
1437
+ if refInstance then
1438
+ instance[propertyName] = refInstance
1439
+ else
1440
+ return { error = propertyName .. " instance not found: " .. propertyValue }
1441
+ end
1442
+ end
1443
+ elseif propertyName == "Name" then
1444
+ instance.Name = tostring(propertyValue)
1445
+ elseif propertyName == "Source" and instance:IsA("LuaSourceContainer") then
1446
+ instance.Source = tostring(propertyValue)
1447
+ else
1448
+ -- Use the generic converter for all other properties
1449
+ local convertedValue = convertPropertyValue(instance, propertyName, propertyValue)
1450
+ if convertedValue ~= nil then
1451
+ instance[propertyName] = convertedValue
1452
+ else
1453
+ instance[propertyName] = propertyValue
1454
+ end
1455
+ end
1456
+
1457
+ ChangeHistoryService:SetWaypoint("Set " .. propertyName .. " property")
1458
+ return true
1459
+ end)
1460
+
1461
+ if success and result ~= false then
1462
+ return {
1463
+ success = true,
1464
+ instancePath = instancePath,
1465
+ propertyName = propertyName,
1466
+ propertyValue = propertyValue,
1467
+ message = "Property set successfully",
1468
+ }
1469
+ else
1470
+ return {
1471
+ error = "Failed to set property: " .. tostring(result),
1472
+ instancePath = instancePath,
1473
+ propertyName = propertyName,
1474
+ }
1475
+ end
1476
+ end
1477
+
1478
+ handlers.createObject = function(requestData)
1479
+ local className = requestData.className
1480
+ local parentPath = requestData.parent
1481
+ local name = requestData.name
1482
+ local properties = requestData.properties or {}
1483
+
1484
+ if not className or not parentPath then
1485
+ return { error = "Class name and parent are required" }
1486
+ end
1487
+
1488
+ local parentInstance = getInstanceByPath(parentPath)
1489
+ if not parentInstance then
1490
+ return { error = "Parent instance not found: " .. parentPath }
1491
+ end
1492
+
1493
+ local success, newInstance = pcall(function()
1494
+ local instance = Instance.new(className)
1495
+
1496
+ if name then
1497
+ instance.Name = name
1498
+ end
1499
+
1500
+ for propertyName, propertyValue in pairs(properties) do
1501
+ pcall(function()
1502
+ instance[propertyName] = propertyValue
1503
+ end)
1504
+ end
1505
+
1506
+ instance.Parent = parentInstance
1507
+ ChangeHistoryService:SetWaypoint("Create " .. className)
1508
+ return instance
1509
+ end)
1510
+
1511
+ if success and newInstance then
1512
+ return {
1513
+ success = true,
1514
+ className = className,
1515
+ parent = parentPath,
1516
+ instancePath = getInstancePath(newInstance),
1517
+ name = newInstance.Name,
1518
+ message = "Object created successfully",
1519
+ }
1520
+ else
1521
+ return {
1522
+ error = "Failed to create object: " .. tostring(newInstance),
1523
+ className = className,
1524
+ parent = parentPath,
1525
+ }
1526
+ end
1527
+ end
1528
+
1529
+ handlers.deleteObject = function(requestData)
1530
+ local instancePath = requestData.instancePath
1531
+
1532
+ if not instancePath then
1533
+ return { error = "Instance path is required" }
1534
+ end
1535
+
1536
+ local instance = getInstanceByPath(instancePath)
1537
+ if not instance then
1538
+ return { error = "Instance not found: " .. instancePath }
1539
+ end
1540
+
1541
+ if instance == game then
1542
+ return { error = "Cannot delete the game instance" }
1543
+ end
1544
+
1545
+ local success, result = pcall(function()
1546
+ local name = instance.Name
1547
+ local className = instance.ClassName
1548
+ instance:Destroy()
1549
+ ChangeHistoryService:SetWaypoint("Delete " .. className .. " (" .. name .. ")")
1550
+ return true
1551
+ end)
1552
+
1553
+ if success then
1554
+ return {
1555
+ success = true,
1556
+ instancePath = instancePath,
1557
+ message = "Object deleted successfully",
1558
+ }
1559
+ else
1560
+ return {
1561
+ error = "Failed to delete object: " .. tostring(result),
1562
+ instancePath = instancePath,
1563
+ }
1564
+ end
1565
+ end
1566
+
1567
+ handlers.massSetProperty = function(requestData)
1568
+ local paths = requestData.paths
1569
+ local propertyName = requestData.propertyName
1570
+ local propertyValue = requestData.propertyValue
1571
+
1572
+ if not paths or type(paths) ~= "table" or #paths == 0 or not propertyName then
1573
+ return { error = "Paths array and property name are required" }
1574
+ end
1575
+
1576
+ local results = {}
1577
+ local successCount = 0
1578
+ local failureCount = 0
1579
+
1580
+ for _, path in ipairs(paths) do
1581
+ local instance = getInstanceByPath(path)
1582
+ if instance then
1583
+ local success, err = pcall(function()
1584
+ instance[propertyName] = propertyValue
1585
+ end)
1586
+
1587
+ if success then
1588
+ successCount = successCount + 1
1589
+ table.insert(results, {
1590
+ path = path,
1591
+ success = true,
1592
+ propertyName = propertyName,
1593
+ propertyValue = propertyValue
1594
+ })
1595
+ else
1596
+ failureCount = failureCount + 1
1597
+ table.insert(results, {
1598
+ path = path,
1599
+ success = false,
1600
+ error = tostring(err)
1601
+ })
1602
+ end
1603
+ else
1604
+ failureCount = failureCount + 1
1605
+ table.insert(results, {
1606
+ path = path,
1607
+ success = false,
1608
+ error = "Instance not found"
1609
+ })
1610
+ end
1611
+ end
1612
+
1613
+ if successCount > 0 then
1614
+ ChangeHistoryService:SetWaypoint("Mass set " .. propertyName .. " property")
1615
+ end
1616
+
1617
+ return {
1618
+ results = results,
1619
+ summary = {
1620
+ total = #paths,
1621
+ succeeded = successCount,
1622
+ failed = failureCount
1623
+ }
1624
+ }
1625
+ end
1626
+
1627
+ handlers.massGetProperty = function(requestData)
1628
+ local paths = requestData.paths
1629
+ local propertyName = requestData.propertyName
1630
+
1631
+ if not paths or type(paths) ~= "table" or #paths == 0 or not propertyName then
1632
+ return { error = "Paths array and property name are required" }
1633
+ end
1634
+
1635
+ local results = {}
1636
+
1637
+ for _, path in ipairs(paths) do
1638
+ local instance = getInstanceByPath(path)
1639
+ if instance then
1640
+ local success, value = pcall(function()
1641
+ return instance[propertyName]
1642
+ end)
1643
+
1644
+ if success then
1645
+ table.insert(results, {
1646
+ path = path,
1647
+ success = true,
1648
+ propertyName = propertyName,
1649
+ propertyValue = value
1650
+ })
1651
+ else
1652
+ table.insert(results, {
1653
+ path = path,
1654
+ success = false,
1655
+ error = tostring(value)
1656
+ })
1657
+ end
1658
+ else
1659
+ table.insert(results, {
1660
+ path = path,
1661
+ success = false,
1662
+ error = "Instance not found"
1663
+ })
1664
+ end
1665
+ end
1666
+
1667
+ return {
1668
+ results = results,
1669
+ propertyName = propertyName
1670
+ }
1671
+ end
1672
+
1673
+ handlers.massCreateObjects = function(requestData)
1674
+ local objects = requestData.objects
1675
+
1676
+ if not objects or type(objects) ~= "table" or #objects == 0 then
1677
+ return { error = "Objects array is required" }
1678
+ end
1679
+
1680
+ local results = {}
1681
+ local successCount = 0
1682
+ local failureCount = 0
1683
+
1684
+ for _, objData in ipairs(objects) do
1685
+ local className = objData.className
1686
+ local parentPath = objData.parent
1687
+ local name = objData.name
1688
+
1689
+ if className and parentPath then
1690
+ local parentInstance = getInstanceByPath(parentPath)
1691
+ if parentInstance then
1692
+ local success, newInstance = pcall(function()
1693
+ local instance = Instance.new(className)
1694
+ if name then
1695
+ instance.Name = name
1696
+ end
1697
+ instance.Parent = parentInstance
1698
+ return instance
1699
+ end)
1700
+
1701
+ if success and newInstance then
1702
+ successCount = successCount + 1
1703
+ table.insert(results, {
1704
+ success = true,
1705
+ className = className,
1706
+ parent = parentPath,
1707
+ instancePath = getInstancePath(newInstance),
1708
+ name = newInstance.Name
1709
+ })
1710
+ else
1711
+ failureCount = failureCount + 1
1712
+ table.insert(results, {
1713
+ success = false,
1714
+ className = className,
1715
+ parent = parentPath,
1716
+ error = tostring(newInstance)
1717
+ })
1718
+ end
1719
+ else
1720
+ failureCount = failureCount + 1
1721
+ table.insert(results, {
1722
+ success = false,
1723
+ className = className,
1724
+ parent = parentPath,
1725
+ error = "Parent instance not found"
1726
+ })
1727
+ end
1728
+ else
1729
+ failureCount = failureCount + 1
1730
+ table.insert(results, {
1731
+ success = false,
1732
+ error = "Class name and parent are required"
1733
+ })
1734
+ end
1735
+ end
1736
+
1737
+ if successCount > 0 then
1738
+ ChangeHistoryService:SetWaypoint("Mass create objects")
1739
+ end
1740
+
1741
+ return {
1742
+ results = results,
1743
+ summary = {
1744
+ total = #objects,
1745
+ succeeded = successCount,
1746
+ failed = failureCount
1747
+ }
1748
+ }
1749
+ end
1750
+
1751
+ handlers.massCreateObjectsWithProperties = function(requestData)
1752
+ local objects = requestData.objects
1753
+
1754
+ if not objects or type(objects) ~= "table" or #objects == 0 then
1755
+ return { error = "Objects array is required" }
1756
+ end
1757
+
1758
+ local results = {}
1759
+ local successCount = 0
1760
+ local failureCount = 0
1761
+
1762
+ for _, objData in ipairs(objects) do
1763
+ local className = objData.className
1764
+ local parentPath = objData.parent
1765
+ local name = objData.name
1766
+ local properties = objData.properties or {}
1767
+
1768
+ if className and parentPath then
1769
+ local parentInstance = getInstanceByPath(parentPath)
1770
+ if parentInstance then
1771
+ local success, newInstance = pcall(function()
1772
+ local instance = Instance.new(className)
1773
+
1774
+ if name then
1775
+ instance.Name = name
1776
+ end
1777
+
1778
+ -- Set Parent first so property type inference works
1779
+ instance.Parent = parentInstance
1780
+
1781
+ for propertyName, propertyValue in pairs(properties) do
1782
+ pcall(function()
1783
+ local convertedValue = convertPropertyValue(instance, propertyName, propertyValue)
1784
+ if convertedValue ~= nil then
1785
+ instance[propertyName] = convertedValue
1786
+ end
1787
+ end)
1788
+ end
1789
+
1790
+ return instance
1791
+ end)
1792
+
1793
+ if success and newInstance then
1794
+ successCount = successCount + 1
1795
+ table.insert(results, {
1796
+ success = true,
1797
+ className = className,
1798
+ parent = parentPath,
1799
+ instancePath = getInstancePath(newInstance),
1800
+ name = newInstance.Name
1801
+ })
1802
+ else
1803
+ failureCount = failureCount + 1
1804
+ table.insert(results, {
1805
+ success = false,
1806
+ className = className,
1807
+ parent = parentPath,
1808
+ error = tostring(newInstance)
1809
+ })
1810
+ end
1811
+ else
1812
+ failureCount = failureCount + 1
1813
+ table.insert(results, {
1814
+ success = false,
1815
+ className = className,
1816
+ parent = parentPath,
1817
+ error = "Parent instance not found"
1818
+ })
1819
+ end
1820
+ else
1821
+ failureCount = failureCount + 1
1822
+ table.insert(results, {
1823
+ success = false,
1824
+ error = "Class name and parent are required"
1825
+ })
1826
+ end
1827
+ end
1828
+
1829
+ if successCount > 0 then
1830
+ ChangeHistoryService:SetWaypoint("Mass create objects with properties")
1831
+ end
1832
+
1833
+ return {
1834
+ results = results,
1835
+ summary = {
1836
+ total = #objects,
1837
+ succeeded = successCount,
1838
+ failed = failureCount
1839
+ }
1840
+ }
1841
+ end
1842
+
1843
+ handlers.smartDuplicate = function(requestData)
1844
+ local instancePath = requestData.instancePath
1845
+ local count = requestData.count
1846
+ local options = requestData.options or {}
1847
+
1848
+ if not instancePath or not count or count < 1 then
1849
+ return { error = "Instance path and count > 0 are required" }
1850
+ end
1851
+
1852
+ local instance = getInstanceByPath(instancePath)
1853
+ if not instance then
1854
+ return { error = "Instance not found: " .. instancePath }
1855
+ end
1856
+
1857
+ local results = {}
1858
+ local successCount = 0
1859
+ local failureCount = 0
1860
+
1861
+ for i = 1, count do
1862
+ local success, newInstance = pcall(function()
1863
+ local clone = instance:Clone()
1864
+
1865
+ if options.namePattern then
1866
+ clone.Name = options.namePattern:gsub("{n}", tostring(i))
1867
+ else
1868
+ clone.Name = instance.Name .. i
1869
+ end
1870
+
1871
+ if options.positionOffset and clone:IsA("BasePart") then
1872
+ local offset = options.positionOffset
1873
+ local currentPos = clone.Position
1874
+ clone.Position = Vector3.new(
1875
+ currentPos.X + (offset[1] or 0) * i,
1876
+ currentPos.Y + (offset[2] or 0) * i,
1877
+ currentPos.Z + (offset[3] or 0) * i
1878
+ )
1879
+ end
1880
+
1881
+ if options.rotationOffset and clone:IsA("BasePart") then
1882
+ local offset = options.rotationOffset
1883
+ local currentCFrame = clone.CFrame
1884
+ clone.CFrame = currentCFrame * CFrame.Angles(
1885
+ math.rad((offset[1] or 0) * i),
1886
+ math.rad((offset[2] or 0) * i),
1887
+ math.rad((offset[3] or 0) * i)
1888
+ )
1889
+ end
1890
+
1891
+ if options.scaleOffset and clone:IsA("BasePart") then
1892
+ local offset = options.scaleOffset
1893
+ local currentSize = clone.Size
1894
+ clone.Size = Vector3.new(
1895
+ currentSize.X * ((offset[1] or 1) ^ i),
1896
+ currentSize.Y * ((offset[2] or 1) ^ i),
1897
+ currentSize.Z * ((offset[3] or 1) ^ i)
1898
+ )
1899
+ end
1900
+
1901
+ if options.propertyVariations then
1902
+ for propName, values in pairs(options.propertyVariations) do
1903
+ if values and #values > 0 then
1904
+ local valueIndex = ((i - 1) % #values) + 1
1905
+ pcall(function()
1906
+ clone[propName] = values[valueIndex]
1907
+ end)
1908
+ end
1909
+ end
1910
+ end
1911
+
1912
+ if options.targetParents and options.targetParents[i] then
1913
+ local targetParent = getInstanceByPath(options.targetParents[i])
1914
+ if targetParent then
1915
+ clone.Parent = targetParent
1916
+ else
1917
+ clone.Parent = instance.Parent
1918
+ end
1919
+ else
1920
+ clone.Parent = instance.Parent
1921
+ end
1922
+
1923
+ return clone
1924
+ end)
1925
+
1926
+ if success and newInstance then
1927
+ successCount = successCount + 1
1928
+ table.insert(results, {
1929
+ success = true,
1930
+ instancePath = getInstancePath(newInstance),
1931
+ name = newInstance.Name,
1932
+ index = i
1933
+ })
1934
+ else
1935
+ failureCount = failureCount + 1
1936
+ table.insert(results, {
1937
+ success = false,
1938
+ index = i,
1939
+ error = tostring(newInstance)
1940
+ })
1941
+ end
1942
+ end
1943
+
1944
+ if successCount > 0 then
1945
+ ChangeHistoryService:SetWaypoint("Smart duplicate " .. instance.Name .. " (" .. successCount .. " copies)")
1946
+ end
1947
+
1948
+ return {
1949
+ results = results,
1950
+ summary = {
1951
+ total = count,
1952
+ succeeded = successCount,
1953
+ failed = failureCount
1954
+ },
1955
+ sourceInstance = instancePath
1956
+ }
1957
+ end
1958
+
1959
+ handlers.massDuplicate = function(requestData)
1960
+ local duplications = requestData.duplications
1961
+
1962
+ if not duplications or type(duplications) ~= "table" or #duplications == 0 then
1963
+ return { error = "Duplications array is required" }
1964
+ end
1965
+
1966
+ local allResults = {}
1967
+ local totalSuccess = 0
1968
+ local totalFailures = 0
1969
+
1970
+ for _, duplication in ipairs(duplications) do
1971
+ local result = handlers.smartDuplicate(duplication)
1972
+ table.insert(allResults, result)
1973
+
1974
+ if result.summary then
1975
+ totalSuccess = totalSuccess + result.summary.succeeded
1976
+ totalFailures = totalFailures + result.summary.failed
1977
+ end
1978
+ end
1979
+
1980
+ if totalSuccess > 0 then
1981
+ ChangeHistoryService:SetWaypoint("Mass duplicate operations (" .. totalSuccess .. " objects)")
1982
+ end
1983
+
1984
+ return {
1985
+ results = allResults,
1986
+ summary = {
1987
+ total = totalSuccess + totalFailures,
1988
+ succeeded = totalSuccess,
1989
+ failed = totalFailures
1990
+ }
1991
+ }
1992
+ end
1993
+
1994
+ local function evaluateFormula(formula, variables, instance, index)
1995
+
1996
+ local value = formula
1997
+
1998
+ value = value:gsub("index", tostring(index))
1999
+
2000
+ if instance and instance:IsA("BasePart") then
2001
+ local pos = instance.Position
2002
+ local size = instance.Size
2003
+ value = value:gsub("Position%.X", tostring(pos.X))
2004
+ value = value:gsub("Position%.Y", tostring(pos.Y))
2005
+ value = value:gsub("Position%.Z", tostring(pos.Z))
2006
+ value = value:gsub("Size%.X", tostring(size.X))
2007
+ value = value:gsub("Size%.Y", tostring(size.Y))
2008
+ value = value:gsub("Size%.Z", tostring(size.Z))
2009
+ value = value:gsub("magnitude", tostring(pos.magnitude))
2010
+ end
2011
+
2012
+ if variables then
2013
+ for k, v in pairs(variables) do
2014
+ value = value:gsub(k, tostring(v))
2015
+ end
2016
+ end
2017
+
2018
+ value = value:gsub("sin%(([%d%.%-]+)%)", function(x) return tostring(math.sin(tonumber(x) or 0)) end)
2019
+ value = value:gsub("cos%(([%d%.%-]+)%)", function(x) return tostring(math.cos(tonumber(x) or 0)) end)
2020
+ value = value:gsub("sqrt%(([%d%.%-]+)%)", function(x) return tostring(math.sqrt(tonumber(x) or 0)) end)
2021
+ value = value:gsub("abs%(([%d%.%-]+)%)", function(x) return tostring(math.abs(tonumber(x) or 0)) end)
2022
+ value = value:gsub("floor%(([%d%.%-]+)%)", function(x) return tostring(math.floor(tonumber(x) or 0)) end)
2023
+ value = value:gsub("ceil%(([%d%.%-]+)%)", function(x) return tostring(math.ceil(tonumber(x) or 0)) end)
2024
+
2025
+ local result = tonumber(value)
2026
+ if result then
2027
+ return result, nil
2028
+ end
2029
+
2030
+ local success, evalResult = pcall(function()
2031
+
2032
+ local num = tonumber(value)
2033
+ if num then
2034
+ return num
2035
+ end
2036
+
2037
+ local a, b = value:match("^([%d%.%-]+)%s*%*%s*([%d%.%-]+)$")
2038
+ if a and b then
2039
+ return (tonumber(a) or 0) * (tonumber(b) or 0)
2040
+ end
2041
+
2042
+ a, b = value:match("^([%d%.%-]+)%s*%+%s*([%d%.%-]+)$")
2043
+ if a and b then
2044
+ return (tonumber(a) or 0) + (tonumber(b) or 0)
2045
+ end
2046
+
2047
+ a, b = value:match("^([%d%.%-]+)%s*%-%s*([%d%.%-]+)$")
2048
+ if a and b then
2049
+ return (tonumber(a) or 0) - (tonumber(b) or 0)
2050
+ end
2051
+
2052
+ a, b = value:match("^([%d%.%-]+)%s*/%s*([%d%.%-]+)$")
2053
+ if a and b then
2054
+ local divisor = tonumber(b) or 1
2055
+ if divisor ~= 0 then
2056
+ return (tonumber(a) or 0) / divisor
2057
+ end
2058
+ end
2059
+
2060
+ error("Unsupported formula pattern: " .. value)
2061
+ end)
2062
+
2063
+ if success and type(evalResult) == "number" then
2064
+ return evalResult, nil
2065
+ else
2066
+ return index, "Complex formulas not supported - using index value"
2067
+ end
2068
+ end
2069
+
2070
+ handlers.setCalculatedProperty = function(requestData)
2071
+ local paths = requestData.paths
2072
+ local propertyName = requestData.propertyName
2073
+ local formula = requestData.formula
2074
+ local variables = requestData.variables
2075
+
2076
+ if not paths or type(paths) ~= "table" or #paths == 0 or not propertyName or not formula then
2077
+ return { error = "Paths, property name, and formula are required" }
2078
+ end
2079
+
2080
+ local results = {}
2081
+ local successCount = 0
2082
+ local failureCount = 0
2083
+
2084
+ for index, path in ipairs(paths) do
2085
+ local instance = getInstanceByPath(path)
2086
+ if instance then
2087
+ local value, evalError = evaluateFormula(formula, variables, instance, index)
2088
+
2089
+ if value ~= nil and not evalError then
2090
+ local success, err = pcall(function()
2091
+ instance[propertyName] = value
2092
+ end)
2093
+
2094
+ if success then
2095
+ successCount = successCount + 1
2096
+ table.insert(results, {
2097
+ path = path,
2098
+ success = true,
2099
+ propertyName = propertyName,
2100
+ calculatedValue = value,
2101
+ formula = formula
2102
+ })
2103
+ else
2104
+ failureCount = failureCount + 1
2105
+ table.insert(results, {
2106
+ path = path,
2107
+ success = false,
2108
+ error = "Property set failed: " .. tostring(err)
2109
+ })
2110
+ end
2111
+ else
2112
+ failureCount = failureCount + 1
2113
+ table.insert(results, {
2114
+ path = path,
2115
+ success = false,
2116
+ error = evalError or "Formula evaluation failed"
2117
+ })
2118
+ end
2119
+ else
2120
+ failureCount = failureCount + 1
2121
+ table.insert(results, {
2122
+ path = path,
2123
+ success = false,
2124
+ error = "Instance not found"
2125
+ })
2126
+ end
2127
+ end
2128
+
2129
+ if successCount > 0 then
2130
+ ChangeHistoryService:SetWaypoint("Set calculated " .. propertyName .. " property")
2131
+ end
2132
+
2133
+ return {
2134
+ results = results,
2135
+ summary = {
2136
+ total = #paths,
2137
+ succeeded = successCount,
2138
+ failed = failureCount
2139
+ },
2140
+ formula = formula
2141
+ }
2142
+ end
2143
+
2144
+ handlers.setRelativeProperty = function(requestData)
2145
+ local paths = requestData.paths
2146
+ local propertyName = requestData.propertyName
2147
+ local operation = requestData.operation
2148
+ local value = requestData.value
2149
+ local component = requestData.component
2150
+
2151
+ if not paths or type(paths) ~= "table" or #paths == 0 or not propertyName or not operation or value == nil then
2152
+ return { error = "Paths, property name, operation, and value are required" }
2153
+ end
2154
+
2155
+ local results = {}
2156
+ local successCount = 0
2157
+ local failureCount = 0
2158
+
2159
+ for _, path in ipairs(paths) do
2160
+ local instance = getInstanceByPath(path)
2161
+ if instance then
2162
+ local success, err = pcall(function()
2163
+ local currentValue = instance[propertyName]
2164
+ local newValue
2165
+
2166
+ if component and typeof(currentValue) == "Vector3" then
2167
+ local x, y, z = currentValue.X, currentValue.Y, currentValue.Z
2168
+ local targetValue = value
2169
+
2170
+ if component == "X" then
2171
+ if operation == "add" then x = x + targetValue
2172
+ elseif operation == "subtract" then x = x - targetValue
2173
+ elseif operation == "multiply" then x = x * targetValue
2174
+ elseif operation == "divide" then x = x / targetValue
2175
+ elseif operation == "power" then x = x ^ targetValue
2176
+ end
2177
+ elseif component == "Y" then
2178
+ if operation == "add" then y = y + targetValue
2179
+ elseif operation == "subtract" then y = y - targetValue
2180
+ elseif operation == "multiply" then y = y * targetValue
2181
+ elseif operation == "divide" then y = y / targetValue
2182
+ elseif operation == "power" then y = y ^ targetValue
2183
+ end
2184
+ elseif component == "Z" then
2185
+ if operation == "add" then z = z + targetValue
2186
+ elseif operation == "subtract" then z = z - targetValue
2187
+ elseif operation == "multiply" then z = z * targetValue
2188
+ elseif operation == "divide" then z = z / targetValue
2189
+ elseif operation == "power" then z = z ^ targetValue
2190
+ end
2191
+ end
2192
+
2193
+ newValue = Vector3.new(x, y, z)
2194
+ elseif typeof(currentValue) == "Color3" and typeof(value) == "Color3" then
2195
+ local r, g, b = currentValue.R, currentValue.G, currentValue.B
2196
+
2197
+ if operation == "add" then
2198
+ newValue = Color3.new(
2199
+ math.min(1, r + value.R),
2200
+ math.min(1, g + value.G),
2201
+ math.min(1, b + value.B)
2202
+ )
2203
+ elseif operation == "subtract" then
2204
+ newValue = Color3.new(
2205
+ math.max(0, r - value.R),
2206
+ math.max(0, g - value.G),
2207
+ math.max(0, b - value.B)
2208
+ )
2209
+ elseif operation == "multiply" then
2210
+ newValue = Color3.new(r * value.R, g * value.G, b * value.B)
2211
+ end
2212
+ elseif type(currentValue) == "number" and type(value) == "number" then
2213
+ if operation == "add" then
2214
+ newValue = currentValue + value
2215
+ elseif operation == "subtract" then
2216
+ newValue = currentValue - value
2217
+ elseif operation == "multiply" then
2218
+ newValue = currentValue * value
2219
+ elseif operation == "divide" then
2220
+ newValue = currentValue / value
2221
+ elseif operation == "power" then
2222
+ newValue = currentValue ^ value
2223
+ end
2224
+ elseif typeof(currentValue) == "Vector3" and type(value) == "number" then
2225
+ local x, y, z = currentValue.X, currentValue.Y, currentValue.Z
2226
+
2227
+ if operation == "add" then
2228
+ newValue = Vector3.new(x + value, y + value, z + value)
2229
+ elseif operation == "subtract" then
2230
+ newValue = Vector3.new(x - value, y - value, z - value)
2231
+ elseif operation == "multiply" then
2232
+ newValue = Vector3.new(x * value, y * value, z * value)
2233
+ elseif operation == "divide" then
2234
+ newValue = Vector3.new(x / value, y / value, z / value)
2235
+ elseif operation == "power" then
2236
+ newValue = Vector3.new(x ^ value, y ^ value, z ^ value)
2237
+ end
2238
+ else
2239
+ error("Unsupported property type or operation")
2240
+ end
2241
+
2242
+ instance[propertyName] = newValue
2243
+ return newValue
2244
+ end)
2245
+
2246
+ if success then
2247
+ successCount = successCount + 1
2248
+ table.insert(results, {
2249
+ path = path,
2250
+ success = true,
2251
+ propertyName = propertyName,
2252
+ operation = operation,
2253
+ value = value,
2254
+ component = component,
2255
+ newValue = err
2256
+ })
2257
+ else
2258
+ failureCount = failureCount + 1
2259
+ table.insert(results, {
2260
+ path = path,
2261
+ success = false,
2262
+ error = tostring(err)
2263
+ })
2264
+ end
2265
+ else
2266
+ failureCount = failureCount + 1
2267
+ table.insert(results, {
2268
+ path = path,
2269
+ success = false,
2270
+ error = "Instance not found"
2271
+ })
2272
+ end
2273
+ end
2274
+
2275
+ if successCount > 0 then
2276
+ ChangeHistoryService:SetWaypoint("Set relative " .. propertyName .. " property")
2277
+ end
2278
+
2279
+ return {
2280
+ results = results,
2281
+ summary = {
2282
+ total = #paths,
2283
+ succeeded = successCount,
2284
+ failed = failureCount
2285
+ },
2286
+ operation = operation,
2287
+ value = value
2288
+ }
2289
+ end
2290
+
2291
+ handlers.getScriptSource = function(requestData)
2292
+ local instancePath = requestData.instancePath
2293
+ local startLine = requestData.startLine
2294
+ local endLine = requestData.endLine
2295
+
2296
+ if not instancePath then
2297
+ return { error = "Instance path is required" }
2298
+ end
2299
+
2300
+ local instance = getInstanceByPath(instancePath)
2301
+ if not instance then
2302
+ return { error = "Instance not found: " .. instancePath }
2303
+ end
2304
+
2305
+ if not instance:IsA("LuaSourceContainer") then
2306
+ return { error = "Instance is not a script-like object: " .. instance.ClassName }
2307
+ end
2308
+
2309
+ local success, result = pcall(function()
2310
+ local fullSource = instance.Source
2311
+ local lines, hasTrailingNewline = splitLines(fullSource)
2312
+ local totalLineCount = #lines
2313
+
2314
+ -- If line range is specified, extract only those lines
2315
+ local sourceToReturn = fullSource
2316
+ local returnedStartLine = 1
2317
+ local returnedEndLine = totalLineCount
2318
+
2319
+ if startLine or endLine then
2320
+ local actualStartLine = math.max(1, startLine or 1)
2321
+ local actualEndLine = math.min(#lines, endLine or #lines)
2322
+
2323
+ local selectedLines = {}
2324
+ for i = actualStartLine, actualEndLine do
2325
+ table.insert(selectedLines, lines[i] or "")
2326
+ end
2327
+
2328
+ sourceToReturn = table.concat(selectedLines, '\n')
2329
+ if hasTrailingNewline and actualEndLine == #lines and sourceToReturn:sub(-1) ~= "\n" then
2330
+ sourceToReturn ..= "\n"
2331
+ end
2332
+ returnedStartLine = actualStartLine
2333
+ returnedEndLine = actualEndLine
2334
+ end
2335
+
2336
+ -- Build numbered source for AI agents to accurately identify line numbers
2337
+ local numberedLines = {}
2338
+ local linesToNumber = startLine and select(1, splitLines(sourceToReturn)) or lines
2339
+ local lineOffset = returnedStartLine - 1
2340
+ for i, line in ipairs(linesToNumber) do
2341
+ table.insert(numberedLines, (i + lineOffset) .. ": " .. line)
2342
+ end
2343
+ local numberedSource = table.concat(numberedLines, "\n")
2344
+
2345
+ local resp = {
2346
+ instancePath = instancePath,
2347
+ className = instance.ClassName,
2348
+ name = instance.Name,
2349
+ source = sourceToReturn,
2350
+ numberedSource = numberedSource,
2351
+ sourceLength = string.len(fullSource),
2352
+ lineCount = totalLineCount,
2353
+ -- Line range info
2354
+ startLine = returnedStartLine,
2355
+ endLine = returnedEndLine,
2356
+ isPartial = (startLine ~= nil or endLine ~= nil),
2357
+ -- Helpful metadata for large scripts
2358
+ truncated = false,
2359
+ }
2360
+
2361
+ -- If the source is very large (>50000 chars) and no range specified,
2362
+ -- return first 1000 lines with truncation notice
2363
+ if not startLine and not endLine and string.len(fullSource) > 50000 then
2364
+ local truncatedLines = {}
2365
+ local truncatedNumberedLines = {}
2366
+ local maxLines = math.min(1000, #lines)
2367
+ for i = 1, maxLines do
2368
+ table.insert(truncatedLines, lines[i])
2369
+ table.insert(truncatedNumberedLines, i .. ": " .. lines[i])
2370
+ end
2371
+ resp.source = table.concat(truncatedLines, '\n')
2372
+ resp.numberedSource = table.concat(truncatedNumberedLines, '\n')
2373
+ resp.truncated = true
2374
+ resp.endLine = maxLines
2375
+ resp.note = "Script truncated to first 1000 lines. Use startLine/endLine parameters to read specific sections."
2376
+ end
2377
+
2378
+ if instance:IsA("BaseScript") then
2379
+ resp.enabled = instance.Enabled
2380
+ end
2381
+ return resp
2382
+ end)
2383
+
2384
+ if success then
2385
+ return result
2386
+ else
2387
+ return { error = "Failed to get script source: " .. tostring(result) }
2388
+ end
2389
+ end
2390
+
2391
+ handlers.setScriptSource = function(requestData)
2392
+ local instancePath = requestData.instancePath
2393
+ local newSource = requestData.source
2394
+
2395
+ if not instancePath or not newSource then
2396
+ return { error = "Instance path and source are required" }
2397
+ end
2398
+
2399
+ local instance = getInstanceByPath(instancePath)
2400
+ if not instance then
2401
+ return { error = "Instance not found: " .. instancePath }
2402
+ end
2403
+
2404
+ if not instance:IsA("LuaSourceContainer") then
2405
+ return { error = "Instance is not a script-like object: " .. instance.ClassName }
2406
+ end
2407
+
2408
+ -- Normalize escape sequences that may have been double-escaped
2409
+ local sourceToSet = newSource :: string
2410
+ -- Fix double-escaped newlines, tabs, etc.
2411
+ sourceToSet = sourceToSet:gsub("\\n", "\n")
2412
+ sourceToSet = sourceToSet:gsub("\\t", "\t")
2413
+ sourceToSet = sourceToSet:gsub("\\r", "\r")
2414
+ sourceToSet = sourceToSet:gsub("\\\\", "\\")
2415
+ local updateSuccess, updateResult = pcall(function()
2416
+ local oldSourceLength = string.len(instance.Source)
2417
+
2418
+ ScriptEditorService:UpdateSourceAsync(instance, function(oldContent)
2419
+ return sourceToSet
2420
+ end)
2421
+
2422
+ ChangeHistoryService:SetWaypoint("Set script source: " .. instance.Name)
2423
+
2424
+ return {
2425
+ success = true,
2426
+ instancePath = instancePath,
2427
+ oldSourceLength = oldSourceLength,
2428
+ newSourceLength = string.len(sourceToSet),
2429
+ method = "UpdateSourceAsync",
2430
+ message = "Script source updated successfully (editor-safe)"
2431
+ }
2432
+ end)
2433
+
2434
+ if updateSuccess then
2435
+ return updateResult
2436
+ end
2437
+
2438
+ -- Fallback to direct assignment if UpdateSourceAsync fails
2439
+ local directSuccess, directResult = pcall(function()
2440
+ local oldSource = instance.Source
2441
+ instance.Source = sourceToSet
2442
+
2443
+ ChangeHistoryService:SetWaypoint("Set script source: " .. instance.Name)
2444
+
2445
+ return {
2446
+ success = true,
2447
+ instancePath = instancePath,
2448
+ oldSourceLength = string.len(oldSource),
2449
+ newSourceLength = string.len(sourceToSet),
2450
+ method = "direct",
2451
+ message = "Script source updated successfully (direct assignment)"
2452
+ }
2453
+ end)
2454
+
2455
+ if directSuccess then
2456
+ return directResult
2457
+ end
2458
+
2459
+ -- Final fallback: replace the script entirely
2460
+ local replaceSuccess, replaceResult = pcall(function()
2461
+ local parent = instance.Parent
2462
+ local name = instance.Name
2463
+ local className = instance.ClassName
2464
+ local wasBaseScript = instance:IsA("BaseScript")
2465
+ local enabled
2466
+ if wasBaseScript then
2467
+ enabled = instance.Enabled
2468
+ end
2469
+
2470
+ local newScript = Instance.new(className)
2471
+ newScript.Name = name
2472
+ newScript.Source = sourceToSet
2473
+ if wasBaseScript then
2474
+ newScript.Enabled = enabled
2475
+ end
2476
+
2477
+ newScript.Parent = parent
2478
+ instance:Destroy()
2479
+
2480
+ ChangeHistoryService:SetWaypoint("Replace script: " .. name)
2481
+
2482
+ return {
2483
+ success = true,
2484
+ instancePath = getInstancePath(newScript),
2485
+ method = "replace",
2486
+ message = "Script replaced successfully with new source"
2487
+ }
2488
+ end)
2489
+
2490
+ if replaceSuccess then
2491
+ return replaceResult
2492
+ else
2493
+ return {
2494
+ error = "Failed to set script source. UpdateSourceAsync failed: " .. tostring(updateResult) ..
2495
+ ". Direct assignment failed: " .. tostring(directResult) ..
2496
+ ". Replace method failed: " .. tostring(replaceResult)
2497
+ }
2498
+ end
2499
+ end
2500
+
2501
+ -- Partial Script Editing: Edit specific lines
2502
+ handlers.editScriptLines = function(requestData)
2503
+ local instancePath = requestData.instancePath
2504
+ local startLine = requestData.startLine
2505
+ local endLine = requestData.endLine
2506
+ local newContent = requestData.newContent
2507
+
2508
+ if not instancePath or not startLine or not endLine or not newContent then
2509
+ return { error = "Instance path, startLine, endLine, and newContent are required" }
2510
+ end
2511
+
2512
+ -- Normalize escape sequences that may have been double-escaped
2513
+ newContent = newContent:gsub("\\n", "\n")
2514
+ newContent = newContent:gsub("\\t", "\t")
2515
+ newContent = newContent:gsub("\\r", "\r")
2516
+ newContent = newContent:gsub("\\\\", "\\")
2517
+
2518
+ local instance = getInstanceByPath(instancePath)
2519
+ if not instance then
2520
+ return { error = "Instance not found: " .. instancePath }
2521
+ end
2522
+
2523
+ if not instance:IsA("LuaSourceContainer") then
2524
+ return { error = "Instance is not a script-like object: " .. instance.ClassName }
2525
+ end
2526
+
2527
+ local success, result = pcall(function()
2528
+ local lines, hadTrailingNewline = splitLines(instance.Source)
2529
+ local totalLines = #lines
2530
+
2531
+ if startLine < 1 or startLine > totalLines then
2532
+ error("startLine out of range (1-" .. totalLines .. ")")
2533
+ end
2534
+ if endLine < startLine or endLine > totalLines then
2535
+ error("endLine out of range (" .. startLine .. "-" .. totalLines .. ")")
2536
+ end
2537
+
2538
+ -- Split new content into lines
2539
+ local newLines = select(1, splitLines(newContent))
2540
+
2541
+ -- Build new source: lines before + new content + lines after
2542
+ local resultLines = {}
2543
+
2544
+ -- Lines before the edit
2545
+ for i = 1, startLine - 1 do
2546
+ table.insert(resultLines, lines[i])
2547
+ end
2548
+
2549
+ -- New content lines
2550
+ for _, line in ipairs(newLines) do
2551
+ table.insert(resultLines, line)
2552
+ end
2553
+
2554
+ -- Lines after the edit
2555
+ for i = endLine + 1, totalLines do
2556
+ table.insert(resultLines, lines[i])
2557
+ end
2558
+
2559
+ local newSource = joinLines(resultLines, hadTrailingNewline)
2560
+
2561
+ -- Use UpdateSourceAsync for editor compatibility
2562
+ ScriptEditorService:UpdateSourceAsync(instance, function(oldContent)
2563
+ return newSource
2564
+ end)
2565
+
2566
+ ChangeHistoryService:SetWaypoint("Edit script lines " .. startLine .. "-" .. endLine .. ": " .. instance.Name)
2567
+
2568
+ return {
2569
+ success = true,
2570
+ instancePath = instancePath,
2571
+ editedLines = { startLine = startLine, endLine = endLine },
2572
+ linesRemoved = endLine - startLine + 1,
2573
+ linesAdded = #newLines,
2574
+ newLineCount = #resultLines,
2575
+ message = "Script lines edited successfully"
2576
+ }
2577
+ end)
2578
+
2579
+ if success then
2580
+ return result
2581
+ else
2582
+ return { error = "Failed to edit script lines: " .. tostring(result) }
2583
+ end
2584
+ end
2585
+
2586
+ -- Partial Script Editing: Insert lines after a specific line
2587
+ handlers.insertScriptLines = function(requestData)
2588
+ local instancePath = requestData.instancePath
2589
+ local afterLine = requestData.afterLine or 0 -- 0 means insert at beginning
2590
+ local newContent = requestData.newContent
2591
+
2592
+ if not instancePath or not newContent then
2593
+ return { error = "Instance path and newContent are required" }
2594
+ end
2595
+
2596
+ -- Normalize escape sequences that may have been double-escaped
2597
+ newContent = newContent:gsub("\\n", "\n")
2598
+ newContent = newContent:gsub("\\t", "\t")
2599
+ newContent = newContent:gsub("\\r", "\r")
2600
+ newContent = newContent:gsub("\\\\", "\\")
2601
+
2602
+ local instance = getInstanceByPath(instancePath)
2603
+ if not instance then
2604
+ return { error = "Instance not found: " .. instancePath }
2605
+ end
2606
+
2607
+ if not instance:IsA("LuaSourceContainer") then
2608
+ return { error = "Instance is not a script-like object: " .. instance.ClassName }
2609
+ end
2610
+
2611
+ local success, result = pcall(function()
2612
+ local lines, hadTrailingNewline = splitLines(instance.Source)
2613
+ local totalLines = #lines
2614
+
2615
+ if afterLine < 0 or afterLine > totalLines then
2616
+ error("afterLine out of range (0-" .. totalLines .. ")")
2617
+ end
2618
+
2619
+ -- Split new content into lines
2620
+ local newLines = select(1, splitLines(newContent))
2621
+
2622
+ -- Build new source
2623
+ local resultLines = {}
2624
+
2625
+ -- Lines before insertion point
2626
+ for i = 1, afterLine do
2627
+ table.insert(resultLines, lines[i])
2628
+ end
2629
+
2630
+ -- New content lines
2631
+ for _, line in ipairs(newLines) do
2632
+ table.insert(resultLines, line)
2633
+ end
2634
+
2635
+ -- Lines after insertion point
2636
+ for i = afterLine + 1, totalLines do
2637
+ table.insert(resultLines, lines[i])
2638
+ end
2639
+
2640
+ local newSource = joinLines(resultLines, hadTrailingNewline)
2641
+
2642
+ -- Use UpdateSourceAsync for editor compatibility
2643
+ ScriptEditorService:UpdateSourceAsync(instance, function(oldContent)
2644
+ return newSource
2645
+ end)
2646
+
2647
+ ChangeHistoryService:SetWaypoint("Insert script lines after line " .. afterLine .. ": " .. instance.Name)
2648
+
2649
+ return {
2650
+ success = true,
2651
+ instancePath = instancePath,
2652
+ insertedAfterLine = afterLine,
2653
+ linesInserted = #newLines,
2654
+ newLineCount = #resultLines,
2655
+ message = "Script lines inserted successfully"
2656
+ }
2657
+ end)
2658
+
2659
+ if success then
2660
+ return result
2661
+ else
2662
+ return { error = "Failed to insert script lines: " .. tostring(result) }
2663
+ end
2664
+ end
2665
+
2666
+ -- Partial Script Editing: Delete specific lines
2667
+ handlers.deleteScriptLines = function(requestData)
2668
+ local instancePath = requestData.instancePath
2669
+ local startLine = requestData.startLine
2670
+ local endLine = requestData.endLine
2671
+
2672
+ if not instancePath or not startLine or not endLine then
2673
+ return { error = "Instance path, startLine, and endLine are required" }
2674
+ end
2675
+
2676
+ local instance = getInstanceByPath(instancePath)
2677
+ if not instance then
2678
+ return { error = "Instance not found: " .. instancePath }
2679
+ end
2680
+
2681
+ if not instance:IsA("LuaSourceContainer") then
2682
+ return { error = "Instance is not a script-like object: " .. instance.ClassName }
2683
+ end
2684
+
2685
+ local success, result = pcall(function()
2686
+ local lines, hadTrailingNewline = splitLines(instance.Source)
2687
+ local totalLines = #lines
2688
+
2689
+ if startLine < 1 or startLine > totalLines then
2690
+ error("startLine out of range (1-" .. totalLines .. ")")
2691
+ end
2692
+ if endLine < startLine or endLine > totalLines then
2693
+ error("endLine out of range (" .. startLine .. "-" .. totalLines .. ")")
2694
+ end
2695
+
2696
+ -- Build new source without the deleted lines
2697
+ local resultLines = {}
2698
+
2699
+ for i = 1, startLine - 1 do
2700
+ table.insert(resultLines, lines[i])
2701
+ end
2702
+
2703
+ for i = endLine + 1, totalLines do
2704
+ table.insert(resultLines, lines[i])
2705
+ end
2706
+
2707
+ local newSource = joinLines(resultLines, hadTrailingNewline)
2708
+
2709
+ -- Use UpdateSourceAsync for editor compatibility
2710
+ ScriptEditorService:UpdateSourceAsync(instance, function(oldContent)
2711
+ return newSource
2712
+ end)
2713
+
2714
+ ChangeHistoryService:SetWaypoint("Delete script lines " .. startLine .. "-" .. endLine .. ": " .. instance.Name)
2715
+
2716
+ return {
2717
+ success = true,
2718
+ instancePath = instancePath,
2719
+ deletedLines = { startLine = startLine, endLine = endLine },
2720
+ linesDeleted = endLine - startLine + 1,
2721
+ newLineCount = #resultLines,
2722
+ message = "Script lines deleted successfully"
2723
+ }
2724
+ end)
2725
+
2726
+ if success then
2727
+ return result
2728
+ else
2729
+ return { error = "Failed to delete script lines: " .. tostring(result) }
2730
+ end
2731
+ end
2732
+
2733
+ -- Attribute Tools: Get a single attribute
2734
+ handlers.getAttribute = function(requestData)
2735
+ local instancePath = requestData.instancePath
2736
+ local attributeName = requestData.attributeName
2737
+
2738
+ if not instancePath or not attributeName then
2739
+ return { error = "Instance path and attribute name are required" }
2740
+ end
2741
+
2742
+ local instance = getInstanceByPath(instancePath)
2743
+ if not instance then
2744
+ return { error = "Instance not found: " .. instancePath }
2745
+ end
2746
+
2747
+ local success, result = pcall(function()
2748
+ local value = instance:GetAttribute(attributeName)
2749
+ local valueType = typeof(value)
2750
+
2751
+ -- Serialize the value for JSON transport
2752
+ local serializedValue = value
2753
+ if valueType == "Vector3" then
2754
+ serializedValue = { X = value.X, Y = value.Y, Z = value.Z, _type = "Vector3" }
2755
+ elseif valueType == "Color3" then
2756
+ serializedValue = { R = value.R, G = value.G, B = value.B, _type = "Color3" }
2757
+ elseif valueType == "CFrame" then
2758
+ serializedValue = { Position = { X = value.Position.X, Y = value.Position.Y, Z = value.Position.Z }, _type = "CFrame" }
2759
+ elseif valueType == "UDim2" then
2760
+ serializedValue = { X = { Scale = value.X.Scale, Offset = value.X.Offset }, Y = { Scale = value.Y.Scale, Offset = value.Y.Offset }, _type = "UDim2" }
2761
+ elseif valueType == "BrickColor" then
2762
+ serializedValue = { Name = value.Name, _type = "BrickColor" }
2763
+ end
2764
+
2765
+ return {
2766
+ instancePath = instancePath,
2767
+ attributeName = attributeName,
2768
+ value = serializedValue,
2769
+ valueType = valueType,
2770
+ exists = value ~= nil
2771
+ }
2772
+ end)
2773
+
2774
+ if success then
2775
+ return result
2776
+ else
2777
+ return { error = "Failed to get attribute: " .. tostring(result) }
2778
+ end
2779
+ end
2780
+
2781
+ -- Attribute Tools: Set an attribute
2782
+ handlers.setAttribute = function(requestData)
2783
+ local instancePath = requestData.instancePath
2784
+ local attributeName = requestData.attributeName
2785
+ local attributeValue = requestData.attributeValue
2786
+ local valueType = requestData.valueType -- Optional type hint
2787
+
2788
+ if not instancePath or not attributeName then
2789
+ return { error = "Instance path and attribute name are required" }
2790
+ end
2791
+
2792
+ local instance = getInstanceByPath(instancePath)
2793
+ if not instance then
2794
+ return { error = "Instance not found: " .. instancePath }
2795
+ end
2796
+
2797
+ local success, result = pcall(function()
2798
+ local value = attributeValue
2799
+
2800
+ -- Handle special type conversions
2801
+ if type(attributeValue) == "table" then
2802
+ if attributeValue._type == "Vector3" or valueType == "Vector3" then
2803
+ value = Vector3.new(attributeValue.X or 0, attributeValue.Y or 0, attributeValue.Z or 0)
2804
+ elseif attributeValue._type == "Color3" or valueType == "Color3" then
2805
+ value = Color3.new(attributeValue.R or 0, attributeValue.G or 0, attributeValue.B or 0)
2806
+ elseif attributeValue._type == "UDim2" or valueType == "UDim2" then
2807
+ value = UDim2.new(
2808
+ attributeValue.X and attributeValue.X.Scale or 0,
2809
+ attributeValue.X and attributeValue.X.Offset or 0,
2810
+ attributeValue.Y and attributeValue.Y.Scale or 0,
2811
+ attributeValue.Y and attributeValue.Y.Offset or 0
2812
+ )
2813
+ elseif attributeValue._type == "BrickColor" or valueType == "BrickColor" then
2814
+ value = BrickColor.new(attributeValue.Name or "Medium stone grey")
2815
+ end
2816
+ end
2817
+
2818
+ instance:SetAttribute(attributeName, value)
2819
+ ChangeHistoryService:SetWaypoint("Set attribute " .. attributeName .. " on " .. instance.Name)
2820
+
2821
+ return {
2822
+ success = true,
2823
+ instancePath = instancePath,
2824
+ attributeName = attributeName,
2825
+ value = attributeValue,
2826
+ message = "Attribute set successfully"
2827
+ }
2828
+ end)
2829
+
2830
+ if success then
2831
+ return result
2832
+ else
2833
+ return { error = "Failed to set attribute: " .. tostring(result) }
2834
+ end
2835
+ end
2836
+
2837
+ -- Attribute Tools: Get all attributes
2838
+ handlers.getAttributes = function(requestData)
2839
+ local instancePath = requestData.instancePath
2840
+
2841
+ if not instancePath then
2842
+ return { error = "Instance path is required" }
2843
+ end
2844
+
2845
+ local instance = getInstanceByPath(instancePath)
2846
+ if not instance then
2847
+ return { error = "Instance not found: " .. instancePath }
2848
+ end
2849
+
2850
+ local success, result = pcall(function()
2851
+ local attributes = instance:GetAttributes()
2852
+ local serializedAttributes = {}
2853
+
2854
+ for name, value in pairs(attributes) do
2855
+ local valueType = typeof(value)
2856
+ local serializedValue = value
2857
+
2858
+ if valueType == "Vector3" then
2859
+ serializedValue = { X = value.X, Y = value.Y, Z = value.Z, _type = "Vector3" }
2860
+ elseif valueType == "Color3" then
2861
+ serializedValue = { R = value.R, G = value.G, B = value.B, _type = "Color3" }
2862
+ elseif valueType == "CFrame" then
2863
+ serializedValue = { Position = { X = value.Position.X, Y = value.Position.Y, Z = value.Position.Z }, _type = "CFrame" }
2864
+ elseif valueType == "UDim2" then
2865
+ serializedValue = { X = { Scale = value.X.Scale, Offset = value.X.Offset }, Y = { Scale = value.Y.Scale, Offset = value.Y.Offset }, _type = "UDim2" }
2866
+ elseif valueType == "BrickColor" then
2867
+ serializedValue = { Name = value.Name, _type = "BrickColor" }
2868
+ end
2869
+
2870
+ serializedAttributes[name] = {
2871
+ value = serializedValue,
2872
+ type = valueType
2873
+ }
2874
+ end
2875
+
2876
+ local count = 0
2877
+ for _ in pairs(serializedAttributes) do count = count + 1 end
2878
+
2879
+ return {
2880
+ instancePath = instancePath,
2881
+ attributes = serializedAttributes,
2882
+ count = count
2883
+ }
2884
+ end)
2885
+
2886
+ if success then
2887
+ return result
2888
+ else
2889
+ return { error = "Failed to get attributes: " .. tostring(result) }
2890
+ end
2891
+ end
2892
+
2893
+ -- Attribute Tools: Delete an attribute
2894
+ handlers.deleteAttribute = function(requestData)
2895
+ local instancePath = requestData.instancePath
2896
+ local attributeName = requestData.attributeName
2897
+
2898
+ if not instancePath or not attributeName then
2899
+ return { error = "Instance path and attribute name are required" }
2900
+ end
2901
+
2902
+ local instance = getInstanceByPath(instancePath)
2903
+ if not instance then
2904
+ return { error = "Instance not found: " .. instancePath }
2905
+ end
2906
+
2907
+ local success, result = pcall(function()
2908
+ local existed = instance:GetAttribute(attributeName) ~= nil
2909
+ instance:SetAttribute(attributeName, nil)
2910
+ ChangeHistoryService:SetWaypoint("Delete attribute " .. attributeName .. " from " .. instance.Name)
2911
+
2912
+ return {
2913
+ success = true,
2914
+ instancePath = instancePath,
2915
+ attributeName = attributeName,
2916
+ existed = existed,
2917
+ message = existed and "Attribute deleted successfully" or "Attribute did not exist"
2918
+ }
2919
+ end)
2920
+
2921
+ if success then
2922
+ return result
2923
+ else
2924
+ return { error = "Failed to delete attribute: " .. tostring(result) }
2925
+ end
2926
+ end
2927
+
2928
+ -- Tag Tools: Get all tags on an instance
2929
+ handlers.getTags = function(requestData)
2930
+ local instancePath = requestData.instancePath
2931
+
2932
+ if not instancePath then
2933
+ return { error = "Instance path is required" }
2934
+ end
2935
+
2936
+ local instance = getInstanceByPath(instancePath)
2937
+ if not instance then
2938
+ return { error = "Instance not found: " .. instancePath }
2939
+ end
2940
+
2941
+ local success, result = pcall(function()
2942
+ local tags = CollectionService:GetTags(instance)
2943
+
2944
+ return {
2945
+ instancePath = instancePath,
2946
+ tags = tags,
2947
+ count = #tags
2948
+ }
2949
+ end)
2950
+
2951
+ if success then
2952
+ return result
2953
+ else
2954
+ return { error = "Failed to get tags: " .. tostring(result) }
2955
+ end
2956
+ end
2957
+
2958
+ -- Tag Tools: Add a tag to an instance
2959
+ handlers.addTag = function(requestData)
2960
+ local instancePath = requestData.instancePath
2961
+ local tagName = requestData.tagName
2962
+
2963
+ if not instancePath or not tagName then
2964
+ return { error = "Instance path and tag name are required" }
2965
+ end
2966
+
2967
+ local instance = getInstanceByPath(instancePath)
2968
+ if not instance then
2969
+ return { error = "Instance not found: " .. instancePath }
2970
+ end
2971
+
2972
+ local success, result = pcall(function()
2973
+ local alreadyHad = CollectionService:HasTag(instance, tagName)
2974
+ CollectionService:AddTag(instance, tagName)
2975
+ ChangeHistoryService:SetWaypoint("Add tag " .. tagName .. " to " .. instance.Name)
2976
+
2977
+ return {
2978
+ success = true,
2979
+ instancePath = instancePath,
2980
+ tagName = tagName,
2981
+ alreadyHad = alreadyHad,
2982
+ message = alreadyHad and "Instance already had this tag" or "Tag added successfully"
2983
+ }
2984
+ end)
2985
+
2986
+ if success then
2987
+ return result
2988
+ else
2989
+ return { error = "Failed to add tag: " .. tostring(result) }
2990
+ end
2991
+ end
2992
+
2993
+ -- Tag Tools: Remove a tag from an instance
2994
+ handlers.removeTag = function(requestData)
2995
+ local instancePath = requestData.instancePath
2996
+ local tagName = requestData.tagName
2997
+
2998
+ if not instancePath or not tagName then
2999
+ return { error = "Instance path and tag name are required" }
3000
+ end
3001
+
3002
+ local instance = getInstanceByPath(instancePath)
3003
+ if not instance then
3004
+ return { error = "Instance not found: " .. instancePath }
3005
+ end
3006
+
3007
+ local success, result = pcall(function()
3008
+ local hadTag = CollectionService:HasTag(instance, tagName)
3009
+ CollectionService:RemoveTag(instance, tagName)
3010
+ ChangeHistoryService:SetWaypoint("Remove tag " .. tagName .. " from " .. instance.Name)
3011
+
3012
+ return {
3013
+ success = true,
3014
+ instancePath = instancePath,
3015
+ tagName = tagName,
3016
+ hadTag = hadTag,
3017
+ message = hadTag and "Tag removed successfully" or "Instance did not have this tag"
3018
+ }
3019
+ end)
3020
+
3021
+ if success then
3022
+ return result
3023
+ else
3024
+ return { error = "Failed to remove tag: " .. tostring(result) }
3025
+ end
3026
+ end
3027
+
3028
+ -- Tag Tools: Get all instances with a specific tag
3029
+ handlers.getTagged = function(requestData)
3030
+ local tagName = requestData.tagName
3031
+
3032
+ if not tagName then
3033
+ return { error = "Tag name is required" }
3034
+ end
3035
+
3036
+ local success, result = pcall(function()
3037
+ local taggedInstances = CollectionService:GetTagged(tagName)
3038
+ local instances = {}
3039
+
3040
+ for _, instance in ipairs(taggedInstances) do
3041
+ table.insert(instances, {
3042
+ name = instance.Name,
3043
+ className = instance.ClassName,
3044
+ path = getInstancePath(instance)
3045
+ })
3046
+ end
3047
+
3048
+ return {
3049
+ tagName = tagName,
3050
+ instances = instances,
3051
+ count = #instances
3052
+ }
3053
+ end)
3054
+
3055
+ if success then
3056
+ return result
3057
+ else
3058
+ return { error = "Failed to get tagged instances: " .. tostring(result) }
3059
+ end
3060
+ end
3061
+
3062
+ -- Selection handlers
3063
+ handlers.getSelection = function(requestData)
3064
+ local selection = Selection:Get()
3065
+
3066
+ if #selection == 0 then
3067
+ return {
3068
+ success = true,
3069
+ selection = {},
3070
+ count = 0,
3071
+ message = "No objects selected"
3072
+ }
3073
+ end
3074
+
3075
+ local selectedObjects = {}
3076
+ for _, instance in ipairs(selection) do
3077
+ table.insert(selectedObjects, {
3078
+ name = instance.Name,
3079
+ className = instance.ClassName,
3080
+ path = getInstancePath(instance),
3081
+ parent = instance.Parent and getInstancePath(instance.Parent) or nil
3082
+ })
3083
+ end
3084
+
3085
+ return {
3086
+ success = true,
3087
+ selection = selectedObjects,
3088
+ count = #selection,
3089
+ message = #selection .. " object(s) selected"
3090
+ }
3091
+ end
3092
+
3093
+ local function updateUIState()
3094
+ if pluginState.isActive then
3095
+ statusLabel.Text = "Connecting..."
3096
+ statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
3097
+ statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
3098
+ statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
3099
+ statusText.Text = "CONNECTING"
3100
+ if pluginState.consecutiveFailures == 0 then
3101
+ detailStatusLabel.Text = "HTTP: ... MCP: ..."
3102
+ else
3103
+ detailStatusLabel.Text = "HTTP: X MCP: X"
3104
+ end
3105
+ detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
3106
+ connectButton.Text = "Disconnect"
3107
+ connectButton.TextColor3 = Color3.fromRGB(255, 255, 255)
3108
+ startPulseAnimation()
3109
+
3110
+ -- Reset steps to connecting state
3111
+ step1Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
3112
+ step1Label.Text = "1. HTTP server reachable (connecting...)"
3113
+ step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
3114
+ step2Label.Text = "2. MCP bridge connected (connecting...)"
3115
+ step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
3116
+ step3Label.Text = "3. Ready for commands (connecting...)"
3117
+ pluginState.mcpWaitStartTime = nil
3118
+ troubleshootLabel.Visible = false
3119
+
3120
+ if not buttonHover then
3121
+ connectButton.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
3122
+ end
3123
+
3124
+
3125
+ urlInput.TextEditable = false
3126
+ urlInput.BackgroundColor3 = Color3.fromRGB(55, 65, 81)
3127
+ urlInput.BorderColor3 = Color3.fromRGB(75, 85, 99)
3128
+ else
3129
+ statusLabel.Text = "Disconnected"
3130
+ statusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
3131
+ statusIndicator.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
3132
+ statusPulse.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
3133
+ statusText.Text = "OFFLINE"
3134
+ detailStatusLabel.Text = "HTTP: X MCP: X"
3135
+ detailStatusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
3136
+ connectButton.Text = "Connect"
3137
+ connectButton.TextColor3 = Color3.fromRGB(255, 255, 255)
3138
+ stopPulseAnimation()
3139
+
3140
+ -- Reset steps to offline state
3141
+ step1Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
3142
+ step1Label.Text = "1. HTTP server reachable (offline)"
3143
+ step2Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
3144
+ step2Label.Text = "2. MCP bridge connected (offline)"
3145
+ step3Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
3146
+ step3Label.Text = "3. Ready for commands (offline)"
3147
+ pluginState.mcpWaitStartTime = nil
3148
+ troubleshootLabel.Visible = false
3149
+
3150
+ if not buttonHover then
3151
+ connectButton.BackgroundColor3 = Color3.fromRGB(16, 185, 129)
3152
+ end
3153
+
3154
+
3155
+ urlInput.TextEditable = true
3156
+ urlInput.BackgroundColor3 = Color3.fromRGB(55, 65, 81)
3157
+ urlInput.BorderColor3 = Color3.fromRGB(99, 102, 241)
3158
+ end
3159
+ end
3160
+
3161
+ local function activatePlugin()
3162
+ pluginState.serverUrl = urlInput.Text
3163
+
3164
+ pluginState.isActive = true
3165
+ pluginState.consecutiveFailures = 0
3166
+ pluginState.currentRetryDelay = 0.5
3167
+ screenGui.Enabled = true
3168
+ updateUIState()
3169
+
3170
+ pcall(function()
3171
+ HttpService:RequestAsync({
3172
+ Url = pluginState.serverUrl .. "/ready",
3173
+ Method = "POST",
3174
+ Headers = {
3175
+ ["Content-Type"] = "application/json",
3176
+ },
3177
+ Body = HttpService:JSONEncode({
3178
+ pluginReady = true,
3179
+ timestamp = tick(),
3180
+ }),
3181
+ })
3182
+ end)
3183
+
3184
+ if not pluginState.connection then
3185
+ pluginState.connection = RunService.Heartbeat:Connect(function()
3186
+ local now = tick()
3187
+ local currentInterval = pluginState.consecutiveFailures > 5 and pluginState.currentRetryDelay
3188
+ or pluginState.pollInterval
3189
+ if now - pluginState.lastPoll > currentInterval then
3190
+ pluginState.lastPoll = now
3191
+ pollForRequests()
3192
+ end
3193
+ end)
3194
+ end
3195
+ end
3196
+
3197
+ local function deactivatePlugin()
3198
+ pluginState.isActive = false
3199
+ updateUIState()
3200
+
3201
+ pcall(function()
3202
+ HttpService:RequestAsync({
3203
+ Url = pluginState.serverUrl .. "/disconnect",
3204
+ Method = "POST",
3205
+ Headers = {
3206
+ ["Content-Type"] = "application/json",
3207
+ },
3208
+ Body = HttpService:JSONEncode({
3209
+ timestamp = tick(),
3210
+ }),
3211
+ })
3212
+ end)
3213
+
3214
+ if pluginState.connection then
3215
+ pluginState.connection:Disconnect()
3216
+ pluginState.connection = nil
3217
+ end
3218
+
3219
+ pluginState.consecutiveFailures = 0
3220
+ pluginState.currentRetryDelay = 0.5
3221
+ end
3222
+
3223
+ connectButton.Activated:Connect(function()
3224
+ if pluginState.isActive then
3225
+ deactivatePlugin()
3226
+ else
3227
+ activatePlugin()
3228
+ end
3229
+ end)
3230
+
3231
+ button.Click:Connect(function()
3232
+ screenGui.Enabled = not screenGui.Enabled
3233
+ end)
3234
+
3235
+ plugin.Unloading:Connect(function()
3236
+ deactivatePlugin()
3237
+ end)
3238
+
3239
+ updateUIState()
3240
+ ]]></ProtectedString>
3241
+ <bool name="Disabled">false</bool>
3242
+ <Content name="LinkedSource"><null></null></Content>
3243
+ <token name="RunContext">0</token>
3244
+ <string name="ScriptGuid">{A3D0AD89-2E27-4747-90E9-3F5D15F7E0F9}</string>
3245
+ <BinaryString name="AttributesSerialize"></BinaryString>
3246
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
3247
+ <bool name="DefinesCapabilities">false</bool>
3248
+ <string name="Name">Script</string>
3249
+ <int64 name="SourceAssetId">-1</int64>
3250
+ <BinaryString name="Tags"></BinaryString>
3251
+ </Properties>
3252
+ </Item>
3253
+ </roblox>