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 +97 -0
- package/README.md +157 -64
- package/THIRD_PARTY_NOTICES +156 -0
- package/dist/local.js +5478 -1617
- package/harness-dist/assets/{index-D1u7TBPd.js → index-DvvjdOuK.js} +169 -169
- package/harness-dist/index.html +1 -1
- package/package.json +19 -5
- package/widget-dist/bundle.js +221 -0
- package/widget-dist/panels.js +188 -0
- package/harness-dist/assets/pixel-optimized-DZsBrOgu.glb +0 -0
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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
11
|
-
[Hosted
|
|
12
|
-
[`/connect`](https://
|
|
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` |
|
|
28
|
-
| `emit_bundle` | `render_strip` (or pre-rendered `panels` — refs or local `{ path }`) + a `fastlane deliver` zip (+ optional share link) | `screenshots` or `panels
|
|
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 `
|
|
30
|
-
| `read_look` | Return a project's saved ShotOps look (styling only), with its latest version, the held version,
|
|
31
|
-
| `read_project` | Return a project's FULL current state as an opaque `ProjectFile` — frame order, per-locale caption words, locale list, styling
|
|
32
|
-
| `render_project` |
|
|
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` |
|
|
37
|
-
| `request_screenshot_upload` | Mint signed upload slots for real screenshots | `count` (1–10), `names?`, `locale?`
|
|
38
|
-
| `import_screenshot` |
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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**;
|
|
48
|
-
is one entry PER PANEL in slot order — each entry is EITHER a single caption object OR
|
|
49
|
-
of caption layers** stacked on that panel (headline + subline + …). Each layer's
|
|
50
|
-
single caption's `subtitle`) is per-RENDER input (never stored — a look carries
|
|
51
|
-
Captions auto-layout
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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 })`
|
|
97
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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://
|
|
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
|
|
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`
|
|
303
|
-
|
|
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
|
|
306
|
-
mid-render.
|
|
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
|
|
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
|
-
**
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
`
|
|
354
|
-
|
|
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
|
-
|
|
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
|
|
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 |
|
|
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
|
-
|
|
366
|
-
`
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
|
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.
|