shotops-mcp 0.8.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 CHANGED
@@ -63,7 +63,7 @@ The key is never copied, printed, or sent to ShotOps, and no release tool is ins
63
63
  | `save_look` | Persist a composed look on a project (styling only — appends a new version) | `look`, `project?`, `sourceName?` |
64
64
  | `hold_look` | Hold which saved version agents render by default (replaces pinning) — the version must exist (`read_look` `versions`) | `version`, `project?` |
65
65
  | `release_look` | Clear the held version — agents Follow latest (render the newest saved look) | `project?` |
66
- | `describe_look` | Five named design presets, two named layout templates, the user-intake script, and the full styling field catalog + defaults | *(none)* |
66
+ | `describe_look` | Five named design presets, three named layout templates, the user-intake script, and the full styling field catalog + defaults | *(none)* |
67
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?` |
68
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
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)* |
@@ -85,15 +85,15 @@ The key is never copied, printed, or sent to ShotOps, and no release tool is ins
85
85
  `text` (and a single caption's `subtitle`) is per-RENDER input (never stored — a look carries
86
86
  styling only). Captions auto-layout in a band at the top of the panel, and the device does NOT
87
87
  move to make room.
88
- - **`style.layout`** — the panel **composition** in one word: `"standard"` or `"bleed"`. It settles
89
- the headline's reserved region *and* the device size + position together, which is three numbers
90
- that are not independent of each other. `standard` (the shipped default) puts the whole device
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
91
91
  under a 2-line region — about 36 characters — and crops nothing; `bleed` reserves 4 lines (~72
92
92
  characters) and runs the device 10% off the bottom edge. The region is **reserved, not fitted**:
93
93
  it is held at full size whether or not the headline fills it, which is what makes every panel in
94
94
  a swiped set land on the same line. **Precedence** — the template expands *first*, then any
95
- `shotLook.phoneHeight`/`hOffset`/`vOffset` or `captions[].sizePt`/`maxWidth` you also pass
96
- **overrides** it (`{ layout: "bleed", shotLook: { vOffset: "45" } }` is "bleed, but a bit lower").
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").
97
97
  A pure macro: nothing stores the template id, so `save_project` persists the expanded values and
98
98
  `read_look` returns them unchanged. **The id is not lost, though — it is DERIVED back:**
99
99
  `read_project` and `read_look` both return a `layout` report read out of the values themselves, so
@@ -236,7 +236,7 @@ own `error` and never cancels the others — re-import just that one. In ChatGPT
236
236
  arrives as the top-level `file` parameter instead of an array entry, because the Apps SDK cannot
237
237
  fill a file parameter nested inside an array; you can pass both in the same call.
238
238
 
239
- `{ 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);
240
240
  `{ path }` needs the local stdio server (the hosted server never reads a caller-supplied
241
241
  filesystem path — that would be an LFI hole). Each refusal names the door that IS open on your
242
242
  tier. When the screenshots are only on the user's machine and you are on the hosted server, there
@@ -255,19 +255,22 @@ is no URL to give, so use the signed-slot flow below:
255
255
  `screenshots` entries accept these shapes: an inline base64 string (small payloads only —
256
256
  inline is capped and returns a clear "payload too large" error above 3MB decoded, pointing
257
257
  you back at this flow), `{ ref }` from step 1, `{ url }` (an https URL this server fetches
258
- itself: image/png, no redirects, ~20MB cap), `{ path }` (local stdio server only — see below),
258
+ itself: PNG or JPEG, converted to PNG when needed, no redirects, ~20MB cap), `{ path }` (local stdio server only — see below),
259
259
  or `{ "locales": { "<locale>": <any of those> } }` for per-locale variants (see
260
260
  [Per-locale screenshots](#per-locale-screenshots--multi-locale-bundles) above).
261
261
 
262
262
  Every raw PNG is automatically palette-optimized before rendering (TinyPNG-style, locally inside
263
263
  ShotOps): dimensions and transparency are preserved, and an already-smaller PNG is left byte-for-byte
264
- unchanged. This happens after the input reaches the MCP, so it does **not** raise the inline request
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
265
266
  cap — use the ref/upload flow for a large source file.
266
267
 
267
- **Output** works the same way in reverse: `render_strip`/`emit_bundle` take an `output` field
268
- — `"inline"` (base64), `"urls"` (uploaded under your own prefix, short-lived signed download
269
- URLs), or omit for auto (inline under ~200KB total, `urls` above). Use `"urls"` for
270
- full-resolution work so results never transit your context either.
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.
271
274
 
272
275
  **Cheap preview, then compose:** `render_strip({ preview: true })` renders at ~25% resolution
273
276
  for fast, cheap styling iteration — small enough to always come back inline; don't ship it,
@@ -308,7 +311,7 @@ Deploy the hosted server at a public HTTPS origin, enable Developer Mode in Chat
308
311
  an app whose MCP server URL is `https://<your-host>/mcp`. Do not paste a personal `shotops_` token
309
312
  into the app definition: ChatGPT discovers the protected-resource metadata from the endpoint and
310
313
  runs the server's OAuth + PKCE browser sign-in flow. This is a tool-only app — no widget or UI
311
- resource is required. User-attached PNGs enter through `import_screenshot`.
314
+ resource is required. User-attached PNGs and JPEGs enter through `import_screenshot`.
312
315
 
313
316
  For public plugin submission, OpenAI supplies a domain-verification token after the MCP domain
314
317
  is entered in the Platform dashboard. Set it on the hosted server as
@@ -368,47 +371,42 @@ since this repo is private.
368
371
 
369
372
  ### Ops (this repo's maintainer only)
370
373
 
371
- - **Deploy** (from the **repo root** the Dockerfile needs `../mockup-engine` and
372
- `../api/_lib` as build-context siblings):
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:
373
387
  ```bash
374
- flyctl deploy . --config shotops-mcp/fly.toml --remote-only --ha=false
388
+ cd shotops-mcp && SHOTOPS_MCP_TOKEN=… node verify-mcp.mjs https://mcp.shotops.dev/mcp
375
389
  ```
376
- - **Secrets** (`fly secrets set -a shotops-mcp KEY=value`): `SUPABASE_URL`,
377
- `SUPABASE_SERVICE_ROLE_KEY`, plus `OPENAI_APPS_CHALLENGE_TOKEN` while verifying the MCP
378
- domain for public ChatGPT plugin submission nothing Apple-related, ever, by construction.
379
- - **Verify after any deploy** a green `flyctl deploy` isn't proof the server answers MCP
380
- calls:
381
- ```bash
382
- node verify-mcp.mjs https://mcp.shotops.dev/mcp <a-real-shotops_-token>
383
- ```
384
- Exercises `tools/list`, `read_look`, `emit_bundle` (asserts the zip unzips to
385
- README/Deliverfile/screenshots), and asserts no-token / bad-token both get `401`.
386
- - **Logs / status:** `flyctl logs -a shotops-mcp`, `flyctl status -a shotops-mcp`.
387
- - The Dockerfile is pinned to `mcr.microsoft.com/playwright:v1.61.1-noble` matching the
388
- `playwright` npm version in `package.json`. If you bump one, bump the other.
389
- - The container builds the browser harness once and serves it through a loopback-only static
390
- server at runtime. Vite is build/parity tooling only and is pruned from production
391
- dependencies; the Docker build context still includes `mockup-engine` so the harness can
392
- bundle the shared engine + GLB.
393
- - **VM size is `shared-cpu-2x` with 4GB and BOTH numbers are load-bearing for different
394
- reasons.** The **memory** was raised 2gb 4gb on 2026-07-26 (#134): a grouped multi-panel
395
- `render_strip` drove `chrome-headless` to ~1.4gb resident, the kernel OOM-killed it, and the
396
- server went unhealthy for minutes — `verify-mcp.mjs` could not run to completion at 2gb and now
397
- does. The **2 vCPUs** are the older finding below; don't conflate them, and don't cut either.
398
- - **Why 2 vCPU and not `shared-cpu-1x`** (found live, Phase 10):
399
- Chromium's software WebGL render (no GPU on the hosted tier) is CPU-heavy enough that on
400
- a single shared vCPU it can starve the whole process — health checks stop responding,
401
- SIGTERM/SIGINT get ignored, and the platform force-kills the machine
402
- mid-render. **`Virtual machine exited abruptly` has TWO causes and they look identical in the
403
- logs:** on 1 vCPU it is this CPU starvation; on 2 vCPU under load it was the #134 OOM, i.e.
404
- MEMORY, fixed by the `4gb` above (`docs/reference/ops-runbook.md` carries that half). Check
405
- which resource ran out before reaching for either fix. On 2 vCPUs the same 3-panel real-screenshot render completes in ~8s. Don't
406
- drop back to `shared-cpu-1x` to save cost — a render-triggering tool call is expected to
407
- occasionally saturate a whole core, and 1x has no second core to keep the process alive
408
- while that happens. Client callers (agents, `verify-mcp.mjs`) should still set a generous
409
- per-call timeout (≥180s) for `render_strip`/`emit_bundle` — even warm, cold-machine renders
410
- can take 60-110s before the CPU fix, and network/Supabase round trips add more on `urls`
411
- output.
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`.
412
410
 
413
411
  ## Local — `npx shotops-mcp`
414
412
 
@@ -579,15 +577,23 @@ npx shotops-mcp release plan \
579
577
 
580
578
  The command resolves the Bundle ID to one accessible app and one editable iOS version, then reads
581
579
  the supported metadata, review, age-rating, compliance, build, screenshot, and App Preview
582
- baseline. It writes a user-only `release-plan.json` containing every create, update, clear,
583
- replacement, upload, attachment, and no-op, plus a portable `release-review.zip` containing that
584
- exact canonical plan and its content-addressed screenshot bytes. The plan expires after 15 minutes
585
- and hash-binds the project revision, desired bytes, normalized Fastlane inputs, Apple setup identity,
586
- policy version, and the complete remote snapshot so later approval cannot drift onto different state.
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.
587
591
 
588
592
  `release-plan-envelope.json` is the separate redacted form safe for a later ShotOps coordination
589
- step: it carries identifiers and value hashes, never metadata values, review credentials, local
590
- paths, Apple credentials, or asset bytes. This command talks directly to Apple with GET requests
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
591
597
  only; it never uploads, creates a version, commits, submits, or sends the private key to ShotOps.
592
598
 
593
599
  ### Review the exact plan locally
@@ -604,13 +610,14 @@ To review an artifact downloaded from owner-controlled CI, pass it directly:
604
610
  npx shotops-mcp release review release-review.zip
605
611
  ```
606
612
 
607
- The command verifies the unexpired exact plan hash, artifact manifest, and every embedded screenshot
608
- before opening a nonce-protected URL on `127.0.0.1` with a random port. The page shows target app and
609
- team fingerprint, Bundle ID, version, expiry, complete or partial scope, every before/after/clear,
610
- build, review, rating, compliance, preview, screenshot order/replacement/omission, no-op, and the
611
- operations excluded by the plan. Partial-scope and no-build warnings cannot be dismissed. It loads
612
- no remote resource, sends nothing, and has no edit or approval action; expired, corrupt, edited, or
613
- content-mismatched inputs fail before a server is started.
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.
614
621
 
615
622
  The saved project remembers the exact on-disk folder your screenshots came from (not just their
616
623
  filenames), so re-opening it on the SAME machine can point right back at it. Share links aren't
@@ -650,7 +657,8 @@ shotops-mcp/
650
657
  vite.harness.config.mjs the STATIC harness build (local mode's publish prerequisite) → harness-dist/
651
658
  build.mjs esbuild bundle of src/local.ts (+ @engine/@api inlined) → dist/local.js
652
659
  Dockerfile hosted image — Playwright's Chromium base + this repo's 3 npm installs
653
- fly.toml Fly app config — ROLLBACK ONLY since the Vercel cutover; see #68
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
654
662
  verify-mcp.mjs post-deploy smoke test — real MCP round-trip against a hosted URL
655
663
  verify-mcp-local.ts local stdio smoke test (tsx src/local.ts, in-repo)
656
664
  static-parity.ts pixel-diffs the static-serve render against the Vite-dev render (must be 0)