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:
9
.clauderc
Normal file
9
.clauderc
Normal 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
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
69
QUICKSTART.md
Normal file
69
QUICKSTART.md
Normal 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
217
README.md
Normal 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
44
examples/demo_game.lua
Normal 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.")
|
||||
12
examples/spinning_part.lua
Normal file
12
examples/spinning_part.lua
Normal 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
48
examples/start_button.lua
Normal 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
1540
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
package.json
Normal file
20
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
598
roblox-plugin/RobloxMCPPlugin.lua
Normal file
598
roblox-plugin/RobloxMCPPlugin.lua
Normal 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,
|
||||
}
|
||||
451
roblox-plugin/RobloxMCPServer.lua
Normal file
451
roblox-plugin/RobloxMCPServer.lua
Normal 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
523
src/index.js
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user