Data Layer
The current MCP revision is stateless: server/discover replaces the handshake, and every request carries its own protocol version and capabilities in _meta. Notifications are opt-in now, and sampling and logging are both deprecated.
The inner of the two layers
MCP is defined in two layers. The transport layer moves bytes, and transport layer covers that half: stdio, streamable HTTP, and what changes when you leave one machine. The data layer is what travels over it.
The specification calls the data layer the inner layer, and it is the half most developers spend their time in. It defines the message format, how a client learns what a server can do, and how the two agree on a protocol version.
Every message is JSON-RPC 2.0
There is no bespoke wire format. A call is a JSON-RPC request with a method, a
params object and an id, and the reply carries a matching id. Messages
that need no reply are sent as JSON-RPC notifications, which carry no id at
all.
The practical benefit is that you can read the traffic. Point the MCP Inspector at a server, or log the raw frames, and the exchange is legible JSON rather than a binary protocol you need tooling to decode. Most MCP debugging starts there.
Every request carries its own context
The current revision makes MCP a stateless protocol, and this is the change
most likely to catch out anyone working from older material. The server infers
nothing from previous requests. Instead, each request carries what the server
needs in a _meta field:
io.modelcontextprotocol/protocolVersion, the version this request speaksio.modelcontextprotocol/clientCapabilities, what the client can handleio.modelcontextprotocol/clientInfo, who is calling
Older revisions established this once, during a handshake, and both sides remembered it for the connection. Now it rides on every message.
That trade buys horizontal scaling. Any instance behind a load balancer can answer a stateless request, which is what a remote server serving many clients needs. It costs a few hundred bytes per call.
Discovery replaces the handshake
A client that wants to know what a server offers sends server/discover. Every
server must implement it, and the response carries the server's supported
versions, its capabilities, its identity, and two caching fields.
Three things are worth pulling out.
Discovery is optional. Because every request already carries the protocol version and capabilities, a client may skip discovery and send whatever it wants first, handling a version error if one comes back. Discovery is a convenience that fetches identity, capabilities and versions in one call.
Version negotiation is an error path. A server that does not speak the
requested version rejects the request with UnsupportedProtocolVersionError
listing what it does support, and the client retries. There is no silent
downgrade, which is the correct design and one worth handling explicitly.
Responses are cacheable. A discovery or tools/list result carries ttlMs,
a freshness hint, and cacheScope, saying who may reuse it. A five-minute TTL
on a tool list means a busy client is not re-listing on every turn.
Notifications are opt-in
A server can tell a client that something changed, but the client has to ask
first. It opens a long-lived stream with subscriptions/listen, naming the
notification types it wants, and the server acknowledges with the subset it will
honour. Anything it does not support is omitted from the acknowledgement rather
than refused.
After that, matching notifications arrive on that stream, each tagged with the subscription ID that produced it.
One caveat sits in the specification and belongs in your code: delivery is best effort. Notifications can be lost across a transport reconnect, so a client that relies on them alone will drift. Keep polling as a backstop, and treat a notification as a prompt to refresh rather than as the source of truth.
Two client features are on the way out
As of protocol version 2026-07-28, sampling and logging are both
deprecated.
Sampling let a server ask the client's application for a model completion, so a
server author could stay model-independent. The guidance now is to integrate
with a provider API directly. Logging let a server send log messages to the
client; the replacement is to write to stderr on stdio, or to use
OpenTelemetry.
Elicitation survives, and is now the client feature to know. It lets a
server ask the user for input or confirm an action, via elicitation/create.
Deprecated is not gone. MCP holds features for at least twelve months before removal, so existing code keeps working. Both are the kind of dependency worth retiring on your own schedule rather than on the protocol's.
Further reading
- MCP, Architecture overview: the two layers, statelessness, discovery, and a worked JSON-RPC exchange.
- Transport layer: the outer layer, and choosing between stdio and streamable HTTP.
- MCP in one sitting: the three primitives and which to reach for.
- Building an MCP server: the same protocol from the implementing side.
Knowledge check
Question 1 of 4
Sign in to save your progress and pick up where you left off.