Try this first
Here is a complete MCP conversation. Not a simplified one — this is what actually travels.
→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2024-11-05",
"capabilities":{},
"clientInfo":{"name":"rover","version":"0.1"}}}
← {"jsonrpc":"2.0","id":1,"result":{
"protocolVersion":"2024-11-05",
"capabilities":{"tools":{}},
"serverInfo":{"name":"notes","version":"0.1"}}}
→ {"jsonrpc":"2.0","method":"notifications/initialized"}
→ {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
← {"jsonrpc":"2.0","id":2,"result":{"tools":[
{"name":"add_note",
"description":"Add a note to the notebook.",
"inputSchema":{"type":"object",
"properties":{"text":{"type":"string"}},
"required":["text"]}}]}}
→ {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{
"name":"add_note","arguments":{"text":"Call the plumber"}}}
← {"jsonrpc":"2.0","id":3,"result":{
"content":[{"type":"text","text":"Added note 1."}]}}
That is MCP. Read it twice. There is nothing else.
What each part is doing
It is JSON-RPC 2.0. A forty-line specification from 2010. Every message has
"jsonrpc":"2.0". Requests carry an id and a method; responses carry the same id and
either a result or an error. Messages without an id are notifications and get no reply.
One JSON object per line, over stdin and stdout. That is the default transport. The client
starts the server as a subprocess and they talk over pipes.
initialize is a handshake, and it is where version and capability negotiation happens. It
comes first, always. The notifications/initialized that follows is the client saying “I am
ready” — no reply expected.
tools/list is the menu. Look closely at what comes back: name, description,
inputSchema. That is the same three fields you wrote by hand in Module 2. MCP did not
invent a new way to describe a tool. It standardised where the description comes from.
tools/call is the execution, and the result is a list of content blocks — the same
shape as the tool results you have been building since Lesson 1.5.
The one mapping you need
Put the two side by side:
| Our tool, since Module 2 | MCP tool, on the wire |
|---|---|
name |
name |
description |
description |
input_schema |
inputSchema |
Snake case on one side, camel case on the other. That is the entire translation, and you write
it yourself in Lesson 5.5 — it is six lines.
Once you have seen that, MCP stops being a new concept. It is your Module 2 tool definition,
sent over a pipe by a program you did not write.
Errors
Failures come back as JSON-RPC errors, or as a result marked as an error:
← {"jsonrpc":"2.0","id":3,"error":{"code":-32602,"message":"Missing required argument: text"}}
-32602 is “invalid params”, from the JSON-RPC spec. You will also see -32601 (no such
method) and -32700 (bad JSON). When an MCP server “does not work”, these are what you are
looking for — and now you can read them.
MCP is JSON-RPC 2.0 over stdin and stdout: a handshake, a menu, and a call. If you can read
those three messages, you can debug any MCP server.
Try this before the next lesson
Find any MCP server on your machine or install one. Run it directly in a terminal and paste
the initialize message from above into stdin, followed by Enter.
You will get a JSON response back. You are speaking the protocol by hand. Do it once and it
stops being magic permanently.