Skip to main content
Glama
ketankshukla

cookie-jar-mcp

by ketankshukla
README.md
# ๐Ÿช 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`
๐Ÿ“ **Write-up:** [An MCP server is one Next.js route handler](https://ketanshukla.dev/blog/an-mcp-server-is-one-route-handler)

**The whole server is one file:** [`app/api/mcp/route.ts`](app/api/mcp/route.ts)

> ๐Ÿ”จ **Want to build it yourself, from an empty folder?**
> **[BUILD_FROM_SCRATCH.md](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.

```mermaid
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:

```mermaid
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.

```mermaid
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.

---

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

```mermaid
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:

```json
{ "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:

```mermaid
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:

```ts
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:

```mermaid
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**:

```mermaid
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

```bash
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

```bash
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

```bash
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:

```mermaid
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:

```mermaid
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

```bash
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`:

```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](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](https://github.com/ketankshukla/learn-mcp-agent-guard) 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.

```mermaid
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"]
    P4["๐Ÿ‘ฅ #4<br/>learn-mcp-agent-crew<br/><i>one agent that HIRES HELP</i><br/>sub-agents, one queue"]
    P5["๐Ÿ’ธ #5<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:#78350f,stroke:#fbbf24,stroke-width:3px,color:#fef3c7
    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:#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](https://github.com/ketankshukla/learn-mcp-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](https://github.com/ketankshukla/learn-mcp-agent-guard)** | Approval gates, Postgres persistence, evals | You want to give an agent a dangerous tool and sleep at night |
| **[#4 โ€” the crew](https://github.com/ketankshukla/learn-mcp-agent-crew)** | Sub-agents, one approval queue, cost measurement | Your agent keeps running out of room on big jobs |
| **[#5 โ€” the ledger](https://github.com/ketankshukla/learn-mcp-agent-ledger)** | Sampling, a spend gate, a ledger | Something you connected can spend your money |

> ๐Ÿ’ธ **This server is a vending machine, and [project #5](https://github.com/ketankshukla/learn-mcp-agent-ledger) explains why that matters.** Everything here is request-in, result-out โ€” it costs you nothing beyond the request. MCP's `sampling` capability turns that around: a server can ask *your* host to run a model call, on your key. That needs a gate, and project #5 builds one.

> ๐Ÿช **About that cookie jar confession in [Part 5](#part-5--whats-in-this-particular-toy-box):** 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](README.md)** *(you are here)* | Understanding | *What is MCP? Why does it exist? What can a server offer?* |
| **[BUILD_FROM_SCRATCH.md](BUILD_FROM_SCRATCH.md)** | Doing | *Which commands, in what order, and what breaks along the way?* |
| **[NEXT_STEP.md](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`](https://www.npmjs.com/package/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

---

<div align="center">

**[How this was built โ†’](BUILD_FROM_SCRATCH.md)** ยท **[What's next โ†’](NEXT_STEP.md)** ยท **[project #2: the agent loop โ†’](https://github.com/ketankshukla/learn-mcp-agent-loop)**

</div>