--[[ Roblox MCP Plugin v2.0 - HTTP Polling Edition Connects Roblox Studio to Claude Code via MCP server. Installation: 1. Copy this file to: %LOCALAPPDATA%\Roblox\Plugins\RobloxMCPPlugin.lua 2. Open Roblox Studio 3. Enable HttpService in: Game Settings > Security > Allow HTTP Requests = ON 4. The plugin will auto-connect to the MCP server 5. Click the toolbar button to connect/disconnect Requirements: - MCP server running: node C:\Users\Admin\roblox-mcp-server\src\index.js - HttpService enabled in Game Settings > Security --]] local Plugin = plugin or {} -- Configuration local CONFIG = { HOST = "localhost", PORT = 37423, POLL_INTERVAL = 0.5, -- seconds between polls RECONNECT_DELAY = 5, MAX_RECONNECT_ATTEMPTS = 20, } -- Build base URL local BASE_URL = "http://" .. CONFIG.HOST .. ":" .. CONFIG.PORT -- State local isConnected = false local isPolling = false local reconnectAttempts = 0 local lastCommandId = 0 local pollThread = nil local toolbar = nil local connectButton = nil -- Get HttpService and FORCE ENABLE it (plugins can do this in Studio) local HttpService = game:GetService("HttpService") HttpService.HttpEnabled = true -- Logging local function log(message, level) level = level or "INFO" print("[RobloxMCP] [" .. level .. "] " .. message) if level == "ERROR" then warn("[RobloxMCP] " .. message) end end -- Safe HTTP request wrapper local function httpRequest(method, path, body) local url = BASE_URL .. path local ok, result = pcall(function() local options = { Url = url, Method = method, Headers = { ["Content-Type"] = "application/json", }, } if body then options.Body = HttpService:JSONEncode(body) end local response = HttpService:RequestAsync(options) return response end) if not ok then return false, tostring(result) end if result.StatusCode ~= 200 then return false, "HTTP " .. result.StatusCode .. ": " .. result.Body end local decoded = HttpService:JSONDecode(result.Body) return true, decoded end -- Get an object by dot-separated path local function getObjectFromPath(path) if not path or path == "" then return nil end if path == "game" or path == "Game" then return game end if path == "Workspace" or path == "workspace" then return workspace end -- Handle special service names local serviceMap = { ["ReplicatedStorage"] = game:GetService("ReplicatedStorage"), ["ServerStorage"] = game:GetService("ServerStorage"), ["ServerScriptService"] = game:GetService("ServerScriptService"), ["StarterGui"] = game:GetService("StarterGui"), ["StarterPack"] = game:GetService("StarterPack"), ["StarterPlayer"] = game:GetService("StarterPlayer"), ["Lighting"] = game:GetService("Lighting"), ["Players"] = game:GetService("Players"), ["Workspace"] = workspace, } local parts = {} for part in string.gmatch(path, "[^%.]+") do table.insert(parts, part) end if #parts == 0 then return nil end -- Start from service or game local obj = serviceMap[parts[1]] or game for i = (serviceMap[parts[1]] and 2 or 1), #parts do local part = parts[i] local child = obj:FindFirstChild(part) if not child then log("Could not find '" .. part .. "' in path: " .. path, "ERROR") return nil end obj = child end return obj end -- Create object at path local function createObjectAt(path, className, properties) local parentPath = string.match(path, "^(.-)%.[^%.]+$") or "Workspace" local objectName = string.match(path, "%.([^%.]+)$") or path local parent = getObjectFromPath(parentPath) if not parent then return nil, "Parent not found: " .. parentPath end local obj = Instance.new(className) obj.Name = objectName if properties then for propName, propValue in pairs(properties) do pcall(function() obj[propName] = propValue end) end end obj.Parent = parent return obj end -- Handle a single command from MCP server local function handleCommand(cmd) local command = cmd.command local params = cmd.params or {} log("Executing: " .. command) local success, result = pcall(function() if command == "createScript" then local scriptObj = createObjectAt(params.path, params.scriptType or "Script", { Name = params.scriptName, }) if scriptObj then pcall(function() scriptObj.Source = params.source end) return { success = true, objectPath = params.path } else return { success = false, error = "Failed to create script" } end elseif command == "createPart" then local props = { Name = params.partName, Anchored = params.anchored ~= false } local shapeEnum = Enum.PartType.Block if params.partType == "Ball" then shapeEnum = Enum.PartType.Ball elseif params.partType == "Cylinder" then shapeEnum = Enum.PartType.Cylinder elseif params.partType == "Wedge" then shapeEnum = Enum.PartType.Wedge elseif params.partType == "CornerWedge" then shapeEnum = Enum.PartType.CornerWedge end props.Shape = shapeEnum if params.position then props.Position = Vector3.new(params.position.x or 0, params.position.y or 0, params.position.z or 0) end if params.size then props.Size = Vector3.new(params.size.x or 4, params.size.y or 4, params.size.z or 4) end if params.color then pcall(function() props.Color = BrickColor.new(params.color).Color end) end local part = createObjectAt(params.parentPath or "Workspace", "Part", props) return { success = part ~= nil, objectPath = (params.parentPath or "Workspace") .. "." .. params.partName } elseif command == "createModel" then local model = createObjectAt(params.parentPath or "Workspace", "Model", { Name = params.modelName }) return { success = model ~= nil, objectPath = (params.parentPath or "Workspace") .. "." .. params.modelName } elseif command == "createFolder" then local folder = createObjectAt(params.parentPath or "Workspace", "Folder", { Name = params.folderName }) return { success = folder ~= nil, objectPath = (params.parentPath or "Workspace") .. "." .. params.folderName } elseif command == "createGUI" then local properties = params.properties or {} properties.Name = params.name if params.guiType == "ScreenGui" then properties.ResetOnSpawn = false end if params.guiType == "Frame" or params.guiType == "TextLabel" or params.guiType == "TextButton" then if not properties.Size then properties.Size = UDim2.new(0, 200, 0, 50) end end if params.guiType == "TextLabel" or params.guiType == "TextButton" then if not properties.Text then properties.Text = params.name end end local gui = createObjectAt(params.parentPath or "StarterGui", params.guiType, properties) return { success = gui ~= nil, objectPath = (params.parentPath or "StarterGui") .. "." .. params.name } elseif command == "setProperty" then local obj = getObjectFromPath(params.path) if not obj then return { success = false, error = "Object not found: " .. params.path } end local value = params.value pcall(function() if params.property == "Position" or params.property == "Size" then value = Vector3.new(value.x, value.y, value.z) elseif params.property == "CFrame" then if value.components then value = CFrame.new(unpack(value.components)) end elseif params.property == "Color" then value = BrickColor.new(value).Color end obj[params.property] = value end) return { success = true, property = params.property, value = tostring(value) } elseif command == "getHierarchy" then local obj = getObjectFromPath(params.path or "Workspace") if not obj then return { success = false, error = "Object not found" } end local function buildTree(object, depth, currentDepth) if currentDepth > depth then return nil end local children = {} for _, child in ipairs(object:GetChildren()) do local childData = { name = child.Name, className = child.ClassName } if currentDepth < depth then childData.children = buildTree(child, depth, currentDepth + 1) end table.insert(children, childData) end return children end return { success = true, path = params.path or "Workspace", children = buildTree(obj, params.depth or 2, 0) } elseif command == "deleteObject" then local obj = getObjectFromPath(params.path) if not obj then return { success = false, error = "Object not found: " .. params.path } end obj:Destroy() return { success = true, deletedPath = params.path } elseif command == "play" then pcall(function() game:Load("PlaySolo") end) return { success = true, mode = params.mode or "Both" } elseif command == "stop" then pcall(function() game:Load("Stop") end) return { success = true } elseif command == "savePlace" then local ok = pcall(function() game:SavePlace() end) return { success = ok } elseif command == "executeCode" then local fn, err = loadstring(params.code) if not fn then return { success = false, error = err } end local ok, execErr = pcall(fn) return { success = ok, error = not ok and tostring(execErr) or nil } else return { success = false, error = "Unknown command: " .. tostring(command) } end end) -- Build response local response = { id = cmd.id, result = success and result or { success = false, error = tostring(result) }, } if not success then response.error = tostring(result) end return response end -- Poll the MCP server for new commands local function pollOnce() local ok, data = httpRequest("GET", "/poll?last=" .. tostring(lastCommandId)) if not ok then return false, data end if data.commands and #data.commands > 0 then for _, cmd in ipairs(data.commands) do log("Received command: " .. (cmd.command or "unknown")) -- Execute command local response = handleCommand(cmd) -- Send result back to MCP server local sendOk, sendErr = httpRequest("POST", "/result", { id = cmd.id, result = response.result, }) if not sendOk then log("Failed to send result: " .. tostring(sendErr), "ERROR") else log("Command completed: " .. (cmd.command or "unknown")) end lastCommandId = math.max(lastCommandId, cmd.id) end -- Update lastCommandId from server if data.lastId then lastCommandId = math.max(lastCommandId, data.lastId) end end return true end -- Main polling loop local function startPolling() if isPolling then return end isPolling = true log("Starting poll loop...") pollThread = spawn(function() while isConnected and isPolling do local ok, err = pollOnce() if not ok then log("Poll error: " .. tostring(err), "ERROR") -- Don't disconnect on single poll failure, just wait end wait(CONFIG.POLL_INTERVAL) end log("Poll loop stopped") end) end -- Connect to MCP server local function connect() if isConnected then return end log("Connecting to MCP server at " .. BASE_URL .. " ...") -- Health check first local ok, data = httpRequest("GET", "/health") if not ok then log("MCP server not reachable: " .. tostring(data), "ERROR") log("Make sure the MCP server is running: node C:\\Users\\Admin\\roblox-mcp-server\\src\\index.js", "ERROR") return false end isConnected = true reconnectAttempts = 0 lastCommandId = 0 log("Connected to MCP server!") startPolling() return true end -- Disconnect from MCP server local function disconnect() isConnected = false isPolling = false if pollThread then pollThread = nil end log("Disconnected from MCP server") end -- Create toolbar button local function createUI() if not Plugin or not Plugin:FindFirstChildWhichIsA("Toolbar") then toolbar = Plugin:CreateToolbar("RobloxMCP") end connectButton = toolbar:CreateButton( "MCP Connect", "Connect/Disconnect from Claude Code MCP server", "rbxassetid://16706090882" ) connectButton.Click:Connect(function() if isConnected then disconnect() connectButton.Icon = "rbxassetid://16706090882" else local success = connect() if success then connectButton.Icon = "rbxassetid://16706100672" end end end) end -- Initialize local function init() log("Roblox MCP Plugin v2.0 (HTTP Polling) loaded") log("Make sure HttpService is enabled: Game Settings > Security > Allow HTTP Requests") createUI() -- Auto-connect attempt spawn(function() wait(2) -- Wait for Studio to fully load connect() if isConnected and connectButton then connectButton.Icon = "rbxassetid://16706100672" end end) end -- Cleanup on unload if Plugin then Plugin.Unloading:Connect(function() disconnect() end) end init()