Every one of the five repos ends its build guide with an appendix titled "the N things that actually broke." Not hypotheticals, not a troubleshooting section written from imagination β the actual failures, with the actual error text.
There are forty of them. This is all of them, in one place.
Click any card to see the cause, the fix, and the part that transfers.
The SDK API changed under userror TS2339: Property 'tool' does not exist on type 'McpServer' β on code copied from a current-looking tutorial.#1 the servertooling
What was actually wrong
server.registerTool(name, config, cb) rather than server.tool(name, desc, shape, cb), inputSchema: z.object({...}) rather than a raw shape, and @modelcontextprotocol/server rather than @modelcontextprotocol/sdk/server/mcp.js.The fix
cat node_modules/mcp-handler/dist/index.d.ts.π‘ The transferable bit
.d.ts files are the ground truth for the version you actually have, and they cannot be out of date. Five minutes reading them beats an hour of guessing β a habit that went on to change the design in all five projects.`406 Not Acceptable` on every requestThe endpoint exists, the build is clean, and every curl returns 406 with nothing useful in the body.#1 the serverprotocol
What was actually wrong
Accept: application/json, text/event-stream header. The spec requires the client to declare it can handle either reply format, and browsers and fetch don't add it for you.The fix
π‘ The transferable bit
The cookie jar forgetsAdd 20 cookies, look in the jar, see 12.#1 the serverpersistence
What was actually wrong
The fix
npx vercel install neon β it provisions from the Vercel Marketplace and doesn't ask for a card). Or, if the data is genuinely per-conversation, keep it client-side and pass it in as a tool argument.π‘ The transferable bit
Windows path and shell frictionerror 2147942593 (0x800700c1) launching npm.ps1; curl rejecting a JSON payload; endless LF will be replaced by CRLF warnings.#1 the servertooling
What was actually wrong
Start-Process can't invoke a .ps1 shim as an executable; cmd.exe doesn't handle single-quoted strings; and git normalises line endings.The fix
npx next start directly rather than through the npm shim. Use Git Bash or Invoke-RestMethod for JSON payloads. The CRLF warnings are harmless β silence them with git config core.autocrlf true.`create-next-app` refuses to share a folderThe directory contains files that could conflict β over a single unrelated .md file.#2 the hosttooling
What was actually wrong
create-next-app refuses to scaffold into a folder containing any file it doesn't recognise. Project #1's guide suggested --skip-install --disable-git in this situation; that advice is wrong for this version, because the check happens before either flag is considered.The fix
npx create-next-app@latest /tmp/scaffold β¦ then cp -r /tmp/scaffold/. ./.π‘ The transferable bit
The MCP package that isn't the one you installedYou install @modelcontextprotocol/sdk β what project #1 installed, and what every tutorial says β and mcp-handler still complains about an unmet peer dependency.#2 the hosttooling
What was actually wrong
mcp-handler peer-depends on `@modelcontextprotocol/server`, a different package. The first line of its own types says so.The fix
npm install @modelcontextprotocol/server.π‘ The transferable bit
The type files you can't findfind node_modules/@modelcontextprotocol/server -name "*.d.ts" returns nothing, on a package that obviously ships types.#2 the hosttooling
What was actually wrong
.d.mts / .d.cts, not .d.ts. Your find pattern was wrong, not the package.The fix
grep -rln "registerTool" node_modules/@modelcontextprotocol/server/dist/. Or check package.json's types / exports field, which tells you exactly which file to open.π‘ The transferable bit
`.env*` swallows `.env.example`You write a nice .env.example, commit, and it isn't in the repo.#2 the hosttooling
What was actually wrong
.gitignore contains .env*, which matches .env.example just as happily as .env.local.The fix
!.env.example β and keep that block last, because gitignore is order-sensitive and the last matching pattern wins.π‘ The transferable bit
The dev server that would not diePort 3000 is in use by process 42444, using available port 3001 instead. Your new server is on 3001 while everything you configured points at 3000 β where the old process, with the old environment, is still cheerfully answering.#2 the hosttooling
What was actually wrong
npx next dev does not necessarily kill the Node process it spawned. This is worse on Windows.The fix
Get-NetTCPConnection -LocalPort 3000 -State Listen | Select-Object OwningProcess, then taskkill /PID <pid> /F. On macOS or Linux, lsof -ti:3000 | xargs kill -9.π‘ The transferable bit
`.env.local` is read at startup, not per requestYou add ANTHROPIC_API_KEY to .env.local, hit the app, and get ANTHROPIC_API_KEY is not set β from a file that visibly contains it.#2 the hosttooling
What was actually wrong
The fix
- Environments: .env.local on startup. If that line is missing, the file wasn't loaded.π‘ The transferable bit
The host called itself through a locked doorEverything works locally. In production the app still answers β but one MCP server is missing from the trace with HTTP 401, while curling that same endpoint on the public URL returns 200 perfectly.#2 the hostdeployment
What was actually wrong
VERCEL_URL is the per-deployment hostname and Deployment Protection answers it with 401. VERCEL_PROJECT_PRODUCTION_URL is the stable alias and is not protected. Every tutorial tells you to use VERCEL_URL for the app's own URL, which is fine for a redirect target and wrong when the server must call itself.The fix
VERCEL_PROJECT_PRODUCTION_URL, falling back to VERCEL_URL.π‘ The transferable bit
Vercel un-ignores your `.env.example`, repeatedlyYou add !.env.example to .gitignore, and later it is ignored again anyway.#3 the gatedeployment
What was actually wrong
vercel link appends its own .env* line β and so does vercel install, and so does anything else that writes .env.local. Each one appends to the end, and gitignore is order-sensitive, so each one silently undoes your negation.The fix
.gitignore after every Vercel command that touches .env.local, and delete the duplicates.π‘ The transferable bit
`git check-ignore -v` lies to you about negationsgit check-ignore -v .env.example prints a matching line and exits 0 β which reads like yes, ignored, so you conclude the negation didn't work.#3 the gatetooling
What was actually wrong
The fix
git add -n .env.example. It prints add '.env.example' if it will be committed, or the following paths are ignored if it won't.π‘ The transferable bit
`tsc --noEmit` fails on a clean cloneCannot find name 'LayoutProps' β on code create-next-app itself generated.#3 the gatetooling
What was actually wrong
LayoutProps<"/"> is a generated type that Next writes into .next/types during a build. On a fresh clone that directory doesn't exist, so neither does the type. It works for anybody who has run npm run build first, which is why it survives into templates.The fix
{ children }: { children: React.ReactNode }.π‘ The transferable bit
The trace was empty, and replay looked brokenAfter several successful runs with approvals, npm run replay listed them all with 0 events. The whole replay feature appeared not to work.#3 the gatepersistence
What was actually wrong
recordEvent was called in the route handlers, and the terminal checkpoint script called the loop directly β so runs created by npm run approval had rows in runs and approvals but nothing in trace_events.The fix
π‘ The transferable bit
The dev server that would not diePort 3000 in use, new server on 3001, and JAR_MCP_URL still pointing at 3000 where the old process is answering with the old environment.#3 the gatetooling
What was actually wrong
The fix
π‘ The transferable bit
The seatbelt that unbuckled itself on every resumeNone. Nothing failed. That is what makes it worth listing.#3 the gatecost
What was actually wrong
MAX_ITERATIONS = 10 is a cost seatbelt, and /api/resume calls runAgentLoop fresh β so iteration starts at 0 again. A run that pauses for approval nine times would get nine separate budgets of ten iterations, and the cap you thought you had would be 90.The fix
runAgentLoop({ β¦, iterationOffset: run.iterations }).π‘ The transferable bit
The model asks for permission, and that is not a safety featureThe good kind. Asked to smash the jar, the model itself stopped and asked for confirmation instead of calling the tool β so the gate never fired and the checkpoint reported the loop finished without ever pausing.#3 the gatemeasurement
What was actually wrong
The fix
π‘ The transferable bit
Project #2's own server locked this project outThe plan was to connect this host to project #2's live /api/toolbox. It returned 401 for every request.#3 the gatedeployment
What was actually wrong
withMcpAuth and set MCP_SHARED_TOKEN in production. It works exactly as designed β this host just isn't holding the token.The fix
π‘ The transferable bit
Promise.allSettled in buildToolbox is what kept this a design decision instead of an outage β and project #4 responded by shipping its servers deliberately open.The premise was wrong, and the eval said soThe whole project is built on sixty jars is too much for one agent. The baseline run scored 100% β sixty jars inspected, nine tampered found, zero false positives, four iterations.#4 the crewmeasurement
What was actually wrong
inspect_jar calls per turn. It was never close to a context limit.The fix
π‘ The transferable bit
A backtick inside a template literal, in SQLERROR: Expected "]" but found "trace_events".#4 the crewtooling
What was actually wrong
-- Attribution lives in the \trace_events\ table β where the backtick ends the string.The fix
π‘ The transferable bit
trace_events, which is a perfectly good table name, and says nothing about backticks. When a parse error names a token that is obviously fine, look at the delimiters around it, not the token.A regex that assumed the data would never growAt 240 jars the eval reported that a worker assigned jars 91-120 had been given one jar, and one assigned jars 211-240 had been given fourteen. It looked like the orchestrator was delegating badly.#4 the crewmeasurement
What was actually wrong
parseJarIds used \d{1,2} β written when the pantry was fixed at sixty and two digits was obviously enough. On "jars 211-240" the engine matches 21, wants a dash, finds 1, slides along one character, matches 11, finds the dash, matches 24, and reports the range 11 to 24. No error anywhere.The fix
\b(\d{1,3})\b, validated against JAR_COUNT.π‘ The transferable bit
Killing the shell does not kill the process, part twoA 240-jar comparison came back scored 0% in both modes, 0 tokens, 0 jars inspected β which reads like a devastating finding about delegation and is actually nothing at all.#4 the crewmeasurement
What was actually wrong
compare run had exceeded a 10-minute command timeout and been killed. It kept running. A second was started, and the two ran concurrently until they exhausted the API credit balance.The fix
π‘ The transferable bit
stopped: error, and the verdict line dutifully printed ONE AGENT IS BETTER from two identically-broken runs. A red tick with no explanation is worse than no tick.`create table if not exists` will not add your new columnsNone on a fresh database. On an existing project #3 database, npm run db:init reports success and then every insert fails on a missing parent_run_id.#4 the crewpersistence
What was actually wrong
create table if not exists sees a table and does nothing. It does not diff columns.The fix
alter table β¦ add column if not exists.π‘ The transferable bit
The spawn cap that reset on every resumeNone. Nothing failed. That is what makes it worth listing.#4 the crewcost
What was actually wrong
MAX_SPAWNS is enforced by a counter in a closure created per run. A run that pauses for approval and resumes in a new HTTP request builds fresh crew tools with a counter starting at zero β so the cap was 8 per resume, not per run.The fix
createCrewTools(spawnOffset), threaded through exactly like iterationOffset.π‘ The transferable bit
A Next.js route module is not a place to keep helpers/api/resume importing stripState from ../chat/route. It typechecks, and it is a build error waiting for a bad day.#4 the crewtooling
What was actually wrong
The fix
lib/run-driver.ts.π‘ The transferable bit
Three places have to agree about the pantry sizeinspect_jar { id: 200 } rejected as out of range while the database happily holds 240 jars.#4 the crewtooling
What was actually wrong
JAR_COUNT. A dev server started before you exported PANTRY_JARS=240 is serving a 60-jar schema no matter what you reseeded.The fix
PANTRY_JARS on all three β db:init, dev, and the script.π‘ The transferable bit
The headline number compared two different amounts of workThe comparison reported the crew cost 1.87Γ what one agent cost. Plausible, quotable, and wrong.#4 the crewmeasurement
What was actually wrong
crew-one-approval is a delegation-only case, so a 3-attempt comparison is 3 runs in single mode and 6 in crew mode β and the script divided one total token count by the other. Part of what it measured was the crew did twice as many runs. Nothing errored. Both totals were correct. The division was the lie.The fix
π‘ The transferable bit
`git check-ignore -v` still lies about negationsExit 0 on .env.example, which reads like ignored and isn't.#4 the crewtooling
What was actually wrong
The fix
git add -n <file> asks what git will actually do.π‘ The transferable bit
The feature everyone documents does not work hereserver.server.createMessage() β the one-liner in every sampling tutorial β throws.#5 the ledgerprotocol
What was actually wrong
The fix
inputRequired(β¦), the client fulfils it, the client calls the tool again with params.inputResponses and params.requestState.π‘ The transferable bit
@deprecated next to the function every tutorial recommends. A deprecation notice on the thing you were about to build on is worth more than a working example of it.`LATEST_PROTOCOL_VERSION` is not the latest protocol versionSUPPORTED_PROTOCOL_VERSIONS doesn't contain 2026-07-28, so you conclude the runtime can't serve it and design around the legacy path.#5 the ledgerprotocol
What was actually wrong
_meta envelope, and lives in a completely separate code path.The fix
π‘ The transferable bit
LATEST_ answers a narrower question than its name implies. When types and constants disagree, the wire is the tiebreaker.Two readers, two shapes, and only one of them documented where I lookedThe handler returned input_required forever. The host answered, the retry arrived, and the handler asked again β an infinite loop that costs a model call per round.#5 the ledgerprotocol
What was actually wrong
acceptedContent(responses, key) is elicitation-only. For sampling it returns undefined, which is indistinguishable from no answer yet. The right reader is inputResponse(responses, key). And they aren't even the same shape: a sampling view wraps its payload in .result, while an elicitation view is flattened.The fix
inputResponse for sampling; destructure elicitation directly.π‘ The transferable bit
I built the replay runner first, and nothing was being recordedThe $0 replay suite was built first, exactly as recommended. Then the live suite ran β 13 cases, all green β and replay reported no stored run for this prompt for every single one.#5 the ledgerpersistence
What was actually wrong
observeOnce calls runAgentLoop directly, so it never went through lib/run-driver.ts, so it never wrote a row. This is project #3's gotcha 4 arriving for a third time: #3 hit it with a script and a route handler, #4 fixed it structurally with one drive-and-persist path β and then left the eval suite as a fifth caller that quietly didn't persist, because in project #4 nothing read those rows.The fix
persist through observeOnce, reusing TraceWriter rather than writing a second persistence path β which would have been the same mistake a fourth time.π‘ The transferable bit
Six cents for a blank pageThe expensive digest came back empty. 26,240 tokens, six cents, and a report with nothing in it β which the server dutifully wrapped in a nice header and returned as a successful tool result.#5 the ledgercost
What was actually wrong
thinking. On Sonnet 5 that does not mean off β adaptive thinking is on by default, and max_tokens caps thinking and answer together. The host's 1,000-token clamp was spent entirely on reasoning.The fix
thinking: { type: "disabled" }, plus an explicit check that empty output is an error rather than an answer.π‘ The transferable bit
max_tokens on somebody else's behalf has this bug available to it.The error path reported a real spend as freeNone, until it was read. When the empty-output check started throwing, the catch recorded actualCents: 0 β for a call that had genuinely burned 26,000 tokens.#5 the ledgercost
What was actually wrong
usage is populated before the check throws. The catch block was written for network failures, where nothing was spent, and inherited by a failure mode where plenty was.The fix
decision says refused; actual_cents tells the truth.π‘ The transferable bit
catch around a billable call needs to ask did this cost anything before it failed?A demo that could not demonstrate anythingThe gate worked, the refusal worked, and the headline demo β set the ceiling to two cents and watch it refuse β did nothing. Every request estimated at 0.22Β’, comfortably under 2Β’.#5 the ledgermeasurement
What was actually wrong
The fix
depth: "brief" | "deep" dial on the tool β same tool, same server, same user question, about 27Γ the price.π‘ The transferable bit
A passing suite that printed a crash after passingSCORE: 100% (1/1 scored) followed immediately by Assertion failed: !(handle->flags & UV_HANDLE_CLOSING).#5 the ledgertooling
What was actually wrong
process.exit() tears the process down while the Neon driver still holds open HTTP handles, and libuv aborts β after a perfectly good report.The fix
process.exitCode = β¦ and let the event loop drain.π‘ The transferable bit
A price table is a dated factNone yet, and that is the point.#5 the ledgercost
What was actually wrong
lib/pricing.ts is the only file in the repo whose correctness expires. Sonnet 5 is on an introductory rate that ends 2026-08-31; hard-code the discounted number and every downstream calculation reports to two decimal places, with total confidence, and is wrong from September.The fix
PRICES_CHECKED_ON, and print it in the UI and the checkpoint scripts. And the load-bearing half: the gate reasons in standard prices while the ledger records effective ones, because a ceiling computed with a temporary discount silently loosens the day the discount ends.π‘ The transferable bit
Project #4's gotcha 9 was still live in project #4's own scriptnpm run compare had never been run in this repo. Run once, it reported the crew cost 2.01Γ what one agent cost β at 60 jars delegation is pure overhead β a conclusion project #4's own corrected README already contradicts.#5 the ledgermeasurement
What was actually wrong
ONE AGENT scored 2 comparable cases; A CREW scored 4. The ratio divided one run's tokens by two runs' tokens and called the difference a price. Project #4 built the fix and never wired it up: usageByGroup was populated and stored on the results object, and the verdict read results[n].usage β the raw totals β ignoring it entirely.The fix
π‘ The transferable bit
A checkpoint whose second half tested nothingnpm run approval printed PASS β the gate fired and both paths worked, above output showing the DENY half had run against an empty jar and never paused.#5 the ledgermeasurement
What was actually wrong
The fix
π‘ The transferable bit
What the distribution says
A few things are worth noticing once they are all in one list.
Eight of the forty had no symptom at all. They are on the list because somebody went looking, not because anything broke. Both reset counters, the under-reported ledger row, the price table with an expiry date, the route module exporting a helper. A gotcha with no symptom is the most expensive kind, because the only thing standing between you and it is somebody deciding to check.
Several are the same bug, recurring. git check-ignore lying about
negations is in projects #3 and #4. The dev server that would not die is in #2,
#3 and #4. The counter that re-scopes itself is in #3 and #4. The code path
that skips the shared one is in #3, #4 and #5.
The categories shift as the series goes on. Projects #1 and #2 are almost
entirely tooling β scaffolding, packages, headers, ports. By project #5 the
list is protocol, cost and measurement. The problems stop being about
getting the thing to run and start being about whether what it reports is
true.
And the two most uncomfortable entries are both about a fix that wasn't one. Project #4's headline ratio, and project #5 discovering that fix had never been wired up. Both are on the list because the documentation of a problem got mistaken for the resolution of it.