Vocabulary for this course

The terms the chapters assume, in plain words. You can open this from any chapter.

capability
A named feature one side declares it supports during the handshake, such as tools or resources.
client
The connector a host creates to talk to one server, sending requests and handling replies.
handshake
The opening exchange where both sides agree on a version of the rules and declare what each supports.
host
The application the user runs, which creates a client and decides what a model may reach.
JSON-RPC
A message format of requests, responses and notifications, where each request carries an id so its reply can be matched.
notification
A message that expects no reply, used to report an event or a change.
prompt
A reusable message template a server offers, which a user can choose and fill in.
resource
Read-only data a server exposes, addressed by a name or address and returned as content.
schema
A description of the arguments a tool accepts, used to check a request before it runs.
server
The program that offers tools, resources or prompts and answers requests over a transport.
stdio
A transport that carries messages over the standard input and output of a program, usually on one machine.
tool
A function a server exposes that a model may ask to run, with named arguments and a result.
transport
The channel that carries messages between client and server, such as stdio or HTTP.

Model Context Protocol from the ground up

JSON-RPC messages

Every MCP message is JSON-RPC 2.0. A request has an id, a response carries the same id, and a notification carries none — get those three shapes right and the rest is method names.

Part 2 of 10

Advanced15 min readBack to Courses

Three shapes, one id

A request has a method, some params and an id. A response has the same id and exactly one of result or error. A notification has a method and params and no id at all, because nobody is waiting for an answer. That is the whole grammar, and every feature in the protocol is a set of method names written in it.

The id is the only thing that correlates an answer with a question. It can be a string or a number, it must be unique among the requests still in flight, and it must not be reused until the response arrives. A response whose id matches nothing is an orphan: log it and drop it, rather than guessing which call it belonged to.

A useful habit is to write the frame before the handler. If you cannot say which id the answer will carry, the handler does not know what it is answering. The frame is the unit of testing, and a frame you can write by hand is a frame you can assert on without a server.

Errors have two homes

A protocol error means the exchange itself failed: the JSON did not parse, the method does not exist, the params are malformed, the server broke. It travels in the error object with a numeric code. A tool that rejects its input is not a protocol error. It is a successful exchange whose result says the tool failed, which is a different field and a different meaning.

The distinction matters because the two errors go to different readers. A protocol error is for the client, which may have a bug or a version mismatch. A tool error is for the model, which can read the message and try again with different arguments. Feed a transport failure to the model as content and it will hallucinate a fix; show it a tool error and it has something to act on.

The error object also has a data field for structured detail. Put a machine-readable reason there, not a paragraph. A client that has to parse English to decide whether to retry is a client that will guess, and the guess will be wrong at the worst moment.

The four frames

request = {"jsonrpc": "2.0", "id": 11, "method": "tools/call",
           "params": {"name": "add", "arguments": {"a": 2, "b": 3}}}

result = {"jsonrpc": "2.0", "id": 11,
          "result": {"content": [{"type": "text", "text": "5"}]}}

error = {"jsonrpc": "2.0", "id": 12,
         "error": {"code": -32601, "message": "Method not found"}}

notice = {"jsonrpc": "2.0", "method": "notifications/progress",
          "params": {"progressToken": "p1", "progress": 1, "total": 3}}

Encode the four frames

Run the encoder, then add a response whose id matches nothing. Decide whether the client should log it, retry or crash, and make the code do that.

Output
Click "Run" to execute your code

Results are lists of content

A tool result is not a string. It is a list of content blocks, each with a type: text, image, audio, a link to a resource, or a resource embedded in the result. That shape lets one call return a sentence and a chart without inventing a second protocol, and it lets a client render what it understands and skip what it does not.

A newer revision adds structuredContent alongside the blocks, so a tool can return JSON that a program consumes without parsing prose. Use it when the caller is code; keep a text block too, because the model reads the blocks and many hosts still show only those.

A result can also carry annotations such as audience and priority, which tell a host whether a block is meant for the user or for the model. They are hints, like tool annotations, and a host that ignores them is still correct; it just renders more than it needs to.

Log every frame with its direction and its id, before any parsing. When a call hangs, the raw log answers the first question — was the response sent, and did its id match?

Notifications carry the push

The initialized notice, a cancellation, a progress update, a list-changed hint and a log message are all notifications: one-way, no reply, no id. They exist because some events are not answers to anything. A server that has rebuilt its tool list tells the client; a client that has given up on a request tells the server.

Progress is the one worth studying. The sender picks an opaque token and puts it in the request params; the receiver echoes it in notifications/progress with a current value and an optional total. The token means nothing on its own, which is exactly why it can be any string and why the two ends must agree on it before the work starts.

A notification that arrives before the handshake is a protocol violation, not a race to tolerate. The initialized notice exists so both ends agree on when the session is live. A server that starts pushing before it is live is a server that will push into a client that is not ready.

A router that matches ids

Send two calls, then deliver the responses out of order with a notification in between. Confirm each response reaches the call it belongs to.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What distinguishes a notification from a request?