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

playguard

v0.7.0

Published

MCP proxy for Playwright and Figma — token optimization, auto session recovery, and unified routing

Readme

PlayGuard

An MCP proxy that sits between your AI agent and browser/design tools. It wraps Playwright MCP and optionally a Figma MCP, exposing a single unified MCP server while transparently adding:

  • Automatic session recovery — dead browser sessions are revived; the agent never sees a crash error
  • Token optimization — compact snapshots, delta diffs, snapshot cache, and eval caching reduce browser snapshot cost by 70–90%
  • Figma response optimization — strips metadata, invisible layers, duplicate component instances, and SVG geometry blobs before the response reaches the agent
  • Screenshot policy — redirect, warn, block, or allow screenshot calls

Works with Claude Code, Claude Desktop, Cursor, Codex, or any MCP-compatible agent.


Architecture

Claude Code / Claude Desktop / Cursor / Codex
           │
           │  MCP (stdio)
           ▼
    ┌─────────────────┐
    │    PlayGuard    │  ← single proxy for everything
    │                 │
    │  • router       │  browser_* → Playwright MCP
    │  • recovery     │  figma_*   → Figma MCP (optional)
    │  • optimizer    │
    │  • cache        │
    │  • analytics    │
    └────────┬────────┘
             │
      ┌──────┴──────┐
      ▼             ▼
Playwright MCP   Figma MCP
      │
      ▼
  Chromium / Firefox / WebKit

Figma MCP is optional. If FIGMA_MCP_CMD is not set, PlayGuard runs in browser-only mode with no behavior changes. Playwright MCP is bundled as a dependency — no separate installation required.


Features

Browser (Playwright)

Automatic Session Recovery

PlayGuard intercepts dead-session errors before the agent sees them:

Target.*closed · Browser.*closed · connect ECONNREFUSED · crashed

It restarts Playwright MCP, restores the last URL, and retries the call transparently. Two concurrent calls during a crash → one restart, both calls continue.

Compact Snapshots

browser_snapshot normally returns the full accessibility tree. PlayGuard keeps only lines with [ref=] (interactive elements) and structural landmarks (nav, main, form, dialog, …). Static text and decorative images have no refs and are not needed for navigation.

[PlayGuard compact: 312/1840 lines, ~83% removed, 124.3KB→14.1KB]

Snapshot Filtering

For pages where even the compact tree is large, browser_snapshot accepts optional params to narrow the result to one subtree:

  • section — return only the landmark subtree whose label starts with or contains this string (e.g. "form", "navigation \"Footer\"")
  • around — return only the landmark subtree containing this [ref=N], useful once you know a specific element's ref
  • depth — cap the returned subtree's depth (combine with section/around for a high-level overview first, or use alone to flatten the whole tree)

Falls back to the full snapshot with a [PlayGuard: ... not found] warning if the requested section/ref doesn't exist. Switching filters between calls always forces a fresh snapshot — it never returns a stale UNCHANGED/cached result from a differently-filtered request.

Smart Wait

Set PLAYGUARD_SMART_WAIT=1 to have browser_snapshot retry when a page looks mid-load — few interactive refs plus a loading/spinner/skeleton indicator in the text. Retries PLAYGUARD_SMART_WAIT_MAX_RETRIES times, PLAYGUARD_SMART_WAIT_MS apart, before returning (with a warning if it still looks like it's loading). Off by default — it adds latency to every snapshot call.

Set PLAYGUARD_COMPACT=false to receive the raw snapshot.

Delta Snapshots

When a page changes only slightly, PlayGuard returns a diff instead of the full snapshot:

[PlayGuard delta: +3 added, 1 removed, ~91% saved]
ADDED:
  - button "Submit" [ref=47]
REMOVED:
  - button "Loading..." [ref=44]

Snapshot Cache + Prefetch

After browser_navigate, PlayGuard immediately fetches a snapshot in the background. The next browser_snapshot is served from cache instantly. An unchanged page returns UNCHANGED without hitting the browser.

Screenshot Policy

| Mode | Behavior | |------|----------| | warn (default) | Screenshot executes; warning written to stderr | | redirect | Replaced with browser_snapshot. Pass {visual:true} for a real screenshot | | block | Blocked with an error; agent is told which env var to change | | allow | No restriction |

Eval Cache + Output Compaction

Repeated browser_evaluate calls with the same script on the same URL are served from cache (configurable TTL). Large eval output is truncated to a character limit to avoid blowing the context window.


Figma Optimizer

PlayGuard intercepts Figma MCP responses and runs them through an optimization pipeline before delivering to the agent.

[PlayGuard figma: -68% (284.0KB→91.0KB)]

Note: Modules 1–4 and 6 target raw Figma REST API JSON (document.children, componentId, fillGeometry). If your Figma MCP upstream returns pre-simplified YAML (e.g. Framelink figma-developer-mcp), those modules find nothing to strip — Module 8 handles that shape instead — and inBytes/outBytes are still measured from the actual upstream response text, so the automatic YAML→JSON reformatting (typically ~2x on its own) and Module 5's metadata trim both still count as real savings. parseSkip in the NDJSON log means the response body couldn't be parsed as JSON or YAML at all, not that the optimizer no-op'd.

Module 1 — Metadata Cleaner: Removes fields irrelevant to layout: createdAt, updatedAt, creator, thumbnailUrl, pluginData, sharedPluginData, exportSettings, reactions, interactions, etc.

Module 2 — Invisible Layer Pruner: Recursively removes nodes where visible === false or opacity === 0.

Module 3 — Component Deduplication: Repeated instances of the same component are collapsed to a reference. 100 buttons → first full definition + 99 references with overrides only.

{ "type": "INSTANCE", "name": "Button/Primary", "_ref": "123:4", "overrides": [] }

Module 4 — SVG Refs: Replaces inline SVG geometry (fillGeometry) with { "_svgRef": "nodeId" }. The agent gets the shape identifier without thousands of path coordinates.

Module 6 — Layout Compressor: Removes absolute x/y from nodes inside Auto Layout containers — they are redundant because position is determined by layoutMode, gap, and padding.

Module 5 — Top-Level Metadata Trim: Drops metadata.thumbnailUrl (a signed, single-use preview URL) and metadata.lastModified from pre-simplified upstream shapes like Framelink's { metadata, nodes, globalVars } — fields Module 1 can't reach because it only walks document/children, not a sibling metadata key.

Module 8 — Framelink Shape Optimizer: Targets the pre-simplified { metadata, nodes[], globalVars.styles } shape (Framelink figma-developer-mcp), where Modules 2/4/6 find nothing to strip. 8a: sibling subtrees identical except for ids collapse to { id, name, _sameAs: firstId } (repeated cards/icons); structural copies that differ only in text/name/position collapse to the same stub plus a _textDiff map. 8b: no-op layout styles ({ mode: "none", sizing: {} }) are dropped from globalVars.styles along with node refs to them. 8c: float noise in styles is rounded to 2 decimals ("1.3999999364217122em""1.4em"); node text is never touched.

Module 7 — Budget Trim: If the optimized tree still exceeds FIGMA_TEXT_COMPACT, it's trimmed structurally instead of sliced as text. Budget is allocated depth-first, proportional to each branch's size; a branch that doesn't fit collapses to an {id, name, type, _stub:true} marker instead of being silently dropped, so every top-level section stays visible and the agent can re-fetch a stubbed branch by its id. Only a genuinely unparseable response (parseSkip) falls back to a raw text slice.


Design Diff

playguard_compare_design compares a Figma node against the live DOM element that implements it and reports the exact differences — no screenshots, no eyeballing.

[PlayGuard design diff: 42:1337 → [data-testid="login-btn"] 2 mismatches, 6 matches]

  fontSize:        Figma 16px              → Browser 14px              ⚠️  MISMATCH (+2.0px)
  color:           Figma rgb(255, 255, 255)→ Browser rgb(255, 255, 255) ✓  MATCH
  backgroundColor: Figma rgb(25, 118, 210) → Browser rgb(25, 118, 210) ✓  MATCH
  padding:         Figma 12px 24px 12px 24px → Browser 12px 16px 12px 16px ⚠️  MISMATCH (Δright:+8px, Δleft:+8px)
  borderRadius:    Figma 8px               → Browser 8px 8px 8px 8px   ✓  MATCH

Three modes:

| Mode | Arguments | Use | |------|-----------|-----| | Single | figmaNodeId + browserSelector | One element | | Batch | pairs: [{figmaNodeId, browserSelector}, …] | Several at once; one bad selector doesn't sink the rest | | Auto-map | figmaNodeId + autoMap: true | Point at a component; layers beneath are matched to DOM elements by data-figma-id, data-testid, id, class, or exact text |

Notes on how it decides:

  • Properties are auto-selected per node. A TEXT layer gets its typography and colour; a container gets backgroundColor/padding/borderRadius/boxShadow. Pass properties[] to override.
  • Tolerances. Sizes compare within PLAYGUARD_DESIGN_DIFF_TOLERANCE_PX, colours within PLAYGUARD_DESIGN_DIFF_TOLERANCE_COLOR per channel. Box properties (padding, margin, borderRadius, borderWidth) compare side-by-side, so 8px 8px 0 0 never passes as 8px.
  • Unset ≠ unknown. A property Figma leaves unset but CSS zero-defaults (no shadow, no border) is still checked, so a stray browser value is caught. margin is the exception — Figma has no margin concept, so it is reported as unknown rather than as a defect.
  • Typography follows the label. A button's font size is read from its TEXT layer at any depth, and its background is never mistaken for its text colour.
  • width/height are never auto-selected — they depend on the viewport, not the design. Request them explicitly and a warning rides along.
  • Auto-map never guesses. A candidate selector matching zero or several elements is reported as unmapped, not silently attached to the wrong element.
  • A missing node is an error, not a substitute. A figmaNodeId the upstream response doesn't contain fails that pair, rather than falling back to whatever the response's root frame happens to be.

Both upstream shapes are handled: the raw Figma REST API (@figma/mcp) and Framelink's pre-simplified globalVars.styles form (figma-developer-mcp).


Requirements

  • Node.js 18+
  • npm

Installation

No install needed — run via npx. For local development instead:

git clone https://github.com/ZenyaDAR/PlayGuard.git
cd playguard
npm install
npm run build

Usage

Claude Code

Add to ~/.claude/claude_desktop_config.json (or your project's .claude/settings.json):

Browser only:

{
  "mcpServers": {
    "playguard": {
      "command": "npx",
      "args": ["-y", "playguard"],
      "env": {
        "PLAYGUARD_SCREENSHOTS": "redirect"
      }
    }
  }
}

Browser + Figma:

{
  "mcpServers": {
    "playguard": {
      "command": "npx",
      "args": ["-y", "playguard"],
      "env": {
        "PLAYGUARD_SCREENSHOTS": "redirect",
        "FIGMA_MCP_CMD": "npx @figma/mcp",
        "FIGMA_API_KEY": "your-figma-api-key",
        "FIGMA_CACHE_TTL": "60000"
      }
    }
  }
}

Do not add Playwright MCP separately — PlayGuard spawns it automatically from its bundled dependency.

Running from a local clone instead? Use "command": "node", "args": ["/path/to/playguard/dist/index.js"].

Claude Desktop

Same JSON format. Config file locations:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Windows paths

Use forward slashes in JSON strings. Spaces in paths do not need escaping:

"args": ["C:/Users/YourName/projects/playguard/dist/index.js"]

Environment Variables

Playwright

| Variable | Default | Description | |----------|---------|-------------| | PLAYGUARD_SCREENSHOTS | warn | allow / warn / block / redirect | | PLAYGUARD_COMPACT | true | Set false to disable compact snapshots and receive the raw accessibility tree | | PLAYGUARD_DELTA | true | Set false to disable delta snapshots | | PLAYGUARD_DELTA_THRESHOLD | 0.4 | Fraction of lines that must change to trigger a full snapshot instead of a delta (0–1) | | PLAYGUARD_TOKEN_BUDGET | 0 | Max tokens per snapshot; truncates on a line boundary so [ref=] tags are never split. 0 = off | | PLAYGUARD_HINT_THRESHOLD | 4 | After N consecutive snapshots without any action, inject a hint listing available interactive refs | | PLAYGUARD_PREFETCH_SNAPSHOT | true | Set false to disable background snapshot prefetch after browser_navigate | | PLAYGUARD_SMART_WAIT | false | Set 1/true to retry a snapshot that looks mid-load (few refs + a loading/spinner indicator) instead of returning it as-is | | PLAYGUARD_SMART_WAIT_MS | 1000 | Delay between smart-wait retries, in ms | | PLAYGUARD_SMART_WAIT_MAX_RETRIES | 3 | Max smart-wait retries before giving up and returning the snapshot with a warning | | PLAYGUARD_SMART_WAIT_MIN_REFS | 5 | A snapshot with at least this many [ref=] elements is never considered "still loading" | | PLAYGUARD_EVAL_CACHE_TTL | 500 | browser_evaluate cache TTL in ms. 0 = off | | PLAYGUARD_EVAL_COMPACT | 10000 | Max characters for eval output. 0 = off | | PLAYWRIGHT_MCP_CMD | bundled binary | Override the Playwright MCP command | | PLAYWRIGHT_MCP_ARGS | — | Extra arguments passed to Playwright MCP (space-separated; wrap an argument in "..." or '...' if it contains a space, e.g. a path). Passing --output-dir here takes precedence over PLAYGUARD_OUTPUT_DIR | | PLAYGUARD_OUTPUT_DIR | .playguard/ | Where all on-disk artifacts go, split by origin: .playguard/.qa/ for everything from the site under test (screenshots, traces, PDFs, downloads, session) and .playguard/.figma/ for Figma image exports. Defaults to .playguard/ in the project root instead of the upstream's .playwright-mcp/ | | PLAYGUARD_PROJECT_ROOT | nearest .git/package.json above cwd | The root .playguard/ is created in. process.cwd() is whatever the MCP client spawned the server with, so it is not trusted; set this to pin the location outright | | PLAYGUARD_LOG_DIR | logs/ | Override the NDJSON analytics log directory |

Figma

| Variable | Default | Description | |----------|---------|-------------| | FIGMA_MCP_CMD | — | Figma MCP launch command. Unset = Figma disabled | | FIGMA_MCP_ARGS | — | Extra arguments for Figma MCP (space-separated; wrap an argument in "..." or '...' if it contains a space) | | FIGMA_CACHE_TTL | 0 | Figma response cache TTL in ms. 0 = off | | FIGMA_SVG_REFS | true | Set false to keep SVG geometry inline | | FIGMA_TEXT_COMPACT | 10000 | Max characters for the Figma text response. When the optimizer parsed the tree, an over-budget response is trimmed structurally (Module 7) — branches collapse to {id,name,type} stubs, never silently disappear. Only falls back to a raw text slice if the response never parsed (parseSkip). 0 = off | | FIGMA_API_KEY | — | Forwarded to the Figma MCP child process |

Design Diff

| Variable | Default | Description | |----------|---------|-------------| | PLAYGUARD_DESIGN_DIFF_TOLERANCE_PX | 2 | Size difference, in px, still counted as a match | | PLAYGUARD_DESIGN_DIFF_TOLERANCE_COLOR | 5 | Per-channel RGB difference still counted as a match |


Configuration Examples

Headless browser + Figma with cache:

"env": {
  "PLAYWRIGHT_MCP_ARGS": "--headless",
  "FIGMA_MCP_CMD": "npx @figma/mcp",
  "FIGMA_API_KEY": "your-figma-api-key",
  "FIGMA_CACHE_TTL": "120000"
}

Firefox + block screenshots:

"env": {
  "PLAYWRIGHT_MCP_ARGS": "--browser firefox",
  "PLAYGUARD_SCREENSHOTS": "block"
}

Persist cookies between sessions:

"env": {
  "PLAYWRIGHT_MCP_ARGS": "--storage-state /tmp/browser-state.json"
}

Redirect screenshots + hard token cap:

"env": {
  "PLAYGUARD_SCREENSHOTS": "redirect",
  "PLAYGUARD_TOKEN_BUDGET": "4000"
}

Analytics

All tool calls are logged to logs/YYYY-MM-DD.ndjson. Run the report:

npm run analyze

Sample output:

── Snapshot token savings ────────────────────────────────
  Cache hits:      12/47 snapshots (8 from prefetch)
  Bytes saved by cache:    84.3 KB  (~21 075 tokens)
  Bytes saved by compact:  312.1 KB (~78 025 tokens)
  Total saved:             396.4 KB (~99 100 tokens)
  Reduction vs raw:        83%  (82.1 KB sent vs 478.5 KB without PlayGuard)

── PlayGuard interceptions ───────────────────────────────
  Screenshot → snapshot: 5 redirected (2 from cache, ~18 400 tokens saved)
  Eval cache hits:       8/11 evaluates (~240ms saved at 30ms avg)

── Figma optimizer ───────────────────────────────────────
  Calls:       24 total  (6 cache hits, 18 optimized)
  Tokens in:   ~71 000  →  out: ~22 750  (saved ~48 250, -68%)

── Latency by tool ───────────────────────────────────────
| tool              | count | errors | err% | intercepted | avg ms | p50 | p95 |

Each NDJSON line includes all fields for downstream analysis:

  • Playwright: rawBytes, keptBytes, savedBytes, delta, cacheHit, prefetchHit, snapCount, scriptHash
  • Figma: fileKey, nodeId, inBytes, outBytes, savedTokens, metaKeysDeleted, invisiblePruned, svgRefsReplaced, instancesCollapsed, layoutCoordsRemoved

Benchmark

npm run bench

Measures proxy overhead, snapshot vs screenshot size, cache hit rate, and crash recovery time. No LLM in the loop — raw numbers only.


Tests

npm test

Covers collapseRuns, compactSnap (including token budget boundary), the full Figma optimizer pipeline (optimizeFigmaResponse), the structural budget trim (budgetTrimFigma), and the artifact-path plumbing — withOutputDir, figmaLocalPath, and resolveUpstream, which locates the Playwright MCP through the module graph — in test/playguard.test.mjs, plus dead() crash detection, splitArgs() quoting, the shared ttlCache() helper, the decideSnapshot() cache/delta/hint decision logic, the section/around/depth snapshot filtering, the looksLikeLoading() smart-wait heuristic, the sortDeep() Figma cache-key normalization, and the Module 8 Framelink-shape optimizations in test/playguard-core.test.mjs. test/design-diff.test.mjs covers the design-diff side: property extraction from both upstream Figma shapes (REST and Framelink), colour/border/typography normalization, node resolution including the not-found case, the comparison and tolerance rules, and the generated auto-map and eval scripts.

CI (GitHub Actions, .github/workflows/ci.yml) runs npm test on every push and pull request to main.


Comparison

| Scenario | Without PlayGuard | With PlayGuard | |----------|-------------------|----------------| | Browser crashes | Error; session broken | Auto-recovery; agent never sees it | | Concurrent calls during crash | Two failures or two browsers | One restart; both calls continue | | browser_snapshot (full tree) | 100–500 KB | 10–80 KB after compact | | Repeated browser_snapshot same page | Re-fetches every time | Cache hit or UNCHANGED | | Figma file with 50+ components | 200–500 KB JSON | 60–150 KB after optimization | | Two MCPs in agent config | Two separate servers | One PlayGuard | | Proxy overhead | None | ~1–3 ms per Playwright call |


Project Structure

playguard/
├── src/
│   ├── index.ts                    MCP server: tool routing, proxying, caches
│   ├── config.ts                   Env-derived settings, resolved paths, upstream commands
│   ├── snapshot.ts                 Snapshot compaction and the cache/delta decision
│   ├── figma-optimize.ts           Figma response optimizer (Modules 1–8)
│   └── design-diff.ts              Figma ↔ browser property extraction and diffing
├── dist/                           Compiled output (generated by npm run build)
├── bench/
│   ├── run.mjs                     Benchmark: latency, token savings, crash recovery
│   └── analyze.mjs                 Analytics report from NDJSON logs
├── test/
│   ├── playguard.test.mjs          Compact/Figma optimizer, output-dir routing, upstream resolution
│   ├── playguard-core.test.mjs     Crash detection, arg parsing, caching, snapshot decision
│   └── design-diff.test.mjs        Property extraction, comparison, auto-map
├── .github/workflows/ci.yml        Runs npm test on push/PR
├── logs/                           Per-day NDJSON call logs (auto-created at runtime)
├── .playguard/                     Auto-created artifact root
│   ├── .qa/                        Screenshots, traces, PDFs, downloads from the site under test
│   └── .figma/                     Figma image exports
├── package.json
└── tsconfig.json

License

MIT