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

Capabilities and negotiation

Capabilities are a promise about methods, not a score. Each side declares what it can answer, and the other side must not call what was never declared, however clearly the specification describes it.

Part 7 of 10

Advanced15 min readBack to Courses

The handshake is a contract

The client opens with initialize. It sends the protocol revision it wants, the capabilities it supports and a name for itself. The server answers with the revision it will use, its own capabilities and its name. If the server supports the requested revision it echoes it; if not, it answers with the newest revision it does support, and the client decides whether that is acceptable.

That last decision is the part implementations skip. A client that cannot speak the returned revision must close the connection, not carry on and hope. The revision is the grammar; the capabilities are the vocabulary. Guessing at either produces a failure that looks like a server bug and is really a missing check in the client.

The instructions field is optional and easy to ignore. It is the server’s chance to say how it expects to be used, and a host that shows it to the model once, clearly labelled, gets better tool choices for free. It is documentation addressed to the model.

Capabilities are promises about methods

A server that declares tools is saying it will answer tools/list and tools/call. Declaring resources adds resources/list and resources/read, with subscribe and listChanged as sub-flags. Prompts, logging and completions work the same way. The client capabilities are the mirror image: roots, sampling and elicitation are things a server may ask the client to do.

The rule is one-directional and easy to state: do not call a method the other side did not declare. A method in the specification is a method that exists in the protocol, not a method that exists in this peer. The check is cheap, and its absence is the most common cause of a client that works against one server and fails against another.

Capabilities are also a review surface. A server that declares sampling and elicitation is asking for two powers that are easy to grant and hard to see, and a client should show that list to the person installing it. Consent needs a sentence, not a checkbox.

What the two ends declare

initialize = {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
        "protocolVersion": "2025-06-18",
        "capabilities": {
            "roots": {"listChanged": True},
            "sampling": {},
            "elicitation": {},
        },
        "clientInfo": {"name": "demo-host", "version": "1.0"},
    },
}

result = {
    "protocolVersion": "2025-06-18",
    "capabilities": {
        "tools": {"listChanged": True},
        "resources": {"subscribe": True, "listChanged": False},
        "prompts": {},
    },
    "serverInfo": {"name": "notes", "version": "0.1"},
    "instructions": "Read notes before writing them.",
}

Agree on a revision

Ask for a supported revision and an unsupported one. Then make an unsupported request fail the handshake instead of silently falling back, and print the reason.

Output
Click "Run" to execute your code

Things change after the handshake

A server that declared listChanged may send notifications/tools/list_changed when its tool list changes. The client then re-lists, because its cache is stale. A server that declared subscribe may accept resources/subscribe and send an update when the resource changes. These notices are the reason the capability has sub-flags: they are promises about pushes, not just about methods.

A client should degrade rather than fail. If the server has no resources, use the tool that returns the same data; if it has no prompts, hide the command menu; if it has no logging, do not wait for a log line. The absence of a capability is information, and the useful response is a different path, not an exception.

A list_changed notice is not a new list. It is a hint to re-list, and the client should debounce it: a server rebuilding a large index may send several in a row, and each one is a round trip. The notice is cheap; the refresh is not.

Check the capability, then call the method. The check is two lines, and it is the difference between a client that travels and one that only works against the server it was written for.

Versioning without a major number

Each revision is a date. Changes are meant to be additive: a new method, a new optional field, a new content type. A peer that meets an unknown field should ignore it rather than reject the frame, which is what lets a newer server talk to an older client without a flag day. The _meta field is the designated place for extensions that are not part of the core.

What a client must not do is branch on serverInfo. The name and version of a server are for logs and for people; they are not a feature flag. If a behaviour matters, it belongs in a capability or in the negotiated revision, where both ends can see it and neither has to parse a vendor string.

When you add a method, add a capability for it. A method without a capability is a method nobody can detect, and the only way to use it is to try and catch the failure, which is not a contract. Capabilities are how a protocol stays honest about what exists.

Degrade when a capability is absent

Run the three plans. Then add a check that refuses tools/call when the server never declared tools, and show the plan for a server with no capabilities at all.

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 does the server return if it does not support the requested revision?