npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

appshots-mcp

v0.3.0

Published

MCP server that composes App Store / Google Play screenshot projects and writes them as .appshots files for the AppShots editor.

Readme

AppShots MCP Server

Lets Claude Code / Codex design App Store & Google Play screenshots: compose the layout, look at what it rendered, fix it, and export PNGs at the exact store pixel size — or save a .appshots project to finish by hand in the AppShots editor.

Rendering runs the editor's real renderer in a headless browser, so what the model sees and what it exports are what the desktop app's Export button would produce.

Install

Claude Code:

claude mcp add appshots -- npx -y appshots-mcp

Codex CLI — in ~/.codex/config.toml:

[mcp_servers.appshots]
command = "npx"
args = ["-y", "appshots-mcp"]

Any other MCP client: run npx -y appshots-mcp as a stdio server. It needs local filesystem access (it reads your screenshots and writes project files), so it runs on your machine — there is no hosted URL to point at.

The server prints nothing to stdout — that channel is the JSON-RPC stream — and reports failures as tool errors.

Requirements

  • Node 18+ — that is all the authoring tools need.
  • AppShots 1.3+ installed, for the rendering tools. It is the renderer: it already holds the editor, the device bezels and the font faces, so this package stays ~35 kB and downloads nothing.

This package is a thin entry point on purpose. It does not carry a copy of the editor, and rendering without the app is a developer fallback, not a supported path — see below.

APPSHOTS_APP points at an AppShots bundle in an unusual place. APPSHOTS_RENDERER=app|chrome forces a backend.

The installed app carries the bezels and font faces, so rendering works offline — except for CJK font faces, which come from Google Fonts. When one fails to load, the render tools say so in missingFonts instead of quietly handing back a screenshot set in the wrong typeface.

From a checkout

claude mcp add appshots -- npx tsx /path/to/appscreenshots/mcp/server.ts

Tools

| Tool | Purpose | |---|---| | list_options | Valid devices (with exact export pixel sizes), templates, bezel ids, font families, locale codes, shape kinds | | create_project | New project: device, pages, base locale, layout template | | open_project / save_project | Read / write .appshots (defaults to ~/Documents/AppShots/<name>.appshots) | | describe_project | Settings + every layer with page, geometry (fractions and pixels) and id | | configure_project | Rename, default locale, locale→font map, per-page backgrounds, page names | | set_background | Solid or multi-stop gradient, whole band or one page | | add_text | Headline/body text, one string per locale | | add_frame | Device frame + the screenshot inside it | | add_shape / add_image | Decorations, logos, badges | | update_layer / remove_layer | Move, resize, restyle (raw merges any field from src/types.ts), delete | | set_locales / set_text | Locale list and per-locale copy — translate in the model, no translation service needed | | import_screenshots | Fill frames from a folder (fastlane screenshots/<locale>/…_01.png layout, en-US_01.png, or plain order) | | render_preview | Render pages and return them as images — the model sees its own layout and iterates | | export_screenshots | Write the final PNGs at exact store pixel size, in the fastlane deliver layout |

The loop that makes this work is add_* → render_preview → update_layer → render_preview → export_screenshots. Without the preview step a model is composing blind.

Geometry is fractions, never pixels

Every coordinate and size is a fraction of one page: x: 0.14, width: 0.72 is a frame inset 14% from the page's left edge, three-quarters of a page wide. The same numbers compose correctly on a 410 px Watch and a 2880 px Mac, and the model can't emit an off-canvas layout by guessing wrong about the device. add_frame takes only width — the height follows the device's real aspect ratio. Passing a pixel value gets a tool error that explains the convention.

describe_project reports both units, and update_layer.raw is the escape hatch that writes raw Layer fields in their own units.

What it reuses

Nothing about the file format, device geometry or naming conventions is restated here — project.ts imports it from the app, and npm run mcp:build inlines those modules into the published bundle:

  • src/utils/appshotsFile.ts — the actual .appshots packer/unpacker
  • src/utils/seed.ts — the layout templates
  • src/constants/devices.ts, bezels.ts, shapeDefaults.ts, fonts.ts, locales.ts
  • src/utils/batchImport.tsparseShotPath, the screenshot naming convention
  • src/utils/exporter.tswithExportStage, the same render path the app's Export button and its App Store Connect upload use; src/headless.tsx mounts the real CanvasStage and calls it

Only two things are local, because they have no DOM here: imageSize.ts (pixel dimensions from file headers, replacing fileToAsset's Image decode) and the session/tool layer itself.

How rendering works

Two backends, one Renderer interface, so the tools never learn which is in use.

The desktop app (preferred, macOS): spawned as appshots --render-server — no Dock icon, no window. It opens headless.html in a hidden webview and answers a line-delimited JSON protocol on stdio (src-tauri/src/render.rs). Nothing is downloaded and no copy of the web build is needed: the app already carries the editor, the bezels and the fonts. An installed app older than 1.3 is skipped by reading its Info.plist, rather than being launched and waited on — spawning one would just open the editor.

A browser (fallback, opt-in): mcp/render.ts serves a web build from an ephemeral localhost port (ES modules can't load over file://), opens headless.html in Chrome, and calls into the page. One browser and page stay open across calls, and the project is only re-injected when it changes, so a multi-locale export doesn't re-fetch fonts per image.

This path needs two things the package deliberately does not ship: a web build (APPSHOTS_DIST, or a checkout's own dist/) and playwright-core (an optional peer). Bundling either would have added ~30 MB to every install to serve a fallback most people never reach. It exists for this repo's tests and for platforms the app has not shipped on yet.

Both mount the actual editor canvas and render through withExportStage. They cannot produce identical bytes — WebKit and Chromium antialias text and dither gradients differently, worth ~0.4% mean channel difference — so mcp/backends.test.mts asserts they agree to within rasterization noise, which a real layout drift would blow straight past.

Develop & publish

Run these from the repo root:

npm run mcp:check         # typecheck
npm run mcp:test          # full suite, pinned to the browser backend
npm run mcp:test:app      # the same suite, pinned to a locally built AppShots
npm run mcp:test:backends # the two backends must lay out identically
npm run mcp:build         # bundle mcp/dist/server.js (the published artifact)
npm run mcp:web           # optional: a local web build for the browser fallback
npm run mcp:test:dist     # pack → install into a clean project → run the suite against it
npm run mcp               # run the server by hand (it will just wait on stdin)

mcp:test needs macOS sips for its screenshot fixtures; the app-backed runs need cargo build in src-tauri first. APPSHOTS_RENDER_VISIBLE=1 shows the render window, which is the only way to watch what a misbehaving backend is drawing.

To publish: bump version in mcp/package.json, then

npm run mcp:test:dist && cd mcp && npm publish

mcp/dist/ and mcp/web/ are generated and gitignored; only dist/, README.md and LICENSE are published (~35 kB). A test asserts web never reappears in files — publishing it would put the whole editor inside an MIT package. The appshotsVersion field records which app version the bundle's inlined modules came from; mcp:build keeps it in step.