learn-mcp-agent-ledger
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@learn-mcp-agent-ledgerSummarise this week's spending using a model, respecting my budget"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
πΈ The Host That Owns The Wallet
Five projects into a series named after the protocol, every MCP server we built was a vending machine. You put a request in, a result comes out, and it costs you exactly what you put in.
This project uses the capability that turns that inside out. Sampling is the server asking your host to run a model call. It has no API key. It has no model. It has yours.
π΄ Live app: https://learn-mcp-agent-ledger.vercel.app
π Live MCP endpoints: /api/kitchen (the one that asks), /api/pantry, /api/jar β all open, deliberately
πͺ #1 the server Β· #2 the loop Β· #3 the gate Β· #4 the crew
The whole idea is two files: lib/spend-gate.ts decides, lib/sampling.ts does.
π¨ Want to build it yourself, from an empty folder? BUILD_FROM_SCRATCH.md is the developer walkthrough β every command, a checkpoint proving each stage, and an appendix of the eleven things that actually broke while this was being built. This README teaches you what sampling is and why it needs a gate; that one teaches you how to build it.
The headline, before anything else
Your own server asked your own host to spend your money, and your host said no:
π 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 youmodel calls servers asked for | 13 |
allowed | 8 Β· 13.0Β’ actually spent |
refused | 5 Β· 24.2Β’ estimated, $0.00 actually spent |
regression suite | 100% β project #3's six, #4's four, #5's three |
cost of re-running that suite | $0.00, from stored traces |
Two things worth saying immediately.
A refusal costs nothing. The gate reasons about an estimate, before the call, so refusing spends no tokens at all. Project #3's gate saved you from a destroyed cookie jar; this one saves you from the bill itself. That is not a nice detail β it is the argument for gating on price rather than on which tool was called.
And the naive implementation of this feature does not work. Every tutorial describes sampling as the server pushing a request down an open connection. On a serverless host there is no connection, and the SDK says so in as many words. What actually works is a retry. That finding, and how we got it, is part 2.
Related MCP server: sampling-mcp
Table of contents
Part | What it covers |
Four capabilities we never used | |
The arrow that points back | |
A budget is not a gate | |
The gate that cannot pause | |
One wallet, many spenders | |
The regression suite that costs nothing | |
Data does not stay yours | |
Run it yourself | |
Where to go next |
Part 1 β Four capabilities we never used
Here is the whole menu, and how much of it four projects had touched:
Capability | What it is | Used in #1β#4 |
tools | the server does something | β all four projects |
resources | the server offers something to read | β never |
prompts | the server offers a recipe for asking | β never |
sampling | the server asks your host to run a model call | β never |
elicitation | the server asks your user a question | β never |
The honest summary of the first three missing ones: they are the same shape as the one you already know. You ask, the server answers. resources/read is tools/call with a URI instead of arguments. prompts/get returns some messages. Both were an afternoon.
There is one distinction inside that afternoon which is worth keeping, because it is easy to blur:
A tool is something the model decides to invoke. A resource is something the host decides to show it.
Reading a resource is not an action the agent takes; it is context somebody put in front of it. That makes resources the boring capability β and the one you would reach for most often in anything real, because most of what an agent needs is not an action.
The same care applies to prompts, and it is a security point rather than a taste one. A server-supplied prompt is an offer. This host fetches it and hands it back as text for a human to look at; it does not splice it into the model's conversation. A host that piped server-supplied prompts directly into its own model would have built a prompt-injection channel with good manners.
Part 2 β The arrow that points back
Sampling is the one that is genuinely different, and here is the picture:
sequenceDiagram
autonumber
participant U as π§ you
participant H as ποΈ your host
participant K as π§° the kitchen server
participant M as π§ the model
U->>H: "summarise the pantry week"
H->>K: tools/call summarise_week
rect rgba(56, 189, 248, 0.12)
note over K: the server CAN read the event logβ¦
K->>K: select * from pantry_events
note over K: β¦but it has no model and no API key
end
rect rgba(251, 191, 36, 0.16)
K-->>H: input_required β<br/>"run this model call for me"
note over H,K: THE ARROW REVERSES.<br/>the server is now the one asking.
end
rect rgba(239, 68, 68, 0.14)
note over H: π° the spend gate decides FIRST
H->>H: estimate the cost Β· check the ceiling
end
H->>M: your key. your bill.
M-->>H: the summary
H->>K: tools/call summarise_week<br/>+ inputResponses + requestState
K-->>H: the finished digest
H-->>U: "here's what happened in the pantry"The part that surprised us
The obvious way to write that server is one line:
const answer = await server.server.createMessage({ ... }); // βIt is what every tutorial shows. It throws here, and the SDK explains why itself:
"Cannot request input (sampling/createMessage): the client on this 2025-era connection did not declare the required capability β per-request legacy serving cannot receive server-to-client requests"
The reason is structural rather than a bug. A serverβclient push needs a connection to travel down. A serverless function does not have one; it has a request, and then it doesn't. There is nowhere for the push to go.
So the 2026-07-28 protocol revision removed the serverβclient channel entirely and replaced it with a retry:
round 1 β tools/call
β {"resultType":"input_required",
"inputRequests":{"summary":{"method":"sampling/createMessage",β¦}},
"requestState":"β¦"}
Β· the host runs the model
round 2 β tools/call + params.inputResponses + params.requestState
β {"resultType":"complete","content":[β¦]}A retry needs no connection at all β which is exactly why they changed it. The protocol grew a stateless shape because everything is serverless now.
We found this by writing a throwaway server and curling it, not by remembering how sampling works. The full trail is stage 2 of BUILD_FROM_SCRATCH.md, and it is the fifth consecutive project where reading the types on disk changed the design.
One consequence worth knowing before you copy
lib/mcp-client.ts: this danger is opt-in. The host declarescapabilities: { sampling: {} }on every request. Delete that one line and every sampling request from every server fails with-32021. The safest configuration is one character shorter than the useful one.
Part 3 β A budget is not a gate
The tempting design is a budget: give the run a spending limit, subtract as you go, stop at zero.
It is worse than it looks, for a reason that has nothing to do with arithmetic. A budget that runs out silently truncates whatever was happening at the moment it ran out. You do not get a decision; you get an arbitrary cut-off point determined by the order things happened in.
The other tempting design is project #3's: gate on what the call is. That worked for destruction because destruction is a property of the verb β smash_jar always asks, jar_history never does.
Cost is not a property of the verb, and this project can prove it with one tool:
the same tool, the same server, the same user question | estimate |
| 0.22Β’ |
| 6.0Β’ |
Twenty-seven times the price, same name. A rule keyed to summarise_week would have to stop both or allow both, and neither is what you want.
So the rule is a number:
below the ceiling β runs immediately, logged to the ledger
above the ceiling β refused, with the price attachedAnd gating on the estimate rather than the result is what makes a refusal free. The host never contacts the model, so the most expensive thing a server can ask for costs exactly as much as the cheapest thing it can ask for: nothing.
Part 4 β The gate that cannot pause
This is the part that took longest to get right, and the answer is a genuine limitation rather than a clever trick.
Project #3's gate pauses. The loop stops, the HTTP request ends, the entire agent is written to Postgres, you close your laptop, and a brand-new request thaws it out five minutes later. That works because the thing being frozen is ours. Our agent does not mind waiting.
A sampling request is not ours. Somebody else's server is sitting inside a tools/call waiting for an answer, and it will time out in under a minute. Freezing it does not pause it β it deadlocks it.
flowchart TB
START(["π§° kitchen asks for a model call<br/><b>estimated 6.0Β’</b>"]) --> GATE{"π° over the<br/>ceiling?"}
GATE -->|"no β 0.22Β’ β€ 2Β’"| RUN["π§ run the model<br/><i>on the host's key</i>"]
RUN --> TAB["π¦ add to the shared TreeBudget<br/>π write a ledger row<br/><i>tagged with WHO asked</i>"]
TAB --> BACK(["β
answer returned to the server"])
GATE -->|"yes β 6.0Β’ > 2Β’"| NO["π REFUSE, synchronously<br/><b>$0.00 spent</b><br/><i>no model was contacted</i>"]
NO --> TELL["π write a ledger row anyway<br/><i>a refusal that leaves no trace is<br/>indistinguishable from a gate<br/>that is switched off</i>"]
TELL --> CARD["π§ a card appears for the human"]
CARD --> GRANT{"grant a<br/>one-shot<br/>allowance?"}
GRANT -->|"yes"| NEXT(["β spent by the agent's NEXT attempt"])
GRANT -->|"no"| DONE(["π« the server is told, plainly,<br/>what it may do instead"])
style START fill:#1e293b,stroke:#38bdf8,color:#f8fafc
style GATE fill:#78350f,stroke:#fbbf24,stroke-width:3px,color:#fef3c7
style NO fill:#7f1d1d,stroke:#ef4444,stroke-width:3px,color:#fecaca
style TELL fill:#450a0a,stroke:#ef4444,color:#fecaca
style CARD fill:#78350f,stroke:#fbbf24,color:#fef3c7
style RUN fill:#052e16,stroke:#4ade80,color:#dcfce7
style TAB fill:#0c4a6e,stroke:#38bdf8,color:#e0f2fe
style BACK fill:#052e16,stroke:#4ade80,color:#dcfce7
style NEXT fill:#052e16,stroke:#4ade80,color:#dcfce7
style DONE fill:#1e293b,stroke:#64748b,color:#f8fafcSo the shape is synchronous refusal, asynchronous approval:
the refusal | immediate, and free |
the human's answer | a card, minutes later, granting a one-shot allowance |
when it takes effect | the agent's next attempt |
That is a real constraint of this boundary, and worth stating plainly instead of hiding: you cannot make somebody else's server wait for your human. Pretending otherwise produces a demo that hangs.
The wording of the refusal is doing real work too β it is project #3's deny-branch lesson, one network hop further out. Say only "denied" and the server's own model reasonably retries the identical call, and each retry costs the agent another round trip. So the text names the price, names the ceiling, and says what would actually help:
REFUSED BY THE HOST'S SPEND GATE. This request is estimated at 6.0Β’, which is over the 2.0Β’ ceiling configured for the server "kitchen". No model call was made and nothing was charged. Do not retry this request unchanged β either ask for a smaller maxTokens, send less text, or proceed without the model's help and say plainly what you could not do.
Three things the host refuses to take on trust
All three arrive from across the boundary, and all three are the biggest inputs to the bill:
the server sends | the host does |
| treats it as a preference, resolves against an allowlist, defaults to the cheapest model on a miss |
| clamps to 1024, with no explanation owed |
| echoes it back without reading it β see part 7 |
Part 5 β One wallet, many spenders
A server's model call could plausibly be billed three ways, and only one of them keeps project #4's seatbelt honest.
Its own budget, per server. Tidy, and wrong β a per-server pot implies an ownership that does not exist, and lets a server be "in budget" while your run is already broke.
Untracked, outside the tree. How it works if you don't think about it. Your
MAX_TREE_TOKENSceiling then guards everything except the one spender you don't control.The same
TreeBudgetthe agents draw from, tagged with who asked. β this one.
Project #4 built one wallet shared by reference across an agent tree so that "the whole run stops at 600,000 tokens" was true rather than approximately true. A server's draw goes on that same tab, or the sentence quietly becomes "600k plus whatever the servers felt like."
One wallet, many spenders, every draw labelled.
The label is the new work, and it is what ledger_entries.server_key is. Which makes a question project #4 could not answer into a group by:
SPEND BY SERVER
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
server allowed refused spent refused value
kitchen 8 5 13.0Β’ 24.2Β’The right-hand column is the one people leave out. Refusals get a ledger row too, with actual_cents = 0, because a ledger with no refusals in it is indistinguishable from a ledger whose gate is switched off β and you would not find out which until the bill arrived.
Part 6 β The regression suite that costs nothing
Project #4's NEXT_STEP.md said to build this before anything else. It was right, and it changed how the whole project was built.
An eval case is two things bolted together, and only one of them costs money:
a PROMPT "empty the cookie jar completely" β costs money
a CHECK "β¦and some call should have been gated" β a pure functionProject #4 already writes every loop event to trace_events, permanently, for the replay UI. So the model's decisions are already on disk. Re-running the check() against a stored trace asks the same question about the same behaviour and costs zero.
β
look-only [single] β
pantry-sweep [crew]
β
chain-dice-to-cookies [single] β
pantry-no-overkill [crew]
β
no-tools [single] β
crew-delegates [crew]
β
gate-fires [single] β
crew-one-approval [crew]
β
picks-right-cipher [single] β
sampling-happens [single]
β
reads-history [single] β
sampling-attributed [single]
β
gate-refuses-expensive
SCORE: 100% (13/13 scored)
Tokens spent by this command: 0Two design decisions made that trustworthy, and both are about not lying.
The extraction lives in one file. There are now two ways to watch an agent work β live from the loop, and replayed from Postgres. If each grew its own "turn events into a score sheet" logic they would drift, and the failure would be vicious: the cheap suite would quietly score things differently from the expensive one while both printed confident percentages. So lib/observation.ts is shared by both, and it is the reason live and replay both say 13/13.
no data is not fail. A case with no stored tape scored nothing, and nothing must never round to zero. Project #4's gotcha 4 was a comparison that reported 0% in both modes and drew a confident conclusion from two identically-broken runs; a replay suite has a brand-new way to produce exactly that lie. The counts are kept apart and named separately.
What replay cannot answer
Stated up front, because a tool that oversells itself gets trusted where it shouldn't be:
replay CAN answer, free | replay CANNOT answer |
did I break a | would the model behave better with a new prompt? |
did a refactor change how calls are recorded? | is this tool description clearer? |
do #3's six and #4's four still score what they scored? | anything about a case never run live |
You need both. But only one of them has to be asked fifty times a day while you refactor, and it is not the expensive one.
Part 7 β Data does not stay yours
The multi-round-trip flow lets a server keep state across the two halves of a sampling exchange: it returns an opaque requestState string, and the client hands it back on the retry.
Which sounds like a session, and is not one. The MCP SDK's own documentation is unusually blunt:
"It travels through the client and comes back as attacker-controlled input: a server that lets it influence authorization, resource access, or business logic MUST integrity-protect it (e.g. HMAC or AEAD) and MUST reject state that fails verification. The SDK does not do this for you."
Read that next to project #3's thesis and it is the mirror image β and sharper, because the data started out as yours:
project #3 | a hint the server sent you is not a permission model |
project #5 | state you sent the client and got back is not your state either |
Data does not stay yours by having been yours.
You minted it, you handed it across a boundary you do not control, and what came back is a string that anything in between could have rewritten. {"jars":3} becoming {"jars":300} is one edit away.
So app/api/kitchen/route.ts signs it on the way out and verifies it on the way back, and the host half never reads it at all β it is a courier that does not open the envelope. Four lines, and without them this repo would be a worked example of the bug.
Part 8 β Run it yourself
git clone https://github.com/ketankshukla/learn-mcp-agent-ledger.git
cd learn-mcp-agent-ledger
npm install
cp .env.example .env.local # add ANTHROPIC_API_KEY, then a Neon DATABASE_URL
npm run db:init # creates the schema and stocks the pantry
npm run devThen, in a second terminal β the first four cost nothing at all:
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.00The demo the project is aiming at, 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 throughAnd project #3's and #4's checkpoints, unchanged:
npm run agent Β· npm run approval Β· npm run crew Β· npm run replay Β· npm run compare
npm run evals -- --all --attempts 1 # the live suite, ~$0.65The map
flowchart TB
subgraph LIB["π lib/ β the brain"]
direction TB
L0["<b>spend-gate.ts</b><br/><i>what counts as expensive</i>"]
L1["<b>sampling.ts</b><br/><i>gate β model β wallet β ledger</i>"]
L2["pricing.ts<br/><i>a dated fact, not a constant</i>"]
L3["ledger.ts<br/><i>who spent what</i>"]
L4["mcp-client.ts<br/><i>hand-written Β· now BOTH eras</i>"]
L5["observation.ts<br/><i>ONE score sheet, two sources</i>"]
L6["replay-evals.ts<br/><i>the $0 suite</i>"]
L7["agent-loop.ts / crew.ts<br/><i>#2, #3 and #4, unchanged</i>"]
end
subgraph APP["π app/ β the face"]
direction TB
A1["api/kitchen<br/><i>THE SERVER THAT ASKS</i>"]
A2["api/pantry Β· api/jar<br/><i>vending machines</i>"]
A3["api/ledger<br/><i>read the meter, turn the dial</i>"]
A4["Ledger.tsx<br/><i>the refusal card</i>"]
end
L2 --> L0
L0 --> L1
L1 --> L3
L4 --> L1
L7 --> L1
L5 --> L6
L3 --> A3
A3 --> A4
A1 -.->|"input_required"| L4
style L0 fill:#7f1d1d,stroke:#ef4444,stroke-width:3px,color:#fecaca
style L1 fill:#78350f,stroke:#fbbf24,stroke-width:2px,color:#fef3c7
style L3 fill:#0c4a6e,stroke:#38bdf8,color:#e0f2fe
style A1 fill:#78350f,stroke:#fbbf24,stroke-width:2px,color:#fef3c7
style A3 fill:#0c4a6e,stroke:#38bdf8,color:#e0f2fe
style LIB fill:#1e293b,stroke:#475569,color:#f8fafc
style APP fill:#1e293b,stroke:#475569,color:#f8fafcPart 9 β Where to go next
Upgrade | Why it matters |
Score Haiku against Sonnet for sub-agents | One string per agent. Project #4's workers do reading, not judgement. The suite to prove it is now free to run. |
Ceilings that learn | A server's first week of draws is a distribution. A ceiling could be a percentile rather than a number somebody typed. |
Elicitation in the browser | The plumbing is built and |
Per-user wallets | Ceilings are per server. In a multi-tenant host they would need to be per server per user, which is the same table and a harder question about defaults. |
NEXT_STEP.md is the series' closing retrospective β what five projects taught, what is genuinely unfinished, and which doors are open for someone else. Short version: the protocol tour is over, and the ledger this project writes is still nobody's input.
This is part of a series
flowchart LR
P1["πͺ #1<br/>learn-mcp-5-year-old<br/><i>an MCP SERVER</i><br/>offers tools, waits"]
P2["π #2<br/>learn-mcp-agent-loop<br/><i>an MCP HOST</i><br/>picks tools, runs the loop"]
P3["β #3<br/>learn-mcp-agent-guard<br/><i>the agent that ASKS FIRST</i><br/>gates, memory, evals"]
P4["π₯ #4<br/>learn-mcp-agent-crew<br/><i>one agent that HIRES HELP</i><br/>sub-agents, one queue"]
P5["πΈ <b>#5 β you are here</b><br/>learn-mcp-agent-ledger<br/><i>the host that OWNS THE WALLET</i><br/>sampling, a spend gate, a ledger"]
P1 --> P2 --> P3 --> P4 --> P5
style P1 fill:#1e293b,stroke:#38bdf8,color:#f8fafc
style P2 fill:#1e293b,stroke:#38bdf8,color:#f8fafc
style P3 fill:#1e293b,stroke:#38bdf8,color:#f8fafc
style P4 fill:#1e293b,stroke:#38bdf8,color:#f8fafc
style P5 fill:#78350f,stroke:#fbbf24,stroke-width:3px,color:#fef3c7What it builds | Start here if⦠| |
An MCP server | MCP itself is new to you | |
An MCP host that owns the loop | You want to know what Claude Desktop was actually doing | |
Approval gates, Postgres, evals, replay | You want to give an agent a dangerous tool and sleep at night | |
Sub-agents, one approval queue, cost measurement | Your agent keeps running out of room on big jobs | |
#5 (you are here) | Sampling, a spend gate, a ledger | Something you connected can spend your money |
Each one is a sequel that reuses the last one's code. This project copies project #4's lib/ wholesale β the loop, the crew, the gate, the notebook, the report card β and teaches its hand-written MCP client the other half of the protocol.
The three documents
For | Answers | |
README.md (you are here) | Understanding | What is sampling? Why does it need a gate? What can a host refuse to trust? |
Doing | Which commands, in what order, and what breaks along the way? | |
Reflecting | What did five projects teach, and what's genuinely unfinished? |
Stack
Next.js 16 (App Router, Turbopack) + React 19 + Tailwind v4
@anthropic-ai/sdkβclaude-sonnet-5for the orchestrator, unchanged from #3 and #4 so eval scores stay comparable;claude-haiku-4-5available to servers that ask@neondatabase/serverlessβ Postgres over HTTP, because serverless has no connection poolmcp-handler+@modelcontextprotocol/serverβ the kitchen, the pantry and the jarThe MCP client is hand-written in
lib/mcp-client.tsβ inherited from project #2, because the protocol is just JSON, and extended here to speak both protocol eras
β project #4: the crew Β· How this was built β Β· The gate itself β
Data does not stay yours by having been yours.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Flicense-qualityDmaintenanceDemonstrates how to implement sampling in MCP servers, allowing tools to request LLM content generation from the client without requiring external API integrations or credentials.1
- FlicenseBqualityCmaintenanceA minimal FastMCP server demonstrating the sampling mechanism, where the server requests a client-side LLM completion to summarize documents.1
- FlicenseAqualityCmaintenanceAn MCP server that demonstrates sampling, enabling the server to request LLM completions from the client to assist in workflow tasks like planning work sessions.3
- AlicenseAqualityBmaintenanceAn MCP server that provides cost and reliability observability for LLM and agent workflows. It records model calls and allows querying and aggregating telemetry data through MCP tools.6MIT
Related MCP Connectors
Hosted MCP server for LLM cost estimation, model comparison, and budget-aware routing.
A paid remote MCP for OpenAI Codex agent coordination MCP, built to return verdicts, receipts, usage
MCP server for MiniMax H3 multimodal video generation
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/ketankshukla/learn-mcp-agent-ledger'
If you have feedback or need assistance with the MCP directory API, please join our Discord server