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.
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.
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.
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?
