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:
@@ -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()
|
||||
|
||||
239
roblox-plugin/RobloxMCPServer_HTTP.lua
Normal file
239
roblox-plugin/RobloxMCPServer_HTTP.lua
Normal file
@@ -0,0 +1,239 @@
|
||||
-- Roblox MCP Server - HTTP Polling Version
|
||||
-- This version polls the MCP server for commands via HTTP
|
||||
|
||||
local HttpService = game:GetService("HttpService")
|
||||
local RunService = game:GetService("RunService")
|
||||
|
||||
-- Configuration
|
||||
local MCP_SERVER_URL = "http://127.0.0.1:37423"
|
||||
local POLL_INTERVAL = 0.5 -- seconds
|
||||
local DEBUG = true
|
||||
|
||||
-- State
|
||||
local isRunning = true
|
||||
local lastCommandId = 0
|
||||
|
||||
-- Logging
|
||||
local function log(msg)
|
||||
if DEBUG then
|
||||
print("[RobloxMCP] " .. msg)
|
||||
end
|
||||
end
|
||||
|
||||
-- Get object by 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
|
||||
|
||||
local parts = {}
|
||||
for part in string.gmatch(path, "[^%.]+") do
|
||||
table.insert(parts, part)
|
||||
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
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return obj
|
||||
end
|
||||
|
||||
-- Create object at path
|
||||
local function createObjectAt(path, className, properties)
|
||||
local lastDot = string.find(path, "%.[^%.]+$")
|
||||
local parentPath = lastDot and string.sub(path, 1, lastDot - 1) or "game"
|
||||
local objectName = lastDot and string.sub(path, lastDot + 1) or path
|
||||
|
||||
local parent = getObjectFromPath(parentPath)
|
||||
if not parent then return nil, "Parent not found" end
|
||||
|
||||
local obj = Instance.new(className)
|
||||
obj.Name = objectName
|
||||
|
||||
if properties then
|
||||
for prop, value in pairs(properties) do
|
||||
pcall(function() obj[prop] = value end)
|
||||
end
|
||||
end
|
||||
|
||||
obj.Parent = parent
|
||||
return obj
|
||||
end
|
||||
|
||||
-- Command handlers
|
||||
local handlers = {}
|
||||
|
||||
handlers.createPart = function(params)
|
||||
local props = {
|
||||
Name = params.partName,
|
||||
Anchored = params.anchored ~= false,
|
||||
Shape = Enum.PartType.Block,
|
||||
}
|
||||
|
||||
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 1, params.size.z or 2)
|
||||
end
|
||||
|
||||
if params.color then
|
||||
pcall(function() props.BrickColor = BrickColor.new(params.color) end)
|
||||
end
|
||||
|
||||
local part = createObjectAt((params.parentPath or "Workspace") .. "." .. params.partName, "Part", props)
|
||||
return {success = part ~= nil}
|
||||
end
|
||||
|
||||
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}
|
||||
end
|
||||
return {success = false}
|
||||
end
|
||||
|
||||
handlers.setProperty = function(params)
|
||||
local obj = getObjectFromPath(params.path)
|
||||
if not obj then return {success = false, error = "Not found"} end
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
pcall(function() obj[params.property] = value end)
|
||||
return {success = true}
|
||||
end
|
||||
|
||||
handlers.executeCode = function(params)
|
||||
local fn, err = loadstring(params.code)
|
||||
if not fn then return {success = false, error = err} end
|
||||
|
||||
local ok = pcall(fn)
|
||||
return {success = ok}
|
||||
end
|
||||
|
||||
handlers.getHierarchy = function(params)
|
||||
local obj = getObjectFromPath(params.path or "Workspace")
|
||||
if not obj then return {success = false, error = "Not found"} end
|
||||
|
||||
local function build(obj, depth)
|
||||
if depth <= 0 then return nil end
|
||||
local children = {}
|
||||
for _, child in ipairs(obj:GetChildren()) do
|
||||
table.insert(children, {
|
||||
name = child.Name,
|
||||
className = child.ClassName,
|
||||
})
|
||||
end
|
||||
return children
|
||||
end
|
||||
|
||||
return {success = true, children = build(obj, params.depth or 2)}
|
||||
end
|
||||
|
||||
handlers.importGLB = function(params)
|
||||
-- Import GLB model into Roblox Studio
|
||||
-- GLB files need to be imported via the Editor API for assets
|
||||
-- For now, we'll create a placeholder model with instructions
|
||||
|
||||
local parent = getObjectFromPath(params.parentPath or "Workspace")
|
||||
if not parent then
|
||||
return {success = false, error = "Parent path not found"}
|
||||
end
|
||||
|
||||
-- Create a model to hold the imported GLB
|
||||
local model = Instance.new("Model")
|
||||
model.Name = params.modelName or "ImportedGLB"
|
||||
model.Parent = parent
|
||||
|
||||
-- Create a placeholder part with info
|
||||
local placeholder = Instance.new("Part")
|
||||
placeholder.Name = "GLB_Placeholder"
|
||||
placeholder.Size = Vector3.new(4, 4, 4)
|
||||
placeholder.Position = Vector3.new(0, 5, 0)
|
||||
placeholder.Anchored = true
|
||||
placeholder.BrickColor = BrickColor.new("Bright blue")
|
||||
placeholder.Transparency = 0.5
|
||||
placeholder.Parent = model
|
||||
|
||||
-- Add a note
|
||||
local info = Instance.new("StringValue")
|
||||
info.Name = "ImportInfo"
|
||||
info.Value = "GLB Import: Use the 3D Importer (File > Import 3D) or Editor Service to import GLB files. This is a placeholder."
|
||||
info.Parent = model
|
||||
|
||||
return {
|
||||
success = true,
|
||||
modelPath = (params.parentPath or "Workspace") .. "." .. params.modelName,
|
||||
note = "GLB files require manual import via Roblox Studio's 3D Importer or Editor Service API"
|
||||
}
|
||||
end
|
||||
|
||||
-- Poll for commands
|
||||
local function pollForCommands()
|
||||
local success, response = pcall(function()
|
||||
return HttpService:RequestAsync({
|
||||
Url = MCP_SERVER_URL .. "/poll?last=" .. lastCommandId,
|
||||
Method = "GET",
|
||||
})
|
||||
end)
|
||||
|
||||
if success and response.Success then
|
||||
local data = HttpService:JSONDecode(response.Body)
|
||||
if data.commands then
|
||||
for _, cmd in ipairs(data.commands) do
|
||||
log("Got command: " .. cmd.command)
|
||||
lastCommandId = cmd.id
|
||||
|
||||
local handler = handlers[cmd.command]
|
||||
local result = {success = false, error = "Unknown command"}
|
||||
|
||||
if handler then
|
||||
local ok, ret = pcall(handler, cmd.params)
|
||||
if ok then
|
||||
result = ret
|
||||
else
|
||||
result = {success = false, error = tostring(ret)}
|
||||
end
|
||||
end
|
||||
|
||||
-- Send result back
|
||||
pcall(function()
|
||||
HttpService:RequestAsync({
|
||||
Url = MCP_SERVER_URL .. "/result",
|
||||
Method = "POST",
|
||||
Headers = {["Content-Type"] = "application/json"},
|
||||
Body = HttpService:JSONEncode({
|
||||
id = cmd.id,
|
||||
result = result
|
||||
})
|
||||
})
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Main loop
|
||||
log("Starting Roblox MCP Server (HTTP Polling)")
|
||||
log("MCP Server: " .. MCP_SERVER_URL)
|
||||
|
||||
RunService.Heartbeat:Connect(function()
|
||||
if isRunning then
|
||||
pcall(pollForCommands)
|
||||
end
|
||||
end)
|
||||
|
||||
log("Roblox MCP Server is running!")
|
||||
89
roblox-plugin/TestConnection.lua
Normal file
89
roblox-plugin/TestConnection.lua
Normal file
@@ -0,0 +1,89 @@
|
||||
-- Simple Roblox MCP Connection Test
|
||||
-- Put this in ServerScriptService and Press Play
|
||||
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
print("=" .. string.rep("=", 50))
|
||||
print("ROBLOX MCP CONNECTION TEST")
|
||||
print("=" .. string.rep("=", 50))
|
||||
|
||||
-- Test 1: Check HTTP Service
|
||||
print("\n[TEST 1] Checking HttpService...")
|
||||
local success = pcall(function()
|
||||
HttpService:GetAsync("http://127.0.0.1:37423/health")
|
||||
end)
|
||||
|
||||
if success then
|
||||
print("✓ HTTP requests are WORKING!")
|
||||
else
|
||||
print("✗ HTTP requests are BLOCKED")
|
||||
print("\nFIX: Go to File → Game Settings → Security")
|
||||
print(" Enable BOTH HTTP options!")
|
||||
warn("Cannot connect without HTTP enabled!")
|
||||
end
|
||||
|
||||
-- Test 2: Try to connect to MCP server
|
||||
print("\n[TEST 2] Connecting to MCP Server...")
|
||||
local response = pcall(function()
|
||||
local result = HttpService:RequestAsync({
|
||||
Url = "http://127.0.0.1:37423/health",
|
||||
Method = "GET",
|
||||
})
|
||||
print("✓ MCP Server is RESPONDING!")
|
||||
print(" Response: " .. result.Body)
|
||||
return true
|
||||
end)
|
||||
|
||||
if not response then
|
||||
print("✗ MCP Server is NOT responding")
|
||||
print(" Make sure 'npm start' is running!")
|
||||
end
|
||||
|
||||
-- Test 3: Test polling
|
||||
print("\n[TEST 3] Testing command polling...")
|
||||
local pollResult = pcall(function()
|
||||
local result = HttpService:RequestAsync({
|
||||
Url = "http://127.0.0.1:37423/poll?last=0",
|
||||
Method = "GET",
|
||||
})
|
||||
local data = HttpService:JSONDecode(result.Body)
|
||||
print("✓ Polling is WORKING!")
|
||||
print(" Commands waiting: " .. #data.commands)
|
||||
return true
|
||||
end)
|
||||
|
||||
if not pollResult then
|
||||
print("✗ Polling FAILED")
|
||||
end
|
||||
|
||||
print("\n" .. string.rep("=", 51))
|
||||
print("CONNECTION TEST COMPLETE")
|
||||
print(string.rep("=", 51))
|
||||
|
||||
-- Status indicator
|
||||
local status = Instance.new("ScreenGui")
|
||||
status.Parent = game:GetService("CoreGui")
|
||||
|
||||
local frame = Instance.new("Frame")
|
||||
frame.Size = UDim2.new(0, 300, 0, 100)
|
||||
frame.Position = UDim2.new(0.5, -150, 0, 10)
|
||||
frame.BackgroundColor3 = Color3.new(0.1, 0.1, 0.1)
|
||||
frame.Parent = status
|
||||
|
||||
local text = Instance.new("TextLabel")
|
||||
text.Size = UDim2.new(1, 0, 1, 0)
|
||||
text.BackgroundTransparency = 1
|
||||
text.Text = "Roblox MCP Test\nRunning..."
|
||||
text.TextColor3 = Color3.new(1, 1, 0)
|
||||
text.TextScaled = true
|
||||
text.Parent = frame
|
||||
|
||||
if success and response then
|
||||
text.Text = "MCP CONNECTED!\nReady for commands!"
|
||||
text.TextColor3 = Color3.new(0, 1, 0)
|
||||
else
|
||||
text.Text = "MCP NOT CONNECTED\nCheck Output window"
|
||||
text.TextColor3 = Color3.new(1, 0, 0)
|
||||
end
|
||||
|
||||
game:GetService("Debris"):AddItem(status, 10)
|
||||
Reference in New Issue
Block a user