shotops-mcp 0.9.9 → 0.10.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/README.md +105 -256
- package/dist/local.js +9647 -11740
- package/harness-dist/assets/index-DsyBFowq.js +1 -0
- package/harness-dist/index.html +1 -1
- package/package.json +1 -1
- package/widget-dist/bundle.js +4 -4
- package/widget-dist/operation.js +282 -0
- package/widget-dist/panels.js +2 -2
- package/harness-dist/assets/index-B8oQTxzE.js +0 -1
package/README.md
CHANGED
|
@@ -20,29 +20,25 @@ below, or the public [MCP docs](https://shotops.dev/docs/mcp)).
|
|
|
20
20
|
|
|
21
21
|
Composing and previewing a strip is free through either door — every device, caption and locale, no
|
|
22
22
|
watermark, and locally without an account at all. **Store-ready output needs an active trial or
|
|
23
|
-
Pro:** a
|
|
24
|
-
|
|
25
|
-
compute being free is not the same as local output being free.
|
|
23
|
+
Pro:** a `render` with `output: "panels"` (full resolution) or `output: "bundle"`. That holds
|
|
24
|
+
wherever the render happens; local compute being free is not the same as local output being free.
|
|
26
25
|
|
|
27
26
|
The two limits are independent and neither substitutes for the other:
|
|
28
27
|
|
|
29
28
|
- **Entitlement** is permission to create store-ready files. One server-owned decision answers it
|
|
30
|
-
for Studio, hosted and local alike
|
|
31
|
-
|
|
32
|
-
written — a refused call costs nothing and leaves nothing behind.
|
|
29
|
+
for Studio, hosted and local alike. Free and anonymous callers are refused before a cloud credit
|
|
30
|
+
is reserved, a screenshot is read or a file is written — a refused call costs nothing.
|
|
33
31
|
- **Cloud credits** price compute ShotOps supplies: 1 per hosted preview panel, 4 per hosted
|
|
34
|
-
full-resolution panel
|
|
35
|
-
cloud credits never unlock store-ready output on Free.
|
|
32
|
+
full-resolution panel. Rendering on your own machine spends none, on any plan.
|
|
36
33
|
|
|
37
34
|
Free accounts get 80 cloud credits a month and 2 synced projects. Every new account starts with a
|
|
38
35
|
7-day Pro trial; after that Pro is €9/month or €90/year. Without any account, the hosted server
|
|
39
36
|
allows 3 preview strips per rolling 30 days, at most 5 panels each. Call `account_status` to see
|
|
40
|
-
exactly where this connection stands — it spends
|
|
41
|
-
allowance.
|
|
37
|
+
exactly where this connection stands — it spends nothing.
|
|
42
38
|
|
|
43
39
|
## Zero-custody
|
|
44
40
|
|
|
45
|
-
The hosted server and every MCP tool **never touch a store-signing credential.**
|
|
41
|
+
The hosted server and every MCP tool **never touch a store-signing credential.** A bundle render
|
|
46
42
|
hands you a zip; **you** upload it with **your own** `fastlane`, signed in as yourself. The
|
|
47
43
|
generated `Deliverfile` is screenshots-only and **never submits for review**.
|
|
48
44
|
|
|
@@ -54,240 +50,102 @@ The key is never copied, printed, or sent to ShotOps, and no release tool is ins
|
|
|
54
50
|
|
|
55
51
|
| Tool | What it does | Key inputs |
|
|
56
52
|
|------|--------------|------------|
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
`
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
a
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
`
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
"differs": { "vOffset": { "is": "45", "template": "38" } } }
|
|
112
|
-
{ "layout": null, "closestLayout": null } // nothing close
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
It also answers per panel (`layout.panels["panel-1"]`), because a strip may mix compositions and a
|
|
116
|
-
multi-device panel whose devices were dragged apart is honestly on no template at all. If you set
|
|
117
|
-
placement by hand with no `layout`, the response `note` names the template that would have produced
|
|
118
|
-
it — templates are the main road, raw fields are how you nudge one.
|
|
119
|
-
- **`look`** — a ShotOps look JSON exactly as `read_look` returns it, but you can also
|
|
120
|
-
hand-author one: it gives each screenshot/device its **own** styling through `shots[]`, including
|
|
121
|
-
two differently coloured devices in one panel. `shots[]` is flattened in panel order; repeat the
|
|
122
|
-
same `panelId` for devices sharing a panel. This is what `style.shotLook` can't do. **Validated**
|
|
123
|
-
— unknown keys are rejected (a typo won't silently render wrong). Omit for default styling.
|
|
124
|
-
- **`style` + `look` compose** — every real App Store strip needs BOTH a different device per
|
|
125
|
-
shot AND captions, so pass them **together**: the `look` supplies the per-device styling +
|
|
126
|
-
background, `style.captions` supplies the words, in one render. (Alone, `style` styles every
|
|
127
|
-
phone identically. If you pass `style.shotLook`/`style.background` *alongside* a `look`, the look
|
|
128
|
-
wins and the result carries a note.) Also composes with `useSavedLook`/`version`.
|
|
129
|
-
- **Clipping** — set `look.shots[].look.clipToFrame: true` to clip one device's complete composite
|
|
130
|
-
(body, screen, shadow, and reflection) to its own panel; omit it or use `false` for overflow.
|
|
131
|
-
Pass top-level `clip: "panel"` to clip every device, or `clip: "strip"` to force continuous
|
|
132
|
-
overflow. Call `describe_look` for the complete Look contract on either MCP tier.
|
|
133
|
-
- **`version`** — render a specific saved look version (implies the saved look). Without it,
|
|
134
|
-
a project with a PINNED version renders the pin; otherwise the latest saved look.
|
|
135
|
-
- **`emit_bundle` output** — a zip (`README.md`, `fastlane/Deliverfile`,
|
|
136
|
-
`fastlane/screenshots/<locale>/NN_shotops.png`) as `zipBase64` or `zipUrl`/`zipRef` (see
|
|
137
|
-
Output below), plus `shareLink` when `share: true`.
|
|
138
|
-
- **`locale`** — an App Store locale code, e.g. `de-DE` (default `en-US`). Labels the render,
|
|
139
|
-
picks which `{ "locales": … }` screenshot variants render (see
|
|
140
|
-
[Per-locale screenshots](#per-locale-screenshots--multi-locale-bundles)), and, for
|
|
141
|
-
`emit_bundle`, picks the `fastlane/screenshots/<locale>/` folder. **It does not
|
|
142
|
-
select caption text** — this server never stores or looks up Studio-typed copy (repo owns
|
|
143
|
-
the words). Pass the right words for that locale yourself in `style.captions[].text` /
|
|
144
|
-
`subtitle`.
|
|
145
|
-
|
|
146
|
-
### Per-locale screenshots & multi-locale bundles
|
|
147
|
-
|
|
148
|
-
Any `screenshots` entry (and any `emit_bundle` `panels` entry) can carry **per-locale
|
|
149
|
-
variants** instead of one image:
|
|
150
|
-
|
|
151
|
-
```json
|
|
152
|
-
{ "locales": { "en-US": { "ref": "…en…" }, "de-DE": { "ref": "…de…" } } }
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
- `render_strip({ locale: "de-DE", … })` renders the `de-DE` variant of each such entry. A
|
|
156
|
-
locale with **no variant of its own falls back** to the `en-US` variant (else the first
|
|
157
|
-
declared) — so you can ship German captions over English screenshots today and upgrade the
|
|
158
|
-
pixels later. Plain (non-`locales`) entries serve every locale. The response echoes the
|
|
159
|
-
declared locales as `screenshotLocales`.
|
|
160
|
-
- `emit_bundle({ locales: ["en-US", "de-DE"], … })` emits **ONE zip** with a
|
|
161
|
-
`fastlane/screenshots/<locale>/` folder per listed locale — `fastlane deliver` uploads every
|
|
162
|
-
locale in a single run. A single-entry list behaves exactly like `locale`; omitting `locales`
|
|
163
|
-
is the unchanged single-locale bundle.
|
|
164
|
-
- `request_screenshot_upload({ count, names, family?, locale? })` returns each slot as
|
|
165
|
-
`{ ref, uploadUrl, name?, variant }`. For `render_project`, pass that `variant` through beside
|
|
166
|
-
the ref: it is the device-family + locale cell the screenshot overrides, not a bookkeeping tag.
|
|
167
|
-
Each `name` must exactly match the saved shot's original filename (`frameName`); the filename
|
|
168
|
-
chooses the shot while `variant` chooses that shot's device-family + locale cell.
|
|
169
|
-
|
|
170
|
-
**Recommended multi-locale flow — render per locale, then compose once.** Caption text is
|
|
171
|
-
per-render input, so this is also the only way to get per-locale *captions* into one bundle,
|
|
172
|
-
and it keeps each call fast (see "Cheap preview, then compose" below):
|
|
173
|
-
|
|
174
|
-
1. Per locale `L`: `render_strip({ locale: L, output: "urls", style: { captions: [<L's words>] },
|
|
175
|
-
screenshots: […] })` → per-panel refs for `L`.
|
|
176
|
-
2. One `emit_bundle({ locales: […], bundleId, panels: [ { "locales": { "en-US": { "ref": "p1-en" },
|
|
177
|
-
"de-DE": { "ref": "p1-de" } } }, … ] })` — zips every locale **without re-rendering**.
|
|
178
|
-
|
|
179
|
-
The one-call form (`emit_bundle` with `screenshots` + `locales`) works too, but renders once
|
|
180
|
-
per locale with the *same* captions for every locale, and multiplies the slow full-res render
|
|
181
|
-
by the locale count.
|
|
182
|
-
|
|
183
|
-
### The `captions.<locale>.json` convention
|
|
184
|
-
|
|
185
|
-
The canonical source of shipping caption copy is a file **in your own repo**, one per locale,
|
|
186
|
-
keyed by **panel ordinal** (0-based, in strip order — not panel id, so the file stays stable
|
|
187
|
-
across re-renders):
|
|
188
|
-
|
|
189
|
-
A panel's entry is EITHER a single caption (`{ headline, subtitle? }`) OR an **array of layers**
|
|
190
|
-
(each `{ text, … }`, stacked in order) — use the array when you want more than a headline + one
|
|
191
|
-
subtitle, e.g. an eyebrow over a headline over a caption:
|
|
192
|
-
|
|
193
|
-
```json
|
|
194
|
-
// captions.de-DE.json
|
|
195
|
-
{
|
|
196
|
-
"0": { "headline": "Alles im Blick", "subtitle": "Deine Tage, übersichtlich." },
|
|
197
|
-
"1": [ { "text": "NEU" }, { "text": "Schneller planen" } ]
|
|
198
|
-
}
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
To render a locale: read that file, map it onto `style.captions[]` in the same order (index
|
|
202
|
-
`i` → panel `i`, `null` for a panel with no caption), and pass `locale` alongside so the
|
|
203
|
-
render/bundle is labeled and routed consistently. A single caption maps to one object; a layer
|
|
204
|
-
array maps straight through as an array:
|
|
53
|
+
| `render` | Render store screenshots from a saved project (it holds its screenshots, captions and look) or from supplied screenshots. Preview panels, full-resolution panels or a `fastlane deliver` zip. Never saves a project | `project?` or `screenshots?`, `output?` (`preview` default, `panels`, `bundle`), `outputs?`, `locale?`, `locales?` (bundle), `style?`/`look?` (supplied screenshots), `bundleId` (bundle), `share?`, `delivery?` |
|
|
54
|
+
| `save_project` | Save screenshots and styling as an editable project. 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?`, `projectName?`, `style?`/`look?`, `outputs?`, `locale?`, `appListing?` |
|
|
55
|
+
| `edit_project` | Apply concrete changes to a saved project and save it: caption words per frame and locale, frame order, layout, preset, caption style, background, device pose. Deterministic — no AI, no credits | `project`, `captions?`, `order?`, `layout?`, `preset?`, `captionStyle?`, `background?`, `frames?`, `devices?` |
|
|
56
|
+
| `read_project` | A project's full current state: frame order, per-locale caption words, locales, styling, the derived `layout`, and a `screenshots` report of which pixels the project holds | `project?` |
|
|
57
|
+
| `read_look` | A project's saved look (styling only) with its version history | `project?` |
|
|
58
|
+
| `save_look` | Save a look on a project as a new version | `look`, `project?`, `sourceName?` |
|
|
59
|
+
| `describe_look` | Design presets, layout templates, the intake script, every styling field with its default, the devices, and `edit_project`'s vocabulary | *(none)* |
|
|
60
|
+
| `search_app_listing` | Find the user's app on the App Store so a save carries its real identity. Free | `term`, `country?` |
|
|
61
|
+
| `import_screenshot` | Store screenshots from a URL, a ChatGPT attachment or (local) a path, returning a ref per entry | `screenshots`, `file?`, `name?` |
|
|
62
|
+
| `request_screenshot_upload` | Signed upload slots for screenshots on the user's machine | `count`, `names?`, `family?`, `locale?` |
|
|
63
|
+
| `delete_assets` | Permanently delete private uploads, panels or bundles this account holds | `assetIds?`, `refs?` |
|
|
64
|
+
| `account_status` | Whether this connection may render full resolution and bundles, the plan and the credit balance. Free | *(none)* |
|
|
65
|
+
| `production_operation` | Follow a queued full-resolution render or bundle: progress, result, cancel | `action`, `operationId` |
|
|
66
|
+
|
|
67
|
+
### Rendering
|
|
68
|
+
|
|
69
|
+
- **A saved project** — `render({ project })` renders the screenshots, frame order, captions and
|
|
70
|
+
look the project holds, with nothing uploaded. `project` takes an id or the exact name. Pass
|
|
71
|
+
`screenshots` with `name` (and optional `variant: { family?, locale? }`) to replace the project's
|
|
72
|
+
file of that name for this render.
|
|
73
|
+
- **Supplied screenshots** — `render({ screenshots: [{ ref }, …], style })`, one panel per entry
|
|
74
|
+
(nest entries in an array to put several phones in one panel). A render never saves;
|
|
75
|
+
`save_project` does, when the user wants to keep the work.
|
|
76
|
+
- **`output`** — `"preview"` (default) renders at about a quarter resolution for iterating;
|
|
77
|
+
`"panels"` renders full resolution; `"bundle"` builds a zip (`README.md`, `fastlane/Deliverfile`,
|
|
78
|
+
`fastlane/screenshots/<locale>/NN_shotops.png`) and needs `bundleId`. `locales: [...]` puts one
|
|
79
|
+
folder per locale in one zip; a project bundle takes each locale's own caption words from the
|
|
80
|
+
project.
|
|
81
|
+
- **`outputs`** — the devices to render (`iphone-6-9`, `iphone-6-5`). Default: the project's saved
|
|
82
|
+
devices, else `iphone-6-9`.
|
|
83
|
+
- **`delivery`** — `"inline"` image blocks or short-lived `"urls"`; omitted, it picks inline for
|
|
84
|
+
small results.
|
|
85
|
+
|
|
86
|
+
### Styling
|
|
87
|
+
|
|
88
|
+
Call `describe_look` for the authoritative complete field catalog on both the hosted MCP and the
|
|
89
|
+
local `npx shotops-mcp` stdio server. In short:
|
|
90
|
+
|
|
91
|
+
- **`style`** — `{ layout?, shotLook?, background?, captions? }`. `layout` is the panel composition
|
|
92
|
+
in one word (`standard`, `bleed`, `top-bleed`, `lean-right`, `lean-left`, `swipe`,
|
|
93
|
+
`corner-bleed`): it reserves the headline region and places the device, and any field you pass
|
|
94
|
+
explicitly overrides it. `shotLook` styles every phone alike. `captions` has one entry per panel,
|
|
95
|
+
a caption object or an array of stacked layers. Caption words are per-render input and never
|
|
96
|
+
stored in a look.
|
|
97
|
+
- **`look`** — a look exactly as `read_look` returns it; `shots[]` gives every device its own
|
|
98
|
+
styling. Pass `look` for the devices and `style.captions` for the words in one render. Set
|
|
99
|
+
`look.shots[].look.clipToFrame: true` to clip one device to its own panel.
|
|
100
|
+
- Every render grades itself: an optional `layout` block names captions sitting on devices and
|
|
101
|
+
colliding captions, and `layout.correctedLook` is a repaired look to pass straight back.
|
|
102
|
+
|
|
103
|
+
### Editing a saved project
|
|
104
|
+
|
|
105
|
+
`edit_project` takes the change itself. Frames and devices are numbered from 1, as the user counts
|
|
106
|
+
them:
|
|
205
107
|
|
|
206
|
-
```
|
|
108
|
+
```jsonc
|
|
207
109
|
{
|
|
208
|
-
"
|
|
209
|
-
"
|
|
210
|
-
{ "
|
|
211
|
-
|
|
212
|
-
]
|
|
110
|
+
"project": "Darts Tracker",
|
|
111
|
+
"captions": [
|
|
112
|
+
{ "frame": 1, "text": "Track every throw" },
|
|
113
|
+
{ "frame": 1, "text": "Jeden Wurf im Blick", "locale": "de-DE" } // adds de-DE if missing
|
|
114
|
+
],
|
|
115
|
+
"order": [2, 1, 3],
|
|
116
|
+
"layout": "bleed",
|
|
117
|
+
"devices": [{ "frame": 2, "roll": 8, "colorway": "blue" }]
|
|
213
118
|
}
|
|
214
119
|
```
|
|
215
120
|
|
|
216
|
-
|
|
217
|
-
|
|
121
|
+
A bad value is refused with the exact field and what it accepts, and nothing is written. The edit
|
|
122
|
+
is saved against the version it read, so a concurrent Studio save is never overwritten.
|
|
218
123
|
|
|
219
|
-
###
|
|
124
|
+
### Getting screenshots in
|
|
220
125
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
it ever reaches this server. Passing full-resolution screenshots inline can blow past your
|
|
224
|
-
context budget (or get silently truncated by a file-read tool) long before rendering happens.
|
|
126
|
+
Don't inline full-resolution screenshots: base64 flows through your own context first, and inline
|
|
127
|
+
entries are capped at 3MB. Instead:
|
|
225
128
|
|
|
226
|
-
|
|
129
|
+
- **`import_screenshot`** takes several sources in one call — `{ "url" }` (PNG or JPEG, fetched
|
|
130
|
+
server-side), `{ "file" }` (a ChatGPT attachment, which arrives as the top-level `file`
|
|
131
|
+
parameter, one per call) or `{ "path" }` (local stdio only) — and returns one ref per entry, in
|
|
132
|
+
order.
|
|
133
|
+
- **`request_screenshot_upload({ count, names?, family?, locale? })`** returns signed slots;
|
|
134
|
+
`curl -T screenshot.png "<uploadUrl>"` each one, then pass `{ "ref" }`. With a project, pass the
|
|
135
|
+
slot's `name` and `variant` too.
|
|
227
136
|
|
|
228
|
-
|
|
229
|
-
|
|
137
|
+
Every raw PNG is palette-optimized before rendering, with dimensions and transparency preserved.
|
|
138
|
+
Private uploads and generated files expire after the retention window unless a project keeps them;
|
|
139
|
+
`delete_assets` removes them sooner when the user asks.
|
|
230
140
|
|
|
231
|
-
|
|
232
|
-
{ "screenshots": [
|
|
233
|
-
{ "url": "https://example.com/01_home.png", "name": "01_home.png" }, // this server fetches it
|
|
234
|
-
{ "path": "/abs/02_stats.png" }, // local stdio server ONLY
|
|
235
|
-
{ "file": { "download_url": "…", "file_id": "…" } } // a ChatGPT attachment
|
|
236
|
-
] }
|
|
237
|
-
```
|
|
141
|
+
### Per-locale captions
|
|
238
142
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
`{ path }` needs the local stdio server (the hosted server never reads a caller-supplied
|
|
246
|
-
filesystem path — that would be an LFI hole). Each refusal names the door that IS open on your
|
|
247
|
-
tier. When the screenshots are only on the user's machine and you are on the hosted server, there
|
|
248
|
-
is no URL to give, so use the signed-slot flow below:
|
|
249
|
-
|
|
250
|
-
1. Call `request_screenshot_upload({ count: N, names?, family?, locale? })` — returns `N`
|
|
251
|
-
`{ ref, uploadUrl, name?, variant }` slots
|
|
252
|
-
under your own private prefix, plus a copy-paste `curl -T` example.
|
|
253
|
-
2. `curl -T screenshot.png "<uploadUrl>"` each screenshot directly (bytes never touch this
|
|
254
|
-
conversation).
|
|
255
|
-
3. Pass `{ "ref": "<ref>" }` — not inline base64 — as that screenshot's entry in
|
|
256
|
-
`render_strip`/`emit_bundle`'s `screenshots` array. For `render_project`, also pass the slot's
|
|
257
|
-
`name` and `variant`; `name` must exactly match the saved shot's original filename, and legacy
|
|
258
|
-
entries without a variant remain the base/fallback cell.
|
|
259
|
-
|
|
260
|
-
`screenshots` entries accept these shapes: an inline base64 string (small payloads only —
|
|
261
|
-
inline is capped and returns a clear "payload too large" error above 3MB decoded, pointing
|
|
262
|
-
you back at this flow), `{ ref }` from step 1, `{ url }` (an https URL this server fetches
|
|
263
|
-
itself: PNG or JPEG, converted to PNG when needed, no redirects, ~20MB cap), `{ path }` (local stdio server only — see below),
|
|
264
|
-
or `{ "locales": { "<locale>": <any of those> } }` for per-locale variants (see
|
|
265
|
-
[Per-locale screenshots](#per-locale-screenshots--multi-locale-bundles) above).
|
|
266
|
-
|
|
267
|
-
Every raw PNG is automatically palette-optimized before rendering (TinyPNG-style, locally inside
|
|
268
|
-
ShotOps): dimensions and transparency are preserved, and an already-smaller PNG is left byte-for-byte
|
|
269
|
-
unchanged. Fetched JPEGs are converted to palette PNGs at the same boundary without changing their
|
|
270
|
-
dimensions. This happens after the input reaches the MCP, so it does **not** raise the inline request
|
|
271
|
-
cap — use the ref/upload flow for a large source file.
|
|
272
|
-
|
|
273
|
-
**Output** works the same way in reverse: `render_strip`/`render_project` take an `output` field
|
|
274
|
-
— `"inline"` (standard MCP `image` blocks), `"urls"` (uploaded under your own prefix and returned
|
|
275
|
-
as short-lived `resource_link` blocks), or omit for auto (inline under ~200KB total, `urls` above).
|
|
276
|
-
The model-visible `structuredContent.panels` contains ordered delivery descriptors rather than PNG
|
|
277
|
-
base64; ChatGPT receives the complete gallery through widget-only result metadata. Programmatic
|
|
278
|
-
clients read the standard MCP content blocks. Use `"urls"` for full-resolution work.
|
|
279
|
-
|
|
280
|
-
**Cheap preview, then compose:** `render_strip({ preview: true })` renders at ~25% resolution
|
|
281
|
-
for fast, cheap styling iteration — small enough to always come back inline; don't ship it,
|
|
282
|
-
re-render without `preview` (or call `emit_bundle` directly) once the look is right. Once
|
|
283
|
-
you have full-resolution panel refs from a `render_strip({ output: "urls" })` call,
|
|
284
|
-
`emit_bundle({ panels: [{ ref }, ...] })` packages them into a bundle WITHOUT rendering again
|
|
285
|
-
— skip `screenshots` entirely in that call.
|
|
286
|
-
|
|
287
|
-
Private uploaded screenshots and generated files are retained until deleted. Treat refs as
|
|
288
|
-
short-lived and single-use, then call `delete_assets({ refs: [...] })` when the files are no
|
|
289
|
-
longer needed. Deleting a ref is irreversible and can make a saved project's referenced
|
|
290
|
-
screenshot unavailable; public privacy disclosures must accurately state this retention model.
|
|
143
|
+
The canonical source of shipping caption copy is a `captions.<locale>.json` file **in your own
|
|
144
|
+
repo**, keyed by panel ordinal (0-based). To render a locale, map it onto `style.captions[]` in the
|
|
145
|
+
same order and pass `locale`. For a saved project, write the words into the project with
|
|
146
|
+
`edit_project` instead, and `render({ project, locales: [...], output: "bundle" })` ships them all.
|
|
147
|
+
A screenshot entry may carry per-locale variants — `{ "locales": { "en-US": { "ref" }, "de-DE":
|
|
148
|
+
{ "ref" } } }` — and a locale with no variant falls back to `en-US`.
|
|
291
149
|
|
|
292
150
|
## Prerequisites
|
|
293
151
|
|
|
@@ -307,7 +165,7 @@ npm install
|
|
|
307
165
|
npm run dev # boots on http://localhost:8788/mcp (override with MCP_PORT)
|
|
308
166
|
```
|
|
309
167
|
|
|
310
|
-
Chromium boots lazily on the first `
|
|
168
|
+
Chromium boots lazily on the first `render` call, so startup is fast — and
|
|
311
169
|
that first render also installs Chromium if it isn't cached yet, adding ~20-25s to that one call.
|
|
312
170
|
`GET /health` is an unauthenticated liveness probe.
|
|
313
171
|
|
|
@@ -349,7 +207,7 @@ before any tool runs.
|
|
|
349
207
|
## After you get a bundle
|
|
350
208
|
|
|
351
209
|
```bash
|
|
352
|
-
# decode zipBase64 from
|
|
210
|
+
# decode zipBase64 from a bundle render's result to a file, then:
|
|
353
211
|
unzip shotops-appstore-upload.zip -d upload && cd upload
|
|
354
212
|
fastlane deliver # signs in as YOU, previews, uploads screenshots to a draft — never submits
|
|
355
213
|
```
|
|
@@ -405,13 +263,13 @@ agent user (not a repo collaborator) can find the connect story, since this repo
|
|
|
405
263
|
server serves it from a loopback-only static server. Vite is build/parity tooling only and is
|
|
406
264
|
pruned from production dependencies; `mockup-engine` stays reachable at build time so the
|
|
407
265
|
harness can bundle the shared engine + GLB.
|
|
408
|
-
- **Hosted render capacity is load-bearing and history-rich.** A `
|
|
266
|
+
- **Hosted render capacity is load-bearing and history-rich.** A `render` boots headless
|
|
409
267
|
Chromium + WebGL for seconds, inline in the request, and full-res multi-panel renders are slow
|
|
410
268
|
by design — seconds per panel, one panel at a time. The function duration ceiling, the memory
|
|
411
269
|
findings behind it, and the two distinct failure shapes (CPU starvation vs. OOM) are recorded in
|
|
412
270
|
`docs/reference/ops-runbook.md` §Deploy. Never cut a memory or duration setting without a green
|
|
413
271
|
`verify-mcp.mjs` run behind the claim. Client callers (agents, `verify-mcp.mjs`) should set a
|
|
414
|
-
generous per-call timeout (≥180s) for `
|
|
272
|
+
generous per-call timeout (≥180s) for `render`.
|
|
415
273
|
|
|
416
274
|
## Local — `npx shotops-mcp`
|
|
417
275
|
|
|
@@ -448,22 +306,13 @@ Chromium, which the first render downloads once as described above.
|
|
|
448
306
|
instead of `{ ref }`/`{ url }`:
|
|
449
307
|
|
|
450
308
|
```json
|
|
451
|
-
{ "screenshots": [
|
|
309
|
+
{ "screenshots": [{ "path": "/Users/you/screens/01_home.png" }] }
|
|
452
310
|
```
|
|
453
311
|
|
|
454
312
|
(`{ "path": ... }` only works over this LOCAL server — the hosted server rejects it, since
|
|
455
313
|
reading an arbitrary server-side path there would be a local-file-inclusion hole.)
|
|
456
314
|
|
|
457
|
-
|
|
458
|
-
can't produce `output: "urls"` refs. Once you are signed in on a trial or Pro, pass the on-disk PNGs
|
|
459
|
-
straight to `emit_bundle` (same local-only `{ path }` door as screenshots) to package a
|
|
460
|
-
`fastlane deliver` zip with no upload and no cloud credits:
|
|
461
|
-
|
|
462
|
-
```json
|
|
463
|
-
{ "bundleId": "com.acme.app", "panels": [{ "path": "/abs/panel-01.png" }, { "path": "/abs/panel-02.png" }] }
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
`emit_bundle` and full-resolution renders are the store-ready half of the offer, so they need that
|
|
315
|
+
Bundles and full-resolution renders are the store-ready half of the offer, so they need that
|
|
467
316
|
trial or Pro even here, where the compute is yours. Preview renders need neither an account nor
|
|
468
317
|
cloud credits. `account_status` reports which side of the line this process is on.
|
|
469
318
|
|
|
@@ -474,27 +323,27 @@ returns `status: "pending_claim"` plus `claimId` and `openUrl`, and deliberately
|
|
|
474
323
|
with its screenshots, and the link expires after 7 days. An existing `project` cannot be updated
|
|
475
324
|
without the token that owns it. Non-path inputs are refused rather than creating a blank claim.
|
|
476
325
|
|
|
477
|
-
Ordinary `
|
|
326
|
+
Ordinary `render` calls still upload **nothing** — pixels and zips are written
|
|
478
327
|
straight to your disk, and no screenshot leaves the machine. Other
|
|
479
|
-
account reads/writes (`
|
|
480
|
-
still need a token or hosted connection. A token-backed local
|
|
328
|
+
account reads/writes (`read_project`, `edit_project`, the look tools, rendering a saved `project`,
|
|
329
|
+
share links) still need a token or hosted connection. A token-backed local project render reads the cloud
|
|
481
330
|
manifest through the Studio control plane, downloads only the resolved cells, and renders locally:
|
|
482
331
|
|
|
483
332
|
| | Local (`npx shotops-mcp`) | Hosted (`shotops_…` token) |
|
|
484
333
|
|---|---|---|
|
|
485
334
|
| Render compute | **your machine** ($0, no timeout, no cloud credits on any plan) | our servers — spends your ShotOps cloud credits: 1 per preview panel, 4 per full-resolution panel |
|
|
486
335
|
| Preview render | free, no account | free on an account; 3 per 30 days, ≤5 panels, without one |
|
|
487
|
-
| Full-resolution render
|
|
336
|
+
| Full-resolution render or bundle | trial or Pro (costs no cloud credits) | trial or Pro, and spends cloud credits |
|
|
488
337
|
| Auth | none to preview; a token for account state and store-ready output | account + API token |
|
|
489
338
|
| Saved looks / editable projects / project read+re-render / share links | new project: pending claim; existing/account state: token (below); share links: hosted | ✅ |
|
|
490
339
|
| Screenshot input | local `{ path }` off disk | `request_screenshot_upload` → `{ ref }` |
|
|
491
340
|
|
|
492
341
|
### Optional: bridge a local render into your hosted account
|
|
493
342
|
|
|
494
|
-
Connect your account and `save_project` / `
|
|
495
|
-
`
|
|
343
|
+
Connect your account and `save_project` / `edit_project` / `read_project` / the look tools /
|
|
344
|
+
`render({ project })` start working too — **rendering still happens locally and spends no cloud
|
|
496
345
|
credits**, and
|
|
497
|
-
a trial or Pro account unlocks full-resolution output and
|
|
346
|
+
a trial or Pro account unlocks full-resolution output and bundles on this machine. A
|
|
498
347
|
deliberate save also uploads its local source PNGs privately so the project reopens with
|
|
499
348
|
screenshots on another device; normal renders and exports never upload.
|
|
500
349
|
|