Data Layer

Intermediate4 min

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.

#protocol

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 speaks
  • io.modelcontextprotocol/clientCapabilities, what the client can handle
  • io.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

Knowledge check

Question 1 of 4

In the current MCP revision, how does a server learn the protocol version?

Sign in to save your progress and pick up where you left off.

Open this article on its own page

Also in Data Layer

1 article