Roblox Studio — build it right, build it once, make it look REAL
Runbook for anything built or edited in Roblox Studio. The tools are the External-MCP tools ext__Roblox_Studio__<tool> (need Multi-Turn + ACPX on). Work as an OPERATOR: preflight, build in a FEW big scripts, verify, report.
STEP 0 — Preflight (ALWAYS, before any build)
external_mcp_status — is Roblox_Studio connected? If not: external_mcp_reconnect, then external_mcp_wait (a first-run stdio child takes seconds).
list_roblox_studios → if more than one, set_active_studio on the intended one.
get_studio_state — a place must be open with the plugin connected.
- No Studio connected: STOP and say "Open Roblox Studio with a place, make sure the MCP plugin is running, then say go." Never fake a build.
Tool map
| Want to… |
Use |
| Run ANY Luau (terrain, parts, scripts, properties) |
execute_luau — the workhorse |
Wait for an async generate_* job |
wait_job_finished |
| See errors / prints |
get_console_output |
| Read the scene tree |
inspect_instance, search_game_tree |
| AI-generate an organic mesh/model/material |
generate_mesh, generate_procedural_model, generate_material (async → poll) |
| Marketplace asset |
search_asset → insert_asset |
| Author/read scripts |
multi_edit, script_read, script_search, script_grep |
| See the result |
screen_capture |
Use execute_luau for deterministic geometry; reserve the generative tools for organic one-off props.
GOLDEN RULES
- Batch — the ENTIRE build in one or a few looping
execute_luau scripts, not one part per call.
- Realism = Terrain VOXELS + Perlin noise (below). Never landscape out of
Parts, never symmetric concentric layers.
- Wrap every script in
pcall, end with print("TLM_OK …") (else warn(err)), then confirm via get_console_output.
- Correct Luau types —
Vector3.new(x,y,z) takes three NUMBERS. "Unable to cast double to Vector3" means you passed a number where a Vector3 belongs: fix it, don't retry unchanged.
- Undo-friendly —
ChangeHistoryService:TryBeginRecording(...) / :FinishRecording(...).
- Never loop on a failing tool. Two errors → stop, read the console, fix the cause or say so honestly (the executor blocks a 3× repeat anyway).
- Verify, then report. "Done" only after the console (ideally a capture) confirms it.
TERRAIN & MOUNTAINS — where builds go WRONG
Two hard requirements; skip either and you get blocky STEPPED PYRAMIDS — a FAIL, not a mountain:
workspace.Terrain VOXELS, never Parts — stacked parts show hard rectangular steps; voxels smooth into rock/snow.
- Shape driven by PERLIN NOISE (
math.noise), never concentric layers — concentric shrinking disks give a cone or a ziggurat. Real mountains are irregular: asymmetric peaks, ridges, spurs, no two slopes alike.
The right way — a Perlin-noise heightmap written with ONE Terrain:WriteVoxels. Per (x,z): height = summed peak falloffs (smoothstep → rounded base) plus multi-octave math.noise; fill below it — Rock, Snow above a NOISY snowline, Grass at the base:
local Terrain = workspace.Terrain
local RES = 4 -- voxel studs (4 = detailed, 8 = faster)
local W = 512 -- W x W studs, centered on origin
local peaks = { -- jitter these; DIFFERENT heights/spreads = natural
{x=0, z=0, h=150, r=175},
{x=-150,z=-130,h=95, r=120},
{x=165, z=140, h=120, r=135},
{x=-135,z=150, h=62, r=95 },
{x=170, z=-125,h=48, r=80 },
}
local AIR,ROCK,SNOW,GRASS = Enum.Material.Air,Enum.Material.Rock,Enum.Material.Snow,Enum.Material.Grass
local function surfaceY(wx, wz)
local h = 6 -- flat-ish base ground
for _,p in ipairs(peaks) do
local dx,dz = wx-p.x, wz-p.z
local f = math.clamp(1 - math.sqrt(dx*dx+dz*dz)/p.r, 0, 1)
f = f*f*(3 - 2*f) -- smoothstep => no cone tip
h = h + p.h*f
end
-- multi-octave noise: ridges + roughness + asymmetry (THIS makes it REAL)
h = h + math.noise(wx*0.006, wz*0.006, 0.3)*40
+ math.noise(wx*0.015, wz*0.015, 2.7)*15
+ math.noise(wx*0.045, wz*0.045, 6.1)*5
return math.max(2, h)
end
local ok, err = pcall(function()
local region = Region3.new(Vector3.new(-W/2,0,-W/2), Vector3.new(W/2,176,W/2)):ExpandToGrid(RES)
local size = region.Size/RES
local origin = region.CFrame.Position - region.Size/2 -- world min corner
local mats, occ = {}, {}
for x=1,size.X do mats[x]={} occ[x]={}
for y=1,size.Y do mats[x][y]={} occ[x][y]={}
for z=1,size.Z do
local wx = origin.X + (x-0.5)*RES
local wy = origin.Y + (y-0.5)*RES
local wz = origin.Z + (z-0.5)*RES
local s = surfaceY(wx, wz)
if wy <= s then
occ[x][y][z] = 1
local snowline = 92 + math.noise(wx*0.02, wz*0.02, 4.0)*22 -- ragged edge
mats[x][y][z] = (wy > snowline and SNOW) or (wy < 9 and GRASS or ROCK)
else
occ[x][y][z] = 0; mats[x][y][z] = AIR
end
end
end
end
Terrain:WriteVoxels(region, RES, mats, occ)
end)
if ok then
print(("TLM_OK terrain: %d peaks, %dx%d studs, noise-ridged, snow-capped"):format(#peaks, W, W))
else
warn("TLM_FAIL "..tostring(err))
end
Tune peaks (count/height/spread), W, RES (bump to 8 for huge volumes) and the three noise amplitudes (bigger = rougher). Keep peak centers inside ±W/2. WriteVoxels caps at ~4.19M voxels per call — loop the region in ≤~256-stud chunks for a bigger world.
Never: landscape from Parts/WedgeParts; concentric FillBlock/FillBall disks without noise; perfectly symmetric peaks or identical mountains. If you truly must use FillBall, still jitter every radius/center with math.noise and overlap many small balls — but the heightmap above is strongly preferred.
GENERATIVE tools
generate_mesh / generate_procedural_model / generate_material start an ASYNC job returning a job id → wait_job_finished(job_id) → place the result with Luau. For organic PROPS, not landscape.
VERIFY & REPORT (always)
get_console_output — the TLM_OK marker printed, no red errors, no TLM_FAIL.
screen_capture — aim the camera first via a quick execute_luau (workspace.CurrentCamera.CFrame = CFrame.lookAt(Vector3.new(400,300,400), Vector3.new(0,60,0))), so you both SEE it is natural, not blocky.
- Say in two lines WHAT was built and HOW to see it (which place; select in Explorer + press F). Claim success only after verifying.
FAILURE HANDLING (honest, never silent)
- Studio not connected → the STEP 0 message; do not pretend to build.
- Luau error /
TLM_FAIL → read the console, fix the real line (types, nil, WriteVoxels region/array sizing), retry ONCE, then report the exact error and stop.
- Generative job never finishes → report the timeout, fall back to
execute_luau.
- Never report "done" unless verify actually confirmed it.
1---2name: roblox-studio3description: Build and edit in Roblox Studio via the Roblox Studio MCP the RIGHT way - preflight the Studio connection, do the whole build in a few big execute_luau scripts (not dozens of tiny calls), make REALISTIC terrain with the Terrain VOXEL api driven by Perlin noise (NEVER stacked Parts or concentric layers - those give ugly blocky stepped pyramids), poll generative jobs, check the console, and fail honestly. Invoke for ANY "in Roblox / Roblox Studio" request - terrain, mountains, parts, scripts, models, materials, assets.4---5<!--6═══════════════════════════════════════════════════════════════════7 ✦ T L A M A T I N I ✦ — "one who knows"8 Created by Angela López Mendoza · @angelahack19 Tlamatini Author Banner — do not remove (Angela's name is kept in every build)10═══════════════════════════════════════════════════════════════════11-->1213# Roblox Studio — build it right, build it once, make it look REAL1415Runbook for anything built or edited in **Roblox Studio**. The tools are the External-MCP tools `ext__Roblox_Studio__<tool>` (need Multi-Turn + ACPX on). Work as an OPERATOR: preflight, build in a FEW big scripts, verify, report.1617## STEP 0 — Preflight (ALWAYS, before any build)18191. `external_mcp_status` — is `Roblox_Studio` connected? If not: `external_mcp_reconnect`, then `external_mcp_wait` (a first-run stdio child takes seconds).202. `list_roblox_studios` → if more than one, `set_active_studio` on the intended one.213. `get_studio_state` — a place must be open with the plugin connected.224. **No Studio connected:** STOP and say *"Open Roblox Studio with a place, make sure the MCP plugin is running, then say go."* Never fake a build.2324## Tool map2526| Want to… | Use |27|---|---|28| Run ANY Luau (terrain, parts, scripts, properties) | `execute_luau` — the workhorse |29| Wait for an async `generate_*` job | `wait_job_finished` |30| See errors / prints | `get_console_output` |31| Read the scene tree | `inspect_instance`, `search_game_tree` |32| AI-generate an organic mesh/model/material | `generate_mesh`, `generate_procedural_model`, `generate_material` (async → poll) |33| Marketplace asset | `search_asset` → `insert_asset` |34| Author/read scripts | `multi_edit`, `script_read`, `script_search`, `script_grep` |35| See the result | `screen_capture` |3637Use `execute_luau` for deterministic geometry; reserve the generative tools for organic one-off props.3839## GOLDEN RULES40411. **Batch** — the ENTIRE build in one or a few looping `execute_luau` scripts, not one part per call.422. **Realism = Terrain VOXELS + Perlin noise** (below). Never landscape out of `Part`s, never symmetric concentric layers.433. **Wrap every script in `pcall`**, end with `print("TLM_OK …")` (else `warn(err)`), then confirm via `get_console_output`.444. **Correct Luau types** — `Vector3.new(x,y,z)` takes three NUMBERS. `"Unable to cast double to Vector3"` means you passed a number where a Vector3 belongs: fix it, don't retry unchanged.455. **Undo-friendly** — `ChangeHistoryService:TryBeginRecording(...)` / `:FinishRecording(...)`.466. **Never loop on a failing tool.** Two errors → stop, read the console, fix the cause or say so honestly (the executor blocks a 3× repeat anyway).477. **Verify, then report.** "Done" only after the console (ideally a capture) confirms it.4849## TERRAIN & MOUNTAINS — where builds go WRONG5051Two hard requirements; skip either and you get **blocky STEPPED PYRAMIDS** — a FAIL, not a mountain:52531. **`workspace.Terrain` VOXELS, never `Part`s** — stacked parts show hard rectangular steps; voxels smooth into rock/snow.542. **Shape driven by PERLIN NOISE (`math.noise`), never concentric layers** — concentric shrinking disks give a cone or a ziggurat. Real mountains are irregular: asymmetric peaks, ridges, spurs, no two slopes alike.5556**The right way — a Perlin-noise heightmap written with ONE `Terrain:WriteVoxels`.** Per (x,z): height = summed peak falloffs (smoothstep → rounded base) **plus multi-octave `math.noise`**; fill below it — Rock, Snow above a NOISY snowline, Grass at the base:5758```lua59local Terrain = workspace.Terrain60local RES = 4 -- voxel studs (4 = detailed, 8 = faster)61local W = 512 -- W x W studs, centered on origin62local peaks = { -- jitter these; DIFFERENT heights/spreads = natural63 {x=0, z=0, h=150, r=175},64 {x=-150,z=-130,h=95, r=120},65 {x=165, z=140, h=120, r=135},66 {x=-135,z=150, h=62, r=95 },67 {x=170, z=-125,h=48, r=80 },68}69local AIR,ROCK,SNOW,GRASS = Enum.Material.Air,Enum.Material.Rock,Enum.Material.Snow,Enum.Material.Grass70local function surfaceY(wx, wz)71 local h = 6 -- flat-ish base ground72 for _,p in ipairs(peaks) do73 local dx,dz = wx-p.x, wz-p.z74 local f = math.clamp(1 - math.sqrt(dx*dx+dz*dz)/p.r, 0, 1)75 f = f*f*(3 - 2*f) -- smoothstep => no cone tip76 h = h + p.h*f77 end78 -- multi-octave noise: ridges + roughness + asymmetry (THIS makes it REAL)79 h = h + math.noise(wx*0.006, wz*0.006, 0.3)*4080 + math.noise(wx*0.015, wz*0.015, 2.7)*1581 + math.noise(wx*0.045, wz*0.045, 6.1)*582 return math.max(2, h)83end84local ok, err = pcall(function()85 local region = Region3.new(Vector3.new(-W/2,0,-W/2), Vector3.new(W/2,176,W/2)):ExpandToGrid(RES)86 local size = region.Size/RES87 local origin = region.CFrame.Position - region.Size/2 -- world min corner88 local mats, occ = {}, {}89 for x=1,size.X do mats[x]={} occ[x]={}90 for y=1,size.Y do mats[x][y]={} occ[x][y]={}91 for z=1,size.Z do92 local wx = origin.X + (x-0.5)*RES93 local wy = origin.Y + (y-0.5)*RES94 local wz = origin.Z + (z-0.5)*RES95 local s = surfaceY(wx, wz)96 if wy <= s then97 occ[x][y][z] = 198 local snowline = 92 + math.noise(wx*0.02, wz*0.02, 4.0)*22 -- ragged edge99 mats[x][y][z] = (wy > snowline and SNOW) or (wy < 9 and GRASS or ROCK)100 else101 occ[x][y][z] = 0; mats[x][y][z] = AIR102 end103 end104 end105 end106 Terrain:WriteVoxels(region, RES, mats, occ)107end)108if ok then109 print(("TLM_OK terrain: %d peaks, %dx%d studs, noise-ridged, snow-capped"):format(#peaks, W, W))110else111 warn("TLM_FAIL "..tostring(err))112end113```114115Tune `peaks` (count/height/spread), `W`, `RES` (bump to 8 for huge volumes) and the three noise amplitudes (bigger = rougher). Keep peak centers inside `±W/2`. **`WriteVoxels` caps at ~4.19M voxels per call** — loop the region in ≤~256-stud chunks for a bigger world.116117**Never:** landscape from `Part`s/`WedgePart`s; concentric `FillBlock`/`FillBall` disks without noise; perfectly symmetric peaks or identical mountains. If you truly must use `FillBall`, still jitter every radius/center with `math.noise` and overlap many small balls — but the heightmap above is strongly preferred.118119## GENERATIVE tools120121`generate_mesh` / `generate_procedural_model` / `generate_material` start an ASYNC job returning a job id → `wait_job_finished(job_id)` → place the result with Luau. For organic PROPS, not landscape.122123## VERIFY & REPORT (always)1241251. `get_console_output` — the `TLM_OK` marker printed, no red errors, no `TLM_FAIL`.1262. `screen_capture` — aim the camera first via a quick `execute_luau` (`workspace.CurrentCamera.CFrame = CFrame.lookAt(Vector3.new(400,300,400), Vector3.new(0,60,0))`), so you both SEE it is natural, not blocky.1273. Say in two lines WHAT was built and HOW to see it (which place; select in Explorer + press F). Claim success only after verifying.128129## FAILURE HANDLING (honest, never silent)130131- Studio not connected → the STEP 0 message; do not pretend to build.132- Luau error / `TLM_FAIL` → read the console, fix the real line (types, nil, WriteVoxels region/array sizing), retry ONCE, then report the exact error and stop.133- Generative job never finishes → report the timeout, fall back to `execute_luau`.134- Never report "done" unless verify actually confirmed it.