# Screenery — the full guide for agents This file is the homepage, as text. It has three parts: how to onboard a repository (the README's own words), the delivery URL grammar, and the MCP server's runbook — the instructions the server hands an agent on connect. # Part 1 — Add the MCP server Claude Code: ``` claude mcp add screenery -- npx -y screenery mcp ``` Cursor (.cursor/mcp.json): ``` { "mcpServers": { "screenery": { "command": "npx", "args": [ "-y", "screenery", "mcp" ] } } } ``` VS Code: ``` code --add-mcp '{"name":"screenery","command":"npx","args":["-y","screenery","mcp"]}' ``` Any MCP host: ``` { "mcpServers": { "screenery": { "command": "npx", "args": [ "-y", "screenery", "mcp" ] } } } ``` No install: ``` claude mcp add --transport http screenery https://app.screenery.dev/mcp ``` Then say: "Add the Screenery MCP server and set up self-updating screenshots for this repo." # Part 2 — Onboarding, by hand ## Zero to an embedded, self-updating screenshot Four steps, and **no secret**. ### 1. Install the GitHub App Open the console, follow `install app`, pick the repository, and choose whether the project is public or private. That is the only decision onboarding asks for; everything else is a default. The install binds the project to the repository's **numeric id** (spec §5.1), so renaming the repository changes nothing and a fork cannot publish to it. One repository can feed several projects — the Action's `project:` input picks which. ### 2. Push screenshots from CI After your Playwright step, in the same job: ```yaml permissions: id-token: write contents: read steps: - uses: screenery/push@v1 with: { project: acme/dashboard, path: test-results } ``` No token. The Action asks the runner for a GitHub OIDC token, exchanges it at `POST /v1/oidc/exchange`, and gets a publish credential that lives **five minutes** — nothing stored, nothing to rotate. CI that is not GitHub Actions sets `SCREENERY_TOKEN` to a static publish token instead (`pnpm --filter @screenery/api token:create`, or the access screen). A pull request from a fork gets no OIDC token, which is GitHub working as intended — a fork's workflow must not be able to publish to your project. The CLI says so in the log, in those words, rather than failing with an auth error. The asset name is the file's path under `./test-results` with the extension removed, so `test-results/checkout/payment-form.png` becomes the asset `checkout/payment-form`. Nothing is renamed for you: a path that cannot be a legal asset name is an error that names itself. Every push also fast-forwards the branch channel — `main` after a merge, `pr-142` on a pull request — so a PR's screenshots are addressable (`…@pr-142.png`) without touching anything a user sees. ### 3. Promote on release You do not write this step. A project created through the install flow comes with spec §7.2's default ruleset, and the first of the two says: > When a **release is published**, promote **main** → **latest** and to the > release tag. Publishing a GitHub release fires it. The App receives the webhook, verifies its signature and its delivery id, and the rules engine moves `@latest` — every asset on `main` at once, which is what release semantics means. The execution lands in the rule's log on the channels screen, including the runs a filter skipped: on a monorepo, `tag_prefix: web-v` is what stops an `api-v2.1` release promoting the web project, and the log says so in those words. Promoting by hand still works and is unchanged: ```bash npx screenery promote acme/dashboard --from main --to latest ``` The command exits non-zero if the edge is not serving the new version within the deadline, so a release nobody can see is not a green CI step. ### 4. Embed it Ask for the snippet rather than assembling the URL yourself — the extension comes from the stored image format, and getting it wrong asks the CDN to transcode on every request: ```bash curl -H "Authorization: Bearer $SCREENERY_TOKEN" \ https://app.screenery.dev/v1/projects/acme/dashboard/embeds/checkout/payment-form@latest ``` ```json { "url": "https://cdn.screenery.dev/acme/dashboard/checkout/payment-form@latest.png", "markdown": "![acme/dashboard/checkout/payment-form@latest](https://cdn.screenery.dev/…)", "html": "\"…\"" } ``` Paste the `markdown` or `html` into your page. That is the last time you touch it. A snippet is only emitted for a **public** asset. An `unlisted` or `private` one needs a credential — a signed URL or a read token — and Screenery will not put an expiring credential inside a string meant to be pasted once, so it returns the facts and tells you which credential the URL needs instead. ### Look at what you have ```bash curl -H "Authorization: Bearer $SCREENERY_TOKEN" \ https://app.screenery.dev/projects/acme/dashboard ``` A plain HTML page: every asset, what `@latest` points at, when it last moved, and the Markdown snippet for each. There is no sign-in form — the token goes in a header — so read it with an HTTP client rather than a browser. # Part 3 — The URL grammar Screenery delivery URL grammar (spec §2) https://cdn.screenery.dev/{org}/{project}/{asset}@{ref}.{ext}[?params] {ref} a channel (latest, main, pr-142, v2.3.1) or a version (v_a1b2c3d4e5f6). Omitted means @latest. {ext} png, jpg, webp, avif, gif, webm, mp4 — the STORED format. Ask screenery_embed for the snippet rather than guessing; a wrong extension asks the CDN to transcode on every request. A channel is a mutable pointer; a version is immutable and cached forever. A channel URL is never HTTP-cached — the edge resolves the pointer per request — so a promotion propagates globally within 60 s with nothing to purge. Visibility (spec §3): public needs no credential; unlisted needs a signed URL; private needs a read token. Unauthorized is a 404, never a 403. # Part 4 — The MCP server's runbook The text below is exactly what the server sends as its `instructions` on connect. It is here so an agent that has not connected yet can see the order. You are connected to Screenery, a package registry for product screenshots. CI pushes screenshots on every build; each one gets a stable public URL that updates itself on release: https://cdn.screenery.dev/{org}/{project}/{asset}@latest.png Paste that URL into a README or a docs page once. Ship a release, and the image behind it changes. Docker Hub semantics, for media. HOW TO ONBOARD A REPOSITORY — follow this order, and after every step call screenery_status again. Every tool result carries `next: { tool, because }`; call that tool next. Do not guess the order. 1. screenery_status Call it first, with `repo_dir` set to the repository you are working in. It reports what is missing and names the next tool. It is safe to call at any time. 2. screenery_login The one step that needs a person. It returns a URL and an eight-letter code. Show BOTH to the person, verbatim, and ask them to open the URL and press Approve. Then call screenery_login again; it completes once they have. If they have not yet, it says so — wait a few seconds and call it again. Never invent a code and never tell the person it is done before the tool says "signed in". 3. screenery_create_project Creates `{org}/{project}`. `visibility` is required: ask the person whether their screenshots may be public. Two paths: - `path: "static_token"` (default): a project with no GitHub App. Works everywhere, needs a publish token in CI, promotes by hand. - `path: "github_app"`: the better path when the repository is on GitHub. The tool walks you through it in several calls: first it gives an install URL for the person to open, then it lists the repositories the install covers so you can pick one, then it binds. Releases then promote themselves and CI needs no secret. 4. screenery_push Push a directory of screenshots from this machine, right now, before any CI exists. Take screenshots however you already can (Playwright, a browser tool, an existing test run) into a directory, then push it. This is the first live URL, and it takes under a minute. 5. screenery_verify Fetch the URL the push returned and confirm it is live. This is the proof to show the person. 6. screenery_setup_ci Give it the repository's Playwright workflow YAML and it returns the same YAML with the Screenery step and the permissions block inserted. Write the result to the file. If there is no workflow, it returns a complete one. For a `static_token` project it also tells you which secret to add. 7. screenery_embed Returns the Markdown and HTML snippets for an asset. Paste the Markdown into the README. That is the last time anyone touches it. Afterwards: screenery_promote moves a channel, screenery_sign makes a time-boxed URL for an unlisted asset, screenery_list_assets and screenery_versions read what exists, screenery_publish_token mints a static publish token for CI that is not GitHub Actions. RULES - Identifiers are lowercase `a-z0-9-` (org: 2–32, project: 1–64). Asset names are the file path under the pushed directory with the extension removed. Nothing is renamed for you; a bad name is an error that names itself. Do not "fix" names by guessing — ask. - A public asset needs no credential. `unlisted` needs a signed URL (screenery_sign). `private` needs a read token. An unauthorized request for either is a 404, never a 403: a 404 on a URL you just built usually means visibility, not a typo. - Never put a token in a URL, a log line, or a file the person did not ask for. The tools never return one except screenery_publish_token, once. - When a tool fails, its message says what to do. Relay it; do not retry the same call blindly.