見出し、例、コード、表、リンク、参照画像を含む原文を表示しています。
Scenario
Overview
Scenario (scenario.com) generates AI 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, API-key setup for headless use, 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 fails later is re-authenticated per client before anything else is debugged: a server-side authentication fix lands only through a fresh handshake, and diagnostics_run names the failing layer (auth, tenant-scope, api-unreachable, or healthy) with trace ids for support.
The default toolset is wider than the core loop below: asset_get, job_get, jobs_list and models_list are in it too and are called directly, so treat the table as the loop rather than the whole list. ?toolsets=full exposes everything. The catalog tools are the ones outside it (collections, tagging, analysis, training, members, keys): scenario_tools_search with the tool name or plain keywords as query (it takes only query and limit) returns the schema and lane, and the matching scenario_tool_execute_read / write / delete runs it with {name, parameters}, scope ids inside parameters when the target's inputSchema declares them, which nearly every catalog tool does (plan_generation, which takes only description, is the exception), unlike a direct tool's top-level team_id/project_id. The lane is the result's own permission, not what the verb sounds like: asset_download and asset_analyze are both write-class.
Scope first
Resolve scope first, then pass team_id and project_id on every later call. teams_list returns the teams with their projects; projects_list requires a team_id, so it cannot come first. Confirm the pair with the user: a guess writes into someone else's project. A non-interactive run takes the pair from its task instructions; when they name none, stop and list the choices.
The server fills scope in only for read-only tools with one candidate remaining; anything else fails rather than guesses, and the error names which half is wrong (see Errors and recovery). Scope errors are the most common failure here, surfacing mid-session 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 only |
| 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 |
A multi-step request ("product video with voiceover", "concept to 3D") goes to plan_generation (catalog-only, read lane): plain words in description, ordered steps out, each naming a tool and optional model hint; it runs nothing. Single-step: recommend.
Worked example
Generating a stylized game prop image:
recommendwith the user's own words aspromptwhen the need is a capability;searchwithtarget="models",query="flux",public=truewhen you have a name.searchranks by keyword and itsfiltershold no capability key, so a capability-worded query can rank the wrong output type first. For the user's own trained models, omitpubliconsearch(searchhas no private flag); onrecommendthe flag isinclude_private_models: true. Re-discover ids each time: availability differs per team.model_schema_geton the pick: exact field names, types, required flags, defaults, and caps such as the prompt'smax_length(an overrun is a 400, never a trim). File fields take asset ids even when named...Url, andcost_impact: trueflags what moves the price.- If the schema carries
runs_as("lora"or"composition"), never send that model's own id tomodel_run. Itsrun_with.required_argumentsholds the real call:model_idthere is the base model, and itsparameters(thelorasormodelIdwiring) merge into inputs from the same schema. Sendingrequired_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. If cost matters (the default assumption unless the user says otherwise), price first withdry_run: true, a top-level argument besidemodel_id: no job is created and the response'screativeUnitsCostis the exact payload's price; arecommendcost quote assumes defaults, and the schema'scost_impactfields move the real number. Then run: assetids come back, or a `jobidforjobswait`. The `status` beside it is `inprogresswhen the server's wait budget ran out; withwait=falseit is the backend's live word at creation (queued,in-progress,warming-up`), a spelling that is not a different state.jobs_waitwithjob_ids=[...](up to 32); each completed row carriesassetIdsandcuCost, so nojob_getfollow-up; on timeout re-call with the returnedpending_job_idsasjob_ids. Failed jobs are reimbursed, except xAI generations stopped by moderation.asset_displayshows the asset inline; itsformatpicks the rendering:display(the default) returns an inline image plus links and viewer data;viewerreturns a lighter payload for a host that renders the interactive widget;jsonandmarkdownreturn metadata and links.displaydoes not disable a host's widget. For PNG files, useasset_downloadwithformat: "png", one call per asset.asset_downloadreturns a file URL (save withcurl -L, it may redirect).formatis an image conversion (png,webp,jpg, andgifto keep an animated GIF animated: thepngdefault flattens it to one frame) and nothing else; omit it for video, 3D, and audio. Whencurlreportshost_not_allowedorCONNECT tunnel failed, response 403while MCP calls work, check the host's proxy or sandbox egress policy. A CDN 403 alone does not establish the cause; request a fresh download URL before diagnosing it: Sandbox network access names the setting that lifts it, an organization-level allowlist on some hosts that the user may not be able to change. Meanwhile hand over theapp_urlthatasset_displayreturns, where the user downloads directly and, for a mesh, picks the 3D export format the app offers, none of whichasset_downloadconverts to. The signed URL also opens in a browser but expires, so it is a last resort, never the deliverable.
Local inputs go up with upload_asset: always file_name, content_type, and kind (image, audio, video, 3d), plus exactly one of file_size or data, since the call fails without either. Prefer file_size, the file's exact byte count read from disk, and omit data: the reply carries presigned part URLs and instructions; 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_complete with the upload_id. Inline base64 data only under 100KB (that path returns the asset directly, with no complete step); a larger file is rejected naming the cap. Scope rides on both; they take no other fields: no parts list, no etags. The server decodes the file at completion, so Upload upl_… failed: Corrupt JPEG data, premature end of data segment, bad Huffman code or libpng read error means the uploaded image could not be decoded: check for a corrupt source, a truncated or re-encoded body, a missing part, or a mismatched file_size. Confirm the local file opens, its content type matches, and its byte count is correct, then start over with a fresh upload_asset and raw PUTs; never re-complete the same upload_id. Unhandled image format at upload lists what kind: "image" accepts: convert locally first. A host with no shell cannot PUT parts at all; the user uploads in the web app at app.scenario.com and the agent continues from the asset id.
Filing is part of delivering, not a tidy-up: run the catalog tools above with arguments under parameters (never arguments, which the executor drops silently, surfacing as a scope error that is not one). collection_create takes a name and the scope pair only; asset_ids sent there is ignored without an error, so adding is always a second call, collection_add_assets with collection_id and asset_ids, and re-adding a filed asset is a hard 400 naming the duplicate: drop it and continue. Confirm membership with assets_get_bulk and read each record's collectionIds; search filters={"collection_ids": [...]} lags on a fresh write. collection_add_assets takes at most 49 ids per call (past it, 400 You can not add more than 49 assets at once), so chunk a larger set. asset_add_tags is additive, one asset_id per call, so a set is one call per asset, and a tag the asset already carries returns 400 Duplicated tag: read tags off asset_get first and send only the missing ones; skip the write when none are missing.
For reusable templates or 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
| Error | Recovery |
|---|---|
context_missing | Nothing resolved: teams_list, then projects_list |
context_ambiguous | Several fit: present the options; the user picks (non-interactive: task instructions name the pair, else stop and list) |
| 403 Forbidden | Usually wrong scope, not missing: re-check the id pair |
| 403 naming a plan | Surface the upgrade or switch models; retrying never clears it. recommend pre-flags these as requires_plan_upgrade (never run one) unless its response says plan gating is _degraded; then this row is the backstop |
429 with details.actionName = parallel-custom-jobs | Per-team generation concurrency ceiling: keep at most actionLimit jobs in flight, use wait=false for launches and jobs_wait to retire existing jobs before launching more. An immediate retry repeats the error |
| 429 naming a quota, balance, or seat limit | Read the reason and details together, including actionLimit and limitScope when present. Generic plan-limit wording alone does not distinguish concurrency from consumption or feature access. A CU limit does not prove the balance is zero; the requested run may exceed what remains. Stop the affected batch, use usage for consumption, and report the stated remedy. A per-user cap goes to the team admin; a seat-cap error can also block uploads. Do not change billing or membership automatically |
| 429 with an explicit retry delay | Respect the returned delay before retrying; this is not evidence that the user needs an upgrade |
Other 429, including an unfamiliar actionName | Do not classify every other action as a count quota, or infer a training quota's behavior from its name. Report the error and run diagnostics_run in the same scope before choosing recovery |
jobs_wait timeout (in_progress) | Not an error: re-call with the returned pending_job_ids, never a second model_run or a cancel; it takes no timeout argument |
Transport error on model_run (no HTTP status) | The request may still have landed: jobs_list before any re-run, or a lost response becomes a double charge; a dry_run call creates no job and always retries safely |
400 Cannot cancel this type of job | A launched job is committed spend: job_cancel rejects most generation jobs, so plan batches with no abort path |
400 Invalid target format | format converts images only: omit it for video, 3D, audio |
Either 'file_size' … or 'data' is required on upload_asset | Read the byte count from disk and send it as file_size (omit data); inline data is for files under 100KB only |
Upload upl_… failed: Corrupt JPEG data (or libpng read error) | The uploaded image could not be decoded: check the source file, content type, byte count, and part transfers before a fresh upload; re-completing does not repair corrupt bytes |
404 on asset_get | The id does not exist and no retry makes it appear: re-read assetIds off the jobs_wait or job_get row, since a guessed or mistyped id is the usual cause |
400 At least one of query, filter, image, or images must be provided | search never lists bare: a "newest first" asset listing is target="assets", filter="createdAt EXISTS" with sort_by=["createdAt:desc"] |
recommend client-side timeout | It ranks on live data and commonly runs 30 seconds, sometimes over a minute: wait or re-call it; do not fall back to search for a capability |
Common mistakes
- A bare value where the schema says
array: true: silently dropped, the run ignoring your reference or LoRA.asset_geton the output echoes what the run consumed (metadata.referenceImages,parentId), the cheapest proof it was not. - Taking
recommend'sranked[0]blindly: readnext_step.typefirst. Onask_user, present the options; the user's pick wins (non-interactive: task instructions, elseproceed). Onproceed, preferspecialty.model_id, else the firstrankedentry its own text does not mark deprecated, readingtradeoffandexplanationas well ascaveats, since the flag lands in any of them: the ranking is by measured performance, not by lifecycle, so a deprecated member can top it. - Debugging blind: the
diagnoseMCP prompt (ordiagnostics_runwith the scope pair the failure happened under and anymcpt_ids seen asobserved_trace_ids) returns trace ids;usageanswers credit questions. - Guessing a file field's name: 400
Input image is required(orimages,referenceImages,startImage,frontImage,video,model, each a different model's name for the same idea) means the payload never carried the field the schema'srequiredlist names, so copy that name verbatim; a URL in a file field reads as missing too, since file fields take asset ids only. - Feeding an image field a video, audio, or 3D asset: 400
Unhandled image formatlists the image types it accepts.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 withfilters.type(the architecture) ortags, or go throughrecommendfor a capability.- Filtering
models_listbytypewithoutprivacy: "public": a 400 says so. A private listing narrows bystatusormodality, or goes throughsearch(private by default).

