Part IV · Applied concepts · Chapter 30

MCP, basic and advanced

The Model Context Protocol (MCP) is an open standard for connecting AI applications to tools and data. A service writes one MCP server, and every application that speaks MCP can use it. This chapter covers the basics (who talks to whom, about what), then the advanced parts (the wire format, HTTP transport, authorization, security and versioning), and ends with a tiny server you can run.

Why it matters

MCP is how a growing number of tools and data sources are connected to AI applications. It also changes quickly: its current revision removed the handshake that most tutorials still teach. Knowing the protocol, and which revision a piece of advice is about, saves days of confusion.

Which revision

Protocol details here follow the official specification. The current revision is 2026-07-28; the one before it, 2025-11-25, is called "legacy" by the specification and is still widely deployed. Where they differ, both are shown and labelled.

The problem it solves

Without a standard, every AI application writes its own connector for every service. With N applications and M services, that is N × M connectors. With a shared protocol, each application implements the client side once and each service the server side once: N + M. Worked: 5 applications and 20 services need 100 connectors without a standard and 25 implementations with one. The same arithmetic made USB and HTTP worth standardising.

Hosts, clients and servers

  • Host: the application the user runs, such as a chat app or a coding tool. It decides which servers to connect, asks the user for consent, and controls what reaches the model.
  • Client: created by the host, one per server connection. It speaks the protocol to exactly one server and keeps that server separate from the others.
  • Server: a program that offers capabilities. It can run locally, as a child process, or remotely, over HTTP. A server should not see the whole conversation or other servers.

A server offers three kinds of thing, distinguished by who decides to use them:

PrimitiveControlled byExample
Toolsthe modelA function the model may call: compound_value(principal, rate_pct, years)
Resourcesthe applicationRead-only content identified by a URI, which the host may show or attach: rates://all
Promptsthe userReady-made request templates, often offered as slash commands: compare_banks(amount)

Clients can offer features back. The main current one is elicitation: a server asks the user for input, either through a form (which must never ask for passwords, keys or payment details) or by sending the user to a URL. Two older client features, sampling (the server borrows the client's model) and roots (the client says which folders to work in), are deprecated in 2026-07-28, as is server logging.

On the wire

Every MCP message is a JSON-RPC 2.0 object, of four kinds: a request (has an id and a method), a result or an error answering a request (same id), and a notification (a method with no id, never answered). The id is a string or an integer, never null. Two transports carry the messages:

  • stdio. The client starts the server as a child process and writes one JSON message per line to its standard input; the server answers on standard output. Messages contain no newlines. Anything the server writes to standard error is a log, not an error. The client ends the session by closing the server's input.
  • Streamable HTTP. The server has one endpoint (such as /mcp) that accepts POST. A request gets back either a JSON body or a stream of server-sent events; a notification gets HTTP 202.

The conversation, step by step

In 2025-11-25 a connection starts with a handshake. The client sends initialize with the revision it wants, its capabilities and its name; the server replies with the revision it agrees to, its own capabilities and name; the client confirms with the notification notifications/initialized. Only then do requests such as tools/list and tools/call flow.

In 2026-07-28 there is no handshake and no session. Every request carries the revision, the client's capabilities and (recommended) its name inside params._meta, so any request can be understood on its own. A server must implement server/discover, which a client may call to learn the supported revisions and capabilities; every result carries a resultType. Statelessness makes servers easy to scale behind a load balancer: no request depends on a previous one.

Try it

Step through a real MCP conversation

These are real messages, captured from this book's tiny server (built with the official Python SDK) by the wire.py script below. Step through the legacy handshake, then switch revision and compare. Which fields moved into _meta?

Tools in detail

  • A tool has a name, a description, an inputSchema (a JSON Schema whose root is an object), and optionally an outputSchema and annotations.
  • A result holds content (text, images, audio, links or embedded resources) for the model, optional structuredContent for code, and isError.
  • Two kinds of failure. A protocol error, such as a malformed request, is a JSON-RPC error. The specification puts an unknown tool in this group too, but SDKs differ: the Python SDK 2.3.0 answered an unknown tool with an isError: true result reading "Unknown tool: nope" when this book was written, so handle both. A failure while running the tool is a normal result with isError: true and a message the model can read and act on.
  • Annotations are hints: readOnlyHint (default false), destructiveHint (default true), idempotentHint (default false) and openWorldHint (default true). A client must not trust them from a server it does not trust.
  • The specification asks that a person be able to see tool calls and refuse them. A host should show which tool is being called with which arguments, and ask before anything with side effects.

Streamable HTTP in detail

  • The client's Accept header lists both application/json and text/event-stream.
  • In 2026-07-28 every request also carries the header MCP-Protocol-Version, which must match the revision in _meta (a mismatch gets HTTP 400), and Mcp-Method; calls that name a tool, resource or prompt add Mcp-Name, so gateways can route and filter without parsing the body.
  • In 2025-11-25 the server could open a session by returning an MCP-Session-Id header, the client echoed it on later requests and ended it with HTTP DELETE, and a GET on the endpoint opened a stream for server-initiated messages. 2026-07-28 removed sessions and the GET stream.
  • Origin checks. A server must validate the Origin header to stop DNS rebinding, where a web page tricks your browser into calling a server on your own machine, and answer 403 to an invalid one. A local server should bind to 127.0.0.1, not to all interfaces. (Inside a container it may bind to all interfaces if the published port is reachable only on the host's own 127.0.0.1.)

Authorization

Authorization is optional and applies to HTTP servers; a stdio server takes its credentials from its environment. When used, it is built on OAuth 2.1:

  • The MCP server is an OAuth resource server; the MCP client is an OAuth client; a separate authorization server issues tokens.
  • The server publishes Protected Resource Metadata (RFC 9728) naming its authorization server; a 401 response points to it.
  • The client names the server it wants a token for, with the resource parameter (RFC 8707), in both the authorization and token requests. The token travels in the Authorization: Bearer header, never in a URL.
  • The server must check that each token was issued for it (its audience) and reject others with 401. It must never pass a client's token on to another service: token passthrough is forbidden, because it bypasses the other service's checks and breaks the audit trail.

Security

The specification's security best-practices page covers, among others: the confused deputy problem (a proxy server with one shared identity at a third party, tricked into sending an authorization code to an attacker), token passthrough, server-side request forgery (a server fetching URLs an attacker chose, including internal ones), hijacking of state handles, compromise of local servers, and keeping scopes minimal. Two lessons apply to every MCP setup:

  • Everything from a server is untrusted input to the model. Tool descriptions and results can carry instructions aimed at the model. Install servers you trust, review their tool descriptions, and keep data marked as data (chapter 35).
  • Least privilege. Give each server only the access its tools need, and prefer read-only tools.
  • Identity comes from outside the model. Take the caller's identity from a header or the authenticated session, never from a tool argument. Arguments are written by the model, which text in the conversation can steer; a header is set by the calling code. Refuse a request with a missing or unknown identity.

Living with a moving standard

2026-07-28 is a large change: it removed the handshake, sessions, ping and the HTTP GET stream, moved tasks into an extension, and deprecated roots, sampling and logging, which can be removed no earlier than the first revision released on or after 2027-07-28. Practical rules: note which revision any tutorial assumes, pin your SDK version, and test against both revisions while servers and clients upgrade at different speeds. The official Python SDK 2 implements 2026-07-28 and still answers 2025-11-25 clients, as the widget above shows.

Project: a tiny MCP server

The folder labs/06-mcp-server/ holds a server with two read-only tools, one resource and one prompt, about a page of Python, and two ways to talk to it:

  • server.py: the server, built with MCPServer from the official SDK. Run over stdio by default, or --http for Streamable HTTP on 127.0.0.1.
  • client.py: starts the server as a child process with the SDK's Client, lists the tools, calls both, reads the resource, gets the prompt, and finally breaks a rule on purpose to show an isError result.
  • wire.py: standard library only. Writes raw JSON-RPC lines to the server and prints every message both ways, for both revisions. The widget above shows its output.
NeedsTimeHardware
Python 3.10 or newer and pip install -r labs/06-mcp-server/requirements.txt (the mcp package) in a fresh virtual environmentAbout 10 minutes to install and run; an hour to add your own toolAny laptop. No GPU, no API key, and no network once installed
# from the repository root python -m venv .venv && . .venv/bin/activate pip install -r labs/06-mcp-server/requirements.txt python labs/06-mcp-server/client.py # the SDK client python labs/06-mcp-server/wire.py # the raw messages, both revisions

When this book was written, both scripts ran cleanly against mcp 2.3.0. client.py ended with:

compound_value with years=99 isError: True Error executing tool compound_value: principal and rate_pct must be >= 0, and years between 0 and 50

The message reaches the model because the tool raises the SDK's ToolError, the exception for a failure you expected. Any other exception counts as a crash, and the model sees only "Error executing tool compound_value", without the reason. The HTTP mode also refused a request with a foreign Origin header with HTTP 403. The official MCP Inspector (npx @modelcontextprotocol/inspector, which needs Node.js) gives a web interface for the same exploration; it was not run here.

Ideas to extend it: add a tool that returns an error your client handles; add a resource template such as rates://{bank}; connect the server to an MCP host you use and watch the model choose between the two tools; write a smoke test that starts the server in process and calls every tool once for each kind of caller.

One pattern is worth copying. Keep each tool's schema fixed and put the data behind it. Start with invented data, as here, and later point the same tools at a real service without changing any host. A client can also fall back to in-process data when the server is down, and show a badge saying which source answered.

Try it

A company has 4 AI applications and 15 internal services, and plans 5 more services next year. How many connectors does each approach need after the expansion, and how many new pieces of work does the expansion itself cost each way?

Answer

Without a standard: 4 × 20 = 80 connectors, of which the expansion adds 4 × 5 = 20. With MCP: 4 + 20 = 24 implementations, of which the expansion adds 5 servers.

Build it

In Lab 6 you build an MCP server of your own, with a tool, a resource and a prompt, and call it from a client you also write.

Self-check

Who decides when a tool is used, when a resource is used, and when a prompt is used?

Tools: the model. Resources: the application. Prompts: the user.

What replaced the initialize handshake in 2026-07-28?

Every request carries the protocol revision, the client's capabilities and its identity in _meta; an optional server/discover call reports what the server supports.

A tool fails because an argument is out of range. How should the server report it?

As a normal result with isError: true and a clear message, so the model can read it and correct its call. JSON-RPC errors are for protocol problems such as a malformed request.

Why should a server take the caller's identity from a header or session, not from a tool argument?

Arguments are written by the model, which can be steered by text in the conversation. A header is set by the calling code, outside the model's reach.

Further reading