Skip to main content
Glama
ketankshukla

cookie-jar-mcp

by ketankshukla

πŸͺ 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:#fecaca

The 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 messages

The 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:#dcfce7

That'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:#fef3c7

Scale 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?

initialize

2

The AI reads the list. Your server sends back every tool's name, description, and inputs.

tools/list

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.

tools/call

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!"
    end

Every 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

roll_dice

πŸ“„ Resource

A noun. Something to read. Has an address, not arguments.

The app loads it

cookiejar://status

πŸ’¬ Prompt

A saved fill-in-the-blank instruction.

The human picks it

bedtime_story

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:#dcfce7

Part 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 >=2

You 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:#e0f2fe

The 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

say_hello

The simplest tool that can exist. One input, one sentence out.

roll_dice

Optional inputs, defaults, and guard rails via .min() / .max().

cookie_jar

State that survives between calls β€” and a tool that politely says no.

secret_code

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:#dcfce7

Memory 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 dev

Open 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/inspector

Point 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:#f8fafc

How 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:#e0f2fe

Part 7 β€” Plug it into Claude

Claude Code

claude mcp add --transport http cookie-jar https://learn-mcp-5-year-old.vercel.app/api/mcp

Claude 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

  1. Build one yourself from an empty folder. β†’ BUILD_FROM_SCRATCH.md, the 13-stage developer walkthrough.

  2. Add your own tool. Copy the say_hello block, rename it, change the description. That's genuinely all it takes.

  3. Give the jar a real memory. Swap the variable for Neon Postgres so it survives a nap β€” npx vercel install neon, then one update ... returning statement. Project #3 does exactly this, if you want the finished version.

  4. Lock the door. mcp-handler ships withMcpAuth for token-checking, so not everyone on the internet can eat your cookies.

  5. Return structured data. Add an outputSchema and structuredContent so 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:#dcfce7

What it builds

Start here if…

#1 (you are here)

An MCP server

MCP itself is new to you

#2 β€” the agent loop

An MCP host that owns the loop

You want to know what Claude Desktop was actually doing

#3 β€” the agent that asks first

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?

BUILD_FROM_SCRATCH.md

Doing

Which commands, in what order, and what breaks along the way?

NEXT_STEP.md

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 SDK

  • Zod v4 β€” describes tool inputs and validates them for free


How this was built β†’ Β· What's next β†’ Β· project #2: the agent loop β†’

F
license - not found
-
quality - not tested
B
maintenance

Maintenance

–Maintainers
–Response time
–Release cycle
–Releases (12mo)
Commit activity

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

  • A
    license
    B
    quality
    D
    maintenance
    A 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.
    3
    20
    66
    Apache 2.0
  • A
    license
    -
    quality
    D
    maintenance
    A toy MCP server for exploring Model Context Protocol capabilities, including resources, tools, and prompts.
    Apache 2.0

View all related MCP servers

Related MCP Connectors

View all MCP Connectors

Latest Blog Posts

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