MCP in Practice

Lesson 6 of 36

The wire: one request, field by field

Everything is JSON-RPC in an envelope you can read

Every MCP interaction is a JSON object you could type by hand. There is no binary framing, no schema registry, no code generation. Learn the envelope once and every feature in the rest of this course is a new value in a slot you already know.

That is worth doing deliberately, because the SDK hides all of it. When something misbehaves — a tool the model never calls, an argument arriving as a string when you expected an integer, a client that reports your server as incompatible — the fix is nearly always visible in the raw message, and unreadable from Python.

The shipping label

A parcel has contents and a label. The contents are what you are actually sending; the label is what every carrier in the chain reads to route it, and none of them open the parcel to do their job.

MCP splits the same way. params holds what you are asking for. _meta is the label — protocol version, client identity, capabilities — read by anything handling the message without needing to understand the request itself. A beginner takes from this that some fields are about the message rather than the request. Anyone who has designed a protocol recognises out-of-band metadata, and immediately asks the right follow-up question: what happens when a field is added that the other end does not know? The answer is that _meta is an open map with namespaced keys, so unknown keys are ignored and forward compatibility is the default rather than a version bump.

One JSON-RPC exchange on the wire, field by field
One JSON-RPC exchange on the wire, field by field

The request

Here is a complete tools/call against the server you will build:

json
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "find_slots",
    "arguments": {"room_id": "studio-b", "day": "2026-09-17", "minutes": 60},
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": {"name": "atrium-client", "version": "1.0.0"},
      "io.modelcontextprotocol/clientCapabilities": {"elicitation": {}}
    }
  }
}

Field by field:

  • jsonrpc is always "2.0". It is JSON-RPC's own version, not MCP's, and it never changes.
  • id correlates request and response. Responses may arrive out of order, so this is how a client matches them up. A message without an id is a notification and expects no reply.
  • method is the operation. tools/call, tools/list, resources/read, server/discover.
  • params.name and params.arguments are specific to tools/call — which tool, and the arguments, which must satisfy the input_schema the server published.
  • params._meta is the label. protocolVersion and clientCapabilities are required on every request; clientInfo should be there unless the client is configured to withhold it.

The namespaced keys (io.modelcontextprotocol/...) look verbose, and they are, deliberately: _meta is open for extensions to use, and namespacing is what stops two extensions colliding on a short name like version.

The response

json
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "resultType": "complete",
    "content": [
      {"type": "text", "text": "{\"room_id\": \"studio-b\", \"start\": \"2026-09-17T09:00\", \"minutes\": 60}"}
    ],
    "structuredContent": {
      "result": [{"room_id": "studio-b", "start": "2026-09-17T09:00", "minutes": 60}]
    }
  }
}

Same id. Then three things worth knowing now:

  • resultType distinguishes a finished answer from other shapes. "complete" means what it says. Two other values matter later: "task", when the server hands back a handle for long-running work (Lesson 25), and the input-required shape, when the server needs to ask the user something (Lesson 22). A client must be prepared for a result that is not the answer.
A result may be complete, may require input from the user, or may hand back a task handle
A result may be complete, may require input from the user, or may hand back a task handle
  • content is a list of blocks, each with a type. Text here, but images and embedded resources are also content types. It is a list because one tool result can legitimately be several things.
  • structured_content is the same answer as data rather than prose. Lesson 16 is about why both exist.

Results can also carry ttlMs and cacheScope — how long the answer stays fresh and who may reuse it. A tool list that is stable for five minutes says so, and clients stop re-fetching it on every turn.

Errors that mean something specific

A failed request returns error instead of result, with a numeric code. Two are MCP-specific and both are worth recognising on sight:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32022,
    "message": "Unsupported protocol version",
    "data": {"supportedVersions": ["2026-07-28"]}
  }
}

-32022 is UnsupportedProtocolVersionError. The data field lists what the server does speak, so the client can retry rather than give up — this is the version negotiation that replaced the handshake.

json
{
  "jsonrpc": "2.0",
  "id": 4,
  "error": {
    "code": -32021,
    "message": "Missing required client capability",
    "data": {"missingCapabilities": ["elicitation"]}
  }
}

-32021 is MissingRequiredClientCapabilityError: the server needed to ask the user a question, and this client never declared it could. You will meet this one for real in Lesson 23.

The rest are standard JSON-RPC. -32700 parse error, -32600 invalid request, -32601 method not found, -32602 invalid params, -32603 internal error. -32602 is the one you will see most, because it is returned both for genuinely malformed arguments and for a request missing a required _meta field — which makes it ambiguous exactly when you are debugging a new client. If you get -32602 and your arguments look right, check _meta before checking anything else.

What to take into the next lesson

Every MCP message is JSON-RPC with params for the request and _meta for the label; results carry resultType plus both a prose and a structured view; and two MCP-specific error codes, -32022 and -32021, tell you about version and capability mismatches rather than about your arguments. Next: the two ways those messages actually travel, and why choosing between them is a trust decision.

← Previous