Skip to main content

fraser-gate — the Experience Merge-Gate for AI coding agents

PyPI

Point FRASER at a build and it generates ICP personas from your product, drives a real browser as each one, and hands back grounded, machine-readable usability findings — what real users felt, where they hesitated, misread, or lost trust — then re-runs the identical cast to gate your fix pass | fail. FRASER is the loop's brain and eyes: it finds where real users break and verifies the fix helped. It never writes or merges your code; your agent does that — after validating each finding.

Why this exists: a build can pass its own typecheck + unit tests and still have a user drop off. That gap — experience ≠ correctness — is what the gate catches.

This package is a thin, dependency-light client (an MCP server + a stdlib CLI) that talks to the hosted FRASER API. The real work runs on FRASER's servers.

Setup (one paste)

Add this to your agent's .mcp.json (Claude Code, Cursor, Windsurf, Copilot, …). uvx fetches and runs fraser-gate on demand — nothing to install first:

{
  "mcpServers": {
    "fraser": {
      "command": "uvx",
      "args": ["--from", "fraser-gate@latest", "fraser-gate-mcp"],
      "env": {
        "FRASER_API_KEY": "<your token — see below>",
        "FRASER_API_URL": "https://fraser.lythe.ai"
      }
    }
  }
}

The @latest makes uvx re-resolve to the newest published version each launch, so you auto-update — no reinstall when we ship a fix. (Drop @latest to pin to whatever uvx first cached; for a reproducible CI run, pin an exact version like fraser-gate==0.4.0.) If you're on an older config, the server tells you when an update is available.

Don't have uvx? It ships with uv (curl -LsSf https://astral.sh/uv/install.sh | sh, or pip install uv). Prefer pip? pipx install fraser-gate (or uv tool install fraser-gate) puts fraser-gate-mcp and fraser-gate on your PATH; then set "command": "fraser-gate-mcp" with no args (update those with pipx upgrade fraser-gate / uv tool upgrade fraser-gate).

Get your token

Sign up at fraser.lythe.ai, then mint a long-lived MCP token from the API (two calls — works anywhere, no dashboard needed):

# 1) log in → a short-lived session token   (or POST /api/auth/signup to make an account)
SESSION=$(curl -s -X POST https://fraser.lythe.ai/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com","password":"your-password"}' \
  | python -c "import sys,json;print(json.load(sys.stdin)['token'])")

# 2) exchange it for the long-lived MCP token you paste into .mcp.json
curl -s -X POST https://fraser.lythe.ai/api/auth/mcp-token -H "Authorization: Bearer $SESSION"
# → {"token":"eyJ…  ← this is FRASER_API_KEY", "base_url":"https://fraser.lythe.ai"}

Signed in with Google (no password)? Grab the session token from your browser after logging in (it's the bearer token the web app sends), then run step 2. The minted token is a normal session JWT with a long TTL — revoke it by rotating your account, same as any session.

The loop

your agent builds + deploys ─▶ fraser_start_review(target_url)   → {run_id, project_id, flow_id}
                                     │  (async, minutes — poll)
                              fraser_get_status(run_id) ─▶ complete
                              fraser_get_findings(run_id)
                                     │  → findings[] {persona, sentiment, what_they_wish, evidence, screenshot, finding_id} + guidance + fix_prompt
   your agent validates + fixes ◀────┘   (FRASER never touches code)
                                     ▼
                              fraser_validate_fix(project_id, flow_id, baseline_run_id, new_url)
                              fraser_get_validation(run_id, baseline_run_id)
                                     │  → {gate: pass|fail, per-finding {fixed|still_present|regressed}, new_findings}
                              gate=fail ⇒ don't merge · gate=pass ⇒ ship

Each finding carries a stable finding_id (content-addressed, survives re-wording) so the gate maps fix→verify on the same defect across runs — no false "fixed" when the model just renames a bug.

The findings envelope also carries an experience funnel — the per-ICP verdict: {goal, tested, funnel{achieved, partial, blocked, gave_up, completion_rate}, cast[per persona: segment, reached_goal, depth, emotion, takeaway], drop_off[who missed the goal and where]}. Prioritize by it: fix the biggest drop-off first, not the loudest finding. (Real run vs. linear.app: 5 ICPs, 40% reached the goal, 3 dropped — 2 of them at /signup, matching a high-severity signup finding.)

Run it end-to-end (autonomous)

Every tool response carries a next_step string, so an agent drives the whole loop without a playbook:

  1. fraser_start_review(target_url) — no credentials needed up front: if the app is gated, FRASER gives each tester a disposable inbox and signs up on its own (the image-captcha + emailed code are handled for it).
  2. Poll fraser_get_status until terminal. If it returns a blocked object, FRASER couldn't get in on its own — blocked.ask says what's needed; ask the user for a test login and re-run fraser_start_review with auth_notes (or, for a bot_wall, retry with bypass_bot_protection).
  3. fraser_get_findings — validate each finding against the product yourself, make the smallest change that resolves it, and redeploy the preview.
  4. fraser_validate_fix → fraser_get_validation — re-runs the identical cast and gates the fix:
    • gate: pass ⇒ ship it — commit and push to main, or open a PR to main, per your repo's usual flow.
    • gate: fail ⇒ don't merge; fix the still_present/regressed findings and validate again.

Tools (8)

Tool Use
fraser_start_review(target_url, mode?, objective?, steps?, success_criteria?, personas?, context?, num_testers?, auth_notes?) Start a usability review. Async — returns a run_id. Steer the flow (objective/steps) or the cast (free-text personas).
fraser_start_ios_review(ipa_path, objective?, context?, num_testers?, auth_notes?) Test an iOS build on a real iPhone (cloud device lab). Pass the local .ipa (device arm64 build); same status/findings loop, but slower (~15-40 min).
fraser_get_status(run_id) Poll until terminal (complete/failed/timed_out). Runs take minutes (iOS: tens of minutes).
fraser_get_findings(run_id) Persona-grounded usability findings + the per-ICP experience funnel (goal-completion + drop-off) + validate-first guidance + fix_prompt. Validate each before changing code; fix the biggest drop-off first.
fraser_validate_fix(project_id, flow_id, baseline_run_id, target_url) Re-run the identical cast on the fix. Async.
fraser_get_validation(run_id, baseline_run_id) The pass/fail gate verdict.
fraser_get_report(run_id, save_path?) Download the client-ready PDF report (the enterprise customer-journey document) for a complete run — the human-facing deliverable.
fraser_cancel(run_id) Stop a run.

It takes minutes. FRASER generates personas from your app, then drives a real browser as each one. Start the review, do other work, and poll fraser_get_status every few seconds.

Knobs (defaults are good — omit everything and you get the grounded ICP cast on open exploration):

  • mode — visual (persona usability review, default — this is the product) or battle (adversarial defect sweep).
  • personas — free-text descriptions to test AS specific people (["a 55-year-old first-time user on an old Android", "a skeptical enterprise buyer"]); omit for the grounded ICP cast.
  • objective / steps / success_criteria — pin a specific flow instead of open exploration.
  • context — one line on what the product is (anchors personas for a new/thin app).
  • auth_notes — app login creds if the core experience is behind a login. Ask the user for a test account and pass them here; secret, never echoed or logged.
  • num_testers — 0 = the full grounded cast.

localhost just works

Point a review at http://localhost:3000 and the MCP opens an ephemeral public tunnel automatically (via cloudflared, downloaded once) so the hosted engine can reach your machine. You set nothing up; the tunnel is verified reachable before the run starts (cloudflared advertises the URL a few seconds before it actually routes — we wait), and it closes when the run finishes.

One requirement — serve the app single-origin. The tunnel exposes exactly the one port you point at. If your page makes browser-side calls to a second localhost port (a separate frontend/API split, common with docker-compose), those calls break through the tunnel — the remote browser resolves that port on its machine, not yours. Put everything behind one port (a dev proxy so /api is same-origin) or hand FRASER a deployed preview URL. A run that couldn't reach the target comes back blocked with reason: "target_unreachable" (not a credentials problem — see below).

CLI (same loop, for CI without MCP)

The fraser CLI is stdlib-only — a CI job needs nothing but Python and this package:

export FRASER_API_KEY=<token> FRASER_API_URL=https://fraser.lythe.ai
fraser review https://myapp-pr42.vercel.app --wait          # prints findings JSON
fraser validate <project_id> <flow_id> <baseline_run_id> <new_url> --wait   # exits non-zero on a FAIL gate

fraser validate --wait exits 3 on a fail gate, so a CI step can block the merge on it directly.

Local development of this package

git clone <repo> && cd gate_client
pip install -e .
python -m fraser_gate.mcp_server     # stdio MCP server
python -m fraser_gate.cli --help     # the CLI

Publishing a new version: see PUBLISHING.md.


MIT licensed. FRASER is a product of Lythe.

Release files for fraser-gate 0.8.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fraser-gate 0.8.0
File Size Uploaded
fraser_gate-0.8.0.tar.gz 20.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for fraser-gate 0.8.0
File Interpreter ABI Platform
fraser_gate-0.8.0-py3-none-any.whl Python 3 none any Details

Total release size: 43.8 kB

Release files / fraser_gate-0.8.0.tar.gz

Download URL fraser_gate-0.8.0.tar.gz
Size 20.2 kB
Tags Source
SHA-256 checksum
How to use checksums
f1ea91f4a210e632d205cb2857dfbe4468b97008be790f2274c63d194bacd3e0
BLAKE2b-256 checksum
How to use checksums
625994e12c3f23f70b090e02d27d3bb7b14d26eeea42a90cb7cc2d6ba3f98143
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.13

Release files / fraser_gate-0.8.0-py3-none-any.whl

Download URL fraser_gate-0.8.0-py3-none-any.whl
Size 23.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6d15b694ef0c6dde048c984d8411418ed0d30f809c584f1770d8a151c5eb9b78
BLAKE2b-256 checksum
How to use checksums
c6e36b069991bd1d645005d2a464144d051307eaefa23ee55632f548eb96a1b3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.13

Release history Release notifications | RSS feed

0.11.1

2 release files

0.11.0

2 release files

0.10.2

2 release files

0.10.1

2 release files

0.10.0

2 release files

0.9.0

2 release files

This release

0.8.0 This release

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page