Why a coding agent misses race conditions
A coding agent such as Claude Code or Cursor works from text: your source files, your tests and what the terminal prints. A race condition is not in that text. The code for a search box reads the same whether the response for the newest query arrives first or last. What differs is the order in which responses arrived at runtime, and the agent never saw it.
The bug also needs a particular timing. On your machine the backend answers fast, so responses tend to come back in the order they were sent. The agent runs the app, types a query, sees the right results and tells you it works. That is true and useless. The failure needs an older request to be slower than a newer one.
So the agent guesses. It adds a debounce or a retry, the tests still pass, and the bug is still there. The guide to race conditions in React covers the fixes in code. This guide is about the evidence the agent needs to pick one and check it.
What an MCP server adds
MCP, the Model Context Protocol, is a standard way to give an agent tools. An MCP server is a small program that the agent's client starts. It lists tools, the agent calls one, and the answer comes back as text the agent can read. For debugging, the agent can then ask for runtime facts, not only read code: which requests went out, which responses came back, in what order, and what each one changed. With those facts it can check a theory before it edits anything.
The loop, step by step
@genclass/mcp is an MCP server for Claude Code and other coding agents. It connects to the pages of your app running on your machine. It gives the agent every click, request and response, the order the responses were delivered, and what each response wrote into the app's state. The agent then works in six steps:
- See what happened.
genclass_statusshows the connected pages and the counts of findings.genclass_findingslists the findings, ranked and deduplicated: races, stale writes, errors and duplicate requests among them. - Explain one finding.
genclass_explaingives one finding in causal order, with the recorded steps, the replay and a Playwright regression test. - Replay it.
genclass_replaywitharmdelivers the exact response order that produced the bug again.resultsays whether it reproduced. - Fix the cause. The agent edits the code that let the older response win.
- Replay again. The same order is delivered to the fixed code. The result should now be: not reproduced.
- Keep the regression test. The test from
genclass_explainfails while the bug is there, so it belongs in your suite.
If the bug has not shown up on your machine yet, there is no finding to replay. genclass_explore (start, report, stop) runs your steps again with the network answering in different orders, and slower if you ask. It reports what ends differently from the run where responses came back in order.
A worked example: the search box
A search box sends a request as the user types. The user types "reac", then finishes the word. The response for "reac" is slow and lands after the newer one, so the list shows results for a query the user has already left. It is the bug described in out-of-order responses in search and autocomplete.
With the server connected, a session has this shape:
you: the search results are sometimes for an older query; find out why and fix it
agent: genclass_findings → f1 [stale-write] /search: the response to GET /api/search "?q=reac"
overwrote "search.results" after a newer GET /api/search had already written it
genclass_explain f1 → steps, delivery order, a replay, a regression test
genclass_replay arm f1 → (loads the page, types) → result: REPRODUCED
(fixes the handler to keep only the latest answer, adds the test)
genclass_replay arm f1 → result: not reproduced
genclass_explore start {slow_ms: 600} → 9 runs → report: no race
The finding names the request, the state it overwrote and the newer write it landed after. The agent could not get that from the code. The fix is then small: keep only the latest answer.
let latest = 0;
async function search(q) {
const id = ++latest;
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`);
const data = await res.json();
if (id === latest) setResults(data); // an older response can no longer overwrite a newer one
}
The second replay delivers the same order to the fixed handler and reports not reproduced. The explore run then tries other orders.
Setup
The shortest path is one command, run in your app's folder:
npx genclass
npx genclass walks you through setup, one Enter per step. Its Coding agent step writes the MCP config, .mcp.json (or .cursor/mcp.json), and the genclass skill, .claude/skills/genclass/SKILL.md. The skill is the procedure the agent follows. The command installs the runtime and MCP server versions it was built with.
Then connect your app's development build, once:
import { GenClass } from "@genclass/runtime";
import { connectAgent } from "@genclass/runtime/agent";
// Next.js: process.env.NODE_ENV === "development"
if (import.meta.env.DEV) {
const rt = GenClass.init();
connectAgent(rt);
}
The two lines inside the check start the runtime and connect it to the server. A production build drops all of this. The agent link needs @genclass/runtime 0.3.0-beta.1 or later. Load a page of your dev server and ask the agent for genclass_status. The agent can also call genclass_setup, which reads your package.json and shows the lines for your framework.
The server runs on your machine, and by default findings stay there. Its bridge listens on 127.0.0.1 only. The MCP server's docs list every tool, the install lines for each client and the exceptions to that default.
What it does not do
- It does not fix the bug for you. The tools report, explain and replay. They do not edit code. The agent writes the fix, and a fix written without a reproduction is still a guess.
- It works on the development build. The findings come from pages of your app running on your machine, not from your live site.
- A replay that does not reproduce is not proof of absence. It shows that one recorded order no longer ends wrong. Other orders are a job for
genclass_explore, and a clean report covers only the steps it ran. A report that compared nothing saysNO VERDICT, not "no race".
Related: race conditions in React data fetching and out-of-order responses in search and autocomplete.