Build with romcp.
Connect your AI tools to Roblox Studio. Inspect your game, make changes, and check the result.
These docs cover the v3 development build. The public npm download is v2.11.4 and does not yet include the v3 tools or SDK examples below.
Overview
romcp is an MCP server and Roblox Studio plugin. Your MCP client supplies the AI model; romcp gives it tools for instances, scripts, building, UI, output, and testing. Open Cloud tools connect to Roblox services using your own scoped API key.
romcp does not require a separate AI API key. Keep your existing Codex, Claude Code, Cursor, or other MCP client setup.
Installation
Install Node.js 20 or newer and pnpm, then run:
pnpm install -g dominus-cli@latest
dominus setup
The guided setup installs the Studio plugin, provisions a local bridge credential, and helps configure supported MCP clients. Open Roblox Studio and your experience, then check the plugin’s connection panel. Reload Studio if the new plugin does not appear.
Check the installation or update it later:
dominus doctor
dominus update
The v3 build also provides romcp, romcp-mcp, and
romcp-install-plugin aliases. Existing Dominus commands and configuration
names remain supported during the transition.
Working in Studio
- Ask your AI client to check
dominus_status. -
If several Studio sessions are open, select the intended one with
dominus_select_studio. - Inspect a narrow part of the scene before editing it.
- Check the resulting instances, output, or playtest evidence.
Inspect the models under Workspace.Map, then identify
unanchored decorative parts. Show me the proposed changes.
Use instance references returned by the tools instead of guessing paths. Prefer batched instance operations when changing several objects. Arbitrary Luau execution requires the plugin’s explicit execution toggle; enable it only for clients you trust.
Open Cloud keys
Create a key in Roblox Creator Dashboard with access to the intended experience or creator. Permissions vary by operation. Never paste keys into game scripts, public repositories, or screenshots.
Ask your client to run show_dominus_ui. In Open Cloud,
enter the key and choose Save encrypted key. Encrypted storage
currently uses Windows DPAPI for your Windows account. On macOS and Linux, use an
environment variable in the process that launches romcp:
# PowerShell — use your actual key locally
$env:ROBLOX_OPEN_CLOUD_API_KEY = 'your-key'
Credential priority is environment variable, encrypted store, then legacy
openCloudApiKey configuration. Removing a saved key can reveal an older
configured source. Saving verifies local storage, not Roblox permissions or key
validity.
| Operation | Permission |
|---|---|
| Read saved player fields | universe-datastores.objects:read |
| Read analytics | universe.analytics:read |
| Assets and commerce | Grant the operation-specific permissions shown by Roblox’s key editor. |
API & SDK
The developer API runs locally and is disabled by default. Build from source with
pnpm install --frozen-lockfile and pnpm build. Start a fresh
bridge with a separate API token and a free port:
$env:DOMINUS_API_PORT = '18090'
$env:DOMINUS_API_TOKEN = node -e "process.stdout.write(require('node:crypto').randomBytes(32).toString('base64url'))"
node dist/bridge-daemon.js
Install the package in your development project to use the SDK. The package root starts
MCP; import the /sdk subpath for developer code:
import { createClient } from 'dominus-cli/sdk';
const client = createClient({
baseUrl: 'http://127.0.0.1:18090/v1',
token: process.env.DOMINUS_API_TOKEN,
timeoutMs: 30000,
});
console.log(await client.capabilities());
const connections = await client.studios.list();
// Choose an exact connection ID from this list.
const studio = await client.studios.connect({
connectionId: process.env.DOMINUS_CONNECTION_ID,
});
const tree = await studio.scene.tree({ maxDepth: 2, maxNodes: 200 });
The API accepts non-browser clients on 127.0.0.1 only. Keep its token
separate from both the Studio bridge token and your Open Cloud key. It is not an
internet-hosted API or a Roblox runtime endpoint.
| Route | Purpose |
|---|---|
GET /v1/capabilities |
Available API operations |
GET /v1/studios |
Connected Studio sessions |
POST /v1/studios/{id}/scene/tree |
Bounded scene hierarchy |
POST /v1/studios/{id}/scene/inspect |
Instance properties |
POST /v1/studios/{id}/scene/selection |
Selected instances |
POST /v1/studios/{id}/output |
Incremental logs |
POST /v1/studios/{id}/tests/{action} |
start, status, stop, or context |
POST /v1/cloud/data-stores/read |
Selected saved-data fields |
POST /v1/cloud/analytics/read |
Metric query or operation check |
POST /v1/cloud/analytics/dimensions |
Discover dimension values or check a discovery operation |
All routes require Authorization: Bearer <API token>. POST bodies use
application/json. Responses contain data or
error, plus a requestId. Requests are limited to 32 KiB,
responses to 1 MiB, and concurrency to eight. Reconnect using a newly discovered Studio
ID after the target disconnects.
Create products together
Use roblox_create_developer_products for up to 30 products in one call. It
checks the catalog once and returns an ID and status for each item. Existing names are
reused without changing their settings.
{
"universeId": 123,
"products": [
{ "name": "100 Coins", "price": 10 },
{ "name": "500 Coins", "price": 40 }
],
"confirm": true
}
If any item has status unknown, inspect the catalog before retrying.
Previously created products remain created. Use returned product IDs when wiring offers
into your game.
Developer-product updates
Use roblox_update_developer_product to change a product’s price, sale
availability, regional pricing, or store-page visibility. Supply an explicit universe
and product ID. Only the fields in changes are updated.
{
"universeId": 123,
"productId": 456,
"changes": { "price": 99, "isForSale": true },
"confirm": true
}
The tool reads the configuration back after updating it. Check both
applied and verified; if verification fails, inspect the
product before repeating the update. This operation requires both developer-product read
and write permissions. Regional pricing is separate from the game’s Managed Pricing
enrollment flow.
Scheduled discounts
From your own code, use client.discounts.schedule(), list(),
cancel(), and resolve(). The HTTP equivalents are POST
/v1/cloud/discounts/schedule, /list, /cancel, and
/resolve under the same prefix.
const { job } = await client.discounts.schedule({
plan: {
universeId: 123, productId: 456,
originalPrice: 100, discountPrice: 50,
startsAt: '2030-10-01T00:00:00Z',
endsAt: '2030-10-02T00:00:00Z',
},
confirm: true,
});
const page = await client.discounts.list({ offset: 0 });
// Cancel before execution, using the latest job revision:
await client.discounts.cancel({
id: job.id, revision: job.revision, confirm: true,
});
Choose your own future dates. Listing returns up to 50 jobs plus
offset and total; increase the offset by the number returned
to read the next page. After a timeout or error, inspect the job list and product before
retrying. A lost response can still leave a saved job or completed price change.
Schedule a developer-product discount with roblox_schedule_discount.
Provide the universe and product IDs, expected original price, lower discount price,
start and end timestamps with a UTC offset, and confirm: true.
The local bridge must stay running to apply and restore prices. Open the control panel
with show_dominus_ui and select Refresh jobs under
Scheduled discounts to inspect prices, times, and execution notes. You
can cancel jobs that have not started. Active and uncertain jobs may still need price
restoration.
To schedule visually, expand Schedule a discount in the control panel, enter the product and universe IDs, prices, and both times in UTC, then select Schedule discount and restoration. For a job needing attention, use Close if original price is present or the button showing the original price to restore it now. romcp checks the current product price and job revision before proceeding.
If a job needs attention, ask your agent to inspect its ID and current product price
before using roblox_resolve_discount. A completed request or a saved
schedule is not proof that a future price change has already run.
Player data
Use roblox_read_data_store or the SDK to read exact fields from one entry.
Supply the universe, store, scope, key, and field paths:
const save = await client.dataStores.read({
universeId: 123,
dataStore: 'PlayerData',
scope: 'global',
key: 'Player_42',
fields: [['Receipts', 'purchase-id'], ['Inventory', '0']],
});
Replace the example target with your game’s actual schema. Array indexes are zero-based
strings. Missing fields return found: false; saved false and
null stay distinct. Available revision metadata accompanies the selected
values.
Saved data is evidence from your game, not Roblox transaction history. A missing receipt does not prove that a player never purchased something. To verify a purchase, interpret the record using your game’s receipt schema and fulfillment logic. This operation reads selected fields from one entry; it does not modify saved data.
Analytics
Use roblox_read_analytics or client.analytics.read() for
aggregate metrics. Each call sends one request:
const result = await client.analytics.read({
universeId: 123,
query: {
metric: 'DailyActiveUsers',
granularity: 'OneDay',
startTime: '2026-01-01T00:00:00Z',
endTime: '2026-02-01T00:00:00Z',
},
});
// If pending, check later using the returned ID.
if (!result.operation.done) {
const status = await client.analytics.read({
universeId: 123,
operationId: result.operationId,
});
}
Check operation.done and operation.error before using results.
The end time is exclusive. Projected values can change; statistically insignificant
samples do not establish regressions. Text metrics use stringValues. Avoid
resubmitting a pending query.
Discover filter values
Use roblox_discover_analytics_dimensions or the SDK to find dimension IDs
before filtering a metric. Keep the returned value as the filter ID;
displayValue is only a label.
const discovery = await client.analytics.dimensions({
universeId: 123,
query: {
metric: 'DailyActiveUsers',
dimensions: ['Country'],
startTime: '2026-01-01T00:00:00Z',
endTime: '2026-02-01T00:00:00Z',
limit: 10,
},
});
If the discovery is pending, call client.analytics.dimensions() again with
its universeId and operationId. Dimension discoveries use a
different operation route from metric queries.
UI to code
studio_export_ui captures a supported UI tree and returns React Luau or
plain Luau code. The SDK also exports the pure convertUiSnapshot converter.
Review warnings for omitted nodes or properties.
The converter preserves supported visual properties, layout objects, and styled values. It does not infer event handlers, gameplay behavior, or React state. Choose React Luau for a Roblox React project, or plain Luau for an Instance-based UI. Check the generated result in Studio.
Local source & sync
For filesystem-backed projects, set DOMINUS_PROJECT_ROOT in the MCP server
environment. Add exact source ownership mappings to .dominus/project.json:
{
"version": 1,
"placeId": 123456789,
"mappings": [{
"pathSegments": ["ServerScriptService", "Main"],
"owner": "filesystem",
"file": "src/server/main.server.luau",
"provider": "rojo"
}]
}
Edit mapped source with your editor’s filesystem tools, then verify sync in Studio. Matching script reads omit duplicate source by default. Diverged copies include conflict evidence. Generated files should be changed through their generator. Keep your existing sync process running. Resolve conflicts in the source you own before syncing again.
Building preferences
Use studio_build_style or the local control panel to save place-specific
material and surface defaults. These defaults apply to supported part creation and
procedural building; explicit properties in the current request take priority.
This helps retain choices such as studded surfaces between build requests. It does not retroactively restyle existing geometry or guarantee a particular visual result. Inspect the finished scene and iterate on specific objects.
Playtesting
studio_test_session and the SDK provide managed start, status, stop, and
context operations:
const started = await studio.tests.start({
mode: 'play', timeoutMs: 120000,
});
if (started.success) {
console.log(await studio.tests.status(started.runId));
}
A started session is not proof that gameplay is ready or a test passed. Inspect phase, failure evidence, and cleanup status. Stop must target the matching server runtime connection; final status comes from the originating edit connection. Check session context and your scenario’s readiness conditions before sending gameplay input.
Use studio.output.read() with the previous nextCursor for
incremental logs. Results report eviction gaps. A timeout in your HTTP client does not
cancel an already dispatched Studio operation.
Profiling
studio_profile reports availability and can summarize existing
MicroProfiler frame data when the optional Roblox LibMP dependency is installed in the
plugin. It reports CPU/GPU timing distributions and slow frames.
The profiler requires Roblox’s LibMP dependency in the plugin. Check availability before requesting a snapshot. Frame timing identifies slow intervals; investigate the relevant scopes in Roblox’s profiler to locate the cause.
Troubleshooting
Studio is disconnected
Run dominus doctor, open the plugin’s connection panel, and reload Studio
after an update. Run dominus setup again if the plugin or local
credential is missing. Check that your MCP client starts the installed romcp server.
A tool changed the wrong experience
Check connected sessions and explicitly select the intended Studio before continuing. SDK handles retain the exact selected connection ID and do not follow changes to the active Studio.
Open Cloud returns a permission error
Check the active credential source, expiry, intended universe, and operation permissions. A successfully saved key has not necessarily been verified against Roblox.
The SDK reports INVALID_REQUEST or CLOUD_ERROR
For INVALID_REQUEST, check the input schema and limits.
CLOUD_ERROR reports a failed upstream read; inspect the sanitized
message. Missing saved data is not proof of no purchase.
A result is too large
Reduce scene depth, choose fewer fields, narrow the date range, or remove breakdowns. romcp applies explicit response limits to keep results manageable.