The full walkthrough is
BUILD_FROM_SCRATCH.md.
This is the shape, and the seven things that broke.
Prerequisites, scaffold and install
An
ANTHROPIC_API_KEYthis time β project #2 is the first one that costs money to run.Same scaffolding gotcha as project #1:
create-next-apprefuses a non-empty folder. Temp dir, then copy in.Read the types before you write any code
The habit that changed the design in all five projects.
Checkpoint
You can state, from memory, the exact signature of
registerTooland whether your model acceptstemperature.The MCP client
About two hundred lines of
fetchβ see Writing the client by hand.Checkpoint
Run it against the live project #1 server, not a local one:
bashnpm run mcp:listSchema translation
inputSchemaβinput_schema, strip$schema, substitute an empty object for a missing one.Checkpoint
bashnpm run mcp:translateThe agent loop (non-streaming)
Get it right before making it pretty.
Checkpoint
The demo, in a terminal, before any React exists:
bashnpm run agentTwo tool calls across two iterations, with the second one's
countmatching the first one's total.The route handler and the chat UI
The key is read in exactly one place. The browser sends plain text and receives an NDJSON stream of loop events.
Checkpoint
bashcurl -N -X POST http://localhost:3000/api/chat \ -H "Content-Type: application/json" \ -d '{"messages":[{"role":"user","content":"roll one d6"}]}'The second MCP server
In this repo, at
/api/toolboxβ and with a deliberatesecret_codecollision, so namespacing has something to prove itself against.Auth on your own server
withMcpAuthand a shared token.Checkpoint
With
MCP_SHARED_TOKENset and the dev server restarted: no token gives 401, the right token gives 200.Typecheck, build, GitHub, deploy
bashnpx tsc --noEmit && npx eslint . && npm run buildCheckpoint
Verify against production, not localhost.
The gotcha worth the whole page
The reason this one is nasty: it works perfectly in development, works perfectly in preview if protection happens to be off, and fails only in the configuration you ship. And the symptom β "my MCP server is down" β points at the server, which is fine.
The other six
| # | What broke | Where it's covered |
|---|---|---|
| 1 | create-next-app refuses to share a folder | Build it, project #1 |
| 2 | The MCP package that isn't the one you installed | Build it, project #1 |
| 3 | The type files you can't find | the compendium |
| 4 | .env* swallows .env.example | below |
| 5 | The dev server that would not die | below |
| 6 | .env.local is read at startup, not per request | below |
| 7 | The host called itself through a locked door | above |
.env* swallows .env.example. The Next.js .gitignore ships with
.env*, which also matches the example file you actually want committed. Add
!.env.example and keep that block last, because a later pattern wins.
The dev server that would not die. Killing the shell that started it does not kill it. Check the port, not the shell:
Get-NetTCPConnection -LocalPort 3000 -State ListenThis one bit project #3 and project #4 as well. Three projects, same surprise β which is a good sign the mental model is wrong rather than the command.
.env.local is read at startup, not per request. Add a variable, and the
running dev server does not know about it. Restart, then re-test β otherwise
you will "fix" a working config three times.