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