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>
This commit is contained in:
admin
2026-01-29 00:38:36 +04:00
Unverified
commit 9c44cb514f
12 changed files with 3535 additions and 0 deletions

9
.clauderc Normal file
View File

@@ -0,0 +1,9 @@
{
"mcpServers": {
"roblox-studio": {
"command": "node",
"args": ["src/index.js"],
"cwd": "/mnt/c/Users/Admin/roblox-mcp-server"
}
}
}

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
*.log
.DS_Store
.env

69
QUICKSTART.md Normal file
View File

@@ -0,0 +1,69 @@
# Roblox MCP - Quick Start Guide
## Step 1: Start the MCP Server
In this directory, run:
```bash
npm start
```
You should see:
```
HTTP server listening on port 37423
WebSocket server listening on port 37424
Waiting for Roblox Studio connection...
```
## Step 2: Install Roblox Studio Script
### Option A: Quick Install (Recommended)
1. Open Roblox Studio
2. In ServerScriptService, create a new Script
3. Copy the contents of `roblox-plugin/RobloxMCPServer.lua`
4. Paste into the script
5. Press Play
6. Look for the green "Roblox MCP Server" indicator in top-right
### Option B: Plugin Install
1. Copy `roblox-plugin/RobloxMCPPlugin.lua` to your Roblox Plugins folder:
- Windows: `C:\Users\YOUR_USERNAME\AppData\Local\Roblox\Plugins\`
2. Restart Roblox Studio
3. Enable via Plugin Management
## Step 3: Enable HTTP Requests
In Roblox Studio:
1. Game Settings → Security
2. Enable "Allow HTTP Requests"
3. Enable "Enable Studio Access to API Services"
## Step 4: Test with Claude Code
Now you can ask Claude to:
- "Create a red block part at position 0, 10, 0"
- "Create a Script in Workspace that prints hello"
- "Create a ScreenGui with a button"
## Troubleshooting
**Server won't start?**
- Check if port 37423/37424 is already in use
- Run `netstat -ano | findstr :37423` to check
**Roblox not connecting?**
- Make sure HTTP requests are enabled
- Check you pressed Play in Roblox Studio
- Look for the status indicator
**Commands not working?**
- Check Roblox Output window for errors
- Make sure paths are correct (e.g., "Workspace" not "workspace")
## Port Configuration
Default ports:
- MCP HTTP: 37423
- MCP WebSocket: 37424
- Roblox HTTP: 37425
Change in `src/index.js` and `roblox-plugin/RobloxMCPServer.lua` if needed.

217
README.md Normal file
View File

@@ -0,0 +1,217 @@
# Roblox MCP Server
Control Roblox Studio directly from Claude Code using the Model Context Protocol (MCP).
## Features
- Create and modify scripts in Roblox Studio
- Create 3D parts, models, and folders
- Build GUI elements (ScreenGui, Frame, TextButton, etc.)
- Set properties on any object
- Get hierarchy information
- Execute Lua code
- Play/Stop playtesting
- Save places
## Architecture
```
Claude Code <--(MCP)--> Node.js MCP Server <--(WebSocket)--> Roblox Studio Plugin
```
## Installation
### 1. Install Node.js Dependencies
```bash
cd roblox-mcp-server
npm install
```
### 2. Configure Claude Code
Add this to your Claude Code settings (or create `.clauderc` in your home directory):
```json
{
"mcpServers": {
"roblox-studio": {
"command": "node",
"args": ["/mnt/c/Users/Admin/roblox-mcp-server/src/index.js"],
"cwd": "/mnt/c/Users/Admin/roblox-mcp-server"
}
}
}
```
### 3. Install the Roblox Studio Plugin
#### Option A: Manual Installation
1. Copy `roblox-plugin/RobloxMCPServer.lua` to:
- **Windows**: `C:\Users\YOUR_USERNAME\AppData\Local\Roblox\Plugins\RobloxMCPServer.lua`
- **Mac**: `~/Library/Application Support/Roblox/Plugins/RobloxMCPServer.lua`
2. Open Roblox Studio
3. Go to **Plugins → Plugin Management**
4. Find "RobloxMCPServer" and enable it
#### Option B: In-Place Installation
1. Open Roblox Studio
2. Create a new place or open an existing one
3. In **ServerScriptService**, create a new **Script**
4. Paste the contents of `roblox-plugin/RobloxMCPServer.lua`
5. The server will auto-start when you press Play
### 4. Enable HTTP Requests (Important!)
In Roblox Studio:
1. Go to **Game Settings → Security**
2. **Enable** "Allow HTTP Requests"
3. Set **Enable Studio Access to API Services** to ON
## Usage
### Starting the MCP Server
```bash
npm start
```
The server will start on:
- HTTP: `http://localhost:37423` (for health checks)
- WebSocket: `ws://localhost:37424` (for Roblox Studio communication)
### Starting Roblox Studio Communication
1. Open Roblox Studio with the plugin installed
2. Press **Play** to start the server script
3. You should see a status indicator in the top-right corner
4. The MCP server will automatically connect
### Using with Claude Code
Once connected, you can ask Claude to:
```bash
# Create a script
"Create a Script in Workspace that prints 'Hello World'"
# Create a part
"Create a red block part at position 0, 10, 0 in Workspace"
# Build a GUI
"Create a ScreenGui with a TextButton that says 'Click Me'"
# Get hierarchy
"Show me the hierarchy of Workspace"
# Execute code
"Execute: print('Testing command')"
```
## Available Tools
| Tool | Description |
|------|-------------|
| `roblox_create_script` | Create a Script, LocalScript, or ModuleScript |
| `roblox_create_part` | Create 3D parts (Block, Ball, Cylinder, etc.) |
| `roblox_create_model` | Create model containers |
| `roblox_create_folder` | Create folders for organization |
| `roblox_create_gui` | Create GUI elements |
| `roblox_set_property` | Set properties on existing objects |
| `roblox_get_hierarchy` | Get the object hierarchy |
| `roblox_delete_object` | Delete an object by path |
| `roblox_execute_code` | Execute arbitrary Lua code |
| `roblox_play` | Start playtest |
| `roblox_stop` | Stop playtest |
| `roblox_save_place` | Save the current place |
## Configuration
Edit `src/index.js` to change ports:
```javascript
const HTTP_PORT = 37423; // Health check endpoint
const WS_PORT = 37424; // WebSocket for Roblox Studio
```
Edit `roblox-plugin/RobloxMCPServer.lua` to change plugin settings:
```lua
local CONFIG = {
PORT = 37425,
POLL_INTERVAL = 0.1,
DEBUG = true,
}
```
## Troubleshooting
### "No Roblox Studio instance connected"
- Make sure Roblox Studio is open
- Make sure the server script is running (press Play)
- Check that HTTP requests are enabled in Game Settings
- Look for the status indicator in the top-right corner
### WebSocket Connection Failed
- Check that the MCP server is running (`npm start`)
- Verify the WS_PORT matches between server and plugin
- Check Windows Firewall if connection is refused
### Scripts Not Executing
- Make sure the script type is correct (Script vs LocalScript vs ModuleScript)
- Check the Output window in Roblox Studio for errors
- Verify the parent path is correct
### HTTP Requests Blocked
- Go to Game Settings → Security
- Enable "Allow HTTP Requests"
- Enable "Enable Studio Access to API Services"
## Development
### Project Structure
```
roblox-mcp-server/
├── src/
│ └── index.js # MCP server + WebSocket server
├── roblox-plugin/
│ ├── RobloxMCPPlugin.lua # Toolbar plugin (optional)
│ └── RobloxMCPServer.lua # Server script (required)
├── package.json
└── README.md
```
### Adding New Tools
1. Add the tool definition to `src/index.js` in the `ListToolsRequestSchema` handler
2. Add a case in the `CallToolRequestSchema` handler
3. Add a corresponding handler in `roblox-plugin/RobloxMCPServer.lua`
## Security Notes
- This plugin allows executing arbitrary Lua code in Roblox Studio
- Only use in trusted environments
- HTTP requests must be enabled in Roblox Studio
- Consider using a reverse proxy for production deployments
## License
MIT
## Contributing
Contributions welcome! Feel free to open issues or pull requests.

44
examples/demo_game.lua Normal file
View File

@@ -0,0 +1,44 @@
-- Example: Simple Obby Game
-- This demonstrates creating a complete mini-game using Claude + MCP
-- 1. Create starting platform
local startPart = Instance.new("Part")
startPart.Name = "StartPlatform"
startPart.Size = Vector3.new(20, 1, 20)
startPart.Position = Vector3.new(0, 1, 0)
startPart.Anchored = true
startPart.BrickColor = BrickColor.new("Bright green")
startPart.Parent = workspace
-- 2. Create checkpoint platforms
local colors = {"Bright red", "Bright orange", "Bright yellow", "Bright blue", "Bright violet"}
for i = 1, 5 do
local platform = Instance.new("Part")
platform.Name = "Checkpoint" .. i
platform.Size = Vector3.new(10, 1, 10)
platform.Position = Vector3.new(0, i * 15, i * 20)
platform.Anchored = true
platform.BrickColor = BrickColor.new(colors[i])
platform.Parent = workspace
end
-- 3. Create kill brick (lava)
local lava = Instance.new("Part")
lava.Name = "Lava"
lava.Size = Vector3.new(100, 1, 200)
lava.Position = Vector3.new(0, -5, 50)
lava.Anchored = true
lava.BrickColor = BrickColor.new("Bright red")
lava.Material = Enum.Material.Neon
lava.Parent = workspace
-- 4. Spawn location
local spawn = Instance.new "SpawnLocation"
spawn.Name = "SpawnLocation"
spawn.Size = Vector3.new(8, 1, 8)
spawn.Position = Vector3.new(0, 1, 0)
spawn.Anchored = true
spawn.Transparency = 1
spawn.Parent = workspace
print("Obby game created! Press Play to test.")

View File

@@ -0,0 +1,12 @@
-- Example: Spinning Part Script
-- Put this in a Script inside a Part to make it spin
local part = script.Parent
while true do
-- Rotate the part
part.CFrame = part.CFrame * CFrame.Angles(0, math.rad(5), 0)
-- Wait for the next frame
task.wait()
end

48
examples/start_button.lua Normal file
View File

@@ -0,0 +1,48 @@
-- Example: Simple Start Button GUI
-- Creates a ScreenGui with a clickable button
-- Create ScreenGui
local screenGui = Instance.new("ScreenGui")
screenGui.Name = "StartGameGui"
screenGui.Parent = game:GetService("StarterGui")
-- Create main frame
local frame = Instance.new("Frame")
frame.Name = "MainFrame"
frame.Size = UDim2.new(0, 400, 0, 300)
frame.Position = UDim2.new(0.5, -200, 0.5, -150)
frame.BackgroundColor3 = Color3.fromRGB(30, 30, 30)
frame.Parent = screenGui
-- Create title
local title = Instance.new("TextLabel")
title.Name = "Title"
title.Size = UDim2.new(1, 0, 0, 100)
title.Position = UDim2.new(0, 0, 0, 50)
title.BackgroundTransparency = 1
title.Text = "MY AWESOME GAME"
title.TextColor3 = Color3.fromRGB(255, 255, 255)
title.TextScaled = true
title.Font = Enum.Font.GothamBold
title.Parent = frame
-- Create start button
local startButton = Instance.new("TextButton")
startButton.Name = "StartButton"
startButton.Size = UDim2.new(0, 200, 0, 50)
startButton.Position = UDim2.new(0.5, -100, 0, 150)
startButton.BackgroundColor3 = Color3.fromRGB(0, 170, 0)
startButton.TextColor3 = Color3.fromRGB(255, 255, 255)
startButton.Text = "START GAME"
startButton.TextScaled = true
startButton.Font = Enum.Font.GothamBold
startButton.Parent = frame
-- Button click handler
startButton.MouseButton1Click:Connect(function()
print("Start button clicked!")
screenGui:Destroy() -- Remove the GUI
-- Add your game start logic here
end)
print("Start button GUI created in StarterGui!")

1540
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "roblox-mcp-server",
"version": "1.0.0",
"description": "MCP server for controlling Roblox Studio via Claude Code",
"main": "src/index.js",
"type": "module",
"scripts": {
"start": "node src/index.js",
"dev": "node --watch src/index.js"
},
"keywords": ["mcp", "roblox", "roblox-studio", "claude"],
"author": "",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4",
"express": "^4.18.2",
"cors": "^2.8.5",
"ws": "^8.16.0"
}
}

View File

@@ -0,0 +1,598 @@
--[[
Roblox MCP Plugin
This plugin connects Roblox Studio to the MCP server, allowing Claude AI to control it.
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
4. The plugin will auto-connect to the MCP server
--]]
local Plugin = plugin or {} -- For testing in Studio without plugin context
-- Configuration
local CONFIG = {
WS_HOST = "localhost",
WS_PORT = 37423,
RECONNECT_DELAY = 3,
MAX_RECONNECT_ATTEMPTS = 10,
}
-- State
local websocket = nil
local isConnected = false
local reconnectAttempts = 0
local reconnectTimer = nil
local pluginGui = nil
-- Logging function
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)
end
end
-- Get an object by path string
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
-- 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
-- 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")
return nil
end
end
return obj
end
-- Create an object at a path
local function createObjectAt(path, className, properties)
local parentPath = string.match(path, "^(.-)%.[^%.]+$") or "game"
local objectName = string.match(path, "%.([^%.]+)$") or path
local parent = getObjectFromPath(parentPath)
if not parent then
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()
obj[propName] = propValue
end)
end
end
obj.Parent = parent
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
log("Received command: " .. command, "info")
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,
}
else
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 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
properties.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
)
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)
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
end
local part = createObjectAt(params.parentPath or "Workspace", "Part", properties)
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
-- 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 == "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
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
-- 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()
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: " .. (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),
}
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
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,
}
elseif command == "stop" then
game:Load("Stop")
return {
success = true,
}
elseif command == "savePlace" then
local success = pcall(function()
game:SavePlace()
end)
return {
success = success,
}
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,
}
else
return {
success = false,
error = "Unknown command: " .. tostring(command),
}
end
end)
-- Send response back
local response = {
id = requestId,
data = result,
}
if not success then
response.error = tostring(result)
end
if isConnected and websocket then
websocket:Send(game:GetService("HttpService"):JSONEncode(response))
end
end
-- WebSocket message handler
local function onMessage(message)
log("Received message from MCP server", "info")
local data = game:GetService("HttpService"):JSONDecode(message)
handleCommand(data)
end
-- Connect to MCP server
local function connectToServer()
if isConnected then
return
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")
end
-- Disconnect from server
local function disconnectFromServer()
if websocket then
websocket:Close()
websocket = nil
end
isConnected = false
log("Disconnected from MCP server", "info")
end
-- Try to reconnect
local function scheduleReconnect()
if reconnectTimer then
return
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
)
button.Click:Connect(function()
if isConnected then
disconnectFromServer()
else
connectToServer()
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")
createPluginGui()
-- Auto-connect on startup
connectToServer()
log("Plugin initialized. Click the toolbar button to connect/disconnect.", "info")
end
-- Cleanup
local function cleanup()
disconnectFromServer()
if reconnectTimer then
reconnectTimer:Cancel()
reconnectTimer = nil
end
end
-- Start the plugin
initialize()
-- Handle plugin unload
if Plugin then
Plugin.Unloading:Connect(cleanup)
end
return {
connect = connectToServer,
disconnect = disconnectFromServer,
isConnected = function()
return isConnected
end,
}

View File

@@ -0,0 +1,451 @@
--[[
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")

523
src/index.js Normal file
View File

@@ -0,0 +1,523 @@
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import express from 'express';
import cors from 'cors';
import { WebSocketServer } from 'ws';
// Express server for Roblox Studio plugin communication
const app = express();
app.use(cors());
app.use(express.json());
const HTTP_PORT = 37423;
const WS_PORT = 37424;
// Store connected Roblox Studio instances
let studioClients = new Set();
let pendingRequests = new Map();
let requestIdCounter = 0;
// Create MCP server
const server = new Server(
{
name: 'roblox-studio-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Helper function to send command to Roblox Studio and wait for response
async function sendToStudio(command, params = {}) {
return new Promise((resolve, reject) => {
const requestId = ++requestIdCounter;
// Check if any Studio client is connected
if (studioClients.size === 0) {
reject(new Error(
'No Roblox Studio instance connected. Please:\n' +
'1. Open Roblox Studio\n' +
'2. Install the RobloxMCP plugin (see RobloxMCFPlugin.lua)\n' +
'3. Make sure the plugin is running'
));
return;
}
// Set up response handler
pendingRequests.set(requestId, { resolve, reject, timeout: setTimeout(() => {
pendingRequests.delete(requestId);
reject(new Error('Request timeout - Roblox Studio did not respond'));
}, 30000) });
// Send to all connected Studio clients
const message = JSON.stringify({ id: requestId, command, params });
studioClients.forEach(ws => {
if (ws.readyState === ws.OPEN) {
ws.send(message);
}
});
console.error(`[MCP] Sent command ${command} (ID: ${requestId})`);
});
}
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'roblox_create_script',
description: 'Create a new Lua script in Roblox Studio',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Hierarchy path (e.g., "Workspace.Part.Script")',
},
scriptName: {
type: 'string',
description: 'Name of the script',
},
scriptType: {
type: 'string',
enum: ['Script', 'LocalScript', 'ModuleScript'],
description: 'Type of script to create',
default: 'Script',
},
source: {
type: 'string',
description: 'Lua source code',
},
},
required: ['path', 'scriptName', 'source'],
},
},
{
name: 'roblox_create_part',
description: 'Create a 3D part in the workspace',
inputSchema: {
type: 'object',
properties: {
parentPath: {
type: 'string',
description: 'Parent path (e.g., "Workspace" or "Workspace.Model")',
default: 'Workspace',
},
partName: {
type: 'string',
description: 'Name of the part',
},
partType: {
type: 'string',
enum: ['Ball', 'Block', 'Cylinder', 'Wedge', 'CornerWedge'],
description: 'Shape of the part',
default: 'Block',
},
position: {
type: 'object',
properties: {
x: { type: 'number' },
y: { type: 'number' },
z: { type: 'number' },
},
description: 'Position in 3D space',
},
size: {
type: 'object',
properties: {
x: { type: 'number' },
y: { type: 'number' },
z: { type: 'number' },
},
description: 'Size of the part',
},
anchored: {
type: 'boolean',
description: 'Whether the part is anchored',
default: true,
},
color: {
type: 'string',
description: 'BrickColor name (e.g., "Bright red")',
},
},
required: ['partName'],
},
},
{
name: 'roblox_create_model',
description: 'Create a new model container',
inputSchema: {
type: 'object',
properties: {
parentPath: {
type: 'string',
description: 'Parent path (e.g., "Workspace")',
default: 'Workspace',
},
modelName: {
type: 'string',
description: 'Name of the model',
},
},
required: ['modelName'],
},
},
{
name: 'roblox_set_property',
description: 'Set a property on an existing object',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Full path to the object',
},
property: {
type: 'string',
description: 'Property name to set',
},
value: {
description: 'Property value (can be string, number, boolean, or object)',
},
},
required: ['path', 'property', 'value'],
},
},
{
name: 'roblox_get_hierarchy',
description: 'Get the hierarchy of objects in a path',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Path to query (e.g., "Workspace")',
default: 'Workspace',
},
depth: {
type: 'number',
description: 'How many levels deep to explore',
default: 2,
},
},
required: ['path'],
},
},
{
name: 'roblox_delete_object',
description: 'Delete an object by path',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Full path to the object to delete',
},
},
required: ['path'],
},
},
{
name: 'roblox_play',
description: 'Start playtest in Roblox Studio',
inputSchema: {
type: 'object',
properties: {
mode: {
type: 'string',
enum: ['Client', 'Server', 'Both'],
description: 'Play mode',
default: 'Both',
},
},
},
},
{
name: 'roblox_stop',
description: 'Stop the current playtest',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'roblox_save_place',
description: 'Save the current place',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'roblox_execute_code',
description: 'Execute arbitrary Lua code in the command bar',
inputSchema: {
type: 'object',
properties: {
code: {
type: 'string',
description: 'Lua code to execute',
},
context: {
type: 'string',
enum: ['Server', 'Client', 'Plugin'],
description: 'Execution context',
default: 'Plugin',
},
},
required: ['code'],
},
},
{
name: 'roblox_create_folder',
description: 'Create a folder object for organization',
inputSchema: {
type: 'object',
properties: {
parentPath: {
type: 'string',
description: 'Parent path (e.g., "Workspace")',
default: 'Workspace',
},
folderName: {
type: 'string',
description: 'Name of the folder',
},
},
required: ['folderName'],
},
},
{
name: 'roblox_create_gui',
description: 'Create a basic GUI element (ScreenGui, Frame, TextButton, etc.)',
inputSchema: {
type: 'object',
properties: {
parentPath: {
type: 'string',
description: 'Parent path (e.g., "PlayerGui" or "StarterGui")',
default: 'StarterGui',
},
guiType: {
type: 'string',
enum: ['ScreenGui', 'Frame', 'TextButton', 'TextLabel', 'TextBox', 'ImageLabel', 'ImageButton', 'ScrollingFrame'],
description: 'Type of GUI element',
},
name: {
type: 'string',
description: 'Name of the GUI element',
},
properties: {
type: 'object',
description: 'Properties to set on the GUI element (size, position, text, color, etc.)',
},
},
required: ['guiType', 'name'],
},
},
],
};
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
console.error(`[MCP] Tool called: ${name}`, JSON.stringify(args));
try {
let result;
switch (name) {
case 'roblox_create_script':
result = await sendToStudio('createScript', {
path: args.path,
scriptName: args.scriptName,
scriptType: args.scriptType || 'Script',
source: args.source,
});
break;
case 'roblox_create_part':
result = await sendToStudio('createPart', {
parentPath: args.parentPath || 'Workspace',
partName: args.partName,
partType: args.partType || 'Block',
position: args.position,
size: args.size,
anchored: args.anchored !== undefined ? args.anchored : true,
color: args.color,
});
break;
case 'roblox_create_model':
result = await sendToStudio('createModel', {
parentPath: args.parentPath || 'Workspace',
modelName: args.modelName,
});
break;
case 'roblox_set_property':
result = await sendToStudio('setProperty', {
path: args.path,
property: args.property,
value: args.value,
});
break;
case 'roblox_get_hierarchy':
result = await sendToStudio('getHierarchy', {
path: args.path || 'Workspace',
depth: args.depth || 2,
});
break;
case 'roblox_delete_object':
result = await sendToStudio('deleteObject', {
path: args.path,
});
break;
case 'roblox_play':
result = await sendToStudio('play', {
mode: args.mode || 'Both',
});
break;
case 'roblox_stop':
result = await sendToStudio('stop', {});
break;
case 'roblox_save_place':
result = await sendToStudio('savePlace', {});
break;
case 'roblox_execute_code':
result = await sendToStudio('executeCode', {
code: args.code,
context: args.context || 'Plugin',
});
break;
case 'roblox_create_folder':
result = await sendToStudio('createFolder', {
parentPath: args.parentPath || 'Workspace',
folderName: args.folderName,
});
break;
case 'roblox_create_gui':
result = await sendToStudio('createGUI', {
parentPath: args.parentPath || 'StarterGui',
guiType: args.guiType,
name: args.name,
properties: args.properties || {},
});
break;
default:
throw new Error(`Unknown tool: ${name}`);
}
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
error: error.message,
}, null, 2),
},
],
isError: true,
};
}
});
// WebSocket server for real-time communication with Roblox Studio
const wss = new WebSocketServer({ port: WS_PORT });
wss.on('connection', (ws) => {
console.error(`[WS] Roblox Studio connected!`);
studioClients.add(ws);
ws.on('message', (data) => {
try {
const message = JSON.parse(data);
console.error(`[WS] Received:`, message);
// Handle responses to pending requests
if (message.id && pendingRequests.has(message.id)) {
const { resolve, reject, timeout } = pendingRequests.get(message.id);
clearTimeout(timeout);
pendingRequests.delete(message.id);
if (message.error) {
reject(new Error(message.error));
} else {
resolve(message.data || message);
}
}
} catch (e) {
console.error(`[WS] Error parsing message:`, e);
}
});
ws.on('close', () => {
console.error(`[WS] Roblox Studio disconnected`);
studioClients.delete(ws);
});
ws.on('error', (e) => {
console.error(`[WS] Error:`, e);
studioClients.delete(ws);
});
});
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'ok',
studioConnected: studioClients.size > 0,
connections: studioClients.size,
});
});
// Start Express server
app.listen(HTTP_PORT, () => {
console.error(`HTTP server listening on port ${HTTP_PORT}`);
console.error(`WebSocket server listening on port ${WS_PORT}`);
console.error(`Waiting for Roblox Studio connection...`);
});
// Start MCP server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Roblox Studio MCP server running on stdio');
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});