Metadata-Version: 2.4
Name: fraser-gate
Version: 0.2.0
Summary: MCP server + CLI that lets an AI coding agent run FRASER's persona-based usability review against a build, read grounded experience findings, and gate the fix pass/fail
Project-URL: Homepage, https://fraser.lythe.ai
Author-email: Lythe <team@lythe.ai>
License-Expression: MIT
License-File: LICENSE
Keywords: ai-agents,code-review,coding-agent,experience,fraser,mcp,merge-gate,model-context-protocol,qa,testing
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: mcp>=1.2.0
Description-Content-Type: text/markdown

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

[![PyPI](https://img.shields.io/pypi/v/fraser-gate.svg)](https://pypi.org/project/fraser-gate/)

Point [FRASER](https://fraser.lythe.ai) 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:

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

Don't have `uvx`? It ships with [uv](https://docs.astral.sh/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`.

### Get your token

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

```bash
# 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.

## Tools (6)

| 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_get_status(run_id)` | Poll until terminal (`complete`/`failed`/`timed_out`). Runs take **minutes**. |
| `fraser_get_findings(run_id)` | Persona-grounded usability findings + validate-first `guidance` + `fix_prompt`. **Validate each before changing code.** |
| `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_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](https://github.com/cloudflare/cloudflared), downloaded once) so the hosted engine
can reach your machine. You set nothing up; the tunnel closes when the run finishes.

## CLI (same loop, for CI without MCP)

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

```bash
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

```bash
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](PUBLISHING.md).

---

MIT licensed. FRASER is a product of [Lythe](https://fraser.lythe.ai).
