scenario
Use when connecting an AI agent to Scenario (scenario.com) through MCP, or when a task involves generating images, video, 3D, audio, sprites, textures, or game assets. Also when picking a Scenario mod
Files3
View on GitHubSKILL.mdskills/scenario/SKILL.md
GitHubScenario
Overview
Scenario generates images, video, 3D, and audio across 500+ models, plus custom training, all through the core loop below.
Setup
- Endpoint:
https://mcp.scenario.com/mcp(Streamable HTTP). Prefer OAuth: no credentials pass through the conversation. - Client config, headless API-key setup, and per-client re-authentication: references/setup.md.
- Never ask an agent to collect, encode, or echo a secret.
- A connection that authenticated once and now fails: re-authenticate its client before debugging
anything else.
diagnostics_runnames the failing layer (auth,tenant-scope,api-unreachable, orhealthy).
Tools come in two kinds:
- Direct: the loop below, plus
asset_get,job_get,jobs_list, andmodels_list.?toolsets=fullexposes everything. - Catalog: the rest (collections, tagging, analysis, training, members, keys).
scenario_tools_searchtakes onlyquery(a tool name or keywords) andlimit. It returns the schema and the lane.scenario_tool_execute_read/write/deleteruns the tool with{name, parameters}.- Every input goes inside
parameters, scope ids anddry_runincluded. Nearly every catalog tool declares scope ids;plan_generationtakes onlydescription. - Direct tools take scope, and
model_runitsdry_run, at top level. - The executor drops a misplaced key silently:
argumentssurfaces as a false scope error, and a top-leveldry_runruns the job, billed. - The lane is the result's own
permission, not the verb:asset_downloadandasset_analyzeare write-class.
Scope first
- Resolve scope first, then pass
team_idandproject_idon every call. teams_listreturns the teams with their projects.projects_listneeds ateam_id, so it cannot come first.- Confirm the pair with the user: a guess writes into someone else's project. Unattended, take the pair from the task; if it names none, stop and list the choices.
- The server fills scope only for read-only tools with one candidate left. Anything else fails, and the error names the wrong half.
- Scope errors are the most common failure. They surface on the first call that drops the pair once a second team or project is in play.
Quick reference
| Step | Tool | Notes |
|---|---|---|
| Resolve scope | teams_list, then projects_list |
Once per session; pass the ids on every call |
| Find a model | search or recommend |
Free; recommend for a capability, search for a name |
| Get the schema | model_schema_get |
Always before model_run; check runs_as and caps |
| Generate | model_run |
Schema-conformant parameters; dry_run for cost |
| Wait | jobs_wait |
Whenever model_run returns a job_id without assets; never loop job_get |
| View / save | asset_display / asset_download |
Never paste raw asset URLs; format converts images, meshes |
| Inspect an asset | asset_get |
Free; dimensions, duration, firstFrame / lastFrame ids |
| Upload inputs | upload_asset + upload_asset_complete |
Local files become asset_ids |
| Refine a prompt | prompt_spark |
Advisory rewrite; needs model_id |
| Quota / debugging | usage, diagnostics_run |
CU consumption; diagnose MCP prompt |
| Saved preferences | memory_recall |
OAuth only; before generating in a project, again after switching |
- Multi-step request ("product video with voiceover", "concept to 3D"): catalog
plan_generation(read lane). Plain words indescription, ordered steps out, each naming a tool and an optional model hint. It runs nothing. A single step goes torecommend. - Stated preference ("always 9:16"): catalog
memory_set(delete lane),scopeproject_user_memory(user_memoryacross projects). It replaces the whole layer:memory_getand merge first.
Worked example
Generating a stylized game prop image:
- Discover the model:
recommendwith the user's own words aspromptfor a capability.searchwithtarget="models",query="<model name>",public=truefor a known name. It ranks by keyword and itsfiltershold no capability key, so a capability-worded query can rank the wrong output type first.- The user's own trained models: omit
publiconsearch(it has no private flag); passinclude_private_models: trueonrecommend. - Re-discover ids each time: availability differs per team.
model_schema_geton the pick: exact field names, types, required flags, defaults, and caps.- Overrunning a cap such as the prompt's
max_lengthis a 400, never a trim. - File fields take asset ids, even when named
...Url. cost_impact: trueflags the fields that move the price.
- Overrunning a cap such as the prompt's
- If the schema carries
runs_as("lora"or"composition"), never send that model's id tomodel_run. Itsrun_with.required_argumentsholds the real call:model_idthere is the base model, and itsparameters(thelorasormodelIdwiring) merge with your inputs from the same schema.required_argumentsalone discards your prompt. - Optional:
prompt_sparkrewrites a thin prompt into an on-model one. Pass the discovered id (a LoRA's own, not its base) and the draftprompt. Skip deliberate prompts. model_runwithmodel_idand schema-conformantparameters.- Unless the user says cost does not matter, price first with
dry_run: true, top level besidemodel_id. No job is created, andcreativeUnitsCostis this exact payload's price. - A
recommendquote assumes defaults;cost_impactfields move the real number. - Then run. Assets come back inline (
status: success, nothing to wait for), or ajob_idforjobs_wait. - A job's
statusisin_progressafter a timed-out wait, or withwait=falsethe backend's word at creation (queued,in-progress,warming-up). All mean the same state.
- Unless the user says cost does not matter, price first with
jobs_waitwithjob_ids=[...](up to 32).- Each completed row carries
assetIdsandcuCost: nojob_getfollow-up. - A timeout (
in_progress) is not an error: re-call with the returnedpending_job_idsasjob_ids, never a secondmodel_runor a cancel. It takes no timeout argument. - Failed jobs are reimbursed, except xAI generations stopped by moderation.
- Each completed row carries
asset_displayshows the asset inline. Itsformatpicks the rendering:display(default): an inline image, links, and viewer data. It does not disable a host's widget.viewer: a lighter payload for a host that renders the interactive widget.json,markdown: metadata and links.
asset_downloadreturns a file URL, one call per asset; save it withcurl -L(it may redirect).formatconverts images (png,webp,jpg;gifkeeps an animated GIF animated, while thepngdefault flattens it to one frame) and meshes (glb,fbx,obj). Omit it for video and audio, or it fails with 400Invalid target format.curlfailing withhost_not_allowedorCONNECT tunnel failed, response 403while MCP calls work points at the host's egress policy. A CDN 403 alone does not: fetch a fresh URL first.- Sandbox network access names the setting. On some hosts it is an organization allowlist the user may not control.
- Meanwhile hand over
asset_display'sapp_url. The user downloads there and, for a mesh, picks an export formatasset_downloaddoes not offer. - The signed URL expires: a last resort, never the deliverable.
Local inputs go up with upload_asset:
- Always
file_name,content_type, andkind(image,audio,video,3d), plus exactly one offile_sizeordata. Without either it fails withEither 'file_size' … or 'data' is required. - Prefer
file_size, the exact byte count read from disk, and omitdata. The reply carries presigned part URLs andinstructions. - PUT each part's raw bytes to its URL with no added headers: a checksum header makes the store
answer 403. Check every PUT returned 200, then
upload_asset_completewith theupload_id. - Inline base64
dataonly under 100KB. It returns the asset directly, with no complete step; a larger file is rejected, naming the cap. - Both calls take scope and nothing else: no parts list, no etags.
- The server decodes the file at completion.
Upload upl_… failed: Corrupt JPEG data,premature end of data segment,bad Huffman code, orlibpng read errormeans it could not.- Check for a corrupt source, a truncated or re-encoded body, a missing part, or a wrong
file_size. - Confirm the local file opens under its declared content type, then start over with a fresh
upload_assetand raw PUTs. Never re-complete the sameupload_id.
- Check for a corrupt source, a truncated or re-encoded body, a missing part, or a wrong
- A phone's HEIC photo uploads as is (
content_type: "image/heic"): do not convert it.Unhandled image formatlists whatkind: "image"accepts; convert any other type locally. - A host with no shell cannot PUT parts. The user uploads at app.scenario.com and the agent continues from the asset id.
Filing is part of delivering, not a tidy-up:
collection_createtakes a name and the scope pair only.asset_idssent there is silently ignored, so adding is always a second call:collection_add_assetswithcollection_idandasset_ids.- It takes any number of ids (it batches them by 49 itself) and skips an id already filed, listing
it under
already_present_asset_idsbesideadded_asset_ids. - So re-sending the whole list after a failed batch is safe: the error says how many went in, and the retry adds the rest.
- Confirm from that reply:
searchfilters={"collection_ids": [...]}lags on a fresh write. asset_add_tagsis additive, oneasset_idper call. A tag the asset already carries returns 400Duplicated tag: readtagsviaassets_get_bulkfirst and send only the missing ones.
Reusable templates and reference content follow the shared asset lifecycle: resolve existing assets, stage new versions, publish only through a supported operation, and verify public access before recording public ids. Uploading and filing alone are not publication.
Errors and recovery
context_missing: nothing resolved.teams_list,projects_listif necessary.context_ambiguous: several fit. Present the options; the user picks (unattended: the task names the pair, else stop and list).- 403 Forbidden: usually wrong scope, not missing; re-check the id pair. A private asset input
reads only from its own project:
- ask which project holds it (unattended: the task names it, else stop);
- run catalog
asset_copy(write lane) with that project asproject_id, plusasset_idandtarget_project_id; - use the returned id.
asset_copycopies only between projects of one team.
- 403 naming a plan: surface the upgrade or switch models; a retry never clears it.
recommendpre-flags these asrequires_plan_upgrade(never run one) unless its response says plan gating is_degraded; then this entry is the backstop. - 429 with
details.actionName=parallel-custom-jobs: the per-team generation concurrency ceiling. Keep at mostactionLimitjobs in flight: launch withwait=false, and retire jobs withjobs_waitbefore launching more. An immediate retry repeats the error. - 429
custom-generation, or naming a quota, balance, or seat limit:actionLimitis the period's CU allowance (Free: 50 a day),actionValuewhat is already consumed. A run priced above the gap is refused, so a lowactionValuemeans this payload costs too much, not that credits ran out.- Stop the batch,
dry_runthe payload, offer a cheaper member or fewer outputs, or report the reset or upgrade (unattended: report and stop). usagetakes the sameproject_id(orscope: "team").- A per-user cap goes to the team admin; a seat-cap error can also block uploads. Never change billing or membership yourself.
- 429
Too Many Requests. Please, wait N seconds(remainingSeconds): Relax mode. The plan's CU are spent, so each job is followed by a wait.- Never re-submit inside
remainingSeconds. - Stop the batch and invite the user to buy CU (a top-up ends Relax mode at once) or upgrade; else offer a cheaper member, fewer outputs, or a later run (unattended: report and stop, never relaunch).
- Never re-submit inside
- Other 429, including an unfamiliar
actionName: never infer its behavior from the name. Report it and rundiagnostics_runin the same scope before choosing a recovery. - Transport error on
model_run(no HTTP status): the request may still have landed. Runjobs_listbefore any re-run, or a lost response becomes a double charge. Adry_runcreates no job and always retries safely. - 400
Cannot cancel this type of job: a launched job is committed spend.job_cancelrejects most generation jobs, so plan batches with no abort path. - 404 on
asset_get: the id does not exist, and no retry makes it appear. Re-readassetIdsoff thejobs_waitorjob_getrow: a guessed or mistyped id is the usual cause. - 400
At least one of query, filter, image, or images must be provided:searchnever lists bare. A newest-first asset listing istarget="assets",filter="createdAt EXISTS",sort_by=["createdAt:desc"]. recommendclient-side timeout: it ranks on live data and commonly runs 30 seconds, sometimes over a minute. Wait or re-call it; never fall back tosearchfor a capability.
Common mistakes
- Pricing a later step with a placeholder id (
asset_placeholder): file ids resolve before pricing, so thedry_runreturns 404 and quotes nothing. Quote it fromrecommend, thendry_runonce the real asset exists. - A bare value where the schema says
array: true: silently dropped, so the run ignores your reference or LoRA.asset_geton the output echoes what the run consumed (metadata.referenceImages,parentId): the cheapest proof. - Taking
recommend'sranked[0]blindly. Readnext_step.typefirst:ask_user: present the options; the user's pick wins (unattended: the task, elseproceed).proceed: preferspecialty.model_id, else the firstrankedentry not marked deprecated in itscaveats,tradeoff, orexplanation. The ranking measures performance, not lifecycle.
- Debugging blind: the
diagnoseMCP prompt returns trace ids, as doesdiagnostics_runwith the failing call's scope pair and anymcpt_ids seen asobserved_trace_ids.usageanswers credit questions. - Guessing a file field's name: 400
Input image is required(orreferenceImages,startImage,frontImage,video: each model names its own) means the payload lacks the schema'srequiredfield. Copy its name verbatim. A URL there reads as missing: file fields take asset ids only. - Feeding an image field a video, audio, or 3D asset: 400
Unhandled image formatlists the accepted types.asset_getreports the asset'smimeType; pull a still with the clip'sfirstFramewhen the model wants an image. filters.kindonsearchwithtarget="models": a 400"kind" is not a filterable field, sincekinddescribes assets. Narrow models byfilters.type(architecture) ortags, orrecommenda capability.- Filtering
models_listbytypewithoutprivacy: "public": a 400 says so. A private listing narrows bystatusormodality, or goes throughsearch(private by default).