Add FPS game example, auto-connect plugin, and Python injection tools

- Updated RobloxMCPPlugin with HTTP polling (auto-enables HttpService)
- Added 20-weapon FPS game example (CoD-style)
- Added Python studio-inject.py for command bar injection via Win32 API
- Added auto-connect setup scripts (VBS + PowerShell)
- Updated MCP server with all FPS game tools

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Admin
2026-03-31 16:57:35 +04:00
Unverified
parent 9c44cb514f
commit a66533206f
16 changed files with 3448 additions and 595 deletions

View File

@@ -1,89 +1,131 @@
--[[
Roblox MCP Plugin
This plugin connects Roblox Studio to the MCP server, allowing Claude AI to control it.
Roblox MCP Plugin v2.0 - HTTP Polling Edition
Connects Roblox Studio to Claude Code via MCP server.
Installation:
1. Copy this file to: Plugins/RobloxMCPPlugin.lua
- Windows: C:\Users\YOUR_USERNAME\AppData\Local\Roblox\Plugins\
- Mac: ~/Library/Application Support/Roblox/Plugins/
2. Restart Roblox Studio
3. Enable the plugin via Plugin Management
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 {} -- For testing in Studio without plugin context
local Plugin = plugin or {}
-- Configuration
local CONFIG = {
WS_HOST = "localhost",
WS_PORT = 37423,
RECONNECT_DELAY = 3,
MAX_RECONNECT_ATTEMPTS = 10,
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 websocket = nil
local isConnected = false
local isPolling = false
local reconnectAttempts = 0
local reconnectTimer = nil
local pluginGui = nil
local lastCommandId = 0
local pollThread = nil
local toolbar = nil
local connectButton = nil
-- Logging function
-- 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"
local prefix = "[RobloxMCP]"
local fullMessage = string.format("%s %s: %s", prefix, level:upper(), message)
print(fullMessage)
-- Also show in a dialog if it's an error
if level == "error" then
warn(fullMessage)
level = level or "INFO"
print("[RobloxMCP] [" .. level .. "] " .. message)
if level == "ERROR" then
warn("[RobloxMCP] " .. message)
end
end
-- Get an object by path string
-- 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
-- Handle special paths
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,
}
-- Split path by dot
local parts = {}
for part in string.gmatch(path, "[^%.]+") do
table.insert(parts, part)
end
if #parts == 0 then return nil end
if #parts == 0 then
return nil
end
-- Start from service or game
local obj = serviceMap[parts[1]] or game
-- Start from game
local obj = game
-- Traverse the path
for i, part in ipairs(parts) do
if obj:IsA("Workspace") and part == "Workspace" then
-- Workspace is a special case
elseif obj:FindFirstChild(part) then
obj = obj[part]
else
log("Could not find part: " .. part .. " in path: " .. path, "error")
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 an object at a path
-- Create object at path
local function createObjectAt(path, className, properties)
local parentPath = string.match(path, "^(.-)%.[^%.]+$") or "game"
local parentPath = string.match(path, "^(.-)%.[^%.]+$") or "Workspace"
local objectName = string.match(path, "%.([^%.]+)$") or path
local parent = getObjectFromPath(parentPath)
@@ -91,11 +133,9 @@ local function createObjectAt(path, className, properties)
return nil, "Parent not found: " .. 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
pcall(function()
@@ -108,491 +148,282 @@ local function createObjectAt(path, className, properties)
return obj
end
-- Handle incoming commands from MCP server
local function handleCommand(data)
local command = data.command
local params = data.params or {}
local requestId = data.id
-- Handle a single command from MCP server
local function handleCommand(cmd)
local command = cmd.command
local params = cmd.params or {}
log("Received command: " .. command, "info")
log("Executing: " .. command)
local success, result = pcall(function()
if command == "createScript" then
-- Create a script object
local scriptObj = createObjectAt(params.path, params.scriptType or "Script", {
Name = params.scriptName,
})
if scriptObj then
-- Set the source code (in Roblox, this is the Source property)
if scriptObj:IsA("ModuleScript") then
-- Wait for it to be parented first, then set source
scriptObj.Source = params.source
else
scriptObj.Source = params.source
end
return {
success = true,
objectPath = params.path .. "." .. params.scriptName,
}
pcall(function() scriptObj.Source = params.source end)
return { success = true, objectPath = params.path }
else
return {
success = false,
error = "Failed to create script",
}
return { success = false, error = "Failed to create script" }
end
elseif command == "createPart" then
local properties = {
Name = params.partName,
Anchored = params.anchored ~= false,
}
-- Set shape based on partType
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
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
properties.Shape = shapeEnum
props.Shape = shapeEnum
-- 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
)
props.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 1, params.size.y or 1, params.size.z or 1)
props.Size = Vector3.new(params.size.x or 4, params.size.y or 4, params.size.z or 4)
end
-- Set color
if params.color then
local success = pcall(function()
properties.BrickColor = BrickColor.new(params.color)
end)
if not success then
-- Try as RGB color3
if 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
pcall(function() props.Color = BrickColor.new(params.color).Color end)
end
local part = createObjectAt(params.parentPath or "Workspace", "Part", properties)
return {
success = part ~= nil,
objectPath = (params.parentPath or "Workspace") .. "." .. params.partName,
}
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,
}
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,
}
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
-- Set default GUI properties
if params.guiType == "ScreenGui" then
properties.ResetOnSpawn = false
elseif params.guiType == "Frame" or params.guiType == "TextLabel" or params.guiType == "TextButton" then
-- Default size and position
if not properties.Size then
properties.Size = UDim2.new(0, 200, 0, 50)
end
if not properties.Position then
properties.Position = UDim2.new(0, 0, 0, 0)
end
end
-- Set text properties for text-based GUI
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
if not properties.TextScaled then
properties.TextScaled = true
end
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,
}
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
if not obj then return { success = false, error = "Object not found: " .. params.path } end
-- Handle special property types
local value = params.value
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" then
if value.components then
value = CFrame.new(unpack(value.components))
end
end
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),
}
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: " .. (params.path or "Workspace"),
}
end
local function buildHierarchy(object, depth, currentDepth)
if currentDepth > depth then
return nil
end
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,
}
local childData = { name = child.Name, className = child.ClassName }
if currentDepth < depth then
childData.children = buildHierarchy(child, depth, currentDepth + 1)
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 = buildHierarchy(obj, params.depth or 2, 0),
}
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
if not obj then return { success = false, error = "Object not found: " .. params.path } end
obj:Destroy()
return {
success = true,
deletedPath = params.path,
}
return { success = true, deletedPath = params.path }
elseif command == "play" then
local mode = params.mode or "Both"
if mode == "Server" then
game:Load("PlaySolo")
elseif mode == "Client" then
game:Load("PlayClient")
else
game:Load("PlaySolo")
end
return {
success = true,
mode = mode,
}
pcall(function() game:Load("PlaySolo") end)
return { success = true, mode = params.mode or "Both" }
elseif command == "stop" then
game:Load("Stop")
return {
success = true,
}
pcall(function() game:Load("Stop") end)
return { success = true }
elseif command == "savePlace" then
local success = pcall(function()
game:SavePlace()
end)
return {
success = success,
}
local ok = pcall(function() game:SavePlace() end)
return { success = ok }
elseif command == "executeCode" then
-- Execute code in the appropriate context
local context = params.context or "Plugin"
-- For security, we'll use the plugin's ExecuteInShell method if available
-- Or use the command bar
local success, err = pcall(function()
loadstring(params.code)()
end)
return {
success = success,
error = err,
context = context,
}
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),
}
return { success = false, error = "Unknown command: " .. tostring(command) }
end
end)
-- Send response back
-- Build response
local response = {
id = requestId,
data = result,
id = cmd.id,
result = success and result or { success = false, error = tostring(result) },
}
if not success then
response.error = tostring(result)
end
if isConnected and websocket then
websocket:Send(game:GetService("HttpService"):JSONEncode(response))
end
return response
end
-- WebSocket message handler
local function onMessage(message)
log("Received message from MCP server", "info")
-- 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
local data = game:GetService("HttpService"):JSONDecode(message)
handleCommand(data)
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 connectToServer()
if isConnected then
return
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
log("Connecting to MCP server at ws://" .. CONFIG.WS_HOST .. ":" .. CONFIG.WS_PORT, "info")
-- Use Roblox's WebSocket implementation
local httpService = game:GetService("HttpService")
-- Note: Roblox doesn't have built-in WebSocket support in plugins yet
-- We'll need to use a polling mechanism via HTTP
-- For now, let's create a simulated connection
-- This is a placeholder - real implementation would need:
-- 1. Either Roblox to add WebSocket support to plugins
-- 2. Or use HTTP polling as a fallback
-- 3. Or use a separate bridge application
log("WebSocket connection initiated", "info")
isConnected = true
reconnectAttempts = 0
-- Send connection confirmation
local connectMsg = game:GetService("HttpService"):JSONEncode({
type = "connected",
pluginVersion = "1.0.0",
studioVersion = version(),
})
-- websocket:Send(connectMsg)
log("Connected to MCP server!", "success")
lastCommandId = 0
log("Connected to MCP server!")
startPolling()
return true
end
-- Disconnect from server
local function disconnectFromServer()
if websocket then
websocket:Close()
websocket = nil
end
-- Disconnect from MCP server
local function disconnect()
isConnected = false
log("Disconnected from MCP server", "info")
isPolling = false
if pollThread then
pollThread = nil
end
log("Disconnected from MCP server")
end
-- Try to reconnect
local function scheduleReconnect()
if reconnectTimer then
return
-- Create toolbar button
local function createUI()
if not Plugin or not Plugin:FindFirstChildWhichIsA("Toolbar") then
toolbar = Plugin:CreateToolbar("RobloxMCP")
end
if reconnectAttempts >= CONFIG.MAX_RECONNECT_ATTEMPTS then
log("Max reconnection attempts reached. Please restart the plugin.", "error")
return
end
reconnectAttempts = reconnectAttempts + 1
log(string.format("Scheduling reconnect in %d seconds (attempt %d/%d)", CONFIG.RECONNECT_DELAY, reconnectAttempts, CONFIG.MAX_RECONNECT_ATTEMPTS), "info")
reconnectTimer = spawn(function()
wait(CONFIG.RECONNECT_DELAY)
reconnectTimer = nil
connectToServer()
end)
end
-- Create plugin GUI
local function createPluginGui()
if not Plugin then
return
end
local toolbar = Plugin:CreateToolbar("RobloxMCP")
local button = toolbar:CreateButton(
"Connect",
"Connect/Disconnect from MCP server",
"rbxassetid://413369506" -- Default icon
connectButton = toolbar:CreateButton(
"MCP Connect",
"Connect/Disconnect from Claude Code MCP server",
"rbxassetid://16706090882"
)
button.Click:Connect(function()
connectButton.Click:Connect(function()
if isConnected then
disconnectFromServer()
disconnect()
connectButton.Icon = "rbxassetid://16706090882"
else
connectToServer()
local success = connect()
if success then
connectButton.Icon = "rbxassetid://16706100672"
end
end
end)
-- Create info dialog
local function showInfo()
local infoGui = Instance.new("ScreenGui")
infoGui.Name = "RobloxMCPInfo"
local frame = Instance.new("Frame")
frame.Size = UDim2.new(0, 400, 0, 300)
frame.Position = UDim2.new(0.5, -200, 0.5, -150)
frame.BackgroundColor3 = Color3.new(0.1, 0.1, 0.1)
frame.Parent = infoGui
local title = Instance.new("TextLabel")
title.Size = UDim2.new(1, 0, 0, 50)
title.Position = UDim2.new(0, 0, 0, 0)
title.BackgroundTransparency = 1
title.Text = "Roblox MCP Plugin"
title.TextColor3 = Color3.new(1, 1, 1)
title.TextSize = 24
title.Font = Enum.Font.GothamBold
title.Parent = frame
local status = Instance.new("TextLabel")
status.Size = UDim2.new(1, -20, 0, 100)
status.Position = UDim2.new(0, 10, 0, 60)
status.BackgroundTransparency = 1
status.Text = "Status: " .. (isConnected and "Connected" or "Disconnected") .. "\n\n" .. "Server: ws://" .. CONFIG.WS_HOST .. ":" .. CONFIG.WS_PORT
status.TextColor3 = isConnected and Color3.new(0, 1, 0) or Color3.new(1, 0, 0)
status.TextSize = 16
status.Font = Enum.Font.Gotham
status.TextXAlignment = Enum.TextXAlignment.Left
status.TextYAlignment = Enum.TextYAlignment.Top
status.Parent = frame
local close = Instance.new("TextButton")
close.Size = UDim2.new(0, 100, 0, 40)
close.Position = UDim2.new(0.5, -50, 1, -50)
close.BackgroundColor3 = Color3.new(0.2, 0.2, 0.2)
close.Text = "Close"
close.TextColor3 = Color3.new(1, 1, 1)
close.TextSize = 18
close.Parent = frame
close.MouseButton1Click:Connect(function()
infoGui:Destroy()
end)
infoGui.Parent = game:GetService("CoreGui")
end
-- Store for later use
pluginGui = {
toolbar = toolbar,
button = button,
showInfo = showInfo,
}
end
-- Initialize plugin
local function initialize()
log("Initializing Roblox MCP Plugin v1.0.0", "info")
-- 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()
createPluginGui()
-- Auto-connect on startup
connectToServer()
log("Plugin initialized. Click the toolbar button to connect/disconnect.", "info")
-- 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
local function cleanup()
disconnectFromServer()
if reconnectTimer then
reconnectTimer:Cancel()
reconnectTimer = nil
end
end
-- Start the plugin
initialize()
-- Handle plugin unload
-- Cleanup on unload
if Plugin then
Plugin.Unloading:Connect(cleanup)
Plugin.Unloading:Connect(function()
disconnect()
end)
end
return {
connect = connectToServer,
disconnect = disconnectFromServer,
isConnected = function()
return isConnected
end,
}
init()