Building an MCP server in Python
Lesson 2 of 2
2Building an MCP server in Python
The big pictureIn the previous lesson you wired tools into your agent by hand. MCP makes the wiring standard: your server announces "these are my tools", and any app supporting the protocol discovers and calls them, like a USB port for AI.
What the picture shows: An MCP client and server conversation: a handshake, tool discovery, then a call. All of it JSON-RPC messages.
Map of this part
The concepts in order, click any one to jump to it:
Two ways into the same idea
Before USB every device had its own cable. Today any mouse works in any computer. MCP does this for tools: you build your "device" (a server that knows the café menu, say) once, and it works in Claude Desktop, in editors and in any agent that supports the standard.
MCP is built on JSON-RPC 2.0 over stdio (a local process) or streamable HTTP (a remote server). A server offers three kinds of thing: tools (functions with an input schema the model calls), resources (read-only data under a URI), and prompts (ready templates the user picks). The lifecycle: initialize to exchange capabilities, then tools/list and tools/call. The official mcp package includes the MCPServer class, which builds the schema from type hints and the docstring. Test with the MCP Inspector before wiring to a client. Security: the server runs with your permissions, so restrict paths and actions.
Before You Start
🤖Building an agent
The previous lesson: the tool loop.
🧾JSON
You read and write JSON.
The Core Idea
1The messages under the hood
Before the library, look at the protocol itself. Here is a tiny server handling three JSON-RPC messages as a client sends them:
import json
MENU = {"latte": 14, "mocha": 16}
TOOLS = [{
"name": "get_price",
"description": "Price in riyals of one café menu item.",
"inputSchema": {"type": "object", "properties": {"item": {"type": "string"}},
"required": ["item"]},
}]
def handle(request):
method, rid = request["method"], request["id"]
if method == "initialize":
result = {"protocolVersion": "2025-06-18", "capabilities": {"tools": {}},
"serverInfo": {"name": "cafe", "version": "1.0"}}
elif method == "tools/list":
result = {"tools": TOOLS}
elif method == "tools/call":
item = request["params"]["arguments"]["item"]
text = f"{item}: {MENU[item]} SAR" if item in MENU else f"no item {item}"
result = {"content": [{"type": "text", "text": text}], "isError": item not in MENU}
else:
return {"jsonrpc": "2.0", "id": rid, "error": {"code": -32601, "message": "unknown method"}}
return {"jsonrpc": "2.0", "id": rid, "result": result}
for req in [{"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_price", "arguments": {"item": "mocha"}}}]:
print(json.dumps(handle(req))[:110])2MCPServer: the real server
The official library writes all of the above for you. Type hints become the input schema, and the docstring becomes the description:
from mcp.server import MCPServer
mcp = MCPServer("cafe")
MENU = {"latte": 14, "mocha": 16, "tea": 6}
@mcp.tool()
def get_price(item: str) -> str:
"""Price in riyals of one café menu item, e.g. 'latte'."""
if item not in MENU:
return f"No item called {item}. Available: {', '.join(MENU)}"
return f"{item}: {MENU[item]} SAR"
@mcp.resource("menu://today")
def today_menu() -> str:
"""Today's full menu as plain text."""
return "\n".join(f"{k}: {v} SAR" for k, v in MENU.items())
# run it under the inspector: uv run mcp dev server.py
# or over HTTP: uv run mcp run server.py --transport streamable-http3Testing and wiring
Run uv run mcp dev server.py to see the tools and try them with no model. Then add the server to the client's settings (say the Claude Desktop config file) with the same run command.
read_file(path) tool with no limits hands the model all your files, keys included. Restrict the path to one folder and reject ...Worked example
Problem: the model never uses your tool. Analysis: the description "gets data" is vague and the name is tool1. Fix: the name search_orders and the description "Search the café's orders by customer phone or date. Returns up to 20 orders, newest first.". After the change the model picks it for the right questions.
Myth vs Reality
| Myth | Reality |
|---|---|
| MCP belongs to one company. | It is an open standard with a published specification and many apps supporting it. |
| The server needs the internet. | A stdio server runs locally as a process on your machine. |
Where you meet it at work
🗂️Reaching your data
A database, files or an internal system made safely available to the model.
🔁Reuse
The same server in the editor, the chat app and agents.
Test your understanding
What message protocol is MCP built on?
Each message has jsonrpc, id, method and params.
What separates a tool from a resource?
Tools act; resources are read by URI.
Where does MCPServer get a tool's description?
And the input types from the type hints.
Quick review cards
Check Your Understanding
Why build an MCP server instead of wiring tools straight into the agent?
So the same server works with any client that supports the standard, without rewriting.
What happens in initialize?
The client and server exchange version and capabilities before any call.
How do you protect a file-reading tool?
By confining it to one folder and rejecting paths that leave it.
Resources for this lesson
Grouped by type. Start with the official docs for precision, or an article or video for the explanation.
Done with this lesson?
Sign in to save your progress.