cookie-jar-mcp

cookie-jar-mcp

A playful, educational MCP server with tools like dice rolling, a cookie jar, and secret codes, built to teach the Model Context Protocol to beginners.

Category
访问服务器

README

🍪 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.


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

<div align="center">

How this was built → · What's next → · project #2: the agent loop →

</div>

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选