MCP: the Model Context Protocol
Every AI application wants tools and data: files, databases, tickets, calendars, internal APIs. Without a standard, every app writes its own integration for every system. The Model Context Protocol (MCP) is an open protocol for this: a server exposes tools, resources and prompts once, and any MCP-capable host (Claude Desktop, Claude Code, IDEs, ChatGPT, your own agent) can use them. This lesson explains the architecture and then shows the actual JSON-RPC messages, sent to a small server written by hand.
USB-C for AI tools
Before USB, every device needed its own port and cable. USB standardised the plug, so any keyboard works with any computer. MCP standardises the plug between AI applications and the systems they use. Write a server for your ticketing system once, and every MCP host can use it.
1. From N Γ M integrations to N + M
2. Hosts, clients and servers
| Server exposes | Controlled by | What it is | Example |
|---|---|---|---|
| Tools | the model | functions the LLM may call, with a JSON Schema for arguments (and optionally for output) | search_docs(query), create_ticket(...) |
| Resources | the application | readable data identified by URIs, which the host chooses to put in context | lesson://sql/06-order-limit.html, a file, a DB schema |
| Prompts | the user | reusable prompt templates the user picks, often shown as slash commands | /review-code, /explain-lesson |
Clients can also offer features to servers. Elicitation lets a server ask the user for input partway through a call. Sampling (a server borrowing the host's LLM) and roots (the host telling the server which folders it may use) exist too, but the current revision marks both as deprecated, so new servers should not depend on them.
3. The wire protocol: JSON-RPC 2.0, stateless
Every MCP message is a JSON-RPC 2.0 request, response or notification. The current revision of the spec,
2026-07-28, is stateless: there is no connection handshake. Every request carries its
protocol version and the client's capabilities in params._meta, and a server must not rely
on anything an earlier request said. A client can call server/discover first to learn the
server's supported versions and capabilities, but it does not have to.
β {"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": {"name": "search_docs", "arguments": {"query": "deadlock retry", "k": 2},
"_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {"name": "tiny-client", "version": "0.1.0"}}}}
β {"jsonrpc": "2.0", "id": 3,
"result": {"resultType": "complete",
"content": [{"type": "text", "text": "[{\"url\": \"sql/25-transactions.html#deadlocks\", β¦}]"}],
"structuredContent": [{"url": "sql/25-transactions.html#deadlocks", β¦}],
"isError": false,
"_meta": {"io.modelcontextprotocol/serverInfo": {"name": "tiny-docs", "version": "0.1.0"}}}}
Two kinds of failure
A protocol error (unknown tool, malformed request, unsupported version) is a JSON-RPC error. A tool execution error (bad date, API down) is a normal result with isError: true and a message the model can act on.
Content and structured content
content is what the model reads (text, images, links to resources). structuredContent is JSON for programs, validated against the tool's outputSchema if it declares one.
4. Watch it happen: a server written by hand
tiny_mcp.py (next to these lessons) is a 150-line MCP server over stdio with two tools that
search this site. It exists only so you can see the protocol. Real servers use the SDK (next lesson). This
example launches it as a subprocess, as a host would, and exchanges raw JSON lines with it:
import json
import subprocess
import sys
server = subprocess.Popen([sys.executable, "tiny_mcp.py"], stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, encoding="utf-8")
META = {"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}}
def send(rid, method, **params):
request = {"jsonrpc": "2.0", "id": rid, "method": method, "params": {**params, "_meta": META}}
server.stdin.write(json.dumps(request) + "\n") # one message per line
server.stdin.flush()
return json.loads(server.stdout.readline())
r = send(1, "server/discover")["result"]
print("discover β", r["supportedVersions"], list(r["capabilities"]), r["_meta"])
r = send(2, "tools/list")["result"]
print("tools β", [t["name"] for t in r["tools"]], f"(cache {r['ttlMs'] // 1000}s, {r['cacheScope']})")
print(" schema of search_docs:", json.dumps(r["tools"][0]["inputSchema"]["properties"]))
r = send(3, "tools/call", name="search_docs", arguments={"query": "deadlock retry", "k": 2})["result"]
print("call β", "isError", r["isError"], [hit["url"] for hit in r["structuredContent"]])
r = send(4, "tools/call", name="count_lessons", arguments={"course": "postgres"})["result"]
print("bad arg β", "isError", r["isError"], r["content"][0]["text"][:62], "β¦")
print("no tool β", send(5, "tools/call", name="drop_tables", arguments={})["error"])
old = {"jsonrpc": "2.0", "id": 6, "method": "tools/list", "params": {"_meta": {
"io.modelcontextprotocol/protocolVersion": "2025-06-18", "io.modelcontextprotocol/clientCapabilities": {}}}}
server.stdin.write(json.dumps(old) + "\n"); server.stdin.flush()
print("old version β", json.loads(server.stdout.readline())["error"])
server.stdin.close() # closing stdin ends a stdio server
print("server exited with code", server.wait(timeout=10))
Every behaviour in that transcript is in the spec: discovery, cacheable list results, structured results,
a tool error the model can recover from, a protocol error for an unknown tool, and a version error
(-32022) that lists the versions the server does support.
5. Transports
| stdio | Streamable HTTP | |
|---|---|---|
| Shape | the host launches the server as a subprocess; newline-delimited JSON-RPC over stdin/stdout | each message is an HTTP POST to one endpoint (e.g. /mcp); the reply is JSON or a request-scoped SSE stream |
| Use for | local tools: files, git, local databases, CLIs | remote and shared servers, SaaS, company services |
| Auth | credentials from environment variables | OAuth 2.1 bearer tokens (lesson 14) |
| Logging | stderr only, because stdout carries protocol messages | normal server logs; OpenTelemetry |
| Headers | n/a | MCP-Protocol-Version, Mcp-Method, Mcp-Name mirror the body for proxies and gateways |
One stray print() in a stdio server corrupts the protocol stream, and the host reports a
confusing parse error. Log to stderr (or use the SDK's logging), always.
6. Protocol versions, briefly
Revisions are named by date. Up to 2025-11-25, a connection began with an initialize
request and notifications/initialized, which set up a session. 2026-07-28 removed the
handshake and sessions, added server/discover, a resultType on every result, and
cache hints (ttlMs, cacheScope) on list results. It also replaced server-initiated
requests with "multi round-trip requests", where the server answers
resultType: "input_required" and the client retries with the input. You rarely handle any of
this yourself: the official Python SDK v2 client probes the server and falls back to the handshake with
older servers automatically.
7. MCP vs plain function calling
| Function calling (lesson 11) | MCP | |
|---|---|---|
| Where tools live | inside your application's code | in separate servers, reusable across hosts |
| Discovery | you hard-code the schemas | tools/list at runtime |
| Who can use them | your app only | any MCP host: Claude, IDEs, other agents |
| Relationship | Complementary. An MCP client lists a server's tools, converts them into the model's function-calling format, and routes the model's calls back to the server (lesson 14). | |
Recap
- MCP standardises how AI hosts use external tools and data: N + M integrations instead of N Γ M.
- Servers expose tools (model-controlled), resources (app-controlled) and prompts (user-controlled).
- JSON-RPC 2.0, stateless in 2026-07-28: every request carries its version and capabilities in
_meta. - stdio for local servers, Streamable HTTP with OAuth for remote ones. Tool errors are results; protocol errors are errors.