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

splay-mcp

v0.8.0

Published

MCP server that screenshots the screens you build locally and collects them in one shared gallery

Readme

splay-mcp

Collect the screens you build locally — automatically — in one shared gallery.

A design that only exists on your local dev server can't be shown to anyone, and it's gone the moment you move on. Splay screenshots it as you work, uploads it, and gives you a link.

Requirements

  • Node.js 20.9+
  • Google Chrome (reused if installed — no extra browser download)
  • A Splay server URL and an API key

Get started

Nothing to install. From your project folder:

npx splay-mcp setup <API_KEY> --url https://your-splay.vercel.app

That one command does three things: saves the API key, scans your routes into splay.config.json, and installs a commit hook.

Then, with your dev server running:

npx splay-mcp capture

From now on every commit captures only the pages that commit touched.

Check that baseUrl in splay.config.json matches the port your dev server actually runs on (defaults to 3000).

Prefer a global install? npm i -g splay-mcp, then drop the npx.

Use it from an agent

Add this to your MCP config (Claude Code, Cursor, …):

{
  "mcpServers": {
    "splay": { "command": "npx", "args": ["-y", "splay-mcp"] }
  }
}

The agent skill

The first time you run capture or init in a project, Splay writes .claude/skills/splay-capture/SKILL.md. It tells coding agents how the two halves of the job divide: listing the pages worth capturing in splay.config.json is a local edit an agent can always make, while the upload leaves your machine — and in a repo holding sensitive context, an agent may be refused permission to make that send.

Without the skill, a refused agent tends to stop at a list of local PNG paths, leaving you to remember and re-enter the pages yourself. With it, the agent records the pages and hands you one line — npx -y splay-mcp capture — which captures all of them.

Edit the file freely; Splay only overwrites what it wrote. --no-skill skips it.

If you already ran setup, credentials are read from ~/.splay/config.json. To skip setup entirely, put them in the config instead:

{
  "mcpServers": {
    "splay": {
      "command": "npx",
      "args": ["-y", "splay-mcp"],
      "env": {
        "SPLAY_URL": "https://your-splay.vercel.app",
        "SPLAY_API_KEY": "…"
      }
    }
  }
}

Running npx splay-mcp with no arguments in a terminal prints this help. It only starts the MCP server when stdin is a pipe, i.e. when a client connects. Force it with --stdio.

The agent gets these tools:

| Tool | What it does | | ----------------------- | ------------------------------------------------------ | | splay_capture | Capture the given pages and return a gallery link | | splay_capture_changed | Capture only what the last commit changed | | splay_comment | Leave a note on a screen already in the gallery | | splay_doctor | Check every prerequisite and say exactly what to fix | | splay_status | Report config, API key, and dev server state |

Adding a line like this to your project's CLAUDE.md makes the agent reach for it on its own:

After any noticeable UI change, capture the affected pages with splay_capture.
Say what changed in `comments` so the designer can reply on the frame.

Comment while you capture

The moment you save a screen is the moment you still remember why. splay_capture takes comments so the note goes up with the screenshot — one round trip, no second visit to the gallery:

{
  "pages": ["/pricing"],
  "note": "Pricing table spacing",
  "comments": [
    { "path": "/pricing", "body": "## Spacing pass\n\nRow gap 12 → 16. **Is the CTA still above the fold on 13\"?**" },
    { "path": "/pricing", "body": "This badge contrast is too light", "placement": "pin", "x": 0.62, "y": 0.28 }
  ]
}
  • placementbelow (default) or above stacks a note under/over the frame. pin drops a marker inside the screen and needs x/y.
  • x / y01 across and down the screen. 0.5, 0.5 is dead center.
  • Omit path to put the same note on every frame captured in that call.
  • Markdown renders — headings, lists, tables, task lists, code fences.

For a screen that is already in the gallery, use splay_comment (or the CLI):

npx splay-mcp comment /pricing "Row gap is 16 now — good?"
npx splay-mcp comment /pricing "This badge is too light" --pin 0.62,0.28
npx splay-mcp comment /pricing "Above-frame note" --above

The note lands on the most recent capture of that route. Whoever opens the gallery sees the pin on the frame and can reply to it — in the canvas or in the single-frame focus view.

Behind a corporate proxy or firewall

Locked-down environments (Coder workspaces, devcontainers, CI) usually reach the internet through a proxy and an egress allowlist. Two things follow:

Set the proxy. Node's fetch does not read HTTPS_PROXY on its own, so splay-mcp reads it for you — set the usual variables and uploads will go through the proxy:

export HTTPS_PROXY=http://proxy.corp:3128
export NO_PROXY=.internal.corp        # optional

localhost and 127.0.0.1 never go through the proxy, so a local dev server is still reached directly.

Ask for the allowlist. Only these hosts are needed:

| Host | Why | | --- | --- | | your Splay server (e.g. splay.example.app) | uploading captures and comments | | registry.npmjs.org | npx splay-mcp | | your staging host (only if baseUrl points at it) | loading the pages to capture |

Images are uploaded to object storage by the Splay server, so the client never talks to the storage host.

Not sure what is blocked? Run the doctor — it checks each prerequisite separately and prints the exact failure, including the proxy's own refusal:

npx splay-mcp doctor
  ✗ Splay 서버 도달
      fetch failed ← UND_ERR_ABORTED: Proxy response (403) !== 200 when HTTP Tunneling
      → 방화벽/allowlist에 splay.example.app 허용 요청

splay.config.json

setup writes this; edit it freely afterwards.

{
  "project": "my-project",       // identifier in the gallery
  "baseUrl": "http://localhost:3000",
  "viewports": [{ "w": 1440, "h": 900 }],
  "pages": [
    { "path": "/", "name": "Home" },
    {
      "path": "/pricing",
      "name": "Pricing",
      "fullPage": true,
      // Dependencies the commit hook can't infer from conventions or imports
      "dependsOn": ["content/prices.json"]
    }
  ]
}

baseUrl must point at the port this project actually serves. If another app owns that port, you'll silently capture that app instead.

Zero local config: store the config on the server

For someone who only captures — a designer pointing splay at a hosted preview — a local splay.config.json is one more file to distribute and keep in sync. Store it on the server instead:

# once, from the folder that has the config (anyone on the team, e.g. you):
npx splay-mcp config push

# the designer's one-time setup — no files created:
npx splay-mcp setup <key> --project my-project

# every capture after that:
npx splay-mcp capture --url https://that-days-preview.example.dev

capture looks for a local splay.config.json first and falls back to the server-stored config of the default project (--project <slug> picks one explicitly). Pages, viewports, and the login section all come from the server, so changing them is a config push — the designer doesn't touch anything.

config show --project <slug> prints what the server has (password masked). The stored login password is encrypted at rest. Writing requires the project owner's key; reading works with any valid API key of the gallery.

Capturing a hosted preview (--url)

When the app runs somewhere whose address changes per session — a Coder workspace, a cloud dev environment, a tunnel — override the configured baseUrl at capture time:

npx splay-mcp capture --url https://my-branch--preview.example.dev

Everything else (project, pages, viewports, login) still comes from splay.config.json. The MCP tool accepts the same thing as a url argument.

Capturing behind a login

If the app asks for a login before showing anything, add a login section:

{
  "baseUrl": "https://staging.example.com",
  "pages": [{ "path": "/dashboard" }],
  "login": {
    "url": "/login",                    // where the form lives (default "/")
    "user": "[email protected]",
    "password": "${STAGING_PASSWORD}"   // ${VAR} reads from the environment
  }
}

Before capturing, splay opens the login page, fills the form, and saves the session to ~/.splay/sessions/ (chmod 600). Later runs reuse that session and only log in again when it has expired — so you don't hammer the login endpoint on every capture.

The email/username field, password field, and submit button are detected automatically, which covers standard login forms. If yours is unusual, point at the elements yourself:

"login": {
  "url": "/login",
  "user": "[email protected]",
  "password": "${STAGING_PASSWORD}",
  "userSelector": "#account-id",
  "passwordSelector": "#account-pw",
  "submitSelector": "button.login-submit"
}

Notes:

  • ${VAR} substitution works in user and password, so the config file itself can stay free of secrets and safe to commit.
  • Single-step forms only. SSO redirects, MFA, and captchas are out of scope — use a plain test account.
  • A wrong password fails the run before anything is uploaded, quoting the error message the page showed.

The commit hook

setup installs a post-commit hook — deliberately not pre-commit:

  • it can never block a commit (runs in the background, swallows every failure)
  • the working tree matches the commit at capture time

If the dev server isn't running it skips silently. That's what keeps commits safe, but it also means a missed capture is easy to overlook. npx splay-mcp status will tell you.

To remove it, delete the # >>> splay >>> block from .git/hooks/post-commit, or run init --no-hook to never install it.

What counts as a "changed page"

Four rules, in order:

  1. Route conventionsapp/pricing/page.tsx/pricing; a layout.tsx covers everything beneath it
  2. Import graph — edit components/Button.tsx and every page importing it is captured
  3. dependsOn — explicit globs from the config
  4. Global filesglobals.css, the root layout, next.config.* mean everything

Commits that only touch API routes or docs capture nothing. Projects without a detectable framework capture every configured page whenever a visual file changes — narrow that with dependsOn.

Environment variables

| Variable | Purpose | | -------------------- | ---------------------------------------------------- | | SPLAY_API_KEY | API key, as an alternative to login | | SPLAY_URL | Splay server URL | | SPLAY_CHROME_PATH | Point at Chrome if it can't be found automatically | | SPLAY_HOOK_COMMAND | Command the hook runs (default npx --yes splay-mcp…) |

Notes

  • Captures are deterministic. Rendering flags are pinned so sub-pixel antialiasing and GPU rasterisation don't drift, web fonts and images are awaited, and animations are settled to their final keyframe before the shot.
  • Next.js is first-class. Route detection follows the App Router. Other frameworks work fine — list your pages manually.
  • Dynamic routes (/blog/[slug]) need a concrete path in the config to be captured.

License

MIT