This project extends project #4 rather than
starting fresh. You copy its lib/ wholesale and teach its hand-written MCP
client the other half of the protocol. If you haven't built #4, build that
first โ the loop, the crew, the gate and the notebook are all load-bearing
here.
Five minutes that save you a day
Before designing anything, find out what you can actually reach.
bashfor u in "https://learn-mcp-agent-crew.vercel.app/api/pantry" \ "https://learn-mcp-5-year-old.vercel.app/api/mcp" \ "https://learn-mcp-agent-guard.vercel.app/api/jar"; do echo -n "$u -> " curl -s -o /dev/null -w "%{http_code}\n" -X POST "$u" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}' doneCheckpoint
And this time the news is good:
terminalhttps://learn-mcp-agent-crew.vercel.app/api/pantry -> 200 open https://learn-mcp-5-year-old.vercel.app/api/mcp -> 200 open https://learn-mcp-agent-guard.vercel.app/api/jar -> 401 locked
Projects #2 and #3 locked their servers and it broke the next project both times. Project #4 shipped its servers deliberately open, and the decision paid off here. Ship yours open too.
Scaffold, and inherit
create-next-apprefuses to scaffold into a folder containing any unrecognised file โ and yours already has the kickoff document in it.bashnpx create-next-app@latest /tmp/scaffold --typescript --app --tailwind \ --no-src-dir --use-npm --turbopack --eslint --skip-install rm -rf /tmp/scaffold/.git cp -r /tmp/scaffold/. ./ git clone --depth 1 https://github.com/ketankshukla/learn-mcp-agent-crew.git /tmp/p4 cp -r /tmp/p4/lib /tmp/p4/scripts . cp -r /tmp/p4/app/api app/Checkpoint
npx tsc --noEmitexits 0 on a clean clone.Verify the APIs from the types on disk
This is the stage that matters. It changed the design in all five projects, and this time it changed it most โ the whole of The finding is this stage.
Short version: the types say
createMessage()exists, everything is marked@deprecated,LATEST_PROTOCOL_VERSIONdoes not contain the latest protocol version, and the only thing that settles it is writing a throwaway server and curling it.Checkpoint
The full round trip returns
"resultType":"complete".The $0 eval runner, FIRST
Project #4's
NEXT_STEP.mdsaid to build this before anything else. Do that โ see The suite that costs nothing.Checkpoint
On a fresh database it is supposed to say this:
terminalNOTHING TO SCORE. No stored run matched any case's prompt. This is not a failure โ it means the tapes don't exist yet.
The ledger schema
Three tables and one column โ
ledger_entries,server_ceilings,spend_grants, andruns.mode. The shapes and the two decisions worth defending are on One wallet, many spenders.bashnpx vercel install neon && npx vercel env pull .env.local npm run db:initCheckpoint
14 tables listed, including
ledger_entries,server_ceilingsandspend_grants.The read-only half
resources/list,resources/read,prompts/list,prompts/get. All four are ordinary request/response โ the same shape astools/call.bashnpm run readonlyCheckpoint
Both servers report an era, and the era column is load-bearing: a server on the legacy era cannot ask your host to think.
The server that asks
Read the tool handler as two passes of the same function, because that is what multi-round-trip means:
tsasync ({ focus, depth }, ctx) => { const view = inputResponse(ctx?.mcpReq?.inputResponses, "summary"); const answer = view.kind === "sampling" ? view.result : undefined; if (answer) return say(formatDigest(answer)); // PASS 2: the host answered return inputRequired({ // PASS 1: ask requestState: await stateCodec.mint({ }), inputRequests: { summary: inputRequired.createMessage({ }) }, }); }Two details that are cheap to add and expensive to skip: sign the
requestState, and give the tool a cost dial. Withoutdepth: "brief" | "deep"you cannot demonstrate a gate that reasons about amount, because everything the server asks for costs the same.Checkpoint
npm run samplingreturns a digest attributed to your host's model.The spend gate
lib/spend-gate.tsislib/approval.ts's sibling and deliberately the same shape. The two differences carry the lesson โ see A budget is not a gate.bashnpm run ledger -- --ceiling kitchen 2 npm run sampling -- --deepCheckpoint
The whole project in three lines:
terminal๐ The Kitchen โ summarise_week [claude-sonnet-5] 6.0ยข estimated โ REFUSED, $0.00 spent tokens on the tab : 0 refused (not spent) : 6.0ยข โ what the ceiling saved you
Wire it into the loop
The handler is built inside the tool-running block of
runAgentLoop, and that placement is not arbitrary โ it needsemit. A server's sampling request happens inside a tool call, and project #4'screateEventQueueis the only thing that can get an event out of a callback and into an async generator'syield.Those twenty lines earn their keep a second time, for a completely unrelated feature.
Checkpoint
npm run evals -- --case gate-refuses-expensive --attempts 1passes โ so the gate fires through the agent loop, not just in the direct script.The UI, and the dial
Two decisions. Two numbers, not one โ "spent" and "refused by the ceiling", side by side, because the second is what the gate is earning and it is invisible if you only render the first.
And the dial is real: you can set a ceiling and grant an allowance from the page. A demo that needs a redeploy to change a number is a screenshot.
Verify and ship
bashnpx tsc --noEmit && npx eslint . && npm run build git add -n .env.example # check-ignore lies about negations; this doesn't gh repo create learn-mcp-agent-ledger --public --source=. --remote=origin --push
The demo, in three commands
npm run ledger -- --ceiling kitchen 2 # two cents per request
npm run sampling -- --deep # ๐ your host tells your own server no
npm run ledger -- --grant kitchen 8 # a human says "yes, this once"
npm run sampling -- --deep # โฆand now it goes throughWhat the whole project cost
The first four checkpoints cost nothing:
npm run evals:replay # THE $0 SUITE โ scores runs you already paid for
npm run readonly # resources + prompts, no model calls
npm run ledger # who spent what
npm run mcp:list # transport โ 4 servers, no AI
npm run sampling # A SERVER ASKS YOU TO THINK โ about 0.1ยข
npm run sampling -- --deep # โฆand gets REFUSED, for $0.00