Skip to content

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.

Ընտրեք տեքստը՝ հայերեն թարգմանությունը տեսնելու համար։Highlight text to see Armenian. Machine-assisted translation; English is the source of truth.

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.

MCP host (Claude Code)MCP client AMCP client Bk8s-ro serverlocal subprocessmonitoring serverremote, OAuthstdioStreamable HTTPone client per server; JSON-RPC 2.0 on both transports
One client per server. The transport depends on where the server runs.

Two layers

  • The data layer is JSON-RPC 2.0: requests, results, errors and notifications. Methods include tools/list, tools/call and resources/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, and server/discover for 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

One MCP tool call, end to end
tool_usetools/callHTTPSresulttool_resultClaudeMCP clientinside the hostMCP serverStreamable HTTPmonitoring API

1/5 When the host starts, its MCP client connects to the server and discovers the tools it offers.

all steps
  1. When the host starts, its MCP client connects to the server and discovers the tools it offers.
  2. During a task Claude asks for one of those tools with a tool_use block, just like a built-in tool.
  3. The client sends a JSON-RPC tools/call request to the server over Streamable HTTP.
  4. The server does the real work (here a read-only query to the monitoring API) with its own credentials.
  5. 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

  1. Run uv init k8s-ro && cd k8s-ro && uv add "mcp[cli]" and save the server from this lesson as server.py.
  2. Try it in the MCP Inspector with uv run mcp dev server.py and call kubectl_get_pods with namespace payments, then with kube-system.
  3. Register it in Claude Code with claude mcp add --transport stdio k8s-ro -- uv run --directory "$PWD" server.py, then run claude mcp list.
  4. In a session, run /mcp to check that the server is connected, and ask Claude which pods in payments aren't Ready.

Check yourself

4 questions. Original practice questions, not taken from the real exam.

  1. Your Claude Code session is connected to three MCP servers: a local kubectl server, a local Terraform docs server and a remote monitoring server.

    1. How many MCP clients does the host maintain?

  2. A teammate's stdio MCP server worked until they added print(f"listing pods in {ns}") for debugging. Now Claude Code shows the server as failing, with JSON parse errors.

    2. What is the cause?

  3. The SRE team wants one central MCP server for incident data that 40 engineers' Claude Code sessions can use, behind the company's SSO.

    3. Which transport fits, and how do engineers add it?

  4. You're upgrading an in-house MCP server to spec revision 2026-07-28.

    4. Which two statements about this revision are true? (Choose two.)

    Choose 2.

Answer every question to check.