Two whole classes of testing look impossible from the MCP tools alone, so it's tempting to punt them to the user. Both are self-serviceable. Check here before claiming you need the user to test something.
For the normal dev loop see roblox-studio-workflow; for the MCP toolbox see roblox-studio-mcp.
1. Device emulation (mobile / tablet / touch) — via Studio's menu bar
Studio exposes no windows to macOS Accessibility (count windows → 0), so you can't resize it.
But it exposes its entire menu bar, which is enough to drive the Test menu.
# one-time: the user must grant Terminal (or your host app) Accessibility permission
# System Settings → Privacy & Security → Accessibility
# symptom if missing: "osascript is not allowed assistive access. (-1719)"
osascript -e 'tell application "Roblox Studio" to activate'
osascript -e 'tell application "System Events" to tell process "RobloxStudio"
to click menu item "Device Emulator" of menu 1 of menu bar item "Test" of menu bar 1'
Process name is **`RobloxStudio`** (not "Roblox Studio", not "RobloxStudioBeta").
**Verify it took**, don't assume — start Play and read the real values:
```lua
local cam = workspace.CurrentCamera
local UIS = game:GetService("UserInputService")
-- desktop: ~1920x1080, TouchEnabled=false
-- emulated: device-sized viewport, TouchEnabled=TRUE
return cam.ViewportSize, UIS.TouchEnabled
TouchEnabled = true is the signal that touch-only code paths (thumbstick clearance, touch layout
branches, POS_SHOWN offsets) are now live.
The full Test menu (useful items):
| Item | Notes |
|---|---|
Device Emulator |
toggles emulation |
Controller Emulator / Player Emulator |
gamepad / player-state emulation |
Start Test Session |
required before Add Clients |
Add Clients |
disabled in Play Solo; enabled inside a Test Session |
Toggle Client View, Pause Client, End Session |
multi-client session control |
Enumerate + check enabled state before clicking:
osascript -e 'tell application "System Events" to tell process "RobloxStudio" \
to get name of every menu item of menu 1 of menu bar item "Test" of menu bar 1'
osascript -e 'tell application "System Events" to tell process "RobloxStudio" \
to get enabled of menu item "Add Clients" of menu 1 of menu bar item "Test" of menu bar 1'
Limits
- Picking a specific device (iPhone vs iPad vs …) uses an in-window dropdown that is NOT AX-exposed. You get whatever device is currently selected. Ask the user to set it once if you need a particular width — narrow phones are where layout actually breaks.
- Toggling the emulator ends the current Play session and can drop the MCP link. Re-point with
list_roblox_studios→set_active_studio, thenstart_stop_play. - Toggle it back OFF when done, or every later screenshot is a touch device.
2. Multiplayer features WITHOUT a second client — fire the real remotes
This is usually faster and more precise than launching a multi-client session. The Server and
Client VMs share the DataModel, so a FireClient from the Server VM reaches the real client
modules and drives everything downstream — signal handlers, cached state, UI refreshes.
-- datamodel_type = "Server"
local Remotes = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared").Remotes)
local plr = game.Players:GetPlayers()[1]
-- pretend the server told us we joined someone's party
Remotes.event("PartyUpdated"):FireClient(plr, {
ownerUserId = 99999999, ownerName = "TestHost",
members = { { userId = plr.UserId, name = plr.DisplayName } },
})
Then assert on observable instance state from the Client VM (never module internals — separate
VM, require gives a phantom copy):
-- datamodel_type = "Client"
local on = 0
for _, p in game:GetService("CollectionService"):GetTagged("Plot") do
for _, d in p:GetDescendants() do
if d:IsA("ProximityPrompt") and d.Name == "PartyPrompt" and d.Enabled then on += 1 end
end
end
return on
Always test the round trip — fire the "joined" state, assert, then fire the "left" state
(FireClient(plr, nil)) and assert it reverts. A one-way test hides stuck state, which is the
scarier bug (a prompt stuck hidden = the feature is dead for that player).
Faking the preconditions
Client-side attribute writes are enough to set up a scenario, because client code reads the same
attributes and GetAttributeChangedSignal fires locally:
-- datamodel_type = "Client" — pretend another player owns this plot
workspace.Map.Plots.Plot1:SetAttribute("OwnerUserId", 99999999)
-- ... run the test ...
workspace.Map.Plots.Plot1:SetAttribute("OwnerUserId", nil) -- ALWAYS clean up
When this does NOT work
Code that requires the faked user to be a real online Player can't be faked this way. Example:
a poll loop that does Players:GetPlayerByUserId(owner) and tears down anything failing that check
will immediately undo your setup. Symptom: the thing you created vanishes within a second.
Fall back to either:
Test ▸ Start Test Session ▸ Add Clientsfor a genuine second player, or- unit-test the pure logic with synthetic inputs in the Client VM — mirror the shipped guard/branch verbatim and table-test it:
local cases = {
{ "nil payload", nil, true },
{ "empty string", "", true },
{ "garbage base64", "!!!not-base64!!!", true },
{ "short buffer", Base64.encode(short),true },
{ "valid buffer", Base64.encode(full), false },
}
-- assert blocked == expected for each; report N/N
That's a real, reportable result — just be explicit that it verifies the logic, not the full render path.
Reporting honestly
Distinguish these three, every time:
- Verified live — drove it and observed the effect.
- Logic-verified — table-tested the branch with synthetic inputs; full path untested.
- Untested — say so, and say exactly what it needs.
Never let "it compiles and looks right" be reported as verified.