plugin.luau•63 kB
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 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,
pollInterval = 0.5,
lastPoll = 0,
consecutiveFailures = 0,
maxFailuresBeforeError = 50,
lastSuccessfulConnection = 0,
currentRetryDelay = 0.5,
maxRetryDelay = 5,
retryBackoffMultiplier = 1.2,
}
local screenGui = plugin:CreateDockWidgetPluginGui(
"MCPServerInterface",
DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, false, 400, 500, 350, 450)
)
screenGui.Title = "MCP Server v1.5.1"
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 • v1.5.1"
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, 243)
contentFrame.AutomaticCanvasSize = Enum.AutomaticSize.None
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, 85)
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
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
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
local processRequest
local sendResponse
local handlers = {}
local function pollForRequests()
if not pluginState.isActive then
return
end
local success, result = pcall(function()
return HttpService:RequestAsync({
Url = pluginState.serverUrl .. "/poll",
Method = "GET",
Headers = {
["Content-Type"] = "application/json",
},
})
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
if mcpConnected and not statusLabel.Text:find("Connected") then
statusLabel.Text = "Connected"
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"
detailStatusLabel.TextColor3 = Color3.fromRGB(34, 197, 94)
stopPulseAnimation()
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)
startPulseAnimation()
end
if data.request and mcpConnected then
local response = processRequest(data.request)
sendResponse(data.requestId, response)
end
elseif pluginState.isActive then
pluginState.consecutiveFailures = pluginState.consecutiveFailures + 1
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)
stopPulseAnimation()
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)
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)
startPulseAnimation()
end
end
end
sendResponse = function(requestId, responseData)
pcall(function()
HttpService:RequestAsync({
Url = pluginState.serverUrl .. "/response",
Method = "POST",
Headers = {
["Content-Type"] = "application/json",
},
Body = HttpService:JSONEncode({
requestId = requestId,
response = responseData,
}),
})
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)
else
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
if instance:IsA("BasePart") then
properties.Shape = tostring(instance.Shape)
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()
if propertyName == "Parent" then
local parentInstance = getInstanceByPath(propertyValue)
if parentInstance then
instance.Parent = parentInstance
else
return { error = "Parent instance not found: " .. propertyValue }
end
elseif propertyName == "Name" then
instance.Name = tostring(propertyValue)
elseif propertyName == "Source" and instance:IsA("LuaSourceContainer") then
instance.Source = tostring(propertyValue)
elseif propertyName == "Enabled" and instance:IsA("BaseScript") then
instance.Enabled = propertyValue == true or propertyValue == "true"
elseif propertyName == "Text" and (instance:IsA("TextLabel") or instance:IsA("TextButton") or instance:IsA("TextBox")) then
instance.Text = tostring(propertyValue)
elseif propertyName == "Visible" and instance:IsA("GuiObject") then
instance.Visible = propertyValue == true or propertyValue == "true"
elseif propertyName == "Anchored" and instance:IsA("BasePart") then
instance.Anchored = propertyValue == true or propertyValue == "true"
elseif propertyName == "CanCollide" and instance:IsA("BasePart") then
instance.CanCollide = propertyValue == true or propertyValue == "true"
elseif propertyName == "Transparency" and instance:IsA("BasePart") then
instance.Transparency = tonumber(propertyValue) or 0
elseif propertyName == "BrickColor" and instance:IsA("BasePart") then
instance.BrickColor = BrickColor.new(tostring(propertyValue))
elseif propertyName == "Material" and instance:IsA("BasePart") then
instance.Material = Enum.Material[tostring(propertyValue)] or instance.Material
elseif propertyName == "Size" and instance:IsA("BasePart") then
local parts = string.split(tostring(propertyValue), ",")
if #parts == 3 then
instance.Size = Vector3.new(tonumber(parts[1]) or 1, tonumber(parts[2]) or 1, tonumber(parts[3]) or 1)
end
elseif propertyName == "Position" and instance:IsA("BasePart") then
local parts = string.split(tostring(propertyValue), ",")
if #parts == 3 then
instance.Position = Vector3.new(tonumber(parts[1]) or 0, tonumber(parts[2]) or 0, tonumber(parts[3]) or 0)
end
else
instance[propertyName] = propertyValue
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()
instance[propertyName] = propertyValue
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
for propertyName, propertyValue in pairs(properties) do
pcall(function()
instance[propertyName] = propertyValue
end)
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 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
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 resp = {
instancePath = instancePath,
className = instance.ClassName,
name = instance.Name,
source = instance.Source,
sourceLength = string.len(instance.Source),
lineCount = select(2, string.gsub(instance.Source, '\n', '\n')) + 1
}
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
local success, result = pcall(function()
local oldSource = instance.Source
instance.Source = newSource
ChangeHistoryService:SetWaypoint("Set script source: " .. instance.Name)
return {
success = true,
instancePath = instancePath,
oldSourceLength = string.len(oldSource),
newSourceLength = string.len(newSource),
message = "Script source updated successfully"
}
end)
if success then
return result
else
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 = newSource
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. Direct assignment failed: " .. tostring(result) ..
". Replace method failed: " .. tostring(replaceResult)
}
end
end
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()
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()
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()
pcall(function()
HttpService:RequestAsync({
Url = pluginState.serverUrl .. "/ready",
Method = "POST",
Headers = {
["Content-Type"] = "application/json",
},
Body = HttpService:JSONEncode({
pluginReady = true,
timestamp = tick(),
}),
})
end)
if not pluginState.connection then
pluginState.connection = RunService.Heartbeat:Connect(function()
local now = tick()
local currentInterval = pluginState.consecutiveFailures > 5 and pluginState.currentRetryDelay
or pluginState.pollInterval
if now - pluginState.lastPoll > currentInterval then
pluginState.lastPoll = now
pollForRequests()
end
end)
end
end
local function deactivatePlugin()
pluginState.isActive = false
updateUIState()
pcall(function()
HttpService:RequestAsync({
Url = pluginState.serverUrl .. "/disconnect",
Method = "POST",
Headers = {
["Content-Type"] = "application/json",
},
Body = HttpService:JSONEncode({
timestamp = tick(),
}),
})
end)
if pluginState.connection then
pluginState.connection:Disconnect()
pluginState.connection = nil
end
pluginState.consecutiveFailures = 0
pluginState.currentRetryDelay = 0.5
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()