storeframe-mcp 0.1.0 → 0.2.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 CHANGED
@@ -25,11 +25,14 @@ share links) — nothing that could reach a store on your behalf.
25
25
  | Tool | What it does | Key inputs |
26
26
  |------|--------------|------------|
27
27
  | `render_strip` | Render screenshots into mocked-up per-panel PNGs | `screenshots`, `panelPresetId?`, `style?` / `look?` / `useSavedLook?`, `project?`, `version?`, `locale?`, `preview?`, `output?` |
28
- | `emit_bundle` | `render_strip` (or pre-rendered `panels`) + a `fastlane deliver` zip (+ optional share link) | `screenshots` or `panels`, `bundleId`, `locale?`, `projectName?`, `share?`, `output?` (+ the same styling params) |
28
+ | `emit_bundle` | `render_strip` (or pre-rendered `panels`) + a `fastlane deliver` zip (+ optional share link) | `screenshots` or `panels`, `bundleId`, `locale?`, `locales?` (multi-locale bundle), `projectName?`, `share?`, `output?` (+ the same styling params) |
29
+ | `save_project` | Save the strip as an editable Studio project (structure + look only — does NOT render, so it never hits the render timeout); returns `projectId` + an `openUrl` | `screenshots`, `project?` (update in place; omit to create), `projectName?`, `panelPresetId?`, `style?` / `look?` / `useSavedLook?`, `version?`, `locale?`, `sourceDir?` |
29
30
  | `read_look` | Return a project's saved Storeframe look (styling only), with its version + pin state | `project?` (default: your most recently edited project) |
31
+ | `read_project` | Return a project's FULL current state as an opaque `ProjectFile` — frame order, per-locale caption words, locale list, styling. Answers *"what did the designer change?"* (remember `updatedAt`, re-read later) | `project?` (default: your most recently edited project) |
32
+ | `render_project` | Re-render a SAVED project with your OWN raw screenshots, matched to its shots **by filename** — the project supplies frame order, captions + styling, so the result reproduces the designer's strip | `project?`, `screenshots` (a FLAT list, each with its original filename as `name`), `locale?`, `preview?`, `output?` |
30
33
  | `save_look` | Persist a composed look on a project (styling only — bumps its content version) | `look`, `project?`, `sourceName?` |
31
34
  | `describe_look` | The styling field catalog + defaults, for authoring from scratch | *(none)* |
32
- | `request_screenshot_upload` | Mint signed upload slots for real screenshots | `count` (1–10) |
35
+ | `request_screenshot_upload` | Mint signed upload slots for real screenshots | `count` (1–10), `names?`, `locale?` (a per-locale batch tag) |
33
36
 
34
37
  - **`panelPresetId`** — App Store size: `r69` (default, 6.9″ iPhone 1290×2796), `r65`, `r55`,
35
38
  `ipad13`, `ipad129`. This enum is a hand-kept copy of `mockup-engine/appstore.ts`'s
@@ -38,8 +41,10 @@ share links) — nothing that could reach a store on your behalf.
38
41
  here.
39
42
  - **`style`** — the structured styling input (call `describe_look` for the full catalog):
40
43
  `{ shotLook?, background?, captions? }`. `captions` is one entry PER PANEL in slot order;
41
- each entry's `text`/`subtitle` is per-RENDER input (never stored a look carries styling
42
- only). Wins over `look`/`useSavedLook`.
44
+ each entry is EITHER a single caption object OR an **array of caption layers** stacked on that
45
+ panel (headline + subline + …). Each layer's `text` (and a single caption's `subtitle`) is
46
+ per-RENDER input (never stored — a look carries styling only). Captions auto-layout above the
47
+ device so they never collide with the phone. Wins over `look`/`useSavedLook`.
43
48
  - **`look`** — a Storeframe look JSON exactly as `read_look` returns it; feed it back in to
44
49
  restyle. Omit for default styling.
45
50
  - **`version`** — render a specific saved look version (implies the saved look). Without it,
@@ -47,34 +52,78 @@ share links) — nothing that could reach a store on your behalf.
47
52
  - **`emit_bundle` output** — a zip (`README.md`, `fastlane/Deliverfile`,
48
53
  `fastlane/screenshots/<locale>/NN_storeframe.png`) as `zipBase64` or `zipUrl`/`zipRef` (see
49
54
  Output below), plus `shareLink` when `share: true`.
50
- - **`locale`** — an App Store locale code, e.g. `de-DE` (default `en-US`). Labels the render
51
- and, for `emit_bundle`, picks the `fastlane/screenshots/<locale>/` folder. **It does not
55
+ - **`locale`** — an App Store locale code, e.g. `de-DE` (default `en-US`). Labels the render,
56
+ picks which `{ "locales": }` screenshot variants render (see
57
+ [Per-locale screenshots](#per-locale-screenshots--multi-locale-bundles)), and, for
58
+ `emit_bundle`, picks the `fastlane/screenshots/<locale>/` folder. **It does not
52
59
  select caption text** — this server never stores or looks up Studio-typed copy (repo owns
53
60
  the words). Pass the right words for that locale yourself in `style.captions[].text` /
54
61
  `subtitle`.
55
62
 
63
+ ### Per-locale screenshots & multi-locale bundles
64
+
65
+ Any `screenshots` entry (and any `emit_bundle` `panels` entry) can carry **per-locale
66
+ variants** instead of one image:
67
+
68
+ ```json
69
+ { "locales": { "en-US": { "ref": "…en…" }, "de-DE": { "ref": "…de…" } } }
70
+ ```
71
+
72
+ - `render_strip({ locale: "de-DE", … })` renders the `de-DE` variant of each such entry. A
73
+ locale with **no variant of its own falls back** to the `en-US` variant (else the first
74
+ declared) — so you can ship German captions over English screenshots today and upgrade the
75
+ pixels later. Plain (non-`locales`) entries serve every locale. The response echoes the
76
+ declared locales as `screenshotLocales`.
77
+ - `emit_bundle({ locales: ["en-US", "de-DE"], … })` emits **ONE zip** with a
78
+ `fastlane/screenshots/<locale>/` folder per listed locale — `fastlane deliver` uploads every
79
+ locale in a single run. A single-entry list behaves exactly like `locale`; omitting `locales`
80
+ is the unchanged single-locale bundle.
81
+ - `request_screenshot_upload({ count, names, locale })` tags a batch of upload slots with the
82
+ locale it's for (echoed back — bookkeeping only; refs are locale-agnostic).
83
+
84
+ **Recommended multi-locale flow — render per locale, then compose once.** Caption text is
85
+ per-render input, so this is also the only way to get per-locale *captions* into one bundle,
86
+ and it keeps each call fast (see "Cheap preview, then compose" below):
87
+
88
+ 1. Per locale `L`: `render_strip({ locale: L, output: "urls", style: { captions: [<L's words>] },
89
+ screenshots: […] })` → per-panel refs for `L`.
90
+ 2. One `emit_bundle({ locales: […], bundleId, panels: [ { "locales": { "en-US": { "ref": "p1-en" },
91
+ "de-DE": { "ref": "p1-de" } } }, … ] })` — zips every locale **without re-rendering**.
92
+
93
+ The one-call form (`emit_bundle` with `screenshots` + `locales`) works too, but renders once
94
+ per locale with the *same* captions for every locale, and multiplies the slow full-res render
95
+ by the locale count.
96
+
56
97
  ### The `captions.<locale>.json` convention
57
98
 
58
99
  The canonical source of shipping caption copy is a file **in your own repo**, one per locale,
59
100
  keyed by **panel ordinal** (0-based, in strip order — not panel id, so the file stays stable
60
101
  across re-renders):
61
102
 
103
+ A panel's entry is EITHER a single caption (`{ headline, subtitle? }`) OR an **array of layers**
104
+ (each `{ text, … }`, stacked in order) — use the array when you want more than a headline + one
105
+ subtitle, e.g. an eyebrow over a headline over a caption:
106
+
62
107
  ```json
63
108
  // captions.de-DE.json
64
109
  {
65
110
  "0": { "headline": "Alles im Blick", "subtitle": "Deine Tage, übersichtlich." },
66
- "1": { "headline": "Schneller planen" }
111
+ "1": [ { "text": "NEU" }, { "text": "Schneller planen" } ]
67
112
  }
68
113
  ```
69
114
 
70
115
  To render a locale: read that file, map it onto `style.captions[]` in the same order (index
71
116
  `i` → panel `i`, `null` for a panel with no caption), and pass `locale` alongside so the
72
- render/bundle is labeled and routed consistently:
117
+ render/bundle is labeled and routed consistently. A single caption maps to one object; a layer
118
+ array maps straight through as an array:
73
119
 
74
120
  ```json
75
121
  {
76
122
  "locale": "de-DE",
77
- "style": { "captions": [{ "text": "Alles im Blick", "subtitle": "Deine Tage, übersichtlich." }, { "text": "Schneller planen" }] }
123
+ "style": { "captions": [
124
+ { "text": "Alles im Blick", "subtitle": "Deine Tage, übersichtlich." },
125
+ [ { "text": "NEU", "sizePt": 28 }, { "text": "Schneller planen" } ]
126
+ ] }
78
127
  }
79
128
  ```
80
129
 
@@ -97,10 +146,12 @@ context budget (or get silently truncated by a file-read tool) long before rende
97
146
  3. Pass `{ "ref": "<ref>" }` — not inline base64 — as that screenshot's entry in
98
147
  `render_strip`/`emit_bundle`'s `screenshots` array.
99
148
 
100
- `screenshots` entries accept three shapes: an inline base64 string (small payloads only —
149
+ `screenshots` entries accept these shapes: an inline base64 string (small payloads only —
101
150
  inline is capped and returns a clear "payload too large" error above ~8MB decoded, pointing
102
- you back at this flow), `{ ref }` from step 1, or `{ url }` (an https URL this server fetches
103
- itself: image/png, no redirects, ~20MB cap).
151
+ you back at this flow), `{ ref }` from step 1, `{ url }` (an https URL this server fetches
152
+ itself: image/png, no redirects, ~20MB cap), `{ path }` (local stdio server only — see below),
153
+ or `{ "locales": { "<locale>": <any of those> } }` for per-locale variants (see
154
+ [Per-locale screenshots](#per-locale-screenshots--multi-locale-bundles) above).
104
155
 
105
156
  **Output** works the same way in reverse: `render_strip`/`emit_bundle` take an `output` field
106
157
  — `"inline"` (base64), `"urls"` (uploaded under your own prefix, short-lived signed download
@@ -248,24 +299,26 @@ instead of `{ ref }`/`{ url }`:
248
299
  (`{ "path": ... }` only works over this LOCAL server — the hosted server rejects it, since
249
300
  reading an arbitrary server-side path there would be a local-file-inclusion hole.)
250
301
 
251
- **What's different locally, and why:** `read_look` / `save_look` / `save_project` / share links
252
- need an ACCOUNT to persist to, which local mode doesn't have by default they cleanly no-op
253
- with a "sign in" message. `render_strip` / `emit_bundle` are otherwise **fully functional**
254
- you lose nothing on rendering itself, only on persistence, which is exactly the free/paid line:
302
+ **What's different locally, and why:** every tool that needs an ACCOUNT to read or persist to —
303
+ `read_look` / `save_look` / `save_project` / `read_project` / `render_project` / share links
304
+ has nothing to talk to in local mode by default, so they cleanly no-op with a "sign in" message.
305
+ `render_strip` / `emit_bundle` are otherwise **fully functional** you lose nothing on rendering
306
+ itself, only on persistence, which is exactly the free/paid line:
255
307
 
256
308
  | | Local (free, `npx storeframe-mcp`) | Hosted (Fly, `sfs_…` token) |
257
309
  |---|---|---|
258
310
  | Render compute | **your machine** ($0, no timeout) | our Fly machine |
259
311
  | Auth | none (the process runs as you) | account + API token |
260
- | Saved looks / editable projects / share links | opt-in via a token (below) — share links still need a direct hosted connection | ✅ |
312
+ | Saved looks / editable projects / project read+re-render / share links | opt-in via a token (below) — share links still need a direct hosted connection | ✅ |
261
313
  | Screenshot input | local `{ path }` off disk | `request_screenshot_upload` → `{ ref }` |
262
314
 
263
315
  ### Optional: bridge a local render into your hosted account
264
316
 
265
317
  Pass a personal Storeframe API token (Studio → account menu → **API tokens** — the same
266
318
  `sfs_…` token the hosted server takes as a bearer) and `save_project` / `read_look` /
267
- `save_look` start working too — **rendering still happens locally and for free; only the small
268
- saved-project JSON (structure + look, never screenshot bytes) goes to your account:**
319
+ `save_look` / `read_project` / `render_project` start working too — **rendering still happens
320
+ locally and for free; only the small saved-project JSON (structure + look, never screenshot
321
+ bytes) goes to your account:**
269
322
 
270
323
  ```bash
271
324
  STOREFRAME_TOKEN=sfs_… claude mcp add storeframe -- npx -y storeframe-mcp