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.
| Tool | Returns | Writes |
|---|---|---|
flawless_run | the child’s exit line plus the --ci blob: exitCode, outDir, results, keptScripts, optional testRun and errors | the suite, and in framework mode the target’s package.json scripts |
flawless_skills | skills list / skills install output | on install only: .agents/skills/<name>/SKILL.md plus the .claude/skills link |
The read plane — six tools, none of which write anything.
| Tool | Returns |
|---|---|
flawless_tasks | every task id, what it emits, whether it runs under Expo |
flawless_reference | every flag with its value form and summary, task ids, skill names, version |
flawless_analyse | props and where each is used, hooks, state, fetches, render guards, unresolved types, the child tree |
flawless_findings | the static engine’s findings as file:line records, plus spec-quality gaps |
flawless_project_status | framework 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. Omittasksand the run doesperf-harnessalone, which is the documented non-interactive default.env: extra environment variables for that one call —FLAWLESS_E2E_BASEURLandFLAWLESS_E2E_IDfor an e2e run,LOAD_BASEandLOAD_AUTHfor a load pipe. Per-call only: nothing carries over, so pass them on every call.max_entriesandmax_findingsbound the read tools (a glob over an app is hundreds of files, each a TypeScript parse). Hitting a cap is reported asentries_skipped/ anote, never as a silent cut.- Unknown arguments are rejected, not ignored. A caller that typos
run_testsasrunTestswould 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_findingsis the static engine only. Thea11ytask needs a real browser (axe) andreact-rulesneeds 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_testswith thee2etask needs a dev server; the load pipes need a seeded database. Each fails with its own named prerequisite rather than reporting a pass. forceregenerates a suite wholesale. Hand-written assertions that live only in generated files are gone. Callflawless_project_statusfirst and read itsinventory.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_runmirrors. - Safe regeneration — what
forcedoes to an adopted suite, and how to avoid losing hand-written assertions.