Module 3 Β· Agents & MCP

MCP: the Model Context Protocol

Advanced 22 min read One standard plug between AI apps and tools

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

without a standard: N Γ— M Claude IDE agent your app GitHub Postgres your docs 9 custom integrations, each maintained separately with MCP: N + M Claude IDE agent your app MCP GitHub srv Postgres srv docs srv each side implements the protocol once

2. Hosts, clients and servers

host (Claude Desktop, IDE, your agent) LLM + app logic decides which tool MCP client A MCP client B one client per server local server (a subprocess) stdio: JSON-RPC lines over stdin/stdout filesystem, git, a local database remote server (a web service) Streamable HTTP + OAuth 2.1 SaaS APIs, company services
Server exposesControlled byWhat it isExample
Toolsthe modelfunctions the LLM may call, with a JSON Schema for arguments (and optionally for output)search_docs(query), create_ticket(...)
Resourcesthe applicationreadable data identified by URIs, which the host chooses to put in contextlesson://sql/06-order-limit.html, a file, a DB schema
Promptsthe userreusable 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))
discover β†’ ['2026-07-28'] ['tools', 'resources'] {'io.modelcontextprotocol/serverInfo': {'name': 'tiny-docs', 'version': '0.1.0'}} tools β†’ ['search_docs', 'count_lessons'] (cache 300s, public) schema of search_docs: {"query": {"type": "string"}, "k": {"type": "integer", "minimum": 1, "maximum": 10}} call β†’ isError False ['sql/25-transactions.html#deadlocks', 'mysql/09-locking.html#deadlock'] bad arg β†’ isError True ValueError: unknown course 'postgres'; valid courses: ['course … no tool β†’ {'code': -32602, 'message': 'Unknown tool: drop_tables'} old version β†’ {'code': -32022, 'message': 'Unsupported protocol version', 'data': {'supported': ['2026-07-28']}} server exited with code 0

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

stdioStreamable HTTP
Shapethe host launches the server as a subprocess; newline-delimited JSON-RPC over stdin/stdouteach message is an HTTP POST to one endpoint (e.g. /mcp); the reply is JSON or a request-scoped SSE stream
Use forlocal tools: files, git, local databases, CLIsremote and shared servers, SaaS, company services
Authcredentials from environment variablesOAuth 2.1 bearer tokens (lesson 14)
Loggingstderr only, because stdout carries protocol messagesnormal server logs; OpenTelemetry
Headersn/aMCP-Protocol-Version, Mcp-Method, Mcp-Name mirror the body for proxies and gateways
stdout is sacred on stdio

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 liveinside your application's codein separate servers, reusable across hosts
Discoveryyou hard-code the schemastools/list at runtime
Who can use themyour app onlyany MCP host: Claude, IDEs, other agents
RelationshipComplementary. 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.

Checkpoint

1 Β· A stdio MCP server works in tests but the host reports JSON parse errors. Most likely cause?
On stdio, stdout carries only JSON-RPC lines. Logs belong on stderr.
2 Β· A tool is called with a date in the wrong format. How should the server report it?
Tool execution errors go back to the model as content, so it can correct the arguments and retry.
3 Β· Which exposes a database schema for the application to include in context, rather than a function for the model to call?
Resources are data addressed by URI and chosen by the application. Tools are actions the model decides to take.