cookie-jar-mcp
πͺ MCP, Explained Like You Are Five
A complete, working Model Context Protocol server built with Next.js and deployed to Vercel β plus the lesson that explains every line of it.
π΄ Live lesson + playground: https://learn-mcp-5-year-old.vercel.app
π Live MCP endpoint: https://learn-mcp-5-year-old.vercel.app/api/mcp
The whole server is one file: app/api/mcp/route.ts
π¨ Want to build it yourself, from an empty folder? BUILD_FROM_SCRATCH.md is the full developer walkthrough β 13 stages, every command, a checkpoint after each one, and an appendix of the four things that actually broke during this build. This README teaches you what MCP is; that one teaches you how to build one.
Part 1 β What is MCP, really?
The problem
An AI is a brain in a jar. It's very smart, and it's completely stuck.
It can think about your files. It can't open them. It can talk about rolling dice. It can't actually roll one β ask it for a random number and it'll pick 7 far more often than chance allows, because it's pattern-matching, not rolling.
Your code, meanwhile, is the opposite: it has hands but no idea what the human wants.
flowchart LR
AI["π§ <b>The AI</b><br/>β
knows a lot<br/>β has no hands"]
GAP["π§ <b>the gap</b><br/>no agreed way<br/>to reach across"]
CODE["π§° <b>Your code</b><br/>β
can touch anything<br/>β knows nothing about you"]
AI -.->|"wants to"| GAP
GAP -.->|"can't"| CODE
style AI fill:#1e293b,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
style CODE fill:#1e293b,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
style GAP fill:#450a0a,stroke:#ef4444,stroke-width:2px,stroke-dasharray: 6 4,color:#fecacaThe fix
MCP is the agreed-upon shape of the message between the two.
Picture a toy box. The AI can't see inside it. So you tape a list to the outside:
Inside this box:
π² 1 dice β for when you need real randomness
πͺ 1 cookie jar β for counting cookies
π 1 decoder ring β for secret messagesThe AI reads the list, points at one, and says "use that one, please, with these settings." You reach in, use it, and hand back the result.
MCP is the plug that closes the gap:
flowchart LR
AI["π§ The AI<br/>brain, no hands"]
MCP{{"π MCP<br/>one agreed shape<br/>of message"}}
SRV["π§° Your server<br/>hands, no brain"]
TOOLS["π² dice<br/>πͺ cookie jar<br/>π decoder ring"]
AI -->|"1 . what have you got?"| MCP
MCP -->|"2 . here's the list"| AI
AI -->|"3 . use roll_dice"| MCP
MCP --> SRV
SRV --> TOOLS
TOOLS -->|"4 . the answer"| AI
style AI fill:#1e293b,stroke:#38bdf8,color:#f8fafc
style MCP fill:#78350f,stroke:#fbbf24,color:#fef3c7
style SRV fill:#1e293b,stroke:#38bdf8,color:#f8fafc
style TOOLS fill:#052e16,stroke:#4ade80,color:#dcfce7That's the entire protocol. The rest is details.
Why a standard matters
Before MCP, connecting 10 AI apps to 10 services meant writing 100 custom integrations. With MCP it's 10 + 10 = 20: each app speaks MCP once, each service speaks MCP once, and everything plugs into everything.
flowchart TB
subgraph AFTER["β
With MCP β 3 + 3 = 6 connections"]
direction TB
B1["Claude"] --- HUB{{"MCP"}}
B2["Cursor"] --- HUB
B3["ChatGPT"] --- HUB
HUB --- B4["GitHub"]
HUB --- B5["Postgres"]
HUB --- B6["Your app"]
end
subgraph BEFORE["β Without MCP β 3 Γ 3 = 9 custom integrations"]
direction TB
A1["Claude"] --- A4["GitHub"]
A1 --- A5["Postgres"]
A1 --- A6["Your app"]
A2["Cursor"] --- A4
A2 --- A5
A2 --- A6
A3["ChatGPT"] --- A4
A3 --- A5
A3 --- A6
end
style BEFORE fill:#450a0a,stroke:#ef4444,color:#fecaca
style AFTER fill:#052e16,stroke:#4ade80,color:#dcfce7
style HUB fill:#78350f,stroke:#fbbf24,color:#fef3c7Scale that to 10 Γ 10 and it's 100 versus 20. It's USB-C for AI. One shape of plug.
Related MCP server: My Learning MCP Server
Part 2 β The six steps of every MCP conversation
There is no magic in here. It's six ordinary HTTP POSTs carrying JSON.
# | What happens | The actual message |
1 | The AI knocks. A handshake: which version do you speak? |
|
2 | The AI reads the list. Your server sends back every tool's name, description, and inputs. |
|
3 | You ask for something. "Roll me three d20s." | (plain English, no MCP yet) |
4 | The AI points at a toy. It matched your sentence to a description. |
|
5 | Your code runs. On your server, with your data. The AI never sees inside. | (just JavaScript) |
6 | The answer comes home. Your text goes back; the AI turns it into a sentence. | (the response) |
Drawn out, the whole conversation looks like this β and this diagram is the protocol. There is nothing hidden behind it:
sequenceDiagram
autonumber
actor You as π§ You
participant AI as π§ Claude
participant SRV as π§° Your MCP server
rect rgba(56, 189, 248, 0.15)
Note over AI,SRV: Handshake β happens once, when Claude starts
AI->>SRV: initialize
SRV-->>AI: "I speak MCP 2025-06-18, I'm cookie-jar-mcp"
AI->>SRV: tools/list
SRV-->>AI: say_hello, roll_dice, cookie_jar, secret_code<br/>+ each one's description and inputs
end
rect rgba(251, 191, 36, 0.15)
Note over You,SRV: Now you actually ask for something
You->>AI: "roll me three twenty-sided dice"
Note right of AI: Reads your sentence.<br/>Matches it against the<br/>DESCRIPTIONS from step 4.
AI->>SRV: tools/call<br/>roll_dice { sides: 20, times: 3 }
Note right of SRV: YOUR JavaScript runs here.<br/>Claude never sees inside.
SRV-->>AI: "Rolled 3d20 -> [17, 9, 18] Total: 44"
AI-->>You: "You rolled 17, 9 and 18 β 44 total!"
endEvery message is wrapped in a boring envelope called JSON-RPC 2.0:
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": { "name": "roll_dice", "arguments": { "sides": 20, "times": 3 } } }That's it. That's the protocol you were nervous about.
Part 3 β The three things a server can offer
Almost everyone only ever builds tools. The other two matter because they answer a question tools can't: who gets to decide?
What it is | Who decides | Example here | |
π§ Tool | A verb. Something to do. | The AI picks it |
|
π Resource | A noun. Something to read. Has an address, not arguments. | The app loads it |
|
π¬ Prompt | A saved fill-in-the-blank instruction. | The human picks it |
|
The easiest way to keep them straight is to follow the arrow back to whoever pulled the trigger:
flowchart TB
SERVER["π§° Your MCP server"]
SERVER --> T["π§ TOOLS<br/><i>verbs β things to DO</i>"]
SERVER --> R["π RESOURCES<br/><i>nouns β things to READ</i>"]
SERVER --> P["π¬ PROMPTS<br/><i>saved instructions</i>"]
T --> TW["π§ the AI decides<br/>picks it when your<br/>description convinces it"]
R --> RW["π» the app decides<br/>loads it as context,<br/>addressed by URI"]
P --> PW["π§ the human decides<br/>you pick it from a menu;<br/>the AI never can"]
TW --> TE["roll_dice"]
RW --> RE["cookiejar://status"]
PW --> PE["bedtime_story"]
style SERVER fill:#78350f,stroke:#fbbf24,color:#fef3c7
style T fill:#1e293b,stroke:#38bdf8,color:#f8fafc
style R fill:#1e293b,stroke:#a78bfa,color:#f8fafc
style P fill:#1e293b,stroke:#4ade80,color:#f8fafc
style TE fill:#0c4a6e,stroke:#38bdf8,color:#e0f2fe
style RE fill:#2e1065,stroke:#a78bfa,color:#ede9fe
style PE fill:#052e16,stroke:#4ade80,color:#dcfce7Part 4 β Anatomy of a tool
Every tool needs exactly four things:
server.registerTool(
"roll_dice", // 1. NAME
{
description: "Roll one or more dice ...", // 2. DESCRIPTION β the important one
inputSchema: z.object({ // 3. SHAPE
sides: z.number().int().min(2).max(1000).default(6),
times: z.number().int().min(1).max(20).default(1),
}),
},
async ({ sides, times }) => { // 4. DO-THING
return { content: [{ type: "text", text: "..." }] };
}
);The description is the whole ballgame. It is the only thing the AI reads when deciding whether to use your tool. Vague description β your tool never gets called. Write it like a job posting, not a variable name.
The schema is a free bouncer. .min(2) means a 1-sided die gets rejected before your code runs, with a clear message the AI can read and correct:
Input validation error: Invalid arguments for tool roll_dice:
sides: Too small: expected number to be >=2You didn't write a line of validation code. You just described the shape honestly. Here's where that check sits β note that bad input never reaches your function at all:
flowchart TB
START(["π§ AI sends tools/call"]) --> EXISTS{"Does a tool<br/>with that name exist?"}
EXISTS -->|no| ERR1["β Unknown tool<br/><i>AI is told, and can retry</i>"]
EXISTS -->|yes| VALID{"Do the arguments match<br/>your inputSchema?"}
VALID -->|"no β sides: 1"| ERR2["β Input validation error<br/>'Too small: expected >= 2'<br/><i>your code never ran</i>"]
VALID -->|"yes β sides: 20"| DEFAULTS["βοΈ Missing optional values<br/>filled in from .default"]
DEFAULTS --> RUN["βΆοΈ YOUR function finally runs"]
RUN --> LOGIC{"Your own rules<br/>e.g. enough cookies?"}
LOGIC -->|no| SOFT["π
A polite refusal in plain English<br/>'only 7 cookies in the jar'<br/><i>not a crash β the AI can explain it</i>"]
LOGIC -->|yes| OK["β
content: [ { type: 'text' } ]"]
SOFT --> BACK(["π§ back to the AI"])
OK --> BACK
ERR1 --> BACK
ERR2 --> BACK
style START fill:#1e293b,stroke:#38bdf8,color:#f8fafc
style BACK fill:#1e293b,stroke:#38bdf8,color:#f8fafc
style ERR1 fill:#450a0a,stroke:#ef4444,color:#fecaca
style ERR2 fill:#450a0a,stroke:#ef4444,color:#fecaca
style SOFT fill:#78350f,stroke:#fbbf24,color:#fef3c7
style OK fill:#052e16,stroke:#4ade80,color:#dcfce7
style RUN fill:#0c4a6e,stroke:#38bdf8,color:#e0f2feThe two red boxes are free β you get them from z.object({...}). The amber box is the one you write yourself, and writing it well is what separates a tool that helps from a tool that confuses.
Part 5 β What's in this particular toy box
Tool | What it teaches |
| The simplest tool that can exist. One input, one sentence out. |
| Optional inputs, defaults, and guard rails via |
| State that survives between calls β and a tool that politely says no. |
| Why tools beat guessing. Letter-shift math an AI would fumble in its head. |
Plus one resource (cookiejar://status) and one prompt (bedtime_story).
πͺ Note on the cookie jar: it's a plain variable in memory. Serverless machines fall asleep, so the count resets on its own sometimes. That's not a bug β it's the lesson. Real state belongs in a database.
You will hit this for real, and it's confusing until you've seen it drawn. Vercel runs your code on however many machines it feels like, and each one gets its own copy of that variable:
flowchart TB
subgraph NOW["β What this repo does β memory in a variable"]
direction TB
U1["you: add 20 cookies"] --> M1["π₯οΈ Machine A<br/>cookiesInJar = 12 β 32"]
U2["you: look in the jar"] --> M2["π₯οΈ Machine B<br/>cookiesInJar = 12<br/><i>never heard of Machine A</i>"]
M2 --> WAT["π 'The jar has 12 cookies'<br/>where did the 20 go?"]
end
subgraph FIX["β
What real servers do β memory in a database"]
direction TB
V1["you: add 20 cookies"] --> N1["π₯οΈ Machine A"]
V2["you: look in the jar"] --> N2["π₯οΈ Machine B"]
N1 --> DB[("ποΈ Neon Postgres<br/>cookies = 32")]
N2 --> DB
DB --> YAY["π 'The jar has 32 cookies'<br/>every machine agrees"]
end
style NOW fill:#450a0a,stroke:#ef4444,color:#fecaca
style FIX fill:#052e16,stroke:#4ade80,color:#dcfce7
style DB fill:#78350f,stroke:#fbbf24,color:#fef3c7
style WAT fill:#7f1d1d,stroke:#ef4444,color:#fecaca
style YAY fill:#14532d,stroke:#4ade80,color:#dcfce7Memory in a serverless function is a sandcastle β real, working, and taken by the tide. Fixing it means changing one line in the tool. The AI can't tell the difference; it just calls cookie_jar and gets a number. How you store the cookies is entirely your business.
Part 6 β Run it yourself
npm install
npm run devOpen http://localhost:3000 β the page includes a live playground that shows you the exact JSON going back and forth.
Poke the server directly
curl -X POST http://localhost:3000/api/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Inspect it with the official GUI
npx @modelcontextprotocol/inspectorPoint it at http://localhost:3000/api/mcp, transport Streamable HTTP.
Where everything lives
Only two files matter. The rest is scaffolding:
flowchart LR
subgraph REPO["π this repo"]
direction TB
ROUTE["β app/api/mcp/route.ts<br/><b>the entire MCP server</b><br/><i>4 tools, 1 resource, 1 prompt</i>"]
PAGE["app/page.tsx<br/>the lesson you're reading"]
PLAY["app/Playground.tsx<br/><i>a tiny MCP client β<br/>the other half of the protocol</i>"]
MCPJSON[".mcp.json<br/>points Claude Code at<br/>local + live"]
end
ROUTE -->|"serves"| EP(["π /api/mcp"])
PLAY -->|"calls"| EP
PAGE -.->|"embeds"| PLAY
MCPJSON -.->|"points at"| EP
EP --> CLIENTS["π§ Claude Code<br/>π§ Claude Desktop<br/>π§ any MCP client"]
style ROUTE fill:#78350f,stroke:#fbbf24,color:#fef3c7
style EP fill:#0c4a6e,stroke:#38bdf8,color:#e0f2fe
style CLIENTS fill:#052e16,stroke:#4ade80,color:#dcfce7
style REPO fill:#1e293b,stroke:#475569,color:#f8fafcHow a change reaches the internet
Vercel is wired to this GitHub repo, so shipping a new tool is just a push:
flowchart LR
EDIT["βοΈ add a tool in<br/>route.ts"] --> COMMIT["git commit"]
COMMIT --> PUSH["git push origin main"]
PUSH --> GH["π GitHub"]
GH -->|"webhook"| VC["β² Vercel builds"]
VC --> LIVE(["π learn-mcp-5-year-old<br/>.vercel.app/api/mcp"])
LIVE --> CLAUDE["π§ Claude sees the<br/>new tool on next<br/>tools/list"]
style EDIT fill:#1e293b,stroke:#38bdf8,color:#f8fafc
style GH fill:#1e293b,stroke:#a78bfa,color:#f8fafc
style VC fill:#78350f,stroke:#fbbf24,color:#fef3c7
style LIVE fill:#052e16,stroke:#4ade80,color:#dcfce7
style CLAUDE fill:#0c4a6e,stroke:#38bdf8,color:#e0f2fePart 7 β Plug it into Claude
Claude Code
claude mcp add --transport http cookie-jar https://learn-mcp-5-year-old.vercel.app/api/mcpClaude Desktop
Add to claude_desktop_config.json:
{
"mcpServers": {
"cookie-jar": {
"type": "http",
"url": "https://learn-mcp-5-year-old.vercel.app/api/mcp"
}
}
}Restart, then say "roll me three twenty-sided dice" and watch it reach into the jar.
Part 8 β Where to go next
Build one yourself from an empty folder. β BUILD_FROM_SCRATCH.md, the 13-stage developer walkthrough.
Add your own tool. Copy the
say_helloblock, rename it, change the description. That's genuinely all it takes.Give the jar a real memory. Swap the variable for Neon Postgres so it survives a nap β
npx vercel install neon, then oneupdate ... returningstatement. Project #3 does exactly this, if you want the finished version.Lock the door.
mcp-handlershipswithMcpAuthfor token-checking, so not everyone on the internet can eat your cookies.Return structured data. Add an
outputSchemaandstructuredContentso the AI gets real JSON instead of a sentence.
This is part of a series
This project is the server β the vending machine that waits to be told what to do. The interesting question it raises is who does the telling, and that turns out to be a much bigger subject.
flowchart LR
P1["πͺ <b>#1 β you are here</b><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 the tools, runs the loop"]
P3["β #3<br/>learn-mcp-agent-guard<br/><i>the agent that ASKS FIRST</i><br/>approval gates, memory, evals"]
P1 --> P2 --> P3
style P1 fill:#78350f,stroke:#fbbf24,stroke-width:3px,color:#fef3c7
style P2 fill:#1e293b,stroke:#38bdf8,color:#f8fafc
style P3 fill:#052e16,stroke:#4ade80,color:#dcfce7What it builds | Start here if⦠| |
#1 (you are here) | 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 persistence, evals | You want to give an agent a dangerous tool and sleep at night |
πͺ About that cookie jar confession in Part 5: project #3 finally fixes it. The jar there is backed by real Postgres, so the count survives restarts and agrees across machines β same tool, same protocol, different storage. The AI genuinely can't tell the difference, which was the point.
The three documents
For | Answers | |
README.md (you are here) | Understanding | What is MCP? Why does it exist? What can a server offer? |
Doing | Which commands, in what order, and what breaks along the way? | |
Deciding | What's missing from this, and what should the next project be? |
Stack
Next.js 16 (App Router, Turbopack) + React 19 + Tailwind v4
mcp-handlerβ Vercel's adapter that turns an MCP server into a route handler@modelcontextprotocol/serverβ the official TypeScript SDKZod v4 β describes tool inputs and validates them for free
How this was built β Β· What's next β Β· project #2: the agent loop β
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
- AlicenseBqualityDmaintenanceA beginner-friendly Model Context Protocol (MCP) server that helps users understand MCP concepts, provides interactive examples, and lists available MCP servers. This server is designed to be a helpful companion for developers working with MCP. Also comes with a huge list of servers you can install.32066Apache 2.0
- Flicense-qualityCmaintenanceAn educational MCP server demonstrating tools, resources, and prompts for learning the Model Context Protocol.112
- Alicense-qualityDmaintenanceA toy MCP server for exploring Model Context Protocol capabilities, including resources, tools, and prompts.Apache 2.0
- FlicenseAqualityCmaintenanceA beginner-friendly MCP server built in plain Node.js/JavaScript that exposes five tools (calculator, UUID generator, read notes, get weather, password generator) to teach how the Model Context Protocol works under the hood.5
Related MCP Connectors
An MCP server for deep research or task groups
Educational MCP server with 17 math/stats tools, visualizations, and persistent workspace
A MCP server built for developers enabling Git based project management with project and personalβ¦
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-5-year-old'
If you have feedback or need assistance with the MCP directory API, please join our Discord server