shotops-mcp 0.7.0 → 0.9.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 +242 -88
- package/THIRD_PARTY_NOTICES +1 -0
- package/dist/local.js +21022 -5365
- package/harness-dist/assets/index-Wb7M1qGz.js +1 -0
- package/harness-dist/assets/three-addons-CvX38LO6.js +1 -0
- package/harness-dist/assets/three-webgl-DCw804Ic.js +6491 -0
- package/harness-dist/index.html +3 -1
- package/package.json +10 -4
- package/widget-dist/bundle.js +4 -4
- package/widget-dist/panels.js +3 -3
- package/harness-dist/assets/index-DvvjdOuK.js +0 -3802
package/README.md
CHANGED
|
@@ -11,20 +11,43 @@ reusable **looks** and editable **projects**, so the next release matches the la
|
|
|
11
11
|
Built for agents (ChatGPT / Claude Code / Cursor / CI). It runs over the same `@engine` render
|
|
12
12
|
spine the Studio web app and the `mockup-mcp` CLI use, driving headless Chromium (Playwright).
|
|
13
13
|
|
|
14
|
-
Ships as **two
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
[
|
|
18
|
-
|
|
14
|
+
Ships as **two doors, one engine**: an `npx shotops-mcp` **local stdio server** that renders on
|
|
15
|
+
your own machine, no account needed to preview (see [Local](#local--npx-shotops-mcp) below), and a
|
|
16
|
+
**hosted** server on Vercel with an account, saved looks, and share links (see [Hosted](#hosted)
|
|
17
|
+
below, or the public [`/connect`](https://shotops.dev/connect) page in the studio app).
|
|
18
|
+
|
|
19
|
+
## Preview free, export store-ready with Pro
|
|
20
|
+
|
|
21
|
+
Composing and previewing a strip is free through either door — every device, caption and locale, no
|
|
22
|
+
watermark, and locally without an account at all. **Store-ready output needs an active trial or
|
|
23
|
+
Pro:** a full-resolution `render_strip`/`render_project`, and every `emit_bundle` form, including
|
|
24
|
+
one that only re-zips panels you already rendered. That holds wherever the render happens; local
|
|
25
|
+
compute being free is not the same as local output being free.
|
|
26
|
+
|
|
27
|
+
The two limits are independent and neither substitutes for the other:
|
|
28
|
+
|
|
29
|
+
- **Entitlement** is permission to create store-ready files. One server-owned decision answers it
|
|
30
|
+
for Studio, hosted and local alike, so no door can be talked into a different answer. Free and
|
|
31
|
+
anonymous callers are refused before a credit is reserved, a screenshot is read or a file is
|
|
32
|
+
written — a refused call costs nothing and leaves nothing behind.
|
|
33
|
+
- **Credits** price compute ShotOps supplies: 0.5 per hosted preview panel, 2 per hosted
|
|
34
|
+
full-resolution panel, plus AI. Rendering on your own machine spends none, on any plan. More
|
|
35
|
+
credits never unlock store-ready output on Free.
|
|
36
|
+
|
|
37
|
+
Free accounts get 40 credits a month and 2 synced projects. Every new account starts with a 7-day
|
|
38
|
+
Pro trial; after that Pro is €9/month or €90/year. Without any account, the hosted server allows 3
|
|
39
|
+
preview strips per rolling 30 days, at most 5 panels each. Call `account_status` to see exactly
|
|
40
|
+
where this connection stands — it spends no credits and none of that anonymous allowance.
|
|
19
41
|
|
|
20
42
|
## Zero-custody
|
|
21
43
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
44
|
+
The hosted server and every MCP tool **never touch a store-signing credential.** `emit_bundle`
|
|
45
|
+
hands you a zip; **you** upload it with **your own** `fastlane`, signed in as yourself. The
|
|
46
|
+
generated `Deliverfile` is screenshots-only and **never submits for review**.
|
|
47
|
+
|
|
48
|
+
The one explicit exception is the local `release setup` CLI below. It validates an existing `.p8`
|
|
49
|
+
on your machine directly with Fastlane and Apple, then stores only its path and public identifiers.
|
|
50
|
+
The key is never copied, printed, or sent to ShotOps, and no release tool is installed or upgraded.
|
|
28
51
|
|
|
29
52
|
## Tools
|
|
30
53
|
|
|
@@ -36,12 +59,15 @@ share links) — nothing that could reach a store on your behalf.
|
|
|
36
59
|
| `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
60
|
| `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
61
|
| `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?` |
|
|
62
|
+
| `refine_project` | Run one bounded Agent turn against a saved project. A supported request commits once and returns the actual diff; ambiguity, missing input, unsupported intent, conflict, and failure return a typed no-write outcome | `project?`, `instruction`, `focus?` |
|
|
39
63
|
| `save_look` | Persist a composed look on a project (styling only — appends a new version) | `look`, `project?`, `sourceName?` |
|
|
40
64
|
| `hold_look` | Hold which saved version agents render by default (replaces pinning) — the version must exist (`read_look` `versions`) | `version`, `project?` |
|
|
41
65
|
| `release_look` | Clear the held version — agents Follow latest (render the newest saved look) | `project?` |
|
|
42
|
-
| `describe_look` | Five named design presets,
|
|
66
|
+
| `describe_look` | Five named design presets, three named layout templates, the user-intake script, and the full styling field catalog + defaults | *(none)* |
|
|
43
67
|
| `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
68
|
| `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?` |
|
|
69
|
+
| `account_status` | What THIS connection may do, for free: signed-in account, plan, trial, remaining ShotOps credits, what a preview costs here, and whether store-ready output (full-resolution renders, any `emit_bundle`) is allowed — with a machine-readable `nextStep`. Spends no credits, uses none of the free anonymous preview allowance, uploads and writes nothing | *(none)* |
|
|
70
|
+
| `production_operation` | Reconnect to one durable paid Hosted render or rendering bundle, report persisted progress, request cooperative cancellation, or mint fresh result download grants without rerendering or charging again | `action` (`get`, `cancel`, or `result`), `operationId` |
|
|
45
71
|
| `delete_assets` | Permanently delete private uploaded screenshots, rendered panels, or generated bundles owned by the signed-in account | `refs` (1–50 ShotOps refs) |
|
|
46
72
|
|
|
47
73
|
- **`panelPresetId`** — App Store size: `r69` (default, 6.9″ iPhone 1290×2796), `r65`, `r55`.
|
|
@@ -50,22 +76,24 @@ share links) — nothing that could reach a store on your behalf.
|
|
|
50
76
|
accepted set is `RENDERABLE_PANEL_PRESET_IDS` in `shotops-mcp/src/panelCatalog.ts`, a subset of
|
|
51
77
|
the hand-kept `PANEL_PRESET_IDS` copy of `mockup-engine/appstore.ts`'s `PANEL_PRESETS` — when
|
|
52
78
|
Apple revises required sizes, add the preset in the engine first, then mirror it here.
|
|
53
|
-
- **`style`** — the structured styling input
|
|
79
|
+
- **`style`** — the structured styling input. On both the hosted MCP and the local
|
|
80
|
+
`npx shotops-mcp` stdio server, call `describe_look` for the authoritative complete field
|
|
81
|
+
catalog and defaults:
|
|
54
82
|
`{ layout?, shotLook?, background?, captions? }`. `shotLook` is **one device for every phone**;
|
|
55
83
|
`captions` is one entry PER PANEL in slot order — each entry is EITHER a single caption object OR
|
|
56
84
|
an **array of caption layers** stacked on that panel (headline + subline + …). Each layer's
|
|
57
85
|
`text` (and a single caption's `subtitle`) is per-RENDER input (never stored — a look carries
|
|
58
86
|
styling only). Captions auto-layout in a band at the top of the panel, and the device does NOT
|
|
59
87
|
move to make room.
|
|
60
|
-
- **`style.layout`** — the panel **composition** in one word: `"standard"
|
|
61
|
-
the headline's reserved region
|
|
62
|
-
|
|
88
|
+
- **`style.layout`** — the panel **composition** in one word: `"standard"`, `"bleed"`, or
|
|
89
|
+
`"top-bleed"`. It settles the headline's reserved region, device placement, camera pose, and
|
|
90
|
+
roll together. `standard` (the shipped default) puts the whole device
|
|
63
91
|
under a 2-line region — about 36 characters — and crops nothing; `bleed` reserves 4 lines (~72
|
|
64
92
|
characters) and runs the device 10% off the bottom edge. The region is **reserved, not fitted**:
|
|
65
93
|
it is held at full size whether or not the headline fills it, which is what makes every panel in
|
|
66
94
|
a swiped set land on the same line. **Precedence** — the template expands *first*, then any
|
|
67
|
-
`shotLook
|
|
68
|
-
|
|
95
|
+
explicit `shotLook` composition field or caption layout field you also pass **overrides** it
|
|
96
|
+
(`{ layout: "bleed", shotLook: { vOffset: "45" } }` is "bleed, but a bit lower").
|
|
69
97
|
A pure macro: nothing stores the template id, so `save_project` persists the expanded values and
|
|
70
98
|
`read_look` returns them unchanged. **The id is not lost, though — it is DERIVED back:**
|
|
71
99
|
`read_project` and `read_look` both return a `layout` report read out of the values themselves, so
|
|
@@ -93,10 +121,10 @@ share links) — nothing that could reach a store on your behalf.
|
|
|
93
121
|
background, `style.captions` supplies the words, in one render. (Alone, `style` styles every
|
|
94
122
|
phone identically. If you pass `style.shotLook`/`style.background` *alongside* a `look`, the look
|
|
95
123
|
wins and the result carries a note.) Also composes with `useSavedLook`/`version`.
|
|
96
|
-
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
124
|
+
- **Clipping** — set `look.shots[].look.clipToFrame: true` to clip one device's complete composite
|
|
125
|
+
(body, screen, shadow, and reflection) to its own panel; omit it or use `false` for overflow.
|
|
126
|
+
Pass top-level `clip: "panel"` to clip every device, or `clip: "strip"` to force continuous
|
|
127
|
+
overflow. Call `describe_look` for the complete Look contract on either MCP tier.
|
|
100
128
|
- **`version`** — render a specific saved look version (implies the saved look). Without it,
|
|
101
129
|
a project with a PINNED version renders the pin; otherwise the latest saved look.
|
|
102
130
|
- **`emit_bundle` output** — a zip (`README.md`, `fastlane/Deliverfile`,
|
|
@@ -208,7 +236,7 @@ own `error` and never cancels the others — re-import just that one. In ChatGPT
|
|
|
208
236
|
arrives as the top-level `file` parameter instead of an array entry, because the Apps SDK cannot
|
|
209
237
|
fill a file parameter nested inside an array; you can pass both in the same call.
|
|
210
238
|
|
|
211
|
-
`{ url }` and `{ file }` need the hosted server (it stores the fetched PNG under your account);
|
|
239
|
+
`{ url }` and `{ file }` need the hosted server (it stores the fetched screenshot as PNG under your account);
|
|
212
240
|
`{ path }` needs the local stdio server (the hosted server never reads a caller-supplied
|
|
213
241
|
filesystem path — that would be an LFI hole). Each refusal names the door that IS open on your
|
|
214
242
|
tier. When the screenshots are only on the user's machine and you are on the hosted server, there
|
|
@@ -227,19 +255,22 @@ is no URL to give, so use the signed-slot flow below:
|
|
|
227
255
|
`screenshots` entries accept these shapes: an inline base64 string (small payloads only —
|
|
228
256
|
inline is capped and returns a clear "payload too large" error above 3MB decoded, pointing
|
|
229
257
|
you back at this flow), `{ ref }` from step 1, `{ url }` (an https URL this server fetches
|
|
230
|
-
itself:
|
|
258
|
+
itself: PNG or JPEG, converted to PNG when needed, no redirects, ~20MB cap), `{ path }` (local stdio server only — see below),
|
|
231
259
|
or `{ "locales": { "<locale>": <any of those> } }` for per-locale variants (see
|
|
232
260
|
[Per-locale screenshots](#per-locale-screenshots--multi-locale-bundles) above).
|
|
233
261
|
|
|
234
262
|
Every raw PNG is automatically palette-optimized before rendering (TinyPNG-style, locally inside
|
|
235
263
|
ShotOps): dimensions and transparency are preserved, and an already-smaller PNG is left byte-for-byte
|
|
236
|
-
unchanged.
|
|
264
|
+
unchanged. Fetched JPEGs are converted to palette PNGs at the same boundary without changing their
|
|
265
|
+
dimensions. This happens after the input reaches the MCP, so it does **not** raise the inline request
|
|
237
266
|
cap — use the ref/upload flow for a large source file.
|
|
238
267
|
|
|
239
|
-
**Output** works the same way in reverse: `render_strip`/`
|
|
240
|
-
— `"inline"` (
|
|
241
|
-
|
|
242
|
-
|
|
268
|
+
**Output** works the same way in reverse: `render_strip`/`render_project` take an `output` field
|
|
269
|
+
— `"inline"` (standard MCP `image` blocks), `"urls"` (uploaded under your own prefix and returned
|
|
270
|
+
as short-lived `resource_link` blocks), or omit for auto (inline under ~200KB total, `urls` above).
|
|
271
|
+
The model-visible `structuredContent.panels` contains ordered delivery descriptors rather than PNG
|
|
272
|
+
base64; ChatGPT receives the complete gallery through widget-only result metadata. Programmatic
|
|
273
|
+
clients read the standard MCP content blocks. Use `"urls"` for full-resolution work.
|
|
243
274
|
|
|
244
275
|
**Cheap preview, then compose:** `render_strip({ preview: true })` renders at ~25% resolution
|
|
245
276
|
for fast, cheap styling iteration — small enough to always come back inline; don't ship it,
|
|
@@ -256,7 +287,7 @@ screenshot unavailable; public privacy disclosures must accurately state this re
|
|
|
256
287
|
## Prerequisites
|
|
257
288
|
|
|
258
289
|
1. **A ShotOps API token.** Sign in at the Studio app → account menu → **API tokens** →
|
|
259
|
-
create one. It looks like `
|
|
290
|
+
create one. It looks like `shotops_9Zq3Xr7T…` and is
|
|
260
291
|
shown once — copy it then.
|
|
261
292
|
2. **Supabase service env** (already in the repo's `.env.local` for local dev): `SUPABASE_URL`
|
|
262
293
|
and `SUPABASE_SERVICE_ROLE_KEY`. The server loads `../.env.local` automatically.
|
|
@@ -280,7 +311,7 @@ Deploy the hosted server at a public HTTPS origin, enable Developer Mode in Chat
|
|
|
280
311
|
an app whose MCP server URL is `https://<your-host>/mcp`. Do not paste a personal `shotops_` token
|
|
281
312
|
into the app definition: ChatGPT discovers the protected-resource metadata from the endpoint and
|
|
282
313
|
runs the server's OAuth + PKCE browser sign-in flow. This is a tool-only app — no widget or UI
|
|
283
|
-
resource is required. User-attached PNGs enter through `import_screenshot`.
|
|
314
|
+
resource is required. User-attached PNGs and JPEGs enter through `import_screenshot`.
|
|
284
315
|
|
|
285
316
|
For public plugin submission, OpenAI supplies a domain-verification token after the MCP domain
|
|
286
317
|
is entered in the Platform dashboard. Set it on the hosted server as
|
|
@@ -294,7 +325,7 @@ tests without private fixture data or third-party hosting.
|
|
|
294
325
|
|
|
295
326
|
```bash
|
|
296
327
|
claude mcp add --transport http shotops http://localhost:8788/mcp \
|
|
297
|
-
--header "Authorization: Bearer
|
|
328
|
+
--header "Authorization: Bearer shotops_…"
|
|
298
329
|
```
|
|
299
330
|
|
|
300
331
|
Then ask the agent to, e.g., *"render these two screenshots into a 6.9″ App Store strip and
|
|
@@ -322,14 +353,14 @@ fastlane deliver # signs in as YOU, previews, uploads screenshots to a dr
|
|
|
322
353
|
The server also runs hosted, 24/7-reachable (scale-to-zero when idle), at:
|
|
323
354
|
|
|
324
355
|
```
|
|
325
|
-
https://shotops.dev/mcp
|
|
356
|
+
https://mcp.shotops.dev/mcp
|
|
326
357
|
```
|
|
327
358
|
|
|
328
359
|
Nothing to install — point any MCP client at that URL with your `Authorization: Bearer
|
|
329
360
|
shotops_…` header, same as local dev, just swap the base URL:
|
|
330
361
|
|
|
331
362
|
```bash
|
|
332
|
-
claude mcp add --transport http shotops https://shotops.dev/mcp \
|
|
363
|
+
claude mcp add --transport http shotops https://mcp.shotops.dev/mcp \
|
|
333
364
|
--header "Authorization: Bearer shotops_…"
|
|
334
365
|
```
|
|
335
366
|
|
|
@@ -340,54 +371,49 @@ since this repo is private.
|
|
|
340
371
|
|
|
341
372
|
### Ops (this repo's maintainer only)
|
|
342
373
|
|
|
343
|
-
-
|
|
344
|
-
|
|
374
|
+
`shotops-mcp` serves from its own Vercel project on `https://mcp.shotops.dev` (health:
|
|
375
|
+
`/health`, MCP endpoint: `/mcp`). **`docs/reference/ops-runbook.md` §Deploy owns the procedure**
|
|
376
|
+
— the notes here are only what is specific to this package.
|
|
377
|
+
|
|
378
|
+
- **Deploy:** run the repo's deploy script from the repo root; deploys run locally, and a push to
|
|
379
|
+
`main` is a production deploy. Do not hand-run a bare `vercel deploy`. Fly was retired (#68) —
|
|
380
|
+
`flyctl` is history, not the live procedure.
|
|
381
|
+
- **Runtime env** lives on the `shotops-mcp` **Vercel project**, not in this repo:
|
|
382
|
+
`SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`, and `MCP_PUBLIC_URL` (pins the public hostname as
|
|
383
|
+
the OAuth issuer), plus `OPENAI_APPS_CHALLENGE_TOKEN` while verifying the MCP domain for public
|
|
384
|
+
ChatGPT submission — nothing Apple-related, ever, by construction. Vercel env applies at the
|
|
385
|
+
NEXT deployment, never to the running one.
|
|
386
|
+
- **Verify after any deploy** — a green deploy isn't proof the server answers MCP calls:
|
|
345
387
|
```bash
|
|
346
|
-
|
|
388
|
+
cd shotops-mcp && SHOTOPS_MCP_TOKEN=… node verify-mcp.mjs https://mcp.shotops.dev/mcp
|
|
347
389
|
```
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
node
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
|
372
|
-
a single shared vCPU it can starve the whole process — health checks stop responding,
|
|
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
|
|
378
|
-
drop back to `shared-cpu-1x` to save cost — a render-triggering tool call is expected to
|
|
379
|
-
occasionally saturate a whole core, and 1x has no second core to keep the process alive
|
|
380
|
-
while that happens. Client callers (agents, `verify-mcp.mjs`) should still set a generous
|
|
381
|
-
per-call timeout (≥180s) for `render_strip`/`emit_bundle` — even warm, cold-machine renders
|
|
382
|
-
can take 60-110s before the CPU fix, and network/Supabase round trips add more on `urls`
|
|
383
|
-
output.
|
|
384
|
-
|
|
385
|
-
## Local (free tier) — `npx shotops-mcp`
|
|
390
|
+
Exercises the exact registered tool set, real render/bundle/project round-trips, signed upload
|
|
391
|
+
refs, asset deletion, and asserts no-token / bad-token both get `401`. It needs the **Studio**
|
|
392
|
+
deployment live too (`delete_project` is a control-plane op) and leaves the account as it found
|
|
393
|
+
it (#333). Add `--artifact-dir <private-dir>` for the ChatGPT portal handoff.
|
|
394
|
+
- **Logs / status:** the `shotops-mcp` project in the Vercel dashboard.
|
|
395
|
+
- **Production builds through Nitro** — `nitro.config.ts` (`preset: 'node-server'`,
|
|
396
|
+
`vercel: { entryFormat: 'node' }`) compiles the Express graph into `dist-server/`. `Dockerfile`
|
|
397
|
+
builds the RETIRED Fly image only; it is pinned to `mcr.microsoft.com/playwright:v1.61.1-noble`
|
|
398
|
+
to match the `playwright` npm version in `package.json`, so if you bump one, bump the other.
|
|
399
|
+
- The build compiles the browser harness once (`build:harness` → `harness-dist/`) and the running
|
|
400
|
+
server serves it from a loopback-only static server. Vite is build/parity tooling only and is
|
|
401
|
+
pruned from production dependencies; `mockup-engine` stays reachable at build time so the
|
|
402
|
+
harness can bundle the shared engine + GLB.
|
|
403
|
+
- **Hosted render capacity is load-bearing and history-rich.** A `render_strip` boots headless
|
|
404
|
+
Chromium + WebGL for seconds, inline in the request, and full-res multi-panel renders are slow
|
|
405
|
+
by design — seconds per panel, one panel at a time. The function duration ceiling, the memory
|
|
406
|
+
findings behind it, and the two distinct failure shapes (CPU starvation vs. OOM) are recorded in
|
|
407
|
+
`docs/reference/ops-runbook.md` §Deploy. Never cut a memory or duration setting without a green
|
|
408
|
+
`verify-mcp.mjs` run behind the claim. Client callers (agents, `verify-mcp.mjs`) should set a
|
|
409
|
+
generous per-call timeout (≥180s) for `render_strip`/`emit_bundle`.
|
|
410
|
+
|
|
411
|
+
## Local — `npx shotops-mcp`
|
|
386
412
|
|
|
387
413
|
The SAME server also runs as a **local stdio MCP**, entirely on your own machine — no URL, no
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
transport and a few account-shaped tools differ.
|
|
414
|
+
hosted cost, and no render-timeout ceiling (the hosted server's one real limit — see "Ops" above).
|
|
415
|
+
Previewing needs no account. It's the same `registerTools`/render engine as the hosted server; only
|
|
416
|
+
the transport and a few account-shaped tools differ.
|
|
391
417
|
|
|
392
418
|
```bash
|
|
393
419
|
claude mcp add shotops -- npx -y shotops-mcp
|
|
@@ -410,14 +436,19 @@ instead of `{ ref }`/`{ url }`:
|
|
|
410
436
|
(`{ "path": ... }` only works over this LOCAL server — the hosted server rejects it, since
|
|
411
437
|
reading an arbitrary server-side path there would be a local-file-inclusion hole.)
|
|
412
438
|
|
|
413
|
-
**`emit_bundle` reads local panels off disk too** —
|
|
414
|
-
produce `output: "urls"` refs.
|
|
415
|
-
(same local-only `{ path }` door as screenshots) to package a
|
|
439
|
+
**`emit_bundle` reads local panels off disk too** — an unsigned local server has no account, so it
|
|
440
|
+
can't produce `output: "urls"` refs. Once you are signed in on a trial or Pro, pass the on-disk PNGs
|
|
441
|
+
straight to `emit_bundle` (same local-only `{ path }` door as screenshots) to package a
|
|
442
|
+
`fastlane deliver` zip with no upload and no credits:
|
|
416
443
|
|
|
417
444
|
```json
|
|
418
445
|
{ "bundleId": "com.acme.app", "panels": [{ "path": "/abs/panel-01.png" }, { "path": "/abs/panel-02.png" }] }
|
|
419
446
|
```
|
|
420
447
|
|
|
448
|
+
`emit_bundle` and full-resolution renders are the store-ready half of the offer, so they need that
|
|
449
|
+
trial or Pro even here, where the compute is yours. Preview renders need neither an account nor
|
|
450
|
+
credits. `account_status` reports which side of the line this process is on.
|
|
451
|
+
|
|
421
452
|
**Saving without a token:** a new `save_project` made from local `{ path }` PNGs creates a private
|
|
422
453
|
**pending claim**. It uploads only that saved project's byte-free record and raw source PNGs,
|
|
423
454
|
returns `status: "pending_claim"` plus `claimId` and `openUrl`, and deliberately returns no
|
|
@@ -425,22 +456,26 @@ returns `status: "pending_claim"` plus `claimId` and `openUrl`, and deliberately
|
|
|
425
456
|
with its screenshots, and the link expires after 7 days. An existing `project` cannot be updated
|
|
426
457
|
without the token that owns it. Non-path inputs are refused rather than creating a blank claim.
|
|
427
458
|
|
|
428
|
-
Ordinary `render_strip` / `emit_bundle` calls still upload **nothing** and
|
|
459
|
+
Ordinary `render_strip` / `emit_bundle` calls still upload **nothing** — pixels and zips are written
|
|
460
|
+
straight to your disk, and no screenshot leaves the machine. Other
|
|
429
461
|
account reads/writes (`read_look`, `save_look`, `read_project`, `render_project`, share links)
|
|
430
462
|
still need a token or hosted connection. A token-backed local `render_project` reads the cloud
|
|
431
463
|
manifest through the Studio control plane, downloads only the resolved cells, and renders locally:
|
|
432
464
|
|
|
433
|
-
| | Local (
|
|
465
|
+
| | Local (`npx shotops-mcp`) | Hosted (`shotops_…` token) |
|
|
434
466
|
|---|---|---|
|
|
435
|
-
| Render compute | **your machine** ($0, no timeout) | our servers — spends your ShotOps credits:
|
|
436
|
-
|
|
|
467
|
+
| Render compute | **your machine** ($0, no timeout, no credits on any plan) | our servers — spends your ShotOps credits: 0.5 per preview panel, 2 per full-resolution panel |
|
|
468
|
+
| Preview render | free, no account | free on an account; 3 per 30 days, ≤5 panels, without one |
|
|
469
|
+
| Full-resolution render, any `emit_bundle` | trial or Pro (costs no credits) | trial or Pro, and spends credits |
|
|
470
|
+
| Auth | none to preview; a token for account state and store-ready output | account + API token |
|
|
437
471
|
| Saved looks / editable projects / project read+re-render / share links | new project: pending claim; existing/account state: token (below); share links: hosted | ✅ |
|
|
438
472
|
| Screenshot input | local `{ path }` off disk | `request_screenshot_upload` → `{ ref }` |
|
|
439
473
|
|
|
440
474
|
### Optional: bridge a local render into your hosted account
|
|
441
475
|
|
|
442
476
|
Connect your account and `save_project` / `read_look` / `save_look` / `read_project` /
|
|
443
|
-
`render_project` start working too — **rendering still happens locally and
|
|
477
|
+
`render_project` start working too — **rendering still happens locally and spends no credits**, and
|
|
478
|
+
a trial or Pro account unlocks full-resolution output and `emit_bundle` on this machine. A
|
|
444
479
|
deliberate save also uploads its local source PNGs privately so the project reopens with
|
|
445
480
|
screenshots on another device; normal renders and exports never upload.
|
|
446
481
|
|
|
@@ -466,6 +501,124 @@ SHOTOPS_TOKEN=shotops_… claude mcp add shotops -- npx -y shotops-mcp
|
|
|
466
501
|
`--token` wins over `SHOTOPS_TOKEN`, which wins over a saved browser login — so a token in the
|
|
467
502
|
environment is never quietly shadowed by whoever last signed in on that machine.
|
|
468
503
|
|
|
504
|
+
### Local Apple release setup
|
|
505
|
+
|
|
506
|
+
Before preparing an App Store release, validate the Apple tooling and API key already on this
|
|
507
|
+
machine:
|
|
508
|
+
|
|
509
|
+
```bash
|
|
510
|
+
npx shotops-mcp release setup
|
|
511
|
+
# non-interactive form:
|
|
512
|
+
npx shotops-mcp release setup \
|
|
513
|
+
--issuer-id "$APP_STORE_CONNECT_ISSUER_ID" \
|
|
514
|
+
--key-id "$APP_STORE_CONNECT_KEY_ID" \
|
|
515
|
+
--key-path /secure/AuthKey_ABC123DEFG.p8
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
The command requires an owner-only `.p8`, Fastlane, and Apple Transporter; parses the key through
|
|
519
|
+
Fastlane; and makes read-only App Store Connect app-list requests to prove the key's real access.
|
|
520
|
+
It does not install or upgrade anything. Only Issuer ID, Key ID, and the canonical key path are
|
|
521
|
+
saved in `~/.shotops/release.json` (or `$SHOTOPS_CONFIG_DIR/release.json`); a failed rerun leaves the
|
|
522
|
+
last valid setup untouched.
|
|
523
|
+
|
|
524
|
+
For CI, create secret inputs named `APP_STORE_CONNECT_ISSUER_ID`,
|
|
525
|
+
`APP_STORE_CONNECT_KEY_ID`, and `APP_STORE_CONNECT_PRIVATE_KEY`. The setup command names these
|
|
526
|
+
inputs but never prints their values.
|
|
527
|
+
|
|
528
|
+
### Prepare a deterministic desired release
|
|
529
|
+
|
|
530
|
+
Once signed in to ShotOps, assemble the saved owner project and repository into one local,
|
|
531
|
+
content-addressed workspace — before reading or changing App Store Connect:
|
|
532
|
+
|
|
533
|
+
```bash
|
|
534
|
+
npx shotops-mcp release prepare --project "My project"
|
|
535
|
+
|
|
536
|
+
# Optional signed build and an explicit partial scope:
|
|
537
|
+
npx shotops-mcp release prepare \
|
|
538
|
+
--project proj_123 \
|
|
539
|
+
--build build/MyApp.ipa \
|
|
540
|
+
--locales en-US,de-DE \
|
|
541
|
+
--outputs iphone-6-9
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
The command renders every authored locale and selected output by default with the same engine as
|
|
545
|
+
Studio and the MCP. `--locales` or `--outputs` deliberately narrows the run and records every
|
|
546
|
+
omission as a **partial** scope. It writes `desired-release.json` plus the rendered PNGs under
|
|
547
|
+
`~/.shotops/release-workspaces/<desired-id>/` (or `--output <directory>`), with user-only
|
|
548
|
+
permissions. Identical project, repository, scope, and build inputs produce the same manifest ID
|
|
549
|
+
and pixels.
|
|
550
|
+
|
|
551
|
+
Repository input conventions are explicit so no Ruby is ever evaluated:
|
|
552
|
+
|
|
553
|
+
- `fastlane/metadata/<locale>/*.txt` — supported localized Deliver metadata;
|
|
554
|
+
- `fastlane/metadata/*.txt` and `review_information/*.txt` — app and review metadata;
|
|
555
|
+
- `fastlane/metadata/app_store_rating_config.json` — Fastlane's current age-rating shape;
|
|
556
|
+
- `fastlane/metadata/submission_information.json` — export/content compliance fields;
|
|
557
|
+
- `fastlane/metadata/app_review_attachment_file.txt` — empty clears the attachment; otherwise it
|
|
558
|
+
names a repository-relative supported attachment;
|
|
559
|
+
- `fastlane/app-previews/<locale>/*` — `.mov`, `.mp4`, or `.m4v`, with the ASC device token in
|
|
560
|
+
each filename (for example `APP_IPHONE_67_01_demo.mp4`).
|
|
561
|
+
|
|
562
|
+
Missing files mean **unchanged**; an existing supported empty text file means **clear**. Unknown
|
|
563
|
+
files, fields, locales, outputs, unreadable artifacts, and Bundle ID disagreements fail closed.
|
|
564
|
+
Fastfile and Deliverfile are never executed. Optional `.ipa`/supported `.pkg` bytes are hashed and
|
|
565
|
+
referenced, never copied into ShotOps state. This step performs no ASC request, upload, version
|
|
566
|
+
creation, signing, submission, or release.
|
|
567
|
+
|
|
568
|
+
### Create the exact App Store Connect plan
|
|
569
|
+
|
|
570
|
+
Read the editable App Store version and bind it to the prepared workspace without changing Apple:
|
|
571
|
+
|
|
572
|
+
```bash
|
|
573
|
+
npx shotops-mcp release plan \
|
|
574
|
+
--workspace ~/.shotops/release-workspaces/<desired-id> \
|
|
575
|
+
--repo .
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
The command resolves the Bundle ID to one accessible app and one editable iOS version, then reads
|
|
579
|
+
the supported metadata, review, age-rating, compliance, build, screenshot, and App Preview
|
|
580
|
+
baseline. It writes a user-only `release-plan.json` containing every operation as a member of one
|
|
581
|
+
closed, typed vocabulary — each carrying its own target, the precondition it needs, the sealed bytes
|
|
582
|
+
it sends, the postcondition it must reach, and a redacted summary — plus a portable
|
|
583
|
+
`release-review.zip` containing that exact canonical plan and its content-addressed screenshot bytes.
|
|
584
|
+
|
|
585
|
+
The plan hash-binds the project revision, desired bytes, normalized Fastlane inputs, Apple setup
|
|
586
|
+
identity and policy version, so later approval cannot drift onto different state. Its **approval
|
|
587
|
+
window is 15 minutes**; the plan itself stays readable for as long as its sealed workspace exists.
|
|
588
|
+
Concurrency is per resource rather than one hash of the whole app: an unrelated App Store edit is
|
|
589
|
+
reported without invalidating the plan, while a change to a resource the plan writes — or to the
|
|
590
|
+
app, version or App Store state it targets — blocks before any write.
|
|
591
|
+
|
|
592
|
+
`release-plan-envelope.json` is the separate redacted form safe for a later ShotOps coordination
|
|
593
|
+
step: it carries keyed commitments and digests, never metadata values, review credentials, local
|
|
594
|
+
paths, Apple credentials, or asset bytes. Identifiers are committed under a per-plan key that stays
|
|
595
|
+
on your machine, so the envelope cannot be used to confirm a guess at your Bundle ID, Apple key ID
|
|
596
|
+
or version string. This command talks directly to Apple with GET requests
|
|
597
|
+
only; it never uploads, creates a version, commits, submits, or sends the private key to ShotOps.
|
|
598
|
+
|
|
599
|
+
### Review the exact plan locally
|
|
600
|
+
|
|
601
|
+
From the plan directory, open its read-only local browser review:
|
|
602
|
+
|
|
603
|
+
```bash
|
|
604
|
+
npx shotops-mcp release review
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
To review an artifact downloaded from owner-controlled CI, pass it directly:
|
|
608
|
+
|
|
609
|
+
```bash
|
|
610
|
+
npx shotops-mcp release review release-review.zip
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
The command verifies the exact plan hash, artifact manifest, and every embedded screenshot before
|
|
614
|
+
opening a nonce-protected URL on `127.0.0.1` with a random port. The page shows target app and team
|
|
615
|
+
fingerprint, Bundle ID, version, the approval window, complete or partial scope, every
|
|
616
|
+
before/after/clear, build, review, rating, compliance, preview, screenshot order/replacement/omission,
|
|
617
|
+
unchanged operation, and the operations excluded by the plan. Partial-scope and no-build warnings
|
|
618
|
+
cannot be dismissed. It loads no remote resource, sends nothing, and has no edit or approval action.
|
|
619
|
+
A plan past its approval window opens read-only and says why; corrupt, edited, or content-mismatched
|
|
620
|
+
inputs fail before a server is started.
|
|
621
|
+
|
|
469
622
|
The saved project remembers the exact on-disk folder your screenshots came from (not just their
|
|
470
623
|
filenames), so re-opening it on the SAME machine can point right back at it. Share links aren't
|
|
471
624
|
bridged yet — they'd need a hosted rendering step, which would defeat local rendering's whole
|
|
@@ -496,7 +649,7 @@ shotops-mcp/
|
|
|
496
649
|
uploads.ts Phase 10 — upload slots, ref ownership check, output:'urls' storage
|
|
497
650
|
screenshotInput.ts Phase 10 — resolves screenshots[] (inline/ref/url/path) to base64
|
|
498
651
|
env.ts loads ../.env.local; MCP_PORT / STUDIO_ORIGIN / MCP_PUBLIC_URL
|
|
499
|
-
local.ts the LOCAL
|
|
652
|
+
local.ts the LOCAL npx entry point — stdio transport, no auth required to preview
|
|
500
653
|
localDeps.ts the local ToolDeps fork — static-serve renderer, no-op/bridged persistence
|
|
501
654
|
hostedBridge.ts local→hosted bridge (a real MCP client using your shotops_ token)
|
|
502
655
|
harness/ the browser render page Chromium loads (render.js exposes window.renderStrip)
|
|
@@ -504,7 +657,8 @@ shotops-mcp/
|
|
|
504
657
|
vite.harness.config.mjs the STATIC harness build (local mode's publish prerequisite) → harness-dist/
|
|
505
658
|
build.mjs esbuild bundle of src/local.ts (+ @engine/@api inlined) → dist/local.js
|
|
506
659
|
Dockerfile hosted image — Playwright's Chromium base + this repo's 3 npm installs
|
|
507
|
-
fly.toml Fly app config —
|
|
660
|
+
fly.toml Fly app config — RETIRED (#68), kept only for the cold-deploy recipe;
|
|
661
|
+
the running fly.dev machine is NOT a rollback
|
|
508
662
|
verify-mcp.mjs post-deploy smoke test — real MCP round-trip against a hosted URL
|
|
509
663
|
verify-mcp-local.ts local stdio smoke test (tsx src/local.ts, in-repo)
|
|
510
664
|
static-parity.ts pixel-diffs the static-serve render against the Vite-dev render (must be 0)
|
package/THIRD_PARTY_NOTICES
CHANGED
|
@@ -71,6 +71,7 @@ Space Grotesk
|
|
|
71
71
|
Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk)
|
|
72
72
|
|
|
73
73
|
Each font is licensed under the SIL Open Font License, Version 1.1:
|
|
74
|
+
https://openfontlicense.org/open-font-license-official-text/
|
|
74
75
|
|
|
75
76
|
-----------------------------------------------------------
|
|
76
77
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|