MCP: JSON-RPC + 3 Method Names
The Model Context Protocol demystified: initialize, tools/list, tools/call. How an agent discovers what a server can do and executes it — the same handshake used by Claude, ChatGPT and every MCP client.
What you'll learn
- Learn the three MCP methods and the version handshake
- Write a tool descriptor with an inputSchema the LLM can read
- Build a complete MCP server by hand in plain Python
The whole protocol is three verbs
Last lesson you built a JSON-RPC server where method could be anything. The Model Context Protocol simply fixes the vocabulary: every MCP server in the world answers initialize, tools/list and tools/call. That's why one client — Claude, ChatGPT, your IDE — can talk to any server it has never seen before.
Step through a real session
These are the exact request/response pairs printed by the notebook's client loop. Use Next / Prev to walk the handshake, the discovery, and the call.
→ client sends
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {}
}← server replies
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"serverInfo": {
"name": "MyServer",
"version": "1.0"
}
}
}The client and server agree on a protocol version and introduce themselves. It happens once, when the connection opens — before any tool is mentioned.
The tool descriptor
A tool descriptor is the contract the LLM reads. The description tells the model when to use the tool; the inputSchema tells it how:
TOOLS = [
{
"name": "add", # matches FUNCTIONS["add"]
"description": "Add two numbers together and return the sum.", # the LLM reads this
"inputSchema": {
"type": "object", # arguments always arrive as a dict
"properties": {"a": {"type": "number"}, # argument a -> a number
"b": {"type": "number"}}, # argument b -> a number
"required": ["a", "b"], # both mandatory
},
},
{
"name": "shout",
"description": "Return the given text in capital letters with an exclamation mark.",
"inputSchema": {
"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"],
},
},
]The server: same shape, three known methods
Compare this with lesson 1's handle() — it is the same function with the method names pinned down:
def mcp_server(req):
m, p, rid = req["method"], req.get("params", {}), req["id"]
if m == "initialize":
result = {"protocolVersion": "2024-11-05",
"serverInfo": {"name": "MyServer", "version": "1.0"}}
elif m == "tools/list":
result = {"tools": TOOLS} # <- hand back the catalogue
elif m == "tools/call":
out = FUNCTIONS[p["name"]](**p["arguments"]) # <- run the real function
result = {"content": [{"type": "text", "text": str(out)}]} # always a list of blocks
return {"jsonrpc": "2.0", "id": rid, "result": result}You have now written a complete MCP server in plain Python — no framework, no SDK. Everything a framework adds from here is convenience, which is exactly what the next lesson is about.