Repository navigation
Releases
Releases · ZenyaDAR/PlayGuard
v0.10.1
v0.10.0
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_designstays 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. Optionalselector(audit part of the
page) andtags(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 withPLAYGUARD_DEVTOOLS_OPTIMIZE=false, or
per call withraw: true):list_network_requestswithout 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_requestcollapses noisy headers and cuts non-JSON bodies
overPLAYGUARD_DEVTOOLS_BODY_MAX(3000) chars (HTML documents −63–72%,
JSON bodies ~−20%, headers only).take_snapshotis compacted without losing an interactive uid (−3% to
−40%), a repeat of an unchanged page is oneUNCHANGEDline, and a small
change comes back asADDED/REMOVEDlines.- Performance traces drop example questions and link targets (−13%).
tools/listdrops$schema, addsraw, and notes on the navigation
tools that this is a separate Chrome frombrowser_*.- 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-routingand--workspace <project root>, each skipped when
you pass the same option yourself;PLAYGUARD_DEVTOOLS_DEFAULTS=falseskips
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_screenshotfollowsPLAYGUARD_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_KEYgoes to Context7 only; Shopify telemetry off via
OPT_OUT_INSTRUMENTATION=true), failure isolated from the other upstreams.
Rewrites (disable withPLAYGUARD_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:feedbackhidden,model/user_promptdropped,
conversationIdoptional and filled in by PlayGuard (26.6K → 21.3K). resolve-library-id: one line per library (−48–50%, every id kept).query-docs: shorter GitHubSource:URLs (−1–2%); code untouched.- Context7
tools/list+ instructions: 5.9K → 2.6K chars.
- Disk cache for
search_docs_chunks,resolve-library-idandquery-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-lineUNCHANGEDnote
that says to call again withraw: trueif 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 ofTool not found. npm run analyzereports DevTools, Shopify Dev and Context7 calls in their own sections;
bench/smoke-devtools.mjschecks 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/listwaits at mostPLAYGUARD_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 firstnpxdownload, an
initialize that never answers — is listed later through
notifications/tools/list_changedinstead of holding up the whole list.
Fixed
- Dependencies:
@modelcontextprotocol/sdk1.32.2 andjs-yaml5.5.0 (plus
their transitive updates) clear all 10npm auditadvisories. 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_screenshotcall withfilename,
element/ref/targetorfullPagewas answered with a snapshot,
silently dropping the file, element or full-page request. It now takes a
real screenshot, as ifvisual: truewere 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.jsonin 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
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 withPLAYGUARD_UPDATE_CHECK=false. - Every log line carries
ver, the PlayGuard version that served the call.
Changed
- README configs use
playguard@latest: a bareplayguardcould keep running a stale
npx-cached or globally installed copy indefinitely.
v0.8.0
Changed
- Snapshot prefetch is now off by default. Real logs showed 3 prefetch hits in
3238 navigates: the next call is almost alwaysbrowser_evaluate, which discards
the prefetched snapshot, while the background call slowed that evaluate down.
Restore withPLAYGUARD_PREFETCH_SNAPSHOT=true. - The bundled Playwright MCP now runs with
--codegen none, dropping the
### Ran Playwright codeblock from every response (80–150 chars per call).
Skipped for a customPLAYWRIGHT_MCP_CMDor an explicit--codegen; restore
withPLAYGUARD_CODEGEN=keep. browser_evaluate,browser_run_code_unsafeandbrowser_wait_fordescriptions
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### Errorheadings stripped so the cause fits). browser_wait_forlogswaitKind/waitTime;browser_run_code_unsafelogs
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.mjsreports Playwright call patterns: top error texts,
evaluate→evaluate chains, evaluate latency after navigate, fixed sleeps in
wait_forandrun_code.
Fixed
- A tool call whose upstream threw (rather than returning
isError) was never
logged. It is now logged withthrown: trueand still rethrown to the client.
v0.7.1
Fixed
- The Figma optimizer had been a silent no-op on Framelink since
figma-developer-mcp0.13, which changed its default output from YAML to atreetext format that parses as neither JSON nor YAML. Everyget_figma_dataresponse was logged asparseSkip: "parse-error"and passed through unoptimized (only the raw text slice still applied). PlayGuard now setsOUTPUT_FORMAT=jsonfor the Figma child process unless you setOUTPUT_FORMATyourself; Figma MCPs that don't read the variable ignore it. The same parse step feedsplayguard_compare_design, which returned "failed to parse Figma response" on Framelink for the same reason.
v0.7.0
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 itsoutputMaxSizeeviction — 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 setPLAYGUARD_OUTPUT_DIR: files now land one level deeper, in.qa/.
Fixed
.playguard/was anchored onprocess.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 ofcwdholding a.gitorpackage.json, overridable withPLAYGUARD_PROJECT_ROOT, and PlayGuard says so on stderr when that walk-up only finds its own install directory.- Figma image downloads with no
localPathwere passed through untouched, so the upstream wrote them into its own cwd instead of the project. They now default to.playguard/.figma/. A relativelocalPathis also clamped to that directory —../../xused 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
Changed
- The shared
ttlCache()behind the eval and Figma caches now evicts the least recently used entry instead of emptying itself. ReachingmaxEntriesused 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
Fixed
- Two different Figma calls could share one cache entry. The cache key was built
withJSON.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_imagesfor two different node sets both serialized to
{"fileKey":…,"nodes":[{}]}. WithFIGMA_CACHE_TTLset, the second call got
the first one's images.argsHashin 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 pastPLAYGUARD_HINT_THRESHOLDit 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, abrowser_take_screenshotcould be
answered with asection/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_navigatecould land after a mutating
tool had already invalidated the cache, writing pre-action DOM back with a
fresh timestamp for theUNCHANGEDshortcut and the screenshot redirect to
serve as current. Late prefetches are now discarded. - A
figmaNodeIdthat 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:getComputedStylereturns the logicalstart/endfor anything
without an explicittext-align, andtextAlignhas no CSS zero-default to
drop it from the auto-selected set.start/endnow normalize to
left/right(LTR). - A
browserSelectorcontaining 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 opaquebrowser 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=1sparsed toNaN, and
setTimeout(res, NaN)fires immediately whileattempts < NaNis 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
Fixed
- The upstream Playwright MCP failed to spawn whenever PlayGuard itself was not
the project root — undernpx, 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
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-madergb()strings now all go through one
normalizer before comparison, andborderColoris 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 ofstyles,
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). marginwas 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.stringifydoes not escape them, and they terminate a line in JS source.
Changed
src/index.tssplit intoconfig.ts,snapshot.ts,figma-optimize.ts,
anddesign-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-diris only injected when the
user has not set it themselves. - Dropped the manual
cmd /cwrapper around upstream spawns on Windows. The
MCP SDK already routes throughcross-spawn; the extra wrapper mangled
arguments containing spaces or non-ASCII characters, so any project path
with a space in it broke screenshot output.