Skip to content

Releases

v0.10.1

Choose a tag to compare

@ZenyaDAR ZenyaDAR released this 01 Nov 15:45

Listed in the official MCP Registry as io.github.ZenyaDAR/playguard (server.json, mcpName in package.json). No code changes.

v0.10.0

Choose a tag to compare

@ZenyaDAR ZenyaDAR released this 01 Nov 14:34

Added

  • Official Figma MCP (desktop server) as an alternative to Framelink. Set
    FIGMA_MCP_URL=http://127.0.0.1:3845/mcp — no API key; the Figma desktop
    app's login authorizes it (Dev or Full seat on a paid plan, Dev Mode →
    "Enable desktop MCP server"). Its tools are routed over Streamable HTTP and
    passed through whole (generated code is never sliced) and not cached (answers
    follow the live selection). playguard_compare_design stays Framelink-only.
    A closed desktop app gives a "not reachable, turn the server on" error; when
    it comes up later its tools are registered and announced with
    tools/list_changed. Only transport failures drop the connection (with one
    retry), so one bad call never aborts the others. Figma's remote server
    (mcp.figma.com) only admits clients from Figma's catalog, so that URL is
    refused up front with the alternatives.
  • playguard_a11y_audit. Runs axe-core (bundled, 4.14) in the current
    Playwright page and returns WCAG 2.0–2.2 A/AA violations grouped by rule, most
    severe first: CSS selector, a short html excerpt and the first fix per
    element, at most 5 elements per rule. Optional selector (audit part of the
    page) and tags (replaces the default; an unknown tag is an error, not a
    clean report). New dependency: axe-core (MPL-2.0).
  • Chrome DevTools MCP as an optional third upstream. Set
    DEVTOOLS_MCP_CMD (e.g. npx -y chrome-devtools-mcp@latest) and
    DEVTOOLS_MCP_ARGS (e.g. --headless --isolated); its tools are merged into
    the tool list and routed to it. The upstream starts lazily, runs in its own
    Chrome, and a failure there leaves Playwright and Figma working. PlayGuard
    rewrites its responses (disable with PLAYGUARD_DEVTOOLS_OPTIMIZE=false, or
    per call with raw: true):
    • list_network_requests without paging/filter params and over
      PLAYGUARD_DEVTOOLS_NETWORK_MAX (30) rows becomes a summary — main
      document, failed and pending rows, per-host counts (Shopify store 34K →
      ~1K chars, Wikipedia 7.6K → ~0.6K).
    • get_network_request collapses noisy headers and cuts non-JSON bodies
      over PLAYGUARD_DEVTOOLS_BODY_MAX (3000) chars (HTML documents −63–72%,
      JSON bodies ~−20%, headers only).
    • take_snapshot is compacted without losing an interactive uid (−3% to
      −40%), a repeat of an unchanged page is one UNCHANGED line, and a small
      change comes back as ADDED/REMOVED lines.
    • Performance traces drop example questions and link targets (−13%).
    • tools/list drops $schema, adds raw, and notes on the navigation
      tools that this is a separate Chrome from browser_*.
    • Five opaque upstream errors get a one-line hint with the next step.
  • DevTools launch defaults: --no-usage-statistics, --no-performance-crux,
    --no-page-id-routing and --workspace <project root>, each skipped when
    you pass the same option yourself; PLAYGUARD_DEVTOOLS_DEFAULTS=false skips
    all of them. The upstream gets an allowlisted environment (no API keys) with
    its own npm update check off, runs in .playguard/.qa, gets a 180 s timeout
    for traces, Lighthouse and heap snapshots, and is respawned with the last URL
    reopened if its process dies.
  • DevTools take_screenshot follows PLAYGUARD_SCREENSHOTS.
  • Shopify Dev MCP and Context7 as optional documentation upstreams. Set
    SHOPIFY_DEV_MCP_CMD (e.g. npx -y @shopify/dev-mcp@latest) and/or
    CONTEXT7_MCP_CMD (e.g. npx -y @upstash/context7-mcp@latest, Node ≥
    20.18.1), plus optional *_ARGS. Lazy start, allowlisted environment
    (CONTEXT7_API_KEY goes to Context7 only; Shopify telemetry off via
    OPT_OUT_INSTRUMENTATION=true), failure isolated from the other upstreams.
    Rewrites (disable with PLAYGUARD_DOCS_OPTIMIZE=false, or per call with
    raw: true):
    • search_docs_chunks: JSON → compact markdown, exact duplicates and
      byte-identical per-API-version twins dropped (−19–20%, every URL kept).
    • learn_shopify_api: escaped fences unescaped; always returned whole.
    • Shopify tools/list: feedback hidden, model/user_prompt dropped,
      conversationId optional and filled in by PlayGuard (26.6K → 21.3K).
    • resolve-library-id: one line per library (−48–50%, every id kept).
    • query-docs: shorter GitHub Source: URLs (−1–2%); code untouched.
    • Context7 tools/list + instructions: 5.9K → 2.6K chars.
  • Disk cache for search_docs_chunks, resolve-library-id and query-docs
    in .playguard/.cache/docs/ (PLAYGUARD_DOCS_CACHE_TTL, 24 h; 7× for
    version-pinned Context7 ids; errors never cached; atomic writes). An
    identical call repeated in the same process gets a one-line UNCHANGED note
    that says to call again with raw: true if the earlier answer is gone.
  • Calling a Shopify Dev or Context7 tool while that upstream failed to start
    returns [PlayGuard] … MCP is not running: <error> with what to check,
    instead of Tool not found.
  • npm run analyze reports DevTools, Shopify Dev and Context7 calls in their own sections;
    bench/smoke-devtools.mjs checks the integration against the real
    chrome-devtools-mcp.

Changed

  • Breaking: Node.js 20.18.1 or newer is required (was 18). Node 18 and 20
    are past end of life, and Context7 needs 20.18.1 anyway. CI and publishing
    run on Node 22.
  • tools/list waits at most PLAYGUARD_LIST_WAIT_MS (6000) for an optional
    upstream (Figma, DevTools, Shopify Dev, Context7); all of them are listed in
    parallel, so the worst case is one wait, not one per upstream. A slow one — a first npx download, an
    initialize that never answers — is listed later through
    notifications/tools/list_changed instead of holding up the whole list.

Fixed

  • Dependencies: @modelcontextprotocol/sdk 1.32.2 and js-yaml 5.5.0 (plus
    their transitive updates) clear all 10 npm audit advisories. Most were in
    the SDK's HTTP server stack, which PlayGuard (stdio only) never runs; the
    js-yaml ones were parse-time DoS on snapshot YAML.
  • Screenshot redirect: a browser_take_screenshot call with filename,
    element/ref/target or fullPage was answered with a snapshot,
    silently dropping the file, element or full-page request. It now takes a
    real screenshot, as if visual: true were passed.
  • A failed handshake with an upstream (Playwright, Figma, DevTools) now closes
    its transport, so retries no longer leave orphaned child processes running.
  • README: the Figma examples used npx @figma/mcp; they now use Framelink
    (FIGMA_MCP_CMD=npx, FIGMA_MCP_ARGS=-y figma-developer-mcp --stdio). The
    Claude Code config location is .mcp.json in the project or
    claude mcp add --scope user.

Docs

  • Repositioned PlayGuard as a proxy MCP (one server, several upstreams chosen
    by env vars) in README, package description/keywords and code comments; added
    an "Upstream MCPs at a glance" table.

v0.9.0

Choose a tag to compare

@ZenyaDAR ZenyaDAR released this 30 Oct 12:43

Added

  • Update check. On startup PlayGuard asks the npm registry (honouring
    npm_config_registry) for the latest version in the background. If the running
    copy is older, the first tool result starts with a one-line notice saying how to
    update, specific to where the copy runs from (npx cache, installed copy, checkout).
    Disable with PLAYGUARD_UPDATE_CHECK=false.
  • Every log line carries ver, the PlayGuard version that served the call.

Changed

  • README configs use playguard@latest: a bare playguard could keep running a stale
    npx-cached or globally installed copy indefinitely.

v0.8.0

Choose a tag to compare

@ZenyaDAR ZenyaDAR released this 28 Oct 14:03

Changed

  • Snapshot prefetch is now off by default. Real logs showed 3 prefetch hits in
    3238 navigates: the next call is almost always browser_evaluate, which discards
    the prefetched snapshot, while the background call slowed that evaluate down.
    Restore with PLAYGUARD_PREFETCH_SNAPSHOT=true.
  • The bundled Playwright MCP now runs with --codegen none, dropping the
    ### Ran Playwright code block from every response (80–150 chars per call).
    Skipped for a custom PLAYWRIGHT_MCP_CMD or an explicit --codegen; restore
    with PLAYGUARD_CODEGEN=keep.
  • browser_evaluate, browser_run_code_unsafe and browser_wait_for descriptions
    carry a one-line hint to batch checks into fewer calls. Disable with
    PLAYGUARD_HINTS=false.

Added

  • Log lines for failed calls carry errText (first 200 chars of the error, with
    ANSI colour codes and ### Error headings stripped so the cause fits).
  • browser_wait_for logs waitKind/waitTime; browser_run_code_unsafe logs
    codeChars/codeHash/codeSleeps (never the code); screenshots log
    fullPage/type.
  • Proxy-level tests against a fake upstream (test/proxy.test.mjs), and
    bench/smoke-codegen.mjs, a real-browser check that evaluate results still
    parse without the codegen block.
  • bench/analyze.mjs reports Playwright call patterns: top error texts,
    evaluate→evaluate chains, evaluate latency after navigate, fixed sleeps in
    wait_for and run_code.

Fixed

  • A tool call whose upstream threw (rather than returning isError) was never
    logged. It is now logged with thrown: true and still rethrown to the client.

v0.7.1

Choose a tag to compare

@ZenyaDAR ZenyaDAR released this 19 Oct 10:20

Fixed

  • The Figma optimizer had been a silent no-op on Framelink since figma-developer-mcp 0.13, which changed its default output from YAML to a tree text format that parses as neither JSON nor YAML. Every get_figma_data response was logged as parseSkip: "parse-error" and passed through unoptimized (only the raw text slice still applied). PlayGuard now sets OUTPUT_FORMAT=json for the Figma child process unless you set OUTPUT_FORMAT yourself; Figma MCPs that don't read the variable ignore it. The same parse step feeds playguard_compare_design, which returned "failed to parse Figma response" on Framelink for the same reason.

v0.7.0

Choose a tag to compare

@ZenyaDAR ZenyaDAR released this 24 Aug 17:32

Changed

  • Artifacts are now split by origin: .playguard/.qa/ holds everything produced by the site under test (screenshots, traces, PDFs, downloads, saved session) and .playguard/.figma/ holds Figma image exports. They used to share one flat directory, which also meant Figma exports sat inside Playwright MCP's output dir and were eligible for its outputMaxSize eviction — that sweep lists files recursively and unlinks the oldest, so a downloaded design asset could be deleted to make room for a screenshot. Breaking for anyone who set PLAYGUARD_OUTPUT_DIR: files now land one level deeper, in .qa/.

Fixed

  • .playguard/ was anchored on process.cwd(), which for an MCP server is whatever directory the client happened to spawn it from — its own install folder, the user's home, /. Artifacts landed somewhere different depending on the client. The root is now the nearest ancestor of cwd holding a .git or package.json, overridable with PLAYGUARD_PROJECT_ROOT, and PlayGuard says so on stderr when that walk-up only finds its own install directory.
  • Figma image downloads with no localPath were passed through untouched, so the upstream wrote them into its own cwd instead of the project. They now default to .playguard/.figma/. A relative localPath is also clamped to that directory — ../../x used to resolve straight back out into the project.
  • An unwritable artifact root no longer kills the server at startup. Snapshots, the main feature, need no disk.

v0.6.3

Choose a tag to compare

@ZenyaDAR ZenyaDAR released this 16 Aug 13:31

Changed

  • The shared ttlCache() behind the eval and Figma caches now evicts the least recently used entry instead of emptying itself. Reaching maxEntries used to throw away all 500 entries at once, so the hot key an agent was hitting on every step died along with the cold ones and the next call was a full miss — the worst behavior exactly when the cache was under the most load. Expired entries are also dropped on read now rather than sitting there until the next overflow. Reported in #1.

Full changelog: https://github.com/ZenyaDAR/PlayGuard/blob/main/CHANGELOG.md

v0.6.2

Choose a tag to compare

@ZenyaDAR ZenyaDAR released this 05 Aug 15:33

Fixed

  • Two different Figma calls could share one cache entry. The cache key was built
    with JSON.stringify's array replacer, which is a property allowlist applied
    at every depth rather than a key ordering, so every nested key was dropped and
    download_figma_images for two different node sets both serialized to
    {"fileKey":…,"nodes":[{}]}. With FIGMA_CACHE_TTL set, the second call got
    the first one's images. argsHash in the analytics log inherited the same
    blindness and reported the two calls as byte-identical.
  • The "N snapshots without action" hint fired even when the agent acted between
    every snapshot: the counter was incremented on every snapshot and never reset,
    so past PLAYGUARD_HINT_THRESHOLD it was really counting total snapshots and
    the message it injected was untrue. It now resets whenever a mutating tool
    succeeds, which is what the documented behavior always claimed.
  • With PLAYGUARD_SCREENSHOTS=redirect, a browser_take_screenshot could be
    answered with a section/around/depth-filtered subtree left in the cache
    by an earlier filtered snapshot — a different question than the one asked. The
    cached redirect now requires an unfiltered cache entry.
  • The background prefetch after browser_navigate could land after a mutating
    tool had already invalidated the cache, writing pre-action DOM back with a
    fresh timestamp for the UNCHANGED shortcut and the screenshot redirect to
    serve as current. Late prefetches are now discarded.
  • A figmaNodeId that matched nothing in the fetched response fell back to the
    response root, so the diff compared the containing page's background, padding
    and radius against the caller's selector and reported confident MATCH/MISMATCH
    rows for an element nobody asked about. A miss is now a pair error.
  • Every ordinary left-aligned text layer reported a false left → start
    mismatch: getComputedStyle returns the logical start/end for anything
    without an explicit text-align, and textAlign has no CSS zero-default to
    drop it from the auto-selected set. start/end now normalize to
    left/right (LTR).
  • A browserSelector containing a line break — as it does when pasted out of a
    formatted CSS block — ended the generated eval script's string literal early
    and failed the pair with an opaque browser error: instead of a clean
    "element not found".
  • A malformed numeric env var silently disabled the feature it configured rather
    than falling back. PLAYGUARD_SMART_WAIT_MS=1s parsed to NaN, and
    setTimeout(res, NaN) fires immediately while attempts < NaN is always
    false, so smart-wait burned its retries with no delay — or never ran at all.
    All numeric variables now fall back to their documented default.
  • The compact snapshot header counted lines before runs were collapsed into
    [×N more similar elements], advertising a body that was never sent.

v0.6.1

Choose a tag to compare

@ZenyaDAR ZenyaDAR released this 18 Jul 13:28

Fixed

  • The upstream Playwright MCP failed to spawn whenever PlayGuard itself was not
    the project root — under npx, a global install, or any hoisted layout. It
    was located by shim (node_modules/.bin/playwright-mcp) with a fall back to
    the bare command name, and in those layouts the shim sits in a directory that
    never reaches the child process's PATH, so every browser tool failed with
    "playwright-mcp.cmd is not recognized". It is now resolved through Node's
    module graph and run with the node binary already in use, which does not
    depend on PATH or on where the package was installed.

v0.6.0

Choose a tag to compare

@ZenyaDAR ZenyaDAR released this 18 Jul 12:41

Added

  • playguard_compare_design: compares a Figma node against the live DOM and
    reports only the properties that actually differ. Three modes — single
    element, batch, and auto-map (generates a browser script that matches Figma
    layers to DOM nodes by text). Agents were previously eyeballing screenshots
    against Figma, which is both expensive in tokens and unreliable for
    spacing/colour deltas.
  • PLAYGUARD_DESIGN_DIFF_TOLERANCE_PX (default 2) and
    PLAYGUARD_DESIGN_DIFF_TOLERANCE_COLOR (default 5) — sub-pixel rounding and
    colour-space drift would otherwise flood the report with false mismatches.

Fixed

  • Border colour never matched on Framelink upstreams: Figma's 0–1 RGB objects,
    hex strings, and ready-made rgb() strings now all go through one
    normalizer before comparison, and borderColor is compared with the colour
    tolerance rather than as an exact string.
  • Nodes served by the Figma REST API with a shared-styles map were detected as
    Framelink-shaped and silently produced all-null properties. Upstream shape is
    now decided by markers unique to each shape, not by the presence of styles,
    which both shapes use for different things.
  • Text properties were read from the container rather than the text layer, so a
    button's background colour was reported as its text colour. Typography and
    colour now resolve against the nearest TEXT descendant at any depth, falling
    back to the container only where it is the node that actually defines the
    property (Framelink hoists typography onto the frame).
  • margin was reported as a mismatch whenever the DOM had one, since Figma has
    no margin concept and always yielded null. It is now treated as unknowable
    and skipped, not as an expected zero.
  • U+2028/U+2029 in Figma layer names broke the generated auto-map script —
    JSON.stringify does not escape them, and they terminate a line in JS source.

Changed

  • src/index.ts split into config.ts, snapshot.ts, figma-optimize.ts,
    and design-diff.ts. It had grown past 1100 lines and the design-diff work
    would have added several hundred more.
  • Screenshots and traces default to .playguard/ in the project directory
    instead of the system temp dir, and --output-dir is only injected when the
    user has not set it themselves.
  • Dropped the manual cmd /c wrapper around upstream spawns on Windows. The
    MCP SDK already routes through cross-spawn; the extra wrapper mangled
    arguments containing spaces or non-ASCII characters, so any project path
    with a space in it broke screenshot output.