- MCP server with 12 tools for Roblox manipulation - WebSocket communication with Roblox Studio - Create scripts, parts, models, GUIs - Execute Lua code, control playtest - Full documentation and examples 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
452 lines
10 KiB
Lua
452 lines
10 KiB
Lua
--[[
|
|
Roblox MCP Server Script
|
|
This script runs inside Roblox Studio and acts as an HTTP polling server
|
|
to communicate with the MCP bridge application.
|
|
|
|
INSTRUCTIONS:
|
|
1. Start this script by pressing Play in Roblox Studio
|
|
2. The script will start an HTTP server on the configured port
|
|
3. The MCP bridge will send commands to this server
|
|
4. Commands are executed and results are returned
|
|
|
|
Alternative: Copy this to ServerScriptService for auto-start
|
|
--]]
|
|
|
|
local HttpService = game:GetService("HttpService")
|
|
local RunService = game:GetService("RunService")
|
|
|
|
-- Configuration
|
|
local CONFIG = {
|
|
-- HTTP server configuration
|
|
PORT = 37425,
|
|
-- How often to check for new commands (seconds)
|
|
POLL_INTERVAL = 0.1,
|
|
-- Enable debug logging
|
|
DEBUG = true,
|
|
}
|
|
|
|
-- State
|
|
local isRunning = false
|
|
local commandQueue = {}
|
|
local responseStore = {}
|
|
|
|
-- Logging
|
|
local function log(message, level)
|
|
level = level or "info"
|
|
if CONFIG.DEBUG then
|
|
print(string.format("[RobloxMCP:%s] %s", level:upper(), message))
|
|
end
|
|
end
|
|
|
|
-- Get object by path
|
|
local function getObjectFromPath(path)
|
|
if not path or path == "" then
|
|
return nil
|
|
end
|
|
|
|
-- Handle special paths
|
|
if path == "game" or path == "Game" then
|
|
return game
|
|
end
|
|
|
|
-- Handle Workspace specially
|
|
if path == "Workspace" or path == "workspace" then
|
|
return workspace
|
|
end
|
|
|
|
-- Split path by dot and traverse
|
|
local parts = {}
|
|
for part in string.gmatch(path, "[^%.]+") do
|
|
table.insert(parts, part)
|
|
end
|
|
|
|
if #parts == 0 then
|
|
return nil
|
|
end
|
|
|
|
local obj = game
|
|
|
|
for _, part in ipairs(parts) do
|
|
if part == "Workspace" or part == "workspace" then
|
|
obj = workspace
|
|
elseif typeof(obj) == "Instance" and obj:FindFirstChild(part) then
|
|
obj = obj[part]
|
|
else
|
|
log("Could not find: " .. part .. " in " .. path, "error")
|
|
return nil
|
|
end
|
|
end
|
|
|
|
return obj
|
|
end
|
|
|
|
-- Create object at path
|
|
local function createObjectAt(path, className, properties)
|
|
local parentPath, objectName
|
|
|
|
-- Extract parent path and object name
|
|
local lastDot = string.find(path, "%.[^%.]+$")
|
|
if lastDot then
|
|
parentPath = string.sub(path, 1, lastDot - 1)
|
|
objectName = string.sub(path, lastDot + 1)
|
|
else
|
|
parentPath = "game"
|
|
objectName = path
|
|
end
|
|
|
|
local parent = getObjectFromPath(parentPath)
|
|
if not parent then
|
|
return nil, "Parent not found: " .. tostring(parentPath)
|
|
end
|
|
|
|
-- Create the object
|
|
local obj = Instance.new(className)
|
|
obj.Name = objectName
|
|
|
|
-- Set properties
|
|
if properties then
|
|
for propName, propValue in pairs(properties) do
|
|
local ok = pcall(function()
|
|
obj[propName] = propValue
|
|
end)
|
|
if not ok then
|
|
log("Failed to set property " .. propName, "warn")
|
|
end
|
|
end
|
|
end
|
|
|
|
obj.Parent = parent
|
|
return obj
|
|
end
|
|
|
|
-- Command handlers
|
|
local handlers = {}
|
|
|
|
handlers.createScript = function(params)
|
|
local obj = createObjectAt(params.path .. "." .. params.scriptName, params.scriptType or "Script", {
|
|
Name = params.scriptName,
|
|
})
|
|
|
|
if obj then
|
|
obj.Source = params.source
|
|
return {
|
|
success = true,
|
|
objectPath = params.path .. "." .. params.scriptName,
|
|
}
|
|
end
|
|
|
|
return { success = false, error = "Failed to create script" }
|
|
end
|
|
|
|
handlers.createPart = function(params)
|
|
local properties = {
|
|
Name = params.partName,
|
|
Anchored = params.anchored ~= false,
|
|
}
|
|
|
|
-- Set shape
|
|
local shapeMap = {
|
|
Ball = Enum.PartType.Ball,
|
|
Block = Enum.PartType.Block,
|
|
Cylinder = Enum.PartType.Cylinder,
|
|
Wedge = Enum.PartType.Wedge,
|
|
CornerWedge = Enum.PartType.CornerWedge,
|
|
}
|
|
properties.Shape = shapeMap[params.partType] or Enum.PartType.Block
|
|
|
|
-- Set position
|
|
if params.position then
|
|
properties.Position = Vector3.new(
|
|
params.position.x or 0,
|
|
params.position.y or 0,
|
|
params.position.z or 0
|
|
)
|
|
end
|
|
|
|
-- Set size
|
|
if params.size then
|
|
properties.Size = Vector3.new(
|
|
params.size.x or 4,
|
|
params.size.y or 1,
|
|
params.size.z or 2
|
|
)
|
|
end
|
|
|
|
-- Set color
|
|
if params.color then
|
|
local ok = pcall(function()
|
|
properties.BrickColor = BrickColor.new(params.color)
|
|
end)
|
|
if not ok and type(params.color) == "table" then
|
|
properties.Color3 = Color3.new(
|
|
params.color.r or 1,
|
|
params.color.g or 1,
|
|
params.color.b or 1
|
|
)
|
|
end
|
|
end
|
|
|
|
local part = createObjectAt(
|
|
(paramss.parentPath or "Workspace") .. "." .. params.partName,
|
|
"Part",
|
|
properties
|
|
)
|
|
|
|
return {
|
|
success = part ~= nil,
|
|
objectPath = (params.parentPath or "Workspace") .. "." .. params.partName,
|
|
}
|
|
end
|
|
|
|
handlers.createModel = function(params)
|
|
local model = createObjectAt(
|
|
(params.parentPath or "Workspace") .. "." .. params.modelName,
|
|
"Model",
|
|
{ Name = params.modelName }
|
|
)
|
|
|
|
return {
|
|
success = model ~= nil,
|
|
objectPath = (params.parentPath or "Workspace") .. "." .. params.modelName,
|
|
}
|
|
end
|
|
|
|
handlers.createFolder = function(params)
|
|
local folder = createObjectAt(
|
|
(params.parentPath or "Workspace") .. "." .. params.folderName,
|
|
"Folder",
|
|
{ Name = params.folderName }
|
|
)
|
|
|
|
return {
|
|
success = folder ~= nil,
|
|
objectPath = (params.parentPath or "Workspace") .. "." .. params.folderName,
|
|
}
|
|
end
|
|
|
|
handlers.createGUI = function(params)
|
|
local properties = params.properties or {}
|
|
properties.Name = params.name
|
|
|
|
if params.guiType == "ScreenGui" then
|
|
properties.ResetOnSpawn = false
|
|
elseif params.guiType == "Frame" or params.guiType == "TextLabel" or params.guiType == "TextButton" then
|
|
properties.Size = properties.Size or UDim2.new(0, 200, 0, 50)
|
|
properties.Position = properties.Position or UDim2.new(0, 0, 0, 0)
|
|
end
|
|
|
|
if params.guiType == "TextLabel" or params.guiType == "TextButton" then
|
|
properties.Text = properties.Text or params.name
|
|
properties.TextScaled = properties.TextScaled ~= false
|
|
end
|
|
|
|
local gui = createObjectAt(
|
|
(params.parentPath or "StarterGui") .. "." .. params.name,
|
|
params.guiType,
|
|
properties
|
|
)
|
|
|
|
return {
|
|
success = gui ~= nil,
|
|
objectPath = (params.parentPath or "StarterGui") .. "." .. params.name,
|
|
}
|
|
end
|
|
|
|
handlers.setProperty = function(params)
|
|
local obj = getObjectFromPath(params.path)
|
|
if not obj then
|
|
return { success = false, error = "Object not found: " .. params.path }
|
|
end
|
|
|
|
local value = params.value
|
|
|
|
-- Convert to proper types
|
|
if params.property == "Position" or params.property == "Size" then
|
|
value = Vector3.new(value.x, value.y, value.z)
|
|
elseif params.property == "Color3" then
|
|
value = Color3.new(value.r, value.g, value.b)
|
|
elseif params.property == "BrickColor" then
|
|
value = BrickColor.new(value)
|
|
elseif params.property == "CFrame" and value.components then
|
|
value = CFrame.new(unpack(value.components))
|
|
end
|
|
|
|
local ok = pcall(function()
|
|
obj[params.property] = value
|
|
end)
|
|
|
|
return {
|
|
success = ok,
|
|
property = params.property,
|
|
value = tostring(value),
|
|
}
|
|
end
|
|
|
|
handlers.getHierarchy = function(params)
|
|
local obj = getObjectFromPath(params.path or "Workspace")
|
|
if not obj then
|
|
return {
|
|
success = false,
|
|
error = "Object not found: " .. (params.path or "Workspace"),
|
|
}
|
|
end
|
|
|
|
local function buildHierarchy(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 = buildHierarchy(child, depth, currentDepth + 1)
|
|
end
|
|
|
|
table.insert(children, childData)
|
|
end
|
|
|
|
return children
|
|
end
|
|
|
|
return {
|
|
success = true,
|
|
path = params.path or "Workspace",
|
|
children = buildHierarchy(obj, params.depth or 2, 0),
|
|
}
|
|
end
|
|
|
|
handlers.deleteObject = function(params)
|
|
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 }
|
|
end
|
|
|
|
handlers.executeCode = function(params)
|
|
local fn, err = loadstring(params.code)
|
|
if not fn then
|
|
return { success = false, error = err }
|
|
end
|
|
|
|
local ok, result = pcall(fn)
|
|
return { success = ok, result = tostring(result), context = params.context or "Plugin" }
|
|
end
|
|
|
|
-- Process a command
|
|
local function processCommand(id, command, params)
|
|
log("Processing command: " .. command, "info")
|
|
|
|
local handler = handlers[command]
|
|
if not handler then
|
|
return {
|
|
id = id,
|
|
success = false,
|
|
error = "Unknown command: " .. tostring(command),
|
|
}
|
|
end
|
|
|
|
local ok, result = pcall(function()
|
|
return handler(params)
|
|
end)
|
|
|
|
if not ok then
|
|
return {
|
|
id = id,
|
|
success = false,
|
|
error = tostring(result),
|
|
}
|
|
end
|
|
|
|
result.id = id
|
|
return result
|
|
end
|
|
|
|
-- HTTP polling endpoint (simulated via HttpService)
|
|
-- Note: This requires HTTP requests to be enabled in Game Settings
|
|
local function checkForCommands()
|
|
-- In a real implementation, this would poll the MCP bridge server
|
|
-- For now, commands can be queued via a shared object or ModuleScript
|
|
|
|
for i, cmd in ipairs(commandQueue) do
|
|
local response = processCommand(cmd.id, cmd.command, cmd.params)
|
|
responseStore[cmd.id] = response
|
|
table.remove(commandQueue, i)
|
|
end
|
|
end
|
|
|
|
-- Main loop
|
|
local function start()
|
|
if isRunning then
|
|
log("Already running", "warn")
|
|
return
|
|
end
|
|
|
|
isRunning = true
|
|
log("Starting Roblox MCP Server on port " .. CONFIG.PORT, "info")
|
|
|
|
-- Create a debug GUI to show status
|
|
local screenGui = Instance.new("ScreenGui")
|
|
screenGui.Name = "RobloxMCPServer"
|
|
screenGui.Parent = game:GetService("CoreGui")
|
|
|
|
local frame = Instance.new("Frame")
|
|
frame.Name = "Status"
|
|
frame.Size = UDim2.new(0, 250, 0, 100)
|
|
frame.Position = UDim2.new(1, -260, 0, 10)
|
|
frame.BackgroundColor3 = Color3.new(0.1, 0.1, 0.1)
|
|
frame.Parent = screenGui
|
|
|
|
local statusLabel = Instance.new("TextLabel")
|
|
statusLabel.Name = "StatusText"
|
|
statusLabel.Size = UDim2.new(1, -10, 1, -10)
|
|
statusLabel.Position = UDim2.new(0, 5, 0, 5)
|
|
statusLabel.BackgroundTransparency = 1
|
|
statusLabel.Text = "Roblox MCP Server\nRunning on port " .. CONFIG.PORT .. "\n\nWaiting for commands..."
|
|
statusLabel.TextColor3 = Color3.new(0, 1, 0)
|
|
statusLabel.TextScaled = true
|
|
statusLabel.Font = Enum.Font.Gotham
|
|
statusLabel.Parent = frame
|
|
|
|
-- Main update loop
|
|
RunService.Heartbeat:Connect(function()
|
|
if isRunning then
|
|
checkForCommands()
|
|
end
|
|
end)
|
|
|
|
log("Server started. Use the MCP bridge to send commands.", "success")
|
|
end
|
|
|
|
local function stop()
|
|
isRunning = false
|
|
log("Server stopped", "info")
|
|
end
|
|
|
|
-- Auto-start
|
|
start()
|
|
|
|
-- Export for external access
|
|
_G.RobloxMCPServer = {
|
|
start = start,
|
|
stop = stop,
|
|
isRunning = function()
|
|
return isRunning
|
|
end,
|
|
queueCommand = function(id, command, params)
|
|
table.insert(commandQueue, { id = id, command = command, params = params })
|
|
end,
|
|
getResponse = function(id)
|
|
return responseStore[id]
|
|
end,
|
|
}
|
|
|
|
log("Roblox MCP Server Module loaded. Type _G.RobloxMCPServer for access.", "info")
|