Project #1 builds an MCP server: four tools, one resource, one prompt, deployed to the internet and reachable from Claude Desktop. It needs no API key, no database, and no environment variables.
The whole thing is one file. By the end of this section you will understand every line of it.
Part 1 Β· A server is a vending machine
It stands there. It has a lit-up panel listing what is inside. It does nothing at all until somebody presses a button, and then it does exactly the one thing that button does and goes back to standing there.
Three properties, and each one matters later:
It advertises. Anybody who asks gets the full list of what it can do, without having to know in advance.
It waits. It cannot decide to do something. It cannot start a conversation, poll you, or push you a notification.
It is stateless between customers. Each request arrives, is served, and is forgotten. The machine does not remember you.
Part 2 Β· Three messages are the whole protocol
Not three kinds of message. Three messages.
| Message | In English | When |
|---|---|---|
initialize | "Hello. Which version of this do you speak?" | once, when a host connects |
tools/list | "What have you got?" | once, just after connecting |
tools/call | "Use that one, please, with these arguments." | every time a tool is used |
That is a complete, working MCP conversation. Everything else in the specification β resources, prompts, sampling, elicitation, notifications, progress β is a refinement of those three moves.
Read step 6 again, and then notice what is missing
Nothing in the server connected the words "three twenty-sided dice" to
{ sides: 20, times: 3 }. The server never saw your sentence. It received
arguments and ran a function.
The matching happened at step 5, inside the model, against the descriptions the server advertised at step 4. That is the entire mechanism, and it is why the next page is about descriptions.
What initialize is actually for
It looks like ceremony. It is doing two real jobs.
Version negotiation. The client says which protocol revision it speaks; the server answers with one it can support. This matters more than it sounds β in project #5 two different protocol eras turn out to coexist, and which one you land on determines whether a whole capability is available.
Capability declaration. Each side says what it can handle. This is the
mechanism that makes the dangerous parts of MCP opt-in: a host that never
declares sampling cannot be asked to spend money, because the request fails
at the protocol layer.
Part 3 Β· The envelope is boring on purpose
Every message is JSON-RPC 2.0, which you can learn in about a minute:
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": { "name": "roll_dice", "arguments": { "sides": 20, "times": 3 } } }| Field | Purpose |
|---|---|
jsonrpc | Always "2.0". Identifies the envelope format. |
id | Any number. The reply carries the same one, so replies can be matched to requests. |
method | What you want. |
params | Arguments for it. |
Replies come back as either result or error, carrying the same id:
{ "jsonrpc": "2.0", "id": 1, "result": { "content": [ β¦ ] } }
{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32601, "message": "Method not found" } }That is the whole wire format. There is no MCP daemon, no special port, no binary encoding, no persistent socket required.
Poke a real one
curl -X POST https://learn-mcp-5-year-old.vercel.app/api/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Checkpoint
Four tools come back, each with a name, a description and an
inputSchema. That is a live server on the public internet answering a
protocol message from your terminal.
Part 4 Β· The whole server is one file
Worth saying plainly, because "build an MCP server" sounds like a project.
Project #1's server is a single Next.js route handler at
app/api/mcp/route.ts. There is no framework, no daemon, no long-lived
process. It is an HTTP endpoint that receives JSON and returns JSON.
The shape is:
import { createMcpHandler } from "mcp-handler";
import { z } from "zod";
const handler = createMcpHandler((server) => {
server.registerTool(
"roll_dice",
{
description: "Roll one or more dice ...",
inputSchema: z.object({
sides: z.number().int().min(2).max(1000).default(6),
times: z.number().int().min(1).max(20).default(1),
}),
},
async ({ sides, times }) => {
const rolls = Array.from({ length: times }, () => 1 + Math.floor(Math.random() * sides));
const total = rolls.reduce((a, b) => a + b, 0);
return { content: [{ type: "text", text: `Rolled ${times}d${sides} -> [${rolls}] Total: ${total}` }] };
},
);
// β¦three more tools, one resource, one prompt
});
export { handler as GET, handler as POST };createMcpHandler does the boring half: parsing the JSON-RPC envelope, routing
tools/list and tools/call, converting your Zod schema to JSON Schema,
validating incoming arguments against it, and formatting the reply.
You write the tools. It writes the protocol.
Part 5 Β· Serverless is a perfect fit for a server, and a trap later
Notice how well a vending machine maps onto a serverless function. Request in, result out, nothing remembered, nothing running between customers. You are billed only for the milliseconds you actually serve.
That fit is real, and it is why project #1 deploys to Vercel in a single push with no configuration.
But it has two consequences you will meet in this course, and both of them are surprising the first time:
It bites in project #1
There is nowhere to keep state between requests. let cookiesInJar = 12 works on your laptop and lies on serverless, because each machine has its own copy and they fall asleep.
β The sandcastle
β¦and again in project #5
There is no open connection, so a server cannot push anything to you β which breaks the way every tutorial implements sampling.
β The finding
What you now know
- A server advertises and waits. It never initiates.
- Three messages do everything:
initialize,tools/list,tools/call. initializenegotiates a protocol version and declares capabilities β the mechanism that keeps the dangerous parts opt-in.- The wire format is JSON-RPC 2.0 over HTTP POST, and you can drive it with
curl. Acceptmust list bothapplication/jsonandtext/event-stream.- A whole server is one route handler; the library writes the protocol and you write the tools.
Next: what is actually inside a tool definition, and why one of its four fields does almost all the work.