Files
ClaudeCode-Roblox-Studio-MCP/roblox-plugin/RobloxMCPServer.lua
admin 9c44cb514f Initial commit: Roblox Studio MCP Server for Claude Code
- 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>
2026-01-29 00:38:36 +04:00

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")