The MCP protocol
Lesson 3 of 6
3The MCP protocol
The big pictureWithout a protocol, every tool you write needs a custom integration with every AI application you want to use it in. MCP (Model Context Protocol) is an open protocol that solves this: you write one tool server, and it works with any client that speaks the protocol, from chat apps to coding environments to your own agent.
What the picture shows: These three messages are most of what happens in a session: agreeing on the version, then the tool list that goes in front of the model, then running get_leave_balance. Every reply carries the request's id, so the client knows which question it answers.
Map of this part
The concepts in order, click any one to jump to it:
Two ways into the same idea
Before standard sockets every device had its own charger. MCP is a standard socket for tools: you write your tool once, and any AI app that supports the standard can use it. No rewriting it for every program.
The Model Context Protocol, MCP, defines a server exposing tools, resources and prompts over JSON-RPC messages on stdio or HTTP. The client discovers capabilities and calls them, and you test your server locally with an inspector before connecting it to an app.
Before You Start
🔧Tools as an interface
You know the name, description and schema are what the model sees of a tool.
📨JSON
You read and write JSON messages, since every protocol message is JSON.
The Core Idea
MCP lets a tool be written once and work with every client. In this lesson you see why it is needed, who talks to whom, the three messages, and then you write a server and test it.
1Why a protocol
Without a protocol, every tool needs a custom integration with every application. With N tools and M apps, you write N×M integrations.
Look at the picture: 5 tools and 3 apps mean 15 integrations without a protocol. With MCP you write 5 servers, and 3 clients speak the protocol, so the total is 8.
2The server and the client
The server exposes tools and runs them. The client, the chat app or your agent, discovers the tools, puts them in front of the model and runs what it asks for.
Look at the picture: the model never talks to the server. The request passes through the client, and the client is what asks for approval before dangerous tools.
A server offers more than tools. Tools are actions the model calls, like create_ticket. Resources are data the client reads into the context, like a file or a page. And prompts are ready templates the user picks.
3A session in three messages
Under the hood, MCP is simple JSON-RPC messages. The client agrees with the server, asks for its tools, then asks it to run one. We write the server by hand to see every message.
import json
TOOLS = {
"get_leave_balance": {
"description": "Return the remaining annual leave days for an employee id.",
"inputSchema": {"type": "object", "properties": {"employee_id": {"type": "string"}},
"required": ["employee_id"]},
"fn": lambda args: {"employee_id": args["employee_id"], "days_left": 12},
}
}
def handle(request):
method, rid = request["method"], request.get("id")
if method == "initialize":
result = {"protocolVersion": "2025-06-18", "capabilities": {"tools": {}},
"serverInfo": {"name": "hr-server", "version": "0.1"}}
elif method == "tools/list":
result = {"tools": [{"name": n, "description": t["description"], "inputSchema": t["inputSchema"]}
for n, t in TOOLS.items()]}
elif method == "tools/call":
tool = TOOLS[request["params"]["name"]]
out = tool["fn"](request["params"]["arguments"])
result = {"content": [{"type": "text", "text": json.dumps(out)}]}
else:
return {"jsonrpc": "2.0", "id": rid, "error": {"code": -32601, "message": f"unknown method {method}"}}
return {"jsonrpc": "2.0", "id": rid, "result": result}
session = [
{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}},
{"jsonrpc": "2.0", "id": 2, "method": "tools/list"},
{"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": {"name": "get_leave_balance", "arguments": {"employee_id": "E-17"}}},
]
for req in session:
print("->", json.dumps(req))
print("<-", json.dumps(handle(req)))
Three messages are all that happens in most sessions. In initialize the two sides agree on the protocol version and capabilities. tools/list returns what the client will put in the model's context. And tools/call runs the tool and returns the result as text content. This is a teaching simplification, since the official library handles all the details, but it is the same shape you will see in the inspector.
4A server with the official library
In real work you do not write the protocol by hand. The official library handles the messages, errors and transport, and you write an ordinary function.
The same leave balance tool, with the official Python library. It needs pip install mcp, so it is here for reading.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("hr-server")
BALANCES = {"E-17": 12, "E-42": 3}
@mcp.tool()
def get_leave_balance(employee_id: str) -> dict:
"""Return the remaining annual leave days for one employee.
Use it when someone asks how many leave days they have left.
employee_id looks like 'E-17'.
"""
if employee_id not in BALANCES:
return {"error": f"No employee {employee_id}. Ask for the id on their badge."}
return {"employee_id": employee_id, "days_left": BALANCES[employee_id]}
if __name__ == "__main__":
mcp.run() # stdio by default
The library reads the function name as the tool name, the docstring as its description and the type hints as its schema. So everything from the tools lesson is written here in its natural place: a clear name, a description saying when, and an error saying what next.
5Test locally, then connect
Before any model touches your server, open it in the official MCP Inspector. It connects to your server, lists its tools and runs them by hand.
# 1. open the inspector against your server, then list and call tools by hand
npx @modelcontextprotocol/inspector python hr_server.py
# 2. once it works, register it in a client's MCP config (the file name differs per client)
{
"mcpServers": {
"hr": { "command": "python", "args": ["/full/path/to/hr_server.py"] }
}
}
In the inspector try every tool with valid, invalid and empty inputs, and read the error messages as if you were the model. If you cannot tell from them what to do next, neither can the model.
How It Works, Step by Step: how a client finds and runs an MCP server's tools
The client starts or connects to the server and sends initialize so they agree on the version and capabilities.
It sends tools/list and the server returns the tools' names, descriptions and schemas.
The client puts that list in the model's context, and the model asks for a tool with arguments.
The client sends tools/call, after approval if needed, and the server returns the result as text appended to the conversation.
Going deeper
An MCP server is code running with your permissions. A run_command or delete_file tool in a server you install means any connected model can ask to run it. Install only servers whose source you trust and whose tools you have read; good clients ask for your approval before running dangerous tools, the subject of the injection and permissions lesson.
Worked example
Under the hood, MCP is simple JSON-RPC messages between a client and a server. The client asks "what tools do you have?", the server returns a list of names, descriptions and schemas, then the client asks "run this tool with these arguments". Let us write a small server by hand that answers these messages.
print in a server running over stdio. Standard output is the protocol channel itself, so any extra line corrupts the messages and drops the connection with no clear error. Write logs to stderr or a file.
Try it without code
Myth vs Reality
| Myth | Reality |
|---|---|
| MCP is a model or an AI library. | It is an open protocol for JSON-RPC messages between a client and a server. It contains no model; it connects tools to any client that speaks it. |
| An MCP server from a public source is safe because it follows the protocol. | A server is code running with your permissions. Install only what you trust and whose tools you have read. |
| A server has to run over the network. | With stdio the client runs it as a subprocess on the same machine, the simplest start for local tools. |
Try it yourself
You wrote a tool for one chat client with a custom integration. Why rewrite it as an MCP server?
That is the whole point of the protocol: the tool is written once. When you move to another client or build your own agent, the tool stays as it is.
Your stdio server fails to connect with no clear error. What do you check first?
In stdio, standard output is reserved for the protocol. One debug print is enough to break the connection.
Quick review cards
create_ticket). Resources are data the client reads into the context (a file, a page). And prompts are ready templates the user picks.Check Your Understanding
What is the difference between a tool and a resource in MCP? Give an example of each in an HR system.
A tool is an action the model decides to call with arguments, like submit_leave_request(start, end). A resource is data the client reads into the context, often chosen by the user, like the "employee policy handbook" as a file. A tool may change something; a resource is for reading.
What from the tools/list reply does the client put in the model's context, and why does that matter when you write the server?
The names, descriptions and parameter schemas. So the docstring and type hints in your function are what the model chooses by, and deserve the same care as in the tools lesson.
Resources for this lesson
Grouped by type. Start with the official docs for precision, or an article or video for the explanation.
Official docs2
Done with this lesson?
Sign in to save your progress.