We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/code-and-relax/robloxstudio-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
local HttpService = game:GetService("HttpService")
local StudioService = game:GetService("StudioService")
local Selection = game:GetService("Selection")
local RunService = game:GetService("RunService")
local ChangeHistoryService = game:GetService("ChangeHistoryService")
local ScriptEditorService = game:GetService("ScriptEditorService")
local CollectionService = game:GetService("CollectionService")
local toolbar = plugin:CreateToolbar("MCP Integration")
local button =
toolbar:CreateButton("MCP Server", "Connect to MCP Server for AI Integration", "rbxassetid://10734944444")
local pluginState = {
serverUrl = "http://localhost:3002",
mcpServerUrl = "http://localhost:3001",
isActive = false,
consecutiveFailures = 0,
maxFailuresBeforeError = 50,
lastSuccessfulConnection = 0,
currentRetryDelay = 0.5,
maxRetryDelay = 5,
retryBackoffMultiplier = 1.2,
-- Long-poll coroutine handle
pollThread = nil,
-- UI step tracking
lastHttpOk = false,
mcpWaitStartTime = nil,
}
local LOG_MAX_ENTRIES = 200
local LOG_CATEGORIES = {
CONNECTION = { color = Color3.fromRGB(59, 130, 246), label = "CONN" },
POLL = { color = Color3.fromRGB(107, 114, 128), label = "POLL" },
REQUEST = { color = Color3.fromRGB(6, 182, 212), label = "REQ" },
RESPONSE = { color = Color3.fromRGB(34, 197, 94), label = "RESP" },
ERROR = { color = Color3.fromRGB(239, 68, 68), label = "ERR" },
WARN = { color = Color3.fromRGB(245, 158, 11), label = "WARN" },
}
local logState = {
entries = {},
entryCount = 0,
isCollapsed = false,
pollCounter = 0,
}
local addLog -- forward declaration
local logScrollFrame -- forward declaration for auto-scroll
local logCountLabel -- forward declaration for count update
local screenGui = plugin:CreateDockWidgetPluginGuiAsync(
"MCPServerInterface",
DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, false, 400, 500, 350, 450)
)
screenGui.Title = "MCP Server v2.0.0"
local mainFrame = Instance.new("Frame")
mainFrame.Size = UDim2.new(1, 0, 1, 0)
mainFrame.BackgroundColor3 = Color3.fromRGB(17, 24, 39)
mainFrame.BorderSizePixel = 0
mainFrame.Parent = screenGui
local mainCorner = Instance.new("UICorner")
mainCorner.CornerRadius = UDim.new(0, 8)
mainCorner.Parent = mainFrame
local headerFrame = Instance.new("Frame")
headerFrame.Size = UDim2.new(1, 0, 0, 60)
headerFrame.Position = UDim2.new(0, 0, 0, 0)
headerFrame.BackgroundColor3 = Color3.fromRGB(59, 130, 246)
headerFrame.BorderSizePixel = 0
headerFrame.Parent = mainFrame
local headerCorner = Instance.new("UICorner")
headerCorner.CornerRadius = UDim.new(0, 8)
headerCorner.Parent = headerFrame
local headerGradient = Instance.new("UIGradient")
headerGradient.Color = ColorSequence.new{
ColorSequenceKeypoint.new(0, Color3.fromRGB(59, 130, 246)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(147, 51, 234))
}
headerGradient.Rotation = 45
headerGradient.Parent = headerFrame
local titleContainer = Instance.new("Frame")
titleContainer.Size = UDim2.new(1, -70, 1, 0)
titleContainer.Position = UDim2.new(0, 15, 0, 0)
titleContainer.BackgroundTransparency = 1
titleContainer.Parent = headerFrame
local titleLabel = Instance.new("TextLabel")
titleLabel.Size = UDim2.new(1, 0, 0, 28)
titleLabel.Position = UDim2.new(0, 0, 0, 8)
titleLabel.BackgroundTransparency = 1
titleLabel.Text = "MCP Server"
titleLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
titleLabel.TextScaled = false
titleLabel.TextSize = 18
titleLabel.Font = Enum.Font.Jura
titleLabel.TextXAlignment = Enum.TextXAlignment.Left
titleLabel.Parent = titleContainer
local versionLabel = Instance.new("TextLabel")
versionLabel.Size = UDim2.new(1, 0, 0, 16)
versionLabel.Position = UDim2.new(0, 0, 0, 32)
versionLabel.BackgroundTransparency = 1
versionLabel.Text = "AI Integration • v2.0.0 (Long Poll)"
versionLabel.TextColor3 = Color3.fromRGB(191, 219, 254)
versionLabel.TextScaled = false
versionLabel.TextSize = 12
versionLabel.Font = Enum.Font.Jura
versionLabel.TextXAlignment = Enum.TextXAlignment.Left
versionLabel.Parent = titleContainer
local statusContainer = Instance.new("Frame")
statusContainer.Size = UDim2.new(0, 50, 0, 40)
statusContainer.Position = UDim2.new(1, -60, 0, 10)
statusContainer.BackgroundTransparency = 1
statusContainer.Parent = headerFrame
local statusIndicator = Instance.new("Frame")
statusIndicator.Size = UDim2.new(0, 16, 0, 16)
statusIndicator.Position = UDim2.new(0.5, -8, 0, 5)
statusIndicator.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
statusIndicator.BorderSizePixel = 0
statusIndicator.Parent = statusContainer
local statusCorner = Instance.new("UICorner")
statusCorner.CornerRadius = UDim.new(1, 0)
statusCorner.Parent = statusIndicator
local statusPulse = Instance.new("Frame")
statusPulse.Size = UDim2.new(0, 16, 0, 16)
statusPulse.Position = UDim2.new(0, 0, 0, 0)
statusPulse.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
statusPulse.BackgroundTransparency = 0.7
statusPulse.BorderSizePixel = 0
statusPulse.Parent = statusIndicator
local pulseCorner = Instance.new("UICorner")
pulseCorner.CornerRadius = UDim.new(1, 0)
pulseCorner.Parent = statusPulse
local statusText = Instance.new("TextLabel")
statusText.Size = UDim2.new(0, 50, 0, 12)
statusText.Position = UDim2.new(0, 0, 0, 24)
statusText.BackgroundTransparency = 1
statusText.Text = "OFFLINE"
statusText.TextColor3 = Color3.fromRGB(255, 255, 255)
statusText.TextScaled = false
statusText.TextSize = 8
statusText.Font = Enum.Font.Jura
statusText.TextXAlignment = Enum.TextXAlignment.Center
statusText.Parent = statusContainer
local contentFrame = Instance.new("ScrollingFrame")
contentFrame.Size = UDim2.new(1, -20, 1, -80)
contentFrame.Position = UDim2.new(0, 10, 0, 70)
contentFrame.BackgroundTransparency = 1
contentFrame.BorderSizePixel = 0
contentFrame.ScrollBarThickness = 6
contentFrame.ScrollBarImageColor3 = Color3.fromRGB(99, 102, 241)
contentFrame.CanvasSize = UDim2.new(0, 0, 0, 0)
contentFrame.AutomaticCanvasSize = Enum.AutomaticSize.Y
contentFrame.Parent = mainFrame
local contentLayout = Instance.new("UIListLayout")
contentLayout.Padding = UDim.new(0, 12)
contentLayout.SortOrder = Enum.SortOrder.LayoutOrder
contentLayout.Parent = contentFrame
local connectionSection = Instance.new("Frame")
connectionSection.Size = UDim2.new(1, 0, 0, 110)
connectionSection.BackgroundColor3 = Color3.fromRGB(31, 41, 55)
connectionSection.BorderSizePixel = 0
connectionSection.LayoutOrder = 1
connectionSection.Parent = contentFrame
local connectionCorner = Instance.new("UICorner")
connectionCorner.CornerRadius = UDim.new(0, 8)
connectionCorner.Parent = connectionSection
local connectionPadding = Instance.new("UIPadding")
connectionPadding.PaddingLeft = UDim.new(0, 15)
connectionPadding.PaddingRight = UDim.new(0, 15)
connectionPadding.PaddingTop = UDim.new(0, 15)
connectionPadding.PaddingBottom = UDim.new(0, 15)
connectionPadding.Parent = connectionSection
local connectionTitle = Instance.new("TextLabel")
connectionTitle.Size = UDim2.new(1, 0, 0, 20)
connectionTitle.Position = UDim2.new(0, 0, 0, 0)
connectionTitle.BackgroundTransparency = 1
connectionTitle.Text = "Connection Settings"
connectionTitle.TextColor3 = Color3.fromRGB(255, 255, 255)
connectionTitle.TextScaled = false
connectionTitle.TextSize = 14
connectionTitle.Font = Enum.Font.Jura
connectionTitle.TextXAlignment = Enum.TextXAlignment.Left
connectionTitle.Parent = connectionSection
local urlLabel = Instance.new("TextLabel")
urlLabel.Size = UDim2.new(1, 0, 0, 16)
urlLabel.Position = UDim2.new(0, 0, 0, 30)
urlLabel.BackgroundTransparency = 1
urlLabel.Text = "Server URL"
urlLabel.TextColor3 = Color3.fromRGB(156, 163, 175)
urlLabel.TextScaled = false
urlLabel.TextSize = 12
urlLabel.Font = Enum.Font.Jura
urlLabel.TextXAlignment = Enum.TextXAlignment.Left
urlLabel.Parent = connectionSection
local urlInput = Instance.new("TextBox")
urlInput.Size = UDim2.new(1, 0, 0, 32)
urlInput.Position = UDim2.new(0, 0, 0, 50)
urlInput.BackgroundColor3 = Color3.fromRGB(55, 65, 81)
urlInput.BorderSizePixel = 1
urlInput.BorderColor3 = Color3.fromRGB(99, 102, 241)
urlInput.Text = "http://localhost:3002"
urlInput.TextColor3 = Color3.fromRGB(255, 255, 255)
urlInput.TextScaled = false
urlInput.TextSize = 12
urlInput.Font = Enum.Font.Jura
urlInput.ClearTextOnFocus = false
urlInput.PlaceholderText = "Enter server URL..."
urlInput.PlaceholderColor3 = Color3.fromRGB(107, 114, 128)
urlInput.Parent = connectionSection
local urlCorner = Instance.new("UICorner")
urlCorner.CornerRadius = UDim.new(0, 6)
urlCorner.Parent = urlInput
local urlPadding = Instance.new("UIPadding")
urlPadding.PaddingLeft = UDim.new(0, 12)
urlPadding.PaddingRight = UDim.new(0, 12)
urlPadding.Parent = urlInput
local statusSection = Instance.new("Frame")
statusSection.Size = UDim2.new(1, 0, 0, 170)
statusSection.BackgroundColor3 = Color3.fromRGB(31, 41, 55)
statusSection.BorderSizePixel = 0
statusSection.LayoutOrder = 2
statusSection.Parent = contentFrame
local statusSectionCorner = Instance.new("UICorner")
statusSectionCorner.CornerRadius = UDim.new(0, 8)
statusSectionCorner.Parent = statusSection
local statusSectionPadding = Instance.new("UIPadding")
statusSectionPadding.PaddingLeft = UDim.new(0, 15)
statusSectionPadding.PaddingRight = UDim.new(0, 15)
statusSectionPadding.PaddingTop = UDim.new(0, 15)
statusSectionPadding.PaddingBottom = UDim.new(0, 15)
statusSectionPadding.Parent = statusSection
local statusTitle = Instance.new("TextLabel")
statusTitle.Size = UDim2.new(1, 0, 0, 20)
statusTitle.Position = UDim2.new(0, 0, 0, 0)
statusTitle.BackgroundTransparency = 1
statusTitle.Text = "Connection Status"
statusTitle.TextColor3 = Color3.fromRGB(255, 255, 255)
statusTitle.TextScaled = false
statusTitle.TextSize = 14
statusTitle.Font = Enum.Font.Jura
statusTitle.TextXAlignment = Enum.TextXAlignment.Left
statusTitle.Parent = statusSection
local statusLabel = Instance.new("TextLabel")
statusLabel.Size = UDim2.new(1, 0, 0, 20)
statusLabel.Position = UDim2.new(0, 0, 0, 30)
statusLabel.BackgroundTransparency = 1
statusLabel.Text = "Disconnected"
statusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
statusLabel.TextScaled = false
statusLabel.TextSize = 13
statusLabel.Font = Enum.Font.Jura
statusLabel.TextXAlignment = Enum.TextXAlignment.Left
statusLabel.TextWrapped = true
statusLabel.Parent = statusSection
local detailStatusLabel = Instance.new("TextLabel")
detailStatusLabel.Size = UDim2.new(1, 0, 0, 12)
detailStatusLabel.Position = UDim2.new(0, 0, 0, 50)
detailStatusLabel.BackgroundTransparency = 1
detailStatusLabel.Text = "HTTP: X MCP: X"
detailStatusLabel.TextColor3 = Color3.fromRGB(156, 163, 175)
detailStatusLabel.TextScaled = false
detailStatusLabel.TextSize = 10
detailStatusLabel.Font = Enum.Font.Jura
detailStatusLabel.TextXAlignment = Enum.TextXAlignment.Left
detailStatusLabel.TextWrapped = true
detailStatusLabel.Parent = statusSection
-- Step-by-step status rows
local stepsFrame = Instance.new("Frame")
stepsFrame.Size = UDim2.new(1, 0, 0, 60)
stepsFrame.Position = UDim2.new(0, 0, 0, 68)
stepsFrame.BackgroundTransparency = 1
stepsFrame.Parent = statusSection
local stepsLayout = Instance.new("UIListLayout")
stepsLayout.Padding = UDim.new(0, 6)
stepsLayout.FillDirection = Enum.FillDirection.Vertical
stepsLayout.SortOrder = Enum.SortOrder.LayoutOrder
stepsLayout.Parent = stepsFrame
local function createStepRow(text)
local row = Instance.new("Frame")
row.Size = UDim2.new(1, 0, 0, 16)
row.BackgroundTransparency = 1
local dot = Instance.new("Frame")
dot.Size = UDim2.new(0, 10, 0, 10)
dot.Position = UDim2.new(0, 0, 0, 3)
dot.BackgroundColor3 = Color3.fromRGB(156, 163, 175)
dot.BorderSizePixel = 0
dot.Parent = row
local dotCorner = Instance.new("UICorner")
dotCorner.CornerRadius = UDim.new(1, 0)
dotCorner.Parent = dot
local label = Instance.new("TextLabel")
label.Size = UDim2.new(1, -18, 1, 0)
label.Position = UDim2.new(0, 18, 0, 0)
label.BackgroundTransparency = 1
label.Text = text
label.TextColor3 = Color3.fromRGB(209, 213, 219)
label.TextScaled = false
label.TextSize = 11
label.Font = Enum.Font.Jura
label.TextXAlignment = Enum.TextXAlignment.Left
label.Parent = row
row.Parent = stepsFrame
return row, dot, label
end
local step1Row, step1Dot, step1Label = createStepRow("1. HTTP server reachable")
local step2Row, step2Dot, step2Label = createStepRow("2. MCP bridge connected")
local step3Row, step3Dot, step3Label = createStepRow("3. Ready for commands")
-- Troubleshooting tip for common stuck state
local troubleshootLabel = Instance.new("TextLabel")
troubleshootLabel.Size = UDim2.new(1, 0, 0, 40)
troubleshootLabel.Position = UDim2.new(0, 0, 0, 130)
troubleshootLabel.BackgroundTransparency = 1
troubleshootLabel.TextWrapped = true
troubleshootLabel.Visible = false
troubleshootLabel.Text = "HTTP is OK but MCP isn't responding. Close all node.exe in Task Manager and restart the server."
troubleshootLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
troubleshootLabel.TextScaled = false
troubleshootLabel.TextSize = 11
troubleshootLabel.Font = Enum.Font.Jura
troubleshootLabel.TextXAlignment = Enum.TextXAlignment.Left
troubleshootLabel.Parent = statusSection
local connectButton = Instance.new("TextButton")
connectButton.Size = UDim2.new(1, 0, 0, 48)
connectButton.BackgroundColor3 = Color3.fromRGB(16, 185, 129)
connectButton.BorderSizePixel = 0
connectButton.Text = "Connect"
connectButton.TextColor3 = Color3.fromRGB(255, 255, 255)
connectButton.TextScaled = false
connectButton.TextSize = 16
connectButton.Font = Enum.Font.Jura
connectButton.LayoutOrder = 3
connectButton.Parent = contentFrame
local connectCorner = Instance.new("UICorner")
connectCorner.CornerRadius = UDim.new(0, 12)
connectCorner.Parent = connectButton
-- ========== ACTIVITY LOG SECTION ==========
local logSection = Instance.new("Frame")
logSection.Size = UDim2.new(1, 0, 0, 230)
logSection.BackgroundColor3 = Color3.fromRGB(31, 41, 55)
logSection.BorderSizePixel = 0
logSection.LayoutOrder = 4
logSection.Parent = contentFrame
local logSectionCorner = Instance.new("UICorner")
logSectionCorner.CornerRadius = UDim.new(0, 8)
logSectionCorner.Parent = logSection
local logSectionPadding = Instance.new("UIPadding")
logSectionPadding.PaddingLeft = UDim.new(0, 15)
logSectionPadding.PaddingRight = UDim.new(0, 15)
logSectionPadding.PaddingTop = UDim.new(0, 10)
logSectionPadding.PaddingBottom = UDim.new(0, 10)
logSectionPadding.Parent = logSection
-- Header bar
local logHeaderBar = Instance.new("Frame")
logHeaderBar.Size = UDim2.new(1, 0, 0, 28)
logHeaderBar.BackgroundTransparency = 1
logHeaderBar.Parent = logSection
local collapseButton = Instance.new("TextButton")
collapseButton.Size = UDim2.new(0, 20, 0, 20)
collapseButton.Position = UDim2.new(0, 0, 0, 4)
collapseButton.BackgroundTransparency = 1
collapseButton.Text = "v"
collapseButton.TextColor3 = Color3.fromRGB(156, 163, 175)
collapseButton.TextSize = 14
collapseButton.Font = Enum.Font.RobotoMono
collapseButton.Parent = logHeaderBar
local logTitleLabel = Instance.new("TextLabel")
logTitleLabel.Size = UDim2.new(0, 90, 0, 28)
logTitleLabel.Position = UDim2.new(0, 26, 0, 0)
logTitleLabel.BackgroundTransparency = 1
logTitleLabel.Text = "Activity Log"
logTitleLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
logTitleLabel.TextSize = 14
logTitleLabel.Font = Enum.Font.Jura
logTitleLabel.TextXAlignment = Enum.TextXAlignment.Left
logTitleLabel.Parent = logHeaderBar
logCountLabel = Instance.new("TextLabel")
logCountLabel.Size = UDim2.new(0, 40, 0, 28)
logCountLabel.Position = UDim2.new(0, 118, 0, 0)
logCountLabel.BackgroundTransparency = 1
logCountLabel.Text = "(0)"
logCountLabel.TextColor3 = Color3.fromRGB(156, 163, 175)
logCountLabel.TextSize = 11
logCountLabel.Font = Enum.Font.Jura
logCountLabel.TextXAlignment = Enum.TextXAlignment.Left
logCountLabel.Parent = logHeaderBar
local logButtonsFrame = Instance.new("Frame")
logButtonsFrame.Size = UDim2.new(0, 84, 0, 24)
logButtonsFrame.Position = UDim2.new(1, -84, 0, 2)
logButtonsFrame.BackgroundTransparency = 1
logButtonsFrame.Parent = logHeaderBar
local logButtonsLayout = Instance.new("UIListLayout")
logButtonsLayout.FillDirection = Enum.FillDirection.Horizontal
logButtonsLayout.Padding = UDim.new(0, 6)
logButtonsLayout.SortOrder = Enum.SortOrder.LayoutOrder
logButtonsLayout.Parent = logButtonsFrame
local function createLogButton(text, textColor, order)
local btn = Instance.new("TextButton")
btn.Size = UDim2.new(0, 24, 0, 24)
btn.BackgroundColor3 = Color3.fromRGB(55, 65, 81)
btn.BorderSizePixel = 0
btn.Text = text
btn.TextColor3 = textColor
btn.TextSize = 12
btn.Font = Enum.Font.RobotoMono
btn.LayoutOrder = order
btn.Parent = logButtonsFrame
local corner = Instance.new("UICorner")
corner.CornerRadius = UDim.new(0, 4)
corner.Parent = btn
return btn
end
local clearButton = createLogButton("X", Color3.fromRGB(239, 68, 68), 1)
local copyButton = createLogButton("C", Color3.fromRGB(156, 163, 175), 2)
local exportButton = createLogButton("E", Color3.fromRGB(156, 163, 175), 3)
-- Log body
local logBodyFrame = Instance.new("Frame")
logBodyFrame.Size = UDim2.new(1, 0, 0, 180)
logBodyFrame.Position = UDim2.new(0, 0, 0, 32)
logBodyFrame.BackgroundTransparency = 1
logBodyFrame.Parent = logSection
logScrollFrame = Instance.new("ScrollingFrame")
logScrollFrame.Size = UDim2.new(1, 0, 1, 0)
logScrollFrame.BackgroundColor3 = Color3.fromRGB(17, 24, 39)
logScrollFrame.BorderSizePixel = 0
logScrollFrame.ScrollBarThickness = 4
logScrollFrame.ScrollBarImageColor3 = Color3.fromRGB(99, 102, 241)
logScrollFrame.CanvasSize = UDim2.new(0, 0, 0, 0)
logScrollFrame.AutomaticCanvasSize = Enum.AutomaticSize.Y
logScrollFrame.Parent = logBodyFrame
local logScrollCorner = Instance.new("UICorner")
logScrollCorner.CornerRadius = UDim.new(0, 4)
logScrollCorner.Parent = logScrollFrame
local logListLayout = Instance.new("UIListLayout")
logListLayout.Padding = UDim.new(0, 1)
logListLayout.SortOrder = Enum.SortOrder.LayoutOrder
logListLayout.Parent = logScrollFrame
local logScrollPadding = Instance.new("UIPadding")
logScrollPadding.PaddingLeft = UDim.new(0, 4)
logScrollPadding.PaddingRight = UDim.new(0, 4)
logScrollPadding.PaddingTop = UDim.new(0, 2)
logScrollPadding.PaddingBottom = UDim.new(0, 2)
logScrollPadding.Parent = logScrollFrame
-- ========== addLog FUNCTION ==========
addLog = function(category, message)
-- Throttle POLL: only every 10 cycles
if category == "POLL" then
logState.pollCounter += 1
if logState.pollCounter % 10 ~= 1 then return end
end
local catInfo = LOG_CATEGORIES[category]
if not catInfo then return end
local timestamp = os.date("%H:%M:%S")
logState.entryCount += 1
local entry = Instance.new("TextLabel")
entry.Size = UDim2.new(1, 0, 0, 14)
entry.BackgroundTransparency = 1
entry.Text = "[" .. timestamp .. "] [" .. catInfo.label .. "] " .. message
entry.TextColor3 = catInfo.color
entry.TextSize = 10
entry.Font = Enum.Font.RobotoMono
entry.TextXAlignment = Enum.TextXAlignment.Left
entry.TextTruncate = Enum.TextTruncate.AtEnd
entry.LayoutOrder = logState.entryCount
entry.Parent = logScrollFrame
table.insert(logState.entries, { timestamp = timestamp, category = category, message = message, frame = entry })
-- Purge oldest entries beyond limit
while #logState.entries > LOG_MAX_ENTRIES do
local oldest = table.remove(logState.entries, 1)
if oldest.frame and oldest.frame.Parent then oldest.frame:Destroy() end
end
logCountLabel.Text = "(" .. #logState.entries .. ")"
-- Auto-scroll to bottom
task.defer(function()
if logScrollFrame and logScrollFrame.Parent then
logScrollFrame.CanvasPosition = Vector2.new(0, math.max(0, logScrollFrame.AbsoluteCanvasSize.Y - logScrollFrame.AbsoluteSize.Y))
end
end)
end
-- ========== LOG BUTTON HANDLERS ==========
-- Clear button
clearButton.Activated:Connect(function()
for _, e in ipairs(logState.entries) do
if e.frame and e.frame.Parent then e.frame:Destroy() end
end
logState.entries = {}
logState.entryCount = 0
logState.pollCounter = 0
logCountLabel.Text = "(0)"
end)
-- Copy button: print to Output for easy copy
copyButton.Activated:Connect(function()
local lines = {}
for _, e in ipairs(logState.entries) do
local catInfo = LOG_CATEGORIES[e.category]
local label = catInfo and catInfo.label or e.category
table.insert(lines, "[" .. e.timestamp .. "] [" .. label .. "] " .. e.message)
end
local text = table.concat(lines, "\n")
print("=== MCP Activity Log ===\n" .. text .. "\n=== End Log ===")
pcall(function()
plugin:SetSetting("MCPLogExport", text)
end)
addLog("CONNECTION", "Log copied to Output (" .. #logState.entries .. " entries)")
end)
-- Export button: create StringValue in ServerStorage
exportButton.Activated:Connect(function()
local lines = {}
for _, e in ipairs(logState.entries) do
local catInfo = LOG_CATEGORIES[e.category]
local label = catInfo and catInfo.label or e.category
table.insert(lines, "[" .. e.timestamp .. "] [" .. label .. "] " .. e.message)
end
local text = table.concat(lines, "\n")
local ss = game:GetService("ServerStorage")
local existing = ss:FindFirstChild("MCPActivityLog")
if existing then existing:Destroy() end
local sv = Instance.new("StringValue")
sv.Name = "MCPActivityLog"
sv.Value = text
sv.Parent = ss
addLog("CONNECTION", "Log exported to ServerStorage.MCPActivityLog (" .. #logState.entries .. " entries)")
end)
-- Collapse button
collapseButton.Activated:Connect(function()
logState.isCollapsed = not logState.isCollapsed
logBodyFrame.Visible = not logState.isCollapsed
collapseButton.Text = logState.isCollapsed and ">" or "v"
logSection.Size = logState.isCollapsed and UDim2.new(1, 0, 0, 38) or UDim2.new(1, 0, 0, 230)
end)
-- ========== END ACTIVITY LOG ==========
local TweenService = game:GetService("TweenService")
local buttonHover = false
connectButton.MouseEnter:Connect(function()
buttonHover = true
connectButton.BackgroundColor3 = not pluginState.isActive and Color3.fromRGB(5, 150, 105) or Color3.fromRGB(220, 38, 38)
end)
connectButton.MouseLeave:Connect(function()
buttonHover = false
connectButton.BackgroundColor3 = not pluginState.isActive and Color3.fromRGB(16, 185, 129) or Color3.fromRGB(239, 68, 68)
end)
local pulseAnimation = nil
local function createPulseAnimation()
if pulseAnimation then
pcall(function()
pulseAnimation:Cancel()
end)
pulseAnimation = nil
end
pcall(function()
pulseAnimation = TweenService:Create(statusPulse, TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut, -1, true), {
Size = UDim2.new(0, 24, 0, 24),
Position = UDim2.new(0, -4, 0, -4),
BackgroundTransparency = 1
})
end)
return pulseAnimation
end
local function stopPulseAnimation()
statusPulse.Size = UDim2.new(0, 16, 0, 16)
statusPulse.Position = UDim2.new(0, 0, 0, 0)
statusPulse.BackgroundTransparency = 0.7
end
local function startPulseAnimation()
statusPulse.Size = UDim2.new(0, 16, 0, 16)
statusPulse.Position = UDim2.new(0, 0, 0, 0)
statusPulse.BackgroundTransparency = 0.7
end
local function safeCall(func, ...)
local success, result = pcall(func, ...)
if success then
return result
else
warn("MCP Plugin Error: " .. tostring(result))
return nil
end
end
local function getInstancePath(instance)
if not instance or instance == game then
return "game"
end
local path = {}
local current = instance
while current and current ~= game do
table.insert(path, 1, current.Name)
current = current.Parent
end
return "game." .. table.concat(path, ".")
end
-- Helper to normalize line endings and split without inventing an extra trailing line
local function splitLines(source)
local normalized = (source or ""):gsub("\r\n", "\n"):gsub("\r", "\n")
local endsWithNewline = normalized:sub(-1) == "\n"
local lines = {}
local start = 1
while true do
local newlinePos = string.find(normalized, "\n", start, true)
if newlinePos then
table.insert(lines, string.sub(normalized, start, newlinePos - 1))
start = newlinePos + 1
else
local remainder = string.sub(normalized, start)
if remainder ~= "" or not endsWithNewline then
table.insert(lines, remainder)
end
break
end
end
if #lines == 0 then
table.insert(lines, "")
end
return lines, endsWithNewline
end
local function joinLines(lines, hadTrailingNewline)
local source = table.concat(lines, "\n")
if hadTrailingNewline and source:sub(-1) ~= "\n" then
source ..= "\n"
end
return source
end
-- Helper to convert property values from JSON to Roblox types
local function convertPropertyValue(instance, propertyName, propertyValue)
-- Handle nil
if propertyValue == nil then
return nil
end
-- Handle arrays (likely Vector3, Color3, UDim2)
if type(propertyValue) == "table" and #propertyValue > 0 then
-- Check if it's a Vector3-like property
if #propertyValue == 3 then
local prop = propertyName:lower()
if prop == "position" or prop == "size" or prop == "orientation" or prop == "velocity" or prop == "angularvelocity" then
return Vector3.new(propertyValue[1] or 0, propertyValue[2] or 0, propertyValue[3] or 0)
elseif prop == "color" or prop == "color3" then
return Color3.new(propertyValue[1] or 0, propertyValue[2] or 0, propertyValue[3] or 0)
else
-- Try to infer from current property type
local success, currentVal = pcall(function() return instance[propertyName] end)
if success then
if typeof(currentVal) == "Vector3" then
return Vector3.new(propertyValue[1] or 0, propertyValue[2] or 0, propertyValue[3] or 0)
elseif typeof(currentVal) == "Color3" then
return Color3.new(propertyValue[1] or 0, propertyValue[2] or 0, propertyValue[3] or 0)
end
end
end
elseif #propertyValue == 2 then
-- Possibly Vector2
local success, currentVal = pcall(function() return instance[propertyName] end)
if success and typeof(currentVal) == "Vector2" then
return Vector2.new(propertyValue[1] or 0, propertyValue[2] or 0)
end
end
end
-- Handle object with X, Y, Z keys (Vector3)
if type(propertyValue) == "table" and (propertyValue.X or propertyValue.Y or propertyValue.Z) then
return Vector3.new(propertyValue.X or 0, propertyValue.Y or 0, propertyValue.Z or 0)
end
-- Handle object with R, G, B keys (Color3)
if type(propertyValue) == "table" and (propertyValue.R or propertyValue.G or propertyValue.B) then
return Color3.new(propertyValue.R or 0, propertyValue.G or 0, propertyValue.B or 0)
end
-- Handle Enum values (strings like "Ball", "Cylinder", etc.)
if type(propertyValue) == "string" then
local success, currentVal = pcall(function() return instance[propertyName] end)
if success and typeof(currentVal) == "EnumItem" then
local enumType = tostring(currentVal.EnumType)
local enumSuccess, enumVal = pcall(function()
return Enum[enumType][propertyValue]
end)
if enumSuccess and enumVal then
return enumVal
end
end
-- Handle BrickColor
if propertyName == "BrickColor" then
return BrickColor.new(propertyValue)
end
end
-- Handle boolean strings
if type(propertyValue) == "string" then
if propertyValue == "true" then return true end
if propertyValue == "false" then return false end
end
-- Return as-is for primitives (number, boolean, string)
return propertyValue
end
local processRequest
local sendResponse
local handlers = {}
-- Long-poll loop: continuously polls /poll (server holds up to 25s)
local function longPollLoop()
while pluginState.isActive do
local success, result = pcall(function()
return HttpService:RequestAsync({
Url = pluginState.serverUrl .. "/poll",
Method = "GET",
Headers = {
["Content-Type"] = "application/json",
},
})
end)
if not pluginState.isActive then break end
if success and (result.Success or result.StatusCode == 503) then
pluginState.consecutiveFailures = 0
pluginState.currentRetryDelay = 0.5
pluginState.lastSuccessfulConnection = tick()
local data = HttpService:JSONDecode(result.Body)
local mcpConnected = data.mcpConnected == true
pluginState.lastHttpOk = true
step1Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
step1Label.Text = "1. HTTP server reachable (OK)"
if mcpConnected and not statusLabel.Text:find("Connected") then
statusLabel.Text = "Connected (Long Poll)"
statusLabel.TextColor3 = Color3.fromRGB(34, 197, 94)
statusIndicator.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
statusPulse.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
statusText.Text = "ONLINE"
detailStatusLabel.Text = "HTTP: OK MCP: OK Mode: Long Poll"
detailStatusLabel.TextColor3 = Color3.fromRGB(34, 197, 94)
step2Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
step2Label.Text = "2. MCP bridge connected (OK)"
step3Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
step3Label.Text = "3. Ready for commands (OK)"
pluginState.mcpWaitStartTime = nil
troubleshootLabel.Visible = false
stopPulseAnimation()
addLog("CONNECTION", "Connected - Long Poll active")
elseif not mcpConnected then
statusLabel.Text = "Waiting for MCP server"
statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
statusText.Text = "WAITING"
detailStatusLabel.Text = "HTTP: OK MCP: ..."
detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
step2Label.Text = "2. MCP bridge connected (waiting...)"
step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
step3Label.Text = "3. Ready for commands (waiting...)"
if not pluginState.mcpWaitStartTime then
pluginState.mcpWaitStartTime = tick()
end
local elapsed = tick() - (pluginState.mcpWaitStartTime or tick())
troubleshootLabel.Visible = elapsed > 8
startPulseAnimation()
addLog("POLL", "HTTP OK, waiting for MCP...")
-- MCP not connected: wait 1s before next poll
task.wait(1)
end
if data.request and mcpConnected then
addLog("REQUEST", "Received: " .. tostring(data.request.endpoint))
local reqStartTime = tick()
local response = processRequest(data.request)
sendResponse(data.requestId, response)
local reqMs = math.floor((tick() - reqStartTime) * 1000)
addLog("RESPONSE", tostring(data.request.endpoint) .. " completed in " .. reqMs .. "ms")
end
-- If no request (long-poll timeout), loop immediately to re-poll
elseif pluginState.isActive then
pluginState.consecutiveFailures = pluginState.consecutiveFailures + 1
if pluginState.consecutiveFailures == 1 then
addLog("WARN", "Connection lost, retrying...")
end
if pluginState.consecutiveFailures > 1 then
pluginState.currentRetryDelay =
math.min(pluginState.currentRetryDelay * pluginState.retryBackoffMultiplier, pluginState.maxRetryDelay)
end
if pluginState.consecutiveFailures >= pluginState.maxFailuresBeforeError then
statusLabel.Text = "Server unavailable"
statusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
statusIndicator.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
statusPulse.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
statusText.Text = "ERROR"
detailStatusLabel.Text = "HTTP: X MCP: X"
detailStatusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
step1Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
step1Label.Text = "1. HTTP server reachable (error)"
step2Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
step2Label.Text = "2. MCP bridge connected (error)"
step3Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
step3Label.Text = "3. Ready for commands (error)"
pluginState.mcpWaitStartTime = nil
troubleshootLabel.Visible = false
stopPulseAnimation()
addLog("ERROR", "Server unavailable after " .. pluginState.consecutiveFailures .. " failures")
elseif pluginState.consecutiveFailures > 5 then
local waitTime = math.ceil(pluginState.currentRetryDelay)
statusLabel.Text = "Retrying (" .. waitTime .. "s)"
statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
statusText.Text = "RETRY"
detailStatusLabel.Text = "HTTP: ... MCP: ..."
detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
step1Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
step1Label.Text = "1. HTTP server reachable (retrying...)"
step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
step2Label.Text = "2. MCP bridge connected (retrying...)"
step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
step3Label.Text = "3. Ready for commands (retrying...)"
pluginState.mcpWaitStartTime = nil
troubleshootLabel.Visible = false
startPulseAnimation()
elseif pluginState.consecutiveFailures > 1 then
statusLabel.Text = "Connecting (attempt " .. pluginState.consecutiveFailures .. ")"
statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
statusText.Text = "CONNECTING"
detailStatusLabel.Text = "HTTP: ... MCP: ..."
detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
step1Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
step1Label.Text = "1. HTTP server reachable (connecting...)"
step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
step2Label.Text = "2. MCP bridge connected (connecting...)"
step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
step3Label.Text = "3. Ready for commands (connecting...)"
pluginState.mcpWaitStartTime = nil
troubleshootLabel.Visible = false
startPulseAnimation()
end
-- Exponential backoff on failures
task.wait(pluginState.currentRetryDelay)
end
end
end
sendResponse = function(requestId, responseData)
local ok, err = pcall(function()
HttpService:RequestAsync({
Url = pluginState.serverUrl .. "/response",
Method = "POST",
Headers = {
["Content-Type"] = "application/json",
},
Body = HttpService:JSONEncode({
requestId = requestId,
response = responseData,
}),
})
end)
if not ok then
addLog("ERROR", "Failed to send response: " .. tostring(err))
end
end
processRequest = function(request)
local endpoint = request.endpoint
local data = request.data or {}
if endpoint == "/api/file-tree" then
return handlers.getFileTree(data)
elseif endpoint == "/api/search-files" then
return handlers.searchFiles(data)
elseif endpoint == "/api/place-info" then
return handlers.getPlaceInfo(data)
elseif endpoint == "/api/services" then
return handlers.getServices(data)
elseif endpoint == "/api/search-objects" then
return handlers.searchObjects(data)
elseif endpoint == "/api/instance-properties" then
return handlers.getInstanceProperties(data)
elseif endpoint == "/api/instance-children" then
return handlers.getInstanceChildren(data)
elseif endpoint == "/api/search-by-property" then
return handlers.searchByProperty(data)
elseif endpoint == "/api/class-info" then
return handlers.getClassInfo(data)
elseif endpoint == "/api/project-structure" then
return handlers.getProjectStructure(data)
elseif endpoint == "/api/set-property" then
return handlers.setProperty(data)
elseif endpoint == "/api/mass-set-property" then
return handlers.massSetProperty(data)
elseif endpoint == "/api/mass-get-property" then
return handlers.massGetProperty(data)
elseif endpoint == "/api/create-object" then
return handlers.createObject(data)
elseif endpoint == "/api/mass-create-objects" then
return handlers.massCreateObjects(data)
elseif endpoint == "/api/mass-create-objects-with-properties" then
return handlers.massCreateObjectsWithProperties(data)
elseif endpoint == "/api/delete-object" then
return handlers.deleteObject(data)
elseif endpoint == "/api/smart-duplicate" then
return handlers.smartDuplicate(data)
elseif endpoint == "/api/mass-duplicate" then
return handlers.massDuplicate(data)
elseif endpoint == "/api/set-calculated-property" then
return handlers.setCalculatedProperty(data)
elseif endpoint == "/api/set-relative-property" then
return handlers.setRelativeProperty(data)
elseif endpoint == "/api/get-script-source" then
return handlers.getScriptSource(data)
elseif endpoint == "/api/set-script-source" then
return handlers.setScriptSource(data)
-- Partial script editing endpoints
elseif endpoint == "/api/edit-script-lines" then
return handlers.editScriptLines(data)
elseif endpoint == "/api/insert-script-lines" then
return handlers.insertScriptLines(data)
elseif endpoint == "/api/delete-script-lines" then
return handlers.deleteScriptLines(data)
-- Attribute endpoints
elseif endpoint == "/api/get-attribute" then
return handlers.getAttribute(data)
elseif endpoint == "/api/set-attribute" then
return handlers.setAttribute(data)
elseif endpoint == "/api/get-attributes" then
return handlers.getAttributes(data)
elseif endpoint == "/api/delete-attribute" then
return handlers.deleteAttribute(data)
-- Tag endpoints
elseif endpoint == "/api/get-tags" then
return handlers.getTags(data)
elseif endpoint == "/api/add-tag" then
return handlers.addTag(data)
elseif endpoint == "/api/remove-tag" then
return handlers.removeTag(data)
elseif endpoint == "/api/get-tagged" then
return handlers.getTagged(data)
-- Selection endpoints
elseif endpoint == "/api/get-selection" then
return handlers.getSelection(data)
-- NEW endpoints (v2.0.0)
elseif endpoint == "/api/reparent-object" then
return handlers.reparentObject(data)
elseif endpoint == "/api/clone-object" then
return handlers.cloneObject(data)
elseif endpoint == "/api/get-descendants" then
return handlers.getDescendants(data)
elseif endpoint == "/api/batch-operations" then
return handlers.batchOperations(data)
elseif endpoint == "/api/undo" then
return handlers.undo(data)
elseif endpoint == "/api/redo" then
return handlers.redo(data)
elseif endpoint == "/api/group-objects" then
return handlers.groupObjects(data)
elseif endpoint == "/api/ungroup-objects" then
return handlers.ungroupObjects(data)
elseif endpoint == "/api/get-bounding-box" then
return handlers.getBoundingBox(data)
elseif endpoint == "/api/create-weld" then
return handlers.createWeld(data)
elseif endpoint == "/api/raycast" then
return handlers.raycast(data)
elseif endpoint == "/api/fill-terrain" then
return handlers.fillTerrain(data)
elseif endpoint == "/api/clear-terrain" then
return handlers.clearTerrain(data)
elseif endpoint == "/api/execute-lua" then
return handlers.executeLua(data)
elseif endpoint == "/api/set-selection" then
return handlers.setSelection(data)
elseif endpoint == "/api/mass-reparent" then
return handlers.massReparent(data)
else
addLog("WARN", "Unknown endpoint: " .. tostring(endpoint))
return { error = "Unknown endpoint: " .. tostring(endpoint) }
end
end
local function getInstanceByPath(path)
if path == "game" or path == "" then
return game
end
path = path:gsub("^game%.", "")
local parts = {}
for part in path:gmatch("[^%.]+") do
table.insert(parts, part)
end
local current = game
for _, part in ipairs(parts) do
current = current:FindFirstChild(part)
if not current then
return nil
end
end
return current
end
handlers.getFileTree = function(requestData)
local path = requestData.path or ""
local startInstance = getInstanceByPath(path)
if not startInstance then
return { error = "Path not found: " .. path }
end
local function buildTree(instance, depth)
if depth > 10 then
return { name = instance.Name, className = instance.ClassName, children = {} }
end
local node = {
name = instance.Name,
className = instance.ClassName,
path = getInstancePath(instance),
children = {},
}
if instance:IsA("LuaSourceContainer") then
node.hasSource = true
node.scriptType = instance.ClassName
end
for _, child in ipairs(instance:GetChildren()) do
table.insert(node.children, buildTree(child, depth + 1))
end
return node
end
return {
tree = buildTree(startInstance, 0),
timestamp = tick(),
}
end
handlers.searchFiles = function(requestData)
local query = requestData.query
local searchType = requestData.searchType or "name"
if not query then
return { error = "Query is required" }
end
local results = {}
local function searchRecursive(instance)
local match = false
if searchType == "name" then
match = instance.Name:lower():find(query:lower()) ~= nil
elseif searchType == "type" then
match = instance.ClassName:lower():find(query:lower()) ~= nil
elseif searchType == "content" and instance:IsA("LuaSourceContainer") then
match = instance.Source:lower():find(query:lower()) ~= nil
end
if match then
table.insert(results, {
name = instance.Name,
className = instance.ClassName,
path = getInstancePath(instance),
hasSource = instance:IsA("LuaSourceContainer"),
})
end
for _, child in ipairs(instance:GetChildren()) do
searchRecursive(child)
end
end
searchRecursive(game)
return {
results = results,
query = query,
searchType = searchType,
count = #results,
}
end
handlers.getPlaceInfo = function(requestData)
return {
placeName = game.Name,
placeId = game.PlaceId,
gameId = game.GameId,
jobId = game.JobId,
workspace = {
name = workspace.Name,
className = workspace.ClassName,
},
}
end
handlers.getServices = function(requestData)
local serviceName = requestData.serviceName
if serviceName then
local service = safeCall(game.GetService, game, serviceName)
if service then
return {
service = {
name = service.Name,
className = service.ClassName,
path = getInstancePath(service),
childCount = #service:GetChildren(),
},
}
else
return { error = "Service not found: " .. serviceName }
end
else
local services = {}
local commonServices = {
"Workspace",
"Players",
"StarterGui",
"StarterPack",
"StarterPlayer",
"ReplicatedStorage",
"ServerStorage",
"ServerScriptService",
"HttpService",
"TeleportService",
"DataStoreService",
}
for _, serviceName in ipairs(commonServices) do
local service = safeCall(game.GetService, game, serviceName)
if service then
table.insert(services, {
name = service.Name,
className = service.ClassName,
path = getInstancePath(service),
childCount = #service:GetChildren(),
})
end
end
return { services = services }
end
end
handlers.searchObjects = function(requestData)
local query = requestData.query
local searchType = requestData.searchType or "name"
local propertyName = requestData.propertyName
if not query then
return { error = "Query is required" }
end
local results = {}
local function searchRecursive(instance)
local match = false
if searchType == "name" then
match = instance.Name:lower():find(query:lower()) ~= nil
elseif searchType == "class" then
match = instance.ClassName:lower():find(query:lower()) ~= nil
elseif searchType == "property" and propertyName then
local success, value = pcall(function()
return tostring(instance[propertyName])
end)
if success then
match = value:lower():find(query:lower()) ~= nil
end
end
if match then
table.insert(results, {
name = instance.Name,
className = instance.ClassName,
path = getInstancePath(instance),
})
end
for _, child in ipairs(instance:GetChildren()) do
searchRecursive(child)
end
end
searchRecursive(game)
return {
results = results,
query = query,
searchType = searchType,
count = #results,
}
end
handlers.getInstanceProperties = function(requestData)
local instancePath = requestData.instancePath
if not instancePath then
return { error = "Instance path is required" }
end
local instance = getInstanceByPath(instancePath)
if not instance then
return { error = "Instance not found: " .. instancePath }
end
local properties = {}
local success, result = pcall(function()
local classInfo = {}
local basicProps = { "Name", "ClassName", "Parent" }
for _, prop in ipairs(basicProps) do
local propSuccess, propValue = pcall(function()
local val = instance[prop]
if prop == "Parent" and val then
return getInstancePath(val)
elseif val == nil then
return "nil"
else
return tostring(val)
end
end)
if propSuccess then
properties[prop] = propValue
end
end
local commonProps = {
"Size",
"Position",
"Rotation",
"CFrame",
"Anchored",
"CanCollide",
"Transparency",
"BrickColor",
"Material",
"Color",
"Text",
"TextColor3",
"BackgroundColor3",
"Image",
"ImageColor3",
"Visible",
"Active",
"ZIndex",
"BorderSizePixel",
"BackgroundTransparency",
"ImageTransparency",
"TextTransparency",
"Value",
"Enabled",
"Brightness",
"Range",
"Shadows",
"Face",
"SurfaceType",
}
for _, prop in ipairs(commonProps) do
local propSuccess, propValue = pcall(function()
return tostring(instance[prop])
end)
if propSuccess then
properties[prop] = propValue
end
end
if instance:IsA("LuaSourceContainer") then
properties.Source = instance.Source
if instance:IsA("BaseScript") then
properties.Enabled = tostring(instance.Enabled)
end
end
-- Only Parts have a Shape property; MeshParts do not.
if instance:IsA("Part") then
properties.Shape = tostring(instance.Shape)
end
-- TopSurface and BottomSurface exist on all BaseParts
if instance:IsA("BasePart") then
properties.TopSurface = tostring(instance.TopSurface)
properties.BottomSurface = tostring(instance.BottomSurface)
end
properties.ChildCount = tostring(#instance:GetChildren())
return properties
end)
if success then
return {
instancePath = instancePath,
className = instance.ClassName,
properties = properties,
}
else
return { error = "Failed to get properties: " .. tostring(result) }
end
end
handlers.getInstanceChildren = function(requestData)
local instancePath = requestData.instancePath
if not instancePath then
return { error = "Instance path is required" }
end
local instance = getInstanceByPath(instancePath)
if not instance then
return { error = "Instance not found: " .. instancePath }
end
local children = {}
for _, child in ipairs(instance:GetChildren()) do
table.insert(children, {
name = child.Name,
className = child.ClassName,
path = getInstancePath(child),
hasChildren = #child:GetChildren() > 0,
hasSource = child:IsA("LuaSourceContainer"),
})
end
return {
instancePath = instancePath,
children = children,
count = #children,
}
end
handlers.searchByProperty = function(requestData)
local propertyName = requestData.propertyName
local propertyValue = requestData.propertyValue
if not propertyName or not propertyValue then
return { error = "Property name and value are required" }
end
local results = {}
local function searchRecursive(instance)
local success, value = pcall(function()
return tostring(instance[propertyName])
end)
if success and value:lower():find(propertyValue:lower()) then
table.insert(results, {
name = instance.Name,
className = instance.ClassName,
path = getInstancePath(instance),
propertyValue = value,
})
end
for _, child in ipairs(instance:GetChildren()) do
searchRecursive(child)
end
end
searchRecursive(game)
return {
propertyName = propertyName,
propertyValue = propertyValue,
results = results,
count = #results,
}
end
handlers.getClassInfo = function(requestData)
local className = requestData.className
if not className then
return { error = "Class name is required" }
end
local success, tempInstance = pcall(function()
return Instance.new(className)
end)
if not success then
return { error = "Invalid class name: " .. className }
end
local classInfo = {
className = className,
properties = {},
methods = {},
events = {},
}
local commonProps = {
"Name",
"ClassName",
"Parent",
"Size",
"Position",
"Rotation",
"CFrame",
"Anchored",
"CanCollide",
"Transparency",
"BrickColor",
"Material",
"Color",
"Text",
"TextColor3",
"BackgroundColor3",
"Image",
"ImageColor3",
"Visible",
"Active",
"ZIndex",
"BorderSizePixel",
"BackgroundTransparency",
"ImageTransparency",
"TextTransparency",
"Value",
"Enabled",
"Brightness",
"Range",
"Shadows",
}
for _, prop in ipairs(commonProps) do
local propSuccess, _ = pcall(function()
return tempInstance[prop]
end)
if propSuccess then
table.insert(classInfo.properties, prop)
end
end
local commonMethods = {
"Destroy",
"Clone",
"FindFirstChild",
"FindFirstChildOfClass",
"GetChildren",
"IsA",
"IsAncestorOf",
"IsDescendantOf",
"WaitForChild",
}
for _, method in ipairs(commonMethods) do
local methodSuccess, _ = pcall(function()
return tempInstance[method]
end)
if methodSuccess then
table.insert(classInfo.methods, method)
end
end
tempInstance:Destroy()
return classInfo
end
handlers.getProjectStructure = function(requestData)
local startPath = requestData.path or ""
local maxDepth = requestData.maxDepth or 3
local showScriptsOnly = requestData.scriptsOnly or false
local startInstance
if startPath == "" or startPath == "game" then
local services = {}
local mainServices = {
"Workspace",
"ServerScriptService",
"ServerStorage",
"ReplicatedStorage",
"StarterGui",
"StarterPack",
"StarterPlayer",
"Players",
}
for _, serviceName in ipairs(mainServices) do
local service = safeCall(game.GetService, game, serviceName)
if service then
local serviceInfo = {
name = service.Name,
className = service.ClassName,
path = getInstancePath(service),
childCount = #service:GetChildren(),
hasChildren = #service:GetChildren() > 0,
}
table.insert(services, serviceInfo)
end
end
return {
type = "service_overview",
services = services,
timestamp = tick(),
note = "Use path parameter to explore specific locations (e.g., 'game.ServerScriptService')",
}
else
startInstance = getInstanceByPath(startPath)
if not startInstance then
return { error = "Path not found: " .. startPath }
end
end
local function getStructure(instance, depth, currentPath)
if depth > maxDepth then
return {
name = instance.Name,
className = instance.ClassName,
path = getInstancePath(instance),
childCount = #instance:GetChildren(),
hasMore = true,
note = "Max depth reached - use this path to explore further",
}
end
local node = {
name = instance.Name,
className = instance.ClassName,
path = getInstancePath(instance),
children = {},
}
if instance:IsA("LuaSourceContainer") then
node.hasSource = true
node.scriptType = instance.ClassName
if instance:IsA("BaseScript") then
node.enabled = instance.Enabled
end
end
if instance:IsA("GuiObject") then
node.visible = instance.Visible
if instance:IsA("Frame") or instance:IsA("ScreenGui") then
node.guiType = "container"
elseif instance:IsA("TextLabel") or instance:IsA("TextButton") then
node.guiType = "text"
if instance.Text and instance.Text ~= "" then
node.text = instance.Text
end
elseif instance:IsA("ImageLabel") or instance:IsA("ImageButton") then
node.guiType = "image"
end
end
local children = instance:GetChildren()
if showScriptsOnly then
local scriptChildren = {}
for _, child in ipairs(children) do
if child:IsA("BaseScript") or child:IsA("Folder") or child:IsA("ModuleScript") then
table.insert(scriptChildren, child)
end
end
children = scriptChildren
end
local childCount = #children
if childCount > 20 and depth < maxDepth then
local classGroups = {}
for _, child in ipairs(children) do
local className = child.ClassName
if not classGroups[className] then
classGroups[className] = {}
end
table.insert(classGroups[className], child)
end
node.childSummary = {}
for className, classChildren in pairs(classGroups) do
table.insert(node.childSummary, {
className = className,
count = #classChildren,
examples = {
classChildren[1] and classChildren[1].Name,
classChildren[2] and classChildren[2].Name,
},
})
end
for className, classChildren in pairs(classGroups) do
for i = 1, math.min(3, #classChildren) do
table.insert(node.children, getStructure(classChildren[i], depth + 1, currentPath))
end
if #classChildren > 3 then
table.insert(node.children, {
name = "... " .. (#classChildren - 3) .. " more " .. className .. " objects",
className = "MoreIndicator",
path = getInstancePath(instance) .. " [" .. className .. " children]",
note = "Use specific path to explore these objects",
})
end
end
else
for _, child in ipairs(children) do
table.insert(node.children, getStructure(child, depth + 1, currentPath))
end
end
return node
end
local result = getStructure(startInstance, 0, startPath)
result.requestedPath = startPath
result.maxDepth = maxDepth
result.scriptsOnly = showScriptsOnly
result.timestamp = tick()
return result
end
handlers.setProperty = function(requestData)
local instancePath = requestData.instancePath
local propertyName = requestData.propertyName
local propertyValue = requestData.propertyValue
if not instancePath or not propertyName then
return { error = "Instance path and property name are required" }
end
local instance = getInstanceByPath(instancePath)
if not instance then
return { error = "Instance not found: " .. instancePath }
end
local success, result = pcall(function()
-- Handle instance reference properties (Parent, PrimaryPart, etc.)
if propertyName == "Parent" or propertyName == "PrimaryPart" then
if type(propertyValue) == "string" then
local refInstance = getInstanceByPath(propertyValue)
if refInstance then
instance[propertyName] = refInstance
else
return { error = propertyName .. " instance not found: " .. propertyValue }
end
end
elseif propertyName == "Name" then
instance.Name = tostring(propertyValue)
elseif propertyName == "Source" and instance:IsA("LuaSourceContainer") then
instance.Source = tostring(propertyValue)
else
-- Use the generic converter for all other properties
local convertedValue = convertPropertyValue(instance, propertyName, propertyValue)
if convertedValue ~= nil then
instance[propertyName] = convertedValue
else
instance[propertyName] = propertyValue
end
end
ChangeHistoryService:SetWaypoint("Set " .. propertyName .. " property")
return true
end)
if success and result ~= false then
return {
success = true,
instancePath = instancePath,
propertyName = propertyName,
propertyValue = propertyValue,
message = "Property set successfully",
}
else
return {
error = "Failed to set property: " .. tostring(result),
instancePath = instancePath,
propertyName = propertyName,
}
end
end
handlers.createObject = function(requestData)
local className = requestData.className
local parentPath = requestData.parent
local name = requestData.name
local properties = requestData.properties or {}
if not className or not parentPath then
return { error = "Class name and parent are required" }
end
local parentInstance = getInstanceByPath(parentPath)
if not parentInstance then
return { error = "Parent instance not found: " .. parentPath }
end
local success, newInstance = pcall(function()
local instance = Instance.new(className)
if name then
instance.Name = name
end
for propertyName, propertyValue in pairs(properties) do
pcall(function()
instance[propertyName] = propertyValue
end)
end
instance.Parent = parentInstance
ChangeHistoryService:SetWaypoint("Create " .. className)
return instance
end)
if success and newInstance then
return {
success = true,
className = className,
parent = parentPath,
instancePath = getInstancePath(newInstance),
name = newInstance.Name,
message = "Object created successfully",
}
else
return {
error = "Failed to create object: " .. tostring(newInstance),
className = className,
parent = parentPath,
}
end
end
handlers.deleteObject = function(requestData)
local instancePath = requestData.instancePath
if not instancePath then
return { error = "Instance path is required" }
end
local instance = getInstanceByPath(instancePath)
if not instance then
return { error = "Instance not found: " .. instancePath }
end
if instance == game then
return { error = "Cannot delete the game instance" }
end
local success, result = pcall(function()
local name = instance.Name
local className = instance.ClassName
instance:Destroy()
ChangeHistoryService:SetWaypoint("Delete " .. className .. " (" .. name .. ")")
return true
end)
if success then
return {
success = true,
instancePath = instancePath,
message = "Object deleted successfully",
}
else
return {
error = "Failed to delete object: " .. tostring(result),
instancePath = instancePath,
}
end
end
handlers.massSetProperty = function(requestData)
local paths = requestData.paths
local propertyName = requestData.propertyName
local propertyValue = requestData.propertyValue
if not paths or type(paths) ~= "table" or #paths == 0 or not propertyName then
return { error = "Paths array and property name are required" }
end
local results = {}
local successCount = 0
local failureCount = 0
for _, path in ipairs(paths) do
local instance = getInstanceByPath(path)
if instance then
local success, err = pcall(function()
-- Use convertPropertyValue to handle Vector3, Color3, etc.
local convertedValue = convertPropertyValue(instance, propertyName, propertyValue)
if convertedValue ~= nil then
instance[propertyName] = convertedValue
else
instance[propertyName] = propertyValue
end
end)
if success then
successCount = successCount + 1
table.insert(results, {
path = path,
success = true,
propertyName = propertyName,
propertyValue = propertyValue
})
else
failureCount = failureCount + 1
table.insert(results, {
path = path,
success = false,
error = tostring(err)
})
end
else
failureCount = failureCount + 1
table.insert(results, {
path = path,
success = false,
error = "Instance not found"
})
end
end
if successCount > 0 then
ChangeHistoryService:SetWaypoint("Mass set " .. propertyName .. " property")
end
return {
results = results,
summary = {
total = #paths,
succeeded = successCount,
failed = failureCount
}
}
end
handlers.massGetProperty = function(requestData)
local paths = requestData.paths
local propertyName = requestData.propertyName
if not paths or type(paths) ~= "table" or #paths == 0 or not propertyName then
return { error = "Paths array and property name are required" }
end
local results = {}
for _, path in ipairs(paths) do
local instance = getInstanceByPath(path)
if instance then
local success, value = pcall(function()
return instance[propertyName]
end)
if success then
table.insert(results, {
path = path,
success = true,
propertyName = propertyName,
propertyValue = value
})
else
table.insert(results, {
path = path,
success = false,
error = tostring(value)
})
end
else
table.insert(results, {
path = path,
success = false,
error = "Instance not found"
})
end
end
return {
results = results,
propertyName = propertyName
}
end
handlers.massCreateObjects = function(requestData)
local objects = requestData.objects
if not objects or type(objects) ~= "table" or #objects == 0 then
return { error = "Objects array is required" }
end
local results = {}
local successCount = 0
local failureCount = 0
for _, objData in ipairs(objects) do
local className = objData.className
local parentPath = objData.parent
local name = objData.name
if className and parentPath then
local parentInstance = getInstanceByPath(parentPath)
if parentInstance then
local success, newInstance = pcall(function()
local instance = Instance.new(className)
if name then
instance.Name = name
end
instance.Parent = parentInstance
return instance
end)
if success and newInstance then
successCount = successCount + 1
table.insert(results, {
success = true,
className = className,
parent = parentPath,
instancePath = getInstancePath(newInstance),
name = newInstance.Name
})
else
failureCount = failureCount + 1
table.insert(results, {
success = false,
className = className,
parent = parentPath,
error = tostring(newInstance)
})
end
else
failureCount = failureCount + 1
table.insert(results, {
success = false,
className = className,
parent = parentPath,
error = "Parent instance not found"
})
end
else
failureCount = failureCount + 1
table.insert(results, {
success = false,
error = "Class name and parent are required"
})
end
end
if successCount > 0 then
ChangeHistoryService:SetWaypoint("Mass create objects")
end
return {
results = results,
summary = {
total = #objects,
succeeded = successCount,
failed = failureCount
}
}
end
handlers.massCreateObjectsWithProperties = function(requestData)
local objects = requestData.objects
if not objects or type(objects) ~= "table" or #objects == 0 then
return { error = "Objects array is required" }
end
local results = {}
local successCount = 0
local failureCount = 0
for _, objData in ipairs(objects) do
local className = objData.className
local parentPath = objData.parent
local name = objData.name
local properties = objData.properties or {}
if className and parentPath then
local parentInstance = getInstanceByPath(parentPath)
if parentInstance then
local success, newInstance = pcall(function()
local instance = Instance.new(className)
if name then
instance.Name = name
end
-- Set Parent first so property type inference works
instance.Parent = parentInstance
for propertyName, propertyValue in pairs(properties) do
pcall(function()
local convertedValue = convertPropertyValue(instance, propertyName, propertyValue)
if convertedValue ~= nil then
instance[propertyName] = convertedValue
end
end)
end
return instance
end)
if success and newInstance then
successCount = successCount + 1
table.insert(results, {
success = true,
className = className,
parent = parentPath,
instancePath = getInstancePath(newInstance),
name = newInstance.Name
})
else
failureCount = failureCount + 1
table.insert(results, {
success = false,
className = className,
parent = parentPath,
error = tostring(newInstance)
})
end
else
failureCount = failureCount + 1
table.insert(results, {
success = false,
className = className,
parent = parentPath,
error = "Parent instance not found"
})
end
else
failureCount = failureCount + 1
table.insert(results, {
success = false,
error = "Class name and parent are required"
})
end
end
if successCount > 0 then
ChangeHistoryService:SetWaypoint("Mass create objects with properties")
end
return {
results = results,
summary = {
total = #objects,
succeeded = successCount,
failed = failureCount
}
}
end
handlers.smartDuplicate = function(requestData)
local instancePath = requestData.instancePath
local count = requestData.count
local options = requestData.options or {}
if not instancePath or not count or count < 1 then
return { error = "Instance path and count > 0 are required" }
end
local instance = getInstanceByPath(instancePath)
if not instance then
return { error = "Instance not found: " .. instancePath }
end
local results = {}
local successCount = 0
local failureCount = 0
for i = 1, count do
local success, newInstance = pcall(function()
local clone = instance:Clone()
if options.namePattern then
clone.Name = options.namePattern:gsub("{n}", tostring(i))
else
clone.Name = instance.Name .. i
end
if options.positionOffset and clone:IsA("BasePart") then
local offset = options.positionOffset
local currentPos = clone.Position
clone.Position = Vector3.new(
currentPos.X + (offset[1] or 0) * i,
currentPos.Y + (offset[2] or 0) * i,
currentPos.Z + (offset[3] or 0) * i
)
end
if options.rotationOffset and clone:IsA("BasePart") then
local offset = options.rotationOffset
local currentCFrame = clone.CFrame
clone.CFrame = currentCFrame * CFrame.Angles(
math.rad((offset[1] or 0) * i),
math.rad((offset[2] or 0) * i),
math.rad((offset[3] or 0) * i)
)
end
if options.scaleOffset and clone:IsA("BasePart") then
local offset = options.scaleOffset
local currentSize = clone.Size
clone.Size = Vector3.new(
currentSize.X * ((offset[1] or 1) ^ i),
currentSize.Y * ((offset[2] or 1) ^ i),
currentSize.Z * ((offset[3] or 1) ^ i)
)
end
if options.propertyVariations then
for propName, values in pairs(options.propertyVariations) do
if values and #values > 0 then
local valueIndex = ((i - 1) % #values) + 1
pcall(function()
clone[propName] = values[valueIndex]
end)
end
end
end
if options.targetParents and options.targetParents[i] then
local targetParent = getInstanceByPath(options.targetParents[i])
if targetParent then
clone.Parent = targetParent
else
clone.Parent = instance.Parent
end
else
clone.Parent = instance.Parent
end
return clone
end)
if success and newInstance then
successCount = successCount + 1
table.insert(results, {
success = true,
instancePath = getInstancePath(newInstance),
name = newInstance.Name,
index = i
})
else
failureCount = failureCount + 1
table.insert(results, {
success = false,
index = i,
error = tostring(newInstance)
})
end
end
if successCount > 0 then
ChangeHistoryService:SetWaypoint("Smart duplicate " .. instance.Name .. " (" .. successCount .. " copies)")
end
return {
results = results,
summary = {
total = count,
succeeded = successCount,
failed = failureCount
},
sourceInstance = instancePath
}
end
handlers.massDuplicate = function(requestData)
local duplications = requestData.duplications
if not duplications or type(duplications) ~= "table" or #duplications == 0 then
return { error = "Duplications array is required" }
end
local allResults = {}
local totalSuccess = 0
local totalFailures = 0
for _, duplication in ipairs(duplications) do
local result = handlers.smartDuplicate(duplication)
table.insert(allResults, result)
if result.summary then
totalSuccess = totalSuccess + result.summary.succeeded
totalFailures = totalFailures + result.summary.failed
end
end
if totalSuccess > 0 then
ChangeHistoryService:SetWaypoint("Mass duplicate operations (" .. totalSuccess .. " objects)")
end
return {
results = allResults,
summary = {
total = totalSuccess + totalFailures,
succeeded = totalSuccess,
failed = totalFailures
}
}
end
local function evaluateFormula(formula, variables, instance, index)
local value = formula
value = value:gsub("index", tostring(index))
if instance and instance:IsA("BasePart") then
local pos = instance.Position
local size = instance.Size
value = value:gsub("Position%.X", tostring(pos.X))
value = value:gsub("Position%.Y", tostring(pos.Y))
value = value:gsub("Position%.Z", tostring(pos.Z))
value = value:gsub("Size%.X", tostring(size.X))
value = value:gsub("Size%.Y", tostring(size.Y))
value = value:gsub("Size%.Z", tostring(size.Z))
value = value:gsub("magnitude", tostring(pos.magnitude))
end
if variables then
for k, v in pairs(variables) do
value = value:gsub(k, tostring(v))
end
end
value = value:gsub("sin%(([%d%.%-]+)%)", function(x) return tostring(math.sin(tonumber(x) or 0)) end)
value = value:gsub("cos%(([%d%.%-]+)%)", function(x) return tostring(math.cos(tonumber(x) or 0)) end)
value = value:gsub("sqrt%(([%d%.%-]+)%)", function(x) return tostring(math.sqrt(tonumber(x) or 0)) end)
value = value:gsub("abs%(([%d%.%-]+)%)", function(x) return tostring(math.abs(tonumber(x) or 0)) end)
value = value:gsub("floor%(([%d%.%-]+)%)", function(x) return tostring(math.floor(tonumber(x) or 0)) end)
value = value:gsub("ceil%(([%d%.%-]+)%)", function(x) return tostring(math.ceil(tonumber(x) or 0)) end)
local result = tonumber(value)
if result then
return result, nil
end
local success, evalResult = pcall(function()
local num = tonumber(value)
if num then
return num
end
local a, b = value:match("^([%d%.%-]+)%s*%*%s*([%d%.%-]+)$")
if a and b then
return (tonumber(a) or 0) * (tonumber(b) or 0)
end
a, b = value:match("^([%d%.%-]+)%s*%+%s*([%d%.%-]+)$")
if a and b then
return (tonumber(a) or 0) + (tonumber(b) or 0)
end
a, b = value:match("^([%d%.%-]+)%s*%-%s*([%d%.%-]+)$")
if a and b then
return (tonumber(a) or 0) - (tonumber(b) or 0)
end
a, b = value:match("^([%d%.%-]+)%s*/%s*([%d%.%-]+)$")
if a and b then
local divisor = tonumber(b) or 1
if divisor ~= 0 then
return (tonumber(a) or 0) / divisor
end
end
error("Unsupported formula pattern: " .. value)
end)
if success and type(evalResult) == "number" then
return evalResult, nil
else
return index, "Complex formulas not supported - using index value"
end
end
handlers.setCalculatedProperty = function(requestData)
local paths = requestData.paths
local propertyName = requestData.propertyName
local formula = requestData.formula
local variables = requestData.variables
if not paths or type(paths) ~= "table" or #paths == 0 or not propertyName or not formula then
return { error = "Paths, property name, and formula are required" }
end
local results = {}
local successCount = 0
local failureCount = 0
for index, path in ipairs(paths) do
local instance = getInstanceByPath(path)
if instance then
local value, evalError = evaluateFormula(formula, variables, instance, index)
if value ~= nil and not evalError then
local success, err = pcall(function()
instance[propertyName] = value
end)
if success then
successCount = successCount + 1
table.insert(results, {
path = path,
success = true,
propertyName = propertyName,
calculatedValue = value,
formula = formula
})
else
failureCount = failureCount + 1
table.insert(results, {
path = path,
success = false,
error = "Property set failed: " .. tostring(err)
})
end
else
failureCount = failureCount + 1
table.insert(results, {
path = path,
success = false,
error = evalError or "Formula evaluation failed"
})
end
else
failureCount = failureCount + 1
table.insert(results, {
path = path,
success = false,
error = "Instance not found"
})
end
end
if successCount > 0 then
ChangeHistoryService:SetWaypoint("Set calculated " .. propertyName .. " property")
end
return {
results = results,
summary = {
total = #paths,
succeeded = successCount,
failed = failureCount
},
formula = formula
}
end
handlers.setRelativeProperty = function(requestData)
local paths = requestData.paths
local propertyName = requestData.propertyName
local operation = requestData.operation
local value = requestData.value
local component = requestData.component
if not paths or type(paths) ~= "table" or #paths == 0 or not propertyName or not operation or value == nil then
return { error = "Paths, property name, operation, and value are required" }
end
local results = {}
local successCount = 0
local failureCount = 0
for _, path in ipairs(paths) do
local instance = getInstanceByPath(path)
if instance then
local success, err = pcall(function()
local currentValue = instance[propertyName]
local newValue
if component and typeof(currentValue) == "Vector3" then
local x, y, z = currentValue.X, currentValue.Y, currentValue.Z
local targetValue = value
if component == "X" then
if operation == "add" then x = x + targetValue
elseif operation == "subtract" then x = x - targetValue
elseif operation == "multiply" then x = x * targetValue
elseif operation == "divide" then x = x / targetValue
elseif operation == "power" then x = x ^ targetValue
end
elseif component == "Y" then
if operation == "add" then y = y + targetValue
elseif operation == "subtract" then y = y - targetValue
elseif operation == "multiply" then y = y * targetValue
elseif operation == "divide" then y = y / targetValue
elseif operation == "power" then y = y ^ targetValue
end
elseif component == "Z" then
if operation == "add" then z = z + targetValue
elseif operation == "subtract" then z = z - targetValue
elseif operation == "multiply" then z = z * targetValue
elseif operation == "divide" then z = z / targetValue
elseif operation == "power" then z = z ^ targetValue
end
end
newValue = Vector3.new(x, y, z)
elseif typeof(currentValue) == "Color3" and typeof(value) == "Color3" then
local r, g, b = currentValue.R, currentValue.G, currentValue.B
if operation == "add" then
newValue = Color3.new(
math.min(1, r + value.R),
math.min(1, g + value.G),
math.min(1, b + value.B)
)
elseif operation == "subtract" then
newValue = Color3.new(
math.max(0, r - value.R),
math.max(0, g - value.G),
math.max(0, b - value.B)
)
elseif operation == "multiply" then
newValue = Color3.new(r * value.R, g * value.G, b * value.B)
end
elseif type(currentValue) == "number" and type(value) == "number" then
if operation == "add" then
newValue = currentValue + value
elseif operation == "subtract" then
newValue = currentValue - value
elseif operation == "multiply" then
newValue = currentValue * value
elseif operation == "divide" then
newValue = currentValue / value
elseif operation == "power" then
newValue = currentValue ^ value
end
elseif typeof(currentValue) == "Vector3" and type(value) == "number" then
local x, y, z = currentValue.X, currentValue.Y, currentValue.Z
if operation == "add" then
newValue = Vector3.new(x + value, y + value, z + value)
elseif operation == "subtract" then
newValue = Vector3.new(x - value, y - value, z - value)
elseif operation == "multiply" then
newValue = Vector3.new(x * value, y * value, z * value)
elseif operation == "divide" then
newValue = Vector3.new(x / value, y / value, z / value)
elseif operation == "power" then
newValue = Vector3.new(x ^ value, y ^ value, z ^ value)
end
else
error("Unsupported property type or operation")
end
instance[propertyName] = newValue
return newValue
end)
if success then
successCount = successCount + 1
table.insert(results, {
path = path,
success = true,
propertyName = propertyName,
operation = operation,
value = value,
component = component,
newValue = err
})
else
failureCount = failureCount + 1
table.insert(results, {
path = path,
success = false,
error = tostring(err)
})
end
else
failureCount = failureCount + 1
table.insert(results, {
path = path,
success = false,
error = "Instance not found"
})
end
end
if successCount > 0 then
ChangeHistoryService:SetWaypoint("Set relative " .. propertyName .. " property")
end
return {
results = results,
summary = {
total = #paths,
succeeded = successCount,
failed = failureCount
},
operation = operation,
value = value
}
end
handlers.getScriptSource = function(requestData)
local instancePath = requestData.instancePath
local startLine = requestData.startLine
local endLine = requestData.endLine
if not instancePath then
return { error = "Instance path is required" }
end
local instance = getInstanceByPath(instancePath)
if not instance then
return { error = "Instance not found: " .. instancePath }
end
if not instance:IsA("LuaSourceContainer") then
return { error = "Instance is not a script-like object: " .. instance.ClassName }
end
local success, result = pcall(function()
local fullSource = instance.Source
local lines, hasTrailingNewline = splitLines(fullSource)
local totalLineCount = #lines
-- If line range is specified, extract only those lines
local sourceToReturn = fullSource
local returnedStartLine = 1
local returnedEndLine = totalLineCount
if startLine or endLine then
local actualStartLine = math.max(1, startLine or 1)
local actualEndLine = math.min(#lines, endLine or #lines)
local selectedLines = {}
for i = actualStartLine, actualEndLine do
table.insert(selectedLines, lines[i] or "")
end
sourceToReturn = table.concat(selectedLines, '\n')
if hasTrailingNewline and actualEndLine == #lines and sourceToReturn:sub(-1) ~= "\n" then
sourceToReturn ..= "\n"
end
returnedStartLine = actualStartLine
returnedEndLine = actualEndLine
end
-- Build numbered source for AI agents to accurately identify line numbers
local numberedLines = {}
local linesToNumber = startLine and select(1, splitLines(sourceToReturn)) or lines
local lineOffset = returnedStartLine - 1
for i, line in ipairs(linesToNumber) do
table.insert(numberedLines, (i + lineOffset) .. ": " .. line)
end
local numberedSource = table.concat(numberedLines, "\n")
local resp = {
instancePath = instancePath,
className = instance.ClassName,
name = instance.Name,
source = sourceToReturn,
numberedSource = numberedSource,
sourceLength = string.len(fullSource),
lineCount = totalLineCount,
-- Line range info
startLine = returnedStartLine,
endLine = returnedEndLine,
isPartial = (startLine ~= nil or endLine ~= nil),
-- Helpful metadata for large scripts
truncated = false,
}
-- If the source is very large (>50000 chars) and no range specified,
-- return first 1000 lines with truncation notice
if not startLine and not endLine and string.len(fullSource) > 50000 then
local truncatedLines = {}
local truncatedNumberedLines = {}
local maxLines = math.min(1000, #lines)
for i = 1, maxLines do
table.insert(truncatedLines, lines[i])
table.insert(truncatedNumberedLines, i .. ": " .. lines[i])
end
resp.source = table.concat(truncatedLines, '\n')
resp.numberedSource = table.concat(truncatedNumberedLines, '\n')
resp.truncated = true
resp.endLine = maxLines
resp.note = "Script truncated to first 1000 lines. Use startLine/endLine parameters to read specific sections."
end
if instance:IsA("BaseScript") then
resp.enabled = instance.Enabled
end
return resp
end)
if success then
return result
else
return { error = "Failed to get script source: " .. tostring(result) }
end
end
handlers.setScriptSource = function(requestData)
local instancePath = requestData.instancePath
local newSource = requestData.source
if not instancePath or not newSource then
return { error = "Instance path and source are required" }
end
local instance = getInstanceByPath(instancePath)
if not instance then
return { error = "Instance not found: " .. instancePath }
end
if not instance:IsA("LuaSourceContainer") then
return { error = "Instance is not a script-like object: " .. instance.ClassName }
end
-- Normalize escape sequences that may have been double-escaped
local sourceToSet = newSource :: string
-- Fix double-escaped newlines, tabs, etc.
sourceToSet = sourceToSet:gsub("\\n", "\n")
sourceToSet = sourceToSet:gsub("\\t", "\t")
sourceToSet = sourceToSet:gsub("\\r", "\r")
sourceToSet = sourceToSet:gsub("\\\\", "\\")
local updateSuccess, updateResult = pcall(function()
local oldSourceLength = string.len(instance.Source)
ScriptEditorService:UpdateSourceAsync(instance, function(oldContent)
return sourceToSet
end)
ChangeHistoryService:SetWaypoint("Set script source: " .. instance.Name)
return {
success = true,
instancePath = instancePath,
oldSourceLength = oldSourceLength,
newSourceLength = string.len(sourceToSet),
method = "UpdateSourceAsync",
message = "Script source updated successfully (editor-safe)"
}
end)
if updateSuccess then
return updateResult
end
-- Fallback to direct assignment if UpdateSourceAsync fails
local directSuccess, directResult = pcall(function()
local oldSource = instance.Source
instance.Source = sourceToSet
ChangeHistoryService:SetWaypoint("Set script source: " .. instance.Name)
return {
success = true,
instancePath = instancePath,
oldSourceLength = string.len(oldSource),
newSourceLength = string.len(sourceToSet),
method = "direct",
message = "Script source updated successfully (direct assignment)"
}
end)
if directSuccess then
return directResult
end
-- Final fallback: replace the script entirely
local replaceSuccess, replaceResult = pcall(function()
local parent = instance.Parent
local name = instance.Name
local className = instance.ClassName
local wasBaseScript = instance:IsA("BaseScript")
local enabled
if wasBaseScript then
enabled = instance.Enabled
end
local newScript = Instance.new(className)
newScript.Name = name
newScript.Source = sourceToSet
if wasBaseScript then
newScript.Enabled = enabled
end
newScript.Parent = parent
instance:Destroy()
ChangeHistoryService:SetWaypoint("Replace script: " .. name)
return {
success = true,
instancePath = getInstancePath(newScript),
method = "replace",
message = "Script replaced successfully with new source"
}
end)
if replaceSuccess then
return replaceResult
else
return {
error = "Failed to set script source. UpdateSourceAsync failed: " .. tostring(updateResult) ..
". Direct assignment failed: " .. tostring(directResult) ..
". Replace method failed: " .. tostring(replaceResult)
}
end
end
-- Partial Script Editing: Edit specific lines
handlers.editScriptLines = function(requestData)
local instancePath = requestData.instancePath
local startLine = requestData.startLine
local endLine = requestData.endLine
local newContent = requestData.newContent
if not instancePath or not startLine or not endLine or not newContent then
return { error = "Instance path, startLine, endLine, and newContent are required" }
end
-- Normalize escape sequences that may have been double-escaped
newContent = newContent:gsub("\\n", "\n")
newContent = newContent:gsub("\\t", "\t")
newContent = newContent:gsub("\\r", "\r")
newContent = newContent:gsub("\\\\", "\\")
local instance = getInstanceByPath(instancePath)
if not instance then
return { error = "Instance not found: " .. instancePath }
end
if not instance:IsA("LuaSourceContainer") then
return { error = "Instance is not a script-like object: " .. instance.ClassName }
end
local success, result = pcall(function()
local lines, hadTrailingNewline = splitLines(instance.Source)
local totalLines = #lines
if startLine < 1 or startLine > totalLines then
error("startLine out of range (1-" .. totalLines .. ")")
end
if endLine < startLine or endLine > totalLines then
error("endLine out of range (" .. startLine .. "-" .. totalLines .. ")")
end
-- Split new content into lines
local newLines = select(1, splitLines(newContent))
-- Build new source: lines before + new content + lines after
local resultLines = {}
-- Lines before the edit
for i = 1, startLine - 1 do
table.insert(resultLines, lines[i])
end
-- New content lines
for _, line in ipairs(newLines) do
table.insert(resultLines, line)
end
-- Lines after the edit
for i = endLine + 1, totalLines do
table.insert(resultLines, lines[i])
end
local newSource = joinLines(resultLines, hadTrailingNewline)
-- Use UpdateSourceAsync for editor compatibility
ScriptEditorService:UpdateSourceAsync(instance, function(oldContent)
return newSource
end)
ChangeHistoryService:SetWaypoint("Edit script lines " .. startLine .. "-" .. endLine .. ": " .. instance.Name)
return {
success = true,
instancePath = instancePath,
editedLines = { startLine = startLine, endLine = endLine },
linesRemoved = endLine - startLine + 1,
linesAdded = #newLines,
newLineCount = #resultLines,
message = "Script lines edited successfully"
}
end)
if success then
return result
else
return { error = "Failed to edit script lines: " .. tostring(result) }
end
end
-- Partial Script Editing: Insert lines after a specific line
handlers.insertScriptLines = function(requestData)
local instancePath = requestData.instancePath
local afterLine = requestData.afterLine or 0 -- 0 means insert at beginning
local newContent = requestData.newContent
if not instancePath or not newContent then
return { error = "Instance path and newContent are required" }
end
-- Normalize escape sequences that may have been double-escaped
newContent = newContent:gsub("\\n", "\n")
newContent = newContent:gsub("\\t", "\t")
newContent = newContent:gsub("\\r", "\r")
newContent = newContent:gsub("\\\\", "\\")
local instance = getInstanceByPath(instancePath)
if not instance then
return { error = "Instance not found: " .. instancePath }
end
if not instance:IsA("LuaSourceContainer") then
return { error = "Instance is not a script-like object: " .. instance.ClassName }
end
local success, result = pcall(function()
local lines, hadTrailingNewline = splitLines(instance.Source)
local totalLines = #lines
if afterLine < 0 or afterLine > totalLines then
error("afterLine out of range (0-" .. totalLines .. ")")
end
-- Split new content into lines
local newLines = select(1, splitLines(newContent))
-- Build new source
local resultLines = {}
-- Lines before insertion point
for i = 1, afterLine do
table.insert(resultLines, lines[i])
end
-- New content lines
for _, line in ipairs(newLines) do
table.insert(resultLines, line)
end
-- Lines after insertion point
for i = afterLine + 1, totalLines do
table.insert(resultLines, lines[i])
end
local newSource = joinLines(resultLines, hadTrailingNewline)
-- Use UpdateSourceAsync for editor compatibility
ScriptEditorService:UpdateSourceAsync(instance, function(oldContent)
return newSource
end)
ChangeHistoryService:SetWaypoint("Insert script lines after line " .. afterLine .. ": " .. instance.Name)
return {
success = true,
instancePath = instancePath,
insertedAfterLine = afterLine,
linesInserted = #newLines,
newLineCount = #resultLines,
message = "Script lines inserted successfully"
}
end)
if success then
return result
else
return { error = "Failed to insert script lines: " .. tostring(result) }
end
end
-- Partial Script Editing: Delete specific lines
handlers.deleteScriptLines = function(requestData)
local instancePath = requestData.instancePath
local startLine = requestData.startLine
local endLine = requestData.endLine
if not instancePath or not startLine or not endLine then
return { error = "Instance path, startLine, and endLine are required" }
end
local instance = getInstanceByPath(instancePath)
if not instance then
return { error = "Instance not found: " .. instancePath }
end
if not instance:IsA("LuaSourceContainer") then
return { error = "Instance is not a script-like object: " .. instance.ClassName }
end
local success, result = pcall(function()
local lines, hadTrailingNewline = splitLines(instance.Source)
local totalLines = #lines
if startLine < 1 or startLine > totalLines then
error("startLine out of range (1-" .. totalLines .. ")")
end
if endLine < startLine or endLine > totalLines then
error("endLine out of range (" .. startLine .. "-" .. totalLines .. ")")
end
-- Build new source without the deleted lines
local resultLines = {}
for i = 1, startLine - 1 do
table.insert(resultLines, lines[i])
end
for i = endLine + 1, totalLines do
table.insert(resultLines, lines[i])
end
local newSource = joinLines(resultLines, hadTrailingNewline)
-- Use UpdateSourceAsync for editor compatibility
ScriptEditorService:UpdateSourceAsync(instance, function(oldContent)
return newSource
end)
ChangeHistoryService:SetWaypoint("Delete script lines " .. startLine .. "-" .. endLine .. ": " .. instance.Name)
return {
success = true,
instancePath = instancePath,
deletedLines = { startLine = startLine, endLine = endLine },
linesDeleted = endLine - startLine + 1,
newLineCount = #resultLines,
message = "Script lines deleted successfully"
}
end)
if success then
return result
else
return { error = "Failed to delete script lines: " .. tostring(result) }
end
end
-- Attribute Tools: Get a single attribute
handlers.getAttribute = function(requestData)
local instancePath = requestData.instancePath
local attributeName = requestData.attributeName
if not instancePath or not attributeName then
return { error = "Instance path and attribute name are required" }
end
local instance = getInstanceByPath(instancePath)
if not instance then
return { error = "Instance not found: " .. instancePath }
end
local success, result = pcall(function()
local value = instance:GetAttribute(attributeName)
local valueType = typeof(value)
-- Serialize the value for JSON transport
local serializedValue = value
if valueType == "Vector3" then
serializedValue = { X = value.X, Y = value.Y, Z = value.Z, _type = "Vector3" }
elseif valueType == "Color3" then
serializedValue = { R = value.R, G = value.G, B = value.B, _type = "Color3" }
elseif valueType == "CFrame" then
serializedValue = { Position = { X = value.Position.X, Y = value.Position.Y, Z = value.Position.Z }, _type = "CFrame" }
elseif valueType == "UDim2" then
serializedValue = { X = { Scale = value.X.Scale, Offset = value.X.Offset }, Y = { Scale = value.Y.Scale, Offset = value.Y.Offset }, _type = "UDim2" }
elseif valueType == "BrickColor" then
serializedValue = { Name = value.Name, _type = "BrickColor" }
end
return {
instancePath = instancePath,
attributeName = attributeName,
value = serializedValue,
valueType = valueType,
exists = value ~= nil
}
end)
if success then
return result
else
return { error = "Failed to get attribute: " .. tostring(result) }
end
end
-- Attribute Tools: Set an attribute
handlers.setAttribute = function(requestData)
local instancePath = requestData.instancePath
local attributeName = requestData.attributeName
local attributeValue = requestData.attributeValue
local valueType = requestData.valueType -- Optional type hint
if not instancePath or not attributeName then
return { error = "Instance path and attribute name are required" }
end
local instance = getInstanceByPath(instancePath)
if not instance then
return { error = "Instance not found: " .. instancePath }
end
local success, result = pcall(function()
local value = attributeValue
-- Handle special type conversions
if type(attributeValue) == "table" then
if attributeValue._type == "Vector3" or valueType == "Vector3" then
value = Vector3.new(attributeValue.X or 0, attributeValue.Y or 0, attributeValue.Z or 0)
elseif attributeValue._type == "Color3" or valueType == "Color3" then
value = Color3.new(attributeValue.R or 0, attributeValue.G or 0, attributeValue.B or 0)
elseif attributeValue._type == "UDim2" or valueType == "UDim2" then
value = UDim2.new(
attributeValue.X and attributeValue.X.Scale or 0,
attributeValue.X and attributeValue.X.Offset or 0,
attributeValue.Y and attributeValue.Y.Scale or 0,
attributeValue.Y and attributeValue.Y.Offset or 0
)
elseif attributeValue._type == "BrickColor" or valueType == "BrickColor" then
value = BrickColor.new(attributeValue.Name or "Medium stone grey")
end
end
instance:SetAttribute(attributeName, value)
ChangeHistoryService:SetWaypoint("Set attribute " .. attributeName .. " on " .. instance.Name)
return {
success = true,
instancePath = instancePath,
attributeName = attributeName,
value = attributeValue,
message = "Attribute set successfully"
}
end)
if success then
return result
else
return { error = "Failed to set attribute: " .. tostring(result) }
end
end
-- Attribute Tools: Get all attributes
handlers.getAttributes = function(requestData)
local instancePath = requestData.instancePath
if not instancePath then
return { error = "Instance path is required" }
end
local instance = getInstanceByPath(instancePath)
if not instance then
return { error = "Instance not found: " .. instancePath }
end
local success, result = pcall(function()
local attributes = instance:GetAttributes()
local serializedAttributes = {}
for name, value in pairs(attributes) do
local valueType = typeof(value)
local serializedValue = value
if valueType == "Vector3" then
serializedValue = { X = value.X, Y = value.Y, Z = value.Z, _type = "Vector3" }
elseif valueType == "Color3" then
serializedValue = { R = value.R, G = value.G, B = value.B, _type = "Color3" }
elseif valueType == "CFrame" then
serializedValue = { Position = { X = value.Position.X, Y = value.Position.Y, Z = value.Position.Z }, _type = "CFrame" }
elseif valueType == "UDim2" then
serializedValue = { X = { Scale = value.X.Scale, Offset = value.X.Offset }, Y = { Scale = value.Y.Scale, Offset = value.Y.Offset }, _type = "UDim2" }
elseif valueType == "BrickColor" then
serializedValue = { Name = value.Name, _type = "BrickColor" }
end
serializedAttributes[name] = {
value = serializedValue,
type = valueType
}
end
local count = 0
for _ in pairs(serializedAttributes) do count = count + 1 end
return {
instancePath = instancePath,
attributes = serializedAttributes,
count = count
}
end)
if success then
return result
else
return { error = "Failed to get attributes: " .. tostring(result) }
end
end
-- Attribute Tools: Delete an attribute
handlers.deleteAttribute = function(requestData)
local instancePath = requestData.instancePath
local attributeName = requestData.attributeName
if not instancePath or not attributeName then
return { error = "Instance path and attribute name are required" }
end
local instance = getInstanceByPath(instancePath)
if not instance then
return { error = "Instance not found: " .. instancePath }
end
local success, result = pcall(function()
local existed = instance:GetAttribute(attributeName) ~= nil
instance:SetAttribute(attributeName, nil)
ChangeHistoryService:SetWaypoint("Delete attribute " .. attributeName .. " from " .. instance.Name)
return {
success = true,
instancePath = instancePath,
attributeName = attributeName,
existed = existed,
message = existed and "Attribute deleted successfully" or "Attribute did not exist"
}
end)
if success then
return result
else
return { error = "Failed to delete attribute: " .. tostring(result) }
end
end
-- Tag Tools: Get all tags on an instance
handlers.getTags = function(requestData)
local instancePath = requestData.instancePath
if not instancePath then
return { error = "Instance path is required" }
end
local instance = getInstanceByPath(instancePath)
if not instance then
return { error = "Instance not found: " .. instancePath }
end
local success, result = pcall(function()
local tags = CollectionService:GetTags(instance)
return {
instancePath = instancePath,
tags = tags,
count = #tags
}
end)
if success then
return result
else
return { error = "Failed to get tags: " .. tostring(result) }
end
end
-- Tag Tools: Add a tag to an instance
handlers.addTag = function(requestData)
local instancePath = requestData.instancePath
local tagName = requestData.tagName
if not instancePath or not tagName then
return { error = "Instance path and tag name are required" }
end
local instance = getInstanceByPath(instancePath)
if not instance then
return { error = "Instance not found: " .. instancePath }
end
local success, result = pcall(function()
local alreadyHad = CollectionService:HasTag(instance, tagName)
CollectionService:AddTag(instance, tagName)
ChangeHistoryService:SetWaypoint("Add tag " .. tagName .. " to " .. instance.Name)
return {
success = true,
instancePath = instancePath,
tagName = tagName,
alreadyHad = alreadyHad,
message = alreadyHad and "Instance already had this tag" or "Tag added successfully"
}
end)
if success then
return result
else
return { error = "Failed to add tag: " .. tostring(result) }
end
end
-- Tag Tools: Remove a tag from an instance
handlers.removeTag = function(requestData)
local instancePath = requestData.instancePath
local tagName = requestData.tagName
if not instancePath or not tagName then
return { error = "Instance path and tag name are required" }
end
local instance = getInstanceByPath(instancePath)
if not instance then
return { error = "Instance not found: " .. instancePath }
end
local success, result = pcall(function()
local hadTag = CollectionService:HasTag(instance, tagName)
CollectionService:RemoveTag(instance, tagName)
ChangeHistoryService:SetWaypoint("Remove tag " .. tagName .. " from " .. instance.Name)
return {
success = true,
instancePath = instancePath,
tagName = tagName,
hadTag = hadTag,
message = hadTag and "Tag removed successfully" or "Instance did not have this tag"
}
end)
if success then
return result
else
return { error = "Failed to remove tag: " .. tostring(result) }
end
end
-- Tag Tools: Get all instances with a specific tag
handlers.getTagged = function(requestData)
local tagName = requestData.tagName
if not tagName then
return { error = "Tag name is required" }
end
local success, result = pcall(function()
local taggedInstances = CollectionService:GetTagged(tagName)
local instances = {}
for _, instance in ipairs(taggedInstances) do
table.insert(instances, {
name = instance.Name,
className = instance.ClassName,
path = getInstancePath(instance)
})
end
return {
tagName = tagName,
instances = instances,
count = #instances
}
end)
if success then
return result
else
return { error = "Failed to get tagged instances: " .. tostring(result) }
end
end
-- Selection handlers
handlers.getSelection = function(requestData)
local selection = Selection:Get()
if #selection == 0 then
return {
success = true,
selection = {},
count = 0,
message = "No objects selected"
}
end
local selectedObjects = {}
for _, instance in ipairs(selection) do
table.insert(selectedObjects, {
name = instance.Name,
className = instance.ClassName,
path = getInstancePath(instance),
parent = instance.Parent and getInstancePath(instance.Parent) or nil
})
end
return {
success = true,
selection = selectedObjects,
count = #selection,
message = #selection .. " object(s) selected"
}
end
-- === NEW HANDLERS (v2.0.0) ===
handlers.reparentObject = function(requestData)
local instancePath = requestData.instancePath
local newParent = requestData.newParent
if not instancePath or not newParent then
return { error = "Instance path and new parent are required" }
end
local instance = getInstanceByPath(instancePath)
if not instance then return { error = "Instance not found: " .. instancePath } end
local parentInstance = getInstanceByPath(newParent)
if not parentInstance then return { error = "New parent not found: " .. newParent } end
local success, err = pcall(function()
instance.Parent = parentInstance
ChangeHistoryService:SetWaypoint("Reparent " .. instance.Name)
end)
if success then
return { success = true, instancePath = getInstancePath(instance), newParent = newParent, message = "Object reparented successfully" }
else
return { error = "Failed to reparent: " .. tostring(err) }
end
end
handlers.cloneObject = function(requestData)
local instancePath = requestData.instancePath
local newParent = requestData.newParent
local newName = requestData.newName
if not instancePath then return { error = "Instance path is required" } end
local instance = getInstanceByPath(instancePath)
if not instance then return { error = "Instance not found: " .. instancePath } end
local success, result = pcall(function()
local clone = instance:Clone()
if newName then clone.Name = newName end
if newParent then
local parentInst = getInstanceByPath(newParent)
if parentInst then clone.Parent = parentInst else clone.Parent = instance.Parent end
else
clone.Parent = instance.Parent
end
ChangeHistoryService:SetWaypoint("Clone " .. instance.Name)
return clone
end)
if success and result then
return { success = true, instancePath = getInstancePath(result), name = result.Name, className = result.ClassName, message = "Object cloned successfully" }
else
return { error = "Failed to clone: " .. tostring(result) }
end
end
handlers.getDescendants = function(requestData)
local instancePath = requestData.instancePath
local classFilter = requestData.classFilter
local nameFilter = requestData.nameFilter
local maxDepth = requestData.maxDepth
if not instancePath then return { error = "Instance path is required" } end
local instance = getInstanceByPath(instancePath)
if not instance then return { error = "Instance not found: " .. instancePath } end
local results = {}
local function traverse(inst, depth)
if maxDepth and depth > maxDepth then return end
for _, child in ipairs(inst:GetChildren()) do
local include = true
if classFilter and not child:IsA(classFilter) then include = false end
if nameFilter and not child.Name:lower():find(nameFilter:lower()) then include = false end
if include then
table.insert(results, { name = child.Name, className = child.ClassName, path = getInstancePath(child) })
end
traverse(child, depth + 1)
end
end
traverse(instance, 1)
return { instancePath = instancePath, descendants = results, count = #results }
end
handlers.batchOperations = function(requestData)
local operations = requestData.operations
if not operations or type(operations) ~= "table" or #operations == 0 then
return { error = "Operations array is required" }
end
local results = {}
local successCount = 0
local failureCount = 0
for i, op in ipairs(operations) do
local opType = op.type
local data = op.data or {}
local opResult
local opSuccess, opErr = pcall(function()
if opType == "setProperty" then
opResult = handlers.setProperty(data)
elseif opType == "createObject" then
opResult = handlers.createObject(data)
elseif opType == "deleteObject" then
opResult = handlers.deleteObject(data)
elseif opType == "reparent" then
opResult = handlers.reparentObject(data)
elseif opType == "clone" then
opResult = handlers.cloneObject(data)
else
opResult = { error = "Unknown operation type: " .. tostring(opType) }
end
end)
if opSuccess and opResult and not opResult.error then
successCount = successCount + 1
table.insert(results, { index = i, type = opType, success = true, result = opResult })
else
failureCount = failureCount + 1
local errMsg = (opResult and opResult.error) or tostring(opErr)
table.insert(results, { index = i, type = opType, success = false, error = errMsg })
end
end
if successCount > 0 then
ChangeHistoryService:SetWaypoint("Batch operations (" .. successCount .. " succeeded)")
end
return { results = results, summary = { total = #operations, succeeded = successCount, failed = failureCount } }
end
handlers.undo = function(requestData)
local success, err = pcall(function()
ChangeHistoryService:Undo()
end)
if success then
return { success = true, message = "Undo executed" }
else
return { error = "Undo failed: " .. tostring(err) }
end
end
handlers.redo = function(requestData)
local success, err = pcall(function()
ChangeHistoryService:Redo()
end)
if success then
return { success = true, message = "Redo executed" }
else
return { error = "Redo failed: " .. tostring(err) }
end
end
handlers.groupObjects = function(requestData)
local paths = requestData.paths
local groupType = requestData.groupType or "Model"
local groupName = requestData.groupName
if not paths or type(paths) ~= "table" or #paths == 0 then
return { error = "Paths array is required" }
end
local instances = {}
local commonParent = nil
for _, path in ipairs(paths) do
local inst = getInstanceByPath(path)
if inst then
table.insert(instances, inst)
if not commonParent then commonParent = inst.Parent end
end
end
if #instances == 0 then return { error = "No valid instances found" } end
local success, result = pcall(function()
local group = Instance.new(groupType)
group.Name = groupName or "Group"
group.Parent = commonParent
for _, inst in ipairs(instances) do
inst.Parent = group
end
ChangeHistoryService:SetWaypoint("Group " .. #instances .. " objects")
return group
end)
if success and result then
return { success = true, groupPath = getInstancePath(result), groupType = groupType, childCount = #instances, message = "Objects grouped successfully" }
else
return { error = "Failed to group: " .. tostring(result) }
end
end
handlers.ungroupObjects = function(requestData)
local instancePath = requestData.instancePath
if not instancePath then return { error = "Instance path is required" } end
local instance = getInstanceByPath(instancePath)
if not instance then return { error = "Instance not found: " .. instancePath } end
local success, result = pcall(function()
local parent = instance.Parent
local children = instance:GetChildren()
local movedCount = 0
for _, child in ipairs(children) do
child.Parent = parent
movedCount = movedCount + 1
end
instance:Destroy()
ChangeHistoryService:SetWaypoint("Ungroup " .. instancePath)
return movedCount
end)
if success then
return { success = true, childrenMoved = result, message = "Group dissolved, " .. result .. " children moved to parent" }
else
return { error = "Failed to ungroup: " .. tostring(result) }
end
end
handlers.getBoundingBox = function(requestData)
local instancePath = requestData.instancePath
if not instancePath then return { error = "Instance path is required" } end
local instance = getInstanceByPath(instancePath)
if not instance then return { error = "Instance not found: " .. instancePath } end
local success, result = pcall(function()
if instance:IsA("Model") then
local cf, size = instance:GetBoundingBox()
return {
center = { X = cf.Position.X, Y = cf.Position.Y, Z = cf.Position.Z },
size = { X = size.X, Y = size.Y, Z = size.Z },
orientation = { X = cf:ToEulerAnglesXYZ() },
instancePath = instancePath
}
elseif instance:IsA("BasePart") then
local pos = instance.Position
local size = instance.Size
return {
center = { X = pos.X, Y = pos.Y, Z = pos.Z },
size = { X = size.X, Y = size.Y, Z = size.Z },
instancePath = instancePath
}
else
error("Instance must be a Model or BasePart")
end
end)
if success then
return result
else
return { error = "Failed to get bounding box: " .. tostring(result) }
end
end
handlers.createWeld = function(requestData)
local part0Path = requestData.part0Path
local part1Path = requestData.part1Path
if not part0Path or not part1Path then return { error = "Both part paths are required" } end
local part0 = getInstanceByPath(part0Path)
local part1 = getInstanceByPath(part1Path)
if not part0 then return { error = "Part0 not found: " .. part0Path } end
if not part1 then return { error = "Part1 not found: " .. part1Path } end
if not part0:IsA("BasePart") then return { error = "Part0 is not a BasePart" } end
if not part1:IsA("BasePart") then return { error = "Part1 is not a BasePart" } end
local success, result = pcall(function()
local weld = Instance.new("WeldConstraint")
weld.Part0 = part0
weld.Part1 = part1
weld.Parent = part0
ChangeHistoryService:SetWaypoint("Create weld between " .. part0.Name .. " and " .. part1.Name)
return weld
end)
if success and result then
return { success = true, weldPath = getInstancePath(result), part0 = part0Path, part1 = part1Path, message = "WeldConstraint created successfully" }
else
return { error = "Failed to create weld: " .. tostring(result) }
end
end
handlers.raycast = function(requestData)
local origin = requestData.origin
local direction = requestData.direction
local filterPaths = requestData.filterPaths
local filterType = requestData.filterType or "Exclude"
if not origin or not direction then return { error = "Origin and direction are required" } end
local success, result = pcall(function()
local originVec = Vector3.new(origin.X or 0, origin.Y or 0, origin.Z or 0)
local dirVec = Vector3.new(direction.X or 0, direction.Y or 0, direction.Z or 0)
local params = RaycastParams.new()
if filterPaths and #filterPaths > 0 then
local filterInstances = {}
for _, path in ipairs(filterPaths) do
local inst = getInstanceByPath(path)
if inst then table.insert(filterInstances, inst) end
end
if filterType == "Include" then
params.FilterType = Enum.RaycastFilterType.Include
else
params.FilterType = Enum.RaycastFilterType.Exclude
end
params.FilterDescendantsInstances = filterInstances
end
local rayResult = workspace:Raycast(originVec, dirVec, params)
if rayResult then
return {
hit = true,
instance = { name = rayResult.Instance.Name, className = rayResult.Instance.ClassName, path = getInstancePath(rayResult.Instance) },
position = { X = rayResult.Position.X, Y = rayResult.Position.Y, Z = rayResult.Position.Z },
normal = { X = rayResult.Normal.X, Y = rayResult.Normal.Y, Z = rayResult.Normal.Z },
distance = (rayResult.Position - originVec).Magnitude,
material = tostring(rayResult.Material)
}
else
return { hit = false, message = "Ray did not hit anything" }
end
end)
if success then
return result
else
return { error = "Raycast failed: " .. tostring(result) }
end
end
handlers.fillTerrain = function(requestData)
local shape = requestData.shape
local center = requestData.center
local size = requestData.size
local material = requestData.material
if not shape or not center or not size or not material then
return { error = "Shape, center, size, and material are required" }
end
local success, err = pcall(function()
local centerVec = Vector3.new(center.X or 0, center.Y or 0, center.Z or 0)
local sizeVec = Vector3.new(size.X or 0, size.Y or 0, size.Z or 0)
local terrainMaterial = Enum.Material[material]
if not terrainMaterial then error("Invalid material: " .. material) end
local cf = CFrame.new(centerVec)
if shape == "Block" then
workspace.Terrain:FillBlock(cf, sizeVec, terrainMaterial)
elseif shape == "Ball" then
workspace.Terrain:FillBall(centerVec, sizeVec.X / 2, terrainMaterial)
elseif shape == "Cylinder" then
workspace.Terrain:FillCylinder(cf, sizeVec.Y, sizeVec.X / 2, terrainMaterial)
else
error("Invalid shape: " .. shape)
end
ChangeHistoryService:SetWaypoint("Fill terrain " .. shape .. " with " .. material)
end)
if success then
return { success = true, shape = shape, material = material, message = "Terrain filled successfully" }
else
return { error = "Failed to fill terrain: " .. tostring(err) }
end
end
handlers.clearTerrain = function(requestData)
local center = requestData.center
local size = requestData.size
if not center or not size then return { error = "Center and size are required" } end
local success, err = pcall(function()
local centerVec = Vector3.new(center.X or 0, center.Y or 0, center.Z or 0)
local sizeVec = Vector3.new(size.X or 0, size.Y or 0, size.Z or 0)
local halfSize = sizeVec / 2
local min = centerVec - halfSize
local max = centerVec + halfSize
local region = Region3.new(min, max):ExpandToGrid(4)
workspace.Terrain:FillRegion(region, 4, Enum.Material.Air)
ChangeHistoryService:SetWaypoint("Clear terrain region")
end)
if success then
return { success = true, message = "Terrain cleared successfully" }
else
return { error = "Failed to clear terrain: " .. tostring(err) }
end
end
handlers.executeLua = function(requestData)
local code = requestData.code
local args = requestData.args
if not code then return { error = "Code is required" } end
local success, result = pcall(function()
-- Wrap in a function to support "return" statements
local fn, compileErr = loadstring("return (function(...)\n" .. code .. "\nend)(...)")
if not fn then
-- Try without return wrapper (for code that already has return)
fn, compileErr = loadstring(code)
if not fn then
error("Compile error: " .. tostring(compileErr))
end
end
local callResult = fn(args)
return callResult
end)
if success then
-- Serialize the result for JSON
local serialized
if result == nil then
serialized = "nil"
elseif type(result) == "table" then
local ok, json = pcall(function() return HttpService:JSONEncode(result) end)
serialized = ok and json or tostring(result)
else
serialized = tostring(result)
end
return { success = true, result = serialized, resultType = type(result), message = "Lua executed successfully" }
else
return { error = "Lua execution failed: " .. tostring(result) }
end
end
handlers.setSelection = function(requestData)
local paths = requestData.paths
if not paths then return { error = "Paths array is required" } end
local instances = {}
for _, path in ipairs(paths) do
local inst = getInstanceByPath(path)
if inst then table.insert(instances, inst) end
end
local success, err = pcall(function()
Selection:Set(instances)
end)
if success then
return { success = true, selectedCount = #instances, message = #instances .. " object(s) selected" }
else
return { error = "Failed to set selection: " .. tostring(err) }
end
end
handlers.massReparent = function(requestData)
local paths = requestData.paths
local newParent = requestData.newParent
if not paths or type(paths) ~= "table" or #paths == 0 or not newParent then
return { error = "Paths array and new parent are required" }
end
local parentInstance = getInstanceByPath(newParent)
if not parentInstance then return { error = "New parent not found: " .. newParent } end
local results = {}
local successCount = 0
local failureCount = 0
for _, path in ipairs(paths) do
local inst = getInstanceByPath(path)
if inst then
local ok, err = pcall(function() inst.Parent = parentInstance end)
if ok then
successCount = successCount + 1
table.insert(results, { path = path, success = true, newPath = getInstancePath(inst) })
else
failureCount = failureCount + 1
table.insert(results, { path = path, success = false, error = tostring(err) })
end
else
failureCount = failureCount + 1
table.insert(results, { path = path, success = false, error = "Instance not found" })
end
end
if successCount > 0 then
ChangeHistoryService:SetWaypoint("Mass reparent " .. successCount .. " objects")
end
return { results = results, summary = { total = #paths, succeeded = successCount, failed = failureCount } }
end
local function updateUIState()
if pluginState.isActive then
statusLabel.Text = "Connecting..."
statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
statusText.Text = "CONNECTING"
if pluginState.consecutiveFailures == 0 then
detailStatusLabel.Text = "HTTP: ... MCP: ..."
else
detailStatusLabel.Text = "HTTP: X MCP: X"
end
detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
connectButton.Text = "Disconnect"
connectButton.TextColor3 = Color3.fromRGB(255, 255, 255)
startPulseAnimation()
-- Reset steps to connecting state
step1Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
step1Label.Text = "1. HTTP server reachable (connecting...)"
step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
step2Label.Text = "2. MCP bridge connected (connecting...)"
step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
step3Label.Text = "3. Ready for commands (connecting...)"
pluginState.mcpWaitStartTime = nil
troubleshootLabel.Visible = false
if not buttonHover then
connectButton.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
end
urlInput.TextEditable = false
urlInput.BackgroundColor3 = Color3.fromRGB(55, 65, 81)
urlInput.BorderColor3 = Color3.fromRGB(75, 85, 99)
else
statusLabel.Text = "Disconnected"
statusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
statusIndicator.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
statusPulse.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
statusText.Text = "OFFLINE"
detailStatusLabel.Text = "HTTP: X MCP: X"
detailStatusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
connectButton.Text = "Connect"
connectButton.TextColor3 = Color3.fromRGB(255, 255, 255)
stopPulseAnimation()
-- Reset steps to offline state
step1Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
step1Label.Text = "1. HTTP server reachable (offline)"
step2Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
step2Label.Text = "2. MCP bridge connected (offline)"
step3Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
step3Label.Text = "3. Ready for commands (offline)"
pluginState.mcpWaitStartTime = nil
troubleshootLabel.Visible = false
if not buttonHover then
connectButton.BackgroundColor3 = Color3.fromRGB(16, 185, 129)
end
urlInput.TextEditable = true
urlInput.BackgroundColor3 = Color3.fromRGB(55, 65, 81)
urlInput.BorderColor3 = Color3.fromRGB(99, 102, 241)
end
end
local function activatePlugin()
pluginState.serverUrl = urlInput.Text
pluginState.isActive = true
pluginState.consecutiveFailures = 0
pluginState.currentRetryDelay = 0.5
screenGui.Enabled = true
updateUIState()
addLog("CONNECTION", "Connecting to " .. pluginState.serverUrl)
pcall(function()
HttpService:RequestAsync({
Url = pluginState.serverUrl .. "/ready",
Method = "POST",
Headers = {
["Content-Type"] = "application/json",
},
Body = HttpService:JSONEncode({
pluginReady = true,
timestamp = tick(),
}),
})
end)
addLog("CONNECTION", "Sent /ready to server")
-- Start long-poll loop in a separate coroutine
if not pluginState.pollThread then
pluginState.pollThread = task.spawn(longPollLoop)
end
end
local function deactivatePlugin()
addLog("CONNECTION", "Disconnecting...")
pluginState.isActive = false
updateUIState()
-- Cancel the long-poll coroutine
if pluginState.pollThread then
pcall(function()
task.cancel(pluginState.pollThread)
end)
pluginState.pollThread = nil
end
pcall(function()
HttpService:RequestAsync({
Url = pluginState.serverUrl .. "/disconnect",
Method = "POST",
Headers = {
["Content-Type"] = "application/json",
},
Body = HttpService:JSONEncode({
timestamp = tick(),
}),
})
end)
pluginState.consecutiveFailures = 0
pluginState.currentRetryDelay = 0.5
addLog("CONNECTION", "Disconnected")
end
connectButton.Activated:Connect(function()
if pluginState.isActive then
deactivatePlugin()
else
activatePlugin()
end
end)
button.Click:Connect(function()
screenGui.Enabled = not screenGui.Enabled
end)
plugin.Unloading:Connect(function()
deactivatePlugin()
end)
updateUIState()