The Studio Plugin is the critical component that runs inside Roblox Studio, providing direct access to Studio APIs that external applications cannot reach. Written in Luau, it implements long-polling, request processing, and comprehensive activity logging.
The plugin is the only component with direct access to Roblox Studio APIs like game, workspace, Selection, HttpService, etc.
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 -- Success path pluginState.consecutiveFailures = 0 pluginState.currentRetryDelay = 0.5 pluginState.lastSuccessfulConnection = tick() local data = HttpService:JSONDecode(result.Body) local mcpConnected = data.mcpConnected == true -- Update connection status pluginState.lastHttpOk = true updateStatusIndicators(true, mcpConnected) if data.request and mcpConnected then -- Process the request addLog("REQUEST", "Received: " .. data.request.endpoint) local reqStartTime = tick() local response = processRequest(data.request) sendResponse(data.requestId, response) local reqMs = math.floor((tick() - reqStartTime) * 1000) addLog("RESPONSE", data.request.endpoint .. " completed in " .. reqMs .. "ms") end -- If no request, immediately re-poll (long-poll timeout) elseif pluginState.isActive then -- Failure path pluginState.consecutiveFailures += 1 if pluginState.consecutiveFailures > 1 then pluginState.currentRetryDelay = math.min( pluginState.currentRetryDelay * pluginState.retryBackoffMultiplier, pluginState.maxRetryDelay ) end updateStatusIndicators(false, false) -- Exponential backoff task.wait(pluginState.currentRetryDelay) end endend
Key Features:
Infinite loop while active (no sleep between successful polls)
Immediate re-poll after receiving response (enables instant subsequent requests)
Error handling with pcall prevents crashes
Connection status tracking with multiple indicators
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
The plugin handles JSON to Roblox type conversion:
local function convertPropertyValue(instance, propertyName, propertyValue) -- Vector3 from object notation {X: n, Y: n, Z: n} 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 -- Color3 from object notation {R: n, G: n, B: n} 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 -- Enum values (strings like "Ball", "Cylinder") 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 -- BrickColor if propertyName == "BrickColor" then return BrickColor.new(propertyValue) end end -- Boolean strings if propertyValue == "true" then return true end if propertyValue == "false" then return false end -- Return as-is for primitives return propertyValueend
Button: Red XAction: Clears all log entries and resets counter
Copy
Button: Gray CAction: Prints all logs to Output window for copy/paste
Export
Button: Gray EAction: Creates StringValue in ServerStorage with full log
Copy to Output:
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 ===") addLog("CONNECTION", "Log copied to Output (" .. #logState.entries .. " entries)")end)
Export to 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") -- Remove existing local existing = ss:FindFirstChild("MCPActivityLog") if existing then existing:Destroy() end -- Create new StringValue local sv = Instance.new("StringValue") sv.Name = "MCPActivityLog" sv.Value = text sv.Parent = ss addLog("CONNECTION", "Log exported to ServerStorage.MCPActivityLog")end)
-- If HTTP OK but MCP not responding for >8 secondsif pluginState.lastHttpOk and not mcpConnected then local elapsed = tick() - (pluginState.mcpWaitStartTime or tick()) if elapsed > 8 then troubleshootLabel.Visible = true troubleshootLabel.Text = "HTTP is OK but MCP isn't responding. Close all node.exe in Task Manager and restart the server." endend
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, endsWithNewlineendlocal function joinLines(lines, hadTrailingNewline) local source = table.concat(lines, "\n") if hadTrailingNewline and source:sub(-1) ~= "\n" then source ..= "\n" end return sourceend
handlers.editScriptLines = function(requestData) local instancePath = requestData.instancePath local startLine = requestData.startLine local endLine = requestData.endLine local newContent = requestData.newContent local instance = getInstanceByPath(instancePath) if not instance or not instance:IsA("LuaSourceContainer") then return { error = "Script not found" } end local lines, hadTrailingNewline = splitLines(instance.Source) -- Delete lines from startLine to endLine for i = endLine, startLine, -1 do if i >= 1 and i <= #lines then table.remove(lines, i) end end -- Insert new content at startLine local newLines = splitLines(newContent) for i = #newLines, 1, -1 do table.insert(lines, startLine, newLines[i]) end instance.Source = joinLines(lines, hadTrailingNewline) return { success = true, linesAffected = (endLine - startLine + 1), newLineCount = #newLines }end