Help

Troubleshooting

Symptom-to-remedy for connection, authentication, Chromium, payload size, missing screenshot cells, readiness, cloud credits, reconnecting to production and expired refs.

View as Markdown

Organised by what you saw, not by which subsystem produced it. The refusals below are eight different conditions with eight different remedies, and the fastest way to waste an afternoon is to treat one as another.

#The client cannot connect

The MCP client shows the server as failed, or lists no tools.

On your own machine, the server is a stdio process. The usual causes, in the order worth checking:

  • The client is not running the command it thinks it is. claude mcp add shotops -- npx -y shotops-mcp is the whole registration; a wrapper script that prints anything to stdout will break it.
  • Something else wrote to stdout. Once the stdio transport is connected, stdout belongs to JSON-RPC and any human-readable line corrupts the stream. This is why the CLI subcommands (login, whoami, release …) run instead of the server rather than alongside it.
  • The process is fine and slow. Chromium boots lazily on the first render, not at startup, so a slow start is not this.

On ShotOps Cloud, the server is an HTTPS endpoint. GET /health is an unauthenticated liveness probe — if that answers and your client does not, the problem is the client's configuration or its credential, not the service. In ChatGPT, create a custom app, scan the endpoint and complete its OAuth flow. Do not paste a personal token into the app definition. A successful render or bundle has a structured model-readable result and may also open ShotOps's panel or bundle UI when the client supports MCP Apps.

#Authentication is refused

Three distinct things wear this shape.

#The tool needs an account and there is none

The message names the tool: "save_project needs a ShotOps account." On the hosted endpoint, the anonymous surface is limited to account_status, import_screenshot and render_strip. Everything else returns the sign-in remedy. Connect the account in the client that opened ShotOps, or set a token.

#The token was revoked, or belongs elsewhere

npx shotops-mcp whoami answers this in one call: a token that no longer resolves prints that it was revoked or belongs to another server. On the hosted endpoint a credential that does not resolve gets a 401 invalid_token — deliberately, so that a revoked token cannot quietly degrade into the anonymous lane while the user believes they are signed in. Create a fresh token in the Studio app under the account menu → API tokens.

#The credential is fine and the capability is refused

That is not this section. See The preview worked and store-ready did not.

#Chromium is missing

The first render fails with a Playwright browser error. Chromium is about 150 MB and is not bundled into the npm package. Playwright downloads it on first run; install it explicitly if that did not happen:

npx playwright install chromium

Nothing else in the server needs it — startup, account_status, read_project and save_project all work without a browser, which is why this only ever surfaces on a render.

#The payload is too large

"payload too large (3.2MB inline, cap 3MB)" — you inlined screenshot bytes. The decoded cap is 3 MB on every deployment and a real full-resolution screenshot is 3–4 MB, so this is the expected outcome rather than an edge case.

The call vanished with no message at all, or a bare HTTP 413. Far enough over the cap, the host discards the request at the edge before the server ever sees it. Do not wait to be told.

The remedy is the same in both cases: stop inlining. Import by URL, or mint slots with request_screenshot_upload and PUT the bytes, then pass { "ref": … } entries. On the local server, pass { "path": … } and nothing moves at all.

An anonymous hosted connection has a much tighter ceiling — 1 MB of wire bytes per request — for the same reason: that tier is meant to be driven by refs.

#A screenshot cell came back empty

A preview or render showed no device for one panel, or a bundle refused because a source was missing.

Preview is deliberately permissive so you can inspect incomplete work. Final output is not: it reports the same gap and stops before delivery. Look at the response.

  • shots[] carries one row per shot with matched, the requestedVariant, the resolvedAt cell actually used, an inherited array naming which axes fell back, and source (supplied or project).
  • missing lists the shots with nothing behind them at all.
  • read_project's screenshots.cells[] is the authoritative coverage report for the whole grid.

Then supply the missing shot. For a new replacement, name plus its coordinate finds the intended cell; once saved, its persistent identity is the stable shot, not the filename. Or have the user drop it into the project in the browser.

One specific cause worth naming: "the designer's screenshot for ref … is no longer stored". The project still points at a ref whose object was deleted. That cell cannot come back; supply the replacement capture or restore it in the project.

#The preview worked and store-ready did not

The preview rendered, and the full-resolution render or emit_bundle refused.

Nothing failed. Store-ready output — full-resolution panels and every form of emit_bundle, including one that only zips panels rendered earlier — needs a signed-in account on an active Pro trial or Pro. The check runs before anything is rendered, written, uploaded or charged, so the call cost nothing and left no half-made files. Do not report it as a render error and do not retry it unchanged.

account_status names which of four conditions it is, and its nextStep is the move to make:

ConditionnextStepWhat to do
Not signed insign_innpx shotops-mcp login locally, or connect the account in the client.
On a plan without itupgradeStart the free trial or upgrade in the Studio app.
The trial ended and no choice was madechoose_planPick Pro or Free in the Studio app, then call again.
Billing could not be readretryNothing is wrong with the account. The decision fails closed rather than guessing on someone's plan. Try again shortly.

Preview rendering is unaffected in all four cases — free and unmetered on your own machine, and at the ordinary preview price on ShotOps Cloud.

The account is entitled, but final output still refused. Inspect contract.failure.code and contract.readiness. Fatal findings such as an unavailable exact Look or ambiguous source identity must be fixed; an empty panel source is fatal when the requested artifact is a bundle. A waivable fallback or layout finding is a specific release risk the authenticated owner may accept for this exact output; that approval does not cover other findings or future runs. Pass the returned waiver fields back unchanged. Preview reports the same findings without requiring approval.

#Out of cloud credits

Two different exhaustions, and they are not the same tier.

#An account ran out of cloud credits

The refusal states what the render costs, what the per-panel price is, what the balance is, and when it refills. It also names the cheaper option: the same panels as a preview: true render cost a fraction of a full-resolution pass. Render fewer panels, preview instead, or top up — see Plans and output.

#An anonymous connection used up its free previews

"The free ShotOps preview for this connection is already used" is a different message, with a different remedy: sign in. The allowance is a small number of previews per rolling window, capped at a few panels each, and it is keyed on the network, not on a person — everyone behind one office NAT draws from the same bucket. Two neighbouring refusals belong to the same tier: a strip with more panels than the anonymous cap allows, and any full-resolution render at all, which anonymous connections cannot produce regardless of how much of the window is left.

Renders on your own machine spend no cloud credits on any plan, so neither of these can happen there.

#A production render outlived the call

The client disconnected after the Hosted production call returned an operation id. The render continues; disconnection does not request cancellation.

  • Keep that id and call production_operation with action: "get". Poll the same id until its contract reports a terminal state; never substitute an internal Workflow run id.
  • A long silence is usually expected work. Each get reports progress.panelsCompleted of progress.panelsTotal and a rough progress.estimatedRemainingSeconds, because a full-resolution panel takes a few seconds. Read those, not progress.completed and progress.total, which count internal scheduling items and are larger than the panel count.
  • If it succeeded, call again with action: "result". That mints fresh delivery URLs for the retained panels or bundle without rerendering or settling the operation twice.
  • Use action: "cancel" only when the user explicitly asks to stop the job. Cancellation is cooperative, so read the operation again until it becomes terminal.
  • If the starting response was ambiguous and no id reached the client, retry the same production request with the same idempotencyKey rather than inventing a second job.

Preview and Local rendering remain synchronous. A compose-only emit_bundle({ panels }) also returns in its original call because it does not render.

A URL that worked an hour ago returns nothing. Signed download URLs from output: "urls" last one hour. They are delivery grants, not the asset itself. If the asset is still retained, request a fresh URL; check contract.artifacts[].retainedUntil rather than assuming the returned handle lasts forever.

An anonymous session's refs stopped resolving. Anything an anonymous connection uploads lives one hour, with the expiry carried in the object's own path, so it stops resolving on time whether or not the bytes have been swept yet. Sign in for anything durable.

A claim link no longer works. A pending-claim link from an unsigned local save_project is good for seven days. After that the project cannot be claimed through it; save again from a signed-in session.

A share link 404s. Share links last fourteen days. The owner can still see and revoke the dead link for a further week in the Studio app, but nothing downloads through it.

"no screenshot was ever uploaded for ref …". Not an expiry. request_screenshot_upload mints a slot and moves no bytes — the PUT never landed. Slots are single-use and short-lived: mint a fresh one and retry, or import the screenshot by URL and let the server fetch it. When the PUT cannot work at all (a sandboxed client that cannot reach the storage host, which is the normal case on ChatGPT mobile), use the route with no upload step: the user drops the screenshots into a project in their browser, and you render it with render_project and no screenshots at all.