Underneath every MCP message is JSON-RPC 2.0. The spec is short — under 10 pages — and worth reading once because every quirk of MCP framing comes from it.
JSON-RPC has three message types. A request has a method, params, and a unique id; the receiver MUST reply with a result or error using the same id. A notification has the same shape but no id; the receiver MUST NOT reply. A response carries either a result field or an error field, never both.
Errors are first-class. Every JSON-RPC error has a numeric code, a short message, and optional data. The standard codes are: -32700 Parse error, -32600 Invalid request, -32601 Method not found, -32602 Invalid params, -32603 Internal error, and the server-defined range -32099 to -32000 for protocol-specific errors. MCP layers its own meanings on top of those last codes.
The piece that catches new MCP server authors is batch and concurrency. JSON-RPC permits batched requests (an array of request objects) and responses out of order on the same connection. MCP transport implementations vary in how strictly they support batches; the safe rule is to write your server such that it can handle interleaved requests and out-of-order responses, even if your current transport happens to serialize them.