roblox-companion
Provides Roblox asset management through Open Cloud, including retrieving, uploading, updating, and archiving assets with safeguards for formats, deduplication, and irreversible uploads.
Companion for Roblox Studio: manages Studio instances, opens places, guards against editing the wrong place, safely executes Luau in chunks, extracts place contents with byte-level verification, and cleans up accumulated MCP stub processes.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
Studio Sidecar — companion MCP server for Roblox Studio
English | 繁體中文

Studio Sidecar fills the gaps in Roblox Studio's built-in MCP server. It does not replace it.
Since 2026, Roblox Studio ships its own MCP server (StudioMCP, installed with Studio, stdio transport), and the standalone Roblox/studio-rust-mcp-server repository has been archived. The built-in server has tools that no third party can rebuild: generate_mesh, generate_material and generate_procedural_model use Roblox's cloud AI with moderation; search_asset, insert_asset and upload_image carry your signed-in inventory permissions; user_keyboard_input and user_mouse_input go through CreateVirtualInput. Replacing it would throw all of that away.
So Studio Sidecar adds only what the built-in server structurally cannot do, and leaves everything else to it:
Open a place that is not open yet, and see every Studio process and StudioMCP broker/stub on the machine.
Guard writes by PlaceId, run Luau with runtime errors returned as readable values and output never truncated, and extract scripts past the 100k output limit with byte-exact verification.
Upload, update and archive assets through Roblox Open Cloud, with an explicit verdict when an update was silently deduplicated.
Publish a place to live servers through Open Cloud, which neither the built-in server nor a plugin can do: by default it republishes the latest saved version (what Team Create autosaves), guarded by an allowlist of placeIds.
No Studio plugin is needed. 12 tools; Windows fully verified, macOS partly (see Platform support).
Contents
Related MCP server: Roblox Studio MCP Server
Tools
Tool | Why the built-in server cannot do it |
| Shows whether Open Cloud is configured and which folders uploads may read from |
| The built-in server lives inside one Studio process; it cannot see other Studio processes or the StudioMCP broker/stub structure |
| Opening a place that is not open yet is an operating-system job: it needs a new sibling process |
| Same reason; reports by default and only terminates stubs with |
| The built-in |
| The built-in |
| There is no tool that handles the 100k truncation boundary: chunking, sentinels and per-file byte checks are left to the caller, and getting them wrong does not raise an error |
| The built-in |
|
|
|
|
| The built-in server has no Open Cloud surface |
| The built-in server has no publish tool, and |
Deliberately not included: script reading and writing, scene queries, execute_luau, screen_capture, playtesting. The built-in server already has them, and a second implementation would only drift.
How Luau runs on the Studio side
An MCP process can be a server and a client at the same time. Studio Sidecar starts StudioMCP as its own child process and connects to it as an MCP client, which gives it the built-in execute_luau. The chunking, sentinel and byte-check machinery is then wrapped into single tools. When the client closes normally, the child process goes away too, so this route does not leak stubs.
To start StudioMCP, Studio Sidecar uses the same launcher as Roblox's setup guide. On Windows that is %LOCALAPPDATA%\Roblox\mcp.bat, run through cmd.exe /c; when mcp.bat is missing, it falls back to the most recently modified StudioMCP.exe under %LOCALAPPDATA%\Roblox\Versions\. On macOS it uses StudioMCP inside RobloxStudio.app, in /Applications or ~/Applications.
Quick start
Requirements: Roblox Studio with Enable Studio as MCP server turned on (in Studio: Assistant → … → Manage MCP Servers), and Node.js 22 or newer.
Register Studio Sidecar with your AI client. Claude Code:
claude mcp add studio-sidecar --scope user \
-e ROBLOX_API_KEY="<your Open Cloud API key>" \
-e ROBLOX_CREATOR_USER_ID="<your Roblox user ID>" \
-e ROBLOX_UPLOAD_ROOT="<folder that uploads may read from>" \
-e ROBLOX_PUBLISH_PLACE_IDS="<placeIds roblox_publish_place may publish>" \
-- npx -y @gjlmotea/studio-sidecar-mcpClients that read an mcpServers JSON config (Claude Desktop, Cursor, Gemini CLI):
{
"mcpServers": {
"studio-sidecar": {
"command": "npx",
"args": ["-y", "@gjlmotea/studio-sidecar-mcp"],
"env": {
"ROBLOX_API_KEY": "<your Open Cloud API key>",
"ROBLOX_CREATOR_USER_ID": "<your Roblox user ID>",
"ROBLOX_UPLOAD_ROOT": "<folder that uploads may read from>",
"ROBLOX_PUBLISH_PLACE_IDS": "<placeIds roblox_publish_place may publish, comma-separated>"
}
}
}
}VS Code uses a top-level servers key instead, and each server has a type. An input variable keeps the key out of the file: VS Code asks for it the first time the server starts and stores it securely.
{
"inputs": [
{
"type": "promptString",
"id": "roblox-api-key",
"description": "Roblox Open Cloud API key",
"password": true
}
],
"servers": {
"studio-sidecar": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@gjlmotea/studio-sidecar-mcp"],
"env": {
"ROBLOX_API_KEY": "${input:roblox-api-key}",
"ROBLOX_CREATOR_USER_ID": "<your Roblox user ID>",
"ROBLOX_UPLOAD_ROOT": "<folder that uploads may read from>"
}
}
}
}Where each client keeps the file:
Client | Config file |
Claude Desktop | macOS: |
Cursor |
|
Gemini CLI |
|
VS Code |
|
Sources: Claude Desktop, Cursor, Gemini CLI, VS Code.
All variables are optional: without the key the Open Cloud tools refuse, without ROBLOX_UPLOAD_ROOT every upload is refused, without ROBLOX_PUBLISH_PLACE_IDS every publish is refused, and the other tools work as usual. Do not put the key in any config file that is tracked by version control.
Register Studio's built-in MCP server as well; script editing, screenshots and playtesting live there. The launch command on each platform, StudioMCP on macOS and cmd.exe /c %LOCALAPPDATA%\Roblox\mcp.bat on Windows, comes from Roblox's setup guide.
macOS:
claude mcp add roblox-studio --scope user -- /Applications/RobloxStudio.app/Contents/MacOS/StudioMCPWindows:
claude mcp add roblox-studio --scope user -- cmd.exe /c "%LOCALAPPDATA%\Roblox\mcp.bat"In a JSON config, the macOS entry is "command": "/Applications/RobloxStudio.app/Contents/MacOS/StudioMCP", and the Windows entry is "command": "cmd.exe" with "args": ["/c", "%LOCALAPPDATA%\\Roblox\\mcp.bat"]. Register mcp.bat rather than StudioMCP.exe on Windows: mcp.bat keeps its path, while StudioMCP.exe lives in %LOCALAPPDATA%\Roblox\Versions\version-<hash>\, a folder that changes with Studio updates.
Restart the session after registering, then call roblox_get_status and check openCloudConfigured, allowedUploadRoots and publishPlaceIds.
Desktop apps started from Finder or the Start menu often do not see your shell's PATH; if the server does not start, use the absolute path of npx (or of node when running from source).
Running from source
git clone https://github.com/gjlmotea/studio-sidecar-mcp.git
cd studio-sidecar-mcp
corepack pnpm install --frozen-lockfile
corepack pnpm run build
claude mcp add studio-sidecar --scope user -- "$(which node)" "$PWD/dist/index.js"Register with absolute paths, as above, rather than a hand-written .mcp.json that points at the project folder. Claude Code sets CLAUDE_PROJECT_DIR in the server's environment, not in its own, so a ${CLAUDE_PROJECT_DIR} reference in the command or args of a .mcp.json entry, or of a local- or user-scope entry in ~/.claude.json, needs a default such as ${CLAUDE_PROJECT_DIR:-.}. Without a default the entry still loads, claude mcp list shows a missing-variable warning, and the unexpanded ${CLAUDE_PROJECT_DIR} text is used as is. Project-scope servers from .mcp.json also need approval: interactive sessions ask before using them, while claude -p runs, Agent SDK sessions and cloud sessions load them without asking. See the Claude Code MCP documentation.
Configuration
Variable | Purpose |
| Open Cloud API key (Creator Dashboard → Open Cloud → API Keys). The scopes each tool needs are listed under Open Cloud API key scopes |
| Creator recorded in |
| Folders uploads may read from. Several are allowed, joined with the platform's path delimiter: |
| placeIds |
| Studio executable; resolved automatically from the current installation when unset |
ROBLOX_UPLOAD_ROOT is a deliberate guard rail: uploading sends a local file to an external service, and for Models it cannot be undone, so the MCP client may not pick arbitrary file paths. Both the file and the allowed folders are resolved with realpath before they are compared, so a symlink inside an allowed folder cannot point the upload elsewhere. The file is opened once, checked again through that open handle (still inside an allowed folder, still the same file), and the bytes that go out are read from that handle, so swapping the path after the check changes nothing. A file with more than one hard link is refused, because a hard link inside an allowed folder can be a file from anywhere else on the same volume; upload a copy instead.
ROBLOX_PUBLISH_PLACE_IDS is the PlaceId guard on the Open Cloud side. Publishing replaces what every new server of the game runs, and an API key with universe-places:write can usually publish more than one experience, so a placeId that is not listed is refused before any request is sent.
Open Cloud API key scopes
Create the key at Creator Dashboard → Open Cloud → API Keys. Add the API systems below to Access Permissions, select the experience where the system asks for one, and allow the IP address the server runs from (or 0.0.0.0/0) under Security. An expired or revoked key, or a request from an IP address the key does not allow, returns 401.
Tools | Scope | Creator Dashboard |
|
| assets: Read, Write |
|
| universe-places: select the experience, Write |
|
| assets: Read |
|
| legacy-asset: Manage |
roblox_publish_place with filePath only needs universe-places:write.
Publishing a place
Team Create saves edits to the cloud as saved versions, but live servers only start the published version, and the built-in server has no way to publish. roblox_publish_place does it through Open Cloud:
GET https://apis.roblox.com/assets/v1/assets/{placeId}/versionsfinds the latest saved version (a place is an asset, and its placeId is its assetId). The order of that list is not documented, so it takes the highest version number. If the latest saved version is already the published one, nothing is sent (verdict: already-published).GET https://apis.roblox.com/asset-delivery-api/v1/assetId/{placeId}/version/{n}returns a temporary CDNlocation; the file comes from there, and the API key is never sent to the CDN. The content must start like a place file (<roblox!for binary,<robloxfor XML), or nothing is published.POST https://apis.roblox.com/universes/v1/{universeId}/places/{placeId}/versions?versionType=Publisheduploads those bytes unchanged (application/octet-streamfor.rbxl,application/xmlfor.rbxlx) and returns the new version number.
Pass sourceVersion to publish an older saved version, or filePath to publish a local .rbxl/.rbxlx inside ROBLOX_UPLOAD_ROOT instead. dryRun: true runs the guard, steps 1 and 2 and the format check, and stops before step 3, so it checks a new key without publishing anything (universe-places:write is only exercised by a real publish).
The result is judged by verdict: published comes with versionNumber; failed means nothing was published and names the stage (config, guard, source, list-versions, download, publish), with httpStatus and, on 401/403, the missing requiredScope. 409 means the place is not part of that universe. indeterminate means the publish was accepted but no version number came back; check the version history in Creator Dashboard.
Limitations:
Only what has been saved is published. Edits that Team Create has not autosaved yet, and script drafts that were not committed, are not in the saved version.
Running servers keep the old version until they shut down; use Restart servers in Creator Dashboard to move players over sooner.
Roblox documents that this API does not update
EditableImage,EditableMesh,PartOperation,SurfaceAppearanceorBaseWrapinstances; publish from Studio after changing those.Roblox documents a limit of 30 publish requests per minute per API key owner and does not document a file size limit. The publish endpoint and the version list are marked Beta.
Roblox's place publishing guide now also shows
PATCH https://apis.roblox.com/assets/v1/assets/{placeId}with"published": true. Studio Sidecar uses theuniverses/v1endpoint because it answers with the version number directly.
What the tools know that the built-in server does not tell you
Each of these was found on real hardware and has a regression test.
Launch intent quoting fails silently. Studio expects -launchIntentString "{\"task\":\"EditPlace\",\"universeid\":\"…\",\"placeid\":\"…\"}". Unescaped quotes are eaten by the Windows command-line parser, Studio receives broken JSON, ignores the intent and opens an empty session with no error. The only clue is a window title of Roblox Studio instead of <place name> - Roblox Studio. Studio Sidecar passes arguments to spawn as an array and makes the CommandLineToArgvW rules testable. universeid is the gameId in scene/place.json.
Never steal window focus. Two seconds after SetForegroundWindow, a stray key press in Studio closed the place that had just loaded. roblox_open_place only polls the broker connection and process titles.
The broker must not be killed, and its identity moves. StudioMCP is one broker plus one stub per client; the first one listens on 127.0.0.1:13469, and Studio itself and every stub connect to it. Killing the broker drops every connection, Studio's included. The broker role also moves to a different process over time, so the broker is always identified as "whoever listens on 13469", never by a remembered PID. roblox_cleanup_stubs gives up when it cannot identify the broker. Stubs pile up because some clients do not reap them on exit (see openai/codex#25893); an age threshold keeps cleanup away from sessions that may still be alive.
An update succeeds by revisionId, not by HTTP status. PATCHing an asset with byte-identical content returns HTTP 200, done: true and no error, but revisionId does not move: the content was deduplicated. roblox_update_asset compares revisionId before and after and reports updated, deduplicated or indeterminate. A higher revisionId still does not guarantee that Studio sees the new content; Studio-side caching is outside what Open Cloud can confirm. To replace content, do not send updateMask (updateMask=fileContent fails with Unknown field file_content); it is only for metadata changes.
The 100k truncation belongs to StudioMCP itself. Measured directly against StudioMCP: a 100,000-character reply arrives whole; a 120,000-character one stops at 100,015 characters ending in ... (truncated). No client can lift it. roblox_extract_place therefore uses three safeguards, all required:
A sentinel at the end of every chunk; a chunk without it counts as incomplete. Length alone cannot tell a complete reply from a truncated one.
The UTF-8-safe cut point is decided in Lua and reported back, and the caller continues from there, so the two sides never compute boundaries separately.
A byte-count check per file (
Buffer.byteLength, notString.length). Any mismatch aborts the whole extraction, because a partially correct snapshot looks like success.
Each call stages its data under its own key, shared.RBXEXTRACT_<32 hex digits> (Lua VM memory only, never the DataModel or the .rbxl), and clears it afterwards, so overlapping calls never read each other's data. If clearing fails, the next call that reaches the same Studio clears it again, up to three attempts in all. Every key also records when it was last used, and each new key first clears any key that has sat idle for 15 minutes, so a key left behind by a failed release, a shutdown or a crash goes away on the next call to that Studio. During shutdown the server never reconnects to Studio just to clean up.
Instance names can be almost anything, including .., path separators and Windows device names such as CON or NUL, so every path segment is sanitized. A device name gets a trailing _ (CON becomes CON_.server.luau) on every platform, so a snapshot made on macOS still opens on Windows, and the final path is checked to be inside the output folder before writing.
The whole new snapshot is first written to a fresh folder that the extraction creates inside the output folder (.studio-sidecar-staging-<random>). Only after every file is there does the switch start: the files of the previous snapshot are moved aside, and the new ones are put in place with hard links that fail if the name is taken (or, on file systems without hard links, with exclusive creation). If anything fails during the switch, the new files are removed and the previous snapshot is moved back, so a failed run leaves the previous snapshot as it was. Extractions into the same folder run one after another. A run that is killed midway can leave a .studio-sidecar-staging-* folder behind; it holds only snapshot files and is safe to delete.
Nothing is ever overwritten. When a name is already taken, including by a name the file system treats as equal (Σ, ς and σ on a case-insensitive volume), the script gets the next ~n suffix, and scene/scripts.json records the path actually written. Re-running into the same folder replaces only the files listed in that folder's previous scene/scripts.json; a folder whose scene/ files were not written by Studio Sidecar is refused. If one of those files has been turned into a hard link, only that name is removed, and the content under the other name stays as it was.
The Studio bridge must live as long as the server. Closing it after every call makes the first call succeed and every later one fail with "Unable to reach Roblox Studio right now", which sounds like Studio is closed. It is not: closing kills the stub, and the next call races the broker. The same message also appears during a cold start before a fresh stub reaches the broker, so the bridge retries the Studio listing on that one message, and only that one, for up to 20 seconds; the wait grows by 250 ms per attempt, up to 1.5 seconds. Once the server starts shutting down, the bridge refuses new connections, so a call that is still finishing cannot start a fresh stub.
Open Cloud formats are narrow. Models accept .fbx only; Decals accept bmp, jpeg, png and tga; Audio accepts mp3 and ogg and can only be created, not updated, under a monthly quota. Wrong formats would only fail after polling the operation, so they are rejected locally. Models and Decals have never supported archiving, and roblox_archive_asset says so up front.
Platform support
Windows | macOS | |
Open a given place | ✅ verified | ✅ implemented, not verified on hardware |
Process and broker inspection | ✅ verified (PowerShell) | ✅ verified ( |
Stub cleanup | ✅ verified | ✅ implemented, not verified on hardware |
Window titles | ✅ | ❌ always |
Open Cloud tools | ✅ | ✅ (plain HTTP) |
Platform differences sit behind ports/process-inspector.ts; broker detection, stub classification and cleanup are shared. Linux is refused explicitly rather than returning an empty snapshot, because Roblox Studio does not run there and an empty answer would read as "no Studio running".
Boundaries
No window focus changes. Stealing focus can make a stray key close a freshly loaded place.
roblox_luau_safeis not a new way to run arbitrary code.execute_luauis already in the client's hands; this tool adds a mandatory PlaceId guard, onepcallaround the whole chunk and chunked results in front of the same entry point. Thepcallcatches a runtime error and returns it as a readable value; the code after the error does not run, and changes made before the error are not rolled back. It is not a sandbox.No reverse engineering of the broker's private protocol on 13469. Talking to
StudioMCPas an ordinary MCP client makes that unnecessary.No "remove from inventory". That goes through
inventory.roblox.comwith a signed-in session cookie and CSRF token, which an Open Cloud API key cannot do.Known limitation: a folder that already exists in the output folder can be swapped during the switch. Files are written into the fresh staging folder, but putting them in place still walks the output folder's existing sub-folders by path. Each level is resolved with
realpathand checked to be inside the output folder first; another program on the same machine could still replace a folder with a symlink between that check and the move (time-of-check/time-of-use). The move only ever creates a new name, so at worst one new file appears outside the output folder; nothing outside is overwritten or deleted.Known limitation: an unreleased buffer lasts until the next call. A Luau buffer that could not be released stays in Studio's
sharedtable until a later call reaches the same Studio (and, for the idle sweep, until it has been idle for 15 minutes), or until Studio closes.
Development
corepack pnpm install
corepack pnpm run verify # typecheck, tests, build, stdio smoke
corepack pnpm run verify:live # read-only checks against real Studio and a real account
corepack pnpm run verify:extract # end to end: open_place → place_guard → luau_safe → extract_placeverify:live only performs read-only operations: no uploads, updates, opened places or terminated processes. Its Open Cloud part needs ROBLOX_API_KEY and an asset ID (node scripts/verify-live.mjs <assetId>) and is skipped otherwise.
verify:extract needs ROBLOX_VERIFY_PLACE_ID, ROBLOX_VERIFY_UNIVERSE_ID (a place you own) and ROBLOX_VERIFY_BASELINE (a hand-made, byte-verified script snapshot). It opens a Studio instance and compares the extracted scripts byte for byte against the snapshot. The steps are chained on purpose, because the bridge lifetime bug only shows up across several consecutive tool calls.
License and trademarks
MIT.
Studio Sidecar is an independent open-source project, not affiliated with or endorsed by Roblox Corporation. Roblox, Roblox Studio and Luau are trademarks of Roblox Corporation.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
This server cannot be deployed
Related MCP Connectors
Build and run AI workflows, agents, data tables and 1000+ API integrations from any MCP client.
Official Rewst MCP server: build, run, and debug Rewst workflows, forms, apps, and integrations.
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
Build, test, publish, and run Kovca workflows from AI agents over remote MCP.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceAutomates Roblox Studio tasks via MCP by combining multiple backends for scene editing, script management, asset insertion, and playtesting with safety features like undo recordings and task leasing.33,994 npmMIT
- AlicenseNot gradedqualityCmaintenanceEnables AI tools to read the Roblox Studio game tree, edit scripts, insert models, and run playtests via the Studio MCP interface.37 npmMIT
- AlicenseNot gradedqualityCmaintenanceEnables AI assistants to explore, edit, and automate Roblox Studio sessions locally, including browsing instance hierarchies, reading and modifying scripts, manipulating properties, creating instances, building terrain, and running playtests.1,057 npmMIT
- AlicenseBqualityCmaintenanceEnables a bot to inspect, query, create, edit, and script Roblox Studio instances in token-cheap batches, plus run Luau, playtests, screenshots, and inputs through Roblox's official Studio MCP connection. It runs locally over stdio or an authenticated local HTTP endpoint, returning small structured answers instead of dumping whole games into context.8MIT