MCP server

flawless-react mcp speaks the Model Context Protocol over stdio, so an MCP-capable agent (OpenCode, Claude Code, Codex, Cursor) generates a suite and reads the analyser directly — eight tools in two planes, the CLI's real exit codes, and a nonzero run that is always an error result, never a false pass.

Goal: let an AI agent maintain your test suite without a shell. flawless-react mcp is a Model Context Protocol server over stdio — JSON-RPC 2.0, one message per line. The agent’s host spawns it once, calls tools/list, and gets eight tools: generate a suite, read what the analyser saw in a component, ask what a project already has, and check the commands in its own documentation. No meta-tool wrapping a CLI; the agent sees flawless_run and calls it.

stdout belongs to the protocol exclusively. The CLI’s own console output is redirected to stderr before anything is served, and a generation runs in a child process whose output is captured per call — so nothing a run prints can land in the middle of a response frame.

Install & register

curl -fsSL https://raw.githubusercontent.com/lariocpt/flawless-react/main/install.sh | bash

Then register the server with a host that speaks MCP. The shape is the same across OpenCode, Claude Code, Codex and Cursor:

{ "mcpServers": { "flawless-react": { "command": "flawless-react", "args": ["mcp"] } } }

mcp is a subcommand, not a flag: it serves on stdin and stdout and takes no options — and it says so rather than ignoring them, because a host config carrying a typo’d flag would otherwise start a server that quietly did not honour it. flawless-react mcp --help prints the same contract, including the tool list, from the terminal.

Verify by hand

The transport is lines on a pipe, so a smoke test needs no host — initialize, then ask for the tool list:

printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}\n{"jsonrpc":"2.0","id":2,"method":"tools/list"}\n' | flawless-react mcp

One frame per line back. initialize echoes a protocol version from the set this server implements (2025-06-18, 2025-03-26, 2024-11-05); a version it does not speak is answered with the newest one it does rather than a promise it cannot keep. A tools/list sent before the handshake is refused with -32002, and a line that is not JSON is answered with -32700 — a client whose call died learns that it did.

Two planes, and why the difference matters

What persists between calls is the whole design question, and the tool names answer it.

The run plane spawns a flawless-react child per call (--ci, JSON on stdout). That is not ceremony: the generated suites are executed by a runner that inherits stdout, so an in-process run would write its own report into a JSON-RPC frame. A child also gets its own cwd and env per call, is killable as a whole process group at the timeout deadline, and returns the CLI’s real exit codes — the contract every other caller of this tool already depends on. Nothing is held between calls, so a server that has been up all day cannot leak one project’s state into the next.

The read plane runs in this process: it opens files, parses TypeScript, returns data. It spawns nothing and writes nothing, which is the point — an agent asking “what is this component” should not be able to change it. Read calls run with the working directory set to the call’s root and restored afterwards (calls are serialized, so this is deterministic), which is what makes a tool answer match the same glob typed in that project, default excludes included.

The eight tools

The run plane — two tools, and these are the ones that write.

ToolReturnsWrites
flawless_runthe child’s exit line plus the --ci blob: exitCode, outDir, results, keptScripts, optional testRun and errorsthe suite, and in framework mode the target’s package.json scripts
flawless_skillsskills list / skills install outputon install only: .agents/skills/<name>/SKILL.md plus the .claude/skills link

The read plane — six tools, none of which write anything.

ToolReturns
flawless_tasksevery task id, what it emits, whether it runs under Expo
flawless_referenceevery flag with its value form and summary, task ids, skill names, version
flawless_analyseprops and where each is used, hooks, state, fetches, render guards, unresolved types, the child tree
flawless_findingsthe static engine’s findings as file:line records, plus spec-quality gaps
flawless_project_statusframework detection, existing suite and reports, expected vs declared scripts, TODO(flawless) count, skill status
flawless_check_docs{checked, skipped, problems} for documented invocations in a text

Read it from the binary instead of trusting this table:

flawless-react mcp --help

Arguments worth knowing

  • root (read tools) / cwd (run tools): every path argument resolves against it, so one agent can drive two repositories without the second call reading the first one’s files.
  • tasks, nextjs, expo, force, run_tests, out, changed, roots, heaps, depth, props: the CLI’s own options, named the same way. Omit tasks and the run does perf-harness alone, which is the documented non-interactive default.
  • env: extra environment variables for that one call — FLAWLESS_E2E_BASEURL and FLAWLESS_E2E_ID for an e2e run, LOAD_BASE and LOAD_AUTH for a load pipe. Per-call only: nothing carries over, so pass them on every call.
  • max_entries and max_findings bound the read tools (a glob over an app is hundreds of files, each a TypeScript parse). Hitting a cap is reported as entries_skipped / a note, never as a silent cut.
  • Unknown arguments are rejected, not ignored. A caller that typos run_tests as runTests would otherwise get a green report for a run that never executed the suites.

A call, framed as the host sends it

The answer is the real output shape, trimmed to what an agent reads first; an untrimmed answer carries every field the analyser has, for every component the glob reached.

{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"flawless_analyse","arguments":{"root":"/srv/app","entry":"src/app/dashboard/page.tsx"}}}
{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{\n  \"root\": \"/srv/app\",\n  \"files_read\": 2,\n  \"components\": [\n    {\n      \"component\": \"DashboardPage\",\n      \"file\": \"src/app/dashboard/page.tsx\",\n      \"export_kind\": \"default\",\n      \"render_mode\": \"rsc\",\n      \"is_async\": true,\n      \"prop_names\": [],\n      \"prop_usage\": [],\n      \"hooks\": [],\n      \"state\": [],\n      \"fetches\": [\n        {\n          \"method\": \"GET\",\n          \"url\": \"/api/metrics\",\n          \"in_effect\": false,\n          \"line\": 14\n        }\n      ],\n      \"unresolved_props\": [],\n      \"children\": [\n        {\n          \"component\": \"MetricCard\",\n          \"file\": \"src/components/MetricCard.tsx\",\n          \"render_mode\": \"client\",\n          \"props\": 3\n        }\n      ],\n      \"descendants\": 1\n    }\n  ],\n  \"errors\": []\n}"}]}}

flawless_run answers in text rather than one JSON document: the child’s exit line first (exit 0, or the timeout notice when its process group was killed), then the parsed --ci blob under a result (--ci --json) marker, then the child’s own diagnostics under a stderr marker. When the CLI died before printing a blob, the raw stream is there in its place — not an empty success — and an output the cap cut says so with the real totals.

Failures are failures

A nonzero exit from the child is an isError result carrying that child’s own output. flawless_check_docs returns isError when a documented invocation does not parse — a broken example is a broken example, even in prose. Two ways the server stops, and they are deliberately different: a signal kills the live child’s process group before the server exits, so a server taken down never leaves a generation holding ports; closing stdin does not, because printf … | flawless-react mcp closes the pipe as it writes the batch and the work that batch asked for still finishes under its own watchdog. Cancel with a signal, not by hanging up.

The exit contract clients may rely on, each kept honest by a permanent test:

  • The process exits when stdin closes and the batch has drained — measured 2–42 ms from the last answer frame to exit, for an idle server and one that just finished a flawless_run.
  • A signal always ends the process. Live children are killed first; the in-flight call gets at most 5 s; an abandoned call is named on stderr.
  • A kept-open stdin is an open session. The server answers calls until you close stdin or signal it — it does not exit after a response, because MCP is a session and the next call is normal.
  • A call never outlives its watchdog by more than 4 s. If the generation’s direct child exits but a straggler in its process group still holds the output pipe open, the group is killed and the result says the output may be cut short — a call that never answers is worse than an answer that names its own gap.

Three things this surface deliberately does not claim:

  • flawless_findings is the static engine only. The a11y task needs a real browser (axe) and react-rules needs Biome over the project; neither runs in a read call. Zero findings is not an accessible, lint-clean component.
  • A run that executes suites needs their prerequisites. run_tests with the e2e task needs a dev server; the load pipes need a seeded database. Each fails with its own named prerequisite rather than reporting a pass.
  • force regenerates a suite wholesale. Hand-written assertions that live only in generated files are gone. Call flawless_project_status first and read its inventory.todo_markers.

Next

  • Agent skills — the same capabilities as instructions an agent reads instead of tools it calls. Use both: the skills decide what to run, the MCP tools run it.
  • CLI reference — every flag flawless_run mirrors.
  • Safe regeneration — what force does to an adopted suite, and how to avoid losing hand-written assertions.