shotops-mcp 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,97 @@
1
+ ShotOps CLI and Local MCP License
2
+ Version 1.1 — 11 August 2026
3
+
4
+ Copyright (c) 2026 Rasmus Gehrke. All rights reserved.
5
+
6
+ This license governs the `shotops-mcp` software package, including the bundled
7
+ ShotOps rendering engine (the "Software"). "You" means the individual or legal
8
+ entity exercising the rights granted below. The Software is licensed, not sold.
9
+ Third-party components and assets are excluded from the definition of Software
10
+ and are governed by their own licenses and notices.
11
+
12
+ GRANT
13
+
14
+ Subject to this license, you are granted a free, worldwide, royalty-free,
15
+ non-exclusive, non-transferable right to:
16
+
17
+ 1. install and run the Software, without limit, in computing environments you
18
+ own, control, or are authorized to use, including via `npx` and in CI;
19
+ 2. use the Software to render device mockups, screenshot strips, and store
20
+ bundles from content you are authorized to use; and
21
+ 3. use, publish, and commercialize the output the Software produces from that
22
+ content, subject to any rights in your content, third-party components,
23
+ assets, device designs, and trademarks.
24
+
25
+ As between you and the copyright holder, the copyright holder claims no
26
+ ownership in your content or in output created from it. This license does not
27
+ grant rights in third-party content, assets, designs, or trademarks that may
28
+ appear in the output.
29
+
30
+ Use of ShotOps hosted services through the Software is additionally governed
31
+ by the ShotOps Terms of Service at https://shotops.dev/terms.
32
+
33
+ RESTRICTIONS
34
+
35
+ You may not:
36
+
37
+ 1. redistribute, republish, sell, rent, sublicense, or otherwise provide the
38
+ Software or any part of it (including the bundled rendering engine) to a
39
+ third party;
40
+ 2. modify the Software or create derivative works from it;
41
+ 3. extract, copy, or reuse components of the Software — including the
42
+ rendering engine — in another product or service;
43
+ 4. offer the Software's functionality to third parties as a service, or use
44
+ it to build a product or service that competes with ShotOps; or
45
+ 5. reverse engineer, decompile, disassemble, or otherwise attempt to derive
46
+ the source code of the Software; or
47
+ 6. remove or obscure copyright, license, or attribution notices.
48
+
49
+ These restrictions apply only to the extent permitted by law. Nothing in this
50
+ license limits a right that cannot lawfully be waived or restricted. In
51
+ particular, where Directive 2009/24/EC or equivalent national law applies, this
52
+ license does not restrict the lawful user's rights to make a necessary backup
53
+ copy, to observe, study, or test the functioning of the Software while
54
+ performing authorized acts, or to reproduce or translate code where that is
55
+ indispensable to achieve interoperability and all statutory conditions are met.
56
+
57
+ THIRD-PARTY COMPONENTS
58
+
59
+ The Software depends on and may bundle third-party components and assets.
60
+ Their copyright notices and license terms are identified in the files,
61
+ metadata, or THIRD_PARTY_NOTICES distributed with the package. Those terms,
62
+ not this license, govern the relevant third-party material. Nothing in this
63
+ license restricts rights granted under a third-party license.
64
+
65
+ TERMINATION
66
+
67
+ If you materially breach this license, your rights terminate if you do not cure
68
+ the breach within 14 days after receiving notice. Rights terminate immediately
69
+ for a breach that cannot be cured or that involves intentional unauthorized
70
+ redistribution or provision of the Software as a service. On termination, you
71
+ must stop using the Software and delete your copies. Provisions that by their
72
+ nature should survive termination, including ownership, restrictions,
73
+ warranty, and liability provisions, remain effective.
74
+
75
+ NO WARRANTY; LIABILITY
76
+
77
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
78
+ IMPLIED, INCLUDING MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND
79
+ NON-INFRINGEMENT. TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE COPYRIGHT HOLDER
80
+ IS NOT LIABLE FOR INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE
81
+ DAMAGES, OR FOR LOSS OF DATA, REVENUE, OR PROFITS, ARISING FROM OR IN CONNECTION
82
+ WITH THE SOFTWARE OR ITS USE.
83
+
84
+ NOTHING IN THIS LICENSE EXCLUDES OR LIMITS MANDATORY STATUTORY RIGHTS OR
85
+ REMEDIES, INCLUDING CONSUMER RIGHTS RELATING TO CONFORMITY OF DIGITAL CONTENT,
86
+ OR LIABILITY THAT CANNOT LAWFULLY BE EXCLUDED OR LIMITED.
87
+
88
+ GOVERNING LAW
89
+
90
+ This license is governed by the law of Denmark, without regard to conflict-of-
91
+ laws rules. If you are a consumer, this choice does not deprive you of mandatory
92
+ protections provided by the law that would apply without this choice, and
93
+ jurisdiction is determined by mandatory consumer-jurisdiction rules. In all
94
+ other cases, disputes are subject to the exclusive jurisdiction of the courts
95
+ of Denmark.
96
+
97
+ Contact: support@shotops.dev
package/README.md CHANGED
@@ -1,15 +1,21 @@
1
1
  # shotops-mcp
2
2
 
3
- A hosted-ready **MCP server** that turns app screenshots into styled 3D App Store mockups,
4
- emits a `fastlane deliver`-ready bundle, and reads your saved ShotOps look for agents
5
- (ChatGPT / Claude Code / Cursor / CI). It runs over the same `@engine` render spine the Studio web app
6
- and the `mockup-mcp` CLI use, driving headless Chromium (Playwright).
3
+ A hosted-ready **MCP server** that makes the marketing screenshots an app store listing needs.
4
+ Give it the raw screen captures from an app and it composites each one into a styled 3D device
5
+ mockup, lays them out as a screenshot strip, sets your headlines over them, and hands back the
6
+ panels as PNGs plus a `fastlane deliver`-ready bundle. Captions are held per locale, so one strip
7
+ ships in every language you list in. It renders at App Store panel sizes and bundles for fastlane
8
+ deliver, so a Google Play listing reuses the designs rather than the exact files. Designs save as
9
+ reusable **looks** and editable **projects**, so the next release matches the last one.
10
+
11
+ Built for agents (ChatGPT / Claude Code / Cursor / CI). It runs over the same `@engine` render
12
+ spine the Studio web app and the `mockup-mcp` CLI use, driving headless Chromium (Playwright).
7
13
 
8
14
  Ships as **two tiers, one engine**: a free `npx shotops-mcp` **local stdio server** that
9
15
  renders on your own machine with no account (see [Local (free tier)](#local-free-tier---npx-shotops-mcp)
10
- below), and a **hosted** server on Fly.io with an account, saved looks, and share links (see
11
- [Hosted (Fly.io)](#hosted-flyio) below, or the public
12
- [`/connect`](https://storeframe-studio.vercel.app/connect) page in the studio app).
16
+ below), and a **hosted** server on Vercel with an account, saved looks, and share links (see
17
+ [Hosted](#hosted) below, or the public
18
+ [`/connect`](https://shotops.dev/connect) page in the studio app).
13
19
 
14
20
  ## Zero-custody
15
21
 
@@ -24,37 +30,66 @@ share links) — nothing that could reach a store on your behalf.
24
30
 
25
31
  | Tool | What it does | Key inputs |
26
32
  |------|--------------|------------|
27
- | `render_strip` | Render screenshots into mocked-up per-panel PNGs | `screenshots`, `panelPresetId?`, `clip?`, `style?` **+** `look?` (they compose) / `useSavedLook?`, `project?`, `version?`, `locale?`, `preview?`, `output?` |
28
- | `emit_bundle` | `render_strip` (or pre-rendered `panels` — refs or local `{ path }`) + a `fastlane deliver` zip (+ optional share link) | `screenshots` or `panels`, `bundleId`, `clip?`, `locale?`, `locales?` (multi-locale bundle), `projectName?`, `share?`, `output?` (+ the same styling params) |
29
- | `save_project` | Save the strip as an editable Studio project (structure + look only — does NOT render, so it never hits the render timeout); returns `projectId` + an `openUrl` | `screenshots`, `project?` (update in place; omit to create), `projectName?`, `panelPresetId?`, `style?` / `look?` / `useSavedLook?`, `version?`, `locale?`, `sourceDir?` |
30
- | `read_look` | Return a project's saved ShotOps look (styling only), with its latest version, the held version, and the `versions` history | `project?` (default: your most recently edited project) |
31
- | `read_project` | Return a project's FULL current state as an opaque `ProjectFile` — frame order, per-locale caption words, locale list, styling. Answers *"what did the designer change?"* (remember `updatedAt`, re-read later) | `project?` (default: your most recently edited project) |
32
- | `render_project` | Re-render a SAVED project with your OWN raw screenshots, matched to its shots **by filename** the project supplies frame order, captions + styling, so the result reproduces the designer's strip | `project?`, `screenshots` (a FLAT list, each with its original filename as `name`), `locale?`, `preview?`, `output?` |
33
+ | `render_strip` | Preferred focused render: turn raw screenshots into mocked-up per-panel PNGs without creating or updating a project | `screenshots`, `panelPresetId?`, `clip?`, `style?` **+** `look?` (they compose) / `useSavedLook?`, `project?`, `version?`, `locale?`, `preview?`, `output?` |
34
+ | `emit_bundle` | `render_strip` (or pre-rendered `panels` — refs or local `{ path }`, or a saved `project`'s own screenshots) + a `fastlane deliver` zip (+ optional share link) | `screenshots` or `panels` or `project` (bundles what that project holds), `bundleId`, `clip?`, `outputs?` (target devices — one bundle for several sizes; omit and a `project`'s saved outputs are used), `locale?`, `locales?` (multi-locale bundle), `projectName?`, `share?`, `output?` (+ the same styling params) |
35
+ | `save_project` | Save the strip as an editable Studio project (structure + look only — does NOT render, so it never hits the render timeout); always returns an `openUrl`. Authenticated saves also return `projectId`; unsigned local saves return a seven-day pending claim the user owns after opening the link and signing in | `screenshots`, `project?` (update in place only when explicitly targeted and authenticated; omit to create a new project or pending claim), `projectName?`, `outputs?`, `panelPresetId?`, `style?` / `look?` / `useSavedLook?`, `version?`, `locale?`, `sourceDir?` |
36
+ | `read_look` | Return a project's saved ShotOps look (styling only), with its latest version, the held version, the `versions` history, and the `layout` template it is composed on (derived from its values, never stored) | `project?` (default: your most recently edited project) |
37
+ | `read_project` | Return a project's FULL current state as an opaque `ProjectFile` — frame order, per-locale caption words, locale list, styling plus the derived `layout` and a `screenshots.cells` report for every shot × target-family × locale. Each cell says whether pixels resolve, where they resolved from, and which axes inherited | `project?` (default: your most recently edited project) |
38
+ | `render_project` | Render a SAVED project. By default it renders the screenshots **the project already holds** — pass none at all, no upload needed into **every supported device the project targets**. Pixels resolve independently for each device family + locale through the shared manifest; genuine fallback is reported in `shots[].inherited` and `note` | `project?`, `screenshots?` (**optional** — omit to use what the project holds; else a FLAT list matched by original `name`, with optional `variant: { family?, locale? }` per cell), `outputs?` (override the project's saved devices for this call), `locale?`, `preview?`, `output?` |
33
39
  | `save_look` | Persist a composed look on a project (styling only — appends a new version) | `look`, `project?`, `sourceName?` |
34
40
  | `hold_look` | Hold which saved version agents render by default (replaces pinning) — the version must exist (`read_look` `versions`) | `version`, `project?` |
35
41
  | `release_look` | Clear the held version — agents Follow latest (render the newest saved look) | `project?` |
36
- | `describe_look` | The styling field catalog + defaults, for authoring from scratch | *(none)* |
37
- | `request_screenshot_upload` | Mint signed upload slots for real screenshots | `count` (1–10), `names?`, `locale?` (a per-locale batch tag) |
38
- | `import_screenshot` | Import a PNG attached in ChatGPT and return a render-ready ref | `file`, `name?`, `locale?` |
42
+ | `describe_look` | Five named design presets, two named layout templates, the user-intake script, and the full styling field catalog + defaults | *(none)* |
43
+ | `request_screenshot_upload` | Mint signed upload slots for real screenshots; every returned slot carries its semantic screenshot `variant` | `count` (1–10), `names?`, `family?`, `locale?` |
44
+ | `import_screenshot` | Get screenshots INTO ShotOps several per call, each from one named source (`{ url }`, `{ file }`, or local-stdio `{ path }`), returning a render-ready ref per entry in input order | `screenshots` (1–10 source entries), `file?` (the ChatGPT top-level attachment), `name?`, `locale?` |
39
45
  | `delete_assets` | Permanently delete private uploaded screenshots, rendered panels, or generated bundles owned by the signed-in account | `refs` (1–50 ShotOps refs) |
40
46
 
41
- - **`panelPresetId`** — App Store size: `r69` (default, 6.9″ iPhone 1290×2796), `r65`, `r55`,
42
- `ipad13`, `ipad129`. This enum is a hand-kept copy of `mockup-engine/appstore.ts`'s
43
- `PANEL_PRESETS` (see `shotops-mcp/src/schemas.ts`'s `PANEL_PRESET_IDS`/`PANEL_DIMENSIONS`)
44
- when Apple revises required sizes, add the preset in the engine first, then mirror it
45
- here.
47
+ - **`panelPresetId`** — App Store size: `r69` (default, 6.9″ iPhone 1290×2796), `r65`, `r55`.
48
+ **iPhone sizes only** the iPad presets are deliberately not offered, because no iPad device
49
+ model exists yet and rendering one would put an iPhone body in an iPad-shaped canvas. The
50
+ accepted set is `RENDERABLE_PANEL_PRESET_IDS` in `shotops-mcp/src/panelCatalog.ts`, a subset of
51
+ the hand-kept `PANEL_PRESET_IDS` copy of `mockup-engine/appstore.ts`'s `PANEL_PRESETS` — when
52
+ Apple revises required sizes, add the preset in the engine first, then mirror it here.
46
53
  - **`style`** — the structured styling input (call `describe_look` for the full catalog):
47
- `{ shotLook?, background?, captions? }`. `shotLook` is **one device for every phone**; `captions`
48
- is one entry PER PANEL in slot order — each entry is EITHER a single caption object OR an **array
49
- of caption layers** stacked on that panel (headline + subline + …). Each layer's `text` (and a
50
- single caption's `subtitle`) is per-RENDER input (never stored — a look carries styling only).
51
- Captions auto-layout above the device so they never collide with the phone.
54
+ `{ layout?, shotLook?, background?, captions? }`. `shotLook` is **one device for every phone**;
55
+ `captions` is one entry PER PANEL in slot order — each entry is EITHER a single caption object OR
56
+ an **array of caption layers** stacked on that panel (headline + subline + …). Each layer's
57
+ `text` (and a single caption's `subtitle`) is per-RENDER input (never stored — a look carries
58
+ styling only). Captions auto-layout in a band at the top of the panel, and the device does NOT
59
+ move to make room.
60
+ - **`style.layout`** — the panel **composition** in one word: `"standard"` or `"bleed"`. It settles
61
+ the headline's reserved region *and* the device size + position together, which is three numbers
62
+ that are not independent of each other. `standard` (the shipped default) puts the whole device
63
+ under a 2-line region — about 36 characters — and crops nothing; `bleed` reserves 4 lines (~72
64
+ characters) and runs the device 10% off the bottom edge. The region is **reserved, not fitted**:
65
+ it is held at full size whether or not the headline fills it, which is what makes every panel in
66
+ a swiped set land on the same line. **Precedence** — the template expands *first*, then any
67
+ `shotLook.phoneHeight`/`hOffset`/`vOffset` or `captions[].sizePt`/`maxWidth` you also pass
68
+ **overrides** it (`{ layout: "bleed", shotLook: { vOffset: "45" } }` is "bleed, but a bit lower").
69
+ A pure macro: nothing stores the template id, so `save_project` persists the expanded values and
70
+ `read_look` returns them unchanged. **The id is not lost, though — it is DERIVED back:**
71
+ `read_project` and `read_look` both return a `layout` report read out of the values themselves, so
72
+ it can never go stale the way a stored id would. When a template was nudged, it reports the near
73
+ miss instead of just failing to match:
74
+
75
+ ```jsonc
76
+ { "layout": "bleed" } // every panel agrees
77
+ { "layout": null, "closestLayout": "bleed", // on Bleed, with one adjustment
78
+ "differs": { "vOffset": { "is": "45", "template": "38" } } }
79
+ { "layout": null, "closestLayout": null } // nothing close
80
+ ```
81
+
82
+ It also answers per panel (`layout.panels["panel-1"]`), because a strip may mix compositions and a
83
+ multi-device panel whose devices were dragged apart is honestly on no template at all. If you set
84
+ placement by hand with no `layout`, the response `note` names the template that would have produced
85
+ it — templates are the main road, raw fields are how you nudge one.
52
86
  - **`look`** — a ShotOps look JSON exactly as `read_look` returns it, but you can also
53
- hand-author one: it gives each panel its **own** device (`shots[]`, tint/tilt/size per panel),
54
- which `style.shotLook` can't. **Validated** unknown keys are rejected (a typo won't silently
55
- render wrong). Omit for default styling.
87
+ hand-author one: it gives each screenshot/device its **own** styling through `shots[]`, including
88
+ two differently coloured devices in one panel. `shots[]` is flattened in panel order; repeat the
89
+ same `panelId` for devices sharing a panel. This is what `style.shotLook` can't do. **Validated**
90
+ — unknown keys are rejected (a typo won't silently render wrong). Omit for default styling.
56
91
  - **`style` + `look` compose** — every real App Store strip needs BOTH a different device per
57
- panel AND captions, so pass them **together**: the `look` supplies the per-panel devices +
92
+ shot AND captions, so pass them **together**: the `look` supplies the per-device styling +
58
93
  background, `style.captions` supplies the words, in one render. (Alone, `style` styles every
59
94
  phone identically. If you pass `style.shotLook`/`style.background` *alongside* a `look`, the look
60
95
  wins and the result carries a note.) Also composes with `useSavedLook`/`version`.
@@ -93,8 +128,11 @@ variants** instead of one image:
93
128
  `fastlane/screenshots/<locale>/` folder per listed locale — `fastlane deliver` uploads every
94
129
  locale in a single run. A single-entry list behaves exactly like `locale`; omitting `locales`
95
130
  is the unchanged single-locale bundle.
96
- - `request_screenshot_upload({ count, names, locale })` tags a batch of upload slots with the
97
- locale it's for (echoed back bookkeeping only; refs are locale-agnostic).
131
+ - `request_screenshot_upload({ count, names, family?, locale? })` returns each slot as
132
+ `{ ref, uploadUrl, name?, variant }`. For `render_project`, pass that `variant` through beside
133
+ the ref: it is the device-family + locale cell the screenshot overrides, not a bookkeeping tag.
134
+ Each `name` must exactly match the saved shot's original filename (`frameName`); the filename
135
+ chooses the shot while `variant` chooses that shot's device-family + locale cell.
98
136
 
99
137
  **Recommended multi-locale flow — render per locale, then compose once.** Caption text is
100
138
  per-render input, so this is also the only way to get per-locale *captions* into one bundle,
@@ -154,25 +192,50 @@ context budget (or get silently truncated by a file-read tool) long before rende
154
192
 
155
193
  **Upload out-of-band instead:**
156
194
 
157
- In ChatGPT, attach the PNG and call `import_screenshot` once per file. ChatGPT supplies the
158
- temporary file parameter; ShotOps downloads and stores it server-to-server, then returns
159
- the same `{ ref }` used by the render tools. In other hosted MCP clients, use the signed-slot
160
- flow below:
195
+ **`import_screenshot` moves them all in one call.** Each entry of `screenshots` names exactly
196
+ one source, and you get one result per entry, in the order you sent them:
197
+
198
+ ```jsonc
199
+ { "screenshots": [
200
+ { "url": "https://example.com/01_home.png", "name": "01_home.png" }, // this server fetches it
201
+ { "path": "/abs/02_stats.png" }, // local stdio server ONLY
202
+ { "file": { "download_url": "…", "file_id": "…" } } // a ChatGPT attachment
203
+ ] }
204
+ ```
205
+
206
+ Two sources in one entry is rejected (split them into two entries). A failed entry reports its
207
+ own `error` and never cancels the others — re-import just that one. In ChatGPT the attachment
208
+ arrives as the top-level `file` parameter instead of an array entry, because the Apps SDK cannot
209
+ fill a file parameter nested inside an array; you can pass both in the same call.
161
210
 
162
- 1. Call `request_screenshot_upload({ count: N })` returns `N` `{ ref, uploadUrl }` slots
211
+ `{ url }` and `{ file }` need the hosted server (it stores the fetched PNG under your account);
212
+ `{ path }` needs the local stdio server (the hosted server never reads a caller-supplied
213
+ filesystem path — that would be an LFI hole). Each refusal names the door that IS open on your
214
+ tier. When the screenshots are only on the user's machine and you are on the hosted server, there
215
+ is no URL to give, so use the signed-slot flow below:
216
+
217
+ 1. Call `request_screenshot_upload({ count: N, names?, family?, locale? })` — returns `N`
218
+ `{ ref, uploadUrl, name?, variant }` slots
163
219
  under your own private prefix, plus a copy-paste `curl -T` example.
164
220
  2. `curl -T screenshot.png "<uploadUrl>"` each screenshot directly (bytes never touch this
165
221
  conversation).
166
222
  3. Pass `{ "ref": "<ref>" }` — not inline base64 — as that screenshot's entry in
167
- `render_strip`/`emit_bundle`'s `screenshots` array.
223
+ `render_strip`/`emit_bundle`'s `screenshots` array. For `render_project`, also pass the slot's
224
+ `name` and `variant`; `name` must exactly match the saved shot's original filename, and legacy
225
+ entries without a variant remain the base/fallback cell.
168
226
 
169
227
  `screenshots` entries accept these shapes: an inline base64 string (small payloads only —
170
- inline is capped and returns a clear "payload too large" error above ~8MB decoded, pointing
228
+ inline is capped and returns a clear "payload too large" error above 3MB decoded, pointing
171
229
  you back at this flow), `{ ref }` from step 1, `{ url }` (an https URL this server fetches
172
230
  itself: image/png, no redirects, ~20MB cap), `{ path }` (local stdio server only — see below),
173
231
  or `{ "locales": { "<locale>": <any of those> } }` for per-locale variants (see
174
232
  [Per-locale screenshots](#per-locale-screenshots--multi-locale-bundles) above).
175
233
 
234
+ Every raw PNG is automatically palette-optimized before rendering (TinyPNG-style, locally inside
235
+ ShotOps): dimensions and transparency are preserved, and an already-smaller PNG is left byte-for-byte
236
+ unchanged. This happens after the input reaches the MCP, so it does **not** raise the inline request
237
+ cap — use the ref/upload flow for a large source file.
238
+
176
239
  **Output** works the same way in reverse: `render_strip`/`emit_bundle` take an `output` field
177
240
  — `"inline"` (base64), `"urls"` (uploaded under your own prefix, short-lived signed download
178
241
  URLs), or omit for auto (inline under ~200KB total, `urls` above). Use `"urls"` for
@@ -224,7 +287,7 @@ is entered in the Platform dashboard. Set it on the hosted server as
224
287
  `OPENAI_APPS_CHALLENGE_TOKEN`; the server then returns that exact value from
225
288
  `/.well-known/openai-apps-challenge`. With no token configured, the route returns 404.
226
289
  The server also exposes a synthetic, non-user PNG at
227
- `/review-fixtures/sample-app-screen.png` so reviewers can reproduce attachment and URL-input
290
+ `/review-fixtures/sample-app-screen-v2.png` so reviewers can reproduce attachment and URL-input
228
291
  tests without private fixture data or third-party hosting.
229
292
 
230
293
  ## Connect Claude Code
@@ -254,23 +317,23 @@ unzip shotops-appstore-upload.zip -d upload && cd upload
254
317
  fastlane deliver # signs in as YOU, previews, uploads screenshots to a draft — never submits
255
318
  ```
256
319
 
257
- ## Hosted (Fly.io)
320
+ ## Hosted
258
321
 
259
322
  The server also runs hosted, 24/7-reachable (scale-to-zero when idle), at:
260
323
 
261
324
  ```
262
- https://shotops-mcp.fly.dev/mcp
325
+ https://shotops.dev/mcp
263
326
  ```
264
327
 
265
328
  Nothing to install — point any MCP client at that URL with your `Authorization: Bearer
266
329
  shotops_…` header, same as local dev, just swap the base URL:
267
330
 
268
331
  ```bash
269
- claude mcp add --transport http shotops https://shotops-mcp.fly.dev/mcp \
332
+ claude mcp add --transport http shotops https://shotops.dev/mcp \
270
333
  --header "Authorization: Bearer shotops_…"
271
334
  ```
272
335
 
273
- The public [`/connect`](https://storeframe-studio.vercel.app/connect) page in the studio
336
+ The public [`/connect`](https://shotops.dev/connect) page in the studio
274
337
  app has the same instructions for Cursor/CI, plus a link to where to create a token — it's
275
338
  the one place an external agent user (not a repo collaborator) can find the connect story,
276
339
  since this repo is private.
@@ -288,7 +351,7 @@ since this repo is private.
288
351
  - **Verify after any deploy** — a green `flyctl deploy` isn't proof the server answers MCP
289
352
  calls:
290
353
  ```bash
291
- node verify-mcp.mjs https://shotops-mcp.fly.dev/mcp <a-real-shotops_-token>
354
+ node verify-mcp.mjs https://shotops.dev/mcp <a-real-shotops_-token>
292
355
  ```
293
356
  Exercises `tools/list`, `read_look`, `emit_bundle` (asserts the zip unzips to
294
357
  README/Deliverfile/screenshots), and asserts no-token / bad-token both get `401`.
@@ -299,11 +362,19 @@ since this repo is private.
299
362
  server at runtime. Vite is build/parity tooling only and is pruned from production
300
363
  dependencies; the Docker build context still includes `mockup-engine` so the harness can
301
364
  bundle the shared engine + GLB.
302
- - **VM size is `shared-cpu-2x` (2 vCPU / 2GB), not `shared-cpu-1x`** (found live, Phase 10):
303
- Chromium's software WebGL render (no GPU on Fly's shared tier) is CPU-heavy enough that on
365
+ - **VM size is `shared-cpu-2x` with 4GB and BOTH numbers are load-bearing for different
366
+ reasons.** The **memory** was raised 2gb → 4gb on 2026-07-26 (#134): a grouped multi-panel
367
+ `render_strip` drove `chrome-headless` to ~1.4gb resident, the kernel OOM-killed it, and the
368
+ server went unhealthy for minutes — `verify-mcp.mjs` could not run to completion at 2gb and now
369
+ does. The **2 vCPUs** are the older finding below; don't conflate them, and don't cut either.
370
+ - **Why 2 vCPU and not `shared-cpu-1x`** (found live, Phase 10):
371
+ Chromium's software WebGL render (no GPU on the hosted tier) is CPU-heavy enough that on
304
372
  a single shared vCPU it can starve the whole process — health checks stop responding,
305
- SIGTERM/SIGINT get ignored, and Fly force-kills the VM ("Virtual machine exited abruptly")
306
- mid-render. On 2 vCPUs the same 3-panel real-screenshot render completes in ~8s. Don't
373
+ SIGTERM/SIGINT get ignored, and the platform force-kills the machine
374
+ mid-render. **`Virtual machine exited abruptly` has TWO causes and they look identical in the
375
+ logs:** on 1 vCPU it is this CPU starvation; on 2 vCPU under load it was the #134 OOM, i.e.
376
+ MEMORY, fixed by the `4gb` above (`docs/reference/ops-runbook.md` carries that half). Check
377
+ which resource ran out before reaching for either fix. On 2 vCPUs the same 3-panel real-screenshot render completes in ~8s. Don't
307
378
  drop back to `shared-cpu-1x` to save cost — a render-triggering tool call is expected to
308
379
  occasionally saturate a whole core, and 1x has no second core to keep the process alive
309
380
  while that happens. Client callers (agents, `verify-mcp.mjs`) should still set a generous
@@ -314,7 +385,7 @@ since this repo is private.
314
385
  ## Local (free tier) — `npx shotops-mcp`
315
386
 
316
387
  The SAME server also runs as a **local stdio MCP**, entirely on your own machine — no URL, no
317
- account, no Fly cost, and no render-timeout ceiling (the hosted server's one real limit — see
388
+ account, no hosted cost, and no render-timeout ceiling (the hosted server's one real limit — see
318
389
  "Ops" above). It's the same `registerTools`/render engine as the hosted server; only the
319
390
  transport and a few account-shaped tools differ.
320
391
 
@@ -347,32 +418,54 @@ produce `output: "urls"` refs. After rendering, pass the on-disk PNGs straight t
347
418
  { "bundleId": "com.acme.app", "panels": [{ "path": "/abs/panel-01.png" }, { "path": "/abs/panel-02.png" }] }
348
419
  ```
349
420
 
350
- **What's different locally, and why:** every tool that needs an ACCOUNT to read or persist to
351
- `read_look` / `save_look` / `save_project` / `read_project` / `render_project` / share links
352
- has nothing to talk to in local mode by default, so they cleanly no-op with a "sign in" message.
353
- `render_strip` / `emit_bundle` are otherwise **fully functional** you lose nothing on rendering
354
- itself, only on persistence, which is exactly the free/paid line:
421
+ **Saving without a token:** a new `save_project` made from local `{ path }` PNGs creates a private
422
+ **pending claim**. It uploads only that saved project's byte-free record and raw source PNGs,
423
+ returns `status: "pending_claim"` plus `claimId` and `openUrl`, and deliberately returns no
424
+ `projectId` yet. Give the `openUrl` to the user: after one Google sign-in they own the project,
425
+ with its screenshots, and the link expires after 7 days. An existing `project` cannot be updated
426
+ without the token that owns it. Non-path inputs are refused rather than creating a blank claim.
355
427
 
356
- | | Local (free, `npx shotops-mcp`) | Hosted (Fly, `shotops_…` token) |
428
+ Ordinary `render_strip` / `emit_bundle` calls still upload **nothing** and work fully. Other
429
+ account reads/writes (`read_look`, `save_look`, `read_project`, `render_project`, share links)
430
+ still need a token or hosted connection. A token-backed local `render_project` reads the cloud
431
+ manifest through the Studio control plane, downloads only the resolved cells, and renders locally:
432
+
433
+ | | Local (free, `npx shotops-mcp`) | Hosted (`shotops_…` token) |
357
434
  |---|---|---|
358
- | Render compute | **your machine** ($0, no timeout) | our Fly machine |
435
+ | Render compute | **your machine** ($0, no timeout) | our servers spends your ShotOps credits: 1,000 per preview panel, 4,000 per full-resolution panel |
359
436
  | Auth | none (the process runs as you) | account + API token |
360
- | Saved looks / editable projects / project read+re-render / share links | opt-in via a token (below) share links still need a direct hosted connection | ✅ |
437
+ | Saved looks / editable projects / project read+re-render / share links | new project: pending claim; existing/account state: token (below); share links: hosted | ✅ |
361
438
  | Screenshot input | local `{ path }` off disk | `request_screenshot_upload` → `{ ref }` |
362
439
 
363
440
  ### Optional: bridge a local render into your hosted account
364
441
 
365
- Pass a personal ShotOps API token (Studio account menu **API tokens** — the same
366
- `shotops_…` token the hosted server takes as a bearer) and `save_project` / `read_look` /
367
- `save_look` / `read_project` / `render_project` start working too **rendering still happens
368
- locally and for free; only the small saved-project JSON (structure + look, never screenshot
369
- bytes) goes to your account:**
442
+ Connect your account and `save_project` / `read_look` / `save_look` / `read_project` /
443
+ `render_project` start working too **rendering still happens locally and for free.** A
444
+ deliberate save also uploads its local source PNGs privately so the project reopens with
445
+ screenshots on another device; normal renders and exports never upload.
446
+
447
+ Sign in through your browser, once per machine:
448
+
449
+ ```bash
450
+ npx shotops-mcp login # opens ShotOps in your browser, saves the token it gets back
451
+ npx shotops-mcp whoami # which account is connected, and its plan
452
+ npx shotops-mcp logout # revokes that token and removes it from this machine
453
+ ```
454
+
455
+ The token lands in `~/.shotops/credentials.json`, readable only by you, and every later
456
+ `npx shotops-mcp` picks it up — nothing to paste into a client config. It is an ordinary
457
+ ShotOps API token: revoke it any time from Studio → account menu → **API tokens**.
458
+
459
+ For CI, or to point one client at a different account, pass the token explicitly instead:
370
460
 
371
461
  ```bash
372
462
  SHOTOPS_TOKEN=shotops_… claude mcp add shotops -- npx -y shotops-mcp
373
463
  # or: npx shotops-mcp --token shotops_…
374
464
  ```
375
465
 
466
+ `--token` wins over `SHOTOPS_TOKEN`, which wins over a saved browser login — so a token in the
467
+ environment is never quietly shadowed by whoever last signed in on that machine.
468
+
376
469
  The saved project remembers the exact on-disk folder your screenshots came from (not just their
377
470
  filenames), so re-opening it on the SAME machine can point right back at it. Share links aren't
378
471
  bridged yet — they'd need a hosted rendering step, which would defeat local rendering's whole
@@ -411,7 +504,7 @@ shotops-mcp/
411
504
  vite.harness.config.mjs the STATIC harness build (local mode's publish prerequisite) → harness-dist/
412
505
  build.mjs esbuild bundle of src/local.ts (+ @engine/@api inlined) → dist/local.js
413
506
  Dockerfile hosted image — Playwright's Chromium base + this repo's 3 npm installs
414
- fly.toml Fly app config (scale-to-zero, /health check) deploy from repo root
507
+ fly.toml Fly app config ROLLBACK ONLY since the Vercel cutover; see #68
415
508
  verify-mcp.mjs post-deploy smoke test — real MCP round-trip against a hosted URL
416
509
  verify-mcp-local.ts local stdio smoke test (tsx src/local.ts, in-repo)
417
510
  static-parity.ts pixel-diffs the static-serve render against the Vite-dev render (must be 0)
@@ -0,0 +1,156 @@
1
+ Third-Party Notices for shotops-mcp
2
+
3
+ This file identifies third-party software, font files and 3D models bundled
4
+ directly in the shotops-mcp package. Dependencies installed as separate npm
5
+ packages carry their own license files.
6
+
7
+ -------------------------------------------------------------------------------
8
+ iPhone 17 Pro 3D model
9
+ -------------------------------------------------------------------------------
10
+
11
+ "iPhone 17 Pro" by Ranguel (https://sketchfab.com/Ranguel)
12
+ Source: https://sketchfab.com/3d-models/iphone-17-pro-4541aa8a28324b33a2baaf81d263aaec
13
+ Licensed under Creative Commons Attribution 4.0 International (CC BY 4.0):
14
+ https://creativecommons.org/licenses/by/4.0/
15
+
16
+ The model file bundled here (dist/harness assets) is a converted and optimized
17
+ derivative of that work, produced with glTF-Transform. CC BY 4.0 permits both
18
+ commercial use and redistribution of the model, including in modified form,
19
+ provided the author is credited — which is what this notice is for. Keep it in
20
+ any redistribution of this package.
21
+
22
+ Apple and iPhone are trademarks of Apple Inc. Google and Pixel are trademarks
23
+ of Google LLC. Neither company sponsors or endorses ShotOps.
24
+
25
+ -------------------------------------------------------------------------------
26
+ Three.js
27
+ -------------------------------------------------------------------------------
28
+
29
+ Copyright (c) 2010-2024 three.js authors
30
+
31
+ The MIT License
32
+
33
+ Permission is hereby granted, free of charge, to any person obtaining a copy
34
+ of this software and associated documentation files (the "Software"), to deal
35
+ in the Software without restriction, including without limitation the rights
36
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
37
+ copies of the Software, and to permit persons to whom the Software is
38
+ furnished to do so, subject to the following conditions:
39
+
40
+ The above copyright notice and this permission notice shall be included in
41
+ all copies or substantial portions of the Software.
42
+
43
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
44
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
45
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
46
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
47
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
48
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
49
+ THE SOFTWARE.
50
+
51
+ -------------------------------------------------------------------------------
52
+ Bundled caption fonts
53
+ -------------------------------------------------------------------------------
54
+
55
+ The package includes unmodified, Latin-subset WOFF2 files from these font
56
+ families:
57
+
58
+ Inter
59
+ Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
60
+
61
+ Manrope
62
+ Copyright 2018 The Manrope Project Authors (https://github.com/sharanda/manrope)
63
+
64
+ Poppins
65
+ Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins)
66
+
67
+ Fraunces
68
+ Copyright 2018 The Fraunces Project Authors (https://github.com/undercasetype/Fraunces)
69
+
70
+ Space Grotesk
71
+ Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk)
72
+
73
+ Each font is licensed under the SIL Open Font License, Version 1.1:
74
+
75
+ -----------------------------------------------------------
76
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
77
+ -----------------------------------------------------------
78
+
79
+ PREAMBLE
80
+ The goals of the Open Font License (OFL) are to stimulate worldwide
81
+ development of collaborative font projects, to support the font creation
82
+ efforts of academic and linguistic communities, and to provide a free and
83
+ open framework in which fonts may be shared and improved in partnership
84
+ with others.
85
+
86
+ The OFL allows the licensed fonts to be used, studied, modified and
87
+ redistributed freely as long as they are not sold by themselves. The fonts,
88
+ including any derivative works, can be bundled, embedded, redistributed
89
+ and/or sold with any software provided that any reserved names are not used
90
+ by derivative works. The fonts and derivatives, however, cannot be released
91
+ under any other type of license. The requirement for fonts to remain under
92
+ this license does not apply to any document created using the fonts or their
93
+ derivatives.
94
+
95
+ DEFINITIONS
96
+ "Font Software" refers to the set of files released by the Copyright
97
+ Holder(s) under this license and clearly marked as such. This may include
98
+ source files, build scripts and documentation.
99
+
100
+ "Reserved Font Name" refers to any names specified as such after the
101
+ copyright statement(s).
102
+
103
+ "Original Version" refers to the collection of Font Software components as
104
+ distributed by the Copyright Holder(s).
105
+
106
+ "Modified Version" refers to any derivative made by adding to, deleting,
107
+ or substituting -- in part or in whole -- any of the components of the
108
+ Original Version, by changing formats or by porting the Font Software to a
109
+ new environment.
110
+
111
+ "Author" refers to any designer, engineer, programmer, technical writer or
112
+ other person who contributed to the Font Software.
113
+
114
+ PERMISSION & CONDITIONS
115
+ Permission is hereby granted, free of charge, to any person obtaining a copy
116
+ of the Font Software, to use, study, copy, merge, embed, modify, redistribute,
117
+ and sell modified and unmodified copies of the Font Software, subject to the
118
+ following conditions:
119
+
120
+ 1) Neither the Font Software nor any of its individual components, in
121
+ Original or Modified Versions, may be sold by itself.
122
+
123
+ 2) Original or Modified Versions of the Font Software may be bundled,
124
+ redistributed and/or sold with any software, provided that each copy contains
125
+ the above copyright notice and this license. These can be included either as
126
+ stand-alone text files, human-readable headers or in the appropriate
127
+ machine-readable metadata fields within text or binary files as long as those
128
+ fields can be easily viewed by the user.
129
+
130
+ 3) No Modified Version of the Font Software may use the Reserved Font Name(s)
131
+ unless explicit written permission is granted by the corresponding Copyright
132
+ Holder. This restriction only applies to the primary font name as presented
133
+ to the users.
134
+
135
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
136
+ Software shall not be used to promote, endorse or advertise any Modified
137
+ Version, except to acknowledge the contribution(s) of the Copyright Holder(s)
138
+ and the Author(s) or with their explicit written permission.
139
+
140
+ 5) The Font Software, modified or unmodified, in part or in whole, must be
141
+ distributed entirely under this license, and must not be distributed under
142
+ any other license. The requirement for fonts to remain under this license
143
+ does not apply to any document created using the Font Software.
144
+
145
+ TERMINATION
146
+ This license becomes null and void if any of the above conditions are not met.
147
+
148
+ DISCLAIMER
149
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
150
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
151
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
152
+ TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR
153
+ ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL,
154
+ INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF
155
+ CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE
156
+ THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.