C4 · lesson 4 · 12 min read
MCP architecture and transports
How MCP hosts, clients and servers fit together, what the stdio and Streamable HTTP transports do, and how to add a server to Claude Code.
Հայերեն
Official video by Anthropic on YouTube. Pressing play loads it from youtube-nocookie.com. ·open on YouTube
The Model Context Protocol (MCP) is an open standard for connecting AI applications to tools and data. You write a capability once as an MCP server, and any MCP host can use it, whether that is Claude Code, Claude Desktop or an IDE. This lesson follows the current spec revision, 2026-07-28.
Hosts, clients and servers
| Participant | What it is | Example |
|---|---|---|
| Host | The AI application that the user works in; it manages the clients | Claude Code |
| Client | A connector inside the host that holds one connection to one server | Claude Code’s connection to k8s-ro |
| Server | A program that exposes tools, resources and prompts | A read-only kubectl server |
The host creates one client per server. If you connect two servers, there are two clients. “Local” and “remote” describe where the server runs, not what it can do.
Two layers
- The data layer is JSON-RPC 2.0: requests, results, errors and
notifications. Methods include
tools/list,tools/callandresources/read. - The transport layer carries those messages and handles framing and authorization. The same JSON-RPC runs over every transport.
Transports
stdio. The client starts the server as a subprocess and exchanges
newline-delimited JSON-RPC over stdin and stdout. It’s the natural choice for local
tools: no network port, and only the host that started the server can talk to it.
The rule that breaks most first servers: stdout may contain only MCP messages.
A stray print() corrupts the stream. Log to stderr instead.
Streamable HTTP. The server is an independent process with one HTTP
endpoint, for example https://mcp.example.com/mcp. Every client message is a
POST, and the server answers each request with either a JSON object or an SSE
stream scoped to that request. It serves many clients, so use it for shared or
remote servers. Requests carry headers such as MCP-Protocol-Version,
Mcp-Method and Mcp-Name, so gateways can route without parsing the body. The
spec’s security rules: validate the Origin header (to stop DNS rebinding),
bind local servers to 127.0.0.1, and authenticate every connection. OAuth
is the recommended way to get tokens.
Adding servers to Claude Code
# Remote server over Streamable HTTP (OAuth sign-in later via /mcp)
claude mcp add --transport http monitoring https://mcp.example.com/mcp
# Local stdio server: Claude's options first, then --, then the server command
claude mcp add --transport stdio --env KUBECONFIG="$HOME/.kube/readonly" \
k8s-ro -- uv run --directory "$PWD" server.py
claude mcp list # every server with its health
claude mcp get k8s-ro # details for one server
claude mcp remove k8s-ro
Everything after -- goes to the server unchanged, so the server’s own flags
aren’t parsed as Claude flags. Inside a session, /mcp shows status and handles
OAuth sign-in.
Lab server
# server.py - read-only kubectl MCP server (stdio)
import json
import logging
import subprocess
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
logging.basicConfig(level=logging.INFO) # stderr, never stdout
mcp = MCPServer("k8s-ro")
ALLOWED_NAMESPACES = {"payments", "checkout"}
@mcp.tool()
def kubectl_get_pods(namespace: str) -> list[dict]:
"""List pods in one namespace with phase, readiness and restart count.
Read-only. Only the payments and checkout namespaces are allowed.
"""
if namespace not in ALLOWED_NAMESPACES:
raise ToolError(f"Namespace {namespace!r} is not allowed. Use one of: {sorted(ALLOWED_NAMESPACES)}.")
out = subprocess.run(["kubectl", "get", "pods", "-n", namespace, "-o", "json"],
capture_output=True, text=True, timeout=20)
if out.returncode != 0:
raise ToolError(f"kubectl failed: {out.stderr.strip()[:300]}")
pods = []
for item in json.loads(out.stdout)["items"]:
statuses = item["status"].get("containerStatuses", [])
pods.append({"name": item["metadata"]["name"], "phase": item["status"].get("phase"),
"ready": all(s.get("ready") for s in statuses),
"restarts": sum(s.get("restartCount", 0) for s in statuses)})
logging.info("listed %d pods in %s", len(pods), namespace)
return pods
if __name__ == "__main__":
mcp.run(transport="stdio")
The SDK builds the tool’s JSON Schema from the type hints and uses the docstring as its description. Lesson 1 still applies: the docstring is the description.
Recap
- Host = the AI app, client = one connection to one server, server = the capabilities. There is one client per server.
- The data layer is JSON-RPC 2.0, and the transports are stdio (local subprocess) and Streamable HTTP (one POST endpoint). HTTP+SSE is deprecated.
- Spec 2026-07-28 is stateless: no
initialize, no sessions, metadata in each request, andserver/discoverfor capabilities. - Never write to stdout in a stdio server. Validate
Origin, bind to localhost and authenticate HTTP servers. claude mcp add --transport http|stdio …, with the stdio command after--.
See it in action
1/5 When the host starts, its MCP client connects to the server and discovers the tools it offers.
all steps
- When the host starts, its MCP client connects to the server and discovers the tools it offers.
- During a task Claude asks for one of those tools with a tool_use block, just like a built-in tool.
- The client sends a JSON-RPC tools/call request to the server over Streamable HTTP.
- The server does the real work (here a read-only query to the monitoring API) with its own credentials.
- The result travels back as the tool result. Claude never sees the server's credentials, only what the tool returns.
$ ./try-it.sh
Try it: a read-only kubectl MCP server
- Run
uv init k8s-ro && cd k8s-ro && uv add "mcp[cli]"and save the server from this lesson asserver.py. - Try it in the MCP Inspector with
uv run mcp dev server.pyand callkubectl_get_podswith namespacepayments, then withkube-system. - Register it in Claude Code with
claude mcp add --transport stdio k8s-ro -- uv run --directory "$PWD" server.py, then runclaude mcp list. - In a session, run
/mcpto check that the server is connected, and ask Claude which pods inpaymentsaren't Ready.
Check yourself
4 questions. Original practice questions, not taken from the real exam.