spawnpop MCP server docs
How to connect an AI agent to spawnpop: the endpoint, authentication, every tool with its inputs and outputs, how money is handled, the errors you will see, and how to get the finished files into Roblox Studio. Everything here is taken from the running server.
What it is
spawnpop makes Roblox game assets: a GUI set, icons, a thumbnail, 3D props and music. The MCP server lets an agent do that on a person's behalf. It plans a kit from one sentence, tells the agent the credit price, makes cheap drafts, generates the finals the person approved, and hands back signed download links plus the next calls for Roblox Studio. For the product side, see the connect page and pricing.
| Item | Value |
|---|---|
| Endpoint | https://spawnpop.com/mcp |
| Transport | Streamable HTTP, JSON replies. One JSON-RPC 2.0 message per POST. |
| Sessions | None. The server is stateless, so there is no session id to keep. There are no server-sent events, and batch arrays are rejected. |
| Protocol versions | 2025-11-25, 2025-06-18, 2025-03-26 |
| Methods | initialize, ping, tools/list, tools/call, and notifications (answered with 202). |
| Authentication | Header Authorization: Bearer <key> |
| Request size | Up to 64 KB per request. |
| Without a key | initialize, ping, tools/list and plan_kit only. Everything else returns HTTP 401. |
Get a key
- Open spawnpop.com/mcp and choose Connect. A key is created after a first credit purchase, because a key is how an agent spends credits. Buying as a guest works; you do not need an account.
- The key starts with
sp_liveand is shown once. Copy it straight into an environment variable namedSPAWNPOP_API_KEY. - Choose a read key (can call get_status, get_files and list_projects) or a read and generate key (can also plan, draft, approve and spend credits). Pick read-only for dashboards and monitoring agents.
- An account holds up to 3 keys. Revoke any of them in Settings, API keys; a revoked key stops working at once.
Never commit a key. Put it in an environment variable or a secret store and reference it from the config, as in the snippets below. Keys are stored hashed, so a lost key cannot be shown again; create a new one.
Set up your agent
Set SPAWNPOP_API_KEY in your shell first, then add the server. Each snippet reads the key from the environment, so the key is not written into a file.
claude mcp add --transport http spawnpop https://spawnpop.com/mcp --header "Authorization: Bearer $SPAWNPOP_API_KEY"# ~/.codex/config.toml
[mcp_servers.spawnpop]
url = "https://spawnpop.com/mcp"
bearer_token_env_var = "SPAWNPOP_API_KEY"{
"mcpServers": {
"spawnpop": {
"url": "https://spawnpop.com/mcp",
"headers": {
"Authorization": "Bearer ${env:SPAWNPOP_API_KEY}"
}
}
}
}export SPAWNPOP_API_KEY=<your key> # created on /mcp
# 1. initialize
curl -s https://spawnpop.com/mcp \
-H "Authorization: Bearer $SPAWNPOP_API_KEY" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"my-agent","version":"0.1"}}}'
# 2. list the tools
curl -s https://spawnpop.com/mcp \
-H "Authorization: Bearer $SPAWNPOP_API_KEY" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
# 3. call one (plan_kit also works without the Authorization header)
curl -s https://spawnpop.com/mcp \
-H "Authorization: Bearer $SPAWNPOP_API_KEY" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"plan_kit","arguments":{"description":"a candy pet collecting simulator","genre":"simulator"}}}'Claude Desktop is not supported yet because it requires OAuth sign-in, which this server does not offer. Any client that can send a bearer header to a Streamable HTTP server works. After connecting, ask the agent to run tools/list; it should see the seven tools below.
Tool reference
All tools return both readable text and a structuredContent object. The tables list the fields of that object. The server also sends short instructions in the initialize reply that summarise the workflow, so a well-behaved client already shows them to the model.
plan_kit
Turns a one-sentence game idea into a priced list of assets (GUI, icons, thumbnail, 3D props, music). With an API key the plan is saved as a project you can draft and generate from. Without a key you get the same plan and price plus a link for the human to connect.
| Input | Type | Required | Meaning |
|---|---|---|---|
description | string, 3 to 500 characters | yes | The game idea in plain words. |
genre | one of simulator, tycoon, tower_defense, obby, rng, horror, roleplay, fighting | no | Steers the plan. Left out, the planner picks one. |
style | string, up to 300 characters | no | Art direction, for example "chunky pastel cartoon". It is carried into every later image. |
| Output field | Meaning |
|---|---|
projectId | The project to use in later calls. null when you called without a key. |
genre | The genre the plan was made for. |
theme | What the game is about, extracted from your idea: palette, materials, motifs, logoText, currency, shopItems (name and price), hudLabels. Every later image uses it, so the kit matches. May be absent on older projects. |
items | One row per asset: id, name, assetType, count, credits (price of all copies), generatable and draftable flags. Items with generatable false are listed but not made. A gui_preview item (the themed preview sheet: an optional paid add-on, optional true, addOnFor the GUI kit id, generate only, draftable false) appears when features says it is on. |
features, styles | Only for accounts with the quality features: features says which are on, styles.gameIcon lists the game icon looks you can pass to generate as options.iconStyle. |
totalCredits | Price of generating every generatable item once. |
balance, canAfford | With a key: the current balance and whether it covers totalCredits. |
payUrl | With a key and a short balance: the link to show the human. |
connectUrl, messageForUser | Without a key: where the human connects, and a sentence you can show them as is. |
Errors you can get: forbidden_scope (read-only key), invalid (description refused or malformed), rate_limited (keyless callers).
- Keyless calls are limited to 5 plans an hour per network. A key is not needed to see a price.
- Descriptions that name a known brand or character, or hit the content filter, are refused with invalid before anything is charged.
draft
Makes low-cost preview images with an AI fit check, so finals are only bought for what works. Drafting an item that already has a draft counts as a redraft at the same price.
| Input | Type | Required | Meaning |
|---|---|---|---|
projectId | string | yes | The projectId that plan_kit returned. |
itemIds | array of 1 to 40 item ids | yes | Items from the plan. Items with draftable false (for example music) have no draft step and are refused with invalid. |
variants | 1 or 4, default 1 | no | Drafts per copy. 4 costs four times as much. |
confirmCredits | integer, 0 or more | yes | The exact price you showed the user. If the real price differs, nothing is charged and price_changed comes back. |
requestId | string, 8 to 64 characters of letters, digits, _ or - | yes | Your idempotency key. Use a new one for each new purchase and the same one to retry that purchase. |
| Output field | Meaning |
|---|---|
jobIds | One id per queued draft. |
creditsCharged | What this call took. A replay with the same requestId takes nothing. |
balance | Balance after the call. |
Errors you can get: insufficient_credits, price_changed, invalid, not_found, rate_limited, provider.
- Price = number of copies drafted x variants x the per-draft price. Use that to compute confirmCredits.
approve
Marks one draft per item copy as the reference its final is made from. Use the outputId that get_status shows for the draft.
| Input | Type | Required | Meaning |
|---|---|---|---|
projectId | string | yes | The projectId that plan_kit returned. |
approvals | array of 1 to 40 objects | yes | Each object: itemId (string), copy (integer 0 to 49, default 0), outputId (string, from get_status drafts). |
| Output field | Meaning |
|---|---|
ok | true when all approvals were stored. |
approved | How many approvals were stored. |
Errors you can get: invalid, not_found.
- A draft whose fit check says misfit can still be approved; unjudged never blocks approval.
generate
Starts the final jobs and charges for them. Pass "approved" as itemIds to make every item whose copies all have an approved draft and no final yet. The optional preview sheet is never part of "approved" unless options.includePreview is true.
| Input | Type | Required | Meaning |
|---|---|---|---|
projectId | string | yes | The projectId that plan_kit returned. |
itemIds | array of 1 to 40 item ids, or the string "approved" | yes | What to make. Ids that are not generatable come back in skipped and cost nothing. |
confirmCredits | integer, 0 or more | yes | The exact price you showed the user. If the real price differs, nothing is charged and price_changed comes back. |
requestId | string, 8 to 64 characters of letters, digits, _ or - | yes | Your idempotency key. Use a new one for each new purchase and the same one to retry that purchase. |
options | object, optional: { iconStyle: "avatar_3d" | "cartoon", includePreview: boolean } | no | iconStyle picks the game icon look, only when plan_kit returned styles.gameIcon for the project; it is stored only after the price and balance checks pass. includePreview adds the optional themed preview sheet (priced in plan_kit) to "approved", together with its GUI kit. |
| Output field | Meaning |
|---|---|
jobIds | One id per copy that was started. |
creditsCharged | What this call actually took from the balance. |
skipped | Item ids that are not generatable. |
balance | Balance after the call. |
replayed | true when this requestId had already started these jobs; nothing was charged again. |
Errors you can get: insufficient_credits, price_changed, invalid, not_found, rate_limited, provider.
- To retry items that failed, call generate again with a new requestId. Failed jobs are refunded automatically.
- The same requestId always returns the same jobs, so a lost response is safe to retry.
get_status
Reports every plan item, every draft with its AI fit verdict, the approvals and the balance. waitSeconds holds the call open and returns as soon as anything changes, so the agent does not need to poll fast.
| Input | Type | Required | Meaning |
|---|---|---|---|
projectId | string | yes | The projectId that plan_kit returned. |
waitSeconds | integer 0 to 25, default 0 | no | Maximum seconds to wait for a change. The call can return sooner. Use 25 while jobs run. |
| Output field | Meaning |
|---|---|
items | One row per item: itemId, name, assetType, status, copies, note. status is not_started, drafting, drafted, queued, processing, succeeded, partial or failed. |
drafts | itemId, copy, outputId, status, verdict, reasons, suggestedPrompt and a signed url for each draft. verdict is fit, misfit, unjudged or pending. |
approved | The itemId and copy pairs that have an approved draft. |
summary | Counts of succeeded, failed and running jobs, and mixed (some succeeded, some failed). |
balance | Current credit balance. |
done | true when nothing is running and at least one item succeeded. |
Errors you can get: not_found, forbidden_scope, unauthorized.
- This is the only tool that blocks. Keep the loop going until done is true or summary.running is 0.
get_files
Lists finished files with download URLs that stay valid for 60 minutes, plus a ready next-step block for Roblox Studio and Open Cloud.
| Input | Type | Required | Meaning |
|---|---|---|---|
projectId | string | yes | The projectId that plan_kit returned. |
| Output field | Meaning |
|---|---|
files | itemId, assetType, name, kind (element, image, audio, audio_loop, glb, obj, fbx or kit_zip), url, bytes, width, height, triangles, and bboxStuds for glb models. |
roblox.uploadImages | tool "upload_image" and the image URLs to pass to it. |
roblox.luau | A Luau script that builds a ScreenGui from image asset ids. Ids are placeholders for you to fill. |
roblox.models | For each glb: its file name and an Open Cloud curl template. |
roblox.audio | A note for audio files, or null. |
roblox.safety | Reminder to keep any Roblox key in your own shell. |
Errors you can get: not_found, forbidden_scope, unauthorized.
- URLs are signed for the key owner. Download them in the same session; call get_files again for fresh ones.
list_projects
Your recent projects, newest first. Use it to find a projectId after a restart.
| Input | Type | Required | Meaning |
|---|---|---|---|
limit | integer 1 to 50, default 20 | no | How many projects to return. |
| Output field | Meaning |
|---|---|
projects | projectId, title, genre and createdAt (ISO 8601) for each. |
Errors you can get: forbidden_scope, unauthorized.
The agent workflow
The recommended order keeps the human informed and the spend small:
plan_kitwith the game idea. Show the human the items andtotalCredits.draftthe items for 10 credits per draft image. Each draft comes with an AI fit verdict.get_statuswithwaitSeconds25 until the drafts are ready. Show the human the draft URLs.approvethe drafts that fit. No charge.generatewithitemIds"approved", the confirmed price and a newrequestId.get_statusagain untildoneis true, thenget_files.- Import into Roblox Studio (see below).
Worked example
A person asks for a candy pet simulator. The totals below are computed from the live price list, so they stay correct if prices change. Responses are trimmed to the interesting fields. Ids such as proj_8f3k2 are examples.
{
"name": "plan_kit",
"arguments": {
"description": "a candy pet collecting simulator",
"genre": "simulator",
"style": "chunky pastel cartoon"
}
}{
"projectId": "proj_8f3k2",
"genre": "simulator",
"theme": {
"palette": [
"#ff8fb1",
"#ffd36e",
"#7ad7c1"
],
"currency": "Candy",
"motifs": [
"lollipops",
"striped awnings"
]
},
"items": [
{
"id": "gui-kit",
"name": "Candy GUI set",
"assetType": "gui_set",
"count": 1,
"credits": 120,
"generatable": true,
"draftable": true
},
{
"id": "game-icon",
"name": "Game icon",
"assetType": "game_icon",
"count": 1,
"credits": 40,
"generatable": true,
"draftable": true
},
{
"id": "pets",
"name": "Pet props",
"assetType": "prop_3d",
"count": 2,
"credits": 160,
"generatable": true,
"draftable": true
}
],
"totalCredits": 320,
"balance": 150,
"canAfford": false,
"payUrl": "https://spawnpop.com/mcp/pay?t=<token>"
}The kit costs 320 credits and the plan shows that the balance is lower. Drafts are cheap, so the agent drafts first.
{
"name": "draft",
"arguments": {
"projectId": "proj_8f3k2",
"itemIds": [
"gui-kit",
"game-icon",
"pets"
],
"variants": 1,
"confirmCredits": 40,
"requestId": "candy-drafts-001"
}
}{
"jobIds": [
"job_a1",
"job_a2",
"job_a3",
"job_a4"
],
"creditsCharged": 40,
"balance": 110
}{
"items": [
{
"itemId": "gui-kit",
"name": "Candy GUI set",
"assetType": "gui_set",
"status": "drafted",
"copies": 1,
"note": "Draft ready; approve it, then generate"
}
],
"drafts": [
{
"itemId": "gui-kit",
"copy": 0,
"outputId": "out_77",
"status": "succeeded",
"verdict": "fit",
"reasons": [],
"url": "https://spawnpop.com/api/assets/dl/out_77?u=...&exp=...&sig=..."
}
],
"summary": {
"succeeded": 0,
"failed": 0,
"running": 0,
"mixed": false
},
"balance": 110,
"done": false
}{
"name": "approve",
"arguments": {
"projectId": "proj_8f3k2",
"approvals": [
{
"itemId": "gui-kit",
"copy": 0,
"outputId": "out_77"
}
]
}
}{
"name": "generate",
"arguments": {
"projectId": "proj_8f3k2",
"itemIds": "approved",
"confirmCredits": 120,
"requestId": "candy-final-001"
}
}The balance is a little short for the GUI set, so the call is refused before anything is charged:
{
"isError": true,
"content": [
{
"type": "text",
"text": "Not enough credits: needs 120, balance 110. Show the user payUrl; after payment retry this exact call with the same requestId."
}
],
"structuredContent": {
"code": "insufficient_credits",
"message": "Not enough credits: needs 120, balance 110. Show the user payUrl; after payment retry this exact call with the same requestId.",
"needed": 120,
"balance": 110,
"shortfall": 10,
"payUrl": "https://spawnpop.com/mcp/pay?t=<token>",
"messageForUser": "This needs 120 credits and the account has 110. Open https://spawnpop.com/mcp/pay?t=<token> to add credits (no sign-up needed), then tell me to continue."
}
}The agent shows messageForUser to the human, who opens the link and pays. The agent then repeats step 5 unchanged, with the same requestId:
{
"jobIds": [
"job_b1"
],
"creditsCharged": 120,
"skipped": [],
"balance": 310,
"replayed": false
}{
"files": [
{
"itemId": "gui-kit",
"assetType": "gui_set",
"name": "buy-button.png",
"kind": "element",
"url": "https://spawnpop.com/api/assets/dl/out_91?u=...&exp=...&sig=...",
"bytes": 48211,
"width": 512,
"height": 256
}
],
"roblox": {
"uploadImages": {
"tool": "upload_image",
"urls": [
"https://spawnpop.com/api/assets/dl/out_91?..."
]
},
"luau": "-- builds a ScreenGui from the ids you fill in",
"models": [],
"audio": null,
"safety": "Set ROBLOX_API_KEY in your own shell."
}
}Credits and payments for agents
Credits are bought in advance, never expire, and failed generations are refunded to the balance. The rules an agent must follow:
- confirmCredits. Every spending call (
draft,generate) must carry the exact price. Compute it from thecreditsfield of the plan items, and show it to the human first. - requestId. A string of 8 to 64 letters, digits,
_or-. The samerequestIdreturns the same jobs and charges nothing the second time, so retry freely after a timeout or after the human has paid. Use a new one for each new purchase, including a retry of items that failed. - insufficient_credits. Nothing is charged. The result carries
needed,balance,shortfall,payUrlandmessageForUser. Give the message to the human, wait for them to say they have paid, then repeat the exact same call. The link works without an account and expires after 24 hours. - price_changed. If the price moved between your plan and your call, nothing is charged and
totalCreditsholds the new price. Show it to the human and call again with the new value. - Refunds. A job that fails is refunded automatically;
get_statusshows it as failed. See the refund policy.
Prices
| Asset type | Credits |
|---|---|
| GUI set (a matching set of panels and buttons) | 120 credits |
| Single GUI element | 80 credits |
| Icon pack | 100 credits |
| Game thumbnail | 80 credits |
| Game icon | 40 credits |
| 3D prop (GLB model), per copy | 80 credits |
| Music track | 60 credits |
| Clothing shirt | 80 credits |
| Clothing pants | 80 credits |
| Draft image (any item) | 10 credits |
The tool results always carry the current price, so read it from there instead of copying this table into your agent. The pricing page lists the credit packs.
Rate limits
| Limit | Value | What you get when you hit it |
|---|---|---|
| All calls with one key | 30 a minute | HTTP 429 with a Retry-After header and retryAfterSeconds in the body. |
| Spending calls with one key (plan_kit, draft, generate) | 20 an hour | A tool result with code rate_limited and retryAfterSeconds. |
| Keyless plan_kit from one network | 5 an hour | A tool result with code rate_limited and retryAfterSeconds. |
Limits use fixed windows, and retryAfterSeconds is the time until the window ends. When a provider is briefly unavailable the result has code provider, refunded true and a retryAfterSeconds; wait that long and retry.
Errors
Tool failures come back as a normal result with isError true and a structuredContent.code. Authentication and transport problems are HTTP statuses. Malformed JSON-RPC gets a JSON-RPC error object.
| Code | Where | Meaning | What the agent should do |
|---|---|---|---|
unauthorized | HTTP 401 | The key is missing, wrong or revoked. The body is identical in all three cases. | Stop and ask the human to check the key in Settings. Do not retry. |
forbidden_scope | tool result | The key is read-only and the tool needs generate. | Tell the human to create a key with generate access. |
invalid | tool result | Any tool can return this: a field failed validation, an id is not in the project, or the description was refused. The message names the field. | Fix the input from the message and call again. Nothing was charged. |
not_found | tool result | The project does not belong to this key owner or does not exist. | Call list_projects for a valid projectId. |
insufficient_credits | tool result | The balance is below the price. Fields: needed, balance, shortfall, payUrl, messageForUser. | Show messageForUser (it contains payUrl) to the human and wait. After they pay, repeat the exact same call with the same requestId. |
price_changed | tool result | confirmCredits no longer matches the price. Field: totalCredits. Nothing was charged. | Show the new price to the human, then call again with confirmCredits set to totalCredits. |
conflict | tool result | The request conflicts with the project state. | Call get_status, then decide again. |
rate_limited | tool result or HTTP 429 | Too many calls. Field: retryAfterSeconds. Over the per-key call budget it is HTTP 429 with a Retry-After header; for the spending budget it is the seconds until the hourly window ends; for keyless plan_kit it is always 3600; for a limit hit inside a job it is 60. | Wait retryAfterSeconds, then retry the same call. |
provider | tool result | The work could not run right now. Field refunded is true, so nothing is kept. When the service is temporarily unavailable (for example while prices are being updated) retryAfterSeconds is 60. | Wait retryAfterSeconds if given (otherwise a few seconds), then retry. Reuse the requestId if no job started; call get_status if unsure. |
server | tool result | An error on our side. | Call get_status to see whether work started, then retry with the same requestId. |
parse_error | HTTP 400 | The body is not valid JSON. The reply is a JSON-RPC error object with code -32700. | Fix the JSON and send again. |
method_not_allowed | HTTP 405 | The endpoint only accepts POST. | Send JSON-RPC messages with POST. |
too_large | HTTP 413 | The request body is over 64 KB. | Send a smaller request. |
Using the files in Roblox Studio
get_files returns a roblox block so the agent does not have to guess. In Roblox Studio, open Assistant, then Manage MCP Servers, and enable Studio as an MCP server; add it to the same agent that has spawnpop.
- Images. Pass
roblox.uploadImages.urlsto Studio'supload_imagetool. The URLs are valid for 60 minutes. Each upload gives an asset id. - Place them. Fill the ids into the Luau script from
roblox.luauand run it withexecute_luauin Edit mode. It builds a ScreenGui in StarterGui:
-- Run with execute_luau in Edit mode, after filling the ids from upload_image.
local ids = {
["buy-button.png"] = "rbxassetid://REPLACE_WITH_ID_FOR_BUY_BUTTON_PNG",
}
local gui = Instance.new("ScreenGui")
gui.Name = "SpawnpopKit"
gui.ResetOnSpawn = false
local frame = Instance.new("Frame")
frame.Size = UDim2.fromScale(1, 1)
frame.BackgroundTransparency = 1
frame.Parent = gui
local grid = Instance.new("UIGridLayout")
grid.CellSize = UDim2.fromOffset(96, 96)
grid.Parent = frame
for name, id in pairs(ids) do
local label = Instance.new("ImageLabel")
label.Name = name
label.Image = id
label.BackgroundTransparency = 1
label.ScaleType = Enum.ScaleType.Fit
label.Parent = frame
end
gui.Parent = game:GetService("StarterGui")- 3D models and audio. These go through Roblox Open Cloud. The user runs the curl commands from
roblox.modelsin their own terminal with their own key:
# Keep the key in your own shell. Never paste it into an AI chat.
export ROBLOX_API_KEY=... # created in the Roblox Creator Hub, scoped to asset upload
export ROBLOX_USER_ID=... # the creator account that will own the asset
curl -L -o "pet.glb" "<url from get_files>"
curl -X POST "https://apis.roblox.com/assets/v1/assets" -H "x-api-key: $ROBLOX_API_KEY" \
-F 'request={"assetType":"Model","displayName":"pet","description":"Made with spawnpop","creationContext":{"creator":{"userId":"'"$ROBLOX_USER_ID"'"}}};type=application/json' \
-F "fileContent=@pet.glb;type=model/gltf-binary"
# The reply holds an operation path. Poll GET https://apis.roblox.com/assets/v1/operations/<id> until done, then read assetId.Never paste your Roblox API key into an AI chat, and never send it to spawnpop. spawnpop does not ask for one. Keep it in $ROBLOX_API_KEY in your own shell. Roblox sets its own limits and review rules for uploaded audio and models; check them in the Creator Hub before you upload.
FAQ
Do I need an API key to try the MCP server?
Not to see a price. plan_kit works without a key and returns the plan, the credit price and a link for the human to connect. Every other tool needs a key, and a key is created after a first credit purchase. A person can hold up to 3 active keys.
Which transport and protocol versions does the server speak?
Streamable HTTP with plain JSON replies: each request is a POST of one JSON-RPC 2.0 message and gets one JSON response. There are no sessions and no server-sent events, and batch requests are not supported. Protocol versions 2025-11-25, 2025-06-18, 2025-03-26 are accepted; an unknown version is answered with 2025-11-25.
Can my agent spend credits without the user knowing?
No. Every spending call must repeat the exact price in confirmCredits, which the agent should have shown the human first, and a key can be limited to read-only. If the price moves, nothing is charged and the agent gets price_changed. The human can revoke a key at any time in Settings.
What happens if a call times out or the agent retries?
Retry with the same requestId. The same requestId returns the same jobs and charges nothing a second time. Use a new requestId only when you really want to buy again, for example to remake items that failed.
What does the agent do when the balance runs out?
It receives insufficient_credits with a payUrl. The agent shows that link to the human, who can pay without creating an account, and then repeats the same call with the same requestId. The link names the account to credit and expires after 24 hours.
How do the generated files get into Roblox Studio?
get_files returns signed download URLs and the exact next steps. Images go through the upload_image tool of Roblox Studio's own MCP server, and a Luau snippet places them in a ScreenGui. 3D models go through Roblox Open Cloud with a key that stays in your own shell. spawnpop never asks for a Roblox key.
More questions? See help, security or the GUI generator for what the assets look like. The genres you can pass to plan_kit are simulator, tycoon, tower_defense, obby, rng, horror, roleplay, fighting.