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.
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.
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:
| Primitive | Controlled by | Example |
|---|---|---|
| Tools | the model | A function the model may call: compound_value(principal, rate_pct, years) |
| Resources | the application | Read-only content identified by a URI, which the host may show or attach: rates://all |
| Prompts | the user | Ready-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.
Tools in detail
- A tool has a
name, adescription, aninputSchema(a JSON Schema whose root is an object), and optionally anoutputSchemaandannotations. - A result holds
content(text, images, audio, links or embedded resources) for the model, optionalstructuredContentfor code, andisError. - 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: trueresult reading "Unknown tool: nope" when this book was written, so handle both. A failure while running the tool is a normal result withisError: trueand a message the model can read and act on. - Annotations are hints:
readOnlyHint(default false),destructiveHint(default true),idempotentHint(default false) andopenWorldHint(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
Acceptheader lists bothapplication/jsonandtext/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), andMcp-Method; calls that name a tool, resource or prompt addMcp-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-Idheader, 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
Originheader 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
resourceparameter (RFC 8707), in both the authorization and token requests. The token travels in theAuthorization: Bearerheader, 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 withMCPServerfrom the official SDK. Run over stdio by default, or--httpfor Streamable HTTP on 127.0.0.1.client.py: starts the server as a child process with the SDK'sClient, lists the tools, calls both, reads the resource, gets the prompt, and finally breaks a rule on purpose to show anisErrorresult.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.
| Needs | Time | Hardware |
|---|---|---|
Python 3.10 or newer and pip install -r labs/06-mcp-server/requirements.txt (the mcp package) in a fresh virtual environment | About 10 minutes to install and run; an hour to add your own tool | Any laptop. No GPU, no API key, and no network once installed |
When this book was written, both scripts ran cleanly against mcp 2.3.0. client.py ended with:
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.
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.
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
- What is the Model Context Protocol (MCP)? docs beginner
Official starting page explaining what MCP is for and its building blocks. - Architecture overview - Model Context Protocol docs beginner
Hosts, clients, servers, the data and transport layers, and the primitives each side exposes. - Specification - Model Context Protocol (2026-07-28) docs intermediate
Front page of the latest spec revision, linking every section below. - Key Changes - Model Context Protocol (2026-07-28) docs intermediate
What changed in the latest revision: the initialize handshake is gone, server/discover added, Roots and Sampling deprecated.
Free, checked on 2026-10-21. All resources for this chapter and 12 more.