Skip to main content

MCP Protocol Basics - JSON-RPC and Stateless Design

This document explains the MCP wire format and how the stateless base protocol introduced in 2026-07-28 works. The big picture is covered in Introduction to MCP - What Is the Model Context Protocol; here we look at what goes into each message at the JSON-RPC level. For changes between versions and migration, see MCP 2026-07-28 Spec Changes and Migration. For transport-specific topics, see Transports.

1. What This Document Covers​

  • JSON-RPC 2.0 basics
  • Stateless design
  • Information carried in _meta
  • server/discover
  • resultType
  • Capabilities and extensions
  • Error code allocation
  • Versioning and compatibility
  • Removed utilities

JSON-RPC 2.0 Basics​

All MCP messages follow JSON-RPC 2.0 and are UTF-8 encoded. There are three kinds.

KindSenderRequired fieldsNotes
RequestClientjsonrpc, id, method, (optional params)id is a string or integer. Unlike base JSON-RPC, null is not allowed. It must not duplicate the ID of another unanswered request
Response (result)Serverjsonrpc, id, resultresult must contain resultType
Response (error)Serverjsonrpc, id, error (code, message, optional data)code is an integer. id may be omitted only when the request could not be read
NotificationEither directionjsonrpc, method, (optional params)Has no id. The receiver does not respond

In 2026-07-28, the server never sends its own JSON-RPC requests to the client. When the server needs input from the client, it embeds the input request in a result (Multi Round-Trip Requests; see Elicitation and Multi Round-Trip Requests). The client does not send JSON-RPC responses either. Message flow is narrowed to "the server responds to a client request" and "notifications".

Stateless Design​

MCP is now a stateless protocol. All the information needed to process a request is contained in the request itself. The server processes each request independently and must not infer state from previous requests, even on the same connection or stream.

  • The initialize and notifications/initialized handshake is removed. You can send any request right after opening a connection
  • The server must not fill in protocol version, capabilities, or client identity from earlier requests. It reads all of them from the _meta of that request
  • An open connection (for example, a stdio process) is neither a conversation nor a session. Clients may interleave unrelated requests on the same transport. The server must not use process or connection identity as a substitute for conversation continuity
  • State that spans multiple requests (long-running tasks, application-level handles, and so on) is referenced by an explicit identifier the client passes each time

A long-lived request such as subscriptions/listen is still a request and response at the protocol level. The response is just a stream that keeps delivering notifications, and its state is scoped to the request itself, not to the connection.

A "stateless protocol" does not mean a "stateless application". If your application needs state, design it so the server issues a handle and the client passes it as a tool argument. See Introduction to MCP - What Is the Model Context Protocol for details.

Information Carried in _meta​

_meta is the field clients and servers use to attach metadata to messages. A key has the form "optional prefix + / + name". The prefix is a series of dot-separated labels, and reverse DNS notation (com.example/) is recommended. A prefix whose second label is modelcontextprotocol or mcp is reserved for MCP. For example, io.modelcontextprotocol/ and dev.mcp/ are reserved, but com.example.mcp/ is not.

The keys reserved by the specification are as follows.

KeyWhereRequiredContent
io.modelcontextprotocol/protocolVersionRequestRequiredProtocol version of that request (e.g. "2026-07-28")
io.modelcontextprotocol/clientCapabilitiesRequestRequiredClient capabilities relevant to that request
io.modelcontextprotocol/clientInfoRequestOptional (recommended to send)Client name and version
io.modelcontextprotocol/serverInfoResultOptional (recommended to include)Server name and version
io.modelcontextprotocol/logLevelRequestOptionalMinimum log level the server may emit
io.modelcontextprotocol/subscriptionIdNotifications from subscriptions/listenRequiredTies a notification to its originating subscription request
progressTokenRequestOptionalOpts the request into progress notifications
traceparent, tracestate, baggageEither directionOptionalOpenTelemetry trace context. Values follow the W3C Trace Context / Baggage formats. An exception to the prefix rule

A request missing a required field is malformed. The server rejects it with the JSON-RPC error code -32602 (Invalid params), and on HTTP returns 400 Bad Request. The server also must not rely on capabilities the client has not declared. If processing requires such a capability, it returns MissingRequiredClientCapabilityError (-32021) and lists the missing capabilities in data.requiredCapabilities.

clientInfo and serverInfo are self-reported by the sender and are not verified by the protocol. Use them for display, logging, and debugging, not for switching behavior or for security decisions.

server/discover​

server/discover returns the server's supported protocol versions, capabilities, and identity in a single request. The server must implement it, and whether the client calls it is optional. The result carries the cache fields (ttlMs, cacheScope).

Calling it is worthwhile mainly in two situations.

  1. Presenting server information: you want to show what the server supports in one call instead of calling tools/list, prompts/list, and resources/list separately
  2. stdio backward-compatibility probe: on stdio, which has no status codes like HTTP, tell whether the server is old or new (described below)

You do not need to call it every time as connection setup. A client can send any RPC directly and, if the version does not match, receive UnsupportedProtocolVersionError and retry.

resultType​

Every result has a resultType. The client uses this value to decide how to interpret result.

ValueMeaning
completeThe request is complete and result holds the final content
input_requiredMore input is needed to proceed. result is an InputRequiredResult, and inputRequests carries the required input

Clients must treat a result without resultType as complete, because servers on 2025-11-25 or earlier do not return this field. On the other hand, a value the client does not know is treated as invalid. Extensions can add resultType values, but only values from extensions advertised through capabilities are valid. The client's retry procedure on receiving input_required is covered in Elicitation and Multi Round-Trip Requests.

Capabilities and Extensions​

Capabilities are declared with ClientCapabilities and ServerCapabilities. The client sends its own in each request as clientCapabilities in _meta. The server's are included in the server/discover result.

Optional features outside the core protocol are negotiated as extensions. The extensions field in capabilities is a map from extension identifier to that extension's own settings object. Identifiers follow the same rules as _meta keys, and a prefix is mandatory. If no settings are needed, an empty object indicates support.

{
"capabilities": {
"tools": {},
"extensions": {
"io.modelcontextprotocol/tasks": {}
}
}
}

If only one party supports an extension, the supporting party must either revert to core protocol behavior or reject the request with an appropriate error. Extensions are recommended to document their fallback behavior. For the list of extensions and how to use them, see MCP Extensions - Tasks, MCP Apps, and Skills.

Error Code Allocation​

For general protocol failures, MCP uses the standard JSON-RPC codes (-32700, -32600 to -32603). The range -32000 to -32099, which JSON-RPC reserves for implementation-defined errors, is partitioned as follows.

RangeTreatment
-32000 to -32019Codes implementations used in the past (legacy). Allocating new codes is prohibited, and new implementations are recommended not to use them. Apart from -32002, receivers must not assume any specific meaning
-32020 to -32099Reserved for the MCP specification. Only codes defined by the specification are used, with their defined meanings

2026-07-28 defines three codes.

CodeNameUse
-32020HeaderMismatchHTTP headers do not match the body, or required headers are missing
-32021MissingRequiredClientCapabilityA client capability required for processing was not declared
-32022UnsupportedProtocolVersionThe server does not support the requested version

Of the codes from earlier versions, implementations of this version must not emit these two.

  • -32002: resource not found (2025-11-25 and earlier). Replaced by -32602 (Invalid params). Clients are recommended to keep accepting -32002 from servers on earlier versions
  • -32042: URL elicitation required (2025-11-25 only)

If you allocate your own codes for application-specific errors, use values outside the JSON-RPC reserved range (-32768 to -32000).

Versioning and Compatibility​

There is no negotiation handshake. Every request declares its own version, and the server decides per request whether to accept or reject it. On HTTP, the same value is also carried in the MCP-Protocol-Version header.

If the server does not support the requested version (whether unknown or known but not supported), it returns UnsupportedProtocolVersionError and lists the versions it supports. The client picks a common version from the list and retries, or reports an error to the user if there is none.

The specification names the eras of versions as follows.

  • Modern: versions that carry version, identity, and capabilities in per-request _meta (2026-07-28 and later)
  • Legacy: versions that create a session with the initialize handshake (2025-11-25 and earlier)
  • Dual-era: implementations that support both

The outcome for each combination is as follows.

ClientServerOutcome
ModernModernWorks. server/discover is optional
ModernLegacyFails. The server may return its own error, stay silent, or process an era-ambiguous method under legacy semantics. On stdio, sending server/discover first is recommended so that it fails deterministically
Dual-eraModernWorks. The client stays modern
Dual-eraLegacyWorks. If a modern request fails without a recognized modern error, the client switches to initialize
LegacyModernFails. A legacy client has no way to move to the newer version
LegacyDual-eraWorks. The server answers initialize and runs on the negotiated older version

A dual-era server treats a request with _meta as modern and one that starts with initialize as legacy. It can serve both at the same time on the same endpoint or process.

Era determination is a property of the server, not of each request. Clients are recommended to cache the result for the lifetime of the server process (stdio) or origin (HTTP), and to probe again when the assumption fails.

How to tell the era differs by transport.

  • stdio: send server/discover before any other request. If a DiscoverResult comes back, the server is modern. If a recognized modern error such as UnsupportedProtocolVersionError comes back, the server is modern but the version does not match, so retry with a supported version. For any other error or a timeout, treat it as legacy and switch to initialize. The switch must not be tied to one specific error code
  • Streamable HTTP: send a modern request first, and on 400 Bad Request inspect the body. If it contains a recognized modern JSON-RPC error, the server is modern. If the body is empty or anything else, switch to initialize

A server that supports only modern versions is recommended to name its supported versions in the error it returns to initialize, because for a legacy client that message may be the only diagnostic it can show the user.

Removed Utilities​

2026-07-28 removed the following utilities.

RemovedInstead
pingNo successor method is defined
logging/setLevelSpecify per request with io.modelcontextprotocol/logLevel in _meta. The server must not emit notifications/message for a request without this field
notifications/roots/list_changedRoots input is obtained through MRTR when needed

This is separate from the deprecation of Roots / Sampling / Logging. Deprecated features keep working during the migration window, whereas the methods listed here have been removed from the specification. For the list of deprecations, see MCP 2026-07-28 Spec Changes and Migration.

2. Foundation Differences Between 2025-11-25 and 2026-07-28​

AspectUp to 2025-11-252026-07-28
Starting a connectioninitialize / initialized handshakeNo handshake. server/discover is optional
SessionsState kept via Mcp-Session-IdNo protocol session. State is managed with explicit handles
Version and capabilitiesNegotiated once at initializationIncluded in _meta of every request
List resultsMay vary per connectionDo not vary per connection. Return ttlMs and cacheScope
Server-initiated requestsSent directly from server to clientReplaced by Multi Round-Trip Requests

3. Common Pitfalls for Developers​

Implementing the Handshake from Old Material​

Material and SDK samples from 2025 assume you call initialize first. If you send initialize to a 2026-07-28 server, it is rejected as an unknown method. Nor do you need to write code that handles initialize. A modern server processes every request from _meta alone. If you also want to accept legacy clients, implement both as a dual-era server.

Forgetting the Required _meta Fields​

When debugging with a bare HTTP client or curl, leaving protocolVersion and clientCapabilities out of _meta gets the request rejected with -32602 and 400 Bad Request. Even a request that uses no capabilities needs the empty object {}. On HTTP, a mismatch between the body value and the MCP-Protocol-Version header is also rejected with HeaderMismatch (-32020).

Branching Behavior or Authorization on clientInfo​

clientInfo and serverInfo are self-reported and unverified. If you switch authorization or feature exposure by client name, it is bypassed just by spoofing the name. Do authorization with the tokens described in MCP Authorization - OAuth 2.1 and Metadata Discovery.

Reading result Blindly Without Checking resultType​

If you assume the result of tools/call and similar calls is complete, then when input_required comes back it looks like an empty result with no content. Check resultType before reading the result, and go to the MRTR procedure on input_required. Conversely, servers on older versions do not return resultType, so treat a missing value as complete.

The Error Code for Resource Not Found​

The code for a missing resource changed from -32002 to -32602. Clients need to treat both old and new as "not found". Servers must not return -32002.

Making the Connection or Process the Conversation Boundary​

Designs that restart a stdio process per conversation, or that assume "this process belongs to one conversation" and keep context in memory, do not fit the specification. Unrelated requests can be mixed in one process. Carry state with explicit handles.

Relying on ping for Liveness​

ping has been removed. There is no successor method for liveness checks. Design for detecting problems through real request timeouts and stream disconnects. When a stream connection drops, reissue the request with a new request ID (see Transports).

Allocating Your Own Error Codes in the -32000 Range​

New allocations in -32000 to -32019 are prohibited, and -32020 to -32099 is reserved by the specification. Allocate application-specific codes outside -32768 to -32000.

4. Code Examples​

Minimal Request and Response​

A tools/call example. Note the required _meta, and the resultType and serverInfo in the result.

{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": { "location": "Tokyo" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"content": [{ "type": "text", "text": "Sunny, 22 degrees" }],
"_meta": {
"io.modelcontextprotocol/serverInfo": { "name": "ExampleServer", "version": "1.0.0" }
}
}
}

server/discover​

{
"jsonrpc": "2.0",
"id": "discover-1",
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
{
"jsonrpc": "2.0",
"id": "discover-1",
"result": {
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"capabilities": { "tools": {}, "resources": {} },
"_meta": {
"io.modelcontextprotocol/serverInfo": { "name": "ExampleServer", "version": "1.0.0" }
},
"instructions": "This server provides weather and resource utilities.",
"ttlMs": 3600000,
"cacheScope": "public"
}
}

Version Mismatch Error​

The client picks a version it supports from supported and retries. On HTTP this is 400 Bad Request.

{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32022,
"message": "Unsupported protocol version",
"data": {
"supported": ["2026-07-28", "2025-11-25"],
"requested": "1900-01-01"
}
}
}

Missing Capability​

The shape of the error when a required capability is not included in the request (data.requiredCapabilities lists the missing ones).

{
"jsonrpc": "2.0",
"id": 2,
"error": {
"code": -32021,
"message": "Missing required client capability",
"data": { "requiredCapabilities": { "elicitation": {} } }
}
}

For the exact schema of data, check MissingRequiredClientCapabilityError in the Schema Reference.

Reading a Result (TypeScript)​

A client-side skeleton that checks resultType before handling the result.

type Result = { resultType?: string; [key: string]: unknown };

function readResult(result: Result): Result {
// Servers on older versions do not return resultType. Treat a missing value as complete
const type = result.resultType ?? "complete";

switch (type) {
case "complete":
return result;
case "input_required":
// Handle inputRequests, attach inputResponses, and retry the original request
throw new Error("MRTR: input required");
default:
// Treat an unknown value as invalid
throw new Error(`Invalid resultType: ${type}`);
}
}

5. Summary​

  • MCP messages are JSON-RPC 2.0. The server responds to client requests, and the server does not send its own requests
  • 2026-07-28 is stateless. It removes initialize and sessions, and carries protocolVersion and clientCapabilities in the _meta of every request
  • server/discover must be implemented by servers, and clients call it optionally. On stdio it serves as the backward-compatibility probe
  • Every result has a resultType. A missing value means complete, and input_required leads to MRTR
  • For error codes, -32020 to -32099 is reserved for the specification. Resource not found is -32602
  • A version mismatch is detected with UnsupportedProtocolVersionError, and the client picks from supported and retries
  • ping, logging/setLevel, and notifications/roots/list_changed were removed

References​